diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html
index ae13f1a43..5b59aa642 100644
--- a/docs/generated/changelog.html
+++ b/docs/generated/changelog.html
@@ -11,6 +11,10 @@
Agent-JS Changelog
Version x.x.x
+
+ - feat: retry query signature verification in case cache is stale
+
+ Version 0.20.0
- feat: uses expirable map for subnet keys in agent-js, with a timeout of 1 hour
- chore: cleanup for node 20 development in agent-js
diff --git a/packages/agent/src/agent/http/http.test.ts b/packages/agent/src/agent/http/http.test.ts
index 0af2b0695..b83fad136 100644
--- a/packages/agent/src/agent/http/http.test.ts
+++ b/packages/agent/src/agent/http/http.test.ts
@@ -790,3 +790,5 @@ test('retry requests that fail due to a network failure', async () => {
expect(mockFetch.mock.calls.length).toBe(4);
}
});
+
+test.todo('retry query signature validation after refreshing the subnet node keys');
diff --git a/packages/agent/src/agent/http/index.ts b/packages/agent/src/agent/http/index.ts
index db0dc78b3..8c942a403 100644
--- a/packages/agent/src/agent/http/index.ts
+++ b/packages/agent/src/agent/http/index.ts
@@ -188,7 +188,7 @@ export class HttpAgent implements Agent {
#updatePipeline: HttpAgentRequestTransformFn[] = [];
#subnetKeys: ExpirableMap = new ExpirableMap({
- expirationTime: 60 * 60 * 1000, // 1 hour
+ expirationTime: 5 * 60 * 1000, // 5 minutes
});
#verifyQuerySignatures = true;
@@ -532,7 +532,22 @@ export class HttpAgent implements Agent {
if (!this.#verifyQuerySignatures) {
return query;
}
- return this.#verifyQueryResponse(query, subnetStatus);
+ try {
+ return this.#verifyQueryResponse(query, subnetStatus);
+ } catch (_) {
+ // In case the node signatures have changed, refresh the subnet keys and try again
+ console.warn('Query response verification failed. Retrying with fresh subnet keys.');
+ this.#subnetKeys.delete(canisterId.toString());
+ await this.fetchSubnetKeys(canisterId.toString());
+
+ const updatedSubnetStatus = this.#subnetKeys.get(canisterId.toString());
+ if (!updatedSubnetStatus) {
+ throw new CertificateVerificationError(
+ 'Invalid signature from replica signed query: no matching node key found.',
+ );
+ }
+ return this.#verifyQueryResponse(query, updatedSubnetStatus);
+ }
}
/**
@@ -554,10 +569,10 @@ export class HttpAgent implements Agent {
'Invalid signature from replica signed query: no matching node key found.',
);
}
- const { status, signatures, requestId } = queryResponse;
+ const { status, signatures = [], requestId } = queryResponse;
const domainSeparator = new TextEncoder().encode('\x0Bic-response');
- signatures?.forEach(sig => {
+ for (const sig of signatures) {
const { timestamp, identity } = sig;
const nodeId = Principal.fromUint8Array(identity).toText();
let hash: ArrayBuffer;
@@ -605,7 +620,7 @@ export class HttpAgent implements Agent {
throw new CertificateVerificationError(
`Invalid signature from replica ${nodeId} signed query.`,
);
- });
+ }
return queryResponse;
};