Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- feat(agent): lookup canister ranges using the `/canister_ranges/<subnet_id>/<ranges>` certificate path
- feat(agent): introduce the `lookupCanisterRanges`, `lookupCanisterRangesFallback`, and `decodeCanisterRanges` utility functions to lookup canister ranges from certificate trees
- feat(agent): introduce the `getSubnetIdFromCanister` and `readSubnetState` methods in the `HttpAgent` class
- feat(agent): introduce the `syncTimeWithSubnet` method in the `HttpAgent` class to sync the time with a particular subnet
- feat(agent): introduce the `SubnetStatus` utility namespace to request subnet information directly from the IC public API
- feat(agent): export `IC_STATE_ROOT_DOMAIN_SEPARATOR` constant
- refactor(agent): only declare IC URLs once in the `HttpAgent` class
Expand Down
79 changes: 66 additions & 13 deletions packages/core/src/agent/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
type HttpHeaderField,
} from './types.ts';
import { type SubnetStatus, request as canisterStatusRequest } from '../../canisterStatus/index.ts';
import { request as subnetStatusRequest } from '../../subnetStatus/index.ts';
import { Certificate, type HashTree, lookup_path, LookupPathStatus } from '../../certificate.ts';
import { ed25519 } from '@noble/curves/ed25519';
import { ExpirableMap } from '../../utils/expirableMap.ts';
Expand Down Expand Up @@ -1221,7 +1222,7 @@ export class HttpAgent implements Agent {
}

/**
* Allows agent to sync its time with the network. Can be called during intialization or mid-lifecycle if the device's clock has drifted away from the network time. This is necessary to set the Expiry for a request
* Allows agent to sync its time with the network. Can be called during initialization or mid-lifecycle if the device's clock has drifted away from the network time. This is necessary to set the Expiry for a request
* @param {Principal} canisterIdOverride - Pass a canister ID if you need to sync the time with a particular subnet. Uses the ICP ledger canister by default.
*/
public async syncTime(canisterIdOverride?: Principal): Promise<void> {
Expand Down Expand Up @@ -1266,18 +1267,7 @@ export class HttpAgent implements Agent {
}, []),
);

const maxReplicaTime = replicaTimes.reduce<number>((max, current) => {
return typeof current === 'number' && current > max ? current : max;
}, 0);

if (maxReplicaTime > 0) {
this.#timeDiffMsecs = maxReplicaTime - callTime;
this.#hasSyncedTime = true;
this.log.notify({
message: `Syncing time: offset of ${this.#timeDiffMsecs}`,
level: 'info',
});
}
this.#setTimeDiffMsecs(callTime, replicaTimes);
} catch (error) {
const syncTimeError =
error instanceof AgentError
Expand All @@ -1294,6 +1284,69 @@ export class HttpAgent implements Agent {
});
}

/**
* Allows agent to sync its time with the network.
* @param {Principal} subnetId - Pass the subnet ID you need to sync the time with.
*/
public async syncTimeWithSubnet(subnetId: Principal): Promise<void> {
await this.#rootKeyGuard();
const callTime = Date.now();

try {
const anonymousAgent = HttpAgent.createSync({
identity: new AnonymousIdentity(),
host: this.host.toString(),
fetch: this.#fetch,
retryTimes: 0,
rootKey: this.rootKey ?? undefined,
shouldSyncTime: false,
});

const replicaTimes = await Promise.all(
Array(3)
.fill(null)
.map(async () => {
const status = await subnetStatusRequest({
subnetId,
agent: anonymousAgent,
paths: ['time'],
disableCertificateTimeVerification: true, // avoid recursive calls to syncTime
});

const date = status.get('time');
if (date instanceof Date) {
return date.getTime();
}
}, []),
);

this.#setTimeDiffMsecs(callTime, replicaTimes);
} catch (error) {
const syncTimeError =
error instanceof AgentError
? error
: UnknownError.fromCode(new UnexpectedErrorCode(error));
this.log.error('Caught exception while attempting to sync time with subnet', syncTimeError);

throw syncTimeError;
}
}

#setTimeDiffMsecs(callTime: number, replicaTimes: Array<number | undefined>): void {
const maxReplicaTime = replicaTimes.reduce<number>((max, current) => {
return typeof current === 'number' && current > max ? current : max;
}, 0);

if (maxReplicaTime > 0) {
this.#timeDiffMsecs = maxReplicaTime - callTime;
this.#hasSyncedTime = true;
this.log.notify({
message: `Syncing time: offset of ${this.#timeDiffMsecs}`,
level: 'info',
});
}
}

public async status(): Promise<JsonObject> {
const headers: Record<string, string> = this.#credentials
? {
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/agent/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export interface CreateCertificateOptions {

/**
* The agent used to sync time with the IC network, if the certificate fails the freshness check.
* If the agent does not implement the {@link HttpAgent.getTimeDiffMsecs}, {@link HttpAgent.hasSyncedTime} and {@link HttpAgent.syncTime} methods,
* If the agent does not implement the {@link HttpAgent.getTimeDiffMsecs}, {@link HttpAgent.hasSyncedTime}, {@link HttpAgent.syncTime} and {@link HttpAgent.syncTimeWithSubnet} methods,
* time will not be synced in case of a freshness check failure.
* @default undefined
*/
Expand All @@ -214,7 +214,7 @@ export interface CreateCertificateOptions {
export class Certificate {
public cert: Cert;
#disableTimeVerification: boolean = false;
#agent: Pick<HttpAgent, 'getTimeDiffMsecs' | 'hasSyncedTime' | 'syncTime'> | undefined =
#agent: Pick<HttpAgent, 'getTimeDiffMsecs' | 'hasSyncedTime' | 'syncTime' | 'syncTimeWithSubnet'> | undefined =
undefined;

/**
Expand Down Expand Up @@ -253,8 +253,8 @@ export class Certificate {
this.#disableTimeVerification = disableTimeVerification;
this.cert = cbor.decode(certificate);

if (agent && 'getTimeDiffMsecs' in agent && 'hasSyncedTime' in agent && 'syncTime' in agent) {
this.#agent = agent as Pick<HttpAgent, 'getTimeDiffMsecs' | 'hasSyncedTime' | 'syncTime'>;
if (agent && 'getTimeDiffMsecs' in agent && 'hasSyncedTime' in agent && 'syncTime' in agent && 'syncTimeWithSubnet' in agent) {
this.#agent = agent as Pick<HttpAgent, 'getTimeDiffMsecs' | 'hasSyncedTime' | 'syncTime' | 'syncTimeWithSubnet'>;
}
}

Expand Down Expand Up @@ -412,8 +412,7 @@ export class Certificate {
if (isCanisterPrincipal(this._principal)) {
await this.#agent.syncTime(this._principal.canisterId);
} else {
// TODO: sync time with subnet once the agent supports it
await this.#agent.syncTime();
await this.#agent.syncTimeWithSubnet(this._principal.subnetId);
}
}
}
Expand Down
Loading