From 113f352ce06ba8b55fd382a728885cafaa369457 Mon Sep 17 00:00:00 2001 From: Kai Peacock Date: Tue, 10 Sep 2024 16:56:22 -0700 Subject: [PATCH] all certificates now read from `agent.replicaTime` --- e2e/node/basic/mainnet.test.ts | 80 +++++++++++++++++++++- packages/agent/src/actor.ts | 9 ++- packages/agent/src/agent/http/http.test.ts | 74 -------------------- packages/agent/src/agent/http/index.ts | 1 + packages/agent/src/canisterStatus/index.ts | 2 + packages/assets/src/index.ts | 6 ++ 6 files changed, 95 insertions(+), 77 deletions(-) diff --git a/e2e/node/basic/mainnet.test.ts b/e2e/node/basic/mainnet.test.ts index b3a530ad..bc1ee723 100644 --- a/e2e/node/basic/mainnet.test.ts +++ b/e2e/node/basic/mainnet.test.ts @@ -7,6 +7,7 @@ import { fromHex, polling, requestIdOf, + ReplicaTimeError, } from '@dfinity/agent'; import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; @@ -21,7 +22,7 @@ const createWhoamiActor = async (identity: Identity) => { const idlFactory = () => { return IDL.Service({ whoami: IDL.Func([], [IDL.Principal], ['query']), - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as unknown as any; }; vi.useFakeTimers(); @@ -142,7 +143,6 @@ describe('call forwarding', () => { }, 15_000); }); - test('it should allow you to set an incorrect root key', async () => { const agent = HttpAgent.createSync({ rootKey: new Uint8Array(31), @@ -159,3 +159,79 @@ test('it should allow you to set an incorrect root key', async () => { expect(actor.whoami).rejects.toThrowError(`Invalid certificate:`); }); + +test('it should throw an error when the clock is out of sync during a query', async () => { + const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai'; + const idlFactory = () => { + return IDL.Service({ + whoami: IDL.Func([], [IDL.Principal], ['query']), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as unknown as any; + }; + vi.useRealTimers(); + + // set date to long ago + vi.spyOn(Date, 'now').mockImplementation(() => { + return new Date('2021-01-01T00:00:00Z').getTime(); + }); + // vi.setSystemTime(new Date('2021-01-01T00:00:00Z')); + + const agent = await HttpAgent.create({ host: 'https://icp-api.io', fetch: globalThis.fetch }); + + const actor = Actor.createActor(idlFactory, { + agent, + canisterId, + }); + try { + // should throw an error + await actor.whoami(); + } catch (err) { + // handle the replica time error + if (err.name === 'ReplicaTimeError') { + const error = err as ReplicaTimeError; + // use the replica time to sync the agent + error.agent.replicaTime = error.replicaTime; + } + } + // retry the call + const result = await actor.whoami(); + expect(Principal.from(result)).toBeInstanceOf(Principal); +}); + +test('it should throw an error when the clock is out of sync during an update', async () => { + const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai'; + const idlFactory = () => { + return IDL.Service({ + whoami: IDL.Func([], [IDL.Principal], []), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as unknown as any; + }; + vi.useRealTimers(); + + // set date to long ago + vi.spyOn(Date, 'now').mockImplementation(() => { + return new Date('2021-01-01T00:00:00Z').getTime(); + }); + // vi.setSystemTime(new Date('2021-01-01T00:00:00Z')); + + const agent = await HttpAgent.create({ host: 'https://icp-api.io', fetch: globalThis.fetch }); + + const actor = Actor.createActor(idlFactory, { + agent, + canisterId, + }); + try { + // should throw an error + await actor.whoami(); + } catch (err) { + // handle the replica time error + if (err.name === 'ReplicaTimeError') { + const error = err as ReplicaTimeError; + // use the replica time to sync the agent + error.agent.replicaTime = error.replicaTime; + // retry the call + const result = await actor.whoami(); + expect(Principal.from(result)).toBeInstanceOf(Principal); + } + } +}); diff --git a/packages/agent/src/actor.ts b/packages/agent/src/actor.ts index f347f6fa..f447da3e 100644 --- a/packages/agent/src/actor.ts +++ b/packages/agent/src/actor.ts @@ -2,6 +2,7 @@ import { Buffer } from 'buffer/'; import { Agent, getDefaultAgent, + HttpAgent, HttpDetailsResponse, QueryResponseRejected, QueryResponseStatus, @@ -535,13 +536,19 @@ function _createActorMethod( }); let reply: ArrayBuffer | undefined; let certificate: Certificate | undefined; + const certTime = (agent as HttpAgent).replicaTime + ? (agent as HttpAgent).replicaTime + : undefined; + + certTime; + if (response.body && response.body.certificate) { const cert = response.body.certificate; certificate = await Certificate.create({ certificate: bufFromBufLike(cert), rootKey: agent.rootKey, canisterId: Principal.from(canisterId), - blsVerify, + certTime, }); const path = [new TextEncoder().encode('request_status'), requestId]; const status = new TextDecoder().decode( diff --git a/packages/agent/src/agent/http/http.test.ts b/packages/agent/src/agent/http/http.test.ts index 2a5089ef..0cf25bc9 100644 --- a/packages/agent/src/agent/http/http.test.ts +++ b/packages/agent/src/agent/http/http.test.ts @@ -815,77 +815,3 @@ test('it should log errors to console if the option is set', async () => { }); jest.setTimeout(5000); -test('it should sync time with the replica for a query', async () => { - const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai'; - const idlFactory = () => { - return IDL.Service({ - whoami: IDL.Func([], [IDL.Principal], ['query']), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as unknown as any; - }; - jest.useRealTimers(); - - // set date to long ago - jest.spyOn(Date, 'now').mockImplementation(() => { - return new Date('2021-01-01T00:00:00Z').getTime(); - }); - // jest.setSystemTime(new Date('2021-01-01T00:00:00Z')); - - const agent = await HttpAgent.create({ host: 'https://icp-api.io', fetch: globalThis.fetch }); - - const actor = Actor.createActor(idlFactory, { - agent, - canisterId, - }); - try { - // should throw an error - await actor.whoami(); - } catch (err) { - // handle the replica time error - if (err.name === 'ReplicaTimeError') { - const error = err as ReplicaTimeError; - // use the replica time to sync the agent - error.agent.replicaTime = error.replicaTime; - } - } - // retry the call - const result = await actor.whoami(); - expect(Principal.from(result)).toBeInstanceOf(Principal); -}); -test('it should sync time with the replica for an update', async () => { - const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai'; - const idlFactory = () => { - return IDL.Service({ - whoami: IDL.Func([], [IDL.Principal], []), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as unknown as any; - }; - jest.useRealTimers(); - - // set date to long ago - jest.spyOn(Date, 'now').mockImplementation(() => { - return new Date('2021-01-01T00:00:00Z').getTime(); - }); - // jest.setSystemTime(new Date('2021-01-01T00:00:00Z')); - - const agent = await HttpAgent.create({ host: 'https://icp-api.io', fetch: globalThis.fetch }); - - const actor = Actor.createActor(idlFactory, { - agent, - canisterId, - }); - try { - // should throw an error - await actor.whoami(); - } catch (err) { - // handle the replica time error - if (err.name === 'ReplicaTimeError') { - const error = err as ReplicaTimeError; - // use the replica time to sync the agent - error.agent.replicaTime = error.replicaTime; - // retry the call - const result = await actor.whoami(); - expect(Principal.from(result)).toBeInstanceOf(Principal); - } - } -}); diff --git a/packages/agent/src/agent/http/index.ts b/packages/agent/src/agent/http/index.ts index aa720304..07c0b956 100644 --- a/packages/agent/src/agent/http/index.ts +++ b/packages/agent/src/agent/http/index.ts @@ -1100,6 +1100,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 * @param {Principal} canisterId - Pass a canister ID if you need to sync the time with a particular replica. Uses the management canister by default + * @throws {ReplicaTimeError} - this method is not guaranteed to work if the device's clock is off by more than 30 seconds. In such cases, the agent will throw an error. */ public async syncTime(canisterId?: Principal): Promise { const CanisterStatus = await import('../../canisterStatus'); diff --git a/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts index c46ba2ca..a0f9b5fe 100644 --- a/packages/agent/src/canisterStatus/index.ts +++ b/packages/agent/src/canisterStatus/index.ts @@ -145,10 +145,12 @@ export const request = async (options: { const response = await agent.readState(canisterId, { paths: [encodedPaths[index]], }); + const certTime = agent.replicaTime ? agent.replicaTime : undefined; const cert = await Certificate.create({ certificate: response.certificate, rootKey: agent.rootKey, canisterId: canisterId, + certTime, }); const lookup = (cert: Certificate, path: Path) => { diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts index db2187bf..73e6fd27 100644 --- a/packages/assets/src/index.ts +++ b/packages/assets/src/index.ts @@ -7,6 +7,7 @@ import { compare, getDefaultAgent, HashTree, + HttpAgent, lookup_path, lookupResultToBuffer, LookupStatus, @@ -530,10 +531,15 @@ class Asset { return false; } + const replicaTime = (agent as HttpAgent).replicaTime + ? (agent as HttpAgent).replicaTime + : undefined; + const cert = await Certificate.create({ certificate: new Uint8Array(certificate), rootKey: agent.rootKey, canisterId, + certTime: replicaTime, }).catch(() => Promise.resolve()); if (!cert) {