Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

- fix: do not subtract the replica permitted clock drift when calculating the ingress expiry.
- fix: round the ingress expiry before applying any eventual clock drift.
- feat: add `Expiry.addMilliseconds` method to add a number of milliseconds to an expiry.

## [3.0.2] - 2025-07-23

- fix: canonicalizes record and variant labels during subtype checking
Expand Down
28 changes: 14 additions & 14 deletions e2e/node/basic/__snapshots__/syncTime.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`syncTime > on async creation > should sync time on when enabled > V3 read state body one 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand All @@ -26,7 +26,7 @@ exports[`syncTime > on async creation > should sync time on when enabled > V3 re
exports[`syncTime > on async creation > should sync time on when enabled > V3 read state body three 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand All @@ -49,7 +49,7 @@ exports[`syncTime > on async creation > should sync time on when enabled > V3 re
exports[`syncTime > on async creation > should sync time on when enabled > V3 read state body two 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand Down Expand Up @@ -105,7 +105,7 @@ exports[`syncTime > on error > should not sync time by default > V3 call body 1`
],
"type": "Buffer",
},
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"method_name": "greet",
"nonce": Any<Uint8Array>,
"request_type": "call",
Expand All @@ -119,7 +119,7 @@ exports[`syncTime > on error > should not sync time by default > V3 call body 1`
exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V2 read state body one 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand Down Expand Up @@ -175,7 +175,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat
],
"type": "Buffer",
},
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"method_name": "greet",
"nonce": Any<Uint8Array>,
"request_type": "call",
Expand All @@ -189,7 +189,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat
exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V3 read state body three 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand All @@ -212,7 +212,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat
exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V3 read state body two 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand Down Expand Up @@ -268,7 +268,7 @@ exports[`syncTime > on first call > should not sync time by default > V3 call bo
],
"type": "Buffer",
},
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"method_name": "greet",
"nonce": Any<Uint8Array>,
"request_type": "call",
Expand Down Expand Up @@ -315,7 +315,7 @@ exports[`syncTime > on first call > should not sync time when explicitly disable
],
"type": "Buffer",
},
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"method_name": "greet",
"nonce": Any<Uint8Array>,
"request_type": "call",
Expand Down Expand Up @@ -362,7 +362,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 call body
],
"type": "Buffer",
},
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"method_name": "greet",
"nonce": Any<Uint8Array>,
"request_type": "call",
Expand All @@ -376,7 +376,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 call body
exports[`syncTime > on first call > should sync time when enabled > V3 read state body one 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand All @@ -399,7 +399,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 read stat
exports[`syncTime > on first call > should sync time when enabled > V3 read state body three 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand All @@ -422,7 +422,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 read stat
exports[`syncTime > on first call > should sync time when enabled > V3 read state body two 1`] = `
{
"content": {
"ingress_expiry": 1746103080000000000n,
"ingress_expiry": 1746103140000000000n,
"paths": [
[
{
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/__snapshots__/actor.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ exports[`makeActor should enrich actor interface with httpDetails 2`] = `
"__principal__": "2chl6-4hpzw-vqaaa-aaaaa-c",
},
"ingress_expiry": {
"__expiry__": "1200000000000",
"__expiry__": "1260000000000",
},
"method_name": "greet_update",
"nonce": Uint8Array [],
Expand Down
9 changes: 7 additions & 2 deletions packages/agent/src/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import * as cbor from './cbor.ts';
import { requestIdOf } from './request_id.ts';
import * as pollingImport from './polling/index.ts';
import { ActorConfig } from './actor.ts';
import { CertifiedRejectErrorCode, RejectError, UnexpectedErrorCode, UnknownError } from './errors.ts';
import {
CertifiedRejectErrorCode,
RejectError,
UnexpectedErrorCode,
UnknownError,
} from './errors.ts';

const importActor = async (mockUpdatePolling?: () => void) => {
jest.dontMock('./polling');
Expand Down Expand Up @@ -323,7 +328,7 @@ describe('makeActor', () => {
"__principal__": "2chl6-4hpzw-vqaaa-aaaaa-c",
},
"ingress_expiry": {
"__expiry__": "1200000000000",
"__expiry__": "1260000000000",
},
"method_name": "greet",
"request_type": "query",
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/src/agent/http/__snapshots__/http.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`error logs for bad signature should throw call errors if provided an invalid signature 1`] = `"{"message":"Error while making call: HTTP request failed:\\n Status: 400 (Bad Request)\\n Headers: [[0,[\\"access-control-allow-origin\\",\\"*\\"]],[1,[\\"content-length\\",\\"332\\"]],[2,[\\"content-type\\",\\"text/plain; charset=utf-8\\"]],[3,[\\"date\\",\\"Fri, 31 Jan 2025 18:53:47 GMT\\"]]]\\n Body: Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n\\n","level":"error","error":{"name":"ProtocolError","cause":{"code":{"isCertified":false,"status":400,"statusText":"Bad Request","headers":[[0,["access-control-allow-origin","*"]],[1,["content-length","332"]],[2,["content-type","text/plain; charset=utf-8"]],[3,["date","Fri, 31 Jan 2025 18:53:47 GMT"]]],"bodyText":"Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n","name":"HttpErrorCode","requestContext":{"requestId":{"0":169,"1":32,"2":231,"3":46,"4":243,"5":122,"6":239,"7":250,"8":200,"9":250,"10":141,"11":141,"12":17,"13":168,"14":88,"15":134,"16":230,"17":254,"18":162,"19":73,"20":4,"21":70,"22":161,"23":155,"24":74,"25":98,"26":86,"27":140,"28":213,"29":26,"30":79,"31":112},"senderPubKey":{"0":48,"1":42,"2":48,"3":5,"4":6,"5":3,"6":43,"7":101,"8":112,"9":3,"10":33,"11":0,"12":59,"13":106,"14":39,"15":188,"16":206,"17":182,"18":164,"19":45,"20":98,"21":163,"22":168,"23":208,"24":42,"25":111,"26":13,"27":115,"28":101,"29":50,"30":21,"31":119,"32":29,"33":226,"34":67,"35":166,"36":58,"37":192,"38":72,"39":161,"40":139,"41":89,"42":218,"43":41},"senderSignature":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0},"ingressExpiry":{"__expiry__":"1738362720000000000"}}},"kind":"Protocol"}}}"`;
exports[`error logs for bad signature should throw call errors if provided an invalid signature 1`] = `"{"message":"Error while making call: HTTP request failed:\\n Status: 400 (Bad Request)\\n Headers: [[0,[\\"access-control-allow-origin\\",\\"*\\"]],[1,[\\"content-length\\",\\"332\\"]],[2,[\\"content-type\\",\\"text/plain; charset=utf-8\\"]],[3,[\\"date\\",\\"Fri, 31 Jan 2025 18:53:47 GMT\\"]]]\\n Body: Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n\\n","level":"error","error":{"name":"ProtocolError","cause":{"code":{"isCertified":false,"status":400,"statusText":"Bad Request","headers":[[0,["access-control-allow-origin","*"]],[1,["content-length","332"]],[2,["content-type","text/plain; charset=utf-8"]],[3,["date","Fri, 31 Jan 2025 18:53:47 GMT"]]],"bodyText":"Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n","name":"HttpErrorCode","requestContext":{"requestId":{"0":4,"1":197,"2":219,"3":183,"4":253,"5":209,"6":9,"7":87,"8":248,"9":119,"10":161,"11":235,"12":160,"13":177,"14":96,"15":153,"16":14,"17":203,"18":29,"19":188,"20":245,"21":205,"22":77,"23":73,"24":124,"25":171,"26":102,"27":97,"28":247,"29":56,"30":3,"31":241},"senderPubKey":{"0":48,"1":42,"2":48,"3":5,"4":6,"5":3,"6":43,"7":101,"8":112,"9":3,"10":33,"11":0,"12":59,"13":106,"14":39,"15":188,"16":206,"17":182,"18":164,"19":45,"20":98,"21":163,"22":168,"23":208,"24":42,"25":111,"26":13,"27":115,"28":101,"29":50,"30":21,"31":119,"32":29,"33":226,"34":67,"35":166,"36":58,"37":192,"38":72,"39":161,"40":139,"41":89,"42":218,"43":41},"senderSignature":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0},"ingressExpiry":{"__expiry__":"1738362780000000000"}}},"kind":"Protocol"}}}"`;

exports[`error logs for bad signature should throw query errors for bad signature 1`] = `"{"message":"Error while making query: HTTP request failed:\\n Status: 400 (Bad Request)\\n Headers: [[0,[\\"access-control-allow-origin\\",\\"*\\"]],[1,[\\"content-length\\",\\"332\\"]],[2,[\\"content-type\\",\\"text/plain; charset=utf-8\\"]],[3,[\\"date\\",\\"Fri, 31 Jan 2025 18:53:47 GMT\\"]]]\\n Body: Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n\\n","level":"error","error":{"name":"ProtocolError","cause":{"code":{"isCertified":false,"status":400,"statusText":"Bad Request","headers":[[0,["access-control-allow-origin","*"]],[1,["content-length","332"]],[2,["content-type","text/plain; charset=utf-8"]],[3,["date","Fri, 31 Jan 2025 18:53:47 GMT"]]],"bodyText":"Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n","name":"HttpErrorCode","requestContext":{"requestId":{"0":60,"1":51,"2":231,"3":183,"4":128,"5":100,"6":0,"7":71,"8":77,"9":188,"10":35,"11":6,"12":226,"13":151,"14":216,"15":210,"16":101,"17":76,"18":233,"19":255,"20":104,"21":78,"22":181,"23":10,"24":199,"25":89,"26":169,"27":15,"28":0,"29":39,"30":84,"31":32},"senderPubKey":{"0":48,"1":42,"2":48,"3":5,"4":6,"5":3,"6":43,"7":101,"8":112,"9":3,"10":33,"11":0,"12":59,"13":106,"14":39,"15":188,"16":206,"17":182,"18":164,"19":45,"20":98,"21":163,"22":168,"23":208,"24":42,"25":111,"26":13,"27":115,"28":101,"29":50,"30":21,"31":119,"32":29,"33":226,"34":67,"35":166,"36":58,"37":192,"38":72,"39":161,"40":139,"41":89,"42":218,"43":41},"senderSignature":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0},"ingressExpiry":{"__expiry__":"1738362720000000000"}}},"kind":"Protocol"}}}"`;
exports[`error logs for bad signature should throw query errors for bad signature 1`] = `"{"message":"Error while making query: HTTP request failed:\\n Status: 400 (Bad Request)\\n Headers: [[0,[\\"access-control-allow-origin\\",\\"*\\"]],[1,[\\"content-length\\",\\"332\\"]],[2,[\\"content-type\\",\\"text/plain; charset=utf-8\\"]],[3,[\\"date\\",\\"Fri, 31 Jan 2025 18:53:47 GMT\\"]]]\\n Body: Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n\\n","level":"error","error":{"name":"ProtocolError","cause":{"code":{"isCertified":false,"status":400,"statusText":"Bad Request","headers":[[0,["access-control-allow-origin","*"]],[1,["content-length","332"]],[2,["content-type","text/plain; charset=utf-8"]],[3,["date","Fri, 31 Jan 2025 18:53:47 GMT"]]],"bodyText":"Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29, signature 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, error: A signature was invalid\\n","name":"HttpErrorCode","requestContext":{"requestId":{"0":85,"1":22,"2":159,"3":9,"4":106,"5":191,"6":76,"7":252,"8":117,"9":41,"10":137,"11":197,"12":32,"13":62,"14":92,"15":92,"16":121,"17":194,"18":75,"19":107,"20":222,"21":55,"22":117,"23":123,"24":156,"25":16,"26":48,"27":198,"28":92,"29":98,"30":33,"31":216},"senderPubKey":{"0":48,"1":42,"2":48,"3":5,"4":6,"5":3,"6":43,"7":101,"8":112,"9":3,"10":33,"11":0,"12":59,"13":106,"14":39,"15":188,"16":206,"17":182,"18":164,"19":45,"20":98,"21":163,"22":168,"23":208,"24":42,"25":111,"26":13,"27":115,"28":101,"29":50,"30":21,"31":119,"32":29,"33":226,"34":67,"35":166,"36":58,"37":192,"38":72,"39":161,"40":139,"41":89,"42":218,"43":41},"senderSignature":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0},"ingressExpiry":{"__expiry__":"1738362780000000000"}}},"kind":"Protocol"}}}"`;

exports[`retry failures should succeed after multiple failures within the configured limit 1`] = `
{
Expand Down
7 changes: 3 additions & 4 deletions packages/agent/src/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,7 @@ export class HttpAgent implements Agent {
}

/**
* Calculates the ingress expiry time based on the maximum allowed expiry in minutes and the time difference in milliseconds.
* Calculates the ingress expiry time based on the maximum allowed expiry in minutes and the time difference in milliseconds. The expiry is first calculated and rounded from the maximum ingress expiry time, and then adjusted by the time difference in milliseconds.
* @param maxIngressExpiryInMinutes - The maximum ingress expiry time in minutes.
* @param timeDiffMsecs - The time difference in milliseconds to adjust the expiry.
* @returns The calculated ingress expiry as an Expiry object.
Expand All @@ -1398,7 +1398,6 @@ export function calculateIngressExpiry(
maxIngressExpiryInMinutes: number,
timeDiffMsecs: number,
): Expiry {
return Expiry.fromDeltaInMilliseconds(
maxIngressExpiryInMinutes * MINUTE_TO_MSECS + timeDiffMsecs,
);
const roundedExpiry = Expiry.fromDeltaInMilliseconds(maxIngressExpiryInMinutes * MINUTE_TO_MSECS);
return roundedExpiry.addMilliseconds(timeDiffMsecs);
}
Loading
Loading