Skip to content

Commit b4d1f65

Browse files
authored
feat(agent): syncTimeWithSubnet method for HttpAgent (#1240)
# Description Introduces the `syncTimeWithSubnet` method in the `HttpAgent` class to sync the time with a particular subnet. Additionally, makes the `Certificate` sync time with subnet in case a subnet is used as principal.
1 parent 3577429 commit b4d1f65

File tree

3 files changed

+72
-19
lines changed

3 files changed

+72
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- feat(agent): lookup canister ranges using the `/canister_ranges/<subnet_id>/<ranges>` certificate path
1616
- feat(agent): introduce the `lookupCanisterRanges`, `lookupCanisterRangesFallback`, and `decodeCanisterRanges` utility functions to lookup canister ranges from certificate trees
1717
- feat(agent): introduce the `getSubnetIdFromCanister` and `readSubnetState` methods in the `HttpAgent` class
18+
- feat(agent): introduce the `syncTimeWithSubnet` method in the `HttpAgent` class to sync the time with a particular subnet
1819
- feat(agent): introduce the `SubnetStatus` utility namespace to request subnet information directly from the IC public API
1920
- feat(agent): export `IC_STATE_ROOT_DOMAIN_SEPARATOR` constant
2021
- refactor(agent): only declare IC URLs once in the `HttpAgent` class

packages/core/src/agent/agent/http/index.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
type HttpHeaderField,
5757
} from './types.ts';
5858
import { type SubnetStatus, request as canisterStatusRequest } from '../../canisterStatus/index.ts';
59+
import { request as subnetStatusRequest } from '../../subnetStatus/index.ts';
5960
import { Certificate, type HashTree, lookup_path, LookupPathStatus } from '../../certificate.ts';
6061
import { ed25519 } from '@noble/curves/ed25519';
6162
import { ExpirableMap } from '../../utils/expirableMap.ts';
@@ -1221,7 +1222,7 @@ export class HttpAgent implements Agent {
12211222
}
12221223

12231224
/**
1224-
* 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
1225+
* 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
12251226
* @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.
12261227
*/
12271228
public async syncTime(canisterIdOverride?: Principal): Promise<void> {
@@ -1266,18 +1267,7 @@ export class HttpAgent implements Agent {
12661267
}, []),
12671268
);
12681269

1269-
const maxReplicaTime = replicaTimes.reduce<number>((max, current) => {
1270-
return typeof current === 'number' && current > max ? current : max;
1271-
}, 0);
1272-
1273-
if (maxReplicaTime > 0) {
1274-
this.#timeDiffMsecs = maxReplicaTime - callTime;
1275-
this.#hasSyncedTime = true;
1276-
this.log.notify({
1277-
message: `Syncing time: offset of ${this.#timeDiffMsecs}`,
1278-
level: 'info',
1279-
});
1280-
}
1270+
this.#setTimeDiffMsecs(callTime, replicaTimes);
12811271
} catch (error) {
12821272
const syncTimeError =
12831273
error instanceof AgentError
@@ -1294,6 +1284,69 @@ export class HttpAgent implements Agent {
12941284
});
12951285
}
12961286

1287+
/**
1288+
* Allows agent to sync its time with the network.
1289+
* @param {Principal} subnetId - Pass the subnet ID you need to sync the time with.
1290+
*/
1291+
public async syncTimeWithSubnet(subnetId: Principal): Promise<void> {
1292+
await this.#rootKeyGuard();
1293+
const callTime = Date.now();
1294+
1295+
try {
1296+
const anonymousAgent = HttpAgent.createSync({
1297+
identity: new AnonymousIdentity(),
1298+
host: this.host.toString(),
1299+
fetch: this.#fetch,
1300+
retryTimes: 0,
1301+
rootKey: this.rootKey ?? undefined,
1302+
shouldSyncTime: false,
1303+
});
1304+
1305+
const replicaTimes = await Promise.all(
1306+
Array(3)
1307+
.fill(null)
1308+
.map(async () => {
1309+
const status = await subnetStatusRequest({
1310+
subnetId,
1311+
agent: anonymousAgent,
1312+
paths: ['time'],
1313+
disableCertificateTimeVerification: true, // avoid recursive calls to syncTime
1314+
});
1315+
1316+
const date = status.get('time');
1317+
if (date instanceof Date) {
1318+
return date.getTime();
1319+
}
1320+
}, []),
1321+
);
1322+
1323+
this.#setTimeDiffMsecs(callTime, replicaTimes);
1324+
} catch (error) {
1325+
const syncTimeError =
1326+
error instanceof AgentError
1327+
? error
1328+
: UnknownError.fromCode(new UnexpectedErrorCode(error));
1329+
this.log.error('Caught exception while attempting to sync time with subnet', syncTimeError);
1330+
1331+
throw syncTimeError;
1332+
}
1333+
}
1334+
1335+
#setTimeDiffMsecs(callTime: number, replicaTimes: Array<number | undefined>): void {
1336+
const maxReplicaTime = replicaTimes.reduce<number>((max, current) => {
1337+
return typeof current === 'number' && current > max ? current : max;
1338+
}, 0);
1339+
1340+
if (maxReplicaTime > 0) {
1341+
this.#timeDiffMsecs = maxReplicaTime - callTime;
1342+
this.#hasSyncedTime = true;
1343+
this.log.notify({
1344+
message: `Syncing time: offset of ${this.#timeDiffMsecs}`,
1345+
level: 'info',
1346+
});
1347+
}
1348+
}
1349+
12971350
public async status(): Promise<JsonObject> {
12981351
const headers: Record<string, string> = this.#credentials
12991352
? {

packages/core/src/agent/certificate.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export interface CreateCertificateOptions {
204204

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

220220
/**
@@ -253,8 +253,8 @@ export class Certificate {
253253
this.#disableTimeVerification = disableTimeVerification;
254254
this.cert = cbor.decode(certificate);
255255

256-
if (agent && 'getTimeDiffMsecs' in agent && 'hasSyncedTime' in agent && 'syncTime' in agent) {
257-
this.#agent = agent as Pick<HttpAgent, 'getTimeDiffMsecs' | 'hasSyncedTime' | 'syncTime'>;
256+
if (agent && 'getTimeDiffMsecs' in agent && 'hasSyncedTime' in agent && 'syncTime' in agent && 'syncTimeWithSubnet' in agent) {
257+
this.#agent = agent as Pick<HttpAgent, 'getTimeDiffMsecs' | 'hasSyncedTime' | 'syncTime' | 'syncTimeWithSubnet'>;
258258
}
259259
}
260260

@@ -412,8 +412,7 @@ export class Certificate {
412412
if (isCanisterPrincipal(this._principal)) {
413413
await this.#agent.syncTime(this._principal.canisterId);
414414
} else {
415-
// TODO: sync time with subnet once the agent supports it
416-
await this.#agent.syncTime();
415+
await this.#agent.syncTimeWithSubnet(this._principal.subnetId);
417416
}
418417
}
419418
}

0 commit comments

Comments
 (0)