diff --git a/.github/workflows/system-record-protocol.yml b/.github/workflows/system-record-protocol.yml index 8ce7bb952e..8302ce10e9 100644 --- a/.github/workflows/system-record-protocol.yml +++ b/.github/workflows/system-record-protocol.yml @@ -66,5 +66,14 @@ jobs: - name: Test protocol evidence run: pnpm test:issue-2052:system-records + - name: Test system-record protocol core + run: pnpm --filter @origintrail-official/dkg-core test:system-record + + - name: Smoke-test published system-record subpath + run: pnpm --filter @origintrail-official/dkg-core test:system-record-export + + - name: Test exhaustive system-record inventory publication + run: pnpm --filter @origintrail-official/dkg-core test:system-record-exhaustive + - name: Characterize committed evidence run: pnpm characterize:issue-2052:system-records diff --git a/docs/adr/0002-system-record-sync-v1.md b/docs/adr/0002-system-record-sync-v1.md index abc000e23e..7a9f06829d 100644 --- a/docs/adr/0002-system-record-sync-v1.md +++ b/docs/adr/0002-system-record-sync-v1.md @@ -108,8 +108,12 @@ An applied version is identified by: ``` `authoritySequence` advances only through a valid wallet transition. `version` -advances within one authority sequence. Lower sequences or versions never replace -higher applied state. Two different heads at the current maximal sequence/version are an +advances within one authority sequence. Lower sequences or ordinary active versions never +replace higher applied state. Once learned and verified, a tombstone is the exception: it is +an authority-sequence-wide revocation marker and dominates every active head in that sequence +regardless of delivery order or version. A bounded active closure cannot prove that an +otherwise valid but undisclosed signed tombstone does not exist; ordinary version ancestry is +unbounded and is deliberately not replayed. Two different active heads at the current maximal sequence/version are an equivocation and quarantine the record. A valid successor that directly names a canonical fork resolution may clear that quarantine; after it applies, unresolved alternative lower-version ordinary heads are stale audit input and cannot re-quarantine state. Expiry affects discovery @@ -118,9 +122,10 @@ V1 accepts authority sequences `0` through `14` only, hence at most 14 transitio 15 current-plus-historical roots. A further rotation is a typed version-cap refusal that leaves the last accepted record available and requires a later protocol version. -An unambiguous tombstone is signed and terminal for its authority sequence; resurrection -needs a valid authority transition. If a same-tuple active conflict makes that tombstone -disputed, the record quarantines and a verified fork successor may continue the sequence. +An authenticated tombstone is signed and terminal for its authority sequence; resurrection +needs a valid authority transition. Active/tombstone conflicts resolve to the tombstone and +cannot enter the ordinary fork-resolution path. If multiple valid tombstones are learned for +one sequence, the lowest version wins and equal versions select the lowest semantic digest. Inventory omission has no deletion authority. ## AgentProfileHeadV1 @@ -152,8 +157,8 @@ version is strictly greater than the resolution version. A resolution object alo changes applied state. Historical resolution delivery is discarded after a fixed-enum counter/sampled log and cannot change quarantine, closure, sidecar, or capacity. -The `tombstone` variant adds no fields. It requires `previousHeadDigest` to be the exact -current accepted active head in the same authority sequence, uses the canonical empty +The `tombstone` variant adds no fields. It requires `previousHeadDigest` to name an exact +verified active predecessor in the same authority sequence, uses the canonical empty subject-table digest, and sets owned-subject/projection counts and bytes to decimal `"0"`. It forbids the five active-only fields and cannot be a protocol-history initial head. Both variants require peer plus current-EVM signature entries in the exact signed @@ -167,8 +172,12 @@ fork, previous-head, inventory-row, and applied-state identity use `objectDigest not a signed-envelope digest, so alternate valid signature encodings cannot create a semantic fork. -Tombstone apply deletes the prior exact projection/subject table and releases their -aggregate projection accounting in the same transaction. It retains the minimal +Tombstone apply over a present record deletes that record's exact currently committed +projection/subject table; cold apply over canonical absent state uses the signed +predecessor's table as its bounded deletion fallback. Cold noninitial apply accepts only an +opaque authority summary minted by the complete signature-verified closure builder; a caller- +authored or structurally cloned summary is rejected. It releases the deleted projection +accounting in the same transaction and retains the minimal terminal applied state, every current/historical root claim and reverse binding, and all precharged status/conflict slots. Resurrection still requires a valid next authority transition signed by the tombstoning EVM authority; the unavailable-prior- @@ -197,7 +206,9 @@ use exactly `0x` plus 130 lowercase hex characters and canonical low-s/recovery EIP-1271 opaque signatures use `0x` plus 2-8,192 lowercase hex characters (1-4,096 bytes). EIP-1271 evidence uses the canonical unsigned-u256 decimal `chainId`, the lowercase nonzero contract-address grammar above, u64-decimal `finalizedBlockNumber`, and a -lowercase 32-byte `finalizedBlockHash`; all four are JSON strings. V1 reuses only +lowercase 32-byte `finalizedBlockHash`; all four are JSON strings. Its `chainId` must equal +the canonical decimal suffix after the final `:` in the signed object's `networkId`; a +network ID without a numeric suffix cannot authorize EIP-1271 evidence. V1 reuses only `assertCanonicalDecimalU64`, `assertCanonicalChainId`, `assertCanonicalDigest`, `assertCanonicalEvmAddress`, and `assertCanonicalHexBytes` from `packages/core/src/sync-wire-scalars.ts`, @@ -288,7 +299,7 @@ applied-head capability: root, network, and record key all refer to the same `agents` record; the graph-scoped seal author equals current EVM authority; and exact bundle/seal verification precedes state advance; -- for `tombstone`, the exact current accepted active predecessor, same authority/root, +- for `tombstone`, an exact verified active predecessor, same authority/root, empty-table digest/count, zero projection accounting, and forbidden active-only fields all validate before deletion. Inventory/provider signatures authenticate availability only and cannot authorize either variant. @@ -297,8 +308,11 @@ An initial active head requires peer and EVM signatures over sequence zero, has head/transition, and satisfies every active equality above. An ordinary active update keeps the same network, peer, issuer, authority sequence, and root; its version is strictly higher. A valid higher version may fast-forward, while `previousHeadDigest` remains -fork/audit evidence rather than a retention-dependent gate. Tombstone is the explicit -exception: it must bind the exact currently accepted active predecessor. +fork/audit evidence rather than a retention-dependent gate. Consequently the active closure +does not claim absence of an unseen tombstone; a tombstone takes terminal precedence as soon +as its own bounded proof is learned. A tombstone instead proves a +noninitial active predecessor and, once verified, dominates all active versions in that +authority sequence; it does not depend on which active version is currently local. The canonical unsigned `AgentProfileAuthorityTransitionV1` fields are exactly `(objectType='authority-transition', kind='agents', mode='co-signed'|'expired-prior', @@ -335,7 +349,7 @@ valid conflict omitted from this bounded list. `resolutionVersion` is strictly greater than `forkedVersion`. Peer and current-EVM signatures are mandatory. At version zero `forkBaseHeadDigest` is omitted and every listed initial head omits `previousHeadDigest`. Above zero it is mandatory, names a -fully verified lower-version same-authority/root head, and every listed head must +fully verified lower-version active same-authority/root head, and every listed active head must name it as `previousHeadDigest`; otherwise V1 leaves the fork quarantined for a later protocol. This order-independent common base replaces the ambiguous notion of a locally first accepted head. @@ -347,10 +361,10 @@ current EVM authority, and has `version > resolutionVersion`. Above version zero the common `forkBaseHeadDigest` as `previousHeadDigest`; at version zero it omits `previousHeadDigest`. This rule is independent of which conflicting projection arrived first and of conflicts omitted from `evidenceHeadDigests`. The resolving successor replaces -any locally retained quarantined projection atomically. Only after that successor applies -may an authority transition name it as `priorHeadDigest`; a disputed head/tombstone fork -cannot transition directly. V1 has no direct terminal fork-resolution mode, and deletion -occurs only through an unambiguous ordinary tombstone head. +any locally retained quarantined projection atomically. Tombstones cannot be a common base, +listed fork evidence, or resolving-successor ancestry. Only after that successor applies +may an authority transition name it as `priorHeadDigest`. V1 has no terminal fork-resolution +mode; a verified tombstone bypasses fork resolution and is the terminal sequence marker. A fork resolution below the current frontier or below an already materialized authority descendant is audit-only: the verifier may record a fixed-enum counter/sampled log, then @@ -497,10 +511,9 @@ current authority bindings, and establishes a resolution version. More than 16 l verified frontier conflicts sets bounded overflow diagnostics while the fork is unresolved, but cannot invalidate a fresh directly bound resolution/successor; a receiver does not infer evidence completeness from the resolution list. A tombstone requires current peer and EVM authority, -advances the version, and is terminal while unambiguous; a same-tuple active conflict -quarantines it and requires a verified resolving successor before any later authority -transition may name that successor. An unambiguous tombstone remains separately eligible -for ordinary resurrection by authority transition. +proves an exact active predecessor, and is the authority-sequence-wide terminal marker. +Same-tuple and cross-version active conflicts do not weaken it or authorize fork resolution. +A tombstone remains eligible for resurrection only by a valid next-sequence authority transition. Expiry changes discovery eligibility only and never authorizes rollback. `issuedAt` more than five minutes in the future is rejected. @@ -689,13 +702,21 @@ offsets never cross descriptors. Stack B1 freezes a canonical applied-state object before storage work. The exact tagged `absent` sentinel and `present` schema cover network/kind, stable key, monotonic state revision, status (`active`, `quarantined`, `tombstone`, `dirty`), -head/conflict-evidence?/projection/owned-subject-table digests and +head/conflict-evidence?/projection/owned-subject-table digests, the exact contiguous +`(priorAuthoritySequence,nextAuthoritySequence,transitionDigest)` lineage for each of +the at-most-14 accepted authority transitions, and counts, optional all-or-none conflict-sidecar intent operation/digest/state-revision, optional pending-deletion-table digest/count/bytes, current root, at most 14 historical roots (15 current-plus-historical roots total), 16 fixed preallocated conflict-digest slots plus one overflow bit, materialization epoch, and accounted bytes. Its digest domain is `dkg-system-record-applied-state-v1\n` and excludes only the digest field. Global capacity state has its own revision, live-record count, and accounted bytes. +For every present record, `accountedBytes` is canonical and exact: +`64 KiB fixed state/security precharge + ownedSubjectTableBytes + projectionBytes + +pendingDeletionTableBytes`. The pending term is zero when omitted; current JSON size is +validated against the 64-KiB envelope but never reduces the precharge. +Tombstones commit the canonical SHA-256 digest of an empty projection under +`dkg-ka-projection-v1\n`; active state rejects that empty-projection digest. Capacity accounting separates state/table bytes, persistent V1 projection bytes, and projection quads. `conflictEvidenceDigest` is present only for a fully cached unresolved availability @@ -829,6 +850,7 @@ recomputes the table digest/count against the verified predecessor head before d the exact deletion. Missing/mismatched/reused/cross-session payloads fail before dispatch. The expected-state CAS covers `(stateRevision, appliedStateDigest, headDigest, +transitionLineage, conflictEvidenceDigest?, ownedSubjectTableDigest, conflictSidecarIntentOperation?, conflictSidecarIntentEvidenceDigest?, conflictSidecarIntentStateRevision?, @@ -853,7 +875,11 @@ count, and whole-graph scan are forbidden. A fully verified tombstone closure may CAS directly from canonical `absent` local state to terminal `tombstone` state. “Cannot be initial” is a protocol-history predicate proved by its exact signed active predecessor and deletion-only owned-subject table, not -a requirement to materialize that predecessor locally first. The one transaction deletes +a requirement to materialize that predecessor locally first. The closure builder returns an +in-memory opaque authority summary binding candidate digest, exact contiguous transition +lineage, ordered unique historical roots, predecessor, and deletion-table digest. Cold apply +requires that branded summary; serialized or caller-constructed equivalents have no authority. +The one transaction deletes the predecessor table's exact subjects, installs terminal head/state, verified current/historical root claims and reverse bindings, empty current table, zero projection, and precharged security slots. It never inserts the predecessor projection, and discovery/ @@ -869,8 +895,9 @@ identity before/after crash and cutover. Fork resolutions never mint a materializer command. They retain quarantine until a verified current-frontier resolving successor applies through the ordinary exact -replacement path. A disputed tombstone therefore cannot delete through a fork resolution; -only an unambiguous ordinary tombstone head can use the cold deletion command. A fork +replacement path. A tombstone never enters fork resolution. When it supersedes a present +active row, the tombstone command deletes the exact currently applied table; only cold +absent-state apply uses the signed predecessor table. A fork resolution below the current frontier performs no projection, root-claim, binding, capacity, or closure mutation. Transition equivocation has no V1 resolution command and remains quarantined. @@ -889,8 +916,9 @@ incumbent slot. It never counts or describes the contender as materialized. Both `root-collision`; opposite arrival order leaves no record discoverable and never overwrites the first projection. V1 has no cross-peer root handoff. Expiry and -tombstone do not release the claim, and each record retains at most 15 current-plus- -historical root claims across wallet transitions. Exceeding that history requires a later +tombstone do not release the claim, and each transition must target a root never previously +claimed by that stable record. Each record retains at most 15 current-plus-historical root +claims across wallet transitions. Exceeding that history requires a later protocol version rather than silent reuse. Quarantine/tombstone/security status and all 16 conflict slots are precharged inside @@ -1233,7 +1261,8 @@ index. Its atomic root manifest pins the current descriptor/tree and each row's verification closure. The closure builder traverses a canonical digest-ordered queue, deduplicates by digest, and applies these exact rules: -1. add the current signed head and, for active state, the current exact bundle; +1. add the current signed head and, for active state, the current exact bundle; ordinary + same-sequence `previousHeadDigest` ancestry is audit evidence and is not traversed; 2. if the current head has `forkResolutionDigest`, add that exact fork resolution, every listed evidence head, and its optional common fork base; add every authority transition referenced by a traversed head; @@ -1248,9 +1277,11 @@ Only the current active head's bundle is materialization evidence and belongs in closure. Historical predecessor/conflict bundles are neither fetched nor retained: their signed heads, graph-scoped seals, coordinates, content digests, and authority/control chain are sufficient to verify authority, ordering, and equivocation, while their old -profile RDF is not materialized. A tombstone has no current bundle and at most one +profile RDF is not materialized. An unseen same-sequence tombstone cannot be disproved by a +bounded partial history; after it is learned, applied-state ordering makes it terminal without +requiring ordinary ancestry replay. A tombstone has no current bundle and at most one 256-KiB deletion table. An active closure with one at-most-1-MiB current bundle and at -most 31 other at-most-64-KiB objects is algebraically below 3 MiB; a tombstone with one +most 29 other at-most-64-KiB objects is algebraically below 3 MiB; a tombstone with one deletion table and 31 64-KiB objects is below 2.25 MiB. A sequence-zero maximum 16-conflict fork resolution also fits. Every control object and successor head preflights the resulting complete closure before it can become accepted, materialized, or advertised. An ordinary @@ -1264,9 +1295,10 @@ chain re-quarantines, remains sidecar-backed/row-quarantined, never enters an ad active closure, and cannot clear in V1. There is no state in which an accepted/advertised head lacks a serveable closure. -The exhaustive edge equations are part of the B1 tests. A resolution-free sequence-14 -active row is `current head + current bundle + 14 * (transition + prior head) = 30 -objects`. Its tombstone is `tombstone head + deletion table + active predecessor + 14 * +The exhaustive edge equations are part of the B1 tests. A worst-case resolution-free +sequence-14 active row is `current head + current bundle + 14 * +(transition + prior head) = 30 objects`. Its tombstone is +`tombstone head + deletion table + active predecessor + 14 * (transition + transition-prior head) = 31 objects`. At authority sequence zero, a first maximum 16-conflict fork resolution is at most `current head + current payload + resolution + 16 conflicts + optional common base = 20 objects`. Recursive authority @@ -1286,7 +1318,8 @@ it; resolution arrival without that successor is audit-only and discarded after diagnostics. Opposite `head -> resolution` and `resolution -> head` delivery, restart, provider failover, and a previously unseen omitted conflict must produce identical head, status, and closure. -The result is sorted by `(objectKind,digest)` and must contain at most 32 objects and +The result and every reference list are sorted by semantic digest (with object kind only +as a collision tie-break) and must contain at most 32 objects and 3 MiB canonical bytes. No chain is truncated and no checkpoint inferred; missing or over-cap closure fails before advertisement and keeps legacy authoritative. Globally, advertised closures use at most 25,000 distinct objects/1 GiB and 262,144 row-to-object @@ -1301,6 +1334,9 @@ signed control objects, hence 17 objects/1,064,960 canonical bytes per row. Aggr caps are 1,024 sidecars, 17,408 row-object references, 128 MiB canonical bytes, and a 2-MiB metadata subcap inside the combined 24-MiB live/4-MiB activation metadata caps; physical objects also count in the existing 50,000-object/2-GiB cache. +The B1 aggregate helper returns an exact encoded cohort delta only. B2 atomically combines +that delta with the complete cache baseline, including roots, inventory nodes, staging, +unreferenced objects, and journal/WAL reserve, before enforcing the physical cache caps. It is excluded from active closure service/drain numerators because a sidecar row is quarantined, but all disk, wire, token-bucket, and cache accounting includes it. Cap refusal never unquarantines locally and never permits an unverified remote quarantine. @@ -1489,7 +1525,7 @@ Re-enabling requires exact record validation before any equal-head no-op. transition conflict set, later heads, restart, and provider failover with identical terminal quarantine and zero further materialized-state change; A-to-B-to-C followed by old-peer/old-wallet transition/successor attempts with zero lineage or materialized-state change; - disputed tombstone fork with zero direct deletion; same-sequence and rotated-authority + invalid or incomplete tombstone proof with zero direct deletion; same-sequence and rotated-authority successor approval vectors; direct resolution/successor delivery orders at version zero and above zero, omitted third-conflict and 16/17- conflict orders, sequence-13/14 by 2/16-conflict closure-cap boundaries, liar, diff --git a/packages/core/package.json b/packages/core/package.json index 0f918614d3..4307b50e87 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,12 +15,21 @@ "import": "./dist/project-ontology.js", "default": "./dist/project-ontology.js" }, + "./system-record-v1": { + "types": "./dist/system-record-v1.d.ts", + "import": "./dist/system-record-v1.js", + "default": "./dist/system-record-v1.js" + }, "./dist/*": "./dist/*", "./package.json": "./package.json" }, "scripts": { "build": "tsc", - "test": "vitest run", + "test": "pnpm run test:baseline && pnpm run test:system-record", + "test:baseline": "vitest run --exclude 'test/system-record-*.test.ts' --maxWorkers=4", + "test:system-record": "vitest run test/system-record-applied-state-v1.test.ts test/system-record-golden-v1.test.ts test/system-record-inventory-v1.test.ts test/system-record-limits-v1.test.ts test/system-record-objects-v1.test.ts test/system-record-wire-v1.test.ts --maxWorkers=1 --no-file-parallelism", + "test:system-record-export": "node test/system-record-package-export-v1.mjs", + "test:system-record-exhaustive": "DKG_SYSTEM_RECORD_EXHAUSTIVE=1 vitest run test/system-record-inventory-v1.test.ts test/system-record-objects-v1.test.ts -t \"complete height-three|exactly 128 MiB\" --maxWorkers=1 --no-file-parallelism", "test:coverage": "vitest run --coverage", "clean": "rm -rf dist tsconfig.tsbuildinfo" }, @@ -43,6 +52,7 @@ "@libp2p/websockets": "^10.1.13", "@libp2p/yamux": "^8.0.1", "@multiformats/multiaddr": "^13.0.3", + "@noble/curves": "2.2.0", "@noble/ed25519": "^3.1.0", "@noble/hashes": "^2.2.0", "@opentelemetry/api": "^1.9.1", @@ -54,6 +64,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@vitest/coverage-v8": "^4.0.18", + "multiformats": "14.0.0", "vitest": "^4.0.18" }, "publishConfig": { diff --git a/packages/core/src/canonical-json.ts b/packages/core/src/canonical-json.ts index f45f77680e..5778ee4859 100644 --- a/packages/core/src/canonical-json.ts +++ b/packages/core/src/canonical-json.ts @@ -18,6 +18,10 @@ export type StrictJsonParseOptions = CanonicalJsonOptions; export const MAX_CANONICAL_JSON_BYTES = 8 * 1024 * 1024; export const MAX_CANONICAL_JSON_DEPTH = 64; const UTF8 = new TextEncoder(); +const TYPED_ARRAY_BYTE_LENGTH = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array.prototype) as object, + 'byteLength', +)?.get; // Preserve a leading BOM so the explicit wire-level rejection below can see it. const UTF8_FATAL = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }); @@ -142,7 +146,22 @@ export function parseJsonStrict( ): CanonicalJsonValue { const { maxBytes, maxDepth } = resolveLimits(options); - const byteLength = typeof input === 'string' ? UTF8.encode(input).byteLength : input.byteLength; + let byteLength: number; + if (typeof input === 'string') { + if (input.length > maxBytes) { + throw new CanonicalJsonError(`JSON input exceeds ${maxBytes} bytes`); + } + byteLength = UTF8.encode(input).byteLength; + } else { + if (!(input instanceof Uint8Array) || TYPED_ARRAY_BYTE_LENGTH === undefined) { + throw new CanonicalJsonError('JSON byte input must be a Uint8Array'); + } + try { + byteLength = Reflect.apply(TYPED_ARRAY_BYTE_LENGTH, input, []) as number; + } catch { + throw new CanonicalJsonError('JSON byte input must be a valid Uint8Array'); + } + } if (byteLength > maxBytes) { throw new CanonicalJsonError(`JSON input exceeds ${maxBytes} bytes`); } diff --git a/packages/core/src/sync-wire-objects.ts b/packages/core/src/sync-wire-objects.ts index fbe0edf6d8..b04fa1e98f 100644 --- a/packages/core/src/sync-wire-objects.ts +++ b/packages/core/src/sync-wire-objects.ts @@ -5,21 +5,118 @@ export function isPlainRecord(value: unknown): value is Record return prototype === Object.prototype || prototype === null; } -/** Snapshot one closed plain record without invoking accessors or re-reading fields. */ -export function snapshotExactDataRecord( +export interface SnapshotDataRecordOptions { + /** Closed protocol objects represent absent optionals by omission, never JSON null. */ + readonly rejectNullValues?: boolean; +} + +export interface SnapshotDataArrayOptions { + readonly minLength?: number; + readonly maxLength: number; +} + +/** + * Snapshot one bounded, dense array without invoking accessors or caller-owned methods. + * + * Protocol arrays are closed containers: only their native length and enumerable own + * data elements are accepted. The returned copy always uses the local Array prototype, + * so later iteration cannot be redirected by a caller-owned prototype or method. + */ +export function snapshotDataArray( value: unknown, - expected: Keys, label: string, -): Readonly> { + options: SnapshotDataArrayOptions, +): readonly unknown[] { + const minLength = options.minLength ?? 0; + const maxLength = options.maxLength; + if (!Number.isSafeInteger(minLength) || minLength < 0 + || !Number.isSafeInteger(maxLength) || maxLength < minLength) { + throw new Error(`${label} has invalid snapshot bounds`); + } + if (!Array.isArray(value)) throw new Error(`${label} must be an array`); + + const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length'); + if (!lengthDescriptor || !Object.prototype.hasOwnProperty.call(lengthDescriptor, 'value') + || lengthDescriptor.enumerable === true + || !Number.isSafeInteger(lengthDescriptor.value) + || lengthDescriptor.value < minLength + || lengthDescriptor.value > maxLength) { + throw new Error(`${label} length is outside its bound`); + } + const length = lengthDescriptor.value as number; + const keys = Reflect.ownKeys(value); + if (keys.length !== length + 1) throw new Error(`${label} must be a dense closed array`); + + const snapshot = new Array(length); + let elements = 0; + for (const key of keys) { + if (key === 'length') continue; + if (typeof key !== 'string') throw new Error(`${label} must not contain symbol properties`); + const index = Number(key); + if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== key) { + throw new Error(`${label} must not contain non-index properties`); + } + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!descriptor?.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) { + throw new Error(`${label} must contain only enumerable data elements`); + } + snapshot[index] = descriptor.value; + elements += 1; + } + if (elements !== length) throw new Error(`${label} must be dense`); + return Object.freeze(snapshot); +} + +/** + * Snapshot every enumerable string data property without invoking accessors. + * + * Codecs use this once to discover which optional fields are present, then pass + * the resulting snapshot through {@link snapshotExactDataRecord}. Keeping that + * two-step boundary here prevents each codec from drifting on prototype, + * symbol, accessor, enumerability, and null handling. + */ +export function snapshotDataRecord( + value: unknown, + label: string, + options: SnapshotDataRecordOptions = {}, +): Readonly> { if (!isPlainRecord(value)) { throw new Error(`${label} must be a plain data object`); } - const actual = Reflect.ownKeys(value); - if (actual.some((key) => typeof key !== 'string')) { - throw new Error(`${label} must not contain symbol properties`); + const snapshot: Record = Object.create(null); + for (const key of Reflect.ownKeys(value)) { + if (typeof key !== 'string') { + throw new Error(`${label} must not contain symbol properties`); + } + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!descriptor?.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) { + throw new Error(`${label} fields must be enumerable data properties`); + } + if (options.rejectNullValues === true && descriptor.value === null) { + throw new Error(`${label} must omit optional fields, not use null`); + } + snapshot[key] = descriptor.value; } - const strings = actual as string[]; + return Object.freeze(snapshot); +} + +/** Accessor-safe presence test for an enumerable own data property. */ +export function hasOwnDataProperty(value: unknown, key: string): boolean { + if (value === null || typeof value !== 'object') return false; + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return descriptor?.enumerable === true + && Object.prototype.hasOwnProperty.call(descriptor, 'value'); +} + +/** Snapshot one closed plain record without invoking accessors or re-reading fields. */ +export function snapshotExactDataRecord( + value: unknown, + expected: Keys, + label: string, +): Readonly> { + const snapshot = snapshotDataRecord(value, label); + const strings = Object.keys(snapshot); const sortedExpected = [...expected].sort(); if ( strings.length !== sortedExpected.length @@ -28,15 +125,7 @@ export function snapshotExactDataRecord( throw new Error(`${label} has unknown or missing fields`); } - const snapshot: Record = Object.create(null); - for (const key of expected) { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - if (!descriptor?.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) { - throw new Error(`${label} fields must be enumerable data properties`); - } - snapshot[key] = descriptor.value; - } - return Object.freeze(snapshot) as Readonly>; + return snapshot as Readonly>; } /** Require one plain record to contain exactly enumerable string data fields. */ diff --git a/packages/core/src/system-record-applied-state-v1.ts b/packages/core/src/system-record-applied-state-v1.ts new file mode 100644 index 0000000000..32bcc2be8e --- /dev/null +++ b/packages/core/src/system-record-applied-state-v1.ts @@ -0,0 +1,582 @@ +import { + canonicalizeJsonBytes, + parseCanonicalJson, + type CanonicalJsonValue, +} from './canonical-json.js'; +import { + assertAgentRootV1, + EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + type AgentProfileAppliedTransitionV1, +} from './system-record-objects-v1.js'; +import { KA_BUNDLE_PROJECTION_DIGEST_DOMAIN_V1 } from './ka-bundle-v1.js'; +import { + assertCanonicalSystemRecordPeerIdV1, + digestSystemRecordBytesV1, +} from './system-record-codec-primitives-v1.js'; +import { computeSystemRecordStableKeyHashV1 } from './system-record-inventory-v1.js'; +import { + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX, + SYSTEM_RECORD_KIND_V1, + SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES, + SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_QUADS, + SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + SYSTEM_RECORD_MAX_CONFLICT_DIGESTS, + SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + SYSTEM_RECORD_MAX_INVENTORY_RECORDS, + SYSTEM_RECORD_MAX_OWNED_SUBJECTS, + SYSTEM_RECORD_MAX_PROJECTION_BYTES, + SYSTEM_RECORD_MAX_PROJECTION_QUADS, + SYSTEM_RECORD_MAX_ROOT_CLAIMS, + SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH, + SYSTEM_RECORD_MAX_TUPLE_JSON_DEPTH, + SYSTEM_RECORD_OBJECT_CAPS_V1, +} from './system-record-limits-v1.js'; +import { assertNetworkIdV1, type NetworkIdV1 } from './sync-wire-identifiers.js'; +import { + assertCanonicalDecimalU64, + assertCanonicalDigest, + parseCanonicalDecimalU64, + type DecimalU64V1, + type Digest32V1, +} from './sync-wire-scalars.js'; +import { + hasOwnDataProperty, + snapshotDataArray, + snapshotDataRecord, + snapshotExactDataRecord, +} from './sync-wire-objects.js'; + +export interface SystemRecordAppliedStateAbsentV1 { + readonly objectType: 'system-record-applied-state'; + readonly state: 'absent'; +} + +export type SystemRecordAppliedStatusV1 = 'active' | 'quarantined' | 'tombstone' | 'dirty'; +export type SystemRecordConflictIntentOperationV1 = 'publish' | 'remove' | 'deferred'; + +export interface SystemRecordAppliedStatePresentV1 { + readonly objectType: 'system-record-applied-state'; + readonly state: 'present'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly stableKeyHash: Digest32V1; + readonly peerId: string; + readonly stateRevision: DecimalU64V1; + readonly status: SystemRecordAppliedStatusV1; + readonly headDigest: Digest32V1; + readonly transitionLineage: readonly AgentProfileAppliedTransitionV1[]; + readonly conflictEvidenceDigest?: Digest32V1; + readonly projectionDigest: Digest32V1; + readonly projectionBytes: DecimalU64V1; + readonly projectionQuads: DecimalU64V1; + readonly ownedSubjectTableDigest: Digest32V1; + readonly ownedSubjectCount: DecimalU64V1; + readonly ownedSubjectTableBytes: DecimalU64V1; + readonly conflictSidecarIntentOperation?: SystemRecordConflictIntentOperationV1; + readonly conflictSidecarIntentEvidenceDigest?: Digest32V1; + readonly conflictSidecarIntentStateRevision?: DecimalU64V1; + readonly pendingDeletionTableDigest?: Digest32V1; + readonly pendingDeletionSubjectCount?: DecimalU64V1; + readonly pendingDeletionTableBytes?: DecimalU64V1; + readonly currentRoot: string; + readonly historicalRoots: readonly string[]; + readonly conflictDigestSlots: readonly Digest32V1[]; + readonly conflictOverflow: boolean; + readonly materializationEpoch: DecimalU64V1; + readonly rootClaimSetDigest: Digest32V1; + readonly accountedBytes: DecimalU64V1; +} + +export type SystemRecordAppliedStateV1 = + | SystemRecordAppliedStateAbsentV1 + | SystemRecordAppliedStatePresentV1; + +export interface SystemRecordRootClaimSetV1 { + readonly objectType: 'system-record-root-claim-set'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly stableKeyHash: Digest32V1; + readonly currentRoot: string; + readonly historicalRoots: readonly string[]; +} + +export interface SystemRecordCapacityStateV1 { + readonly objectType: 'system-record-capacity-state'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly revision: DecimalU64V1; + readonly liveRecordCount: DecimalU64V1; + readonly stateBytes: DecimalU64V1; + readonly tableBytes: DecimalU64V1; + readonly projectionBytes: DecimalU64V1; + readonly projectionQuads: DecimalU64V1; +} + +export interface SystemRecordMaterializationReceiptV1 { + readonly objectType: 'system-record-materialization-receipt'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly stableKeyHash: Digest32V1; + readonly stateRevision: DecimalU64V1; + readonly appliedStateDigest: Digest32V1; + readonly headDigest: Digest32V1; + readonly materializationEpoch: DecimalU64V1; +} + +const ABSENT: SystemRecordAppliedStateAbsentV1 = Object.freeze({ + objectType: 'system-record-applied-state', + state: 'absent', +}); + +/** Projection digest committed by every terminal state with zero projection bytes. */ +export const SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1 = digestSystemRecordBytesV1( + KA_BUNDLE_PROJECTION_DIGEST_DOMAIN_V1, + new Uint8Array(), +); + +export function systemRecordAppliedStateAbsentV1(): SystemRecordAppliedStateAbsentV1 { + return ABSENT; +} + +export function assertSystemRecordAppliedStateV1( + value: unknown, +): asserts value is SystemRecordAppliedStateV1 { + validateAppliedState(value); +} + +export function canonicalizeSystemRecordAppliedStateV1( + value: SystemRecordAppliedStateV1, +): Uint8Array { + return canonicalizeJsonBytes(validateAppliedState(value) as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + }); +} + +export function parseCanonicalSystemRecordAppliedStateV1( + input: string | Uint8Array, +): SystemRecordAppliedStateV1 { + return validateAppliedState(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, maxDepth: SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH, + })); +} + +export function computeSystemRecordAppliedStateDigestV1( + value: SystemRecordAppliedStateV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.appliedState, + canonicalizeSystemRecordAppliedStateV1(value), + ); +} + +function validateAppliedState(value: unknown): SystemRecordAppliedStateV1 { + const probe = snapshotDataRecord(value, 'system-record applied state', { + rejectNullValues: true, + }); + if (probe.state === 'absent') { + const absent = snapshotExactDataRecord(probe, ['objectType', 'state'], 'absent applied state'); + if (absent.objectType !== 'system-record-applied-state') throw new Error('absent applied-state tag is invalid'); + return ABSENT; + } + const optional = [ + 'conflictEvidenceDigest', + 'conflictSidecarIntentOperation', 'conflictSidecarIntentEvidenceDigest', + 'conflictSidecarIntentStateRevision', + 'pendingDeletionTableDigest', 'pendingDeletionSubjectCount', 'pendingDeletionTableBytes', + ].filter((key) => hasOwnDataProperty(probe, key)); + const state = snapshotExactDataRecord( + probe, + [ + 'objectType', 'state', 'kind', 'networkId', 'stableKeyHash', 'peerId', + 'stateRevision', 'status', 'headDigest', ...optional, + 'transitionLineage', + 'projectionDigest', 'projectionBytes', 'projectionQuads', + 'ownedSubjectTableDigest', 'ownedSubjectCount', 'ownedSubjectTableBytes', + 'currentRoot', 'historicalRoots', 'conflictDigestSlots', 'conflictOverflow', + 'materializationEpoch', 'rootClaimSetDigest', 'accountedBytes', + ], + 'present applied state', + ); + if (state.objectType !== 'system-record-applied-state' || state.state !== 'present' + || state.kind !== SYSTEM_RECORD_KIND_V1) { + throw new Error('present applied-state tag is invalid'); + } + assertNetworkIdV1(state.networkId); + assertCanonicalDigest(state.stableKeyHash); + assertCanonicalSystemRecordPeerIdV1(state.peerId); + if (state.stableKeyHash !== computeSystemRecordStableKeyHashV1( + state.networkId as NetworkIdV1, + state.peerId as string, + )) { + throw new Error('applied-state stableKeyHash does not bind networkId/peerId'); + } + assertCanonicalDecimalU64(state.stateRevision); + if (!['active', 'quarantined', 'tombstone', 'dirty'].includes(state.status as string)) { + throw new Error('applied-state status is invalid'); + } + assertCanonicalDigest(state.headDigest); + const transitionLineage = validateTransitionLineage(state.transitionLineage); + if (hasOwnDataProperty(state, 'conflictEvidenceDigest')) { + assertCanonicalDigest(state.conflictEvidenceDigest); + } + assertCanonicalDigest(state.projectionDigest); + const projectionBytes = boundedU64(state.projectionBytes, SYSTEM_RECORD_MAX_PROJECTION_BYTES, 'projectionBytes'); + const projectionQuads = boundedU64(state.projectionQuads, SYSTEM_RECORD_MAX_PROJECTION_QUADS, 'projectionQuads'); + assertCanonicalDigest(state.ownedSubjectTableDigest); + const ownedCount = boundedU64(state.ownedSubjectCount, SYSTEM_RECORD_MAX_OWNED_SUBJECTS, 'ownedSubjectCount'); + boundedU64( + state.ownedSubjectTableBytes, + SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], + 'ownedSubjectTableBytes', + ); + validateAllOrNoneGroup(state, [ + 'conflictSidecarIntentOperation', + 'conflictSidecarIntentEvidenceDigest', + 'conflictSidecarIntentStateRevision', + ], 'conflict sidecar intent'); + if (hasOwnDataProperty(state, 'conflictSidecarIntentOperation')) { + if (!['publish', 'remove', 'deferred'].includes(state.conflictSidecarIntentOperation as string)) { + throw new Error('conflict sidecar intent operation is invalid'); + } + assertCanonicalDigest(state.conflictSidecarIntentEvidenceDigest); + assertCanonicalDecimalU64(state.conflictSidecarIntentStateRevision); + if (state.conflictSidecarIntentStateRevision !== state.stateRevision) { + throw new Error('conflict sidecar intent must bind the authoritative state revision'); + } + const operation = state.conflictSidecarIntentOperation; + if (operation === 'remove') { + if (state.status !== 'active' + || state.conflictEvidenceDigest !== state.conflictSidecarIntentEvidenceDigest) { + throw new Error('remove intent requires discoverable state retaining the installed evidence digest'); + } + } else if (state.status !== 'quarantined' + || hasOwnDataProperty(state, 'conflictEvidenceDigest')) { + throw new Error('publish/deferred intent requires quarantine without installed evidence'); + } + } else if (hasOwnDataProperty(state, 'conflictEvidenceDigest') + && state.status !== 'quarantined') { + throw new Error('installed conflict evidence requires quarantine or an active remove intent'); + } + if (state.status === 'quarantined' + && !hasOwnDataProperty(state, 'conflictEvidenceDigest') + && !hasOwnDataProperty(state, 'conflictSidecarIntentOperation')) { + throw new Error('quarantine requires installed conflict evidence or a resumable sidecar intent'); + } + validateAllOrNoneGroup(state, [ + 'pendingDeletionTableDigest', 'pendingDeletionSubjectCount', 'pendingDeletionTableBytes', + ], 'pending deletion'); + if (hasOwnDataProperty(state, 'pendingDeletionTableDigest')) { + if (state.status !== 'dirty') throw new Error('pending deletion is valid only on dirty shadow state'); + assertCanonicalDigest(state.pendingDeletionTableDigest); + boundedU64(state.pendingDeletionSubjectCount, SYSTEM_RECORD_MAX_OWNED_SUBJECTS, 'pendingDeletionSubjectCount'); + boundedU64( + state.pendingDeletionTableBytes, + SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], + 'pendingDeletionTableBytes', + ); + } + assertAgentRootV1(state.currentRoot as string); + const historicalRoots = validateRootArray(state.historicalRoots, state.currentRoot as string); + if (historicalRoots.length !== transitionLineage.length) { + throw new Error('root history must match the retained authority-transition lineage'); + } + const conflictDigestSlots = validateDigestSlots(state.conflictDigestSlots); + if (typeof state.conflictOverflow !== 'boolean') throw new Error('conflictOverflow must be boolean'); + assertCanonicalDecimalU64(state.materializationEpoch); + assertCanonicalDigest(state.rootClaimSetDigest); + const expectedClaimDigest = computeSystemRecordRootClaimSetDigestV1({ + objectType: 'system-record-root-claim-set', + kind: SYSTEM_RECORD_KIND_V1, + networkId: state.networkId as NetworkIdV1, + stableKeyHash: state.stableKeyHash as Digest32V1, + currentRoot: state.currentRoot as string, + historicalRoots, + }); + if (state.rootClaimSetDigest !== expectedClaimDigest) { + throw new Error('rootClaimSetDigest does not bind the applied roots and stable key'); + } + const accountedBytes = boundedU64( + state.accountedBytes, + SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES, + 'accountedBytes', + ); + const pendingBytes = hasOwnDataProperty(state, 'pendingDeletionTableBytes') + ? parseCanonicalDecimalU64(state.pendingDeletionTableBytes) + : 0n; + const normalizedState = { + ...state, + historicalRoots, + conflictDigestSlots, + transitionLineage, + } as unknown as CanonicalJsonValue; + canonicalizeJsonBytes(normalizedState, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + }); + const expectedAccounted = BigInt(computeSystemRecordAccountedBytesV1( + Number(parseCanonicalDecimalU64(state.ownedSubjectTableBytes)), + Number(projectionBytes), + Number(pendingBytes), + )); + if (accountedBytes !== expectedAccounted) { + throw new Error('accountedBytes must equal the fixed state precharge plus exact persistent bytes'); + } + if (state.status === 'tombstone' && (projectionBytes !== 0n || projectionQuads !== 0n + || state.projectionDigest !== SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1 + || ownedCount !== 0n || state.ownedSubjectTableBytes !== '0' + || state.ownedSubjectTableDigest !== EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1)) { + throw new Error('tombstone applied state must commit the canonical empty projection/table'); + } + if (state.status === 'active' && (projectionBytes === 0n || projectionQuads === 0n || ownedCount === 0n + || state.projectionDigest === SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1 + || state.ownedSubjectTableDigest === EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1)) { + throw new Error('active applied state must commit a nonempty projection/table'); + } + return Object.freeze(normalizedState) as unknown as SystemRecordAppliedStatePresentV1; +} + +export function assertSystemRecordRootClaimSetV1( + value: unknown, +): asserts value is SystemRecordRootClaimSetV1 { + validateRootClaimSet(value); +} + +export function canonicalizeSystemRecordRootClaimSetV1( + value: SystemRecordRootClaimSetV1, +): Uint8Array { + return canonicalizeJsonBytes(validateRootClaimSet(value) as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + }); +} + +export function parseCanonicalSystemRecordRootClaimSetV1( + input: string | Uint8Array, +): SystemRecordRootClaimSetV1 { + return validateRootClaimSet(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, maxDepth: SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + })); +} + +export function computeSystemRecordRootClaimSetDigestV1( + value: SystemRecordRootClaimSetV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.rootClaimSet, + canonicalizeSystemRecordRootClaimSetV1(value), + ); +} + +function validateRootClaimSet(value: unknown): SystemRecordRootClaimSetV1 { + const claims = snapshotExactDataRecord( + value, + ['objectType', 'kind', 'networkId', 'stableKeyHash', 'currentRoot', 'historicalRoots'], + 'root claim set', + ); + if (claims.objectType !== 'system-record-root-claim-set' || claims.kind !== SYSTEM_RECORD_KIND_V1) { + throw new Error('root claim set tag is invalid'); + } + assertNetworkIdV1(claims.networkId); + assertCanonicalDigest(claims.stableKeyHash); + assertAgentRootV1(claims.currentRoot as string); + const historicalRoots = validateRootArray(claims.historicalRoots, claims.currentRoot as string); + return Object.freeze({ ...claims, historicalRoots }) as unknown as SystemRecordRootClaimSetV1; +} + +export function assertSystemRecordCapacityStateV1( + value: unknown, +): asserts value is SystemRecordCapacityStateV1 { + validateCapacityState(value); +} + +export function canonicalizeSystemRecordCapacityStateV1( + value: SystemRecordCapacityStateV1, +): Uint8Array { + return canonicalizeJsonBytes(validateCapacityState(value) as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + }); +} + +export function parseCanonicalSystemRecordCapacityStateV1( + input: string | Uint8Array, +): SystemRecordCapacityStateV1 { + return validateCapacityState(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, maxDepth: SYSTEM_RECORD_MAX_TUPLE_JSON_DEPTH, + })); +} + +export function computeSystemRecordCapacityStateDigestV1( + value: SystemRecordCapacityStateV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.capacityState, + canonicalizeSystemRecordCapacityStateV1(value), + ); +} + +function validateCapacityState(value: unknown): SystemRecordCapacityStateV1 { + const state = snapshotExactDataRecord( + value, + [ + 'objectType', 'kind', 'networkId', 'revision', 'liveRecordCount', + 'stateBytes', 'tableBytes', 'projectionBytes', 'projectionQuads', + ], + 'system-record capacity state', + ); + if (state.objectType !== 'system-record-capacity-state' || state.kind !== SYSTEM_RECORD_KIND_V1) { + throw new Error('capacity-state tag is invalid'); + } + assertNetworkIdV1(state.networkId); + assertCanonicalDecimalU64(state.revision); + boundedU64(state.liveRecordCount, SYSTEM_RECORD_MAX_INVENTORY_RECORDS, 'liveRecordCount'); + const stateBytes = boundedU64(state.stateBytes, SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES, 'stateBytes'); + const tableBytes = boundedU64(state.tableBytes, SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES, 'tableBytes'); + const projectionBytes = boundedU64( + state.projectionBytes, + SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES, + 'projectionBytes', + ); + boundedU64(state.projectionQuads, SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_QUADS, 'projectionQuads'); + if (stateBytes + tableBytes + projectionBytes > BigInt(SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES)) { + throw new Error('capacity-state combined bytes exceed the aggregate bound'); + } + return Object.freeze({ ...state }) as unknown as SystemRecordCapacityStateV1; +} + +export function assertSystemRecordMaterializationReceiptV1( + value: unknown, +): asserts value is SystemRecordMaterializationReceiptV1 { + validateReceipt(value); +} + +export function canonicalizeSystemRecordMaterializationReceiptV1( + value: SystemRecordMaterializationReceiptV1, +): Uint8Array { + return canonicalizeJsonBytes(validateReceipt(value) as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + }); +} + +export function parseCanonicalSystemRecordMaterializationReceiptV1( + input: string | Uint8Array, +): SystemRecordMaterializationReceiptV1 { + return validateReceipt(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES, + maxDepth: SYSTEM_RECORD_MAX_TUPLE_JSON_DEPTH, + })); +} + +export function computeSystemRecordMaterializationReceiptDigestV1( + value: SystemRecordMaterializationReceiptV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.materializationReceipt, + canonicalizeSystemRecordMaterializationReceiptV1(value), + ); +} + +function validateReceipt(value: unknown): SystemRecordMaterializationReceiptV1 { + const receipt = snapshotExactDataRecord( + value, + [ + 'objectType', 'kind', 'networkId', 'stableKeyHash', 'stateRevision', + 'appliedStateDigest', 'headDigest', 'materializationEpoch', + ], + 'materialization receipt', + ); + if (receipt.objectType !== 'system-record-materialization-receipt' + || receipt.kind !== SYSTEM_RECORD_KIND_V1) { + throw new Error('materialization receipt tag is invalid'); + } + assertNetworkIdV1(receipt.networkId); + assertCanonicalDigest(receipt.stableKeyHash); + assertCanonicalDecimalU64(receipt.stateRevision); + assertCanonicalDigest(receipt.appliedStateDigest); + assertCanonicalDigest(receipt.headDigest); + assertCanonicalDecimalU64(receipt.materializationEpoch); + return Object.freeze({ ...receipt }) as unknown as SystemRecordMaterializationReceiptV1; +} + +export function computeSystemRecordAccountedBytesV1( + ownedSubjectTableBytes: number, + projectionBytes: number, + pendingDeletionTableBytes = 0, +): number { + const values = [ownedSubjectTableBytes, projectionBytes, pendingDeletionTableBytes]; + if (values.some((value) => !Number.isSafeInteger(value) || value < 0)) { + throw new Error('accounted bytes must be non-negative safe integers'); + } + const total = SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES + + ownedSubjectTableBytes + + projectionBytes + + pendingDeletionTableBytes; + if (!Number.isSafeInteger(total)) throw new Error('accounted byte sum overflows safe integer range'); + if (total > SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES) { + throw new Error('record accounting exceeds the aggregate byte cap'); + } + return total; +} + +function validateTransitionLineage(value: unknown): readonly AgentProfileAppliedTransitionV1[] { + const lineage = snapshotDataArray(value, 'transition lineage', { + maxLength: Number(SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX), + }); + let expectedPrior = 0n; + return Object.freeze(lineage.map((candidate) => { + const entry = snapshotExactDataRecord( + candidate, + ['priorAuthoritySequence', 'nextAuthoritySequence', 'transitionDigest'], + 'applied transition lineage entry', + ); + const prior = parseCanonicalDecimalU64(entry.priorAuthoritySequence); + const next = parseCanonicalDecimalU64(entry.nextAuthoritySequence); + assertCanonicalDigest(entry.transitionDigest); + if (prior !== expectedPrior || next !== prior + 1n) { + throw new Error('transition lineage must be contiguous from authority sequence zero'); + } + expectedPrior = next; + return Object.freeze({ ...entry }) as unknown as AgentProfileAppliedTransitionV1; + })); +} + +function validateRootArray(value: unknown, currentRoot: string): readonly string[] { + const values = snapshotDataArray(value, 'historical roots', { + maxLength: SYSTEM_RECORD_MAX_ROOT_CLAIMS - 1, + }); + const seen = new Set([currentRoot]); + const roots = values.map((candidate) => { + assertAgentRootV1(candidate as string); + if (seen.has(candidate as string)) throw new Error('root claims must be duplicate-free'); + seen.add(candidate as string); + return candidate as string; + }); + return Object.freeze(roots); +} + +function validateDigestSlots(value: unknown): readonly Digest32V1[] { + const values = snapshotDataArray(value, 'conflict digest slots', { + maxLength: SYSTEM_RECORD_MAX_CONFLICT_DIGESTS, + }); + const slots = values.map((candidate, index) => { + assertCanonicalDigest(candidate); + if (index > 0 && (values[index - 1] as string) >= candidate) { + throw new Error('conflict digest slots must be sorted and duplicate-free'); + } + return candidate; + }); + return Object.freeze(slots) as readonly Digest32V1[]; +} + +function validateAllOrNoneGroup( + record: Readonly>, + keys: readonly string[], + label: string, +): void { + const present = keys.filter((key) => hasOwnDataProperty(record, key)).length; + if (present !== 0 && present !== keys.length) throw new Error(`${label} fields must be all present or all omitted`); +} + +function boundedU64(value: unknown, maximum: number, label: string): bigint { + const parsed = parseCanonicalDecimalU64(value, label); + if (parsed > BigInt(maximum)) throw new Error(`${label} exceeds its V1 bound`); + return parsed; +} diff --git a/packages/core/src/system-record-codec-primitives-v1.ts b/packages/core/src/system-record-codec-primitives-v1.ts new file mode 100644 index 0000000000..69075436b6 --- /dev/null +++ b/packages/core/src/system-record-codec-primitives-v1.ts @@ -0,0 +1,116 @@ +import { peerIdFromString } from '@libp2p/peer-id'; +import { sha256 } from '@noble/hashes/sha2.js'; + +import { SYSTEM_RECORD_MAX_PEER_ID_BYTES } from './system-record-limits-v1.js'; +import type { Digest32V1 } from './sync-wire-scalars.js'; + +const UTF8 = new TextEncoder(); +const BASE64URL = /^[A-Za-z0-9_-]+$/; +const TYPED_ARRAY_BYTE_LENGTH = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array.prototype) as object, + 'byteLength', +)?.get; + +export type SystemRecordPeerPublicKeyV1 = string & { readonly __peerPublicKeyV1: true }; + +export type SystemRecordObjectErrorCodeV1 = + | 'system-record-schema' + | 'system-record-scalar' + | 'system-record-binding' + | 'system-record-history' + | 'system-record-signature' + | 'system-record-order' + | 'system-record-limit' + | 'system-record-closure'; + +export class SystemRecordObjectErrorV1 extends Error { + constructor( + readonly code: SystemRecordObjectErrorCodeV1, + message: string, + options: ErrorOptions = {}, + ) { + super(`[${code}] ${message}`, options); + this.name = 'SystemRecordObjectErrorV1'; + } +} + +export function failSystemRecordObjectV1( + code: SystemRecordObjectErrorCodeV1, + message: string, + cause?: unknown, +): never { + throw new SystemRecordObjectErrorV1(code, message, cause === undefined ? {} : { cause }); +} + +export function decodeUnpaddedBase64UrlV1( + value: unknown, + expectedBytes: number, + label: string, +): Uint8Array { + const expectedCharacters = Math.ceil(expectedBytes * 4 / 3); + if (typeof value !== 'string' + || value.length !== expectedCharacters + || value.includes('=') + || !BASE64URL.test(value)) { + failSystemRecordObjectV1('system-record-scalar', `${label} must be unpadded base64url`); + } + const bytes = Uint8Array.from(Buffer.from(value, 'base64url')); + if (bytes.byteLength !== expectedBytes || Buffer.from(bytes).toString('base64url') !== value) { + failSystemRecordObjectV1( + 'system-record-scalar', + `${label} must canonically encode exactly ${expectedBytes} bytes`, + ); + } + return bytes; +} + +export function assertCanonicalSystemRecordPeerIdV1(value: unknown): asserts value is string { + if (typeof value !== 'string' + || value.length > SYSTEM_RECORD_MAX_PEER_ID_BYTES + || UTF8.encode(value).byteLength > SYSTEM_RECORD_MAX_PEER_ID_BYTES) { + failSystemRecordObjectV1('system-record-scalar', 'peerId is outside its byte bound'); + } + try { + if (peerIdFromString(value).toString() !== value) throw new Error('noncanonical'); + } catch (cause) { + failSystemRecordObjectV1('system-record-scalar', 'peerId is not canonical', cause); + } +} + +export function digestSystemRecordBytesV1(domain: string, bytes: Uint8Array): Digest32V1 { + const byteLength = systemRecordByteLengthV1(bytes, 'system-record digest bytes'); + const domainBytes = UTF8.encode(domain); + const input = new Uint8Array(domainBytes.byteLength + byteLength); + input.set(domainBytes); + Uint8Array.prototype.set.call(input, bytes, domainBytes.byteLength); + return (`0x${Buffer.from(sha256(input)).toString('hex')}`) as Digest32V1; +} + +/** Copy bounded bytes through typed-array intrinsics, ignoring subclass methods and species. */ +export function copyBoundedSystemRecordBytesV1( + value: unknown, + maxBytes: number, + label: string, +): Uint8Array { + if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) { + failSystemRecordObjectV1('system-record-scalar', `${label} must be bounded Uint8Array bytes`); + } + const byteLength = systemRecordByteLengthV1(value, label); + if (byteLength > maxBytes) { + failSystemRecordObjectV1('system-record-limit', `${label} exceeds ${maxBytes} bytes`); + } + const copy = new Uint8Array(byteLength); + Uint8Array.prototype.set.call(copy, value as Uint8Array); + return copy; +} + +function systemRecordByteLengthV1(value: unknown, label: string): number { + if (!(value instanceof Uint8Array) || TYPED_ARRAY_BYTE_LENGTH === undefined) { + failSystemRecordObjectV1('system-record-scalar', `${label} must be Uint8Array bytes`); + } + try { + return Reflect.apply(TYPED_ARRAY_BYTE_LENGTH, value, []) as number; + } catch (cause) { + failSystemRecordObjectV1('system-record-scalar', `${label} is not a valid Uint8Array`, cause); + } +} diff --git a/packages/core/src/system-record-inventory-v1.ts b/packages/core/src/system-record-inventory-v1.ts new file mode 100644 index 0000000000..3c4636959d --- /dev/null +++ b/packages/core/src/system-record-inventory-v1.ts @@ -0,0 +1,1775 @@ +import { publicKeyFromRaw } from '@libp2p/crypto/keys'; +import { peerIdFromPublicKey } from '@libp2p/peer-id'; +import { verifyAsync as verifyEd25519 } from '@noble/ed25519'; +import { sha256 } from '@noble/hashes/sha2.js'; + +import { + canonicalizeJsonBytes, + parseCanonicalJson, + type CanonicalJsonValue, +} from './canonical-json.js'; +import { + assertCanonicalSystemRecordPeerIdV1, + copyBoundedSystemRecordBytesV1, + decodeUnpaddedBase64UrlV1, + digestSystemRecordBytesV1, + type SystemRecordPeerPublicKeyV1, +} from './system-record-codec-primitives-v1.js'; +import { + SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX, + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_ED25519_PUBLIC_KEY_BYTES, + SYSTEM_RECORD_ED25519_SIGNATURE_BYTES, + SYSTEM_RECORD_INTERNAL_MAX_ENTRIES, + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + SYSTEM_RECORD_INTERNAL_TARGET_BYTES, + SYSTEM_RECORD_INVENTORY_ROW_VERSION, + SYSTEM_RECORD_KIND_V1, + SYSTEM_RECORD_LEAF_MAX_ROWS, + SYSTEM_RECORD_LEAF_MIN_ROWS, + SYSTEM_RECORD_LEAF_TARGET_BYTES, + SYSTEM_RECORD_MAX_EVIDENCE_ROW_BYTES, + SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + SYSTEM_RECORD_MAX_HEADER_BYTES, + SYSTEM_RECORD_MAX_INTERNAL_ENTRY_BYTES, + SYSTEM_RECORD_MAX_INVENTORY_INTERNAL_JSON_DEPTH, + SYSTEM_RECORD_MAX_INVENTORY_LEAVES, + SYSTEM_RECORD_MAX_INVENTORY_OBJECTS, + SYSTEM_RECORD_MAX_INVENTORY_RECORDS, + SYSTEM_RECORD_MAX_ORDINARY_ROW_BYTES, + SYSTEM_RECORD_MAX_PEER_ID_BYTES, + SYSTEM_RECORD_MAX_ROW_BYTES, + SYSTEM_RECORD_MAX_SLICE_REQUESTS, + SYSTEM_RECORD_MAX_SLICE_WIRE_BYTES, + SYSTEM_RECORD_MAX_TREE_HEIGHT, + SYSTEM_RECORD_MAX_TREE_UPDATE_BYTES, + SYSTEM_RECORD_MAX_TREE_UPDATE_OBJECTS, + SYSTEM_RECORD_OBJECT_CAPS_V1, + SYSTEM_RECORD_ROOT_MAX_ENTRIES, + SYSTEM_RECORD_ROOT_MIN_ENTRIES, + SYSTEM_RECORD_SIGNATURE_DOMAINS_V1, + SYSTEM_RECORD_SLICE_TIMEOUT_MS, +} from './system-record-limits-v1.js'; +import { assertNetworkIdV1, type NetworkIdV1 } from './sync-wire-identifiers.js'; +import { + assertCanonicalDecimalU64, + assertCanonicalDigest, + parseCanonicalDecimalU64, + type DecimalU64V1, + type Digest32V1, +} from './sync-wire-scalars.js'; +import { + hasOwnDataProperty, + snapshotDataArray, + snapshotDataRecord, + snapshotExactDataRecord, +} from './sync-wire-objects.js'; + +const UTF8 = new TextEncoder(); +const MAP_GET = Map.prototype.get; +const MAP_HAS = Map.prototype.has; +const ROW_FLAG_TOMBSTONE = 1; +const ROW_FLAG_QUARANTINED = 2; +const ROW_FLAG_CONFLICT_EVIDENCE = 4; +const ROW_ALLOWED_FLAGS = ROW_FLAG_TOMBSTONE | ROW_FLAG_QUARANTINED | ROW_FLAG_CONFLICT_EVIDENCE; + +export interface SystemRecordInventoryRowV1 { + readonly stableKeyHash: Digest32V1; + readonly peerId: string; + readonly authoritySequence: DecimalU64V1; + readonly version: DecimalU64V1; + readonly headDigest: Digest32V1; + readonly conflictEvidenceDigest?: Digest32V1; + readonly tombstone: boolean; + readonly quarantined: boolean; +} + +export interface SystemRecordInventoryLeafObjectV1 { + readonly objectType: 'inventory-leaf'; + readonly firstKeyHash?: Digest32V1; + readonly lastKeyHash?: Digest32V1; + readonly rows: readonly string[]; +} + +export interface SystemRecordInventoryInternalEntryV1 { + readonly separatorKeyHash: Digest32V1; + readonly childDigest: Digest32V1; + readonly childKind: 'inventory-internal' | 'inventory-leaf'; +} + +export interface SystemRecordInventoryInternalObjectV1 { + readonly objectType: 'inventory-internal'; + readonly firstKeyHash: Digest32V1; + readonly lastKeyHash: Digest32V1; + readonly entries: readonly SystemRecordInventoryInternalEntryV1[]; +} + +export type SystemRecordInventoryObjectV1 = + | SystemRecordInventoryLeafObjectV1 + | SystemRecordInventoryInternalObjectV1; + +export interface SystemRecordRootDescriptorObjectV1 { + readonly objectType: 'root-descriptor'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly epoch: DecimalU64V1; + readonly version: DecimalU64V1; + readonly priorRootDigest?: Digest32V1; + readonly treeRootDigest: Digest32V1; + readonly totalRows: DecimalU64V1; +} + +export interface SignedSystemRecordRootDescriptorEnvelopeV1 { + readonly object: SystemRecordRootDescriptorObjectV1; + readonly objectDigest: Digest32V1; + readonly providerPeerId: string; + readonly signatureSuite: 'ed25519-v1'; + readonly signature: string; +} + +export function computeSystemRecordStableKeyHashV1( + networkId: NetworkIdV1, + peerId: string, +): Digest32V1 { + assertNetworkIdV1(networkId); + assertCanonicalSystemRecordPeerIdV1(peerId); + const network = UTF8.encode(networkId); + const peer = UTF8.encode(peerId); + if (peer.byteLength < 1 || peer.byteLength > SYSTEM_RECORD_MAX_PEER_ID_BYTES) { + throw new Error(`peerId exceeds ${SYSTEM_RECORD_MAX_PEER_ID_BYTES} bytes`); + } + const input = new Uint8Array(network.byteLength + 1 + peer.byteLength); + input.set(network); + input[network.byteLength] = 0; + input.set(peer, network.byteLength + 1); + return (`0x${Buffer.from(sha256(input)).toString('hex')}`) as Digest32V1; +} + +export function encodeSystemRecordInventoryRowV1( + networkId: NetworkIdV1, + row: SystemRecordInventoryRowV1, +): Uint8Array { + const validated = validateInventoryRow(row, networkId); + const peer = UTF8.encode(validated.peerId); + const hasEvidence = validated.conflictEvidenceDigest !== undefined; + const bytes = new Uint8Array(1 + 32 + 2 + peer.byteLength + 8 + 8 + 32 + (hasEvidence ? 32 : 0) + 1); + let offset = 0; + bytes[offset++] = SYSTEM_RECORD_INVENTORY_ROW_VERSION; + bytes.set(hexDigestBytes(validated.stableKeyHash), offset); offset += 32; + bytes[offset++] = peer.byteLength >>> 8; + bytes[offset++] = peer.byteLength & 0xff; + bytes.set(peer, offset); offset += peer.byteLength; + writeU64(bytes, offset, parseCanonicalDecimalU64(validated.authoritySequence)); offset += 8; + writeU64(bytes, offset, parseCanonicalDecimalU64(validated.version)); offset += 8; + bytes.set(hexDigestBytes(validated.headDigest), offset); offset += 32; + if (hasEvidence) { + bytes.set(hexDigestBytes(validated.conflictEvidenceDigest!), offset); offset += 32; + } + bytes[offset] = (validated.tombstone ? ROW_FLAG_TOMBSTONE : 0) + | (validated.quarantined ? ROW_FLAG_QUARANTINED : 0) + | (hasEvidence ? ROW_FLAG_CONFLICT_EVIDENCE : 0); + const cap = hasEvidence ? SYSTEM_RECORD_MAX_EVIDENCE_ROW_BYTES : SYSTEM_RECORD_MAX_ORDINARY_ROW_BYTES; + if (bytes.byteLength > cap || bytes.byteLength > SYSTEM_RECORD_MAX_ROW_BYTES) { + throw new Error('encoded inventory row exceeds its V1 cap'); + } + return bytes; +} + +export function systemRecordInventoryRowMaxEncodedBytesV1( + peerIdBytes = SYSTEM_RECORD_MAX_PEER_ID_BYTES, + withConflictEvidence = false, +): number { + if (!Number.isInteger(peerIdBytes) || peerIdBytes < 1 || peerIdBytes > SYSTEM_RECORD_MAX_PEER_ID_BYTES) { + throw new Error(`peerIdBytes must be in 1..${SYSTEM_RECORD_MAX_PEER_ID_BYTES}`); + } + return 1 + 32 + 2 + peerIdBytes + 8 + 8 + 32 + (withConflictEvidence ? 32 : 0) + 1; +} + +export function decodeSystemRecordInventoryRowV1( + networkId: NetworkIdV1, + bytes: Uint8Array, +): SystemRecordInventoryRowV1 { + assertNetworkIdV1(networkId); + const encoded = copyBoundedSystemRecordBytesV1( + bytes, + SYSTEM_RECORD_MAX_ROW_BYTES, + 'inventory row bytes', + ); + if (encoded.byteLength < 84) { + throw new Error('inventory row has an invalid encoded length'); + } + let offset = 0; + if (encoded[offset++] !== SYSTEM_RECORD_INVENTORY_ROW_VERSION) throw new Error('inventory row version is invalid'); + const stableKeyHash = bytesDigest(encoded.subarray(offset, offset + 32)); offset += 32; + const peerLength = (encoded[offset++] << 8) | encoded[offset++]; + const hasEvidence = encoded.byteLength === 1 + 32 + 2 + peerLength + 8 + 8 + 32 + 32 + 1; + const ordinary = encoded.byteLength === 1 + 32 + 2 + peerLength + 8 + 8 + 32 + 1; + if (peerLength < 1 || peerLength > SYSTEM_RECORD_MAX_PEER_ID_BYTES || (!ordinary && !hasEvidence)) { + throw new Error('inventory row peer/evidence length is invalid'); + } + const peerId = new TextDecoder('utf-8', { fatal: true }).decode(encoded.subarray(offset, offset + peerLength)); + offset += peerLength; + const authoritySequence = readU64(encoded, offset).toString() as DecimalU64V1; offset += 8; + const version = readU64(encoded, offset).toString() as DecimalU64V1; offset += 8; + const headDigest = bytesDigest(encoded.subarray(offset, offset + 32)); offset += 32; + const conflictEvidenceDigest = hasEvidence + ? bytesDigest(encoded.subarray(offset, offset + 32)) + : undefined; + if (hasEvidence) offset += 32; + const flags = encoded[offset]; + if ((flags & ~ROW_ALLOWED_FLAGS) !== 0 + || Boolean(flags & ROW_FLAG_CONFLICT_EVIDENCE) !== hasEvidence) { + throw new Error('inventory row flags are invalid'); + } + const row: SystemRecordInventoryRowV1 = { + stableKeyHash, + peerId, + authoritySequence, + version, + headDigest, + ...(conflictEvidenceDigest === undefined ? {} : { conflictEvidenceDigest }), + tombstone: Boolean(flags & ROW_FLAG_TOMBSTONE), + quarantined: Boolean(flags & ROW_FLAG_QUARANTINED), + }; + return validateInventoryRow(row, networkId); +} + +export function encodeInventoryRowBase64UrlV1( + networkId: NetworkIdV1, + row: SystemRecordInventoryRowV1, +): string { + return Buffer.from(encodeSystemRecordInventoryRowV1(networkId, row)).toString('base64url'); +} + +export function decodeInventoryRowBase64UrlV1(networkId: NetworkIdV1, value: string): SystemRecordInventoryRowV1 { + if (typeof value !== 'string' + || value.length > Math.ceil(SYSTEM_RECORD_MAX_ROW_BYTES * 4 / 3) + || value.includes('=') + || !/^[A-Za-z0-9_-]+$/.test(value)) { + throw new Error('inventory row must be unpadded base64url'); + } + const bytes = Uint8Array.from(Buffer.from(value, 'base64url')); + if (Buffer.from(bytes).toString('base64url') !== value) throw new Error('inventory row base64url is noncanonical'); + return decodeSystemRecordInventoryRowV1(networkId, bytes); +} + +export function assertSystemRecordInventoryLeafObjectV1( + value: unknown, + networkId: NetworkIdV1, + root = false, +): asserts value is SystemRecordInventoryLeafObjectV1 { + validateLeaf(value, networkId, root); +} + +export function canonicalizeSystemRecordInventoryLeafObjectV1( + value: SystemRecordInventoryLeafObjectV1, + networkId: NetworkIdV1, + root = false, +): Uint8Array { + const validated = validateLeaf(value, networkId, root); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-leaf'], + }); +} + +export function parseCanonicalSystemRecordInventoryLeafObjectV1( + input: string | Uint8Array, + networkId: NetworkIdV1, + root = false, +): SystemRecordInventoryLeafObjectV1 { + return validateLeaf(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-leaf'], maxDepth: SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + }), networkId, root); +} + +export function computeSystemRecordInventoryLeafDigestV1( + value: SystemRecordInventoryLeafObjectV1, + networkId: NetworkIdV1, + root = false, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryLeaf, + canonicalizeSystemRecordInventoryLeafObjectV1(value, networkId, root), + ); +} + +function validateLeaf( + value: unknown, + networkId: NetworkIdV1, + root: boolean, +): SystemRecordInventoryLeafObjectV1 { + assertNetworkIdV1(networkId); + const probe = snapshotDataRecord(value, 'inventory leaf', { rejectNullValues: true }); + const encodedRows = snapshotDataArray(probe.rows, 'inventory leaf rows', { + maxLength: SYSTEM_RECORD_LEAF_MAX_ROWS, + }); + const empty = encodedRows.length === 0; + const expected = empty + ? ['objectType', 'rows'] as const + : ['objectType', 'firstKeyHash', 'lastKeyHash', 'rows'] as const; + const leaf = snapshotExactDataRecord(probe, expected, 'inventory leaf'); + if (leaf.objectType !== 'inventory-leaf') throw new Error('inventory leaf tag is invalid'); + const minimum = root ? 0 : SYSTEM_RECORD_LEAF_MIN_ROWS; + if (encodedRows.length < minimum || encodedRows.length > SYSTEM_RECORD_LEAF_MAX_ROWS) { + throw new Error('inventory leaf occupancy is outside its V1 bound'); + } + const rows = encodedRows.map((encoded) => decodeInventoryRowBase64UrlV1(networkId, encoded as string)); + for (let index = 1; index < rows.length; index += 1) { + if (compareRows(rows[index - 1], rows[index]) >= 0) { + throw new Error('inventory leaf rows must be sorted and stable-key unique'); + } + } + if (!empty) { + assertCanonicalDigest(leaf.firstKeyHash); + assertCanonicalDigest(leaf.lastKeyHash); + if (leaf.firstKeyHash !== rows[0].stableKeyHash + || leaf.lastKeyHash !== rows[rows.length - 1].stableKeyHash) { + throw new Error('inventory leaf key range is not derived from its rows'); + } + } + const validated = empty + ? { objectType: 'inventory-leaf' as const, rows: encodedRows as readonly string[] } + : { + objectType: 'inventory-leaf' as const, + firstKeyHash: leaf.firstKeyHash as Digest32V1, + lastKeyHash: leaf.lastKeyHash as Digest32V1, + rows: encodedRows as readonly string[], + }; + const bytes = canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-leaf'], + }); + if (bytes.byteLength > SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-leaf']) { + throw new Error('inventory leaf exceeds its V1 byte cap'); + } + return Object.freeze(validated); +} + +export function assertSystemRecordInventoryInternalObjectV1( + value: unknown, + root = false, +): asserts value is SystemRecordInventoryInternalObjectV1 { + validateInternal(value, root); +} + +export function canonicalizeSystemRecordInventoryInternalObjectV1( + value: SystemRecordInventoryInternalObjectV1, + root = false, +): Uint8Array { + const validated = validateInternal(value, root); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-internal'], + }); +} + +export function parseCanonicalSystemRecordInventoryInternalObjectV1( + input: string | Uint8Array, + root = false, +): SystemRecordInventoryInternalObjectV1 { + return validateInternal(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-internal'], maxDepth: SYSTEM_RECORD_MAX_INVENTORY_INTERNAL_JSON_DEPTH, + }), root); +} + +export function computeSystemRecordInventoryInternalDigestV1( + value: SystemRecordInventoryInternalObjectV1, + root = false, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryInternal, + canonicalizeSystemRecordInventoryInternalObjectV1(value, root), + ); +} + +function validateInternal(value: unknown, root: boolean): SystemRecordInventoryInternalObjectV1 { + const internal = snapshotExactDataRecord( + value, + ['objectType', 'firstKeyHash', 'lastKeyHash', 'entries'], + 'inventory internal node', + ); + if (internal.objectType !== 'inventory-internal') throw new Error('inventory internal node tag is invalid'); + assertCanonicalDigest(internal.firstKeyHash); + assertCanonicalDigest(internal.lastKeyHash); + const min = root ? SYSTEM_RECORD_ROOT_MIN_ENTRIES : SYSTEM_RECORD_INTERNAL_MIN_ENTRIES; + const max = root ? SYSTEM_RECORD_ROOT_MAX_ENTRIES : SYSTEM_RECORD_INTERNAL_MAX_ENTRIES; + const internalEntries = snapshotDataArray(internal.entries, 'inventory internal entries', { + minLength: min, + maxLength: max, + }); + let previousSeparator: string | undefined; + let childKind: SystemRecordInventoryInternalEntryV1['childKind'] | undefined; + const entries = internalEntries.map((candidate, index) => { + const entry = snapshotExactDataRecord( + candidate, + ['separatorKeyHash', 'childDigest', 'childKind'], + `inventory internal entry ${index}`, + ); + assertCanonicalDigest(entry.separatorKeyHash); + assertCanonicalDigest(entry.childDigest); + if (entry.childKind !== 'inventory-internal' && entry.childKind !== 'inventory-leaf') { + throw new Error('inventory internal child kind is invalid'); + } + if (childKind !== undefined && entry.childKind !== childKind) { + throw new Error('inventory internal node must not mix leaf and internal children'); + } + childKind = entry.childKind; + const encoded = canonicalizeJsonBytes(entry as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_INTERNAL_ENTRY_BYTES, + }); + if (encoded.byteLength > SYSTEM_RECORD_MAX_INTERNAL_ENTRY_BYTES) { + throw new Error('inventory internal entry exceeds its V1 cap'); + } + if (previousSeparator !== undefined && previousSeparator >= (entry.separatorKeyHash as string)) { + throw new Error('inventory internal separators must be sorted and unique'); + } + previousSeparator = entry.separatorKeyHash as string; + return Object.freeze({ ...entry }) as unknown as SystemRecordInventoryInternalEntryV1; + }); + if (internal.firstKeyHash !== entries[0].separatorKeyHash + || internal.lastKeyHash < entries[entries.length - 1].separatorKeyHash) { + throw new Error('inventory internal key range is not derived from its entries'); + } + const validated = Object.freeze({ + objectType: 'inventory-internal' as const, + firstKeyHash: internal.firstKeyHash as Digest32V1, + lastKeyHash: internal.lastKeyHash as Digest32V1, + entries: Object.freeze(entries), + }); + canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['inventory-internal'], + }); + return validated; +} + +export function assertSystemRecordRootDescriptorObjectV1( + value: unknown, +): asserts value is SystemRecordRootDescriptorObjectV1 { + validateRootDescriptor(value); +} + +export function canonicalizeSystemRecordRootDescriptorObjectV1( + value: SystemRecordRootDescriptorObjectV1, +): Uint8Array { + return canonicalizeJsonBytes( + validateRootDescriptor(value) as unknown as CanonicalJsonValue, + { maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['root-descriptor'] }, + ); +} + +export function parseCanonicalSystemRecordRootDescriptorObjectV1( + input: string | Uint8Array, +): SystemRecordRootDescriptorObjectV1 { + return validateRootDescriptor(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['root-descriptor'], maxDepth: SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + })); +} + +export function computeSystemRecordRootDescriptorDigestV1( + value: SystemRecordRootDescriptorObjectV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.rootDescriptor, + canonicalizeSystemRecordRootDescriptorObjectV1(value), + ); +} + +function validateRootDescriptor(value: unknown): SystemRecordRootDescriptorObjectV1 { + const probe = snapshotDataRecord(value, 'root descriptor', { rejectNullValues: true }); + const hasPrior = hasOwnDataProperty(probe, 'priorRootDigest'); + const descriptor = snapshotExactDataRecord( + probe, + [ + 'objectType', 'kind', 'networkId', 'epoch', 'version', + ...(hasPrior ? ['priorRootDigest'] : []), + 'treeRootDigest', 'totalRows', + ], + 'root descriptor', + ); + if (descriptor.objectType !== 'root-descriptor' || descriptor.kind !== SYSTEM_RECORD_KIND_V1) { + throw new Error('root descriptor tag is invalid'); + } + assertNetworkIdV1(descriptor.networkId); + assertCanonicalDecimalU64(descriptor.epoch); + const version = parseCanonicalDecimalU64(descriptor.version); + if ((version === 0n) === hasPrior) { + throw new Error('priorRootDigest is omitted only for root version zero'); + } + if (hasPrior) assertCanonicalDigest(descriptor.priorRootDigest); + assertCanonicalDigest(descriptor.treeRootDigest); + const totalRows = parseCanonicalDecimalU64(descriptor.totalRows); + if (totalRows > BigInt(SYSTEM_RECORD_MAX_INVENTORY_RECORDS)) { + throw new Error('root descriptor exceeds the incoming inventory row cap'); + } + return Object.freeze({ ...descriptor }) as unknown as SystemRecordRootDescriptorObjectV1; +} + +export function assertSignedSystemRecordRootDescriptorEnvelopeV1( + value: unknown, +): asserts value is SignedSystemRecordRootDescriptorEnvelopeV1 { + validateSignedRootDescriptor(value); +} + +export function canonicalizeSignedSystemRecordRootDescriptorEnvelopeV1( + value: SignedSystemRecordRootDescriptorEnvelopeV1, +): Uint8Array { + return canonicalizeJsonBytes( + validateSignedRootDescriptor(value) as unknown as CanonicalJsonValue, + { maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['root-descriptor'] }, + ); +} + +export function parseCanonicalSignedSystemRecordRootDescriptorEnvelopeV1( + input: string | Uint8Array, +): SignedSystemRecordRootDescriptorEnvelopeV1 { + return validateSignedRootDescriptor(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['root-descriptor'], + maxDepth: SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + })); +} + +export function buildSystemRecordProviderSignatureMessageV1( + descriptor: SystemRecordRootDescriptorObjectV1, + descriptorObjectDigest: Digest32V1, + providerPeerId: string, +): Uint8Array { + const validatedDescriptor = validateRootDescriptor(descriptor); + assertCanonicalDigest(descriptorObjectDigest); + const tuple: CanonicalJsonValue = [ + validatedDescriptor.kind, + validatedDescriptor.networkId, + providerPeerId, + descriptorObjectDigest, + ]; + return concatBytes( + UTF8.encode(SYSTEM_RECORD_SIGNATURE_DOMAINS_V1.provider), + canonicalizeJsonBytes(tuple), + ); +} + +export async function verifySignedSystemRecordRootDescriptorEnvelopeV1( + envelope: SignedSystemRecordRootDescriptorEnvelopeV1, + providerPeerPublicKey: SystemRecordPeerPublicKeyV1, +): Promise { + const validated = validateSignedRootDescriptor(envelope); + const keyBytes = decodeUnpaddedBase64UrlV1( + providerPeerPublicKey, + SYSTEM_RECORD_ED25519_PUBLIC_KEY_BYTES, + 'providerPeerPublicKey', + ); + if (peerIdFromPublicKey(publicKeyFromRaw(keyBytes)).toString() !== validated.providerPeerId) { + return false; + } + const signature = decodeUnpaddedBase64UrlV1( + validated.signature, + SYSTEM_RECORD_ED25519_SIGNATURE_BYTES, + 'provider signature', + ); + return verifyEd25519( + signature, + buildSystemRecordProviderSignatureMessageV1( + validated.object, + validated.objectDigest, + validated.providerPeerId, + ), + keyBytes, + ); +} + +function validateSignedRootDescriptor(value: unknown): SignedSystemRecordRootDescriptorEnvelopeV1 { + const envelope = snapshotExactDataRecord( + value, + ['object', 'objectDigest', 'providerPeerId', 'signatureSuite', 'signature'], + 'signed root descriptor envelope', + ); + const object = validateRootDescriptor(envelope.object); + assertCanonicalDigest(envelope.objectDigest); + if (envelope.objectDigest !== computeSystemRecordRootDescriptorDigestV1(object)) { + throw new Error('root descriptor objectDigest is invalid'); + } + try { + assertCanonicalSystemRecordPeerIdV1(envelope.providerPeerId); + } catch { + throw new Error('providerPeerId is not canonical'); + } + if (envelope.signatureSuite !== 'ed25519-v1') throw new Error('provider signature suite is invalid'); + decodeUnpaddedBase64UrlV1( + envelope.signature, + SYSTEM_RECORD_ED25519_SIGNATURE_BYTES, + 'provider signature', + ); + return Object.freeze({ ...envelope, object }) as unknown as SignedSystemRecordRootDescriptorEnvelopeV1; +} + +export interface ValidatedSystemRecordInventoryTreeV1 { + readonly totalRows: number; + readonly leaves: number; + readonly height: number; + readonly objectDigests: ReadonlySet; +} + +export interface SystemRecordInventoryLoadedObjectV1 { + readonly outcome: 'ok'; + readonly objectKind: 'inventory-internal' | 'inventory-leaf'; + readonly canonicalBytes: Uint8Array; + /** Actual prefix + header + payload bytes consumed from the response stream. */ + readonly wireBytes: number; +} + +export interface SystemRecordInventoryRejectedLoadV1 { + readonly outcome: 'rejected'; + readonly wireBytes: number; + readonly rejection: 'not-found' | 'invalid-response' | 'busy' | 'transport'; +} + +export interface SystemRecordInventoryTraversalSliceV1 { + readonly signal?: AbortSignal; + readonly maxRequests: number; + readonly maxWireBytes: number; + readonly deadlineMs: number; + readonly nowMs?: () => number; +} + +export interface SystemRecordInventoryTraversalSliceResultV1 { + readonly status: 'paused' | 'complete' | 'rejected'; + readonly requests: number; + readonly wireBytes: number; + readonly rejection?: SystemRecordInventoryRejectedLoadV1['rejection']; + readonly result?: ValidatedSystemRecordInventoryTreeV1; +} + +export interface SystemRecordInventoryTraversalV1 { + /** Advance one bounded slice. Concurrent calls are rejected. */ + advance( + load: ( + digest: Digest32V1, + expectedKind: 'inventory-internal' | 'inventory-leaf' | undefined, + signal?: AbortSignal, + ) => Promise, + slice: SystemRecordInventoryTraversalSliceV1, + ): Promise; +} + +interface SystemRecordInventoryTraversalWorkV1 { + readonly digest: Digest32V1; + readonly depth: number; + readonly expectedKind?: 'inventory-internal' | 'inventory-leaf'; + readonly expectedFirst?: Digest32V1; + readonly upperExclusive?: Digest32V1; + readonly expectedLast?: Digest32V1; +} + +/** Create one opaque pinned traversal; callers must explicitly admit every bounded slice. */ +export function createSystemRecordInventoryTraversalV1( + descriptor: SystemRecordRootDescriptorObjectV1, +): SystemRecordInventoryTraversalV1 { + const pinned = validateRootDescriptor(descriptor); + const expectedRows = Number(parseCanonicalDecimalU64(pinned.totalRows)); + const seen = new Set(); + const pending: SystemRecordInventoryTraversalWorkV1[] = [{ + digest: pinned.treeRootDigest, + depth: 1, + }]; + let rows = 0; + let leaves = 0; + let maximumDepth = 0; + let leafDepth: number | undefined; + let advancing = false; + let completed: Readonly<{ + totalRows: number; + leaves: number; + height: number; + objectDigests: readonly string[]; + }> | undefined; + + return Object.freeze({ advance }); + + async function advance( + load: ( + digest: Digest32V1, + expectedKind: 'inventory-internal' | 'inventory-leaf' | undefined, + signal?: AbortSignal, + ) => Promise, + slice: SystemRecordInventoryTraversalSliceV1, + ): Promise { + if (advancing) throw new Error('inventory traversal already has an active slice'); + if (completed !== undefined) { + return Object.freeze({ status: 'complete', requests: 0, wireBytes: 0, result: completedResult() }); + } + advancing = true; + try { + // Pin the admitted budget before the first await. Callers commonly reuse mutable + // scheduler state; re-reading it after load() would let one slice grow in flight. + const signal = slice.signal; + const maxRequests = slice.maxRequests; + const maxWireBytes = slice.maxWireBytes; + const deadlineMs = slice.deadlineMs; + const now = slice.nowMs ?? Date.now; + if (typeof now !== 'function') throw new Error('inventory traversal slice budget is invalid'); + const readNow = (): number => { + const current = now(); + if (!Number.isSafeInteger(current) || current < 0) { + throw new Error('inventory traversal clock is invalid'); + } + return current; + }; + const admittedAtMs = readNow(); + if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 + || maxRequests > SYSTEM_RECORD_MAX_SLICE_REQUESTS + || !Number.isSafeInteger(maxWireBytes) + || maxWireBytes < framedObjectMaximum('inventory-leaf') + || maxWireBytes > SYSTEM_RECORD_MAX_SLICE_WIRE_BYTES + || !Number.isSafeInteger(deadlineMs) + || !Number.isSafeInteger(admittedAtMs) + || admittedAtMs < 0 + || deadlineMs > admittedAtMs + SYSTEM_RECORD_SLICE_TIMEOUT_MS) { + throw new Error('inventory traversal slice budget is invalid'); + } + let requests = 0; + let wireBytes = 0; + while (pending.length > 0) { + abortIfNeeded(signal); + if (readNow() >= deadlineMs) break; + const work = pending[pending.length - 1]; + const maximumNextBytes = framedObjectMaximum( + work.expectedKind === 'inventory-internal' ? 'inventory-internal' : 'inventory-leaf', + ); + if (requests >= maxRequests || wireBytes + maximumNextBytes > maxWireBytes) break; + if (work.depth > SYSTEM_RECORD_MAX_TREE_HEIGHT) throw new Error('inventory tree exceeds height three'); + if (seen.has(work.digest)) throw new Error('inventory tree must not contain a cycle or duplicate path'); + if (seen.size >= SYSTEM_RECORD_MAX_INVENTORY_OBJECTS) { + throw new Error('inventory traversal exceeds its object budget'); + } + const remainingMs = deadlineMs - readNow(); + if (remainingMs <= 0) break; + const loaded = await loadInventoryObjectWithinDeadlineV1( + (loadSignal) => load(work.digest, work.expectedKind, loadSignal), + signal, + remainingMs, + ); + requests += 1; + abortIfNeeded(signal); + if (readNow() >= deadlineMs) throw new Error('inventory traversal slice deadline expired during load'); + if (loaded === undefined) throw new Error(`inventory tree is missing ${work.digest}`); + const probe = snapshotDataRecord(loaded, 'inventory loader result', { rejectNullValues: true }); + if (probe.outcome === 'rejected') { + const rejected = snapshotExactDataRecord( + probe, + ['outcome', 'wireBytes', 'rejection'], + 'rejected inventory loader result', + ); + if (!Number.isSafeInteger(rejected.wireBytes) + || (rejected.wireBytes as number) < 6 + || (rejected.wireBytes as number) > maximumNextBytes + || wireBytes + (rejected.wireBytes as number) > maxWireBytes) { + throw new Error('inventory loader returned invalid actual wire accounting'); + } + wireBytes += rejected.wireBytes as number; + if (rejected.rejection !== 'not-found' + && rejected.rejection !== 'invalid-response' + && rejected.rejection !== 'busy' + && rejected.rejection !== 'transport') { + throw new Error('inventory loader returned an invalid rejection'); + } + return Object.freeze({ + status: 'rejected', requests, wireBytes, rejection: rejected.rejection, + }) as SystemRecordInventoryTraversalSliceResultV1; + } + if (probe.outcome !== 'ok') throw new Error('inventory loader returned an invalid outcome'); + const artifact = snapshotExactDataRecord( + probe, + ['outcome', 'objectKind', 'canonicalBytes', 'wireBytes'], + 'successful inventory loader result', + ); + if (!Number.isSafeInteger(artifact.wireBytes) + || (artifact.wireBytes as number) < 6 + || (artifact.wireBytes as number) > maximumNextBytes + || wireBytes + (artifact.wireBytes as number) > maxWireBytes) { + throw new Error('inventory loader returned invalid actual wire accounting'); + } + wireBytes += artifact.wireBytes as number; + if (artifact.objectKind !== 'inventory-leaf' && artifact.objectKind !== 'inventory-internal') { + throw new Error('inventory loader returned an invalid object kind'); + } + const canonicalBytes = copyBoundedSystemRecordBytesV1( + artifact.canonicalBytes, + SYSTEM_RECORD_OBJECT_CAPS_V1[artifact.objectKind], + 'inventory loader canonical bytes', + ); + if ((artifact.wireBytes as number) < 6 + canonicalBytes.byteLength) { + throw new Error('inventory loader returned an over-cap object'); + } + if (work.expectedKind !== undefined && artifact.objectKind !== work.expectedKind) { + throw new Error('inventory child kind mismatch'); + } + const root = work.depth === 1; + const object = artifact.objectKind === 'inventory-leaf' + ? parseCanonicalSystemRecordInventoryLeafObjectV1(canonicalBytes, pinned.networkId, root) + : parseCanonicalSystemRecordInventoryInternalObjectV1(canonicalBytes, root); + const actualDigest = artifact.objectKind === 'inventory-leaf' + ? computeSystemRecordInventoryLeafDigestV1(object as SystemRecordInventoryLeafObjectV1, pinned.networkId, root) + : computeSystemRecordInventoryInternalDigestV1(object as SystemRecordInventoryInternalObjectV1, root); + if (actualDigest !== work.digest) throw new Error('inventory object digest mismatch'); + const first = object.firstKeyHash; + const last = object.lastKeyHash; + if (work.expectedFirst !== undefined && first !== work.expectedFirst) { + throw new Error('inventory child lower range mismatch'); + } + if (work.upperExclusive !== undefined && last !== undefined && last >= work.upperExclusive) { + throw new Error('inventory child range overlaps its next sibling'); + } + if (work.expectedLast !== undefined && last !== work.expectedLast) { + throw new Error('inventory final child range mismatch'); + } + pending.pop(); + seen.add(work.digest); + maximumDepth = Math.max(maximumDepth, work.depth); + if (artifact.objectKind === 'inventory-leaf') { + const leaf = object as SystemRecordInventoryLeafObjectV1; + if (leafDepth !== undefined && work.depth !== leafDepth) { + throw new Error('inventory tree leaves must all have the same depth'); + } + leafDepth = work.depth; + leaves += 1; + rows += leaf.rows.length; + if (leaves > SYSTEM_RECORD_MAX_INVENTORY_LEAVES + || rows > SYSTEM_RECORD_MAX_INVENTORY_RECORDS + || rows > expectedRows) { + throw new Error('inventory traversal exceeds its leaf/row bound'); + } + } else { + if (work.depth === SYSTEM_RECORD_MAX_TREE_HEIGHT) { + throw new Error('internal node appears below height bound'); + } + const internal = object as SystemRecordInventoryInternalObjectV1; + for (let index = internal.entries.length - 1; index >= 0; index -= 1) { + const entry = internal.entries[index]; + pending.push({ + digest: entry.childDigest, + depth: work.depth + 1, + expectedKind: entry.childKind, + expectedFirst: entry.separatorKeyHash, + ...(index === internal.entries.length - 1 ? {} : { + upperExclusive: internal.entries[index + 1].separatorKeyHash, + }), + ...(index === internal.entries.length - 1 ? { expectedLast: internal.lastKeyHash } : {}), + }); + } + } + } + if (pending.length !== 0) { + return Object.freeze({ status: 'paused', requests, wireBytes }); + } + if (rows !== expectedRows) throw new Error('inventory traversal total does not match descriptor.totalRows'); + if (rows === 0 && leaves !== 1) throw new Error('empty inventory must use one root leaf'); + completed = Object.freeze({ + totalRows: rows, + leaves, + height: maximumDepth, + objectDigests: Object.freeze([...seen]), + }); + return Object.freeze({ status: 'complete', requests, wireBytes, result: completedResult() }); + } finally { + advancing = false; + } + } + + function completedResult(): ValidatedSystemRecordInventoryTreeV1 { + if (completed === undefined) throw new Error('inventory traversal is not complete'); + return Object.freeze({ + totalRows: completed.totalRows, + leaves: completed.leaves, + height: completed.height, + objectDigests: new Set(completed.objectDigests), + }); + } +} + +function framedObjectMaximum(objectKind: 'inventory-internal' | 'inventory-leaf'): number { + return 4 + SYSTEM_RECORD_MAX_HEADER_BYTES + SYSTEM_RECORD_OBJECT_CAPS_V1[objectKind]; +} + +function abortIfNeeded(signal?: AbortSignal): void { + if (signal?.aborted) throw signal.reason ?? new Error('inventory traversal aborted'); +} + +async function loadInventoryObjectWithinDeadlineV1( + load: (signal: AbortSignal) => Promise, + admittedSignal: AbortSignal | undefined, + remainingMs: number, +): Promise { + if (!Number.isSafeInteger(remainingMs) || remainingMs < 1 + || remainingMs > SYSTEM_RECORD_SLICE_TIMEOUT_MS) { + throw new Error('inventory traversal load deadline is invalid'); + } + const controller = new AbortController(); + let timeout: ReturnType | undefined; + let rejectBoundary: ((reason?: unknown) => void) | undefined; + const boundary = new Promise((_resolve, reject) => { + rejectBoundary = reject; + timeout = setTimeout(() => { + const reason = new Error('inventory traversal slice deadline expired during load'); + controller.abort(reason); + reject(reason); + }, remainingMs); + }); + const onAbort = (): void => { + let reason: unknown; + try { + reason = admittedSignal?.reason; + } catch { + reason = undefined; + } + reason ??= new Error('inventory traversal aborted'); + controller.abort(reason); + rejectBoundary?.(reason); + }; + try { + if (admittedSignal?.aborted) onAbort(); + else admittedSignal?.addEventListener('abort', onAbort, { once: true }); + return await Promise.race([ + Promise.resolve().then(() => { + abortIfNeeded(controller.signal); + return load(controller.signal); + }), + boundary, + ]); + } finally { + if (timeout !== undefined) clearTimeout(timeout); + admittedSignal?.removeEventListener('abort', onAbort); + } +} + +/** Pick the deterministic split nearest half encoded bytes while preserving minima. */ +export function chooseSystemRecordByteAwareSplitIndexV1( + encodedEntryBytes: readonly number[], + minimumLeft: number, + minimumRight: number, +): number { + const lengths = snapshotDataArray(encodedEntryBytes, 'split byte lengths', { + maxLength: SYSTEM_RECORD_LEAF_MAX_ROWS + 1, + }) as readonly number[]; + if (!Number.isInteger(minimumLeft) || !Number.isInteger(minimumRight) + || minimumLeft < 1 || minimumRight < 1 + || lengths.length < minimumLeft + minimumRight) { + throw new Error('split cardinality cannot preserve occupancy'); + } + const maximumEntryBytes = Math.max( + SYSTEM_RECORD_MAX_ROW_BYTES, + SYSTEM_RECORD_MAX_INTERNAL_ENTRY_BYTES, + ); + if (lengths.some((value) => !Number.isSafeInteger(value) + || value <= 0 + || value > maximumEntryBytes)) { + throw new Error('split byte lengths must be positive safe integers'); + } + let total = 0; + for (const value of lengths) { + total += value; + if (!Number.isSafeInteger(total)) throw new Error('split byte-length total is unsafe'); + } + let prefix = 0; + for (let index = 0; index < minimumLeft; index += 1) { + prefix += lengths[index]; + if (!Number.isSafeInteger(prefix)) throw new Error('split byte-length prefix is unsafe'); + } + let best = minimumLeft; + let bestDistance = Math.abs(total - 2 * prefix); + for (let index = minimumLeft + 1; index <= lengths.length - minimumRight; index += 1) { + prefix += lengths[index - 1]; + if (!Number.isSafeInteger(prefix)) throw new Error('split byte-length prefix is unsafe'); + const distance = Math.abs(total - 2 * prefix); + if (distance < bestDistance) { + best = index; + bestDistance = distance; + } + } + return best; +} + +export type SystemRecordRebalanceChoiceV1 = 'borrow-left' | 'borrow-right' | 'merge-left' | 'merge-right'; + +export function chooseSystemRecordRebalanceV1( + leftCount: number | undefined, + rightCount: number | undefined, + minimum: number, +): SystemRecordRebalanceChoiceV1 { + if (leftCount !== undefined && leftCount > minimum) return 'borrow-left'; + if (rightCount !== undefined && rightCount > minimum) return 'borrow-right'; + if (leftCount !== undefined) return 'merge-left'; + if (rightCount !== undefined) return 'merge-right'; + throw new Error('rebalance requires an adjacent sibling'); +} + +export interface SystemRecordInventoryCowUpdateAccountingV1 { + readonly leafObjects: number; + readonly internalObjects: number; + readonly rootObjects: number; + readonly descriptorObjects: number; + readonly encodedBytes: number; +} + +export function assertSystemRecordInventoryCowUpdateBoundV1( + accounting: SystemRecordInventoryCowUpdateAccountingV1, +): void { + const validated = snapshotExactDataRecord( + accounting, + ['leafObjects', 'internalObjects', 'rootObjects', 'descriptorObjects', 'encodedBytes'], + 'COW accounting', + ); + const values = [ + validated.leafObjects, + validated.internalObjects, + validated.rootObjects, + validated.descriptorObjects, + validated.encodedBytes, + ]; + if (values.some((value) => !Number.isSafeInteger(value) || (value as number) < 0)) { + throw new Error('COW accounting values must be non-negative safe integers'); + } + const objects = (validated.leafObjects as number) + (validated.internalObjects as number) + + (validated.rootObjects as number) + (validated.descriptorObjects as number); + if ((validated.leafObjects as number) > 2 || (validated.internalObjects as number) > 2 + || (validated.rootObjects as number) > 1 || validated.descriptorObjects !== 1 + || objects > SYSTEM_RECORD_MAX_TREE_UPDATE_OBJECTS + || (validated.encodedBytes as number) > SYSTEM_RECORD_MAX_TREE_UPDATE_BYTES) { + throw new Error('inventory update exceeds the six-object/1-MiB COW bound'); + } +} + +export const SYSTEM_RECORD_INVENTORY_REBALANCE_TARGETS_V1 = Object.freeze({ + leafBytes: SYSTEM_RECORD_LEAF_TARGET_BYTES, + internalBytes: SYSTEM_RECORD_INTERNAL_TARGET_BYTES, +}); + +export interface SystemRecordInventoryStoredObjectV1 { + readonly objectKind: 'inventory-leaf' | 'inventory-internal'; + readonly object: SystemRecordInventoryObjectV1; + readonly canonicalBytes: Uint8Array; +} + +export interface SystemRecordInventoryTreeSnapshotV1 { + readonly networkId: NetworkIdV1; + readonly descriptor: SystemRecordRootDescriptorObjectV1; + readonly descriptorDigest: Digest32V1; + readonly objects: ReadonlyMap; +} + +export interface SystemRecordInventoryCowWriteV1 extends SystemRecordInventoryStoredObjectV1 { + readonly digest: Digest32V1; + readonly role: 'leaf' | 'internal' | 'root'; +} + +export type SystemRecordInventoryMutationV1 = + | { readonly operation: 'upsert'; readonly row: SystemRecordInventoryRowV1 } + | { readonly operation: 'delete'; readonly stableKeyHash: Digest32V1; readonly peerId: string }; + +export interface SystemRecordInventoryCowUpdateV1 { + readonly changed: boolean; + readonly descriptor: SystemRecordRootDescriptorObjectV1; + readonly descriptorDigest: Digest32V1; + readonly writes: readonly SystemRecordInventoryCowWriteV1[]; + readonly descriptorBytes?: Uint8Array; + readonly accounting: SystemRecordInventoryCowUpdateAccountingV1; + readonly reusedObjectDigests: ReadonlySet; + readonly loadedObjectDigests: ReadonlySet; +} + +/** Build the first immutable tree; subsequent publications must use the COW updater. */ +export function buildSystemRecordInventoryTreeV1( + networkId: NetworkIdV1, + rows: readonly SystemRecordInventoryRowV1[], + epoch: DecimalU64V1 = '0' as DecimalU64V1, +): SystemRecordInventoryTreeSnapshotV1 { + assertNetworkIdV1(networkId); + assertCanonicalDecimalU64(epoch); + const inputRows = snapshotDataArray(rows, 'initial inventory rows', { + maxLength: SYSTEM_RECORD_MAX_INVENTORY_RECORDS, + }) as readonly SystemRecordInventoryRowV1[]; + const sorted = inputRows.map((row) => validateInventoryRow(row, networkId)); + sorted.sort(compareRows); + for (let index = 1; index < sorted.length; index += 1) { + if (compareRows(sorted[index - 1], sorted[index]) >= 0) { + throw new Error('initial inventory rows must be stable-key unique'); + } + } + const objects = new Map(); + let rootDigest: Digest32V1; + if (sorted.length <= SYSTEM_RECORD_LEAF_MAX_ROWS) { + rootDigest = storeLeaf(sorted, true).digest; + } else { + const leafGroups = partitionByTarget( + sorted, + SYSTEM_RECORD_LEAF_MIN_ROWS, + SYSTEM_RECORD_LEAF_MAX_ROWS, + SYSTEM_RECORD_LEAF_TARGET_BYTES, + (row) => encodeSystemRecordInventoryRowV1(networkId, row).byteLength, + ); + if (leafGroups.length > SYSTEM_RECORD_MAX_INVENTORY_LEAVES) { + throw new Error('initial inventory exceeds the V1 leaf bound'); + } + const leafRefs = leafGroups.map((group) => storeLeaf(group, false)); + if (leafRefs.length <= SYSTEM_RECORD_ROOT_MAX_ENTRIES) { + rootDigest = storeInternal(leafRefs, true).digest; + } else { + const internalGroups = partitionByTarget( + leafRefs, + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + SYSTEM_RECORD_INTERNAL_MAX_ENTRIES, + SYSTEM_RECORD_INTERNAL_TARGET_BYTES, + () => SYSTEM_RECORD_MAX_INTERNAL_ENTRY_BYTES, + ); + const internalRefs = internalGroups.map((group) => storeInternal(group, false)); + if (internalRefs.length > SYSTEM_RECORD_ROOT_MAX_ENTRIES) { + throw new Error('initial inventory exceeds the V1 root fanout'); + } + rootDigest = storeInternal(internalRefs, true).digest; + } + } + const descriptor: SystemRecordRootDescriptorObjectV1 = { + objectType: 'root-descriptor', + kind: SYSTEM_RECORD_KIND_V1, + networkId, + epoch, + version: '0' as DecimalU64V1, + treeRootDigest: rootDigest, + totalRows: inputRows.length.toString() as DecimalU64V1, + }; + return Object.freeze({ + networkId, + descriptor: validateRootDescriptor(descriptor), + descriptorDigest: computeSystemRecordRootDescriptorDigestV1(descriptor), + objects, + }); + + function storeLeaf(group: readonly SystemRecordInventoryRowV1[], root: boolean): CowChildRef { + const object = makeLeafObject(networkId, group); + const canonicalBytes = canonicalizeSystemRecordInventoryLeafObjectV1(object, networkId, root); + const digest = computeSystemRecordInventoryLeafDigestV1(object, networkId, root); + objects.set(digest, Object.freeze({ objectKind: 'inventory-leaf', object, canonicalBytes })); + return { digest, objectKind: 'inventory-leaf', first: object.firstKeyHash, last: object.lastKeyHash }; + } + + function storeInternal(group: readonly CowChildRef[], root: boolean): CowChildRef { + const object = makeInternalObject(group); + const canonicalBytes = canonicalizeSystemRecordInventoryInternalObjectV1(object, root); + const digest = computeSystemRecordInventoryInternalDigestV1(object, root); + objects.set(digest, Object.freeze({ objectKind: 'inventory-internal', object, canonicalBytes })); + return { digest, objectKind: 'inventory-internal', first: object.firstKeyHash, last: object.lastKeyHash }; + } +} + +interface CowChildRef { + readonly digest: Digest32V1; + readonly objectKind: 'inventory-leaf' | 'inventory-internal'; + readonly first?: Digest32V1; + readonly last?: Digest32V1; +} + +interface CowPathFrame { + readonly digest: Digest32V1; + readonly object: SystemRecordInventoryInternalObjectV1; + readonly childIndex: number; + readonly root: boolean; +} + +/** + * Apply one localized immutable mutation. Only the search path and the adjacent siblings + * needed by the deterministic lend-before-merge rule are loaded; every returned write is + * derived from canonical bytes, never reported by the caller. + */ +export function updateSystemRecordInventoryTreeV1( + snapshot: SystemRecordInventoryTreeSnapshotV1, + mutation: SystemRecordInventoryMutationV1, +): SystemRecordInventoryCowUpdateV1 { + const pinnedSnapshot = snapshotExactDataRecord( + snapshot, + ['networkId', 'descriptor', 'descriptorDigest', 'objects'], + 'inventory tree snapshot', + ); + const descriptor = validateRootDescriptor(pinnedSnapshot.descriptor); + const networkId = pinnedSnapshot.networkId as NetworkIdV1; + const pinnedDescriptorDigest = pinnedSnapshot.descriptorDigest as Digest32V1; + const objects = pinnedSnapshot.objects; + assertNetworkIdV1(networkId); + assertCanonicalDigest(pinnedDescriptorDigest); + if (!(objects instanceof Map)) throw new Error('inventory snapshot objects must be a native map'); + try { + Reflect.apply(MAP_HAS, objects, [descriptor.treeRootDigest]); + } catch { + throw new Error('inventory snapshot objects must be a native map'); + } + if (descriptor.networkId !== networkId + || computeSystemRecordRootDescriptorDigestV1(descriptor) !== pinnedDescriptorDigest) { + throw new Error('inventory snapshot descriptor binding is invalid'); + } + const mutationProbe = snapshotDataRecord(mutation, 'inventory mutation', { rejectNullValues: true }); + const normalizedMutation: SystemRecordInventoryMutationV1 = mutationProbe.operation === 'upsert' + ? Object.freeze({ + operation: 'upsert', + row: validateInventoryRow( + snapshotExactDataRecord(mutationProbe, ['operation', 'row'], 'inventory upsert mutation').row, + networkId, + ), + }) + : mutationProbe.operation === 'delete' + ? Object.freeze({ + ...snapshotExactDataRecord( + mutationProbe, + ['operation', 'stableKeyHash', 'peerId'], + 'inventory delete mutation', + ), + operation: 'delete' as const, + }) as unknown as SystemRecordInventoryMutationV1 + : (() => { throw new Error('inventory mutation operation is invalid'); })(); + const targetKey = normalizedMutation.operation === 'upsert' + ? normalizedMutation.row.stableKeyHash + : normalizedMutation.stableKeyHash; + const targetPeer = normalizedMutation.operation === 'upsert' + ? normalizedMutation.row.peerId + : normalizedMutation.peerId; + assertCanonicalDigest(targetKey); + assertCanonicalSystemRecordPeerIdV1(targetPeer); + if (targetKey !== computeSystemRecordStableKeyHashV1(networkId, targetPeer)) { + throw new Error('inventory mutation key does not bind networkId/peerId'); + } + + const loaded = new Set(); + const validatedStoredObjects = new Map< + string, + Pick + >(); + const path: CowPathFrame[] = []; + let currentDigest = descriptor.treeRootDigest; + let depth = 1; + let leaf: SystemRecordInventoryLeafObjectV1; + while (true) { + const stored = loadObject(currentDigest, depth === 1); + if (stored.objectKind === 'inventory-leaf') { + leaf = stored.object as SystemRecordInventoryLeafObjectV1; + break; + } + const internal = stored.object as SystemRecordInventoryInternalObjectV1; + const childIndex = findChildIndex(internal.entries, targetKey); + path.push({ digest: currentDigest, object: internal, childIndex, root: depth === 1 }); + currentDigest = internal.entries[childIndex].childDigest; + depth += 1; + if (depth > SYSTEM_RECORD_MAX_TREE_HEIGHT) throw new Error('inventory mutation path exceeds height bound'); + } + const rows = leaf.rows.map((encoded) => decodeInventoryRowBase64UrlV1(networkId, encoded)); + const index = findRowIndex(rows, targetKey, targetPeer); + const exists = index < rows.length && rows[index].stableKeyHash === targetKey; + if (exists && rows[index].peerId !== targetPeer) throw new Error('stable-key hash collision'); + if (normalizedMutation.operation === 'delete' && !exists) return unchanged(); + if (normalizedMutation.operation === 'upsert' && exists + && Buffer.from(encodeSystemRecordInventoryRowV1(networkId, rows[index])).equals( + Buffer.from(encodeSystemRecordInventoryRowV1(networkId, normalizedMutation.row)), + )) return unchanged(); + if (normalizedMutation.operation === 'upsert') rows.splice(index, exists ? 1 : 0, normalizedMutation.row); + else rows.splice(index, 1); + + const writes: SystemRecordInventoryCowWriteV1[] = []; + // Bounded write overlay only. The caller persists these objects before publishing the + // returned descriptor; copying the complete provider cache would defeat COW locality. + const nextObjects = new Map(); + let replacement: CowChildRef[]; + let parentReplaceIndex = path.at(-1)?.childIndex ?? 0; + let parentReplaceCount = 1; + const leafIsRoot = path.length === 0; + if (leafIsRoot) { + if (rows.length <= SYSTEM_RECORD_LEAF_MAX_ROWS) { + replacement = [persistLeaf(rows, true, 'root')]; + return finish(replacement[0].digest, rows.length); + } + const split = chooseSystemRecordByteAwareSplitIndexV1( + rows.map((row) => encodeSystemRecordInventoryRowV1(networkId, row).byteLength), + SYSTEM_RECORD_LEAF_MIN_ROWS, + SYSTEM_RECORD_LEAF_MIN_ROWS, + ); + const children = [persistLeaf(rows.slice(0, split), false, 'leaf'), persistLeaf(rows.slice(split), false, 'leaf')]; + const root = persistInternal(children, true, 'root'); + return finish(root.digest, rows.length); + } + + if (rows.length > SYSTEM_RECORD_LEAF_MAX_ROWS) { + const split = chooseSystemRecordByteAwareSplitIndexV1( + rows.map((row) => encodeSystemRecordInventoryRowV1(networkId, row).byteLength), + SYSTEM_RECORD_LEAF_MIN_ROWS, + SYSTEM_RECORD_LEAF_MIN_ROWS, + ); + replacement = [ + persistLeaf(rows.slice(0, split), false, 'leaf'), + persistLeaf(rows.slice(split), false, 'leaf'), + ]; + } else if (rows.length >= SYSTEM_RECORD_LEAF_MIN_ROWS) { + replacement = [persistLeaf(rows, false, 'leaf')]; + } else { + const parent = path.at(-1)!; + const leftIndex = parent.childIndex > 0 ? parent.childIndex - 1 : undefined; + const rightIndex = parent.childIndex + 1 < parent.object.entries.length + ? parent.childIndex + 1 + : undefined; + const leftRows = leftIndex === undefined ? undefined : loadLeafRows(leftIndex); + const rightRows = leftRows !== undefined && leftRows.length > SYSTEM_RECORD_LEAF_MIN_ROWS + ? undefined + : rightIndex === undefined ? undefined : loadLeafRows(rightIndex); + const rebalance = chooseSystemRecordRebalanceV1( + leftRows?.length, + rightRows?.length, + SYSTEM_RECORD_LEAF_MIN_ROWS, + ); + const siblingIsLeft = rebalance.endsWith('left'); + const siblingIndex = siblingIsLeft ? leftIndex! : rightIndex!; + const siblingRows = siblingIsLeft ? leftRows! : rightRows!; + parentReplaceIndex = Math.min(parent.childIndex, siblingIndex); + parentReplaceCount = 2; + if (rebalance.startsWith('borrow')) { + if (siblingIsLeft) rows.unshift(siblingRows.pop()!); + else rows.push(siblingRows.shift()!); + replacement = siblingIsLeft + ? [persistLeaf(siblingRows, false, 'leaf'), persistLeaf(rows, false, 'leaf')] + : [persistLeaf(rows, false, 'leaf'), persistLeaf(siblingRows, false, 'leaf')]; + } else { + const merged = siblingIsLeft ? [...siblingRows, ...rows] : [...rows, ...siblingRows]; + replacement = [persistLeaf(merged, false, 'leaf')]; + } + + function loadLeafRows(index: number): SystemRecordInventoryRowV1[] { + const siblingEntry = parent.object.entries[index]; + if (siblingEntry?.childKind !== 'inventory-leaf') throw new Error('leaf rebalance sibling is unavailable'); + const sibling = loadObject(siblingEntry.childDigest, false).object as SystemRecordInventoryLeafObjectV1; + return sibling.rows.map((encoded) => decodeInventoryRowBase64UrlV1(networkId, encoded)); + } + } + + let frame = path.pop()!; + let parentEntries = replaceChildEntries(frame.object.entries, parentReplaceIndex, parentReplaceCount, replacement); + if (frame.root) return finishRootEntries(parentEntries, rowsDelta()); + + let nextParentRefs: CowChildRef[]; + if (parentEntries.length > SYSTEM_RECORD_INTERNAL_MAX_ENTRIES) { + const split = chooseSystemRecordByteAwareSplitIndexV1( + parentEntries.map((entry) => canonicalizeJsonBytes(entry as unknown as CanonicalJsonValue).byteLength), + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + ); + nextParentRefs = [ + persistInternalEntries(parentEntries.slice(0, split), false, 'internal'), + persistInternalEntries(parentEntries.slice(split), false, 'internal'), + ]; + } else if (parentEntries.length >= SYSTEM_RECORD_INTERNAL_MIN_ENTRIES) { + nextParentRefs = [persistInternalEntries(parentEntries, false, 'internal')]; + } else { + const rootFrame = path.pop(); + if (rootFrame === undefined || !rootFrame.root) throw new Error('non-root internal node lacks root parent'); + const rootParent = rootFrame; + const leftIndex = rootParent.childIndex > 0 ? rootParent.childIndex - 1 : undefined; + const rightIndex = rootParent.childIndex + 1 < rootParent.object.entries.length + ? rootParent.childIndex + 1 + : undefined; + const left = leftIndex === undefined ? undefined : loadInternalSibling(leftIndex); + const right = left !== undefined && left.entries.length > SYSTEM_RECORD_INTERNAL_MIN_ENTRIES + ? undefined + : rightIndex === undefined ? undefined : loadInternalSibling(rightIndex); + const rebalance = chooseSystemRecordRebalanceV1( + left?.entries.length, + right?.entries.length, + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + ); + const siblingIsLeft = rebalance.endsWith('left'); + const siblingIndex = siblingIsLeft ? leftIndex! : rightIndex!; + const sibling = siblingIsLeft ? left! : right!; + let rootReplaceIndex = Math.min(rootParent.childIndex, siblingIndex); + if (rebalance.startsWith('borrow')) { + const siblingEntries = [...sibling.entries]; + if (siblingIsLeft) parentEntries.unshift(siblingEntries.pop()!); + else parentEntries.push(siblingEntries.shift()!); + nextParentRefs = siblingIsLeft + ? [persistInternalEntries(siblingEntries, false, 'internal'), persistInternalEntries(parentEntries, false, 'internal')] + : [persistInternalEntries(parentEntries, false, 'internal'), persistInternalEntries(siblingEntries, false, 'internal')]; + } else { + const merged = siblingIsLeft ? [...sibling.entries, ...parentEntries] : [...parentEntries, ...sibling.entries]; + const collapsesRoot = rootParent.object.entries.length === 2; + nextParentRefs = [persistInternalEntries( + merged, + collapsesRoot, + collapsesRoot ? 'root' : 'internal', + )]; + } + const rootEntries = replaceChildEntries(rootParent.object.entries, rootReplaceIndex, 2, nextParentRefs); + if (rootEntries.length === 1) return finish(rootEntries[0].childDigest, rowsDelta()); + return finishRootEntries(rootEntries, rowsDelta()); + + function loadInternalSibling(index: number): SystemRecordInventoryInternalObjectV1 { + const siblingEntry = rootParent.object.entries[index]; + if (siblingEntry?.childKind !== 'inventory-internal') { + throw new Error('internal rebalance sibling is unavailable'); + } + return loadObject(siblingEntry.childDigest, false).object as SystemRecordInventoryInternalObjectV1; + } + } + + const rootFrame = path.pop(); + if (rootFrame === undefined || !rootFrame.root) throw new Error('inventory height exceeds the V1 update model'); + const rootEntries = replaceChildEntries(rootFrame.object.entries, rootFrame.childIndex, 1, nextParentRefs); + return finishRootEntries(rootEntries, rowsDelta()); + + function rowsDelta(): number { + return Number(parseCanonicalDecimalU64(descriptor.totalRows)) + + (normalizedMutation.operation === 'upsert' && !exists + ? 1 + : normalizedMutation.operation === 'delete' ? -1 : 0); + } + + function loadObject( + digest: Digest32V1, + root: boolean, + ): Pick { + const cacheKey = `${root ? 'root' : 'child'}:${digest}`; + const cached = validatedStoredObjects.get(cacheKey); + if (cached !== undefined) return cached; + const candidate = Reflect.apply(MAP_GET, objects, [digest]) as unknown; + if (candidate === undefined) throw new Error(`inventory snapshot is missing ${digest}`); + const stored = snapshotExactDataRecord( + candidate, + ['objectKind', 'object', 'canonicalBytes'], + 'inventory snapshot stored object', + ); + if (stored.objectKind !== 'inventory-leaf' && stored.objectKind !== 'inventory-internal') { + throw new Error('inventory snapshot object kind is invalid'); + } + const object = stored.objectKind === 'inventory-leaf' + ? validateLeaf(stored.object, networkId, root) + : validateInternal(stored.object, root); + const canonicalBytes = canonicalizeJsonBytes(object as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1[stored.objectKind], + }); + const retainedCanonicalBytes = copyBoundedSystemRecordBytesV1( + stored.canonicalBytes, + SYSTEM_RECORD_OBJECT_CAPS_V1[stored.objectKind], + 'inventory snapshot stored canonical bytes', + ); + if (!sameBytes(canonicalBytes, retainedCanonicalBytes)) { + throw new Error('inventory snapshot stored object does not match its canonical bytes'); + } + const actual = digestSystemRecordBytesV1( + stored.objectKind === 'inventory-leaf' + ? SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryLeaf + : SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryInternal, + canonicalBytes, + ); + if (actual !== digest) throw new Error('inventory snapshot object digest is invalid'); + loaded.add(digest); + const validated = Object.freeze({ objectKind: stored.objectKind, object }); + validatedStoredObjects.set(cacheKey, validated); + return validated; + } + + function persistLeaf( + nextRows: readonly SystemRecordInventoryRowV1[], + root: boolean, + role: SystemRecordInventoryCowWriteV1['role'], + ): CowChildRef { + const object = makeLeafObject(networkId, nextRows); + const bytes = canonicalizeSystemRecordInventoryLeafObjectV1(object, networkId, root); + const digest = computeSystemRecordInventoryLeafDigestV1(object, networkId, root); + persist(digest, 'inventory-leaf', object, bytes, role); + return { digest, objectKind: 'inventory-leaf', first: object.firstKeyHash, last: object.lastKeyHash }; + } + + function persistInternal( + children: readonly CowChildRef[], + root: boolean, + role: SystemRecordInventoryCowWriteV1['role'], + ): CowChildRef { + return persistInternalObject(makeInternalObject(children), root, role); + } + + function persistInternalEntries( + entries: readonly SystemRecordInventoryInternalEntryV1[], + root: boolean, + role: SystemRecordInventoryCowWriteV1['role'], + ): CowChildRef { + const lastChild = childRef(entries.at(-1)!); + const object: SystemRecordInventoryInternalObjectV1 = { + objectType: 'inventory-internal', + firstKeyHash: entries[0].separatorKeyHash, + lastKeyHash: lastChild.last!, + entries: Object.freeze([...entries]), + }; + return persistInternalObject(object, root, role); + } + + function persistInternalObject( + object: SystemRecordInventoryInternalObjectV1, + root: boolean, + role: SystemRecordInventoryCowWriteV1['role'], + ): CowChildRef { + const bytes = canonicalizeSystemRecordInventoryInternalObjectV1(object, root); + const digest = computeSystemRecordInventoryInternalDigestV1(object, root); + persist(digest, 'inventory-internal', object, bytes, role); + return { digest, objectKind: 'inventory-internal', first: object.firstKeyHash, last: object.lastKeyHash }; + } + + function persist( + digest: Digest32V1, + objectKind: SystemRecordInventoryStoredObjectV1['objectKind'], + object: SystemRecordInventoryObjectV1, + canonicalBytes: Uint8Array, + role: SystemRecordInventoryCowWriteV1['role'], + ): void { + const stored = Object.freeze({ objectKind, object, canonicalBytes }); + nextObjects.set(digest, stored); + const existsInSnapshot = Reflect.apply(MAP_HAS, objects, [digest]) as boolean; + if (existsInSnapshot) { + const existing = loadObject(digest, role === 'root'); + if (existing.objectKind !== objectKind) { + throw new Error('inventory snapshot reuses a digest under a different object kind'); + } + } + if (!existsInSnapshot && !writes.some((write) => write.digest === digest)) { + writes.push(Object.freeze({ digest, objectKind, object, canonicalBytes, role })); + } + } + + function childRef(entry: SystemRecordInventoryInternalEntryV1): CowChildRef { + const overlay = nextObjects.get(entry.childDigest); + const stored = overlay ?? loadObject(entry.childDigest, false); + if (stored.objectKind !== entry.childKind) { + throw new Error('updated inventory child kind does not match its reference'); + } + const object = stored.object; + return { + digest: entry.childDigest, + objectKind: entry.childKind, + first: object.firstKeyHash, + last: object.lastKeyHash, + }; + } + + function finishRootEntries(entries: readonly SystemRecordInventoryInternalEntryV1[], totalRows: number): SystemRecordInventoryCowUpdateV1 { + if (entries.length === 1) return finish(entries[0].childDigest, totalRows); + if (entries.length <= SYSTEM_RECORD_ROOT_MAX_ENTRIES) { + const root = persistInternalEntries(entries, true, 'root'); + return finish(root.digest, totalRows); + } + const split = chooseSystemRecordByteAwareSplitIndexV1( + entries.map((entry) => canonicalizeJsonBytes(entry as unknown as CanonicalJsonValue).byteLength), + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + SYSTEM_RECORD_INTERNAL_MIN_ENTRIES, + ); + const children = [ + persistInternalEntries(entries.slice(0, split), false, 'internal'), + persistInternalEntries(entries.slice(split), false, 'internal'), + ]; + const root = persistInternal(children, true, 'root'); + return finish(root.digest, totalRows); + } + + function finish(rootDigest: Digest32V1, totalRows: number): SystemRecordInventoryCowUpdateV1 { + const nextVersion = parseCanonicalDecimalU64(descriptor.version) + 1n; + if (nextVersion > 0xffff_ffff_ffff_ffffn) throw new Error('root descriptor version overflow'); + const nextDescriptor: SystemRecordRootDescriptorObjectV1 = { + objectType: 'root-descriptor', kind: SYSTEM_RECORD_KIND_V1, networkId, + epoch: descriptor.epoch, version: nextVersion.toString() as DecimalU64V1, + priorRootDigest: pinnedDescriptorDigest, treeRootDigest: rootDigest, + totalRows: totalRows.toString() as DecimalU64V1, + }; + const descriptorBytes = canonicalizeSystemRecordRootDescriptorObjectV1(nextDescriptor); + const accounting: SystemRecordInventoryCowUpdateAccountingV1 = { + leafObjects: writes.filter((write) => write.role === 'leaf').length, + internalObjects: writes.filter((write) => write.role === 'internal').length, + rootObjects: writes.filter((write) => write.role === 'root').length, + descriptorObjects: 1, + encodedBytes: descriptorBytes.byteLength + writes.reduce((sum, write) => sum + write.canonicalBytes.byteLength, 0), + }; + assertSystemRecordInventoryCowUpdateBoundV1(accounting); + const reused = new Set(); + if (Reflect.apply(MAP_HAS, objects, [rootDigest])) reused.add(rootDigest); + for (const write of writes) { + if (write.objectKind !== 'inventory-internal') continue; + for (const entry of (write.object as SystemRecordInventoryInternalObjectV1).entries) { + if (Reflect.apply(MAP_HAS, objects, [entry.childDigest])) reused.add(entry.childDigest); + } + } + const validatedDescriptor = validateRootDescriptor(nextDescriptor); + const nextDescriptorDigest = computeSystemRecordRootDescriptorDigestV1(nextDescriptor); + return Object.freeze({ changed: true, descriptor: validatedDescriptor, descriptorDigest: nextDescriptorDigest, + writes: Object.freeze(writes), + descriptorBytes, accounting: Object.freeze(accounting), reusedObjectDigests: reused, loadedObjectDigests: loaded }); + } + + function unchanged(): SystemRecordInventoryCowUpdateV1 { + return Object.freeze({ changed: false, descriptor, descriptorDigest: pinnedDescriptorDigest, + writes: Object.freeze([]), + accounting: Object.freeze({ leafObjects: 0, internalObjects: 0, rootObjects: 0, descriptorObjects: 0, encodedBytes: 0 }), + // A no-op publishes no descriptor, so it has no reuse closure to report. Enumerating the + // complete snapshot here would turn an otherwise path-bounded lookup into O(tree size). + reusedObjectDigests: new Set(), loadedObjectDigests: loaded }); + } +} + +function sameBytes(left: Uint8Array, right: Uint8Array): boolean { + if (left.byteLength !== right.byteLength) return false; + for (let index = 0; index < left.byteLength; index += 1) { + if (left[index] !== right[index]) return false; + } + return true; +} + +function makeLeafObject( + networkId: NetworkIdV1, + rows: readonly SystemRecordInventoryRowV1[], +): SystemRecordInventoryLeafObjectV1 { + const encoded = rows.map((row) => encodeInventoryRowBase64UrlV1(networkId, row)); + return rows.length === 0 + ? Object.freeze({ objectType: 'inventory-leaf', rows: Object.freeze(encoded) }) + : Object.freeze({ objectType: 'inventory-leaf', firstKeyHash: rows[0].stableKeyHash, + lastKeyHash: rows.at(-1)!.stableKeyHash, rows: Object.freeze(encoded) }); +} + +function makeInternalObject(children: readonly CowChildRef[]): SystemRecordInventoryInternalObjectV1 { + if (children.length === 0 || children.some((child) => child.first === undefined || child.last === undefined)) { + throw new Error('internal inventory node requires nonempty ranged children'); + } + const childKind = children[0].objectKind; + if (children.some((child) => child.objectKind !== childKind)) throw new Error('internal children must have one kind'); + return Object.freeze({ + objectType: 'inventory-internal', firstKeyHash: children[0].first!, lastKeyHash: children.at(-1)!.last!, + entries: Object.freeze(children.map((child) => Object.freeze({ + separatorKeyHash: child.first!, childDigest: child.digest, childKind, + }))), + }); +} + +function replaceChildEntries( + entries: readonly SystemRecordInventoryInternalEntryV1[], + index: number, + count: number, + replacements: readonly CowChildRef[], +): SystemRecordInventoryInternalEntryV1[] { + const next = [...entries]; + next.splice(index, count, ...replacements.map((child) => ({ + separatorKeyHash: child.first!, childDigest: child.digest, childKind: child.objectKind, + }))); + return next; +} + +function findChildIndex(entries: readonly SystemRecordInventoryInternalEntryV1[], key: Digest32V1): number { + let low = 0; + let high = entries.length; + while (low < high) { + const middle = (low + high) >>> 1; + if (entries[middle].separatorKeyHash <= key) low = middle + 1; + else high = middle; + } + return Math.max(0, low - 1); +} + +function findRowIndex(rows: readonly SystemRecordInventoryRowV1[], key: Digest32V1, peerId: string): number { + let low = 0; + let high = rows.length; + while (low < high) { + const middle = (low + high) >>> 1; + const candidate = rows[middle]; + const comparison = candidate.stableKeyHash === key + ? candidate.peerId === peerId ? 0 : (() => { throw new Error('stable-key hash collision'); })() + : candidate.stableKeyHash < key ? -1 : 1; + if (comparison < 0) low = middle + 1; + else high = middle; + } + return low; +} + +function partitionByTarget( + values: readonly T[], + minimum: number, + maximum: number, + targetBytes: number, + encodedBytes: (value: T) => number, +): T[][] { + if (values.length < minimum) throw new Error('partition cannot meet minimum occupancy'); + const totalBytes = values.reduce((sum, value) => sum + encodedBytes(value), 0); + const minimumGroups = Math.ceil(values.length / maximum); + const maximumGroups = Math.floor(values.length / minimum); + const groups = Math.max(minimumGroups, Math.min(maximumGroups, Math.max(1, Math.round(totalBytes / targetBytes)))); + const base = Math.floor(values.length / groups); + const remainder = values.length % groups; + const result: T[][] = []; + let offset = 0; + for (let index = 0; index < groups; index += 1) { + const count = base + (index < remainder ? 1 : 0); + result.push(values.slice(offset, offset + count)); + offset += count; + } + return result; +} + + +function validateInventoryRow(row: unknown, networkId?: NetworkIdV1): SystemRecordInventoryRowV1 { + const probe = snapshotDataRecord(row, 'inventory row', { rejectNullValues: true }); + const expected = [ + 'stableKeyHash', 'peerId', 'authoritySequence', 'version', 'headDigest', + ...(hasOwnDataProperty(probe, 'conflictEvidenceDigest') ? ['conflictEvidenceDigest'] : []), + 'tombstone', 'quarantined', + ]; + const validated = snapshotExactDataRecord(probe, expected, 'inventory row'); + assertCanonicalDigest(validated.stableKeyHash); + try { + assertCanonicalSystemRecordPeerIdV1(validated.peerId); + } catch { + throw new Error('inventory row peerId is not canonical'); + } + if (parseCanonicalDecimalU64(validated.authoritySequence) > SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX) { + throw new Error('inventory row authoritySequence exceeds the V1 cap'); + } + assertCanonicalDecimalU64(validated.version); + assertCanonicalDigest(validated.headDigest); + if (hasOwnDataProperty(validated, 'conflictEvidenceDigest')) { + assertCanonicalDigest(validated.conflictEvidenceDigest); + if (!validated.quarantined) throw new Error('conflict evidence may appear only on quarantined rows'); + } + if (typeof validated.tombstone !== 'boolean' || typeof validated.quarantined !== 'boolean') { + throw new Error('inventory row flags must be booleans'); + } + if (networkId !== undefined && validated.stableKeyHash + !== computeSystemRecordStableKeyHashV1(networkId, validated.peerId as string)) { + throw new Error('inventory row stable key hash does not bind networkId/peerId'); + } + return Object.freeze({ ...validated }) as unknown as SystemRecordInventoryRowV1; +} + +function compareRows(left: SystemRecordInventoryRowV1, right: SystemRecordInventoryRowV1): number { + if (left.stableKeyHash !== right.stableKeyHash) return left.stableKeyHash < right.stableKeyHash ? -1 : 1; + if (left.peerId === right.peerId) return 0; + // Same hash with a different peer is a collision refusal, not a secondary bucket. + throw new Error('stable-key hash collision between different canonical peers'); +} + +function writeU64(target: Uint8Array, offset: number, value: bigint): void { + for (let index = 7; index >= 0; index -= 1) { + target[offset + index] = Number(value & 0xffn); + value >>= 8n; + } +} + +function readU64(source: Uint8Array, offset: number): bigint { + let value = 0n; + for (let index = 0; index < 8; index += 1) value = (value << 8n) | BigInt(source[offset + index]); + return value; +} + +function hexDigestBytes(value: Digest32V1): Uint8Array { + assertCanonicalDigest(value); + return Uint8Array.from(Buffer.from(value.slice(2), 'hex')); +} + +function bytesDigest(value: Uint8Array): Digest32V1 { + if (value.byteLength !== 32) throw new Error('digest must be 32 bytes'); + return `0x${Buffer.from(value).toString('hex')}` as Digest32V1; +} + +function concatBytes(...values: readonly Uint8Array[]): Uint8Array { + const output = new Uint8Array(values.reduce((sum, value) => sum + value.byteLength, 0)); + let offset = 0; + for (const value of values) { output.set(value, offset); offset += value.byteLength; } + return output; +} diff --git a/packages/core/src/system-record-limits-v1.ts b/packages/core/src/system-record-limits-v1.ts new file mode 100644 index 0000000000..c34d4b5099 --- /dev/null +++ b/packages/core/src/system-record-limits-v1.ts @@ -0,0 +1,190 @@ +/** + * Frozen numeric and domain constants for the dormant agents system-record V1 lane. + * + * Keeping the limits in one dependency-free module is intentional: codecs, cache + * accounting, transport, storage, and conformance tests must consume the same values. + */ +export const SYSTEM_RECORD_PROTOCOL_V1 = '/dkg/system-records/1.0.0' as const; +export const SYSTEM_RECORD_WIRE_VERSION_V1 = '1' as const; +export const SYSTEM_RECORD_KIND_V1 = 'agents' as const; + +export const SYSTEM_RECORD_MAX_HEADER_BYTES = 8 * 1024; +export const SYSTEM_RECORD_MAX_FRAME_BYTES = 1_056_772; +export const SYSTEM_RECORD_MAX_FRAME_PAYLOAD_BYTES = 1024 * 1024; +export const SYSTEM_RECORD_MAX_JSON_DEPTH = 16; +export const SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH = 8; +export const SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH = 4; +export const SYSTEM_RECORD_MAX_ARRAY_JSON_DEPTH = 2; +export const SYSTEM_RECORD_MAX_TUPLE_JSON_DEPTH = 3; +export const SYSTEM_RECORD_MAX_INVENTORY_INTERNAL_JSON_DEPTH = 5; +export const SYSTEM_RECORD_MAX_WIRE_REQUEST_JSON_DEPTH = 6; +export const SYSTEM_RECORD_MAX_SIGNED_HEAD_JSON_DEPTH = 20; +export const SYSTEM_RECORD_MAX_SIGNED_CONTROL_JSON_DEPTH = 12; +export const SYSTEM_RECORD_MAX_INVENTORY_PATH_DEPTH = 2; +export const SYSTEM_RECORD_MAX_INVENTORY_CHILD_INDEX = 255; + +export const SYSTEM_RECORD_OBJECT_CAPS_V1 = Object.freeze({ + 'root-descriptor': 16 * 1024, + 'inventory-internal': 64 * 1024, + 'inventory-leaf': 256 * 1024, + 'agent-profile-head': 64 * 1024, + 'authority-transition': 64 * 1024, + 'fork-resolution': 64 * 1024, + 'conflict-evidence': 16 * 1024, + 'owned-subject-table': 256 * 1024, + 'profile-bundle': SYSTEM_RECORD_MAX_FRAME_PAYLOAD_BYTES, +} as const); + +export type SystemRecordObjectKindV1 = keyof typeof SYSTEM_RECORD_OBJECT_CAPS_V1; + +export const SYSTEM_RECORD_MAX_PEER_ID_BYTES = 256; +export const SYSTEM_RECORD_ED25519_PUBLIC_KEY_BYTES = 32; +export const SYSTEM_RECORD_ED25519_SIGNATURE_BYTES = 64; +export const SYSTEM_RECORD_EIP191_SIGNATURE_BYTES = 65; +export const SYSTEM_RECORD_MAX_EIP1271_SIGNATURE_BYTES = 4_096; +export const SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX = 14n; +export const SYSTEM_RECORD_MAX_ROOT_CLAIMS = 15; +export const SYSTEM_RECORD_MAX_RESOLVED_FORK_TUPLES = 16; +export const SYSTEM_RECORD_MAX_CONFLICT_DIGESTS = 16; +export const SYSTEM_RECORD_MAX_CONFLICT_ENTRIES = 8; +export const SYSTEM_RECORD_MAX_OWNED_SUBJECTS = 2_048; +export const SYSTEM_RECORD_MAX_PROJECTION_QUADS = 10_000; +export const SYSTEM_RECORD_MAX_PROJECTION_BYTES = 2 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES = 64 * 1024; +export const SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES = 512 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_QUADS = 5_000_000; +export const SYSTEM_RECORD_MAX_DIRTY_RECORDS = 10_000; +export const SYSTEM_RECORD_MAX_DIRTY_STATE_BYTES = 16 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_CLOCK_SKEW_MS = 5 * 60 * 1_000; +export const SYSTEM_RECORD_MAX_STATUS_ROWS = 100; +export const SYSTEM_RECORD_MAX_STATUS_BYTES = 256 * 1024; +export const SYSTEM_RECORD_STATUS_TIMEOUT_MS = 2_000; +export const SYSTEM_RECORD_MAX_ATOMIC_BUNDLE_BYTES = 1024 * 1024; +export const SYSTEM_RECORD_MAX_ATOMIC_SIGNED_HEAD_BYTES = 64 * 1024; +export const SYSTEM_RECORD_MAX_ATOMIC_DECODED_TERM_BYTES = 2 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_ATOMIC_SPARQL_REQUEST_BYTES = 4 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES = 12 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_BUNDLE_DECODE_CONCURRENCY = 1; +export const SYSTEM_RECORD_MAX_MATERIALIZER_WRITE_CONCURRENCY = 1; + +export const SYSTEM_RECORD_MAX_CLOSURE_OBJECTS = 32; +export const SYSTEM_RECORD_MAX_CLOSURE_BYTES = 3 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_SIDECAR_OBJECTS = 17; +export const SYSTEM_RECORD_MAX_SIDECAR_BYTES = 1_064_960; +export const SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_OBJECTS = 25_000; +export const SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_BYTES = 1024 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES = 262_144; +export const SYSTEM_RECORD_MAX_CLOSURE_SIDECAR_LIVE_METADATA_BYTES = 24 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_CONFLICT_SIDECARS = 1_024; +export const SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_REFERENCES = 17_408; +export const SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_AGGREGATE_BYTES = 128 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_METADATA_BYTES = 2 * 1024 * 1024; + +export const SYSTEM_RECORD_MAX_ACTIVATION_RECORDS = 512; +export const SYSTEM_RECORD_MAX_ACTIVATION_BUNDLE_BYTES = 128 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_ACTIVATION_CLOSURE_BYTES = 256 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_ACTIVATION_INVENTORY_LEAVES = 4; +export const SYSTEM_RECORD_MAX_ACTIVATION_REFERENCES = 32_768; +export const SYSTEM_RECORD_MAX_ACTIVATION_METADATA_BYTES = 4 * 1024 * 1024; + +export const SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES = 64 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_RUNTIME_ROOT_DIRECTORY_BYTES = 8 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_RUNTIME_LEAF_BYTES = 16 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_RUNTIME_HEAD_BYTES = 8 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_RUNTIME_DECODE_BYTES = 12 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_RUNTIME_EIP1271_BYTES = 8 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_RUNTIME_CONTROL_BYTES = 12 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_OBJECT_CACHE_BYTES = 2 * 1024 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_OBJECT_CACHE_OBJECTS = 50_000; +export const SYSTEM_RECORD_MAX_RESTART_VALIDATION_KEYS = 8_192; +export const SYSTEM_RECORD_MAX_RESTART_VALIDATION_BYTES = 1024 * 1024; +export const SYSTEM_RECORD_MAX_PROVIDER_CONTINUATIONS = 1_024; +export const SYSTEM_RECORD_MAX_PROVIDER_CONTINUATION_BYTES = 1024 * 1024; +export const SYSTEM_RECORD_MAX_COMPLETED_LEAF_DIGESTS = 4_096; +export const SYSTEM_RECORD_MAX_COMPLETED_LEAF_BYTES = 256 * 1024; +export const SYSTEM_RECORD_MAX_PENDING_EXACT_FETCHES = 128; +export const SYSTEM_RECORD_MAX_EXACT_FETCH_WAITERS = 16; +export const SYSTEM_RECORD_MAX_EIP1271_CONCURRENCY = 2; +export const SYSTEM_RECORD_MAX_EIP1271_CALLS_PER_SLICE = 2; +export const SYSTEM_RECORD_MAX_EIP1271_CACHE_ENTRIES = 2_048; + +export const SYSTEM_RECORD_MAX_SLICE_ADVANCES = 8; +export const SYSTEM_RECORD_MAX_SLICE_WIRE_BYTES = 2 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_SLICE_REQUESTS = 12; +export const SYSTEM_RECORD_MAX_SLICE_RETRIES_PER_REQUEST = 1; +export const SYSTEM_RECORD_SLICE_TIMEOUT_MS = 3_000; +export const SYSTEM_RECORD_MAX_CONTINUATION_SLICES = 512; +export const SYSTEM_RECORD_MAX_CONTINUATION_ADVANCES = 4_096; +export const SYSTEM_RECORD_MAX_CONTINUATION_WIRE_BYTES = 1024 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_CONTINUATION_CLOSURE_WIRE_BYTES = 768 * 1024 * 1024; +export const SYSTEM_RECORD_CONTINUATION_TIMEOUT_MS = 30 * 60 * 1_000; +export const SYSTEM_RECORD_PROVIDER_EXCHANGE_TIMEOUT_MS = 3_000; +export const SYSTEM_RECORD_PROVIDER_REQUEST_TOKEN_CAPACITY = 32; +export const SYSTEM_RECORD_PROVIDER_REQUEST_TOKEN_REFILL_PER_MINUTE = 256; +export const SYSTEM_RECORD_PROVIDER_RESPONSE_TOKEN_CAPACITY = 4 * SYSTEM_RECORD_MAX_FRAME_BYTES; +export const SYSTEM_RECORD_PROVIDER_RESPONSE_TOKEN_REFILL_PER_MINUTE = 32 * 1024 * 1024; + +export const SYSTEM_RECORD_MAX_CACHE_STAGING_BYTES = 32 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_CACHE_LIVE_METADATA_BYTES = 32 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_CACHE_RESERVE_METADATA_BYTES = 32 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_UNREFERENCED_OBJECTS = 128; +export const SYSTEM_RECORD_MAX_UNREFERENCED_BYTES = 64 * 1024 * 1024; +export const SYSTEM_RECORD_MAX_PUBLICATION_JOURNAL_REFERENCES = 110; +export const SYSTEM_RECORD_MAX_PUBLICATION_JOURNAL_BYTES = 64 * 1024; +export const SYSTEM_RECORD_MAX_RECLAIM_DELETES = 16; +export const SYSTEM_RECORD_MAX_RECLAIM_BYTES = 16 * 1024 * 1024; +export const SYSTEM_RECORD_RECLAIM_TIMEOUT_MS = 50; +export const SYSTEM_RECORD_MAX_SERVE_PINS = 64; +export const SYSTEM_RECORD_SERVE_PIN_TIMEOUT_MS = 30_000; +export const SYSTEM_RECORD_REPAIR_MIN_DISPATCH_BUDGET_MS = 1_500; +export const SYSTEM_RECORD_MATERIALIZER_HEALTHY_P99_MS = 750; +export const SYSTEM_RECORD_MATERIALIZER_HARD_TIMEOUT_MS = 1_000; + +export const SYSTEM_RECORD_INVENTORY_ROW_VERSION = 1; +export const SYSTEM_RECORD_MAX_ROW_BYTES = 512; +export const SYSTEM_RECORD_MAX_ORDINARY_ROW_BYTES = 340; +export const SYSTEM_RECORD_MAX_EVIDENCE_ROW_BYTES = 372; +export const SYSTEM_RECORD_MAX_INTERNAL_ENTRY_BYTES = 256; +export const SYSTEM_RECORD_LEAF_MIN_ROWS = 128; +export const SYSTEM_RECORD_LEAF_MAX_ROWS = 512; +export const SYSTEM_RECORD_INTERNAL_MIN_ENTRIES = 128; +export const SYSTEM_RECORD_INTERNAL_MAX_ENTRIES = 256; +export const SYSTEM_RECORD_ROOT_MIN_ENTRIES = 2; +export const SYSTEM_RECORD_ROOT_MAX_ENTRIES = 256; +export const SYSTEM_RECORD_MAX_INVENTORY_RECORDS = 262_144; +export const SYSTEM_RECORD_MAX_INVENTORY_LEAVES = 2_048; +export const SYSTEM_RECORD_MAX_INVENTORY_OBJECTS = 2_065; +export const SYSTEM_RECORD_MAX_TREE_HEIGHT = 3; +export const SYSTEM_RECORD_MAX_TREE_UPDATE_OBJECTS = 6; +export const SYSTEM_RECORD_MAX_TREE_UPDATE_BYTES = 1024 * 1024; +export const SYSTEM_RECORD_LEAF_TARGET_BYTES = 64 * 1024; +export const SYSTEM_RECORD_INTERNAL_TARGET_BYTES = 32 * 1024; + +export const SYSTEM_RECORD_DIGEST_DOMAINS_V1 = Object.freeze({ + rootDescriptor: 'dkg-system-record-root-descriptor-object-v1\n', + inventoryInternal: 'dkg-system-record-inventory-internal-object-v1\n', + inventoryLeaf: 'dkg-system-record-inventory-leaf-object-v1\n', + agentProfileHead: 'dkg-system-record-agent-profile-head-object-v1\n', + authorityTransition: 'dkg-system-record-authority-transition-object-v1\n', + forkResolution: 'dkg-system-record-fork-resolution-object-v1\n', + conflictEvidence: 'dkg-system-record-conflict-evidence-object-v1\n', + profileBundle: 'dkg-system-record-profile-bundle-v1\n', + signedEnvelope: 'dkg-system-record-signed-envelope-v1\n', + signedRootDescriptorEnvelope: 'dkg-system-record-signed-root-descriptor-envelope-v1\n', + rootCollisionEvidence: 'dkg-system-record-root-collision-evidence-v1\n', + ownedSubjectTable: 'dkg-system-record-owned-subject-table-v1\n', + appliedState: 'dkg-system-record-applied-state-v1\n', + rootClaimSet: 'dkg-system-record-root-claim-set-v1\n', + capacityState: 'dkg-system-record-capacity-state-v1\n', + materializationReceipt: 'dkg-system-record-materialization-receipt-v1\n', +} as const); + +export const SYSTEM_RECORD_SIGNATURE_DOMAINS_V1 = Object.freeze({ + provider: 'dkg-system-record-provider-signature-v1\n', + peer: 'dkg-system-record-peer-signature-v1\n', + evm: 'dkg-system-record-evm-signature-v1\n', +} as const); + +export const SYSTEM_RECORD_EIP191_MAX_S = BigInt( + '0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', +); diff --git a/packages/core/src/system-record-objects-v1.ts b/packages/core/src/system-record-objects-v1.ts new file mode 100644 index 0000000000..961d842356 --- /dev/null +++ b/packages/core/src/system-record-objects-v1.ts @@ -0,0 +1,2896 @@ +import { publicKeyFromRaw } from '@libp2p/crypto/keys'; +import { peerIdFromPublicKey, peerIdFromString } from '@libp2p/peer-id'; +import { verifyAsync as verifyEd25519 } from '@noble/ed25519'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { sha256 } from '@noble/hashes/sha2.js'; + +import { + assertAssertionCoordinateV1, + type AssertionCoordinateV1, +} from './author-catalog-codec.js'; +import { + assertCanonicalGraphScopedAuthorSealV1, + type CanonicalGraphScopedAuthorSealV1, +} from './canonical-graph-scoped-author-seal.js'; +import { + canonicalizeJson, + canonicalizeJsonBytes, + parseCanonicalJson, + type CanonicalJsonValue, +} from './canonical-json.js'; +import { keccak256 } from './crypto/keccak.js'; +import { workspaceAgentEncryptionKeyId } from './crypto/workspace-encryption.js'; +import { parseDeterministicKnowledgeAssetUal } from './ka-content-scope.js'; +import { + assertCanonicalSystemRecordPeerIdV1, + copyBoundedSystemRecordBytesV1, + decodeUnpaddedBase64UrlV1, + digestSystemRecordBytesV1, + failSystemRecordObjectV1 as fail, + SystemRecordObjectErrorV1, + type SystemRecordObjectErrorCodeV1, + type SystemRecordPeerPublicKeyV1, +} from './system-record-codec-primitives-v1.js'; +import { parseCanonicalSignedSystemRecordRootDescriptorEnvelopeV1 } from './system-record-inventory-v1.js'; +import { + SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX, + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_ED25519_PUBLIC_KEY_BYTES, + SYSTEM_RECORD_ED25519_SIGNATURE_BYTES, + SYSTEM_RECORD_EIP191_MAX_S, + SYSTEM_RECORD_EIP191_SIGNATURE_BYTES, + SYSTEM_RECORD_KIND_V1, + SYSTEM_RECORD_MAX_ACTIVATION_BUNDLE_BYTES, + SYSTEM_RECORD_MAX_ACTIVATION_CLOSURE_BYTES, + SYSTEM_RECORD_MAX_ACTIVATION_INVENTORY_LEAVES, + SYSTEM_RECORD_MAX_ACTIVATION_METADATA_BYTES, + SYSTEM_RECORD_MAX_ACTIVATION_RECORDS, + SYSTEM_RECORD_MAX_ACTIVATION_REFERENCES, + SYSTEM_RECORD_MAX_ARRAY_JSON_DEPTH, + SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_BYTES, + SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_OBJECTS, + SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES, + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS, + SYSTEM_RECORD_MAX_CLOSURE_BYTES, + SYSTEM_RECORD_MAX_CLOSURE_OBJECTS, + SYSTEM_RECORD_MAX_CLOSURE_SIDECAR_LIVE_METADATA_BYTES, + SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_AGGREGATE_BYTES, + SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_METADATA_BYTES, + SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_REFERENCES, + SYSTEM_RECORD_MAX_CONFLICT_SIDECARS, + SYSTEM_RECORD_MAX_CONFLICT_DIGESTS, + SYSTEM_RECORD_MAX_CONFLICT_ENTRIES, + SYSTEM_RECORD_MAX_EIP1271_SIGNATURE_BYTES, + SYSTEM_RECORD_MAX_INVENTORY_RECORDS, + SYSTEM_RECORD_MAX_OWNED_SUBJECTS, + SYSTEM_RECORD_MAX_PEER_ID_BYTES, + SYSTEM_RECORD_MAX_PROJECTION_BYTES, + SYSTEM_RECORD_MAX_PROJECTION_QUADS, + SYSTEM_RECORD_MAX_RESOLVED_FORK_TUPLES, + SYSTEM_RECORD_MAX_ROOT_CLAIMS, + SYSTEM_RECORD_MAX_JSON_DEPTH, + SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH, + SYSTEM_RECORD_MAX_SIGNED_CONTROL_JSON_DEPTH, + SYSTEM_RECORD_MAX_SIGNED_HEAD_JSON_DEPTH, + SYSTEM_RECORD_MAX_SIDECAR_BYTES, + SYSTEM_RECORD_MAX_SIDECAR_OBJECTS, + SYSTEM_RECORD_OBJECT_CAPS_V1, + SYSTEM_RECORD_SIGNATURE_DOMAINS_V1, + type SystemRecordObjectKindV1, +} from './system-record-limits-v1.js'; +import { assertNetworkIdV1, type NetworkIdV1 } from './sync-wire-identifiers.js'; +import { + assertCanonicalChainId, + assertCanonicalDecimalU64, + assertCanonicalDigest, + assertCanonicalEvmAddress, + assertCanonicalHexBytes, + parseCanonicalDecimalU64, + type ChainIdV1, + type DecimalU64V1, + type Digest32V1, + type EvmAddressV1, +} from './sync-wire-scalars.js'; +import { + hasOwnDataProperty, + snapshotDataArray, + snapshotDataRecord, + snapshotExactDataRecord, +} from './sync-wire-objects.js'; + +const UTF8 = new TextEncoder(); +const RFC3339_SECONDS = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/; +const AGENT_ROOT = /^did:dkg:agent:(0x[0-9a-f]{40})$/; +const REQUEST_RECORD_KIND = SYSTEM_RECORD_KIND_V1; + +export type CanonicalRfc3339SecondsV1 = string & { readonly __rfc3339SecondsV1: true }; +export { + assertCanonicalSystemRecordPeerIdV1, + decodeUnpaddedBase64UrlV1, + digestSystemRecordBytesV1, + SystemRecordObjectErrorV1, +}; +export type { SystemRecordObjectErrorCodeV1, SystemRecordPeerPublicKeyV1 }; + +export interface AgentProfileHeadCommonV1 { + readonly objectType: 'agent-profile-head'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly state: 'active' | 'tombstone'; + readonly networkId: NetworkIdV1; + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerPublicKeyV1; + readonly authoritySequence: DecimalU64V1; + readonly version: DecimalU64V1; + readonly previousHeadDigest?: Digest32V1; + readonly acceptedTransitionDigest?: Digest32V1; + readonly forkResolutionDigest?: Digest32V1; + readonly evmIssuer: EvmAddressV1; + readonly rootSubject: string; + readonly projectionSchemaDigest: Digest32V1; + readonly issuedAt: CanonicalRfc3339SecondsV1; + readonly ownedSubjectTableDigest: Digest32V1; + readonly ownedSubjectCount: DecimalU64V1; + readonly projectionBytes: DecimalU64V1; + readonly projectionQuads: DecimalU64V1; +} + +export interface AgentProfileActiveHeadObjectV1 extends AgentProfileHeadCommonV1 { + readonly state: 'active'; + readonly validUntil: CanonicalRfc3339SecondsV1; + readonly assertionCoordinate: AssertionCoordinateV1; + readonly graphScopedAuthorSeal: CanonicalGraphScopedAuthorSealV1; + readonly contentDigest: Digest32V1; + readonly bundleDigest: Digest32V1; +} + +export interface AgentProfileTombstoneHeadObjectV1 extends AgentProfileHeadCommonV1 { + readonly state: 'tombstone'; + readonly previousHeadDigest: Digest32V1; +} + +export type AgentProfileHeadObjectV1 = + | AgentProfileActiveHeadObjectV1 + | AgentProfileTombstoneHeadObjectV1; + +export interface AgentProfileAuthorityTransitionV1 { + readonly objectType: 'authority-transition'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly mode: 'co-signed' | 'expired-prior'; + readonly networkId: NetworkIdV1; + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerPublicKeyV1; + readonly priorAuthoritySequence: DecimalU64V1; + readonly nextAuthoritySequence: DecimalU64V1; + readonly priorHeadDigest: Digest32V1; + readonly priorEvmIssuer: EvmAddressV1; + readonly nextEvmIssuer: EvmAddressV1; + readonly nextRoot: string; + readonly issuedAt: CanonicalRfc3339SecondsV1; + readonly priorValidUntil?: CanonicalRfc3339SecondsV1; +} + +export interface AgentProfileForkResolutionV1 { + readonly objectType: 'fork-resolution'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerPublicKeyV1; + readonly evmIssuer: EvmAddressV1; + readonly authoritySequence: DecimalU64V1; + readonly forkedVersion: DecimalU64V1; + readonly resolutionVersion: DecimalU64V1; + readonly forkBaseHeadDigest?: Digest32V1; + readonly evidenceHeadDigests: readonly Digest32V1[]; + readonly issuedAt: CanonicalRfc3339SecondsV1; +} + +export interface AgentProfileForkConflictEntryV1 { + readonly type: 'fork'; + readonly authoritySequence: DecimalU64V1; + readonly version: DecimalU64V1; + readonly objectDigests: readonly Digest32V1[]; +} + +export interface AgentProfileTransitionConflictEntryV1 { + readonly type: 'transition'; + readonly priorAuthoritySequence: DecimalU64V1; + readonly nextAuthoritySequence: DecimalU64V1; + readonly objectDigests: readonly Digest32V1[]; +} + +export interface AgentProfileConflictEvidenceV1 { + readonly objectType: 'conflict-evidence'; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly peerId: string; + readonly entries: readonly ( + | AgentProfileForkConflictEntryV1 + | AgentProfileTransitionConflictEntryV1 + )[]; +} + +export type SystemRecordSignatureRoleV1 = 'peer' | 'prior-evm' | 'next-evm' | 'current-evm'; +export type SystemRecordSignatureSuiteV1 = + | 'ed25519-v1' + | 'eip191-personal-sign-digest-v1' + | 'eip1271-current-finalized-v1'; + +export interface SystemRecordNoSignatureEvidenceV1 { + readonly kind: 'none'; +} + +export interface SystemRecordEip1271EvidenceV1 { + readonly kind: 'eip1271-current-finalized'; + readonly chainId: ChainIdV1; + readonly contractAddress: EvmAddressV1; + readonly finalizedBlockNumber: DecimalU64V1; + readonly finalizedBlockHash: Digest32V1; +} + +export interface SystemRecordSignatureEntryV1 { + readonly role: SystemRecordSignatureRoleV1; + readonly suite: SystemRecordSignatureSuiteV1; + readonly signer: string; + readonly evidence: SystemRecordNoSignatureEvidenceV1 | SystemRecordEip1271EvidenceV1; + readonly signature: string; +} + +export interface SignedSystemRecordEnvelopeV1 { + readonly object: T; + readonly objectDigest: Digest32V1; + readonly signatures: readonly SystemRecordSignatureEntryV1[]; +} + +export type SignedAgentProfileHeadEnvelopeV1 = SignedSystemRecordEnvelopeV1; +export type SignedAgentProfileAuthorityTransitionEnvelopeV1 = + SignedSystemRecordEnvelopeV1; +export type SignedAgentProfileForkResolutionEnvelopeV1 = + SignedSystemRecordEnvelopeV1; + +export type OwnedSubjectTableObjectV1 = readonly string[]; + +export function assertCanonicalRfc3339SecondsV1( + value: unknown, + label = 'timestamp', +): asserts value is CanonicalRfc3339SecondsV1 { + if (typeof value !== 'string') fail('system-record-scalar', `${label} must be an RFC3339 UTC second`); + const match = RFC3339_SECONDS.exec(value); + if (match === null || match[1] === '0000' || match[6] === '60') { + fail('system-record-scalar', `${label} must be YYYY-MM-DDTHH:mm:ssZ without leap seconds`); + } + const millis = Date.parse(value); + if (!Number.isFinite(millis) || new Date(millis).toISOString() !== `${value.slice(0, -1)}.000Z`) { + fail('system-record-scalar', `${label} must be a calendar-valid RFC3339 UTC second`); + } +} + +export function parseCanonicalRfc3339SecondsV1(value: CanonicalRfc3339SecondsV1): number { + assertCanonicalRfc3339SecondsV1(value); + return Date.parse(value); +} + +export function assertSystemRecordPeerBindingV1( + peerId: unknown, + peerPublicKey: unknown, +): asserts peerPublicKey is SystemRecordPeerPublicKeyV1 { + if (typeof peerId !== 'string' + || peerId.length > SYSTEM_RECORD_MAX_PEER_ID_BYTES + || UTF8.encode(peerId).byteLength > SYSTEM_RECORD_MAX_PEER_ID_BYTES) { + fail('system-record-scalar', 'peerId is outside its byte bound'); + } + try { + if (peerIdFromString(peerId).toString() !== peerId) throw new Error('noncanonical peer ID'); + const keyBytes = decodeUnpaddedBase64UrlV1( + peerPublicKey, + SYSTEM_RECORD_ED25519_PUBLIC_KEY_BYTES, + 'peerPublicKey', + ); + const derived = peerIdFromPublicKey(publicKeyFromRaw(keyBytes)).toString(); + if (derived !== peerId) fail('system-record-binding', 'peerPublicKey does not derive peerId'); + } catch (cause) { + if (cause instanceof SystemRecordObjectErrorV1) throw cause; + fail('system-record-binding', 'peerId/public-key binding is invalid', cause); + } +} + +export function assertAgentRootV1(value: unknown, issuer?: string): asserts value is string { + if (typeof value !== 'string' || !AGENT_ROOT.test(value)) { + fail('system-record-scalar', 'agent root must be a canonical did:dkg:agent address'); + } + const rootAddress = AGENT_ROOT.exec(value)![1]; + try { + assertCanonicalEvmAddress(rootAddress, 'agent root address'); + } catch (cause) { + fail('system-record-scalar', 'agent root address is invalid', cause); + } + if (issuer !== undefined && value !== `did:dkg:agent:${issuer}`) { + fail('system-record-binding', 'agent root does not match its EVM issuer'); + } +} + +export function digestSystemRecordJsonV1( + domain: string, + value: CanonicalJsonValue, + maxBytes: number, +): Digest32V1 { + return digestSystemRecordBytesV1(domain, canonicalizeJsonBytes(value, { maxBytes })); +} + +export interface SystemRecordRootCollisionEvidenceV1 { + readonly networkId: NetworkIdV1; + readonly root: string; + readonly incumbentRecordKey: readonly [NetworkIdV1, string]; + readonly contenderStableKey: Digest32V1; + readonly contenderHeadDigest: Digest32V1; +} + +export function canonicalizeSystemRecordRootCollisionEvidenceV1( + value: SystemRecordRootCollisionEvidenceV1, +): Uint8Array { + const evidence = snapshotExactDataRecord( + value, + ['networkId', 'root', 'incumbentRecordKey', 'contenderStableKey', 'contenderHeadDigest'], + 'root-collision evidence', + ); + const incumbentRecordKey = snapshotRootCollisionRecordKey(evidence.incumbentRecordKey); + assertNetwork(evidence.networkId); + assertAgentRootV1(evidence.root); + if (incumbentRecordKey[0] !== evidence.networkId) { + fail('system-record-binding', 'root-collision incumbent record key must bind networkId'); + } + assertCanonicalSystemRecordPeerIdV1(incumbentRecordKey[1]); + digest(evidence.contenderStableKey, 'contenderStableKey'); + digest(evidence.contenderHeadDigest, 'contenderHeadDigest'); + return canonicalizeJsonBytes([ + evidence.networkId, + evidence.root, + incumbentRecordKey, + evidence.contenderStableKey, + evidence.contenderHeadDigest, + ], { maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['conflict-evidence'] }); +} + +function snapshotRootCollisionRecordKey(value: unknown): readonly [NetworkIdV1, string] { + try { + return snapshotDataArray(value, 'root-collision incumbent record key', { + minLength: 2, + maxLength: 2, + }) as readonly [NetworkIdV1, string]; + } catch (cause) { + fail('system-record-schema', 'root-collision incumbent record key must be a closed two-item tuple', cause); + } +} + +export function computeSystemRecordRootCollisionEvidenceDigestV1( + value: SystemRecordRootCollisionEvidenceV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.rootCollisionEvidence, + canonicalizeSystemRecordRootCollisionEvidenceV1(value), + ); +} + +export const EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1 = digestSystemRecordJsonV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.ownedSubjectTable, + [], + SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], +); + +const HEAD_COMMON_KEYS = [ + 'objectType', 'kind', 'state', 'networkId', 'peerId', 'peerPublicKey', + 'authoritySequence', 'version', 'evmIssuer', 'rootSubject', + 'projectionSchemaDigest', 'issuedAt', 'ownedSubjectTableDigest', + 'ownedSubjectCount', 'projectionBytes', 'projectionQuads', +] as const; +const HEAD_OPTIONAL_DIGEST_KEYS = [ + 'previousHeadDigest', 'acceptedTransitionDigest', 'forkResolutionDigest', +] as const; +const ACTIVE_HEAD_KEYS = [ + 'validUntil', 'assertionCoordinate', 'graphScopedAuthorSeal', 'contentDigest', + 'bundleDigest', +] as const; + +export function assertAgentProfileHeadObjectV1( + value: unknown, +): asserts value is AgentProfileHeadObjectV1 { + validateAgentProfileHeadObjectV1(value); +} + +export function canonicalizeAgentProfileHeadObjectV1( + value: AgentProfileHeadObjectV1, +): Uint8Array { + const validated = validateAgentProfileHeadObjectV1(value); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['agent-profile-head'], + }); +} + +export function parseCanonicalAgentProfileHeadObjectV1( + input: string | Uint8Array, +): AgentProfileHeadObjectV1 { + const parsed = parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['agent-profile-head'], + maxDepth: SYSTEM_RECORD_MAX_JSON_DEPTH, + }); + return validateAgentProfileHeadObjectV1(parsed); +} + +export function computeAgentProfileHeadObjectDigestV1( + value: AgentProfileHeadObjectV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.agentProfileHead, + canonicalizeAgentProfileHeadObjectV1(value), + ); +} + +function validateAgentProfileHeadObjectV1(value: unknown): AgentProfileHeadObjectV1 { + const probe = snapshotSystemRecordDataRecord(value, 'agent profile head'); + const state = probe.state; + if (state !== 'active' && state !== 'tombstone') { + fail('system-record-schema', 'agent profile head state must be active or tombstone'); + } + const optional = HEAD_OPTIONAL_DIGEST_KEYS.filter((key) => hasOwnDataProperty(probe, key)); + const expected = state === 'active' + ? [...HEAD_COMMON_KEYS, ...optional, ...ACTIVE_HEAD_KEYS] + : [...HEAD_COMMON_KEYS, ...optional]; + const head = snapshotExactDataRecord(probe, expected, 'agent profile head'); + if (head.objectType !== 'agent-profile-head' || head.kind !== REQUEST_RECORD_KIND) { + fail('system-record-schema', 'agent profile head tag is invalid'); + } + assertNetwork(head.networkId); + assertSystemRecordPeerBindingV1(head.peerId, head.peerPublicKey); + const authoritySequence = u64(head.authoritySequence, 'authoritySequence'); + const version = u64(head.version, 'version'); + if (authoritySequence > SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX) { + fail('system-record-limit', 'authoritySequence exceeds the V1 root-claim bound'); + } + for (const key of optional) digest(head[key], key); + address(head.evmIssuer, 'evmIssuer'); + assertAgentRootV1(head.rootSubject, head.evmIssuer as string); + digest(head.projectionSchemaDigest, 'projectionSchemaDigest'); + assertCanonicalRfc3339SecondsV1(head.issuedAt, 'issuedAt'); + digest(head.ownedSubjectTableDigest, 'ownedSubjectTableDigest'); + const ownedSubjectCount = u64(head.ownedSubjectCount, 'ownedSubjectCount'); + const projectionBytes = u64(head.projectionBytes, 'projectionBytes'); + const projectionQuads = u64(head.projectionQuads, 'projectionQuads'); + if (ownedSubjectCount > BigInt(SYSTEM_RECORD_MAX_OWNED_SUBJECTS)) { + fail('system-record-limit', 'ownedSubjectCount exceeds the V1 subject bound'); + } + if (projectionBytes > BigInt(SYSTEM_RECORD_MAX_PROJECTION_BYTES) + || projectionQuads > BigInt(SYSTEM_RECORD_MAX_PROJECTION_QUADS)) { + fail('system-record-limit', 'profile projection exceeds the V1 bound'); + } + + const previous = hasOwnDataProperty(head, 'previousHeadDigest'); + const transition = hasOwnDataProperty(head, 'acceptedTransitionDigest'); + const resolution = hasOwnDataProperty(head, 'forkResolutionDigest'); + if (authoritySequence === 0n && transition) { + fail('system-record-history', 'sequence zero must omit acceptedTransitionDigest'); + } + if (authoritySequence > 0n && !transition) { + fail('system-record-history', 'nonzero authority sequence requires acceptedTransitionDigest'); + } + if (authoritySequence === 0n && version === 0n && (previous || transition || resolution)) { + fail('system-record-history', 'ordinary initial head must omit all history digests'); + } + if (version === 0n && previous) { + fail('system-record-history', 'version-zero head must omit previousHeadDigest'); + } + if (!resolution && version > 0n && !previous) { + fail('system-record-history', 'ordinary noninitial head requires previousHeadDigest'); + } + + let graphScopedAuthorSeal: Readonly | undefined; + if (state === 'active') { + assertCanonicalRfc3339SecondsV1(head.validUntil, 'validUntil'); + if (Date.parse(head.validUntil as string) <= Date.parse(head.issuedAt as string)) { + fail('system-record-history', 'validUntil must be later than issuedAt'); + } + try { + assertAssertionCoordinateV1(head.assertionCoordinate); + const sealSnapshot = snapshotDataRecord( + head.graphScopedAuthorSeal, + 'graph-scoped author seal', + ); + assertCanonicalGraphScopedAuthorSealV1(sealSnapshot); + graphScopedAuthorSeal = sealSnapshot; + } catch (cause) { + fail('system-record-binding', 'active head coordinate/seal is invalid', cause); + } + digest(head.contentDigest, 'contentDigest'); + digest(head.bundleDigest, 'bundleDigest'); + if (ownedSubjectCount === 0n || projectionBytes === 0n || projectionQuads === 0n) { + fail('system-record-binding', 'active head projection counts must be nonzero'); + } + const seal = graphScopedAuthorSeal; + const ual = parseDeterministicKnowledgeAssetUal(seal.kaUal); + if (ual.chainId !== head.networkId) { + fail('system-record-binding', 'graph-scoped seal UAL network must equal the head networkId'); + } + if (seal.assertedAtChainId !== numericChainIdForNetworkV1(head.networkId)) { + fail('system-record-binding', 'graph-scoped seal asserted chain must equal the head network chain'); + } + if (seal.authorAddress !== head.evmIssuer) { + fail('system-record-binding', 'graph-scoped seal author must equal evmIssuer'); + } + if (seal.assertionMerkleRoot !== head.contentDigest) { + fail('system-record-binding', 'contentDigest must equal the graph-scoped assertion Merkle root'); + } + if (seal.publicTripleCount !== head.projectionQuads) { + fail('system-record-binding', 'projectionQuads must equal the graph-scoped public triple count'); + } + if (seal.privateTripleCount !== '0' || seal.privateMerkleRoot !== null) { + fail('system-record-binding', 'agents system records require a public-only graph-scoped seal'); + } + if (head.ownedSubjectTableDigest === EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1) { + fail('system-record-binding', 'active head must commit a nonempty owned-subject table'); + } + } else { + if (!previous || version === 0n) { + fail('system-record-history', 'tombstone must name an accepted active predecessor'); + } + if (resolution) { + fail('system-record-history', 'V1 has no direct terminal fork-resolution tombstone'); + } + if (ownedSubjectCount !== 0n || projectionBytes !== 0n || projectionQuads !== 0n) { + fail('system-record-binding', 'tombstone projection accounting must be zero'); + } + if (head.ownedSubjectTableDigest !== EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1) { + fail('system-record-binding', 'tombstone must commit the canonical empty subject table'); + } + } + return Object.freeze(state === 'active' + ? { ...head, graphScopedAuthorSeal } + : { ...head }) as unknown as AgentProfileHeadObjectV1; +} + +export function assertAgentProfileAuthorityTransitionV1( + value: unknown, +): asserts value is AgentProfileAuthorityTransitionV1 { + validateAuthorityTransition(value); +} + +export function canonicalizeAgentProfileAuthorityTransitionV1( + value: AgentProfileAuthorityTransitionV1, +): Uint8Array { + const validated = validateAuthorityTransition(value); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['authority-transition'], + }); +} + +export function parseCanonicalAgentProfileAuthorityTransitionV1( + input: string | Uint8Array, +): AgentProfileAuthorityTransitionV1 { + return validateAuthorityTransition(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['authority-transition'], maxDepth: SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH, + })); +} + +export function computeAgentProfileAuthorityTransitionDigestV1( + value: AgentProfileAuthorityTransitionV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.authorityTransition, + canonicalizeAgentProfileAuthorityTransitionV1(value), + ); +} + +function validateAuthorityTransition(value: unknown): AgentProfileAuthorityTransitionV1 { + const probe = snapshotSystemRecordDataRecord(value, 'authority transition'); + const mode = probe.mode; + if (mode !== 'co-signed' && mode !== 'expired-prior') { + fail('system-record-schema', 'authority transition mode is invalid'); + } + const expected = [ + 'objectType', 'kind', 'mode', 'networkId', 'peerId', 'peerPublicKey', + 'priorAuthoritySequence', 'nextAuthoritySequence', 'priorHeadDigest', + 'priorEvmIssuer', 'nextEvmIssuer', 'nextRoot', 'issuedAt', + ...(mode === 'expired-prior' ? ['priorValidUntil'] : []), + ] as const; + const transition = snapshotExactDataRecord(probe, expected, 'authority transition'); + if (transition.objectType !== 'authority-transition' || transition.kind !== REQUEST_RECORD_KIND) { + fail('system-record-schema', 'authority transition tag is invalid'); + } + assertNetwork(transition.networkId); + assertSystemRecordPeerBindingV1(transition.peerId, transition.peerPublicKey); + const prior = u64(transition.priorAuthoritySequence, 'priorAuthoritySequence'); + const next = u64(transition.nextAuthoritySequence, 'nextAuthoritySequence'); + if (next !== prior + 1n || next > SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX) { + fail('system-record-history', 'authority transition must increment within the V1 bound'); + } + digest(transition.priorHeadDigest, 'priorHeadDigest'); + address(transition.priorEvmIssuer, 'priorEvmIssuer'); + address(transition.nextEvmIssuer, 'nextEvmIssuer'); + assertAgentRootV1(transition.nextRoot, transition.nextEvmIssuer as string); + if (transition.priorEvmIssuer === transition.nextEvmIssuer) { + fail('system-record-history', 'authority transition must rotate to a new wallet root'); + } + assertCanonicalRfc3339SecondsV1(transition.issuedAt, 'issuedAt'); + if (mode === 'expired-prior') { + assertCanonicalRfc3339SecondsV1(transition.priorValidUntil, 'priorValidUntil'); + } + return Object.freeze({ ...transition }) as unknown as AgentProfileAuthorityTransitionV1; +} + +export function assertAgentProfileForkResolutionV1( + value: unknown, +): asserts value is AgentProfileForkResolutionV1 { + validateForkResolution(value); +} + +export function canonicalizeAgentProfileForkResolutionV1( + value: AgentProfileForkResolutionV1, +): Uint8Array { + const validated = validateForkResolution(value); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['fork-resolution'], + }); +} + +export function parseCanonicalAgentProfileForkResolutionV1( + input: string | Uint8Array, +): AgentProfileForkResolutionV1 { + return validateForkResolution(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['fork-resolution'], maxDepth: SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH, + })); +} + +export function computeAgentProfileForkResolutionDigestV1( + value: AgentProfileForkResolutionV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.forkResolution, + canonicalizeAgentProfileForkResolutionV1(value), + ); +} + +function validateForkResolution(value: unknown): AgentProfileForkResolutionV1 { + const probe = snapshotSystemRecordDataRecord(value, 'fork resolution'); + const expected = [ + 'objectType', 'kind', 'networkId', 'peerId', 'peerPublicKey', 'evmIssuer', + 'authoritySequence', 'forkedVersion', 'resolutionVersion', + ...(hasOwnDataProperty(probe, 'forkBaseHeadDigest') ? ['forkBaseHeadDigest'] : []), + 'evidenceHeadDigests', 'issuedAt', + ] as const; + const resolution = snapshotExactDataRecord(probe, expected, 'fork resolution'); + if (resolution.objectType !== 'fork-resolution' || resolution.kind !== REQUEST_RECORD_KIND) { + fail('system-record-schema', 'fork resolution tag is invalid'); + } + assertNetwork(resolution.networkId); + assertSystemRecordPeerBindingV1(resolution.peerId, resolution.peerPublicKey); + address(resolution.evmIssuer, 'evmIssuer'); + const sequence = u64(resolution.authoritySequence, 'authoritySequence'); + const forked = u64(resolution.forkedVersion, 'forkedVersion'); + const version = u64(resolution.resolutionVersion, 'resolutionVersion'); + if (sequence > SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX || version <= forked) { + fail('system-record-history', 'fork resolution sequence/version is invalid'); + } + if ((forked === 0n) === hasOwnDataProperty(resolution, 'forkBaseHeadDigest')) { + fail('system-record-history', 'fork base is omitted only for a version-zero fork'); + } + if (hasOwnDataProperty(resolution, 'forkBaseHeadDigest')) digest(resolution.forkBaseHeadDigest, 'forkBaseHeadDigest'); + const evidenceHeadDigests = digestArray( + resolution.evidenceHeadDigests, + 'evidenceHeadDigests', + 2, + SYSTEM_RECORD_MAX_CONFLICT_DIGESTS, + ); + assertCanonicalRfc3339SecondsV1(resolution.issuedAt, 'issuedAt'); + return Object.freeze({ ...resolution, evidenceHeadDigests }) as unknown as AgentProfileForkResolutionV1; +} + +export function assertAgentProfileConflictEvidenceV1( + value: unknown, +): asserts value is AgentProfileConflictEvidenceV1 { + validateConflictEvidence(value); +} + +export function canonicalizeAgentProfileConflictEvidenceV1( + value: AgentProfileConflictEvidenceV1, +): Uint8Array { + const validated = validateConflictEvidence(value); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['conflict-evidence'], + }); +} + +export function parseCanonicalAgentProfileConflictEvidenceV1( + input: string | Uint8Array, +): AgentProfileConflictEvidenceV1 { + return validateConflictEvidence(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['conflict-evidence'], maxDepth: SYSTEM_RECORD_MAX_SHALLOW_JSON_DEPTH, + })); +} + +export function computeAgentProfileConflictEvidenceDigestV1( + value: AgentProfileConflictEvidenceV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.conflictEvidence, + canonicalizeAgentProfileConflictEvidenceV1(value), + ); +} + +function validateConflictEvidence(value: unknown): AgentProfileConflictEvidenceV1 { + const evidence = snapshotExactDataRecord( + value, + ['objectType', 'kind', 'networkId', 'peerId', 'entries'], + 'conflict evidence', + ); + if (evidence.objectType !== 'conflict-evidence' || evidence.kind !== REQUEST_RECORD_KIND) { + fail('system-record-schema', 'conflict evidence tag is invalid'); + } + assertNetwork(evidence.networkId); + assertCanonicalSystemRecordPeerIdV1(evidence.peerId); + let conflictEntries: readonly unknown[]; + try { + conflictEntries = snapshotDataArray(evidence.entries, 'conflict evidence entries', { + minLength: 1, + maxLength: SYSTEM_RECORD_MAX_CONFLICT_ENTRIES, + }); + } catch (cause) { + fail('system-record-limit', 'conflict evidence must contain 1-8 closed entries', cause); + } + let totalDigests = 0; + let priorSortKey = ''; + const entries = conflictEntries.map((candidate, index) => { + const probe = snapshotSystemRecordDataRecord(candidate, `conflict evidence entry ${index}`); + let entry: AgentProfileForkConflictEntryV1 | AgentProfileTransitionConflictEntryV1; + let sortKey: string; + if (probe.type === 'fork') { + const row = snapshotExactDataRecord( + probe, + ['type', 'authoritySequence', 'version', 'objectDigests'], + `fork conflict entry ${index}`, + ); + const sequence = u64(row.authoritySequence, 'authoritySequence'); + const version = u64(row.version, 'version'); + const objectDigests = digestArray( + row.objectDigests, + 'objectDigests', + 2, + SYSTEM_RECORD_MAX_CONFLICT_DIGESTS, + ); + sortKey = `0:${sequence.toString().padStart(20, '0')}:${version.toString().padStart(20, '0')}`; + entry = Object.freeze({ ...row, objectDigests }) as unknown as AgentProfileForkConflictEntryV1; + } else if (probe.type === 'transition') { + const row = snapshotExactDataRecord( + probe, + ['type', 'priorAuthoritySequence', 'nextAuthoritySequence', 'objectDigests'], + `transition conflict entry ${index}`, + ); + const prior = u64(row.priorAuthoritySequence, 'priorAuthoritySequence'); + const next = u64(row.nextAuthoritySequence, 'nextAuthoritySequence'); + if (next !== prior + 1n) fail('system-record-history', 'transition conflict tuple must increment by one'); + const objectDigests = digestArray( + row.objectDigests, + 'objectDigests', + 2, + SYSTEM_RECORD_MAX_CONFLICT_DIGESTS, + ); + sortKey = `1:${prior.toString().padStart(20, '0')}:${next.toString().padStart(20, '0')}`; + entry = Object.freeze({ ...row, objectDigests }) as unknown as AgentProfileTransitionConflictEntryV1; + } else { + fail('system-record-schema', 'conflict entry type is invalid'); + } + if (index > 0 && priorSortKey >= sortKey) { + fail('system-record-order', 'conflict evidence entries must use canonical type/tuple order'); + } + priorSortKey = sortKey; + totalDigests += entry.objectDigests.length; + return entry; + }); + if (totalDigests > SYSTEM_RECORD_MAX_CONFLICT_DIGESTS) { + fail('system-record-limit', 'conflict evidence exceeds 16 total object digests'); + } + return Object.freeze({ ...evidence, entries: Object.freeze(entries) }) as unknown as AgentProfileConflictEvidenceV1; +} + +export type AgentProfileOwnedSubjectKindV1 = + | 'root' + | 'capability' + | 'offering' + | 'registration' + | 'hosting' + | 'x25519'; + +const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; +const SCHEMA = 'https://schema.org/'; +const DKG = 'https://dkg.network/ontology#'; +const ERC8004 = 'https://eips.ethereum.org/erc-8004#'; +const PROV = 'http://www.w3.org/ns/prov#'; +const SKILL = 'https://dkg.origintrail.io/skill#'; + +export const AGENT_PROFILE_LINK_PREDICATES_V1 = Object.freeze({ + capability: `${ERC8004}capabilities`, + offering: `${SKILL}offersSkill`, + registration: `${PROV}wasGeneratedBy`, + hosting: `${SKILL}hostingProfile`, +} as const); + +const AGENT_PROFILE_PREDICATES_V1: Readonly>> = { + root: new Set([ + RDF_TYPE, `${SCHEMA}name`, `${SCHEMA}description`, `${DKG}peerId`, `${DKG}nodeRole`, + `${DKG}publicKey`, `${DKG}relayAddress`, `${DKG}agentAddress`, `${DKG}multiaddr`, + `${DKG}lastSeen`, `${DKG}publicEncryptionKey`, `${DKG}encryptionKeyAlgorithm`, + `${DKG}encryptionKeyProof`, `${SKILL}framework`, + ...Object.values(AGENT_PROFILE_LINK_PREDICATES_V1), + ]), + capability: new Set([RDF_TYPE, `${SCHEMA}name`]), + offering: new Set([ + RDF_TYPE, `${SKILL}skill`, `${SKILL}pricePerCall`, `${SKILL}currency`, + `${SKILL}successRate`, `${SKILL}pricing`, + ]), + registration: new Set([RDF_TYPE, `${PROV}atTime`]), + hosting: new Set([RDF_TYPE, `${SKILL}contextGraphsServed`, `${SKILL}paranetsServed`]), + x25519: new Set([`${DKG}revokedAt`, `${DKG}revokedBy`, `${DKG}encryptionKeyRevocationProof`]), +}; + +export function classifyAgentProfileOwnedSubjectV1( + rootSubject: string, + subject: string, +): AgentProfileOwnedSubjectKindV1 | null { + if (typeof rootSubject !== 'string' || typeof subject !== 'string' + || rootSubject.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'] + || subject.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table']) return null; + if (!AGENT_ROOT.test(rootSubject)) return null; + if (subject === rootSubject) return 'root'; + const wellKnown = `${rootSubject}/.well-known/genid/`; + if (subject.startsWith(wellKnown)) { + const suffix = subject.slice(wellKnown.length); + if (/^cap[1-9][0-9]*$/.test(suffix)) return 'capability'; + if (/^offering[1-9][0-9]*$/.test(suffix)) return 'offering'; + if (suffix === 'registration') return 'registration'; + if (suffix === 'hosting') return 'hosting'; + return null; + } + const encryptionPrefix = `${rootSubject}#x25519-`; + return subject.startsWith(encryptionPrefix) + && /^[0-9a-f]{32}$/.test(subject.slice(encryptionPrefix.length)) + ? 'x25519' + : null; +} + +export function isAllowedAgentProfilePredicateV1( + kind: AgentProfileOwnedSubjectKindV1, + predicate: string, +): boolean { + return AGENT_PROFILE_PREDICATES_V1[kind].has(predicate); +} + +export function assertDerivedAgentEncryptionSubjectV1( + rootSubject: string, + subject: string, + publicKeyBytes: Uint8Array, +): void { + assertAgentRootV1(rootSubject); + let ownedPublicKey: Uint8Array; + try { + ownedPublicKey = copyBoundedSystemRecordBytesV1(publicKeyBytes, 32, 'x25519 public key'); + } catch (cause) { + fail('system-record-binding', 'x25519 public key must contain exactly 32 bytes', cause); + } + if (ownedPublicKey.byteLength !== 32) { + fail('system-record-binding', 'x25519 public key must contain exactly 32 bytes'); + } + const address = AGENT_ROOT.exec(rootSubject)![1]; + const expected = workspaceAgentEncryptionKeyId(address, ownedPublicKey); + if (subject !== expected) { + fail('system-record-binding', 'x25519 owned subject is not derived from its root and public key'); + } +} + +export function assertOwnedSubjectTableObjectV1( + rootSubject: string, + value: unknown, +): asserts value is OwnedSubjectTableObjectV1 { + validateOwnedSubjectTableObjectV1(rootSubject, value); +} + +function validateOwnedSubjectTableObjectV1( + rootSubject: string, + value: unknown, +): OwnedSubjectTableObjectV1 { + assertAgentRootV1(rootSubject); + let subjects: readonly unknown[]; + try { + subjects = snapshotDataArray(value, 'owned-subject table', { + maxLength: SYSTEM_RECORD_MAX_OWNED_SUBJECTS, + }); + } catch (cause) { + fail('system-record-limit', 'owned-subject table exceeds its closed-array bound', cause); + } + let previous: Uint8Array | undefined; + let encodedLowerBound = 2; + for (const candidate of subjects) { + if (typeof candidate !== 'string' + || candidate.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'] + || classifyAgentProfileOwnedSubjectV1(rootSubject, candidate) === null) { + fail('system-record-binding', 'owned-subject table contains an invalid subject'); + } + const bytes = UTF8.encode(candidate); + encodedLowerBound += bytes.byteLength + (previous === undefined ? 2 : 3); + if (encodedLowerBound > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table']) { + fail('system-record-limit', 'owned-subject table exceeds its encoded byte cap'); + } + if (previous !== undefined && compareBytes(previous, bytes) >= 0) { + fail('system-record-order', 'owned-subject table must be UTF-8 sorted and duplicate-free'); + } + previous = bytes; + } + canonicalizeJson(subjects as readonly string[], { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], + }); + return subjects as OwnedSubjectTableObjectV1; +} + +export function canonicalizeOwnedSubjectTableObjectV1( + rootSubject: string, + value: OwnedSubjectTableObjectV1, +): Uint8Array { + const validated = validateOwnedSubjectTableObjectV1(rootSubject, value); + return canonicalizeJsonBytes(validated, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], + }); +} + +export function parseCanonicalOwnedSubjectTableObjectV1( + rootSubject: string, + input: string | Uint8Array, +): OwnedSubjectTableObjectV1 { + const parsed = parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], maxDepth: SYSTEM_RECORD_MAX_ARRAY_JSON_DEPTH, + }); + return validateOwnedSubjectTableObjectV1(rootSubject, parsed); +} + +export function computeOwnedSubjectTableDigestV1( + rootSubject: string, + value: OwnedSubjectTableObjectV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.ownedSubjectTable, + canonicalizeOwnedSubjectTableObjectV1(rootSubject, value), + ); +} + +const SIGNATURE_ROLE_ORDER: Readonly> = { + peer: 0, + 'prior-evm': 1, + 'next-evm': 2, + 'current-evm': 3, +}; + +export function assertSignedAgentProfileHeadEnvelopeV1( + value: unknown, +): asserts value is SignedAgentProfileHeadEnvelopeV1 { + validateSignedEnvelope(value, 'head'); +} + +export function assertSignedAgentProfileAuthorityTransitionEnvelopeV1( + value: unknown, +): asserts value is SignedAgentProfileAuthorityTransitionEnvelopeV1 { + validateSignedEnvelope(value, 'transition'); +} + +export function assertSignedAgentProfileForkResolutionEnvelopeV1( + value: unknown, +): asserts value is SignedAgentProfileForkResolutionEnvelopeV1 { + validateSignedEnvelope(value, 'fork'); +} + +export function canonicalizeSignedSystemRecordEnvelopeV1( + value: SignedSystemRecordEnvelopeV1, +): Uint8Array { + const { kind, validated } = validateDispatchedSignedEnvelope(value); + return canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1[objectKindForEnvelope(kind)], + }); +} + +export function computeSignedSystemRecordEnvelopeDigestV1( + value: SignedSystemRecordEnvelopeV1, +): Digest32V1 { + return digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.signedEnvelope, + canonicalizeSignedSystemRecordEnvelopeV1(value), + ); +} + +export function parseCanonicalSignedAgentProfileHeadEnvelopeV1( + input: string | Uint8Array, +): SignedAgentProfileHeadEnvelopeV1 { + return validateSignedEnvelope(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['agent-profile-head'], maxDepth: SYSTEM_RECORD_MAX_SIGNED_HEAD_JSON_DEPTH, + }), 'head') as SignedAgentProfileHeadEnvelopeV1; +} + +export function parseCanonicalSignedAgentProfileAuthorityTransitionEnvelopeV1( + input: string | Uint8Array, +): SignedAgentProfileAuthorityTransitionEnvelopeV1 { + return validateSignedEnvelope(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['authority-transition'], maxDepth: SYSTEM_RECORD_MAX_SIGNED_CONTROL_JSON_DEPTH, + }), 'transition') as SignedAgentProfileAuthorityTransitionEnvelopeV1; +} + +export function parseCanonicalSignedAgentProfileForkResolutionEnvelopeV1( + input: string | Uint8Array, +): SignedAgentProfileForkResolutionEnvelopeV1 { + return validateSignedEnvelope(parseCanonicalJson(input, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['fork-resolution'], maxDepth: SYSTEM_RECORD_MAX_SIGNED_CONTROL_JSON_DEPTH, + }), 'fork') as SignedAgentProfileForkResolutionEnvelopeV1; +} + +function validateSignedEnvelope( + value: unknown, + kind: 'head' | 'transition' | 'fork', +): SignedSystemRecordEnvelopeV1 { + const envelope = snapshotExactDataRecord( + value, + ['object', 'objectDigest', 'signatures'], + 'signed system-record envelope', + ); + const object = kind === 'head' + ? validateAgentProfileHeadObjectV1(envelope.object) + : kind === 'transition' + ? validateAuthorityTransition(envelope.object) + : validateForkResolution(envelope.object); + const expectedDigest = kind === 'head' + ? computeAgentProfileHeadObjectDigestV1(object as AgentProfileHeadObjectV1) + : kind === 'transition' + ? computeAgentProfileAuthorityTransitionDigestV1(object as AgentProfileAuthorityTransitionV1) + : computeAgentProfileForkResolutionDigestV1(object as AgentProfileForkResolutionV1); + digest(envelope.objectDigest, 'objectDigest'); + if (envelope.objectDigest !== expectedDigest) { + fail('system-record-binding', 'signed envelope objectDigest does not match the object'); + } + const requiredRoles: readonly SystemRecordSignatureRoleV1[] = kind === 'transition' + ? (object as AgentProfileAuthorityTransitionV1).mode === 'co-signed' + ? ['peer', 'prior-evm', 'next-evm'] + : ['peer', 'next-evm'] + : ['peer', 'current-evm']; + let signatureEntries: readonly unknown[]; + try { + signatureEntries = snapshotDataArray(envelope.signatures, 'signed envelope signatures', { + minLength: requiredRoles.length, + maxLength: requiredRoles.length, + }); + } catch (cause) { + fail('system-record-signature', 'signed envelope has the wrong closed signature cardinality', cause); + } + const signatures = signatureEntries.map((entry, index) => + validateSignatureEntry(entry, requiredRoles[index], object)); + return Object.freeze({ object, objectDigest: envelope.objectDigest, signatures: Object.freeze(signatures) }); +} + +function validateSignatureEntry( + value: unknown, + requiredRole: SystemRecordSignatureRoleV1, + object: AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1, +): SystemRecordSignatureEntryV1 { + const entry = snapshotExactDataRecord( + value, + ['role', 'suite', 'signer', 'evidence', 'signature'], + `signature entry ${requiredRole}`, + ); + if (entry.role !== requiredRole || SIGNATURE_ROLE_ORDER[requiredRole] === undefined) { + fail('system-record-signature', 'signature roles are missing, extra, duplicated, or reordered'); + } + const isPeer = requiredRole === 'peer'; + let evidence: SystemRecordSignatureEntryV1['evidence']; + if (isPeer) { + if (entry.suite !== 'ed25519-v1' || entry.signer !== object.peerId) { + fail('system-record-signature', 'peer signature suite/signer is invalid'); + } + evidence = exactNoneEvidence(entry.evidence); + decodeUnpaddedBase64UrlV1( + entry.signature, + SYSTEM_RECORD_ED25519_SIGNATURE_BYTES, + 'Ed25519 signature', + ); + } else { + const issuer = issuerForRole(object, requiredRole); + if (entry.signer !== issuer) { + fail('system-record-signature', `${requiredRole} signer does not match the object authority`); + } + if (entry.suite === 'eip191-personal-sign-digest-v1') { + evidence = exactNoneEvidence(entry.evidence); + assertCanonicalEip191SignatureV1(entry.signature); + } else if (entry.suite === 'eip1271-current-finalized-v1') { + evidence = validateEip1271Evidence(entry.evidence, issuer, object.networkId); + try { + assertCanonicalHexBytes( + entry.signature, + 'EIP-1271 signature', + 1, + SYSTEM_RECORD_MAX_EIP1271_SIGNATURE_BYTES, + ); + } catch (cause) { + fail('system-record-signature', 'EIP-1271 signature bytes are invalid', cause); + } + } else { + fail('system-record-signature', 'EVM signature suite is invalid'); + } + } + return Object.freeze({ ...entry, evidence }) as unknown as SystemRecordSignatureEntryV1; +} + +export function assertCanonicalEip191SignatureV1(value: unknown): asserts value is string { + try { + assertCanonicalHexBytes( + value, + 'EIP-191 signature', + SYSTEM_RECORD_EIP191_SIGNATURE_BYTES, + SYSTEM_RECORD_EIP191_SIGNATURE_BYTES, + ); + } catch (cause) { + fail('system-record-signature', 'EIP-191 signature bytes are invalid', cause); + } + const bytes = hexToBytes(value as string); + const s = bytesToBigInt(bytes.subarray(32, 64)); + if (s === 0n || s > SYSTEM_RECORD_EIP191_MAX_S) { + fail('system-record-signature', 'EIP-191 signature must use canonical low-s form'); + } + try { + secp256k1.Signature.fromBytes(bytes.subarray(0, 64), 'compact'); + } catch (cause) { + fail('system-record-signature', 'EIP-191 compact signature is not canonical', cause); + } + if (bytes[64] !== 27 && bytes[64] !== 28) { + fail('system-record-signature', 'EIP-191 recovery byte must be 27 or 28'); + } +} + +export function buildSystemRecordSignatureMessageV1( + object: AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1, + objectDigest: Digest32V1, + role: SystemRecordSignatureRoleV1, +): Uint8Array { + digest(objectDigest, 'objectDigest'); + const kind = classifyEnvelopeObject(object); + const validatedObject = kind === 'head' + ? validateAgentProfileHeadObjectV1(object) + : kind === 'transition' + ? validateAuthorityTransition(object) + : validateForkResolution(object); + const recordKey: CanonicalJsonValue = [validatedObject.networkId, validatedObject.peerId]; + let tuple: CanonicalJsonValue; + if (validatedObject.objectType === 'agent-profile-head') { + if (role !== 'peer' && role !== 'current-evm') fail('system-record-signature', 'head role is invalid'); + tuple = [ + 'agent-profile-head', objectDigest, validatedObject.networkId, recordKey, + validatedObject.authoritySequence, validatedObject.version, + ...(role === 'peer' ? [] : ['current-evm', validatedObject.evmIssuer]), + ]; + } else if (validatedObject.objectType === 'authority-transition') { + if (role !== 'peer' && role !== 'prior-evm' && role !== 'next-evm') { + fail('system-record-signature', 'transition role is invalid'); + } + tuple = [ + 'authority-transition', objectDigest, validatedObject.networkId, recordKey, + validatedObject.priorAuthoritySequence, validatedObject.nextAuthoritySequence, validatedObject.priorHeadDigest, + role, + ...(role === 'peer' ? [] : [issuerForRole(validatedObject, role)]), + ]; + } else { + if (role !== 'peer' && role !== 'current-evm') fail('system-record-signature', 'resolution role is invalid'); + tuple = [ + 'fork-resolution', objectDigest, validatedObject.networkId, recordKey, + validatedObject.authoritySequence, validatedObject.forkedVersion, validatedObject.resolutionVersion, + role, + ...(role === 'peer' ? [] : [validatedObject.evmIssuer]), + ]; + } + const domain = role === 'peer' + ? SYSTEM_RECORD_SIGNATURE_DOMAINS_V1.peer + : SYSTEM_RECORD_SIGNATURE_DOMAINS_V1.evm; + return concatBytes(UTF8.encode(domain), canonicalizeJsonBytes(tuple)); +} + +export interface VerifySystemRecordEnvelopeOptionsV1 { + readonly verifyEip1271?: ( + entry: SystemRecordSignatureEntryV1, + personalMessageHash: Uint8Array, + ) => boolean | Promise; +} + +/** Cryptographically verify a structurally valid envelope without opening a materializer capability. */ +export async function verifySignedSystemRecordEnvelopeV1( + envelope: SignedSystemRecordEnvelopeV1, + options: VerifySystemRecordEnvelopeOptionsV1 = {}, +): Promise { + const verifyEip1271 = options.verifyEip1271; + if (verifyEip1271 !== undefined && typeof verifyEip1271 !== 'function') return false; + const { validated } = validateDispatchedSignedEnvelope(envelope) as { + readonly validated: SignedSystemRecordEnvelopeV1; + }; + const publicKey = decodeUnpaddedBase64UrlV1( + validated.object.peerPublicKey, + SYSTEM_RECORD_ED25519_PUBLIC_KEY_BYTES, + 'peerPublicKey', + ); + for (const entry of validated.signatures) { + const message = buildSystemRecordSignatureMessageV1( + validated.object, + validated.objectDigest, + entry.role, + ); + if (entry.role === 'peer') { + const signature = decodeUnpaddedBase64UrlV1( + entry.signature, + SYSTEM_RECORD_ED25519_SIGNATURE_BYTES, + 'Ed25519 signature', + ); + if (!await verifyEd25519(signature, message, publicKey)) return false; + continue; + } + const personalHash = eip191PersonalMessageHashV1(message); + if (entry.suite === 'eip191-personal-sign-digest-v1') { + if (recoverEip191SignerV1(entry.signature, personalHash) !== entry.signer) return false; + } else if (verifyEip1271 === undefined + || await verifyEip1271(entry, personalHash) !== true) { + return false; + } + } + return true; +} + +export function eip191PersonalMessageHashV1(message: Uint8Array): Uint8Array { + const ownedMessage = copyBoundedSystemRecordBytesV1( + message, + SYSTEM_RECORD_OBJECT_CAPS_V1['agent-profile-head'], + 'EIP-191 personal message', + ); + const prefix = UTF8.encode(`\x19Ethereum Signed Message:\n${ownedMessage.byteLength}`); + return keccak256(concatBytes(prefix, ownedMessage)); +} + +export function recoverEip191SignerV1(signature: string, personalHash: Uint8Array): string { + assertCanonicalEip191SignatureV1(signature); + const ownedPersonalHash = copyBoundedSystemRecordBytesV1( + personalHash, + 32, + 'EIP-191 personal message hash', + ); + if (ownedPersonalHash.byteLength !== 32) fail('system-record-signature', 'personal message hash must be 32 bytes'); + try { + const bytes = hexToBytes(signature); + const compact = secp256k1.Signature.fromBytes(bytes.subarray(0, 64), 'compact') + .addRecoveryBit(bytes[64] - 27); + const publicKey = compact.recoverPublicKey(ownedPersonalHash).toBytes(false); + return `0x${Buffer.from(keccak256(publicKey.subarray(1)).subarray(12)).toString('hex')}`; + } catch (cause) { + fail('system-record-signature', 'EIP-191 signature recovery failed', cause); + } +} + +export type SystemRecordAuthorityDecisionV1 = + | { readonly decision: 'accept' } + | { readonly decision: 'stale' } + | { readonly decision: 'quarantine'; readonly reason: 'head-fork' | 'transition-equivocation' } + | { readonly decision: 'reject'; readonly reason: string }; + +export interface AgentProfileAppliedTransitionV1 { + readonly priorAuthoritySequence: DecimalU64V1; + readonly nextAuthoritySequence: DecimalU64V1; + readonly transitionDigest: Digest32V1; +} + +export interface AgentProfileAcceptedAuthorityStateV1 { + readonly current?: AgentProfileHeadObjectV1; + readonly disposition: 'discoverable' | 'head-fork-quarantined' | 'transition-equivocation-quarantined'; + readonly transitionLineage: readonly AgentProfileAppliedTransitionV1[]; + /** Duplicate-free prior roots in authority-sequence order; current root is excluded. */ + readonly historicalRoots: readonly string[]; + /** Bounded local diagnostics only; never an authority-completeness predicate. */ + readonly frontierConflictHeads?: readonly AgentProfileHeadObjectV1[]; +} + +export interface AgentProfileHeadAdvanceEvidenceV1 { + readonly nowMs: number; + readonly acceptedTransition?: AgentProfileAuthorityTransitionV1; + readonly tombstonePredecessor?: AgentProfileActiveHeadObjectV1; + /** Opaque proof minted only by buildAgentProfileVerificationClosureV1. */ + readonly verifiedAuthoritySummary?: AgentProfileVerifiedAuthoritySummaryV1; + readonly forkResolution?: AgentProfileForkResolutionV1; + readonly forkEvidenceHeads?: readonly AgentProfileHeadObjectV1[]; + readonly forkBaseHead?: AgentProfileHeadObjectV1; +} + +export function evaluateAuthorityTransitionV1( + transition: AgentProfileAuthorityTransitionV1, + priorHead: AgentProfileHeadObjectV1, + nowMs: number, +): SystemRecordAuthorityDecisionV1 { + const validatedTransition = validateAuthorityTransition(transition); + const validatedPrior = validateAgentProfileHeadObjectV1(priorHead); + if (!isSafeNow(nowMs)) return { decision: 'reject', reason: 'verification clock is invalid' }; + if (isIssuedTooFarInFuture(validatedTransition.issuedAt, nowMs)) { + return { decision: 'reject', reason: 'transition issuedAt exceeds the future clock-skew bound' }; + } + const priorDigest = computeAgentProfileHeadObjectDigestV1(validatedPrior); + if (validatedTransition.networkId !== validatedPrior.networkId + || validatedTransition.peerId !== validatedPrior.peerId + || validatedTransition.peerPublicKey !== validatedPrior.peerPublicKey + || validatedTransition.priorAuthoritySequence !== validatedPrior.authoritySequence + || validatedTransition.priorHeadDigest !== priorDigest + || validatedTransition.priorEvmIssuer !== validatedPrior.evmIssuer) { + return { decision: 'reject', reason: 'transition does not bind the accepted predecessor' }; + } + if (validatedTransition.mode === 'expired-prior') { + if (validatedPrior.state !== 'active') { + return { decision: 'reject', reason: 'expired-prior transition cannot resurrect a tombstone' }; + } + if (validatedTransition.priorValidUntil !== validatedPrior.validUntil) { + return { decision: 'reject', reason: 'expired-prior transition does not bind prior validity' }; + } + if (!Number.isSafeInteger(nowMs) + || nowMs < Date.parse(validatedPrior.validUntil) + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS) { + return { decision: 'reject', reason: 'prior authority has not passed the expiry skew' }; + } + } + return { decision: 'accept' }; +} + +/** Bind a successor head to the exact accepted transition for the same stable record. */ +export function isAgentProfileHeadBoundToAcceptedTransitionV1( + head: AgentProfileHeadObjectV1, + transition: AgentProfileAuthorityTransitionV1, +): boolean { + const validatedHead = validateAgentProfileHeadObjectV1(head); + const validatedTransition = validateAuthorityTransition(transition); + return validatedHead.networkId === validatedTransition.networkId + && validatedHead.peerId === validatedTransition.peerId + && validatedHead.peerPublicKey === validatedTransition.peerPublicKey + && validatedHead.acceptedTransitionDigest === computeAgentProfileAuthorityTransitionDigestV1(validatedTransition) + && validatedHead.authoritySequence === validatedTransition.nextAuthoritySequence + && validatedHead.evmIssuer === validatedTransition.nextEvmIssuer + && validatedHead.rootSubject === validatedTransition.nextRoot; +} + +export function evaluateAgentProfileHeadAdvanceV1( + accepted: AgentProfileAcceptedAuthorityStateV1, + candidate: AgentProfileHeadObjectV1, + evidence: AgentProfileHeadAdvanceEvidenceV1, +): SystemRecordAuthorityDecisionV1 { + const candidateState = validateAgentProfileHeadObjectV1(candidate); + const acceptedState = snapshotAcceptedAuthorityStateV1(accepted); + const evidenceState = snapshotHeadAdvanceEvidenceV1(evidence); + if (!isSafeNow(evidenceState.nowMs)) return { decision: 'reject', reason: 'verification clock is invalid' }; + if (isIssuedTooFarInFuture(candidateState.issuedAt, evidenceState.nowMs)) { + return { decision: 'reject', reason: 'head issuedAt exceeds the future clock-skew bound' }; + } + const lineage = validateAppliedTransitionLineage(acceptedState.transitionLineage); + const current = acceptedState.current === undefined + ? undefined + : validateAgentProfileHeadObjectV1(acceptedState.current); + const historicalRoots = validateAcceptedRootHistoryV1(acceptedState, current, lineage); + const candidateDigest = computeAgentProfileHeadObjectDigestV1(candidateState); + if (current === undefined) { + if (acceptedState.disposition !== 'discoverable' || lineage.length !== 0) { + return { decision: 'reject', reason: 'absent state cannot retain authority history or quarantine' }; + } + if (candidateState.state === 'active' + && candidateState.authoritySequence === '0' + && candidateState.version === '0') { + return { decision: 'accept' }; + } + const summary = evidenceState.verifiedAuthoritySummary; + if (!(summary instanceof AgentProfileVerifiedAuthoritySummaryValueV1) + || !MINTED_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARIES_V1.has(summary) + || summary.candidateHeadDigest !== candidateDigest) { + return { decision: 'reject', reason: 'cold noninitial head requires its verified authority closure' }; + } + const summaryLineage = validateAppliedTransitionLineage(summary.transitionLineage); + if (BigInt(summaryLineage.length) !== parseCanonicalDecimalU64(candidateState.authoritySequence) + || summary.historicalRoots.length !== summaryLineage.length) { + return { decision: 'reject', reason: 'verified authority closure has incomplete lineage' }; + } + if (candidateState.state === 'tombstone') { + const predecessor = summary.tombstonePredecessor; + if (predecessor === undefined || !isTombstoneBoundToPredecessorV1(candidateState, predecessor) + || summary.deletionTableDigest !== predecessor.ownedSubjectTableDigest) { + return { decision: 'reject', reason: 'cold tombstone closure lacks its exact deletion predecessor' }; + } + } else if (summary.tombstonePredecessor !== undefined + || summary.deletionTableDigest !== undefined) { + return { decision: 'reject', reason: 'active closure contains tombstone-only authority evidence' }; + } + return { decision: 'accept' }; + } + const currentSequence = parseCanonicalDecimalU64(current.authoritySequence); + if (BigInt(lineage.length) !== currentSequence) { + return { decision: 'reject', reason: 'accepted authority state has incomplete transition lineage' }; + } + if (current.networkId !== candidateState.networkId || current.peerId !== candidateState.peerId) { + return { decision: 'reject', reason: 'stable record key changed' }; + } + if (acceptedState.disposition === 'transition-equivocation-quarantined') { + return { decision: 'quarantine', reason: 'transition-equivocation' }; + } + const candidateSequence = parseCanonicalDecimalU64(candidateState.authoritySequence); + if (candidateSequence < currentSequence) return { decision: 'stale' }; + if (candidateSequence > currentSequence + 1n) { + return { decision: 'reject', reason: 'authority history is incomplete' }; + } + if (candidateSequence === currentSequence + 1n) { + if (acceptedState.disposition === 'head-fork-quarantined') { + return { decision: 'reject', reason: 'unresolved head fork cannot advance authority sequence' }; + } + const transition = evidenceState.acceptedTransition === undefined + ? undefined + : validateAuthorityTransition(evidenceState.acceptedTransition); + if (transition === undefined || candidateState.acceptedTransitionDigest === undefined + || candidateState.acceptedTransitionDigest !== computeAgentProfileAuthorityTransitionDigestV1(transition)) { + return { decision: 'reject', reason: 'exact accepted authority transition is missing' }; + } + if (candidateState.state === 'tombstone') { + return { decision: 'reject', reason: 'next-sequence tombstone requires its exact same-sequence active predecessor' }; + } + const transitionDecision = evaluateAuthorityTransitionV1(transition, current, evidenceState.nowMs); + if (transitionDecision.decision !== 'accept') return transitionDecision; + if (historicalRoots.includes(transition.nextRoot) || transition.nextRoot === current.rootSubject) { + return { decision: 'reject', reason: 'authority transition reuses a root retained by this record' }; + } + if (candidateState.evmIssuer !== transition.nextEvmIssuer + || candidateState.rootSubject !== transition.nextRoot) { + return { decision: 'reject', reason: 'next-sequence head does not bind transition issuer/root' }; + } + const existing = lineage.find((entry) => entry.nextAuthoritySequence === candidateState.authoritySequence); + if (existing !== undefined && existing.transitionDigest !== candidateState.acceptedTransitionDigest) { + return { decision: 'quarantine', reason: 'transition-equivocation' }; + } + return { decision: 'accept' }; + } + if (candidateState.evmIssuer !== current.evmIssuer || candidateState.rootSubject !== current.rootSubject) { + return { decision: 'reject', reason: 'same-sequence authority changed' }; + } + const currentDigest = computeAgentProfileHeadObjectDigestV1(current); + if (candidateState.acceptedTransitionDigest !== current.acceptedTransitionDigest) { + return { decision: 'quarantine', reason: 'transition-equivocation' }; + } + const currentVersion = parseCanonicalDecimalU64(current.version); + const candidateVersion = parseCanonicalDecimalU64(candidateState.version); + if (candidateState.state === 'tombstone') { + const predecessor = evidenceState.tombstonePredecessor === undefined + ? (current.state === 'active' ? current : undefined) + : validateAgentProfileHeadObjectV1(evidenceState.tombstonePredecessor); + if (predecessor === undefined || predecessor.state !== 'active' + || !isTombstoneBoundToPredecessorV1(candidateState, predecessor)) { + return { decision: 'reject', reason: 'tombstone lacks its exact verified active predecessor' }; + } + if (current.state === 'active') return { decision: 'accept' }; + if (candidateVersion !== currentVersion) { + return candidateVersion < currentVersion ? { decision: 'accept' } : { decision: 'stale' }; + } + if (candidateDigest === currentDigest) return { decision: 'stale' }; + return candidateDigest < currentDigest ? { decision: 'accept' } : { decision: 'stale' }; + } + if (current.state === 'tombstone') { + return { decision: 'reject', reason: 'tombstone is terminal within its authority sequence' }; + } + if (candidateVersion < currentVersion) return { decision: 'stale' }; + if (candidateVersion === currentVersion) { + return candidateDigest === currentDigest + ? { decision: 'stale' } + : { decision: 'quarantine', reason: 'head-fork' }; + } + if (acceptedState.disposition === 'head-fork-quarantined') { + const resolution = evidenceState.forkResolution; + const conflicts = evidenceState.forkEvidenceHeads; + if (resolution === undefined || conflicts === undefined + || candidateState.state !== 'active' + || resolution.forkedVersion !== current.version + || computeAgentProfileForkResolutionDigestV1(resolution) !== candidateState.forkResolutionDigest + || !isDirectResolvingSuccessorV1(candidateState, resolution)) { + return { decision: 'reject', reason: 'current frontier fork requires its exact direct resolving successor' }; + } + if (isIssuedTooFarInFuture(resolution.issuedAt, evidenceState.nowMs)) { + return { decision: 'reject', reason: 'fork resolution issuedAt exceeds the future clock-skew bound' }; + } + const validatedForkEvidence = validateAgentProfileForkResolutionEvidenceV1( + resolution, + conflicts, + evidenceState.forkBaseHead, + ); + const resolutionTransitionDigest = validatedForkEvidence.evidenceHeads[0]?.acceptedTransitionDigest; + if (resolutionTransitionDigest !== current.acceptedTransitionDigest + || (validatedForkEvidence.forkBase !== undefined + && validatedForkEvidence.forkBase.acceptedTransitionDigest !== current.acceptedTransitionDigest)) { + return { decision: 'quarantine', reason: 'transition-equivocation' }; + } + } else if (candidateState.forkResolutionDigest !== undefined) { + return { decision: 'reject', reason: 'historical or unsolicited fork resolution is audit-only' }; + } + return { decision: 'accept' }; +} + +export function isDirectResolvingSuccessorV1( + successor: AgentProfileHeadObjectV1, + resolution: AgentProfileForkResolutionV1, +): boolean { + const validatedSuccessor = validateAgentProfileHeadObjectV1(successor); + const validatedResolution = validateForkResolution(resolution); + if (validatedSuccessor.networkId !== validatedResolution.networkId + || validatedSuccessor.peerId !== validatedResolution.peerId + || validatedSuccessor.peerPublicKey !== validatedResolution.peerPublicKey + || validatedSuccessor.evmIssuer !== validatedResolution.evmIssuer + || validatedSuccessor.authoritySequence !== validatedResolution.authoritySequence + || validatedSuccessor.forkResolutionDigest !== computeAgentProfileForkResolutionDigestV1(validatedResolution) + || parseCanonicalDecimalU64(validatedSuccessor.version) + <= parseCanonicalDecimalU64(validatedResolution.resolutionVersion)) { + return false; + } + return validatedResolution.forkedVersion === '0' + ? validatedSuccessor.previousHeadDigest === undefined + : validatedSuccessor.previousHeadDigest === validatedResolution.forkBaseHeadDigest; +} + +function isTombstoneBoundToPredecessorV1( + tombstone: AgentProfileTombstoneHeadObjectV1, + predecessor: AgentProfileActiveHeadObjectV1, +): boolean { + const validatedTombstone = validateAgentProfileHeadObjectV1(tombstone); + const validatedPredecessor = validateAgentProfileHeadObjectV1(predecessor); + if (validatedTombstone.state !== 'tombstone' || validatedPredecessor.state !== 'active') return false; + return validatedTombstone.previousHeadDigest === computeAgentProfileHeadObjectDigestV1(validatedPredecessor) + && validatedTombstone.networkId === validatedPredecessor.networkId + && validatedTombstone.peerId === validatedPredecessor.peerId + && validatedTombstone.peerPublicKey === validatedPredecessor.peerPublicKey + && validatedTombstone.authoritySequence === validatedPredecessor.authoritySequence + && validatedTombstone.acceptedTransitionDigest === validatedPredecessor.acceptedTransitionDigest + && validatedTombstone.evmIssuer === validatedPredecessor.evmIssuer + && validatedTombstone.rootSubject === validatedPredecessor.rootSubject + && validatedTombstone.projectionSchemaDigest === validatedPredecessor.projectionSchemaDigest + && parseCanonicalDecimalU64(validatedTombstone.version) + > parseCanonicalDecimalU64(validatedPredecessor.version); +} + +export function assertAgentProfileForkResolutionEvidenceV1( + resolution: AgentProfileForkResolutionV1, + evidenceHeads: readonly AgentProfileHeadObjectV1[], + forkBase?: AgentProfileHeadObjectV1, +): void { + validateAgentProfileForkResolutionEvidenceV1(resolution, evidenceHeads, forkBase); +} + +function validateAgentProfileForkResolutionEvidenceV1( + resolution: AgentProfileForkResolutionV1, + evidenceHeads: readonly AgentProfileHeadObjectV1[], + forkBase?: AgentProfileHeadObjectV1, +): Readonly<{ + resolution: AgentProfileForkResolutionV1; + evidenceHeads: readonly AgentProfileHeadObjectV1[]; + forkBase?: AgentProfileHeadObjectV1; +}> { + const validatedResolution = validateForkResolution(resolution); + let rawHeads: readonly unknown[]; + try { + rawHeads = snapshotDataArray(evidenceHeads, 'fork resolution evidence heads', { + minLength: validatedResolution.evidenceHeadDigests.length, + maxLength: validatedResolution.evidenceHeadDigests.length, + }); + } catch (cause) { + fail('system-record-history', 'fork resolution evidence set is incomplete or not closed', cause); + } + const heads = Object.freeze(rawHeads.map((head) => validateAgentProfileHeadObjectV1(head))); + const byDigest = new Map(heads.map((head) => { + return [computeAgentProfileHeadObjectDigestV1(head), head] as const; + })); + if (byDigest.size !== heads.length + || validatedResolution.evidenceHeadDigests.some((candidate) => !byDigest.has(candidate))) { + fail('system-record-history', 'fork resolution evidence digests do not match supplied heads'); + } + const forkedVersion = parseCanonicalDecimalU64(validatedResolution.forkedVersion); + const authoritySequence = parseCanonicalDecimalU64(validatedResolution.authoritySequence); + let baseDigest: Digest32V1 | undefined; + const validatedForkBase = forkBase === undefined + ? undefined + : validateAgentProfileHeadObjectV1(forkBase); + if (forkedVersion === 0n) { + if (validatedForkBase !== undefined) fail('system-record-history', 'version-zero fork must not supply a base'); + } else { + if (validatedForkBase === undefined) fail('system-record-history', 'nonzero fork requires its common base'); + baseDigest = computeAgentProfileHeadObjectDigestV1(validatedForkBase); + if (validatedForkBase.state !== 'active' + || baseDigest !== validatedResolution.forkBaseHeadDigest + || validatedForkBase.networkId !== validatedResolution.networkId + || validatedForkBase.peerId !== validatedResolution.peerId + || validatedForkBase.authoritySequence !== validatedResolution.authoritySequence + || validatedForkBase.evmIssuer !== validatedResolution.evmIssuer + || parseCanonicalDecimalU64(validatedForkBase.version) >= forkedVersion) { + fail('system-record-history', 'fork base is not a verified lower same-authority head'); + } + } + const expectedTransitionDigest = heads[0]?.acceptedTransitionDigest; + if ((authoritySequence === 0n && expectedTransitionDigest !== undefined) + || (authoritySequence > 0n && expectedTransitionDigest === undefined)) { + fail('system-record-history', 'fork evidence has invalid accepted-transition lineage'); + } + for (const head of heads) { + if (head.state !== 'active') { + fail('system-record-history', 'fork resolution cannot use tombstone evidence'); + } + if (head.acceptedTransitionDigest !== expectedTransitionDigest) { + fail('system-record-history', 'fork evidence changed accepted-transition lineage'); + } + if (head.networkId !== validatedResolution.networkId + || head.peerId !== validatedResolution.peerId + || head.peerPublicKey !== validatedResolution.peerPublicKey + || head.evmIssuer !== validatedResolution.evmIssuer + || head.authoritySequence !== validatedResolution.authoritySequence + || head.version !== validatedResolution.forkedVersion + || (forkedVersion === 0n + ? head.previousHeadDigest !== undefined + : head.previousHeadDigest !== baseDigest)) { + fail('system-record-history', 'fork evidence head does not share the canonical fork tuple/base'); + } + } + if (validatedForkBase !== undefined + && validatedForkBase.acceptedTransitionDigest !== expectedTransitionDigest) { + fail('system-record-history', 'fork base changed accepted-transition lineage'); + } + return Object.freeze({ + resolution: validatedResolution, + evidenceHeads: heads, + ...(validatedForkBase === undefined ? {} : { forkBase: validatedForkBase }), + }); +} + +export function evaluateAuthorityTransitionConflictV1( + left: AgentProfileAuthorityTransitionV1, + right: AgentProfileAuthorityTransitionV1, +): SystemRecordAuthorityDecisionV1 { + const validatedLeft = validateAuthorityTransition(left); + const validatedRight = validateAuthorityTransition(right); + if (validatedLeft.networkId !== validatedRight.networkId || validatedLeft.peerId !== validatedRight.peerId + || validatedLeft.priorAuthoritySequence !== validatedRight.priorAuthoritySequence + || validatedLeft.nextAuthoritySequence !== validatedRight.nextAuthoritySequence) { + return { decision: 'reject', reason: 'transitions do not target the same authority tuple' }; + } + return computeAgentProfileAuthorityTransitionDigestV1(validatedLeft) + === computeAgentProfileAuthorityTransitionDigestV1(validatedRight) + ? { decision: 'stale' } + : { decision: 'quarantine', reason: 'transition-equivocation' }; +} + +/** Compare a verified transition with durable accepted lineage, including late delivery. */ +export function evaluateAuthorityTransitionAgainstAcceptedStateV1( + accepted: AgentProfileAcceptedAuthorityStateV1, + transition: AgentProfileAuthorityTransitionV1, + nowMs: number, +): SystemRecordAuthorityDecisionV1 { + const validatedTransition = validateAuthorityTransition(transition); + const acceptedState = snapshotAcceptedAuthorityStateV1(accepted); + if (!isSafeNow(nowMs) || isIssuedTooFarInFuture(validatedTransition.issuedAt, nowMs)) { + return { decision: 'reject', reason: 'transition verification time is invalid' }; + } + const lineage = validateAppliedTransitionLineage(acceptedState.transitionLineage); + const current = acceptedState.current === undefined + ? undefined + : validateAgentProfileHeadObjectV1(acceptedState.current); + const historicalRoots = validateAcceptedRootHistoryV1(acceptedState, current, lineage); + if (current !== undefined + && BigInt(lineage.length) !== parseCanonicalDecimalU64(current.authoritySequence)) { + return { decision: 'reject', reason: 'accepted authority state has incomplete transition lineage' }; + } + if (current !== undefined + && (current.networkId !== validatedTransition.networkId || current.peerId !== validatedTransition.peerId)) { + return { decision: 'reject', reason: 'stable record key changed' }; + } + const digestValue = computeAgentProfileAuthorityTransitionDigestV1(validatedTransition); + const retained = lineage.find( + (entry) => entry.priorAuthoritySequence === validatedTransition.priorAuthoritySequence + && entry.nextAuthoritySequence === validatedTransition.nextAuthoritySequence, + ); + if (retained !== undefined) { + return retained.transitionDigest === digestValue + ? { decision: 'stale' } + : { decision: 'quarantine', reason: 'transition-equivocation' }; + } + if (acceptedState.disposition === 'transition-equivocation-quarantined') { + return { decision: 'quarantine', reason: 'transition-equivocation' }; + } + if (acceptedState.disposition === 'head-fork-quarantined') { + return { decision: 'reject', reason: 'unresolved head fork cannot advance authority sequence' }; + } + if (current === undefined) { + return { decision: 'reject', reason: 'transition has no accepted predecessor' }; + } + if (historicalRoots.includes(validatedTransition.nextRoot) + || validatedTransition.nextRoot === current.rootSubject) { + return { decision: 'reject', reason: 'authority transition reuses a root retained by this record' }; + } + return evaluateAuthorityTransitionV1(validatedTransition, current, nowMs); +} + +export interface SystemRecordVerificationClosureObjectV1 { + readonly objectKind: SystemRecordObjectKindV1; + readonly digest: Digest32V1; + readonly canonicalBytes: Uint8Array; + readonly references: readonly Pick[]; +} + +export interface SystemRecordVerificationClosureV1 { + readonly objects: readonly SystemRecordVerificationClosureObjectV1[]; + readonly canonicalBytes: number; + readonly rootClaims: number; + readonly resolvedForkTuples: number; + readonly authoritySummary: AgentProfileVerifiedAuthoritySummaryV1; +} + +const MINT_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARY_V1 = Symbol('mint-agent-profile-verified-authority-summary-v1'); +const MINTED_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARIES_V1 = new WeakSet(); + +class AgentProfileVerifiedAuthoritySummaryValueV1 { + private declare readonly __opaqueAgentProfileVerifiedAuthoritySummaryV1: void; + + constructor( + token: typeof MINT_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARY_V1, + public readonly candidateHeadDigest: Digest32V1, + public readonly transitionLineage: readonly AgentProfileAppliedTransitionV1[], + public readonly historicalRoots: readonly string[], + public readonly tombstonePredecessor?: AgentProfileActiveHeadObjectV1, + public readonly deletionTableDigest?: Digest32V1, + ) { + if (token !== MINT_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARY_V1) { + fail('system-record-closure', 'verified authority summary is factory-only'); + } + MINTED_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARIES_V1.add(this); + Object.freeze(this); + } +} + +/** + * Process-local, non-serializable authority capability minted only by closure verification. + * Consume it with this physical package instance; copying, persistence, worker transfer, or + * reconstruction intentionally destroys its authority. + */ +export type AgentProfileVerifiedAuthoritySummaryV1 = AgentProfileVerifiedAuthoritySummaryValueV1; + +export interface SystemRecordClosureArtifactV1 { + readonly objectKind: SystemRecordObjectKindV1; + readonly digest: Digest32V1; + readonly canonicalBytes: Uint8Array; +} + +export interface AgentProfileClosureVerifierV1 { + readonly nowMs: number; + readonly resolve: ( + reference: Readonly>, + ) => Promise; + readonly verifyAuthorityEnvelope: ( + envelope: SignedAgentProfileHeadEnvelopeV1 + | SignedAgentProfileAuthorityTransitionEnvelopeV1 + | SignedAgentProfileForkResolutionEnvelopeV1, + ) => boolean | Promise; + readonly verifyCurrentBundle: ( + head: AgentProfileActiveHeadObjectV1, + canonicalBundleBytes: Uint8Array, + ) => boolean | Promise; +} + +type ClosurePurposeV1 = + | 'current' + | 'history' + | 'fork-evidence' + | 'tombstone-predecessor' + | 'deletion-predecessor'; + +/** + * Derive the row closure from canonical objects rather than trusting caller-supplied edges. + * Authority and current-bundle verification stay injected because final chain/seal proofs + * belong to later stacks, but a false/missing verifier result always fails closed. + */ +export async function buildAgentProfileVerificationClosureV1( + currentHeadDigest: Digest32V1, + verifier: AgentProfileClosureVerifierV1, +): Promise { + digest(currentHeadDigest, 'currentHeadDigest'); + const nowMs = verifier.nowMs; + const resolve = verifier.resolve; + const verifyAuthorityEnvelope = verifier.verifyAuthorityEnvelope; + const verifyCurrentBundle = verifier.verifyCurrentBundle; + if (!isSafeNow(nowMs)) fail('system-record-closure', 'closure verifier clock is invalid'); + if (typeof resolve !== 'function' + || typeof verifyAuthorityEnvelope !== 'function' + || typeof verifyCurrentBundle !== 'function') { + fail('system-record-closure', 'closure verifier callbacks are invalid'); + } + const pending = new Map(); + const artifacts: SystemRecordVerificationClosureObjectV1[] = []; + const parsedHeads = new Map(); + const parsedTransitions = new Map(); + const parsedResolutions: AgentProfileForkResolutionV1[] = []; + const seen = new Map(); + const rootClaims = new Set(); + let bytes = 0; + enqueue('agent-profile-head', currentHeadDigest, 'current'); + + while (pending.size > 0) { + const context = Object.freeze({ + ...[...pending.values()].sort(compareClosureObjects)[0], + }); + const key = context.digest; + pending.delete(key); + const seenKind = seen.get(key); + if (seenKind !== undefined) { + if (seenKind !== context.objectKind) { + fail('system-record-closure', 'one closure digest was presented under different object kinds'); + } + continue; + } + const resolved = await resolve(Object.freeze({ + objectKind: context.objectKind, + digest: context.digest, + })); + if (resolved === undefined) fail('system-record-closure', `verification closure is missing ${key}`); + const artifact = snapshotExactDataRecord( + resolved, + ['objectKind', 'digest', 'canonicalBytes'], + 'verification closure artifact', + ); + if (artifact.objectKind !== context.objectKind || artifact.digest !== context.digest) { + fail('system-record-closure', 'closure resolver returned a different artifact'); + } + const objectKind = context.objectKind; + const objectDigest = context.digest; + let canonicalBytes: Uint8Array; + try { + canonicalBytes = copyBoundedSystemRecordBytesV1( + artifact.canonicalBytes, + SYSTEM_RECORD_OBJECT_CAPS_V1[objectKind], + 'closure artifact canonical bytes', + ); + } catch (cause) { + fail('system-record-closure', 'closure artifact exceeds its kind cap', cause); + } + const references: Pick[] = []; + const add = ( + objectKind: SystemRecordObjectKindV1, + objectDigest: Digest32V1, + purpose: ClosurePurposeV1, + rootSubject?: string, + referencedByHeadDigest?: Digest32V1, + ) => { + enqueue(objectKind, objectDigest, purpose, rootSubject, referencedByHeadDigest); + references.push(Object.freeze({ objectKind, digest: objectDigest })); + }; + + if (objectKind === 'agent-profile-head') { + const envelope = parseCanonicalSignedAgentProfileHeadEnvelopeV1(canonicalBytes); + if (envelope.objectDigest !== objectDigest + || await verifyAuthorityEnvelope(envelope) !== true) { + fail('system-record-closure', 'head authority verification failed'); + } + const head = envelope.object; + if (isIssuedTooFarInFuture(head.issuedAt, nowMs)) { + fail('system-record-closure', 'head issuedAt exceeds the future clock-skew bound'); + } + parsedHeads.set(objectDigest, head); + rootClaims.add(head.rootSubject); + if (head.acceptedTransitionDigest !== undefined) { + add('authority-transition', head.acceptedTransitionDigest, 'history', undefined, objectDigest); + } + if (head.forkResolutionDigest !== undefined) { + add('fork-resolution', head.forkResolutionDigest, 'history', undefined, objectDigest); + } + if (context.purpose === 'current' && head.state === 'active') { + add('profile-bundle', head.bundleDigest, 'current'); + } + if (head.state === 'tombstone') { + add( + 'agent-profile-head', + head.previousHeadDigest, + context.purpose === 'current' ? 'deletion-predecessor' : 'tombstone-predecessor', + ); + } + if (context.purpose === 'deletion-predecessor' || context.purpose === 'tombstone-predecessor') { + if (head.state !== 'active') fail('system-record-closure', 'tombstone predecessor must be active'); + if (context.purpose === 'deletion-predecessor') { + add('owned-subject-table', head.ownedSubjectTableDigest, 'history', head.rootSubject); + } + } + } else if (objectKind === 'authority-transition') { + const envelope = parseCanonicalSignedAgentProfileAuthorityTransitionEnvelopeV1(canonicalBytes); + if (envelope.objectDigest !== objectDigest + || await verifyAuthorityEnvelope(envelope) !== true) { + fail('system-record-closure', 'authority-transition verification failed'); + } + parsedTransitions.set(objectDigest, envelope.object); + if (context.referencedByHeadDigest !== undefined) { + const referencingHead = parsedHeads.get(context.referencedByHeadDigest); + if (referencingHead === undefined + || !isAgentProfileHeadBoundToAcceptedTransitionV1(referencingHead, envelope.object)) { + fail('system-record-closure', 'head does not bind its accepted authority transition'); + } + } + rootClaims.add(envelope.object.nextRoot); + add('agent-profile-head', envelope.object.priorHeadDigest, 'history'); + } else if (objectKind === 'fork-resolution') { + const envelope = parseCanonicalSignedAgentProfileForkResolutionEnvelopeV1(canonicalBytes); + if (envelope.objectDigest !== objectDigest + || await verifyAuthorityEnvelope(envelope) !== true) { + fail('system-record-closure', 'fork-resolution verification failed'); + } + parsedResolutions.push(envelope.object); + if (isIssuedTooFarInFuture(envelope.object.issuedAt, nowMs)) { + fail('system-record-closure', 'fork resolution issuedAt exceeds the future clock-skew bound'); + } + if (context.referencedByHeadDigest !== undefined) { + const referencingHead = parsedHeads.get(context.referencedByHeadDigest); + if (referencingHead === undefined || !isDirectResolvingSuccessorV1(referencingHead, envelope.object)) { + fail('system-record-closure', 'current head is not the direct successor of its fork resolution'); + } + } + for (const headDigest of envelope.object.evidenceHeadDigests) { + add('agent-profile-head', headDigest, 'fork-evidence'); + } + if (envelope.object.forkBaseHeadDigest !== undefined) { + add('agent-profile-head', envelope.object.forkBaseHeadDigest, 'history'); + } + } else if (objectKind === 'profile-bundle') { + const current = parsedHeads.get(currentHeadDigest); + if (current?.state !== 'active' + || digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, canonicalBytes) + !== objectDigest + || await verifyCurrentBundle(current, canonicalBytes.slice()) !== true) { + fail('system-record-closure', 'current profile bundle verification failed'); + } + } else if (objectKind === 'owned-subject-table') { + if (context.rootSubject === undefined) fail('system-record-closure', 'subject table lacks root context'); + const table = parseCanonicalOwnedSubjectTableObjectV1(context.rootSubject, canonicalBytes); + if (computeOwnedSubjectTableDigestV1(context.rootSubject, table) !== objectDigest) { + fail('system-record-closure', 'owned-subject table digest mismatch'); + } + } else { + fail('system-record-closure', `${objectKind} is not part of an advertised row closure`); + } + + bytes += canonicalBytes.byteLength; + if (artifacts.length + 1 > SYSTEM_RECORD_MAX_CLOSURE_OBJECTS + || bytes > SYSTEM_RECORD_MAX_CLOSURE_BYTES + || rootClaims.size > SYSTEM_RECORD_MAX_ROOT_CLAIMS + || parsedResolutions.length > SYSTEM_RECORD_MAX_RESOLVED_FORK_TUPLES) { + fail('system-record-closure', 'verification closure exceeds a V1 bound'); + } + seen.set(key, objectKind); + artifacts.push(Object.freeze({ + objectKind, + digest: objectDigest, + canonicalBytes, + references: Object.freeze(references.sort(compareClosureObjects)), + })); + if (seen.size + pending.size > SYSTEM_RECORD_MAX_CLOSURE_OBJECTS) { + fail('system-record-closure', 'verification closure cannot fit before dependency fetch'); + } + } + + for (const resolution of parsedResolutions) { + const evidence = resolution.evidenceHeadDigests.map((headDigest) => parsedHeads.get(headDigest)); + if (evidence.some((head) => head === undefined)) { + fail('system-record-closure', 'fork evidence is incomplete after traversal'); + } + const base = resolution.forkBaseHeadDigest === undefined + ? undefined + : parsedHeads.get(resolution.forkBaseHeadDigest); + assertAgentProfileForkResolutionEvidenceV1( + resolution, + evidence as AgentProfileHeadObjectV1[], + base, + ); + const transitionDigest = (evidence as AgentProfileHeadObjectV1[])[0].acceptedTransitionDigest; + const resolutionDigest = computeAgentProfileForkResolutionDigestV1(resolution); + for (const head of parsedHeads.values()) { + if (head.forkResolutionDigest === resolutionDigest + && head.acceptedTransitionDigest !== transitionDigest) { + fail('system-record-closure', 'resolving successor changed accepted-transition lineage'); + } + } + } + const transitionTupleDigests = new Map(); + for (const [transitionDigest, transition] of parsedTransitions) { + const prior = parsedHeads.get(transition.priorHeadDigest); + if (prior === undefined + || evaluateAuthorityTransitionV1(transition, prior, nowMs).decision !== 'accept') { + fail('system-record-closure', `authority transition ${transitionDigest} lacks its exact accepted predecessor`); + } + const tuple = [ + transition.networkId, + transition.peerId, + transition.priorAuthoritySequence, + transition.nextAuthoritySequence, + ].join('\u0000'); + const priorDigest = transitionTupleDigests.get(tuple); + if (priorDigest !== undefined && priorDigest !== transitionDigest) { + fail('system-record-closure', 'verification closure contains authority-transition equivocation'); + } + transitionTupleDigests.set(tuple, transitionDigest); + } + for (const [headDigest, head] of parsedHeads) { + assertCompleteUniqueRootLineage(headDigest, head); + if (head.acceptedTransitionDigest !== undefined) { + const transition = parsedTransitions.get(head.acceptedTransitionDigest); + if (transition === undefined + || !isAgentProfileHeadBoundToAcceptedTransitionV1(head, transition)) { + fail('system-record-closure', `head ${headDigest} does not bind its accepted transition`); + } + } + if (head.forkResolutionDigest !== undefined) { + const resolution = parsedResolutions.find( + (candidate) => computeAgentProfileForkResolutionDigestV1(candidate) === head.forkResolutionDigest, + ); + if (resolution === undefined || !isDirectResolvingSuccessorV1(head, resolution)) { + fail('system-record-closure', `head ${headDigest} does not directly bind its fork resolution`); + } + } + } + for (const head of parsedHeads.values()) { + if (head.state === 'tombstone') { + const predecessor = parsedHeads.get(head.previousHeadDigest); + if (predecessor?.state !== 'active' || !isTombstoneBoundToPredecessorV1(head, predecessor)) { + fail('system-record-closure', 'tombstone predecessor is not the exact prior active authority state'); + } + } + } + const authoritySummary = createVerifiedAuthoritySummary(); + artifacts.sort(compareClosureObjects); + return Object.freeze({ + objects: Object.freeze(artifacts), + canonicalBytes: bytes, + rootClaims: rootClaims.size, + resolvedForkTuples: parsedResolutions.length, + authoritySummary, + }); + + function enqueue( + objectKind: SystemRecordObjectKindV1, + objectDigest: Digest32V1, + purpose: ClosurePurposeV1, + rootSubject?: string, + referencedByHeadDigest?: Digest32V1, + ): void { + digest(objectDigest, 'closure reference digest'); + const key = objectDigest; + const seenKind = seen.get(key); + if (seenKind !== undefined) { + if (seenKind !== objectKind) { + fail('system-record-closure', 'one closure digest was referenced under different object kinds'); + } + return; + } + const existing = pending.get(key); + if (existing !== undefined && existing.objectKind !== objectKind) { + fail('system-record-closure', 'one pending closure digest has conflicting object kinds'); + } + const priority: Record = { + history: 0, 'fork-evidence': 1, + 'tombstone-predecessor': 2, 'deletion-predecessor': 3, current: 4, + }; + if (existing === undefined || priority[purpose] > priority[existing.purpose]) { + pending.set(key, { + objectKind, + digest: objectDigest, + purpose, + ...(rootSubject === undefined ? {} : { rootSubject }), + ...(referencedByHeadDigest === undefined ? {} : { referencedByHeadDigest }), + }); + } + } + + function assertCompleteUniqueRootLineage( + headDigest: Digest32V1, + head: AgentProfileHeadObjectV1, + ): void { + let cursor = head; + let sequence = parseCanonicalDecimalU64(head.authoritySequence); + const roots = new Set([head.rootSubject]); + for (let depth = 0; sequence > 0n; depth += 1) { + if (depth >= Number(SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX) + || cursor.acceptedTransitionDigest === undefined) { + fail('system-record-history', `head ${headDigest} has incomplete authority/root lineage`); + } + const transition = parsedTransitions.get(cursor.acceptedTransitionDigest); + const prior = transition === undefined ? undefined : parsedHeads.get(transition.priorHeadDigest); + if (transition === undefined || prior === undefined + || parseCanonicalDecimalU64(transition.nextAuthoritySequence) !== sequence + || parseCanonicalDecimalU64(prior.authoritySequence) + 1n !== sequence) { + fail('system-record-history', `head ${headDigest} has incomplete authority/root lineage`); + } + if (roots.has(prior.rootSubject)) { + fail('system-record-history', `head ${headDigest} reuses a historical wallet root`); + } + roots.add(prior.rootSubject); + cursor = prior; + sequence -= 1n; + } + if (cursor.acceptedTransitionDigest !== undefined) { + fail('system-record-history', `head ${headDigest} has authority evidence below sequence zero`); + } + } + + function createVerifiedAuthoritySummary(): AgentProfileVerifiedAuthoritySummaryV1 { + const current = parsedHeads.get(currentHeadDigest); + if (current === undefined) fail('system-record-closure', 'verified closure lost its current head'); + const reverseLineage: AgentProfileAppliedTransitionV1[] = []; + const reverseRoots: string[] = []; + let cursor = current; + let sequence = parseCanonicalDecimalU64(current.authoritySequence); + while (sequence > 0n) { + const transition = cursor.acceptedTransitionDigest === undefined + ? undefined + : parsedTransitions.get(cursor.acceptedTransitionDigest); + const prior = transition === undefined ? undefined : parsedHeads.get(transition.priorHeadDigest); + if (transition === undefined || prior === undefined) { + fail('system-record-history', 'verified closure lost its authority lineage'); + } + reverseLineage.push(Object.freeze({ + priorAuthoritySequence: transition.priorAuthoritySequence, + nextAuthoritySequence: transition.nextAuthoritySequence, + transitionDigest: computeAgentProfileAuthorityTransitionDigestV1(transition), + })); + reverseRoots.push(prior.rootSubject); + cursor = prior; + sequence -= 1n; + } + const tombstonePredecessor = current.state === 'tombstone' + ? parsedHeads.get(current.previousHeadDigest) + : undefined; + if (tombstonePredecessor !== undefined && tombstonePredecessor.state !== 'active') { + fail('system-record-history', 'verified tombstone closure lost its active predecessor'); + } + return new AgentProfileVerifiedAuthoritySummaryValueV1( + MINT_AGENT_PROFILE_VERIFIED_AUTHORITY_SUMMARY_V1, + currentHeadDigest, + Object.freeze(reverseLineage.reverse()), + Object.freeze(reverseRoots.reverse()), + tombstonePredecessor?.state === 'active' ? tombstonePredecessor : undefined, + tombstonePredecessor?.ownedSubjectTableDigest, + ); + } +} + +function compareClosureObjects( + left: Pick, + right: Pick, +): number { + if (left.digest !== right.digest) return left.digest < right.digest ? -1 : 1; + if (left.objectKind === right.objectKind) return 0; + return left.objectKind < right.objectKind ? -1 : 1; +} + +export function assertSystemRecordClosureAlgebraV1( + authoritySequence: bigint, + mode: 'active' | 'tombstone' | 'fork', + conflictHeads = 0, +): number { + if (authoritySequence < 0n || authoritySequence > SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX) { + fail('system-record-closure', 'authority sequence is outside V1'); + } + let objects = mode === 'active' + ? 2 + Number(authoritySequence) * 2 + : mode === 'tombstone' + ? 3 + Number(authoritySequence) * 2 + : 4 + Number(authoritySequence) * 2 + conflictHeads; + if (mode === 'fork' && (conflictHeads < 2 || conflictHeads > SYSTEM_RECORD_MAX_CONFLICT_DIGESTS)) { + fail('system-record-closure', 'fork closure needs 2-16 evidence heads'); + } + if (objects > SYSTEM_RECORD_MAX_CLOSURE_OBJECTS) { + fail('system-record-closure', `closure requires ${objects} objects, over the V1 cap`); + } + return objects; +} + +interface SystemRecordCacheReferenceFactsV1 { + readonly byteLength: number; + readonly fingerprint: string; +} + +const MINT_SYSTEM_RECORD_CACHE_REFERENCE_V1 = Symbol('mint-system-record-cache-reference-v1'); +const SYSTEM_RECORD_CACHE_REFERENCE_FACTS_V1 = new WeakMap(); + +class SystemRecordCacheReferenceValueV1 { + constructor( + token: typeof MINT_SYSTEM_RECORD_CACHE_REFERENCE_V1, + /** Semantic object identity used by authority, closure edges, and inventory rows. */ + public readonly digest: Digest32V1, + /** Exact physical cache identity; signed controls bind their complete envelope bytes. */ + public readonly cacheDigest: Digest32V1, + public readonly objectKind: SystemRecordObjectKindV1, + facts: SystemRecordCacheReferenceFactsV1, + ) { + if (token !== MINT_SYSTEM_RECORD_CACHE_REFERENCE_V1) { + fail('system-record-closure', 'cache reference is factory-only'); + } + SYSTEM_RECORD_CACHE_REFERENCE_FACTS_V1.set(this, Object.freeze({ ...facts })); + Object.freeze(this); + } +} + +/** + * Process-local, factory-only accounting capability bound to exact canonical bytes. + * It must not be reconstructed, cloned, serialized, or transferred between module instances. + */ +export type SystemRecordCacheReferenceV1 = SystemRecordCacheReferenceValueV1; + +/** Create an exact byte-derived accounting reference; unbranded caller counters are rejected. */ +export function createSystemRecordCacheReferenceV1( + objectKind: SystemRecordObjectKindV1, + objectDigest: Digest32V1, + canonicalBytes: Uint8Array, +): SystemRecordCacheReferenceV1 { + digest(objectDigest, 'cache reference digest'); + if (!Object.prototype.hasOwnProperty.call(SYSTEM_RECORD_OBJECT_CAPS_V1, objectKind)) { + fail('system-record-closure', 'cache reference bytes exceed their object-kind cap'); + } + let ownedBytes: Uint8Array; + try { + ownedBytes = copyBoundedSystemRecordBytesV1( + canonicalBytes, + SYSTEM_RECORD_OBJECT_CAPS_V1[objectKind], + 'cache reference canonical bytes', + ); + } catch (cause) { + fail('system-record-closure', 'cache reference bytes exceed their object-kind cap', cause); + } + if (ownedBytes.byteLength < 1) fail('system-record-closure', 'cache reference bytes must not be empty'); + const identities = deriveCacheReferenceArtifactIdentitiesV1(objectKind, ownedBytes, 'cache reference'); + if (identities.semanticDigest !== objectDigest) { + fail('system-record-closure', 'cache reference semantic digest does not bind its canonical bytes'); + } + return new SystemRecordCacheReferenceValueV1( + MINT_SYSTEM_RECORD_CACHE_REFERENCE_V1, + identities.semanticDigest, + identities.cacheDigest, + objectKind, + { + byteLength: ownedBytes.byteLength, + fingerprint: Buffer.from(sha256(ownedBytes)).toString('hex'), + }, + ); +} + +export interface SystemRecordCacheRowAccountingV1 { + readonly closure: readonly SystemRecordCacheReferenceV1[]; + readonly sidecar?: readonly SystemRecordCacheReferenceV1[]; + readonly metadata: SystemRecordCacheMetadataV1; + readonly sidecarMetadata?: SystemRecordCacheMetadataV1; +} + +const MINT_SYSTEM_RECORD_CACHE_METADATA_V1 = Symbol('mint-system-record-cache-metadata-v1'); +const SYSTEM_RECORD_CACHE_METADATA_BYTES_V1 = new WeakMap(); + +class SystemRecordCacheMetadataValueV1 { + constructor(token: typeof MINT_SYSTEM_RECORD_CACHE_METADATA_V1, byteLength: number) { + if (token !== MINT_SYSTEM_RECORD_CACHE_METADATA_V1) { + fail('system-record-closure', 'cache metadata is factory-only'); + } + SYSTEM_RECORD_CACHE_METADATA_BYTES_V1.set(this, byteLength); + Object.freeze(this); + } +} + +/** + * Process-local, factory-only metadata accounting capability. Serialization or cloning + * intentionally loses the private byte-accounting authority. + */ +export type SystemRecordCacheMetadataV1 = SystemRecordCacheMetadataValueV1; + +/** Brand the exact encoded metadata bytes that B2 will include in its atomic baseline preflight. */ +export function createSystemRecordCacheMetadataV1( + encodedMetadata: Uint8Array, +): SystemRecordCacheMetadataV1 { + let ownedBytes: Uint8Array; + try { + ownedBytes = copyBoundedSystemRecordBytesV1( + encodedMetadata, + SYSTEM_RECORD_MAX_CLOSURE_SIDECAR_LIVE_METADATA_BYTES, + 'cache metadata bytes', + ); + } catch (cause) { + fail('system-record-closure', 'cache metadata bytes exceed the live metadata bound', cause); + } + return new SystemRecordCacheMetadataValueV1( + MINT_SYSTEM_RECORD_CACHE_METADATA_V1, + ownedBytes.byteLength, + ); +} + +export interface SystemRecordCachePreflightResultV1 { + /** Exact proposed cohort delta; B2 must combine it with its full physical-cache baseline. */ + readonly cohortPhysicalObjects: number; + readonly cohortPhysicalBytes: number; + readonly closureReferences: number; + readonly closurePhysicalBytes: number; + readonly closureReferencedBytes: number; + readonly sidecarReferences: number; + readonly sidecars: number; + readonly sidecarPhysicalBytes: number; + readonly sidecarReferencedBytes: number; + readonly activationBundleBytes: number; + readonly activationInventoryLeaves: number; + readonly metadataBytes: number; +} + +export interface SystemRecordCachePreflightInputV1 { + readonly mode: 'live' | 'activation'; + readonly rows: readonly SystemRecordCacheRowAccountingV1[]; + readonly inventoryLeaves?: readonly SystemRecordCacheReferenceV1[]; +} + +/** Pure all-or-nothing aggregate preflight; shared physical digests are charged once. */ +export function preflightSystemRecordCacheAccountingV1( + input: SystemRecordCachePreflightInputV1, +): SystemRecordCachePreflightResultV1 { + const hasInventoryLeaves = hasOwnDataProperty(input, 'inventoryLeaves'); + const exact = snapshotExactDataRecord( + input, + ['mode', 'rows', ...(hasInventoryLeaves ? ['inventoryLeaves'] : [])], + 'cache preflight input', + ); + if (exact.mode !== 'live' && exact.mode !== 'activation') { + fail('system-record-closure', 'cache preflight mode is invalid'); + } + let rows: readonly SystemRecordCacheRowAccountingV1[]; + let inventoryLeaves: readonly SystemRecordCacheReferenceV1[]; + try { + rows = snapshotDataArray(exact.rows, 'cache rows', { + maxLength: exact.mode === 'activation' + ? SYSTEM_RECORD_MAX_ACTIVATION_RECORDS + : SYSTEM_RECORD_MAX_INVENTORY_RECORDS, + }) as readonly SystemRecordCacheRowAccountingV1[]; + inventoryLeaves = snapshotDataArray( + hasInventoryLeaves ? exact.inventoryLeaves : [], + 'activation inventory leaves', + { maxLength: SYSTEM_RECORD_MAX_ACTIVATION_INVENTORY_LEAVES }, + ) as readonly SystemRecordCacheReferenceV1[]; + } catch (cause) { + fail('system-record-closure', 'cache cohort arrays exceed their closed bounds', cause); + } + if (exact.mode === 'live' && inventoryLeaves.length !== 0) { + fail('system-record-closure', 'live preflight must not carry activation inventory leaves'); + } + if (inventoryLeaves.length > SYSTEM_RECORD_MAX_ACTIVATION_INVENTORY_LEAVES) { + fail('system-record-closure', 'activation inventory exceeds its leaf bound'); + } + const physical = new Map(); + const closurePhysical = new Map(); + const sidecarPhysical = new Map(); + const bundlePhysical = new Map(); + let closureReferences = 0; + let closureReferencedBytes = 0; + let sidecarReferences = 0; + let sidecars = 0; + let sidecarReferencedBytes = 0; + let metadataBytes = 0; + let sidecarMetadataBytes = 0; + for (const row of rows) { + const hasSidecar = hasOwnDataProperty(row, 'sidecar'); + const hasSidecarMetadata = hasOwnDataProperty(row, 'sidecarMetadata'); + const exactRow = snapshotExactDataRecord( + row, + [ + 'closure', + 'metadata', + ...(hasSidecar ? ['sidecar'] : []), + ...(hasSidecarMetadata ? ['sidecarMetadata'] : []), + ], + 'cache accounting row', + ); + let closure: readonly SystemRecordCacheReferenceV1[]; + let sidecar: readonly SystemRecordCacheReferenceV1[] | undefined; + try { + closure = snapshotDataArray(exactRow.closure, 'cache row closure', { + maxLength: SYSTEM_RECORD_MAX_CLOSURE_OBJECTS, + }) as readonly SystemRecordCacheReferenceV1[]; + sidecar = hasSidecar + ? snapshotDataArray(exactRow.sidecar, 'cache row sidecar', { + maxLength: SYSTEM_RECORD_MAX_SIDECAR_OBJECTS, + }) as readonly SystemRecordCacheReferenceV1[] + : undefined; + } catch (cause) { + fail('system-record-closure', 'cache row arrays exceed their closed bounds', cause); + } + const rowMetadataBytes = requireCacheMetadataBytes(exactRow.metadata, 'cache row metadata'); + if (hasSidecar !== hasSidecarMetadata) { + fail('system-record-closure', 'cache row sidecar and sidecar metadata must be present together'); + } + const rowSidecarMetadataBytes = hasSidecarMetadata + ? requireCacheMetadataBytes(exactRow.sidecarMetadata, 'cache row sidecar metadata') + : 0; + const rowClosureBytes = accountReferences(closure, closurePhysical, 'closure'); + if (rowClosureBytes > SYSTEM_RECORD_MAX_CLOSURE_BYTES) { + fail('system-record-closure', 'row closure exceeds its byte bound'); + } + closureReferences += closure.length; + closureReferencedBytes += rowClosureBytes; + if (!Number.isSafeInteger(closureReferences) || !Number.isSafeInteger(closureReferencedBytes)) { + fail('system-record-closure', 'aggregate closure accounting overflow'); + } + for (const reference of closure) { + if (reference.objectKind === 'profile-bundle') { + bundlePhysical.set(reference.cacheDigest, requireCacheReferenceFacts(reference, 'closure')); + } + } + if (sidecar !== undefined) { + sidecars += 1; + const rowSidecarBytes = accountReferences(sidecar, sidecarPhysical, 'sidecar'); + if (sidecar.filter((reference) => reference.objectKind === 'conflict-evidence').length !== 1 + || sidecar.some((reference) => reference.objectKind !== 'conflict-evidence' + && reference.objectKind !== 'agent-profile-head' + && reference.objectKind !== 'authority-transition' + && reference.objectKind !== 'fork-resolution')) { + fail('system-record-closure', 'row sidecar must contain one evidence object and only signed controls'); + } + if (rowSidecarBytes > SYSTEM_RECORD_MAX_SIDECAR_BYTES) { + fail('system-record-closure', 'row sidecar exceeds its byte bound'); + } + sidecarReferences += sidecar.length; + sidecarReferencedBytes += rowSidecarBytes; + if (!Number.isSafeInteger(sidecarReferences) || !Number.isSafeInteger(sidecarReferencedBytes)) { + fail('system-record-closure', 'aggregate sidecar accounting overflow'); + } + } + metadataBytes += rowMetadataBytes + rowSidecarMetadataBytes; + sidecarMetadataBytes += rowSidecarMetadataBytes; + if (!Number.isSafeInteger(metadataBytes)) fail('system-record-closure', 'cache metadata accounting overflow'); + if (closurePhysical.size > SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_OBJECTS + || closureReferences > SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES + || metadataBytes > SYSTEM_RECORD_MAX_CLOSURE_SIDECAR_LIVE_METADATA_BYTES + || sidecars > SYSTEM_RECORD_MAX_CONFLICT_SIDECARS + || sidecarReferences > SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_REFERENCES + || sidecarMetadataBytes > SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_METADATA_BYTES) { + fail('system-record-closure', 'aggregate cache accounting exceeds a live V1 bound'); + } + } + accountReferences(inventoryLeaves, new Map(), 'activation inventory'); + if (inventoryLeaves.some((reference) => reference.objectKind !== 'inventory-leaf')) { + fail('system-record-closure', 'activation inventory may contain only leaf objects'); + } + const physicalBytes = [...physical.values()].reduce((sum, entry) => sum + entry.facts.byteLength, 0); + const closurePhysicalBytes = sumPhysicalBytes(closurePhysical); + const sidecarPhysicalBytes = sumPhysicalBytes(sidecarPhysical); + const closureSidecarPhysicalBytes = sumPhysicalBytes(new Map([ + ...closurePhysical, + ...sidecarPhysical, + ])); + const activationBundleBytes = sumPhysicalBytes(bundlePhysical); + if (closurePhysical.size > SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_OBJECTS + || closurePhysicalBytes > SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_BYTES + || closureReferences > SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES + || metadataBytes > SYSTEM_RECORD_MAX_CLOSURE_SIDECAR_LIVE_METADATA_BYTES + || sidecars > SYSTEM_RECORD_MAX_CONFLICT_SIDECARS + || sidecarReferences > SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_REFERENCES + || sidecarPhysicalBytes > SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_AGGREGATE_BYTES + || sidecarMetadataBytes > SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_METADATA_BYTES) { + fail('system-record-closure', 'aggregate cache accounting exceeds a live V1 bound'); + } + if (exact.mode === 'activation' && (activationBundleBytes > SYSTEM_RECORD_MAX_ACTIVATION_BUNDLE_BYTES + || closureSidecarPhysicalBytes > SYSTEM_RECORD_MAX_ACTIVATION_CLOSURE_BYTES + || closureReferences + sidecarReferences + inventoryLeaves.length > SYSTEM_RECORD_MAX_ACTIVATION_REFERENCES + || metadataBytes > SYSTEM_RECORD_MAX_ACTIVATION_METADATA_BYTES)) { + fail('system-record-closure', 'activation cache accounting exceeds its cohort bound'); + } + return Object.freeze({ + cohortPhysicalObjects: physical.size, + cohortPhysicalBytes: physicalBytes, + closureReferences, + closurePhysicalBytes, + closureReferencedBytes, + sidecarReferences, + sidecars, + sidecarPhysicalBytes, + sidecarReferencedBytes, + activationBundleBytes, + activationInventoryLeaves: inventoryLeaves.length, + metadataBytes, + }); + + function accountReferences( + references: readonly SystemRecordCacheReferenceV1[], + category: Map, + label: string, + ): number { + let total = 0; + const logical = new Set(); + for (const reference of references as readonly SystemRecordCacheReferenceV1[]) { + const facts = requireCacheReferenceFacts(reference, label); + digest(reference.digest, `${label} digest`); + digest(reference.cacheDigest, `${label} cache digest`); + const logicalKey = `${reference.objectKind}:${reference.digest}`; + if (logical.has(logicalKey)) { + fail('system-record-closure', `${label} contains a duplicate semantic reference`); + } + logical.add(logicalKey); + const prior = physical.get(reference.cacheDigest); + if (prior !== undefined && (prior.reference.objectKind !== reference.objectKind + || prior.reference.digest !== reference.digest + || prior.facts.byteLength !== facts.byteLength + || prior.facts.fingerprint !== facts.fingerprint)) { + fail('system-record-closure', 'one cache digest was reported with conflicting canonical bytes'); + } + physical.set(reference.cacheDigest, { reference, facts }); + category.set(reference.cacheDigest, facts); + total += facts.byteLength; + if (!Number.isSafeInteger(total)) fail('system-record-closure', `${label} byte accounting overflow`); + } + return total; + } + + function sumPhysicalBytes(references: ReadonlyMap): number { + return [...references.values()].reduce((sum, facts) => sum + facts.byteLength, 0); + } +} + +function requireCacheMetadataBytes(value: unknown, label: string): number { + const byteLength = typeof value === 'object' && value !== null + ? SYSTEM_RECORD_CACHE_METADATA_BYTES_V1.get(value) + : undefined; + if (byteLength === undefined || Object.keys(value as object).length !== 0) { + fail('system-record-closure', `${label} was not derived from encoded bytes`); + } + return byteLength; +} + +function requireCacheReferenceFacts( + reference: SystemRecordCacheReferenceV1, + label: string, +): SystemRecordCacheReferenceFactsV1 { + const facts = typeof reference === 'object' && reference !== null + ? SYSTEM_RECORD_CACHE_REFERENCE_FACTS_V1.get(reference) + : undefined; + if (facts === undefined + || Object.keys(reference).sort().join('\u0000') !== 'cacheDigest\u0000digest\u0000objectKind') { + fail('system-record-closure', `${label} reference was not derived from canonical bytes`); + } + digest(reference.digest, `${label} digest`); + digest(reference.cacheDigest, `${label} cache digest`); + if (!Object.prototype.hasOwnProperty.call(SYSTEM_RECORD_OBJECT_CAPS_V1, reference.objectKind)) { + fail('system-record-closure', `${label} object kind is invalid`); + } + return facts; +} + +function deriveCacheReferenceArtifactIdentitiesV1( + objectKind: SystemRecordObjectKindV1, + canonicalBytes: Uint8Array, + label: string, +): Readonly<{ semanticDigest: Digest32V1; cacheDigest: Digest32V1 }> { + let semanticDigest: Digest32V1; + let cacheDigest: Digest32V1; + if (objectKind === 'agent-profile-head') { + const envelope = parseCanonicalSignedAgentProfileHeadEnvelopeV1(canonicalBytes); + semanticDigest = envelope.objectDigest; + cacheDigest = computeSignedSystemRecordEnvelopeDigestV1(envelope); + } else if (objectKind === 'authority-transition') { + const envelope = parseCanonicalSignedAgentProfileAuthorityTransitionEnvelopeV1(canonicalBytes); + semanticDigest = envelope.objectDigest; + cacheDigest = computeSignedSystemRecordEnvelopeDigestV1(envelope); + } else if (objectKind === 'fork-resolution') { + const envelope = parseCanonicalSignedAgentProfileForkResolutionEnvelopeV1(canonicalBytes); + semanticDigest = envelope.objectDigest; + cacheDigest = computeSignedSystemRecordEnvelopeDigestV1(envelope); + } else if (objectKind === 'root-descriptor') { + const envelope = parseCanonicalSignedSystemRecordRootDescriptorEnvelopeV1(canonicalBytes); + semanticDigest = envelope.objectDigest; + cacheDigest = digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.signedRootDescriptorEnvelope, + canonicalBytes, + ); + } else { + const domains: Record, string> = { + 'inventory-internal': SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryInternal, + 'inventory-leaf': SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryLeaf, + 'conflict-evidence': SYSTEM_RECORD_DIGEST_DOMAINS_V1.conflictEvidence, + 'owned-subject-table': SYSTEM_RECORD_DIGEST_DOMAINS_V1.ownedSubjectTable, + 'profile-bundle': SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + }; + semanticDigest = digestSystemRecordBytesV1(domains[objectKind], canonicalBytes); + cacheDigest = semanticDigest; + } + if (semanticDigest.length !== 66 || cacheDigest.length !== 66) { + fail('system-record-closure', `${label} artifact identity is invalid`); + } + return Object.freeze({ semanticDigest, cacheDigest }); +} + +function validateAppliedTransitionLineage( + value: unknown, +): readonly AgentProfileAppliedTransitionV1[] { + let lineage: readonly unknown[]; + try { + lineage = snapshotDataArray(value, 'applied transition lineage', { + maxLength: Number(SYSTEM_RECORD_AUTHORITY_SEQUENCE_MAX), + }); + } catch (cause) { + fail('system-record-history', 'applied transition lineage exceeds its closed V1 bound', cause); + } + let expectedPrior = 0n; + const result: AgentProfileAppliedTransitionV1[] = []; + for (const entry of lineage) { + const exact = snapshotExactDataRecord( + entry, + ['priorAuthoritySequence', 'nextAuthoritySequence', 'transitionDigest'], + 'applied transition lineage entry', + ); + const prior = u64(exact.priorAuthoritySequence, 'priorAuthoritySequence'); + const next = u64(exact.nextAuthoritySequence, 'nextAuthoritySequence'); + digest(exact.transitionDigest, 'transitionDigest'); + if (prior !== expectedPrior || next !== prior + 1n) { + fail('system-record-history', 'applied transition lineage must be contiguous from sequence zero'); + } + expectedPrior = next; + result.push(Object.freeze({ ...exact }) as unknown as AgentProfileAppliedTransitionV1); + } + return Object.freeze(result); +} + +function snapshotAcceptedAuthorityStateV1(value: unknown): AgentProfileAcceptedAuthorityStateV1 { + const probe = snapshotSystemRecordDataRecord(value, 'accepted authority state'); + const state = snapshotExactDataRecord( + probe, + [ + ...(hasOwnDataProperty(probe, 'current') ? ['current'] : []), + 'disposition', + 'transitionLineage', + 'historicalRoots', + ...(hasOwnDataProperty(probe, 'frontierConflictHeads') ? ['frontierConflictHeads'] : []), + ], + 'accepted authority state', + ); + if (state.disposition !== 'discoverable' + && state.disposition !== 'head-fork-quarantined' + && state.disposition !== 'transition-equivocation-quarantined') { + fail('system-record-history', 'accepted authority disposition is invalid'); + } + return Object.freeze({ + ...(hasOwnDataProperty(state, 'current') ? { current: state.current } : {}), + disposition: state.disposition, + transitionLineage: state.transitionLineage, + historicalRoots: state.historicalRoots, + }) as unknown as AgentProfileAcceptedAuthorityStateV1; +} + +function snapshotHeadAdvanceEvidenceV1(value: unknown): AgentProfileHeadAdvanceEvidenceV1 { + const probe = snapshotSystemRecordDataRecord(value, 'head advance evidence'); + const optionals = [ + 'acceptedTransition', + 'tombstonePredecessor', + 'verifiedAuthoritySummary', + 'forkResolution', + 'forkEvidenceHeads', + 'forkBaseHead', + ].filter((key) => hasOwnDataProperty(probe, key)); + const evidence = snapshotExactDataRecord( + probe, + ['nowMs', ...optionals], + 'head advance evidence', + ); + return Object.freeze({ ...evidence }) as unknown as AgentProfileHeadAdvanceEvidenceV1; +} + +function validateAcceptedRootHistoryV1( + accepted: AgentProfileAcceptedAuthorityStateV1, + current: AgentProfileHeadObjectV1 | undefined, + lineage: readonly AgentProfileAppliedTransitionV1[], +): readonly string[] { + let historicalRoots: readonly unknown[]; + try { + historicalRoots = snapshotDataArray(accepted.historicalRoots, 'accepted root history', { + maxLength: SYSTEM_RECORD_MAX_ROOT_CLAIMS - 1, + }); + } catch (cause) { + fail('system-record-history', 'accepted authority state lacks a closed root history', cause); + } + if (current === undefined) { + if (historicalRoots.length !== 0) { + fail('system-record-history', 'absent authority state cannot retain root history'); + } + return historicalRoots as readonly string[]; + } + if (historicalRoots.length !== lineage.length) { + fail('system-record-history', 'accepted root history must match transition lineage'); + } + const roots = new Set([current.rootSubject]); + for (const root of historicalRoots) { + assertAgentRootV1(root); + if (roots.has(root)) fail('system-record-history', 'accepted root history must be duplicate-free'); + roots.add(root); + } + return historicalRoots as readonly string[]; +} + +function isSafeNow(nowMs: number): boolean { + return Number.isSafeInteger(nowMs) && nowMs >= 0; +} + +function isIssuedTooFarInFuture(issuedAt: CanonicalRfc3339SecondsV1, nowMs: number): boolean { + return Date.parse(issuedAt) > nowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS; +} + +function validateEip1271Evidence( + value: unknown, + issuer: string, + networkId: NetworkIdV1, +): SystemRecordEip1271EvidenceV1 { + const evidence = snapshotExactDataRecord( + value, + ['kind', 'chainId', 'contractAddress', 'finalizedBlockNumber', 'finalizedBlockHash'], + 'EIP-1271 evidence', + ); + if (evidence.kind !== 'eip1271-current-finalized') { + fail('system-record-signature', 'EIP-1271 evidence kind is invalid'); + } + try { + assertCanonicalChainId(evidence.chainId); + assertCanonicalEvmAddress(evidence.contractAddress); + assertCanonicalDecimalU64(evidence.finalizedBlockNumber); + assertCanonicalDigest(evidence.finalizedBlockHash); + } catch (cause) { + fail('system-record-signature', 'EIP-1271 finalized evidence is invalid', cause); + } + if (evidence.contractAddress !== issuer) { + fail('system-record-binding', 'EIP-1271 contract does not match signer'); + } + const expectedChainId = numericChainIdForNetworkV1(networkId); + if (evidence.chainId !== expectedChainId) { + fail('system-record-binding', 'EIP-1271 evidence chainId does not match the record network'); + } + return Object.freeze({ ...evidence }) as unknown as SystemRecordEip1271EvidenceV1; +} + +function numericChainIdForNetworkV1(networkId: NetworkIdV1): ChainIdV1 { + const separator = networkId.lastIndexOf(':'); + const chainId = separator <= 0 ? '' : networkId.slice(separator + 1); + try { + assertCanonicalChainId(chainId, 'network chainId'); + } catch (cause) { + fail('system-record-binding', 'record requires a numeric chain-bound networkId', cause); + } + return chainId as ChainIdV1; +} + +function exactNoneEvidence(value: unknown): SystemRecordNoSignatureEvidenceV1 { + const evidence = snapshotExactDataRecord(value, ['kind'], 'signature evidence'); + if (evidence.kind !== 'none') fail('system-record-signature', 'signature evidence must be none'); + return Object.freeze({ kind: 'none' }); +} + +function issuerForRole( + object: AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1, + role: Exclude, +): string { + if (object.objectType === 'authority-transition') { + if (role === 'prior-evm') return object.priorEvmIssuer; + if (role === 'next-evm') return object.nextEvmIssuer; + } else if (role === 'current-evm') { + return object.evmIssuer; + } + fail('system-record-signature', `${role} is not valid for ${object.objectType}`); +} + +function classifyEnvelopeObject(value: unknown): 'head' | 'transition' | 'fork' { + const record = snapshotSystemRecordDataRecord(value, 'signed envelope object'); + if (record.objectType === 'agent-profile-head') return 'head'; + if (record.objectType === 'authority-transition') return 'transition'; + if (record.objectType === 'fork-resolution') return 'fork'; + fail('system-record-schema', 'signed envelope object type is unsupported'); +} + +function validateDispatchedSignedEnvelope(value: unknown): { + readonly kind: 'head' | 'transition' | 'fork'; + readonly validated: SignedSystemRecordEnvelopeV1; +} { + const envelope = snapshotExactDataRecord( + value, + ['object', 'objectDigest', 'signatures'], + 'signed system-record envelope', + ); + const kind = classifyEnvelopeObject(envelope.object); + return Object.freeze({ kind, validated: validateSignedEnvelope(envelope, kind) }); +} + +function objectKindForEnvelope( + value: 'head' | 'transition' | 'fork', +): 'agent-profile-head' | 'authority-transition' | 'fork-resolution' { + return value === 'head' + ? 'agent-profile-head' + : value === 'transition' + ? 'authority-transition' + : 'fork-resolution'; +} + +function snapshotSystemRecordDataRecord( + value: unknown, + label: string, +): Readonly> { + try { + return snapshotDataRecord(value, label, { rejectNullValues: true }); + } catch (cause) { + fail('system-record-schema', cause instanceof Error ? cause.message : `${label} is invalid`, cause); + } +} + +function assertNetwork(value: unknown): asserts value is NetworkIdV1 { + try { + assertNetworkIdV1(value); + } catch (cause) { + fail('system-record-scalar', 'networkId is invalid', cause); + } +} + +function address(value: unknown, label: string): asserts value is EvmAddressV1 { + try { assertCanonicalEvmAddress(value, label); } catch (cause) { + fail('system-record-scalar', `${label} is invalid`, cause); + } +} + +function digest(value: unknown, label: string): asserts value is Digest32V1 { + try { assertCanonicalDigest(value, label); } catch (cause) { + fail('system-record-scalar', `${label} is invalid`, cause); + } +} + +function u64(value: unknown, label: string): bigint { + try { return parseCanonicalDecimalU64(value, label); } catch (cause) { + fail('system-record-scalar', `${label} is invalid`, cause); + } +} + +function digestArray( + value: unknown, + label: string, + min: number, + max: number, +): readonly Digest32V1[] { + let snapshot: readonly unknown[]; + try { + snapshot = snapshotDataArray(value, label, { minLength: min, maxLength: max }); + } catch (cause) { + fail('system-record-limit', `${label} must contain ${min}-${max} closed digests`, cause); + } + for (let index = 0; index < snapshot.length; index += 1) { + digest(snapshot[index], `${label}[${index}]`); + if (index > 0 && (snapshot[index - 1] as string) >= (snapshot[index] as string)) { + fail('system-record-order', `${label} must be sorted and duplicate-free`); + } + } + return snapshot as readonly Digest32V1[]; +} + +function compareBytes(left: Uint8Array, right: Uint8Array): number { + const length = Math.min(left.byteLength, right.byteLength); + for (let index = 0; index < length; index += 1) { + if (left[index] !== right[index]) return left[index] - right[index]; + } + return left.byteLength - right.byteLength; +} + +function hexToBytes(value: string): Uint8Array { + return Uint8Array.from(Buffer.from(value.slice(2), 'hex')); +} + +function bytesToBigInt(value: Uint8Array): bigint { + return BigInt(`0x${Buffer.from(value).toString('hex')}`); +} + +function concatBytes(...values: readonly Uint8Array[]): Uint8Array { + const length = values.reduce((sum, value) => sum + value.byteLength, 0); + const result = new Uint8Array(length); + let offset = 0; + for (const value of values) { + result.set(value, offset); + offset += value.byteLength; + } + return result; +} diff --git a/packages/core/src/system-record-v1.ts b/packages/core/src/system-record-v1.ts new file mode 100644 index 0000000000..2dc01d5dc8 --- /dev/null +++ b/packages/core/src/system-record-v1.ts @@ -0,0 +1,11 @@ +/** + * Dormant system-record V1 protocol foundation. + * + * Kept off the broad core barrel so legacy/default-disabled node startup does not + * eagerly load the protocol's cryptographic and canonical-codec dependency graph. + */ +export * from './system-record-limits-v1.js'; +export * from './system-record-objects-v1.js'; +export * from './system-record-applied-state-v1.js'; +export * from './system-record-inventory-v1.js'; +export * from './system-record-wire-v1.js'; diff --git a/packages/core/src/system-record-wire-v1.ts b/packages/core/src/system-record-wire-v1.ts new file mode 100644 index 0000000000..01c3af9f04 --- /dev/null +++ b/packages/core/src/system-record-wire-v1.ts @@ -0,0 +1,494 @@ +import { + canonicalizeJsonBytes, + parseCanonicalJson, + type CanonicalJsonValue, +} from './canonical-json.js'; +import { + computeAgentProfileAuthorityTransitionDigestV1, + computeAgentProfileConflictEvidenceDigestV1, + computeAgentProfileForkResolutionDigestV1, + computeAgentProfileHeadObjectDigestV1, + parseCanonicalAgentProfileConflictEvidenceV1, + parseCanonicalSignedAgentProfileAuthorityTransitionEnvelopeV1, + parseCanonicalSignedAgentProfileForkResolutionEnvelopeV1, + parseCanonicalSignedAgentProfileHeadEnvelopeV1, +} from './system-record-objects-v1.js'; +import { + copyBoundedSystemRecordBytesV1, + digestSystemRecordBytesV1, +} from './system-record-codec-primitives-v1.js'; +import { + computeSystemRecordInventoryInternalDigestV1, + computeSystemRecordInventoryLeafDigestV1, + parseCanonicalSignedSystemRecordRootDescriptorEnvelopeV1, + parseCanonicalSystemRecordInventoryInternalObjectV1, + parseCanonicalSystemRecordInventoryLeafObjectV1, +} from './system-record-inventory-v1.js'; +import { + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_KIND_V1, + SYSTEM_RECORD_MAX_ARRAY_JSON_DEPTH, + SYSTEM_RECORD_MAX_FRAME_BYTES, + SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + SYSTEM_RECORD_MAX_HEADER_BYTES, + SYSTEM_RECORD_MAX_INVENTORY_CHILD_INDEX, + SYSTEM_RECORD_MAX_INVENTORY_PATH_DEPTH, + SYSTEM_RECORD_MAX_WIRE_REQUEST_JSON_DEPTH, + SYSTEM_RECORD_OBJECT_CAPS_V1, + SYSTEM_RECORD_WIRE_VERSION_V1, + type SystemRecordObjectKindV1, +} from './system-record-limits-v1.js'; +import { assertNetworkIdV1, type NetworkIdV1 } from './sync-wire-identifiers.js'; +import { + assertCanonicalDecimalU64, + assertCanonicalDigest, + parseCanonicalDecimalU64, + type DecimalU64V1, + type Digest32V1, +} from './sync-wire-scalars.js'; +import { snapshotDataArray, snapshotDataRecord, snapshotExactDataRecord } from './sync-wire-objects.js'; + +const REQUEST_ID = /^[0-9a-f]{32}$/; +const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype) as object; +const TYPED_ARRAY_BUFFER = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, 'buffer')?.get; +const TYPED_ARRAY_BYTE_LENGTH = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, 'byteLength')?.get; +const TYPED_ARRAY_BYTE_OFFSET = Object.getOwnPropertyDescriptor(TYPED_ARRAY_PROTOTYPE, 'byteOffset')?.get; + +export type SystemRecordRequestOperationV1 = + | 'get-root' + | 'get-inventory-object' + | 'get-control-object' + | 'get-bundle'; + +interface SystemRecordRequestCommonV1 { + readonly wireVersion: typeof SYSTEM_RECORD_WIRE_VERSION_V1; + readonly requestId: string; + readonly kind: typeof SYSTEM_RECORD_KIND_V1; + readonly networkId: NetworkIdV1; + readonly operation: SystemRecordRequestOperationV1; + readonly payloadBytes: '0'; +} + +export interface SystemRecordGetRootRequestV1 extends SystemRecordRequestCommonV1 { + readonly operation: 'get-root'; +} + +export interface SystemRecordGetInventoryObjectRequestV1 extends SystemRecordRequestCommonV1 { + readonly operation: 'get-inventory-object'; + readonly rootDescriptorDigest: Digest32V1; + readonly path: readonly number[]; + readonly objectKind: 'inventory-internal' | 'inventory-leaf'; + readonly objectDigest: Digest32V1; +} + +export interface SystemRecordGetControlObjectRequestV1 extends SystemRecordRequestCommonV1 { + readonly operation: 'get-control-object'; + readonly objectKind: + | 'agent-profile-head' + | 'authority-transition' + | 'fork-resolution' + | 'conflict-evidence' + | 'owned-subject-table'; + readonly objectDigest: Digest32V1; +} + +export interface SystemRecordGetBundleRequestV1 extends SystemRecordRequestCommonV1 { + readonly operation: 'get-bundle'; + readonly objectKind: 'profile-bundle'; + readonly objectDigest: Digest32V1; +} + +export type SystemRecordRequestHeaderV1 = + | SystemRecordGetRootRequestV1 + | SystemRecordGetInventoryObjectRequestV1 + | SystemRecordGetControlObjectRequestV1 + | SystemRecordGetBundleRequestV1; + +export type SystemRecordResponseStatusV1 = + | 'ok' + | 'not-found' + | 'invalid-request' + | 'unsupported' + | 'busy' + | 'internal'; + +export interface SystemRecordOkResponseHeaderV1 { + readonly wireVersion: typeof SYSTEM_RECORD_WIRE_VERSION_V1; + readonly requestId: string; + readonly status: 'ok'; + readonly objectKind: SystemRecordObjectKindV1; + readonly objectDigest: Digest32V1; + readonly payloadBytes: DecimalU64V1; +} + +export interface SystemRecordErrorResponseHeaderV1 { + readonly wireVersion: typeof SYSTEM_RECORD_WIRE_VERSION_V1; + readonly requestId: string; + readonly status: Exclude; + readonly payloadBytes: '0'; + readonly errorCode: 'not_found' | 'invalid_request' | 'unsupported' | 'busy' | 'internal'; +} + +export type SystemRecordResponseHeaderV1 = + | SystemRecordOkResponseHeaderV1 + | SystemRecordErrorResponseHeaderV1; + +export interface SystemRecordDecodedResponseHeaderV1 { + readonly header: SystemRecordResponseHeaderV1; + readonly payloadCap: number; +} + +export interface SystemRecordDecodedResponseFrameV1 { + readonly header: SystemRecordResponseHeaderV1; + readonly payload: Uint8Array; +} + +export function readSystemRecordHeaderLengthV1(prefix: Uint8Array): number { + const ownedPrefix = copyBoundedSystemRecordBytesV1(prefix, 4, 'system-record frame prefix'); + if (ownedPrefix.byteLength !== 4) { + throw new Error('system-record frame prefix must be exactly four bytes'); + } + const length = new DataView(ownedPrefix.buffer, ownedPrefix.byteOffset, 4).getUint32(0, false); + if (length < 2 || length > SYSTEM_RECORD_MAX_HEADER_BYTES) { + throw new Error('system-record header length exceeds the preallocation cap'); + } + return length; +} + +export function encodeSystemRecordRequestFrameV1( + header: SystemRecordRequestHeaderV1, +): Uint8Array { + const validated = validateRequestHeader(header); + const headerBytes = canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_HEADER_BYTES, + }); + return frame(headerBytes, new Uint8Array()); +} + +export function decodeSystemRecordRequestFrameV1(frameBytes: Uint8Array): SystemRecordRequestHeaderV1 { + const { headerBytes, payload } = splitFrame(frameBytes); + if (payload.byteLength !== 0) throw new Error('system-record requests must be payload-free'); + return validateRequestHeader(parseCanonicalJson(headerBytes, { + maxBytes: SYSTEM_RECORD_MAX_HEADER_BYTES, maxDepth: SYSTEM_RECORD_MAX_WIRE_REQUEST_JSON_DEPTH, + })); +} + +export function encodeSystemRecordResponseFrameV1( + header: SystemRecordResponseHeaderV1, + payload: Uint8Array, +): Uint8Array { + const validated = validateResponseHeader(header); + const expected = Number(parseCanonicalDecimalU64(validated.payloadBytes)); + const cap = validated.status === 'ok' ? SYSTEM_RECORD_OBJECT_CAPS_V1[validated.objectKind] : 0; + const ownedPayload = copyBoundedSystemRecordBytesV1(payload, cap, 'system-record response payload'); + if (ownedPayload.byteLength !== expected) { + throw new Error('system-record response payload length does not match its header'); + } + const headerBytes = canonicalizeJsonBytes(validated as unknown as CanonicalJsonValue, { + maxBytes: SYSTEM_RECORD_MAX_HEADER_BYTES, + }); + return frame(headerBytes, ownedPayload); +} + +/** Parse only validated header bytes and expose the kind-specific payload allocation cap. */ +export function decodeSystemRecordResponseHeaderV1( + headerBytes: Uint8Array, +): SystemRecordDecodedResponseHeaderV1 { + const header = validateResponseHeader(parseCanonicalJson(headerBytes, { + maxBytes: SYSTEM_RECORD_MAX_HEADER_BYTES, maxDepth: SYSTEM_RECORD_MAX_FLAT_JSON_DEPTH, + })); + const payloadCap = header.status === 'ok' ? SYSTEM_RECORD_OBJECT_CAPS_V1[header.objectKind] : 0; + if (Number(parseCanonicalDecimalU64(header.payloadBytes)) > payloadCap) { + throw new Error('response declares more payload than its validated object kind permits'); + } + return Object.freeze({ header, payloadCap }); +} + +export function decodeSystemRecordResponseFrameV1( + frameBytes: Uint8Array, +): SystemRecordDecodedResponseFrameV1 { + const { headerBytes, payload } = splitFrame(frameBytes); + const { header, payloadCap } = decodeSystemRecordResponseHeaderV1(headerBytes); + if (payload.byteLength !== Number(parseCanonicalDecimalU64(header.payloadBytes))) { + throw new Error('response payload length does not match its header'); + } + if (payload.byteLength > payloadCap) throw new Error('response payload exceeds its object-kind cap'); + // The decoded payload is a borrowed view of frameBytes; callers copy only when retaining it. + return Object.freeze({ header, payload }); +} + +export function verifySystemRecordResponsePayloadV1( + request: SystemRecordRequestHeaderV1, + response: SystemRecordResponseHeaderV1, + payload: Uint8Array, +): void { + const validatedRequest = validateRequestHeader(request); + const validatedResponse = validateResponseHeader(response); + if (validatedResponse.requestId !== validatedRequest.requestId) { + throw new Error('system-record response requestId mismatch'); + } + if (validatedResponse.status !== 'ok') { + const ownedPayload = copyBoundedSystemRecordBytesV1( + payload, + 0, + 'system-record error response payload', + ); + if (ownedPayload.byteLength !== 0) throw new Error('error response must be payload-free'); + return; + } + const expectedKind = validatedRequest.operation === 'get-root' + ? 'root-descriptor' + : validatedRequest.objectKind; + if (validatedResponse.objectKind !== expectedKind) { + throw new Error('system-record response object kind does not match the request'); + } + if (validatedRequest.operation !== 'get-root' + && validatedResponse.objectDigest !== validatedRequest.objectDigest) { + throw new Error('system-record response object digest does not match the request'); + } + const ownedPayload = copyBoundedSystemRecordBytesV1( + payload, + SYSTEM_RECORD_OBJECT_CAPS_V1[validatedResponse.objectKind], + 'system-record response payload', + ); + if (ownedPayload.byteLength !== Number(parseCanonicalDecimalU64(validatedResponse.payloadBytes))) { + throw new Error('system-record payload length mismatch'); + } + const computed = computePayloadObjectDigestV1(validatedRequest, validatedResponse.objectKind, ownedPayload); + if (computed !== validatedResponse.objectDigest) { + throw new Error('system-record payload canonical object digest mismatch'); + } +} + +function computePayloadObjectDigestV1( + request: SystemRecordRequestHeaderV1, + objectKind: SystemRecordObjectKindV1, + payload: Uint8Array, +): Digest32V1 { + switch (objectKind) { + case 'root-descriptor': { + const envelope = parseCanonicalSignedSystemRecordRootDescriptorEnvelopeV1(payload); + if (request.operation !== 'get-root' + || envelope.object.kind !== request.kind + || envelope.object.networkId !== request.networkId) { + throw new Error('root descriptor does not bind the requested kind/network'); + } + return envelope.objectDigest; + } + case 'inventory-internal': { + if (request.operation !== 'get-inventory-object') throw new Error('inventory response/request mismatch'); + const root = request.path.length === 0; + const object = parseCanonicalSystemRecordInventoryInternalObjectV1(payload, root); + return computeSystemRecordInventoryInternalDigestV1(object, root); + } + case 'inventory-leaf': { + if (request.operation !== 'get-inventory-object') throw new Error('inventory response/request mismatch'); + const root = request.path.length === 0; + const object = parseCanonicalSystemRecordInventoryLeafObjectV1(payload, request.networkId, root); + return computeSystemRecordInventoryLeafDigestV1(object, request.networkId, root); + } + case 'agent-profile-head': { + const envelope = parseCanonicalSignedAgentProfileHeadEnvelopeV1(payload); + assertRequestedControlContextV1(request, envelope.object); + return computeAgentProfileHeadObjectDigestV1(envelope.object); + } + case 'authority-transition': { + const envelope = parseCanonicalSignedAgentProfileAuthorityTransitionEnvelopeV1(payload); + assertRequestedControlContextV1(request, envelope.object); + return computeAgentProfileAuthorityTransitionDigestV1(envelope.object); + } + case 'fork-resolution': { + const envelope = parseCanonicalSignedAgentProfileForkResolutionEnvelopeV1(payload); + assertRequestedControlContextV1(request, envelope.object); + return computeAgentProfileForkResolutionDigestV1(envelope.object); + } + case 'conflict-evidence': { + const evidence = parseCanonicalAgentProfileConflictEvidenceV1(payload); + assertRequestedControlContextV1(request, evidence); + return computeAgentProfileConflictEvidenceDigestV1(evidence); + } + case 'owned-subject-table': { + const canonical = parseCanonicalJson(payload, { + maxBytes: SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'], maxDepth: SYSTEM_RECORD_MAX_ARRAY_JSON_DEPTH, + }); + if (!Array.isArray(canonical)) throw new Error('owned-subject table payload must be an array'); + return digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.ownedSubjectTable, payload); + } + case 'profile-bundle': + return digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, payload); + } +} + +function assertRequestedControlContextV1( + request: SystemRecordRequestHeaderV1, + object: Readonly<{ kind: string; networkId: string }>, +): void { + if (request.operation !== 'get-control-object' + || object.kind !== request.kind + || object.networkId !== request.networkId) { + throw new Error('control object does not bind the requested kind/network'); + } +} + +function validateRequestHeader(value: unknown): SystemRecordRequestHeaderV1 { + const probe = snapshotDataRecord(value, 'system-record request header', { + rejectNullValues: true, + }); + const operation = probe.operation; + let expected: readonly string[]; + if (operation === 'get-root') { + expected = ['wireVersion', 'requestId', 'kind', 'networkId', 'operation', 'payloadBytes']; + } else if (operation === 'get-inventory-object') { + expected = [ + 'wireVersion', 'requestId', 'kind', 'networkId', 'operation', + 'rootDescriptorDigest', 'path', 'objectKind', 'objectDigest', 'payloadBytes', + ]; + } else if (operation === 'get-control-object' || operation === 'get-bundle') { + expected = [ + 'wireVersion', 'requestId', 'kind', 'networkId', 'operation', + 'objectKind', 'objectDigest', 'payloadBytes', + ]; + } else { + throw new Error('system-record request operation is invalid'); + } + const request = snapshotExactDataRecord(probe, expected, 'system-record request header'); + validateHeaderCommon(request); + if (request.kind !== SYSTEM_RECORD_KIND_V1 || request.payloadBytes !== '0') { + throw new Error('system-record request kind/payloadBytes is invalid'); + } + assertNetworkIdV1(request.networkId); + let path: readonly number[] | undefined; + if (operation === 'get-inventory-object') { + assertCanonicalDigest(request.rootDescriptorDigest); + path = validatePath(request.path); + if (request.objectKind !== 'inventory-internal' && request.objectKind !== 'inventory-leaf') { + throw new Error('inventory request object kind is invalid'); + } + assertCanonicalDigest(request.objectDigest); + } else if (operation === 'get-control-object') { + if (![ + 'agent-profile-head', 'authority-transition', 'fork-resolution', + 'conflict-evidence', 'owned-subject-table', + ].includes(request.objectKind as string)) { + throw new Error('control request object kind is invalid'); + } + assertCanonicalDigest(request.objectDigest); + } else if (operation === 'get-bundle') { + if (request.objectKind !== 'profile-bundle') throw new Error('bundle request object kind is invalid'); + assertCanonicalDigest(request.objectDigest); + } + return Object.freeze({ + ...request, + ...(path === undefined ? {} : { path }), + }) as unknown as SystemRecordRequestHeaderV1; +} + +function validateResponseHeader(value: unknown): SystemRecordResponseHeaderV1 { + const probe = snapshotDataRecord(value, 'system-record response header', { + rejectNullValues: true, + }); + const status = probe.status; + if (status === 'ok') { + const response = snapshotExactDataRecord( + probe, + ['wireVersion', 'requestId', 'status', 'objectKind', 'objectDigest', 'payloadBytes'], + 'successful system-record response header', + ); + validateHeaderCommon(response); + if (typeof response.objectKind !== 'string' + || !Object.prototype.hasOwnProperty.call(SYSTEM_RECORD_OBJECT_CAPS_V1, response.objectKind)) { + throw new Error('response object kind is invalid'); + } + assertCanonicalDigest(response.objectDigest); + assertCanonicalDecimalU64(response.payloadBytes); + const payloadBytes = parseCanonicalDecimalU64(response.payloadBytes); + if (payloadBytes < 1n + || payloadBytes > BigInt(SYSTEM_RECORD_OBJECT_CAPS_V1[response.objectKind as SystemRecordObjectKindV1])) { + throw new Error('successful response payloadBytes is outside its object cap'); + } + return Object.freeze({ ...response }) as unknown as SystemRecordOkResponseHeaderV1; + } + const errors = { + 'not-found': 'not_found', + 'invalid-request': 'invalid_request', + unsupported: 'unsupported', + busy: 'busy', + internal: 'internal', + } as const; + if (typeof status !== 'string' || !Object.prototype.hasOwnProperty.call(errors, status)) { + throw new Error('response status is invalid'); + } + const response = snapshotExactDataRecord( + probe, + ['wireVersion', 'requestId', 'status', 'payloadBytes', 'errorCode'], + 'error system-record response header', + ); + validateHeaderCommon(response); + if (response.payloadBytes !== '0' + || response.errorCode !== errors[status as keyof typeof errors]) { + throw new Error('response status/errorCode/payloadBytes tuple is invalid'); + } + return Object.freeze({ ...response }) as unknown as SystemRecordErrorResponseHeaderV1; +} + +function validateHeaderCommon(header: Readonly>): void { + if (header.wireVersion !== SYSTEM_RECORD_WIRE_VERSION_V1 + || typeof header.requestId !== 'string' + || !REQUEST_ID.test(header.requestId)) { + throw new Error('system-record wireVersion/requestId is invalid'); + } +} + +function validatePath(value: unknown): readonly number[] { + const path = snapshotDataArray(value, 'inventory traversal path', { + maxLength: SYSTEM_RECORD_MAX_INVENTORY_PATH_DEPTH, + }); + for (const index of path) { + if (!Number.isInteger(index) || (index as number) < 0 + || (index as number) > SYSTEM_RECORD_MAX_INVENTORY_CHILD_INDEX) { + throw new Error('inventory traversal path must contain bounded child indexes'); + } + } + return path as readonly number[]; +} + +function frame(header: Uint8Array, payload: Uint8Array): Uint8Array { + if (header.byteLength > SYSTEM_RECORD_MAX_HEADER_BYTES) throw new Error('system-record header exceeds cap'); + const total = 4 + header.byteLength + payload.byteLength; + if (total > SYSTEM_RECORD_MAX_FRAME_BYTES) throw new Error('system-record frame exceeds cap'); + const result = new Uint8Array(total); + new DataView(result.buffer).setUint32(0, header.byteLength, false); + result.set(header, 4); + result.set(payload, 4 + header.byteLength); + return result; +} + +function splitFrame(frameBytes: Uint8Array): { headerBytes: Uint8Array; payload: Uint8Array } { + const frame = intrinsicSystemRecordByteView(frameBytes); + if (frame.byteLength < 6 || frame.byteLength > SYSTEM_RECORD_MAX_FRAME_BYTES) { + throw new Error('system-record frame length is invalid'); + } + const headerLength = readSystemRecordHeaderLengthV1(frame.subarray(0, 4)); + if (4 + headerLength > frame.byteLength) throw new Error('system-record frame is truncated'); + return { + headerBytes: frame.subarray(4, 4 + headerLength), + payload: frame.subarray(4 + headerLength), + }; +} + +function intrinsicSystemRecordByteView(value: unknown): Uint8Array { + if (!(value instanceof Uint8Array) + || TYPED_ARRAY_BUFFER === undefined + || TYPED_ARRAY_BYTE_LENGTH === undefined + || TYPED_ARRAY_BYTE_OFFSET === undefined) { + throw new Error('system-record frame must be Uint8Array bytes'); + } + try { + const buffer = Reflect.apply(TYPED_ARRAY_BUFFER, value, []) as ArrayBufferLike; + const byteLength = Reflect.apply(TYPED_ARRAY_BYTE_LENGTH, value, []) as number; + const byteOffset = Reflect.apply(TYPED_ARRAY_BYTE_OFFSET, value, []) as number; + return new Uint8Array(buffer, byteOffset, byteLength); + } catch { + throw new Error('system-record frame must be valid Uint8Array bytes'); + } +} diff --git a/packages/core/test/canonical-json.test.ts b/packages/core/test/canonical-json.test.ts index 6298b71dfc..21b9e0fc4a 100644 --- a/packages/core/test/canonical-json.test.ts +++ b/packages/core/test/canonical-json.test.ts @@ -85,6 +85,12 @@ describe('RFC 8785 canonical JSON', () => { } }); + it('rejects oversized strings before UTF-8 expansion and retains the exact byte cap', () => { + expect(() => parseJsonStrict('0'.repeat(9), { maxBytes: 8 })).toThrow(/exceeds 8 bytes/); + expect(() => parseJsonStrict('"é"', { maxBytes: 3 })).toThrow(/exceeds 3 bytes/); + expect(parseJsonStrict('"é"', { maxBytes: 4 })).toBe('é'); + }); + it('rejects duplicate decoded keys, including differently escaped spellings', () => { expect(() => parseJsonStrict('{"a":1,"a":2}')).toThrow(/Duplicate object key/); expect(() => parseJsonStrict(String.raw`{"a":1,"\u0061":2}`)).toThrow( @@ -131,6 +137,13 @@ describe('RFC 8785 canonical JSON', () => { it('enforces byte and nesting ceilings even for empty nested containers', () => { expect(() => parseJsonStrict('{"a":1}', { maxBytes: 6 })).toThrow(/exceeds 6 bytes/); + class MisleadingJsonBytes extends Uint8Array { + override get byteLength(): number { return 1; } + } + expect(() => parseJsonStrict( + new MisleadingJsonBytes(new TextEncoder().encode('{"a":1}')), + { maxBytes: 1 }, + )).toThrow(/exceeds 1 byte/); expect(parseJsonStrict('{"a":1}', { maxDepth: 1 })).toEqual({ a: 1 }); expect(() => parseJsonStrict('{"a":{}}', { maxDepth: 1 })).toThrow( /nesting exceeds 1/, diff --git a/packages/core/test/fixtures/system-record-v1/generate.mjs b/packages/core/test/fixtures/system-record-v1/generate.mjs new file mode 100644 index 0000000000..f6cb730e30 --- /dev/null +++ b/packages/core/test/fixtures/system-record-v1/generate.mjs @@ -0,0 +1,252 @@ +/** Independent system-record V1 golden-vector generator. Do not import production codecs. */ +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'; +import { peerIdFromPublicKey } from '@libp2p/peer-id'; +import { signAsync as ed25519Sign } from '@noble/ed25519'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { sha256 } from '@noble/hashes/sha2.js'; +import { keccak_256 } from '@noble/hashes/sha3.js'; + +const UTF8 = new TextEncoder(); +const HEX = (byte) => `0x${byte.repeat(32)}`; +const DOMAINS = { + head: 'dkg-system-record-agent-profile-head-object-v1\n', + transition: 'dkg-system-record-authority-transition-object-v1\n', + fork: 'dkg-system-record-fork-resolution-object-v1\n', + table: 'dkg-system-record-owned-subject-table-v1\n', + envelope: 'dkg-system-record-signed-envelope-v1\n', + provider: 'dkg-system-record-provider-signature-v1\n', + peer: 'dkg-system-record-peer-signature-v1\n', + evm: 'dkg-system-record-evm-signature-v1\n', + root: 'dkg-system-record-root-descriptor-object-v1\n', + rootCollision: 'dkg-system-record-root-collision-evidence-v1\n', +}; +const peerSeed = Uint8Array.from({ length: 32 }, (_, index) => index + 1); +const peerKey = await generateKeyPairFromSeed('Ed25519', peerSeed); +const peerId = peerIdFromPublicKey(peerKey.publicKey).toString(); +const peerPublicKey = Buffer.from(peerKey.publicKey.raw).toString('base64url'); +const evmPrivateKey = Uint8Array.from({ length: 32 }, (_, index) => index + 33); +const evmPublicKey = secp256k1.getPublicKey(evmPrivateKey, false); +const evmIssuer = `0x${Buffer.from(keccak_256(evmPublicKey.subarray(1)).subarray(12)).toString('hex')}`; +const nextEvmPrivateKey = Uint8Array.from({ length: 32 }, (_, index) => 96 - index); +const nextEvmPublicKey = secp256k1.getPublicKey(nextEvmPrivateKey, false); +const nextIssuer = `0x${Buffer.from(keccak_256(nextEvmPublicKey.subarray(1)).subarray(12)).toString('hex')}`; +const rootSubject = `did:dkg:agent:${evmIssuer}`; +const emptyTableDigest = digest(DOMAINS.table, []); +const table = [rootSubject]; +const tableDigest = digest(DOMAINS.table, table); +const seal = { + assertionMerkleRoot: HEX('aa'), authorAddress: evmIssuer, + authorAttestationR: HEX('11'), authorAttestationVS: HEX('22'), + authorSchemeVersion: '1', assertedAtChainId: '20430', + assertedAtKav10Address: '0x4444444444444444444444444444444444444444', + reservedKaId: ((BigInt(evmIssuer) << 96n) | 7n).toString(), + assertionFinalizedAt: '2026-08-05T11:59:59.000Z', contentScopeVersion: '2', + kaUal: `did:dkg:otp:20430/${evmIssuer}/7`, assertionVersion: '1', + publicTripleCount: '3', privateTripleCount: '0', privateMerkleRoot: null, +}; +const active = { + objectType: 'agent-profile-head', kind: 'agents', state: 'active', + networkId: 'otp:20430', peerId, peerPublicKey, authoritySequence: '0', version: '0', + evmIssuer, rootSubject, projectionSchemaDigest: HEX('cc'), issuedAt: '2026-08-05T12:00:00Z', + ownedSubjectTableDigest: tableDigest, ownedSubjectCount: '1', projectionBytes: '256', + projectionQuads: '3', validUntil: '2026-08-06T12:00:00Z', + assertionCoordinate: 'agent-profile-v1', graphScopedAuthorSeal: seal, + contentDigest: HEX('aa'), bundleDigest: HEX('bb'), +}; +const activeDigest = digest(DOMAINS.head, active); +const tombstone = { + objectType: 'agent-profile-head', kind: 'agents', state: 'tombstone', + networkId: 'otp:20430', peerId, peerPublicKey, authoritySequence: '0', version: '1', + previousHeadDigest: activeDigest, evmIssuer, rootSubject, + projectionSchemaDigest: HEX('cc'), issuedAt: '2026-08-05T12:10:00Z', + ownedSubjectTableDigest: emptyTableDigest, ownedSubjectCount: '0', projectionBytes: '0', + projectionQuads: '0', +}; +const transitionCommon = { + objectType: 'authority-transition', kind: 'agents', networkId: 'otp:20430', + peerId, peerPublicKey, priorAuthoritySequence: '0', nextAuthoritySequence: '1', + priorHeadDigest: activeDigest, priorEvmIssuer: evmIssuer, nextEvmIssuer: nextIssuer, + nextRoot: `did:dkg:agent:${nextIssuer}`, issuedAt: '2026-08-07T12:00:00Z', +}; +const coSignedTransition = { ...transitionCommon, mode: 'co-signed' }; +const expiredTransition = { + ...transitionCommon, mode: 'expired-prior', priorValidUntil: active.validUntil, +}; +const forkV0 = { + objectType: 'fork-resolution', kind: 'agents', networkId: 'otp:20430', peerId, + peerPublicKey, evmIssuer, authoritySequence: '0', forkedVersion: '0', resolutionVersion: '2', + evidenceHeadDigests: [HEX('aa'), HEX('bb')], issuedAt: '2026-08-05T12:05:00Z', +}; +const forkV1 = { + ...forkV0, forkedVersion: '1', resolutionVersion: '3', forkBaseHeadDigest: activeDigest, +}; +const signed = Object.fromEntries(await Promise.all([ + signedVector('activeEip191', active, DOMAINS.head, ['peer', 'current-evm']), + signedVector('tombstoneEip191', tombstone, DOMAINS.head, ['peer', 'current-evm']), + signedVector('coSignedTransitionEip191', coSignedTransition, DOMAINS.transition, + ['peer', 'prior-evm', 'next-evm']), + signedVector('expiredTransitionEip191', expiredTransition, DOMAINS.transition, + ['peer', 'next-evm']), + signedVector('forkV0Eip191', forkV0, DOMAINS.fork, ['peer', 'current-evm']), + signedVector('forkV1Eip191', forkV1, DOMAINS.fork, ['peer', 'current-evm']), + signedVector('activeEip1271', active, DOMAINS.head, ['peer', 'current-evm'], + new Set(['current-evm'])), + signedVector('coSignedTransitionEip1271', coSignedTransition, DOMAINS.transition, + ['peer', 'prior-evm', 'next-evm'], new Set(['prior-evm', 'next-evm'])), + signedVector('forkV1Eip1271', forkV1, DOMAINS.fork, ['peer', 'current-evm'], + new Set(['current-evm'])), +])); +const rootDescriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: 'otp:20430', epoch: '0', + version: '0', treeRootDigest: HEX('dd'), totalRows: '1', +}; +const rootDigest = digest(DOMAINS.root, rootDescriptor); +const providerMessage = signatureMessage(DOMAINS.provider, [ + 'agents', rootDescriptor.networkId, peerId, rootDigest, +]); +const rootCollisionInput = { + networkId: active.networkId, + root: rootSubject, + incumbentRecordKey: [active.networkId, peerId], + contenderStableKey: HEX('dd'), + contenderHeadDigest: HEX('ee'), +}; +const rootCollisionTuple = [ + rootCollisionInput.networkId, + rootCollisionInput.root, + rootCollisionInput.incumbentRecordKey, + rootCollisionInput.contenderStableKey, + rootCollisionInput.contenderHeadDigest, +]; + +const vectors = { + identities: { peerId, peerPublicKey, evmIssuer, rootSubject }, + emptyTableDigest, + variants: Object.fromEntries([ + ['active', vector(DOMAINS.head, active)], + ['tombstone', vector(DOMAINS.head, tombstone)], + ['coSignedTransition', vector(DOMAINS.transition, coSignedTransition)], + ['expiredTransition', vector(DOMAINS.transition, expiredTransition)], + ['forkV0', vector(DOMAINS.fork, forkV0)], + ['forkV1', vector(DOMAINS.fork, forkV1)], + ]), + signed, + provider: { + rootDescriptor, rootDigest, + messageHex: Buffer.from(providerMessage).toString('hex'), + signature: Buffer.from(await ed25519Sign(providerMessage, peerSeed)).toString('base64url'), + }, + rootCollision: { + input: rootCollisionInput, + canonical: canonical(rootCollisionTuple), + digest: digest(DOMAINS.rootCollision, rootCollisionTuple), + }, +}; + +const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), 'vectors.json'); +const rendered = `${JSON.stringify(vectors, null, 2)}\n`; +if (process.argv.includes('--check')) { + if (readFileSync(fixturePath, 'utf8') !== rendered) { + throw new Error('system-record V1 golden vectors are stale; rerun generate.mjs'); + } +} else { + writeFileSync(fixturePath, rendered); +} + +function vector(domain, object) { + return { object, canonical: canonical(object), digest: digest(domain, object) }; +} + +function canonical(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`; +} + +function digest(domain, value) { + const bytes = UTF8.encode(`${domain}${canonical(value)}`); + return `0x${Buffer.from(sha256(bytes)).toString('hex')}`; +} + +function signatureMessage(domain, tuple) { + return UTF8.encode(`${domain}${canonical(tuple)}`); +} + +async function signedVector(name, object, objectDomain, roles, eip1271Roles = new Set()) { + const objectDigest = digest(objectDomain, object); + const messages = Object.fromEntries(roles.map((role) => [ + role, + Buffer.from(roleMessage(object, objectDigest, role)).toString('hex'), + ])); + const signatures = await Promise.all(roles.map(async (role) => { + const message = Uint8Array.from(Buffer.from(messages[role], 'hex')); + if (role === 'peer') { + return { + role, suite: 'ed25519-v1', signer: peerId, evidence: { kind: 'none' }, + signature: Buffer.from(await ed25519Sign(message, peerSeed)).toString('base64url'), + }; + } + const signer = role === 'next-evm' ? nextIssuer : evmIssuer; + if (eip1271Roles.has(role)) { + return { + role, suite: 'eip1271-current-finalized-v1', signer, + evidence: { + kind: 'eip1271-current-finalized', chainId: '20430', contractAddress: signer, + finalizedBlockNumber: '123456', finalizedBlockHash: HEX('ef'), + }, + signature: '0x1234', + }; + } + return { + role, suite: 'eip191-personal-sign-digest-v1', signer, evidence: { kind: 'none' }, + signature: signEvm(message, role === 'next-evm' ? nextEvmPrivateKey : evmPrivateKey), + }; + })); + const envelope = { object, objectDigest, signatures }; + return [name, { + envelope, + canonical: canonical(envelope), + envelopeDigest: digest(DOMAINS.envelope, envelope), + messages, + }]; +} + +function roleMessage(object, objectDigest, role) { + const recordKey = [object.networkId, object.peerId]; + let tuple; + if (object.objectType === 'agent-profile-head') { + tuple = ['agent-profile-head', objectDigest, object.networkId, recordKey, + object.authoritySequence, object.version, + ...(role === 'peer' ? [] : ['current-evm', object.evmIssuer])]; + } else if (object.objectType === 'authority-transition') { + const issuer = role === 'prior-evm' ? object.priorEvmIssuer : object.nextEvmIssuer; + tuple = ['authority-transition', objectDigest, object.networkId, recordKey, + object.priorAuthoritySequence, object.nextAuthoritySequence, object.priorHeadDigest, + role, ...(role === 'peer' ? [] : [issuer])]; + } else { + tuple = ['fork-resolution', objectDigest, object.networkId, recordKey, + object.authoritySequence, object.forkedVersion, object.resolutionVersion, + role, ...(role === 'peer' ? [] : [object.evmIssuer])]; + } + return signatureMessage(role === 'peer' ? DOMAINS.peer : DOMAINS.evm, tuple); +} + +function personalHash(message) { + return keccak_256(Uint8Array.from(Buffer.concat([ + Buffer.from(`\x19Ethereum Signed Message:\n${message.byteLength}`), Buffer.from(message), + ]))); +} + +function signEvm(message, privateKey) { + const recovered = secp256k1.sign(personalHash(message), privateKey, { + format: 'recovered', prehash: false, lowS: true, + }); + const bytes = new Uint8Array(65); + bytes.set(recovered.subarray(1)); + bytes[64] = recovered[0] + 27; + return `0x${Buffer.from(bytes).toString('hex')}`; +} diff --git a/packages/core/test/fixtures/system-record-v1/vectors.json b/packages/core/test/fixtures/system-record-v1/vectors.json new file mode 100644 index 0000000000..b5c9192bbf --- /dev/null +++ b/packages/core/test/fixtures/system-record-v1/vectors.json @@ -0,0 +1,695 @@ +{ + "identities": { + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "rootSubject": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91" + }, + "emptyTableDigest": "0xa5d0d8dd0d515e86b3ac7b3f369f207292697946eddfd1c6801cb85e2fde068c", + "variants": { + "active": { + "object": { + "objectType": "agent-profile-head", + "kind": "agents", + "state": "active", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "authoritySequence": "0", + "version": "0", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "rootSubject": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "projectionSchemaDigest": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "issuedAt": "2026-08-05T12:00:00Z", + "ownedSubjectTableDigest": "0xd3b20a13150e282ad850425126571c262561854e01c0f52266b5852596084187", + "ownedSubjectCount": "1", + "projectionBytes": "256", + "projectionQuads": "3", + "validUntil": "2026-08-06T12:00:00Z", + "assertionCoordinate": "agent-profile-v1", + "graphScopedAuthorSeal": { + "assertionMerkleRoot": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "authorAddress": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authorAttestationR": "0x1111111111111111111111111111111111111111111111111111111111111111", + "authorAttestationVS": "0x2222222222222222222222222222222222222222222222222222222222222222", + "authorSchemeVersion": "1", + "assertedAtChainId": "20430", + "assertedAtKav10Address": "0x4444444444444444444444444444444444444444", + "reservedKaId": "46806809665542465519237074629721481070900809877125630156311884283657903407111", + "assertionFinalizedAt": "2026-08-05T11:59:59.000Z", + "contentScopeVersion": "2", + "kaUal": "did:dkg:otp:20430/0x677bb7270e0b03f0a2993a697654fb8ecb6dee91/7", + "assertionVersion": "1", + "publicTripleCount": "3", + "privateTripleCount": "0", + "privateMerkleRoot": null + }, + "contentDigest": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bundleDigest": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "canonical": "{\"assertionCoordinate\":\"agent-profile-v1\",\"authoritySequence\":\"0\",\"bundleDigest\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"contentDigest\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"graphScopedAuthorSeal\":{\"assertedAtChainId\":\"20430\",\"assertedAtKav10Address\":\"0x4444444444444444444444444444444444444444\",\"assertionFinalizedAt\":\"2026-08-05T11:59:59.000Z\",\"assertionMerkleRoot\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"assertionVersion\":\"1\",\"authorAddress\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"authorAttestationR\":\"0x1111111111111111111111111111111111111111111111111111111111111111\",\"authorAttestationVS\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"authorSchemeVersion\":\"1\",\"contentScopeVersion\":\"2\",\"kaUal\":\"did:dkg:otp:20430/0x677bb7270e0b03f0a2993a697654fb8ecb6dee91/7\",\"privateMerkleRoot\":null,\"privateTripleCount\":\"0\",\"publicTripleCount\":\"3\",\"reservedKaId\":\"46806809665542465519237074629721481070900809877125630156311884283657903407111\"},\"issuedAt\":\"2026-08-05T12:00:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"agent-profile-head\",\"ownedSubjectCount\":\"1\",\"ownedSubjectTableDigest\":\"0xd3b20a13150e282ad850425126571c262561854e01c0f52266b5852596084187\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"projectionBytes\":\"256\",\"projectionQuads\":\"3\",\"projectionSchemaDigest\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"rootSubject\":\"did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"state\":\"active\",\"validUntil\":\"2026-08-06T12:00:00Z\",\"version\":\"0\"}", + "digest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8" + }, + "tombstone": { + "object": { + "objectType": "agent-profile-head", + "kind": "agents", + "state": "tombstone", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "authoritySequence": "0", + "version": "1", + "previousHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "rootSubject": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "projectionSchemaDigest": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "issuedAt": "2026-08-05T12:10:00Z", + "ownedSubjectTableDigest": "0xa5d0d8dd0d515e86b3ac7b3f369f207292697946eddfd1c6801cb85e2fde068c", + "ownedSubjectCount": "0", + "projectionBytes": "0", + "projectionQuads": "0" + }, + "canonical": "{\"authoritySequence\":\"0\",\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"issuedAt\":\"2026-08-05T12:10:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"agent-profile-head\",\"ownedSubjectCount\":\"0\",\"ownedSubjectTableDigest\":\"0xa5d0d8dd0d515e86b3ac7b3f369f207292697946eddfd1c6801cb85e2fde068c\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"previousHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"projectionBytes\":\"0\",\"projectionQuads\":\"0\",\"projectionSchemaDigest\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"rootSubject\":\"did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"state\":\"tombstone\",\"version\":\"1\"}", + "digest": "0x22671ae962d0292c4e1d6aca64d6a4d5255fc1cefd40ba9fcd1cbdcd83f49068" + }, + "coSignedTransition": { + "object": { + "objectType": "authority-transition", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "priorAuthoritySequence": "0", + "nextAuthoritySequence": "1", + "priorHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "priorEvmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "nextEvmIssuer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "nextRoot": "did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "issuedAt": "2026-08-07T12:00:00Z", + "mode": "co-signed" + }, + "canonical": "{\"issuedAt\":\"2026-08-07T12:00:00Z\",\"kind\":\"agents\",\"mode\":\"co-signed\",\"networkId\":\"otp:20430\",\"nextAuthoritySequence\":\"1\",\"nextEvmIssuer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"nextRoot\":\"did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"objectType\":\"authority-transition\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"priorAuthoritySequence\":\"0\",\"priorEvmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"priorHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\"}", + "digest": "0xb3b7feb0a81c8dd30035d2a245c457be0866bc9b03a643f84efc8cc362749aa2" + }, + "expiredTransition": { + "object": { + "objectType": "authority-transition", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "priorAuthoritySequence": "0", + "nextAuthoritySequence": "1", + "priorHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "priorEvmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "nextEvmIssuer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "nextRoot": "did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "issuedAt": "2026-08-07T12:00:00Z", + "mode": "expired-prior", + "priorValidUntil": "2026-08-06T12:00:00Z" + }, + "canonical": "{\"issuedAt\":\"2026-08-07T12:00:00Z\",\"kind\":\"agents\",\"mode\":\"expired-prior\",\"networkId\":\"otp:20430\",\"nextAuthoritySequence\":\"1\",\"nextEvmIssuer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"nextRoot\":\"did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"objectType\":\"authority-transition\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"priorAuthoritySequence\":\"0\",\"priorEvmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"priorHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"priorValidUntil\":\"2026-08-06T12:00:00Z\"}", + "digest": "0x6e4ca59473ad065b9beb8e8ce3c5d4169a66134902d13f6f3f963795edd8fda3" + }, + "forkV0": { + "object": { + "objectType": "fork-resolution", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authoritySequence": "0", + "forkedVersion": "0", + "resolutionVersion": "2", + "evidenceHeadDigests": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "issuedAt": "2026-08-05T12:05:00Z" + }, + "canonical": "{\"authoritySequence\":\"0\",\"evidenceHeadDigests\":[\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"forkedVersion\":\"0\",\"issuedAt\":\"2026-08-05T12:05:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"fork-resolution\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"resolutionVersion\":\"2\"}", + "digest": "0xf61937886fa5dd508fa58dca57a7e21106c9b69322d1f3ef8f8e2e36bddd680b" + }, + "forkV1": { + "object": { + "objectType": "fork-resolution", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authoritySequence": "0", + "forkedVersion": "1", + "resolutionVersion": "3", + "evidenceHeadDigests": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "issuedAt": "2026-08-05T12:05:00Z", + "forkBaseHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8" + }, + "canonical": "{\"authoritySequence\":\"0\",\"evidenceHeadDigests\":[\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"forkBaseHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"forkedVersion\":\"1\",\"issuedAt\":\"2026-08-05T12:05:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"fork-resolution\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"resolutionVersion\":\"3\"}", + "digest": "0x2ed1a4de1b37e7014ea11b8286abe5fe3b6af0df7f32767f8ff5760ba17b297b" + } + }, + "signed": { + "activeEip191": { + "envelope": { + "object": { + "objectType": "agent-profile-head", + "kind": "agents", + "state": "active", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "authoritySequence": "0", + "version": "0", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "rootSubject": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "projectionSchemaDigest": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "issuedAt": "2026-08-05T12:00:00Z", + "ownedSubjectTableDigest": "0xd3b20a13150e282ad850425126571c262561854e01c0f52266b5852596084187", + "ownedSubjectCount": "1", + "projectionBytes": "256", + "projectionQuads": "3", + "validUntil": "2026-08-06T12:00:00Z", + "assertionCoordinate": "agent-profile-v1", + "graphScopedAuthorSeal": { + "assertionMerkleRoot": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "authorAddress": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authorAttestationR": "0x1111111111111111111111111111111111111111111111111111111111111111", + "authorAttestationVS": "0x2222222222222222222222222222222222222222222222222222222222222222", + "authorSchemeVersion": "1", + "assertedAtChainId": "20430", + "assertedAtKav10Address": "0x4444444444444444444444444444444444444444", + "reservedKaId": "46806809665542465519237074629721481070900809877125630156311884283657903407111", + "assertionFinalizedAt": "2026-08-05T11:59:59.000Z", + "contentScopeVersion": "2", + "kaUal": "did:dkg:otp:20430/0x677bb7270e0b03f0a2993a697654fb8ecb6dee91/7", + "assertionVersion": "1", + "publicTripleCount": "3", + "privateTripleCount": "0", + "privateMerkleRoot": null + }, + "contentDigest": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bundleDigest": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "objectDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "zP8KK4K3R2owxR45Vd5JnNkP9Pm8GB79eZQkud9aJ3JajOiRwfBqnHo9vgxXA6R_dyd1vaXI8ZUjWRBk2pAuCw" + }, + { + "role": "current-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "none" + }, + "signature": "0xb2db36ad50653c5d5d6d378032c630cb02240af76335a2214ae575d5c6928d0a4cb399f5fc2dca9738e36b012dbc6faee95b78887e07927d624f4deebd6dacd31c" + } + ] + }, + "canonical": "{\"object\":{\"assertionCoordinate\":\"agent-profile-v1\",\"authoritySequence\":\"0\",\"bundleDigest\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"contentDigest\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"graphScopedAuthorSeal\":{\"assertedAtChainId\":\"20430\",\"assertedAtKav10Address\":\"0x4444444444444444444444444444444444444444\",\"assertionFinalizedAt\":\"2026-08-05T11:59:59.000Z\",\"assertionMerkleRoot\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"assertionVersion\":\"1\",\"authorAddress\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"authorAttestationR\":\"0x1111111111111111111111111111111111111111111111111111111111111111\",\"authorAttestationVS\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"authorSchemeVersion\":\"1\",\"contentScopeVersion\":\"2\",\"kaUal\":\"did:dkg:otp:20430/0x677bb7270e0b03f0a2993a697654fb8ecb6dee91/7\",\"privateMerkleRoot\":null,\"privateTripleCount\":\"0\",\"publicTripleCount\":\"3\",\"reservedKaId\":\"46806809665542465519237074629721481070900809877125630156311884283657903407111\"},\"issuedAt\":\"2026-08-05T12:00:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"agent-profile-head\",\"ownedSubjectCount\":\"1\",\"ownedSubjectTableDigest\":\"0xd3b20a13150e282ad850425126571c262561854e01c0f52266b5852596084187\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"projectionBytes\":\"256\",\"projectionQuads\":\"3\",\"projectionSchemaDigest\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"rootSubject\":\"did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"state\":\"active\",\"validUntil\":\"2026-08-06T12:00:00Z\",\"version\":\"0\"},\"objectDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"zP8KK4K3R2owxR45Vd5JnNkP9Pm8GB79eZQkud9aJ3JajOiRwfBqnHo9vgxXA6R_dyd1vaXI8ZUjWRBk2pAuCw\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"current-evm\",\"signature\":\"0xb2db36ad50653c5d5d6d378032c630cb02240af76335a2214ae575d5c6928d0a4cb399f5fc2dca9738e36b012dbc6faee95b78887e07927d624f4deebd6dacd31c\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip191-personal-sign-digest-v1\"}]}", + "envelopeDigest": "0x9aa1b1db771a34b58a0de412214354d8c75153b36c7a6dcd5284ec3006bf6e7d", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b226167656e742d70726f66696c652d68656164222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2230225d", + "current-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b226167656e742d70726f66696c652d68656164222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2230222c2263757272656e742d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d" + } + }, + "tombstoneEip191": { + "envelope": { + "object": { + "objectType": "agent-profile-head", + "kind": "agents", + "state": "tombstone", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "authoritySequence": "0", + "version": "1", + "previousHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "rootSubject": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "projectionSchemaDigest": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "issuedAt": "2026-08-05T12:10:00Z", + "ownedSubjectTableDigest": "0xa5d0d8dd0d515e86b3ac7b3f369f207292697946eddfd1c6801cb85e2fde068c", + "ownedSubjectCount": "0", + "projectionBytes": "0", + "projectionQuads": "0" + }, + "objectDigest": "0x22671ae962d0292c4e1d6aca64d6a4d5255fc1cefd40ba9fcd1cbdcd83f49068", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "LLv6lWof-1A-4STNlp-6J40k0itYC5Cwg0LrxNYhhxOKVIV20z19GVsD2WndW4rK-RdDcWHcdjNpIaiMHASPDg" + }, + { + "role": "current-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "none" + }, + "signature": "0x01673df5f422affa9545b8a1188e371703ced1c605180d062a3a1474a1ed7d2b78c4c2b806bd296cae4b79e6114efe1731caca32d3d76e5487c85d3281c86da21b" + } + ] + }, + "canonical": "{\"object\":{\"authoritySequence\":\"0\",\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"issuedAt\":\"2026-08-05T12:10:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"agent-profile-head\",\"ownedSubjectCount\":\"0\",\"ownedSubjectTableDigest\":\"0xa5d0d8dd0d515e86b3ac7b3f369f207292697946eddfd1c6801cb85e2fde068c\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"previousHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"projectionBytes\":\"0\",\"projectionQuads\":\"0\",\"projectionSchemaDigest\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"rootSubject\":\"did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"state\":\"tombstone\",\"version\":\"1\"},\"objectDigest\":\"0x22671ae962d0292c4e1d6aca64d6a4d5255fc1cefd40ba9fcd1cbdcd83f49068\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"LLv6lWof-1A-4STNlp-6J40k0itYC5Cwg0LrxNYhhxOKVIV20z19GVsD2WndW4rK-RdDcWHcdjNpIaiMHASPDg\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"current-evm\",\"signature\":\"0x01673df5f422affa9545b8a1188e371703ced1c605180d062a3a1474a1ed7d2b78c4c2b806bd296cae4b79e6114efe1731caca32d3d76e5487c85d3281c86da21b\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip191-personal-sign-digest-v1\"}]}", + "envelopeDigest": "0x680b0f9f1c96b4d5e36a436d63966b31a233e71ed5d8f1ed157c6b3c28fa23b9", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b226167656e742d70726f66696c652d68656164222c22307832323637316165393632643032393263346531643661636136346436613464353235356663316365666434306261396663643163626463643833663439303638222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231225d", + "current-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b226167656e742d70726f66696c652d68656164222c22307832323637316165393632643032393263346531643661636136346436613464353235356663316365666434306261396663643163626463643833663439303638222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c2263757272656e742d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d" + } + }, + "coSignedTransitionEip191": { + "envelope": { + "object": { + "objectType": "authority-transition", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "priorAuthoritySequence": "0", + "nextAuthoritySequence": "1", + "priorHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "priorEvmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "nextEvmIssuer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "nextRoot": "did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "issuedAt": "2026-08-07T12:00:00Z", + "mode": "co-signed" + }, + "objectDigest": "0xb3b7feb0a81c8dd30035d2a245c457be0866bc9b03a643f84efc8cc362749aa2", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "TkX2HPrtyFM_tuIKQ62gOcZ-S2vAtpfLAJwTOrbFIkIs6_coMvukB7veJ9FQUlU4piJ_qYUGYGwEch9422vlAA" + }, + { + "role": "prior-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "none" + }, + "signature": "0xc5b16013b5075b219a85fbb733700333ddcd93c9f1a7f80dfc62570cc823d3b87246447822dfb9b6f21876d46faca9909d812e143fbf05a1c49cb876e9d8f5981b" + }, + { + "role": "next-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "evidence": { + "kind": "none" + }, + "signature": "0x3c1389025b614c80fa54d773be022dfc31e139e85a1e2c94eaeb3fb7c542f726778166d79e78aa37269213f760fc0c7f11ead1c20f42145240a342bd46f213681b" + } + ] + }, + "canonical": "{\"object\":{\"issuedAt\":\"2026-08-07T12:00:00Z\",\"kind\":\"agents\",\"mode\":\"co-signed\",\"networkId\":\"otp:20430\",\"nextAuthoritySequence\":\"1\",\"nextEvmIssuer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"nextRoot\":\"did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"objectType\":\"authority-transition\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"priorAuthoritySequence\":\"0\",\"priorEvmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"priorHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\"},\"objectDigest\":\"0xb3b7feb0a81c8dd30035d2a245c457be0866bc9b03a643f84efc8cc362749aa2\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"TkX2HPrtyFM_tuIKQ62gOcZ-S2vAtpfLAJwTOrbFIkIs6_coMvukB7veJ9FQUlU4piJ_qYUGYGwEch9422vlAA\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"prior-evm\",\"signature\":\"0xc5b16013b5075b219a85fbb733700333ddcd93c9f1a7f80dfc62570cc823d3b87246447822dfb9b6f21876d46faca9909d812e143fbf05a1c49cb876e9d8f5981b\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip191-personal-sign-digest-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"next-evm\",\"signature\":\"0x3c1389025b614c80fa54d773be022dfc31e139e85a1e2c94eaeb3fb7c542f726778166d79e78aa37269213f760fc0c7f11ead1c20f42145240a342bd46f213681b\",\"signer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"suite\":\"eip191-personal-sign-digest-v1\"}]}", + "envelopeDigest": "0xd5f26fd6b6960f9f36c152a2fbde5cd67735e20458716d73cb2e469c53f92b64", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307862336237666562306138316338646433303033356432613234356334353762653038363662633962303361363433663834656663386363333632373439616132222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c2270656572225d", + "prior-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307862336237666562306138316338646433303033356432613234356334353762653038363662633962303361363433663834656663386363333632373439616132222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c227072696f722d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d", + "next-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307862336237666562306138316338646433303033356432613234356334353762653038363662633962303361363433663834656663386363333632373439616132222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226e6578742d65766d222c22307837336134636465333031376366386436343261653665363337663533323161653566343363393835225d" + } + }, + "expiredTransitionEip191": { + "envelope": { + "object": { + "objectType": "authority-transition", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "priorAuthoritySequence": "0", + "nextAuthoritySequence": "1", + "priorHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "priorEvmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "nextEvmIssuer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "nextRoot": "did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "issuedAt": "2026-08-07T12:00:00Z", + "mode": "expired-prior", + "priorValidUntil": "2026-08-06T12:00:00Z" + }, + "objectDigest": "0x6e4ca59473ad065b9beb8e8ce3c5d4169a66134902d13f6f3f963795edd8fda3", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "aRrTXMjLTWbEc3r_p7fGrMo3bSl-WDJDctK2PXlTzORemjq1TZrM-8Iy2CLUqjizg3KfSAGdSHxt_5fgx-YQBQ" + }, + { + "role": "next-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "evidence": { + "kind": "none" + }, + "signature": "0x665ce0b05b23cdd719d0f76bd41e1a03f7d0f831b63837d16605374cc92b00ee69b0da17b63eb77e2fd496381748d83efee06ec0373e436df8fd4b0f18e84a5a1b" + } + ] + }, + "canonical": "{\"object\":{\"issuedAt\":\"2026-08-07T12:00:00Z\",\"kind\":\"agents\",\"mode\":\"expired-prior\",\"networkId\":\"otp:20430\",\"nextAuthoritySequence\":\"1\",\"nextEvmIssuer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"nextRoot\":\"did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"objectType\":\"authority-transition\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"priorAuthoritySequence\":\"0\",\"priorEvmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"priorHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"priorValidUntil\":\"2026-08-06T12:00:00Z\"},\"objectDigest\":\"0x6e4ca59473ad065b9beb8e8ce3c5d4169a66134902d13f6f3f963795edd8fda3\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"aRrTXMjLTWbEc3r_p7fGrMo3bSl-WDJDctK2PXlTzORemjq1TZrM-8Iy2CLUqjizg3KfSAGdSHxt_5fgx-YQBQ\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"next-evm\",\"signature\":\"0x665ce0b05b23cdd719d0f76bd41e1a03f7d0f831b63837d16605374cc92b00ee69b0da17b63eb77e2fd496381748d83efee06ec0373e436df8fd4b0f18e84a5a1b\",\"signer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"suite\":\"eip191-personal-sign-digest-v1\"}]}", + "envelopeDigest": "0x0233cfb213ecb78d099456e7f9974a98635202e8b3271bf14af2bf2b3f03aa0f", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307836653463613539343733616430363562396265623865386365336335643431363961363631333439303264313366366633663936333739356564643866646133222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c2270656572225d", + "next-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307836653463613539343733616430363562396265623865386365336335643431363961363631333439303264313366366633663936333739356564643866646133222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226e6578742d65766d222c22307837336134636465333031376366386436343261653665363337663533323161653566343363393835225d" + } + }, + "forkV0Eip191": { + "envelope": { + "object": { + "objectType": "fork-resolution", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authoritySequence": "0", + "forkedVersion": "0", + "resolutionVersion": "2", + "evidenceHeadDigests": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "issuedAt": "2026-08-05T12:05:00Z" + }, + "objectDigest": "0xf61937886fa5dd508fa58dca57a7e21106c9b69322d1f3ef8f8e2e36bddd680b", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "rcG2pKkNngKVjuxX4npNKAAwPciGejqQ3betEkE2Ye_C1qbdQEKuaBvenWU-5yD4orEXLNq6v04riTHjGui4BA" + }, + { + "role": "current-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "none" + }, + "signature": "0x8dd43cf4910486ad962326f7fbdf3af3f89091692ed3db2b6e1f07f9511882d7086b46f9cb92a6ed53cb6af79cc80478296f819d172fbcc08278df3a637cbea01c" + } + ] + }, + "canonical": "{\"object\":{\"authoritySequence\":\"0\",\"evidenceHeadDigests\":[\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"forkedVersion\":\"0\",\"issuedAt\":\"2026-08-05T12:05:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"fork-resolution\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"resolutionVersion\":\"2\"},\"objectDigest\":\"0xf61937886fa5dd508fa58dca57a7e21106c9b69322d1f3ef8f8e2e36bddd680b\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"rcG2pKkNngKVjuxX4npNKAAwPciGejqQ3betEkE2Ye_C1qbdQEKuaBvenWU-5yD4orEXLNq6v04riTHjGui4BA\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"current-evm\",\"signature\":\"0x8dd43cf4910486ad962326f7fbdf3af3f89091692ed3db2b6e1f07f9511882d7086b46f9cb92a6ed53cb6af79cc80478296f819d172fbcc08278df3a637cbea01c\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip191-personal-sign-digest-v1\"}]}", + "envelopeDigest": "0x3d42b22cdcc649c4e222165a920c7ba04b6078699159ae55cc4b6a25b87dd207", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b22666f726b2d7265736f6c7574696f6e222c22307866363139333738383666613564643530386661353864636135376137653231313036633962363933323264316633656638663865326533366264646436383062222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2230222c2232222c2270656572225d", + "current-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22666f726b2d7265736f6c7574696f6e222c22307866363139333738383666613564643530386661353864636135376137653231313036633962363933323264316633656638663865326533366264646436383062222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2230222c2232222c2263757272656e742d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d" + } + }, + "forkV1Eip191": { + "envelope": { + "object": { + "objectType": "fork-resolution", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authoritySequence": "0", + "forkedVersion": "1", + "resolutionVersion": "3", + "evidenceHeadDigests": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "issuedAt": "2026-08-05T12:05:00Z", + "forkBaseHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8" + }, + "objectDigest": "0x2ed1a4de1b37e7014ea11b8286abe5fe3b6af0df7f32767f8ff5760ba17b297b", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "AS63-V73oUOo14LujC6PCN0ftk7OwaUAMBBWQuOJFfQ4TOIjhJNOmg8nfIjkkxF2mJwAU_sSfSH46hWw1ADHCQ" + }, + { + "role": "current-evm", + "suite": "eip191-personal-sign-digest-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "none" + }, + "signature": "0x1b03f31a0ed48acc8b03c990643c9444b8d1d1bed47c4b5d57489a17985e3c605f2c3d32cc15bd4bc554ea896a678ed6bddea6ac5594399e8b85700b4757ba981b" + } + ] + }, + "canonical": "{\"object\":{\"authoritySequence\":\"0\",\"evidenceHeadDigests\":[\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"forkBaseHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"forkedVersion\":\"1\",\"issuedAt\":\"2026-08-05T12:05:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"fork-resolution\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"resolutionVersion\":\"3\"},\"objectDigest\":\"0x2ed1a4de1b37e7014ea11b8286abe5fe3b6af0df7f32767f8ff5760ba17b297b\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"AS63-V73oUOo14LujC6PCN0ftk7OwaUAMBBWQuOJFfQ4TOIjhJNOmg8nfIjkkxF2mJwAU_sSfSH46hWw1ADHCQ\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"kind\":\"none\"},\"role\":\"current-evm\",\"signature\":\"0x1b03f31a0ed48acc8b03c990643c9444b8d1d1bed47c4b5d57489a17985e3c605f2c3d32cc15bd4bc554ea896a678ed6bddea6ac5594399e8b85700b4757ba981b\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip191-personal-sign-digest-v1\"}]}", + "envelopeDigest": "0x0768fb1361d95eead964342da43f73dd5a8d94c28d40662bc9eab62a44d1b509", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b22666f726b2d7265736f6c7574696f6e222c22307832656431613464653162333765373031346561313162383238366162653566653362366166306466376633323736376638666635373630626131376232393762222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c2233222c2270656572225d", + "current-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22666f726b2d7265736f6c7574696f6e222c22307832656431613464653162333765373031346561313162383238366162653566653362366166306466376633323736376638666635373630626131376232393762222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c2233222c2263757272656e742d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d" + } + }, + "activeEip1271": { + "envelope": { + "object": { + "objectType": "agent-profile-head", + "kind": "agents", + "state": "active", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "authoritySequence": "0", + "version": "0", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "rootSubject": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "projectionSchemaDigest": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "issuedAt": "2026-08-05T12:00:00Z", + "ownedSubjectTableDigest": "0xd3b20a13150e282ad850425126571c262561854e01c0f52266b5852596084187", + "ownedSubjectCount": "1", + "projectionBytes": "256", + "projectionQuads": "3", + "validUntil": "2026-08-06T12:00:00Z", + "assertionCoordinate": "agent-profile-v1", + "graphScopedAuthorSeal": { + "assertionMerkleRoot": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "authorAddress": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authorAttestationR": "0x1111111111111111111111111111111111111111111111111111111111111111", + "authorAttestationVS": "0x2222222222222222222222222222222222222222222222222222222222222222", + "authorSchemeVersion": "1", + "assertedAtChainId": "20430", + "assertedAtKav10Address": "0x4444444444444444444444444444444444444444", + "reservedKaId": "46806809665542465519237074629721481070900809877125630156311884283657903407111", + "assertionFinalizedAt": "2026-08-05T11:59:59.000Z", + "contentScopeVersion": "2", + "kaUal": "did:dkg:otp:20430/0x677bb7270e0b03f0a2993a697654fb8ecb6dee91/7", + "assertionVersion": "1", + "publicTripleCount": "3", + "privateTripleCount": "0", + "privateMerkleRoot": null + }, + "contentDigest": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bundleDigest": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "objectDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "zP8KK4K3R2owxR45Vd5JnNkP9Pm8GB79eZQkud9aJ3JajOiRwfBqnHo9vgxXA6R_dyd1vaXI8ZUjWRBk2pAuCw" + }, + { + "role": "current-evm", + "suite": "eip1271-current-finalized-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "eip1271-current-finalized", + "chainId": "20430", + "contractAddress": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "finalizedBlockNumber": "123456", + "finalizedBlockHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef" + }, + "signature": "0x1234" + } + ] + }, + "canonical": "{\"object\":{\"assertionCoordinate\":\"agent-profile-v1\",\"authoritySequence\":\"0\",\"bundleDigest\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"contentDigest\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"graphScopedAuthorSeal\":{\"assertedAtChainId\":\"20430\",\"assertedAtKav10Address\":\"0x4444444444444444444444444444444444444444\",\"assertionFinalizedAt\":\"2026-08-05T11:59:59.000Z\",\"assertionMerkleRoot\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"assertionVersion\":\"1\",\"authorAddress\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"authorAttestationR\":\"0x1111111111111111111111111111111111111111111111111111111111111111\",\"authorAttestationVS\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"authorSchemeVersion\":\"1\",\"contentScopeVersion\":\"2\",\"kaUal\":\"did:dkg:otp:20430/0x677bb7270e0b03f0a2993a697654fb8ecb6dee91/7\",\"privateMerkleRoot\":null,\"privateTripleCount\":\"0\",\"publicTripleCount\":\"3\",\"reservedKaId\":\"46806809665542465519237074629721481070900809877125630156311884283657903407111\"},\"issuedAt\":\"2026-08-05T12:00:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"agent-profile-head\",\"ownedSubjectCount\":\"1\",\"ownedSubjectTableDigest\":\"0xd3b20a13150e282ad850425126571c262561854e01c0f52266b5852596084187\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"projectionBytes\":\"256\",\"projectionQuads\":\"3\",\"projectionSchemaDigest\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"rootSubject\":\"did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"state\":\"active\",\"validUntil\":\"2026-08-06T12:00:00Z\",\"version\":\"0\"},\"objectDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"zP8KK4K3R2owxR45Vd5JnNkP9Pm8GB79eZQkud9aJ3JajOiRwfBqnHo9vgxXA6R_dyd1vaXI8ZUjWRBk2pAuCw\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"chainId\":\"20430\",\"contractAddress\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"finalizedBlockHash\":\"0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef\",\"finalizedBlockNumber\":\"123456\",\"kind\":\"eip1271-current-finalized\"},\"role\":\"current-evm\",\"signature\":\"0x1234\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip1271-current-finalized-v1\"}]}", + "envelopeDigest": "0x8cdd2172841f71ae26813b77928b617ecd9aeb7236eda47c0b69265743101bbe", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b226167656e742d70726f66696c652d68656164222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2230225d", + "current-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b226167656e742d70726f66696c652d68656164222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2230222c2263757272656e742d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d" + } + }, + "coSignedTransitionEip1271": { + "envelope": { + "object": { + "objectType": "authority-transition", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "priorAuthoritySequence": "0", + "nextAuthoritySequence": "1", + "priorHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8", + "priorEvmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "nextEvmIssuer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "nextRoot": "did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "issuedAt": "2026-08-07T12:00:00Z", + "mode": "co-signed" + }, + "objectDigest": "0xb3b7feb0a81c8dd30035d2a245c457be0866bc9b03a643f84efc8cc362749aa2", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "TkX2HPrtyFM_tuIKQ62gOcZ-S2vAtpfLAJwTOrbFIkIs6_coMvukB7veJ9FQUlU4piJ_qYUGYGwEch9422vlAA" + }, + { + "role": "prior-evm", + "suite": "eip1271-current-finalized-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "eip1271-current-finalized", + "chainId": "20430", + "contractAddress": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "finalizedBlockNumber": "123456", + "finalizedBlockHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef" + }, + "signature": "0x1234" + }, + { + "role": "next-evm", + "suite": "eip1271-current-finalized-v1", + "signer": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "evidence": { + "kind": "eip1271-current-finalized", + "chainId": "20430", + "contractAddress": "0x73a4cde3017cf8d642ae6e637f5321ae5f43c985", + "finalizedBlockNumber": "123456", + "finalizedBlockHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef" + }, + "signature": "0x1234" + } + ] + }, + "canonical": "{\"object\":{\"issuedAt\":\"2026-08-07T12:00:00Z\",\"kind\":\"agents\",\"mode\":\"co-signed\",\"networkId\":\"otp:20430\",\"nextAuthoritySequence\":\"1\",\"nextEvmIssuer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"nextRoot\":\"did:dkg:agent:0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"objectType\":\"authority-transition\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"priorAuthoritySequence\":\"0\",\"priorEvmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"priorHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\"},\"objectDigest\":\"0xb3b7feb0a81c8dd30035d2a245c457be0866bc9b03a643f84efc8cc362749aa2\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"TkX2HPrtyFM_tuIKQ62gOcZ-S2vAtpfLAJwTOrbFIkIs6_coMvukB7veJ9FQUlU4piJ_qYUGYGwEch9422vlAA\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"chainId\":\"20430\",\"contractAddress\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"finalizedBlockHash\":\"0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef\",\"finalizedBlockNumber\":\"123456\",\"kind\":\"eip1271-current-finalized\"},\"role\":\"prior-evm\",\"signature\":\"0x1234\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip1271-current-finalized-v1\"},{\"evidence\":{\"chainId\":\"20430\",\"contractAddress\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"finalizedBlockHash\":\"0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef\",\"finalizedBlockNumber\":\"123456\",\"kind\":\"eip1271-current-finalized\"},\"role\":\"next-evm\",\"signature\":\"0x1234\",\"signer\":\"0x73a4cde3017cf8d642ae6e637f5321ae5f43c985\",\"suite\":\"eip1271-current-finalized-v1\"}]}", + "envelopeDigest": "0x99ec4e01cb998c0f4b23dd30d6b1c2955a44cd99f3a4f21cb3297defc516283c", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307862336237666562306138316338646433303033356432613234356334353762653038363662633962303361363433663834656663386363333632373439616132222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c2270656572225d", + "prior-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307862336237666562306138316338646433303033356432613234356334353762653038363662633962303361363433663834656663386363333632373439616132222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c227072696f722d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d", + "next-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22617574686f726974792d7472616e736974696f6e222c22307862336237666562306138316338646433303033356432613234356334353762653038363662633962303361363433663834656663386363333632373439616132222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c22307835353634393334616437646139636166326565643834313131633834306665386166313462343630353131393365663632353664333535653235366233366238222c226e6578742d65766d222c22307837336134636465333031376366386436343261653665363337663533323161653566343363393835225d" + } + }, + "forkV1Eip1271": { + "envelope": { + "object": { + "objectType": "fork-resolution", + "kind": "agents", + "networkId": "otp:20430", + "peerId": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "peerPublicKey": "ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ", + "evmIssuer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "authoritySequence": "0", + "forkedVersion": "1", + "resolutionVersion": "3", + "evidenceHeadDigests": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "issuedAt": "2026-08-05T12:05:00Z", + "forkBaseHeadDigest": "0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8" + }, + "objectDigest": "0x2ed1a4de1b37e7014ea11b8286abe5fe3b6af0df7f32767f8ff5760ba17b297b", + "signatures": [ + { + "role": "peer", + "suite": "ed25519-v1", + "signer": "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf", + "evidence": { + "kind": "none" + }, + "signature": "AS63-V73oUOo14LujC6PCN0ftk7OwaUAMBBWQuOJFfQ4TOIjhJNOmg8nfIjkkxF2mJwAU_sSfSH46hWw1ADHCQ" + }, + { + "role": "current-evm", + "suite": "eip1271-current-finalized-v1", + "signer": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "evidence": { + "kind": "eip1271-current-finalized", + "chainId": "20430", + "contractAddress": "0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "finalizedBlockNumber": "123456", + "finalizedBlockHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef" + }, + "signature": "0x1234" + } + ] + }, + "canonical": "{\"object\":{\"authoritySequence\":\"0\",\"evidenceHeadDigests\":[\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"evmIssuer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"forkBaseHeadDigest\":\"0x5564934ad7da9caf2eed84111c840fe8af14b46051193ef6256d355e256b36b8\",\"forkedVersion\":\"1\",\"issuedAt\":\"2026-08-05T12:05:00Z\",\"kind\":\"agents\",\"networkId\":\"otp:20430\",\"objectType\":\"fork-resolution\",\"peerId\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"peerPublicKey\":\"ebVWLo_mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ\",\"resolutionVersion\":\"3\"},\"objectDigest\":\"0x2ed1a4de1b37e7014ea11b8286abe5fe3b6af0df7f32767f8ff5760ba17b297b\",\"signatures\":[{\"evidence\":{\"kind\":\"none\"},\"role\":\"peer\",\"signature\":\"AS63-V73oUOo14LujC6PCN0ftk7OwaUAMBBWQuOJFfQ4TOIjhJNOmg8nfIjkkxF2mJwAU_sSfSH46hWw1ADHCQ\",\"signer\":\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\",\"suite\":\"ed25519-v1\"},{\"evidence\":{\"chainId\":\"20430\",\"contractAddress\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"finalizedBlockHash\":\"0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef\",\"finalizedBlockNumber\":\"123456\",\"kind\":\"eip1271-current-finalized\"},\"role\":\"current-evm\",\"signature\":\"0x1234\",\"signer\":\"0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",\"suite\":\"eip1271-current-finalized-v1\"}]}", + "envelopeDigest": "0x013908cce58764f752e46dcd64a94c6dba26f27951b27c75f5919d00aa54f17f", + "messages": { + "peer": "646b672d73797374656d2d7265636f72642d706565722d7369676e61747572652d76310a5b22666f726b2d7265736f6c7574696f6e222c22307832656431613464653162333765373031346561313162383238366162653566653362366166306466376633323736376638666635373630626131376232393762222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c2233222c2270656572225d", + "current-evm": "646b672d73797374656d2d7265636f72642d65766d2d7369676e61747572652d76310a5b22666f726b2d7265736f6c7574696f6e222c22307832656431613464653162333765373031346561313162383238366162653566653362366166306466376633323736376638666635373630626131376232393762222c226f74703a3230343330222c5b226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966225d2c2230222c2231222c2233222c2263757272656e742d65766d222c22307836373762623732373065306230336630613239393361363937363534666238656362366465653931225d" + } + } + }, + "provider": { + "rootDescriptor": { + "objectType": "root-descriptor", + "kind": "agents", + "networkId": "otp:20430", + "epoch": "0", + "version": "0", + "treeRootDigest": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "totalRows": "1" + }, + "rootDigest": "0x9354cb9f4dcedd4166790841b9450dc51fc0edc0117165738a730d5a07757efd", + "messageHex": "646b672d73797374656d2d7265636f72642d70726f76696465722d7369676e61747572652d76310a5b226167656e7473222c226f74703a3230343330222c22313244334b6f6f574a315473696a48374835463734686641443558697368517a337378726d41745659333747744e643943715966222c22307839333534636239663464636564643431363637393038343162393435306463353166633065646330313137313635373338613733306435613037373537656664225d", + "signature": "o7Y4DQmTyi2mhfqvDC8rsf81bGecRIciiKoVq3HEqSU-2poro12G9QHSn2bD-t-vFBPbHsNA4s1TPAuS8mWGDg" + }, + "rootCollision": { + "input": { + "networkId": "otp:20430", + "root": "did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91", + "incumbentRecordKey": [ + "otp:20430", + "12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf" + ], + "contenderStableKey": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "contenderHeadDigest": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "canonical": "[\"otp:20430\",\"did:dkg:agent:0x677bb7270e0b03f0a2993a697654fb8ecb6dee91\",[\"otp:20430\",\"12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf\"],\"0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\"]", + "digest": "0xa4624fbf59b16237dc2501dec06de396411f4329db1efadc8a9e424e007e9cc1" + } +} diff --git a/packages/core/test/sync-wire-objects.test.ts b/packages/core/test/sync-wire-objects.test.ts index 01793c7999..f15ce0dc50 100644 --- a/packages/core/test/sync-wire-objects.test.ts +++ b/packages/core/test/sync-wire-objects.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { assertExactKeys, isPlainRecord } from '../src/sync-wire-objects.js'; +import { + assertExactKeys, + hasOwnDataProperty, + isPlainRecord, + snapshotDataRecord, +} from '../src/sync-wire-objects.js'; describe('RFC-64 sync wire object helpers', () => { it('accepts ordinary and null-prototype records', () => { @@ -56,4 +61,17 @@ describe('RFC-64 sync wire object helpers', () => { /enumerable data properties/, ); }); + + it('snapshots optional data properties once and rejects null optionals centrally', () => { + const source = Object.create(null) as Record; + source.required = 'ok'; + source.optional = 1; + const snapshot = snapshotDataRecord(source, 'fixture', { rejectNullValues: true }); + expect(snapshot).toEqual({ required: 'ok', optional: 1 }); + expect(hasOwnDataProperty(snapshot, 'optional')).toBe(true); + expect(hasOwnDataProperty(snapshot, 'missing')).toBe(false); + expect(() => snapshotDataRecord({ required: 'ok', optional: null }, 'fixture', { + rejectNullValues: true, + })).toThrow(/omit optional fields/); + }); }); diff --git a/packages/core/test/system-record-applied-state-v1.test.ts b/packages/core/test/system-record-applied-state-v1.test.ts new file mode 100644 index 0000000000..c027e5b749 --- /dev/null +++ b/packages/core/test/system-record-applied-state-v1.test.ts @@ -0,0 +1,223 @@ +import { describe, expect, it } from 'vitest'; + +import { + canonicalizeSystemRecordAppliedStateV1, + canonicalizeSystemRecordCapacityStateV1, + canonicalizeSystemRecordMaterializationReceiptV1, + computeSystemRecordAccountedBytesV1, + computeSystemRecordAppliedStateDigestV1, + computeSystemRecordRootClaimSetDigestV1, + parseCanonicalSystemRecordAppliedStateV1, + parseCanonicalSystemRecordCapacityStateV1, + parseCanonicalSystemRecordMaterializationReceiptV1, + systemRecordAppliedStateAbsentV1, + SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1, + type SystemRecordAppliedStatePresentV1, +} from '../src/system-record-applied-state-v1.js'; +import { computeSystemRecordStableKeyHashV1 } from '../src/system-record-inventory-v1.js'; +import { SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES } from '../src/system-record-limits-v1.js'; +import { EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1 } from '../src/system-record-objects-v1.js'; + +const HASH_A = `0x${'aa'.repeat(32)}` as const; +const HASH_B = `0x${'bb'.repeat(32)}` as const; +const ROOT_A = 'did:dkg:agent:0x1111111111111111111111111111111111111111'; +const ROOT_B = 'did:dkg:agent:0x2222222222222222222222222222222222222222'; +const PEER = '12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf'; +const STABLE_KEY = computeSystemRecordStableKeyHashV1('otp:20430', PEER); + +describe('system-record applied-state codecs', () => { + it('pins the absent sentinel and round-trips present state', () => { + const absent = systemRecordAppliedStateAbsentV1(); + expect(parseCanonicalSystemRecordAppliedStateV1( + canonicalizeSystemRecordAppliedStateV1(absent), + )).toBe(absent); + expect(computeSystemRecordAppliedStateDigestV1(absent)).toMatch(/^0x[0-9a-f]{64}$/); + + const present = activeState(); + const parsed = parseCanonicalSystemRecordAppliedStateV1( + canonicalizeSystemRecordAppliedStateV1(present), + ); + expect(parsed).toEqual(present); + }); + + it('rejects partial intent/deletion groups, nulls, duplicate roots, and over-cap slots', () => { + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), conflictSidecarIntentOperation: 'publish', + } as SystemRecordAppliedStatePresentV1)).toThrow(/all present/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), pendingDeletionTableDigest: HASH_A, + pendingDeletionSubjectCount: '1', pendingDeletionTableBytes: '10', + } as SystemRecordAppliedStatePresentV1)).toThrow(/dirty/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), conflictEvidenceDigest: null, + } as unknown as SystemRecordAppliedStatePresentV1)).toThrow(/omit optional/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), historicalRoots: [ROOT_A], + })).toThrow(/duplicate/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), conflictDigestSlots: Array.from({ length: 17 }, (_, index) => + `0x${index.toString(16).padStart(64, '0')}`), + } as SystemRecordAppliedStatePresentV1)).toThrow(/slots/); + }); + + it('requires closed persisted arrays and returns owned frozen snapshots', () => { + const lineage = Object.assign([], { map: () => [{ unexpected: true }] }); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), transitionLineage: lineage, + } as unknown as SystemRecordAppliedStatePresentV1)).toThrow(/closed|non-index/); + + const rootAccessor = Object.defineProperty([ROOT_B], '0', { + enumerable: true, + get: () => ROOT_B, + }); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), historicalRoots: rootAccessor, + } as unknown as SystemRecordAppliedStatePresentV1)).toThrow(/data elements/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), conflictDigestSlots: new Array(1), + } as unknown as SystemRecordAppliedStatePresentV1)).toThrow(/dense/); + + const parsed = parseCanonicalSystemRecordAppliedStateV1( + canonicalizeSystemRecordAppliedStateV1(activeState()), + ); + if (parsed.state !== 'present') throw new Error('expected present state'); + expect(Object.isFrozen(parsed.transitionLineage)).toBe(true); + expect(Object.isFrozen(parsed.historicalRoots)).toBe(true); + expect(Object.isFrozen(parsed.conflictDigestSlots)).toBe(true); + }); + + it('enforces tombstone zero accounting and capacity aggregate arithmetic', () => { + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), status: 'tombstone', projectionBytes: '1', projectionQuads: '0', + ownedSubjectCount: '0', + accountedBytes: computeSystemRecordAccountedBytesV1(80, 1).toString(), + })).toThrow(/canonical empty/); + + const capacity = { + objectType: 'system-record-capacity-state', kind: 'agents', networkId: 'otp:20430', + revision: '1', liveRecordCount: '1', stateBytes: '1024', tableBytes: '2048', + projectionBytes: '4096', projectionQuads: '3', + } as const; + expect(parseCanonicalSystemRecordCapacityStateV1( + canonicalizeSystemRecordCapacityStateV1(capacity), + )).toEqual(capacity); + expect(computeSystemRecordAccountedBytesV1(2048, 4096)).toBe( + SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES + 2048 + 4096, + ); + const exact = Number(activeState().accountedBytes); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), + accountedBytes: (exact - 1).toString(), + })).toThrow(/must equal/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), + accountedBytes: (exact + 1).toString(), + })).toThrow(/must equal/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), + conflictDigestSlots: Array.from({ length: 16 }, (_, index) => + `0x${index.toString(16).padStart(64, '0')}`), + })).not.toThrow(); + }); + + it('binds a root-claim-set digest into present state', () => { + const claims = { + objectType: 'system-record-root-claim-set', kind: 'agents', networkId: 'otp:20430', + stableKeyHash: STABLE_KEY, currentRoot: ROOT_A, historicalRoots: [ROOT_B], + } as const; + const digest = computeSystemRecordRootClaimSetDigestV1(claims); + expect(computeSystemRecordAppliedStateDigestV1({ + ...activeState(), + transitionLineage: [{ priorAuthoritySequence: '0', nextAuthoritySequence: '1', transitionDigest: HASH_A }], + historicalRoots: [ROOT_B], + rootClaimSetDigest: digest, + })).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it('binds canonical peer identity, roots, lineage, and sidecar saga relations', () => { + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), stableKeyHash: HASH_A, + })).toThrow(/stableKeyHash/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), rootClaimSetDigest: HASH_A, + })).toThrow(/rootClaimSetDigest/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), historicalRoots: [ROOT_B], + })).toThrow(/lineage/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), status: 'quarantined', + })).toThrow(/installed conflict evidence|resumable sidecar intent/); + + const publish = { + ...activeState(), status: 'quarantined' as const, + conflictSidecarIntentOperation: 'publish' as const, + conflictSidecarIntentEvidenceDigest: HASH_A, + conflictSidecarIntentStateRevision: '1' as const, + }; + expect(() => canonicalizeSystemRecordAppliedStateV1(publish)).not.toThrow(); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...publish, status: 'active', + })).toThrow(/publish\/deferred/); + const remove = { + ...activeState(), conflictEvidenceDigest: HASH_A, + conflictSidecarIntentOperation: 'remove' as const, + conflictSidecarIntentEvidenceDigest: HASH_A, + conflictSidecarIntentStateRevision: '1' as const, + }; + expect(() => canonicalizeSystemRecordAppliedStateV1(remove)).not.toThrow(); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...remove, conflictSidecarIntentEvidenceDigest: HASH_B, + })).toThrow(/retaining/); + }); + + it('round-trips terminal tombstone state and materialization receipts', () => { + expect(SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1).toBe( + '0x4d798c66290f2feed54b20ad25eab62df38360cab298332be5e6d921ad1b5f3c', + ); + const tombstone = { + ...activeState(), status: 'tombstone' as const, + projectionDigest: SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1, + projectionBytes: '0' as const, projectionQuads: '0' as const, + ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + ownedSubjectCount: '0' as const, ownedSubjectTableBytes: '0' as const, + accountedBytes: SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES.toString(), + }; + expect(parseCanonicalSystemRecordAppliedStateV1( + canonicalizeSystemRecordAppliedStateV1(tombstone), + )).toEqual(tombstone); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...tombstone, + projectionDigest: HASH_B, + })).toThrow(/canonical empty projection/); + expect(() => canonicalizeSystemRecordAppliedStateV1({ + ...activeState(), + projectionDigest: SYSTEM_RECORD_EMPTY_PROJECTION_DIGEST_V1, + })).toThrow(/nonempty projection/); + const receipt = { + objectType: 'system-record-materialization-receipt', kind: 'agents', networkId: 'otp:20430', + stableKeyHash: STABLE_KEY, stateRevision: '1', appliedStateDigest: HASH_A, + headDigest: HASH_B, materializationEpoch: '2', + } as const; + expect(parseCanonicalSystemRecordMaterializationReceiptV1( + canonicalizeSystemRecordMaterializationReceiptV1(receipt), + )).toEqual(receipt); + }); +}); + +function activeState(): SystemRecordAppliedStatePresentV1 { + const rootClaimSetDigest = computeSystemRecordRootClaimSetDigestV1({ + objectType: 'system-record-root-claim-set', kind: 'agents', networkId: 'otp:20430', + stableKeyHash: STABLE_KEY, currentRoot: ROOT_A, historicalRoots: [], + }); + return { + objectType: 'system-record-applied-state', state: 'present', kind: 'agents', + networkId: 'otp:20430', stableKeyHash: STABLE_KEY, peerId: PEER, + stateRevision: '1', status: 'active', headDigest: HASH_A, + transitionLineage: [], + projectionDigest: HASH_B, projectionBytes: '4096', projectionQuads: '3', + ownedSubjectTableDigest: HASH_A, ownedSubjectCount: '1', ownedSubjectTableBytes: '80', + currentRoot: ROOT_A, historicalRoots: [], conflictDigestSlots: [], + conflictOverflow: false, materializationEpoch: '2', rootClaimSetDigest, + accountedBytes: computeSystemRecordAccountedBytesV1(80, 4096).toString(), + }; +} diff --git a/packages/core/test/system-record-golden-v1.test.ts b/packages/core/test/system-record-golden-v1.test.ts new file mode 100644 index 0000000000..388b4d7db2 --- /dev/null +++ b/packages/core/test/system-record-golden-v1.test.ts @@ -0,0 +1,197 @@ +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { keccak_256 } from '@noble/hashes/sha3.js'; +import { describe, expect, it } from 'vitest'; + +import { + buildSystemRecordProviderSignatureMessageV1, + verifySignedSystemRecordRootDescriptorEnvelopeV1, + type SignedSystemRecordRootDescriptorEnvelopeV1, +} from '../src/system-record-inventory-v1.js'; +import { + EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + buildSystemRecordSignatureMessageV1, + canonicalizeAgentProfileAuthorityTransitionV1, + canonicalizeAgentProfileForkResolutionV1, + canonicalizeAgentProfileHeadObjectV1, + canonicalizeSignedSystemRecordEnvelopeV1, + computeAgentProfileAuthorityTransitionDigestV1, + computeAgentProfileForkResolutionDigestV1, + computeAgentProfileHeadObjectDigestV1, + computeSignedSystemRecordEnvelopeDigestV1, + canonicalizeSystemRecordRootCollisionEvidenceV1, + computeSystemRecordRootCollisionEvidenceDigestV1, + verifySignedSystemRecordEnvelopeV1, + type AgentProfileAuthorityTransitionV1, + type AgentProfileForkResolutionV1, + type AgentProfileHeadObjectV1, + type SignedSystemRecordEnvelopeV1, + type SystemRecordSignatureRoleV1, + type SystemRecordPeerPublicKeyV1, +} from '../src/system-record-objects-v1.js'; + +const vectors = JSON.parse(readFileSync( + new URL('./fixtures/system-record-v1/vectors.json', import.meta.url), + 'utf8', +)) as GoldenVectors; + +describe('independently generated system-record V1 golden vectors', () => { + it('reproduces the committed fixture without importing production codecs', () => { + execFileSync(process.execPath, [ + new URL('./fixtures/system-record-v1/generate.mjs', import.meta.url).pathname, + '--check', + ]); + }); + + it('pins canonical bytes and semantic digests for every object omission variant', () => { + for (const name of ['active', 'tombstone'] as const) { + const vector = vectors.variants[name]; + expect(Buffer.from(canonicalizeAgentProfileHeadObjectV1(vector.object)).toString()) + .toBe(vector.canonical); + expect(computeAgentProfileHeadObjectDigestV1(vector.object)).toBe(vector.digest); + } + for (const name of ['coSignedTransition', 'expiredTransition'] as const) { + const vector = vectors.variants[name]; + expect(Buffer.from(canonicalizeAgentProfileAuthorityTransitionV1(vector.object)).toString()) + .toBe(vector.canonical); + expect(computeAgentProfileAuthorityTransitionDigestV1(vector.object)).toBe(vector.digest); + } + for (const name of ['forkV0', 'forkV1'] as const) { + const vector = vectors.variants[name]; + expect(Buffer.from(canonicalizeAgentProfileForkResolutionV1(vector.object)).toString()) + .toBe(vector.canonical); + expect(computeAgentProfileForkResolutionDigestV1(vector.object)).toBe(vector.digest); + } + expect(EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1).toBe(vectors.emptyTableDigest); + }); + + it('pins every role/suite message, signature, envelope byte branch, and digest', async () => { + for (const vector of Object.values(vectors.signed)) { + const envelope = vector.envelope; + expect(Buffer.from(canonicalizeSignedSystemRecordEnvelopeV1(envelope)).toString()) + .toBe(vector.canonical); + expect(computeSignedSystemRecordEnvelopeDigestV1(envelope)).toBe(vector.envelopeDigest); + for (const entry of envelope.signatures) { + expect(Buffer.from(buildSystemRecordSignatureMessageV1( + envelope.object, + envelope.objectDigest, + entry.role, + )).toString('hex')).toBe(vector.messages[entry.role]); + } + expect(await verifySignedSystemRecordEnvelopeV1(envelope, { + verifyEip1271: (entry, hash) => { + const messageHex = vector.messages[entry.role]; + if (entry.evidence.kind !== 'eip1271-current-finalized' || messageHex === undefined) { + return false; + } + const message = Buffer.from(messageHex, 'hex'); + const expected = keccak_256(Buffer.concat([ + Buffer.from(`\x19Ethereum Signed Message:\n${message.byteLength}`), + message, + ])); + return Buffer.from(hash).equals(Buffer.from(expected)); + }, + })).toBe(true); + } + }); + + it('fails closed when EIP-1271 verification is absent or rejects', async () => { + for (const name of [ + 'activeEip1271', + 'coSignedTransitionEip1271', + 'forkV1Eip1271', + ] as const) { + const envelope = vectors.signed[name].envelope; + expect(await verifySignedSystemRecordEnvelopeV1(envelope)).toBe(false); + expect(await verifySignedSystemRecordEnvelopeV1(envelope, { + verifyEip1271: () => false, + })).toBe(false); + } + + const valid = vectors.signed.activeEip1271.envelope; + const crossNetwork = { + ...valid, + signatures: valid.signatures.map((entry) => entry.suite === 'eip1271-current-finalized-v1' + ? { ...entry, evidence: { ...entry.evidence, chainId: '1' } } + : entry), + } as unknown as typeof valid; + let verifierCalled = false; + await expect(verifySignedSystemRecordEnvelopeV1(crossNetwork, { + verifyEip1271: () => { + verifierCalled = true; + return true; + }, + })).rejects.toThrow(/chainId.*record network/); + expect(verifierCalled).toBe(false); + }); + + it('pins provider descriptor message/signature independently of authority', async () => { + const envelope: SignedSystemRecordRootDescriptorEnvelopeV1 = { + object: vectors.provider.rootDescriptor, + objectDigest: vectors.provider.rootDigest, + providerPeerId: vectors.identities.peerId, + signatureSuite: 'ed25519-v1', + signature: vectors.provider.signature, + }; + expect(Buffer.from(buildSystemRecordProviderSignatureMessageV1( + envelope.object, + envelope.objectDigest, + envelope.providerPeerId, + )).toString('hex')).toBe(vectors.provider.messageHex); + expect(await verifySignedSystemRecordRootDescriptorEnvelopeV1( + envelope, + vectors.identities.peerPublicKey, + )).toBe(true); + }); + + it('pins the exact root-collision tuple and digest', () => { + expect(Buffer.from(canonicalizeSystemRecordRootCollisionEvidenceV1( + vectors.rootCollision.input, + )).toString()).toBe(vectors.rootCollision.canonical); + expect(computeSystemRecordRootCollisionEvidenceDigestV1(vectors.rootCollision.input)) + .toBe(vectors.rootCollision.digest); + }); +}); + +interface GoldenVector { + readonly object: T; + readonly canonical: string; + readonly digest: `0x${string}`; +} + +interface GoldenVectors { + readonly identities: { + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerPublicKeyV1; + readonly evmIssuer: string; + readonly rootSubject: string; + }; + readonly emptyTableDigest: `0x${string}`; + readonly variants: { + readonly active: GoldenVector; + readonly tombstone: GoldenVector; + readonly coSignedTransition: GoldenVector; + readonly expiredTransition: GoldenVector; + readonly forkV0: GoldenVector; + readonly forkV1: GoldenVector; + }; + readonly signed: Readonly; + readonly canonical: string; + readonly envelopeDigest: `0x${string}`; + readonly messages: Partial>; + }>>; + readonly provider: { + readonly rootDescriptor: SignedSystemRecordRootDescriptorEnvelopeV1['object']; + readonly rootDigest: `0x${string}`; + readonly messageHex: string; + readonly signature: string; + }; + readonly rootCollision: { + readonly input: import('../src/system-record-objects-v1.js').SystemRecordRootCollisionEvidenceV1; + readonly canonical: string; + readonly digest: `0x${string}`; + }; +} diff --git a/packages/core/test/system-record-inventory-v1.test.ts b/packages/core/test/system-record-inventory-v1.test.ts new file mode 100644 index 0000000000..457832e557 --- /dev/null +++ b/packages/core/test/system-record-inventory-v1.test.ts @@ -0,0 +1,1048 @@ +import { peerIdFromMultihash } from '@libp2p/peer-id'; +import { create as createMultihashDigest } from 'multiformats/hashes/digest'; +import { describe, expect, it } from 'vitest'; + +import { + assertSystemRecordInventoryCowUpdateBoundV1, + buildSystemRecordInventoryTreeV1, + createSystemRecordInventoryTraversalV1, + chooseSystemRecordByteAwareSplitIndexV1, + chooseSystemRecordRebalanceV1, + canonicalizeSystemRecordInventoryInternalObjectV1, + canonicalizeSystemRecordRootDescriptorObjectV1, + computeSystemRecordInventoryLeafDigestV1, + computeSystemRecordInventoryInternalDigestV1, + computeSystemRecordRootDescriptorDigestV1, + computeSystemRecordStableKeyHashV1, + decodeInventoryRowBase64UrlV1, + decodeSystemRecordInventoryRowV1, + encodeInventoryRowBase64UrlV1, + encodeSystemRecordInventoryRowV1, + parseCanonicalSystemRecordInventoryLeafObjectV1, + canonicalizeSystemRecordInventoryLeafObjectV1, + updateSystemRecordInventoryTreeV1, + systemRecordInventoryRowMaxEncodedBytesV1, + type SystemRecordInventoryLeafObjectV1, + type SystemRecordInventoryInternalObjectV1, + type SystemRecordInventoryCowUpdateV1, + type SystemRecordInventoryRowV1, + type SystemRecordInventoryStoredObjectV1, + type SystemRecordInventoryTreeSnapshotV1, +} from '../src/system-record-inventory-v1.js'; +import { + SYSTEM_RECORD_MAX_EVIDENCE_ROW_BYTES, + SYSTEM_RECORD_MAX_INVENTORY_RECORDS, + SYSTEM_RECORD_MAX_ORDINARY_ROW_BYTES, + SYSTEM_RECORD_MAX_ROW_BYTES, + SYSTEM_RECORD_SLICE_TIMEOUT_MS, +} from '../src/system-record-limits-v1.js'; + +const NETWORK = 'otp:20430' as const; +const HEAD = `0x${'aa'.repeat(32)}` as const; +const EVIDENCE = `0x${'bb'.repeat(32)}` as const; +const PEER_A = '12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf'; +const PEER_B = '12D3KooWDxBauQDeJjCmcvWiREFALfKsr5VfTzGUJbZJ6CUcc7aF'; + +describe('system-record compact inventory rows', () => { + it('pins ordinary/evidence maximum rows at 340/372 bytes and accepts version zero', () => { + expect(systemRecordInventoryRowMaxEncodedBytesV1(256)).toBe(SYSTEM_RECORD_MAX_ORDINARY_ROW_BYTES); + expect(systemRecordInventoryRowMaxEncodedBytesV1(256, true)).toBe(SYSTEM_RECORD_MAX_EVIDENCE_ROW_BYTES); + const ordinary = row(PEER_A); + const evidence = { ...ordinary, quarantined: true, conflictEvidenceDigest: EVIDENCE }; + expect(encodeSystemRecordInventoryRowV1(NETWORK, ordinary).byteLength).toBeLessThanOrEqual(SYSTEM_RECORD_MAX_ORDINARY_ROW_BYTES); + expect(encodeSystemRecordInventoryRowV1(NETWORK, evidence).byteLength).toBeLessThanOrEqual(SYSTEM_RECORD_MAX_EVIDENCE_ROW_BYTES); + expect(decodeSystemRecordInventoryRowV1(NETWORK, encodeSystemRecordInventoryRowV1(NETWORK, ordinary))) + .toEqual(ordinary); + expect(decodeSystemRecordInventoryRowV1(NETWORK, encodeSystemRecordInventoryRowV1(NETWORK, evidence))) + .toEqual(evidence); + }); + + it('normalizes row objects and bytes through intrinsic snapshots', () => { + const value = row(PEER_A); + const encoded = encodeSystemRecordInventoryRowV1(NETWORK, value); + const hostileRow = new Proxy(value, { + get(target, property, receiver) { + if (property === 'peerId') return PEER_B; + return Reflect.get(target, property, receiver); + }, + }); + expect(encodeSystemRecordInventoryRowV1(NETWORK, hostileRow)).toEqual(encoded); + + class MisleadingRowBytes extends Uint8Array { + override get byteLength(): number { return 1; } + override subarray(): Uint8Array { return new Uint8Array(); } + } + expect(decodeSystemRecordInventoryRowV1(NETWORK, new MisleadingRowBytes(encoded))).toEqual(value); + }); + + it('rejects mismatched stable keys and evidence without quarantine', () => { + expect(() => encodeSystemRecordInventoryRowV1(NETWORK, { + ...row(PEER_A), quarantined: false, conflictEvidenceDigest: EVIDENCE, + })).toThrow(/quarantined/); + const encoded = encodeSystemRecordInventoryRowV1(NETWORK, row(PEER_A)); + expect(() => decodeSystemRecordInventoryRowV1('other:network' as typeof NETWORK, encoded)) + .toThrow(/stable key/); + expect(() => decodeInventoryRowBase64UrlV1( + NETWORK, + 'A'.repeat(Math.ceil(SYSTEM_RECORD_MAX_ROW_BYTES * 4 / 3) + 1), + )).toThrow(/base64url/); + }); + + it('accepts authority sequence 14 and rejects sequence 15', () => { + const atCap = { ...row(PEER_A), authoritySequence: '14' as const }; + expect(decodeSystemRecordInventoryRowV1( + NETWORK, + encodeSystemRecordInventoryRowV1(NETWORK, atCap), + )).toEqual(atCap); + expect(() => encodeSystemRecordInventoryRowV1(NETWORK, { + ...row(PEER_A), authoritySequence: '15', + })).toThrow(/authoritySequence.*V1 cap/); + }); +}); + +describe('system-record immutable B+tree objects', () => { + it('round-trips a root leaf and rejects mutable publication/path fields', async () => { + const rows = [row(PEER_A), row(PEER_B)].sort((a, b) => + a.stableKeyHash.localeCompare(b.stableKeyHash)); + const leaf = leafFor(rows); + const bytes = canonicalizeSystemRecordInventoryLeafObjectV1(leaf, NETWORK, true); + expect(parseCanonicalSystemRecordInventoryLeafObjectV1(bytes, NETWORK, true)).toEqual(leaf); + expect(() => canonicalizeSystemRecordInventoryLeafObjectV1({ + ...leaf, epoch: '1', path: [0], + } as unknown as SystemRecordInventoryLeafObjectV1, NETWORK, true)).toThrow(/unknown or missing/); + + const treeDigest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, true); + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, + epoch: '0', version: '0', treeRootDigest: treeDigest, totalRows: '2', + } as const; + const result = await validateTree( + descriptor, + async (digest) => digest === treeDigest ? leaf : undefined, + ); + expect(result).toMatchObject({ totalRows: 2, leaves: 1, height: 1 }); + expect(computeSystemRecordRootDescriptorDigestV1(descriptor)).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it('validates descriptor totals and refuses incomplete traversal', async () => { + const rows = [row(PEER_A)]; + const leaf = leafFor(rows); + const digest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, true); + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, + epoch: '0', version: '0', treeRootDigest: digest, totalRows: '2', + } as const; + await expect(validateTree(descriptor, async () => leaf)) + .rejects.toThrow(/total/); + await expect(validateTree( + { ...descriptor, totalRows: '1' }, + async () => undefined, + )).rejects.toThrow(/missing/); + let loads = 0; + await expect(validateTree( + { ...descriptor, totalRows: '0' }, + async () => { loads += 1; return leaf; }, + )).rejects.toThrow(/row bound/); + expect(loads).toBe(1); + const controller = new AbortController(); + controller.abort(new Error('stop-now')); + await expect(validateTree( + { ...descriptor, totalRows: '1' }, + async () => leaf, + controller.signal, + )).rejects.toThrow(/stop-now/); + }); + + it('enforces slice request/byte budgets and observes abort after an awaited load', async () => { + const snapshot = buildSystemRecordInventoryTreeV1(NETWORK, deterministicPeers(513).map(row)); + const traversal = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + let slices = 0; + let totalWireBytes = 0; + while (true) { + const result = await traversal.advance(async (digest) => { + const stored = snapshot.objects.get(digest)!; + return loadedInventoryObject(stored); + }, { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 3_000, + }); + slices += 1; + totalWireBytes += result.wireBytes; + expect(result.requests).toBeLessThanOrEqual(1); + expect(result.wireBytes).toBeLessThanOrEqual(2 * 1024 * 1024); + if (result.status === 'complete') break; + } + expect(slices).toBeGreaterThan(1); + expect(totalWireBytes).toBe([...snapshot.objects.values()].reduce( + (sum, stored) => sum + loadedInventoryObject(stored).wireBytes, + 0, + )); + + const rejected = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(rejected.advance(async () => ({ + outcome: 'rejected', wireBytes: 8_196, rejection: 'busy', + }), { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 3_000, + })).resolves.toEqual({ + status: 'rejected', requests: 1, wireBytes: 8_196, rejection: 'busy', + }); + + const aborted = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + const controller = new AbortController(); + await expect(aborted.advance(async (digest) => { + controller.abort(new Error('aborted-after-load')); + const stored = snapshot.objects.get(digest)!; + return loadedInventoryObject(stored); + }, { + signal: controller.signal, + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 3_000, + })).rejects.toThrow(/aborted-after-load/); + }); + + it('pins slice admission while an awaited loader mutates its source object', async () => { + const snapshot = buildSystemRecordInventoryTreeV1(NETWORK, deterministicPeers(513).map(row)); + const requestBoundTraversal = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + const requestBoundSlice = { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 3_000, + }; + const requestBound = await requestBoundTraversal.advance(async (digest) => { + requestBoundSlice.maxRequests = 12; + const stored = snapshot.objects.get(digest)!; + return loadedInventoryObject(stored); + }, requestBoundSlice); + expect(requestBound).toMatchObject({ status: 'paused', requests: 1 }); + + const deadlineTraversal = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + let nowMs = 0; + const deadlineSlice = { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: 1, + nowMs: () => nowMs, + }; + await expect(deadlineTraversal.advance(async (digest) => { + nowMs = 2; + deadlineSlice.deadlineMs = 3; + const stored = snapshot.objects.get(digest)!; + return loadedInventoryObject(stored); + }, deadlineSlice)).rejects.toThrow(/deadline expired during load/); + + const abortTraversal = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + const admitted = new AbortController(); + const replacement = new AbortController(); + const abortSlice = { + signal: admitted.signal, + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 3_000, + }; + await expect(abortTraversal.advance(async (digest) => { + admitted.abort(new Error('admitted-signal-aborted')); + abortSlice.signal = replacement.signal; + const stored = snapshot.objects.get(digest)!; + return loadedInventoryObject(stored); + }, abortSlice)).rejects.toThrow(/admitted-signal-aborted/); + }); + + it('latches traversal admission before invoking a reentrant clock', async () => { + const snapshot = buildSystemRecordInventoryTreeV1(NETWORK, [row(PEER_A)]); + const traversal = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + let nested: Promise | undefined; + let nestedLoads = 0; + let reentered = false; + const outer = traversal.advance(async (digest) => ( + loadedInventoryObject(snapshot.objects.get(digest)!) + ), { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: SYSTEM_RECORD_SLICE_TIMEOUT_MS, + nowMs: () => { + if (!reentered) { + reentered = true; + nested = traversal.advance(async () => { + nestedLoads += 1; + throw new Error('nested loader must not run'); + }, { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: SYSTEM_RECORD_SLICE_TIMEOUT_MS, + nowMs: () => 0, + }); + } + return 0; + }, + }); + await expect(nested).rejects.toThrow(/already has an active slice/); + expect(nestedLoads).toBe(0); + await expect(outer).resolves.toMatchObject({ status: 'complete', requests: 1 }); + }); + + it('enforces the three-second admitted slice ceiling', async () => { + const snapshot = buildSystemRecordInventoryTreeV1(NETWORK, [row(PEER_A)]); + const overlong = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + let loads = 0; + await expect(overlong.advance(async (digest) => { + loads += 1; + return loadedInventoryObject(snapshot.objects.get(digest)!); + }, { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: SYSTEM_RECORD_SLICE_TIMEOUT_MS + 1, + nowMs: () => 0, + })).rejects.toThrow(/slice budget/); + expect(loads).toBe(0); + + const boundary = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(boundary.advance(async (digest) => ( + loadedInventoryObject(snapshot.objects.get(digest)!) + ), { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: SYSTEM_RECORD_SLICE_TIMEOUT_MS, + nowMs: () => 0, + })).resolves.toMatchObject({ status: 'complete', requests: 1 }); + }); + + it('times out a stalled loader and permits a later retry on the same traversal', async () => { + const leaf = leafFor([row(PEER_A)]); + const digest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, true); + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, + epoch: '0', version: '0', treeRootDigest: digest, totalRows: '1', + } as const; + const traversal = createSystemRecordInventoryTraversalV1(descriptor); + await expect(traversal.advance( + async () => new Promise(() => undefined), + { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 20, + }, + )).rejects.toThrow(/deadline expired during load/); + + const recovered = await traversal.advance( + async () => loadedInventoryObject({ + objectKind: 'inventory-leaf', + object: leaf, + canonicalBytes: canonicalizeSystemRecordInventoryLeafObjectV1(leaf, NETWORK, true), + }), + { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: Date.now() + 1_000, + }, + ); + expect(recovered.status).toBe('complete'); + }); + + it('does not dispatch loader work when the admitted signal aborts before its microtask', async () => { + const leaf = leafFor([row(PEER_A)]); + const digest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, true); + const traversal = createSystemRecordInventoryTraversalV1({ + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, + epoch: '0', version: '0', treeRootDigest: digest, totalRows: '1', + }); + const controller = new AbortController(); + let clockReads = 0; + let loaderCalls = 0; + await expect(traversal.advance(async () => { + loaderCalls += 1; + return undefined; + }, { + signal: controller.signal, + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: 1_000, + nowMs: () => { + clockReads += 1; + if (clockReads === 3) controller.abort(new Error('test abort')); + return 0; + }, + })).rejects.toThrow(/test abort/); + expect(loaderCalls).toBe(0); + }); + + it('exact-snapshots loader unions and does not retain caller-mutable completion sets', async () => { + class MisleadingSliceBytes extends Uint8Array { + override slice(): Uint8Array { return new Uint8Array(); } + } + const snapshot = buildSystemRecordInventoryTreeV1(NETWORK, [row(PEER_A)]); + const slice = { + maxRequests: 1, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: SYSTEM_RECORD_SLICE_TIMEOUT_MS, + nowMs: () => 0, + }; + const invalidOutcome = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(invalidOutcome.advance(async () => ({ + outcome: 'unexpected', wireBytes: 128, + } as never), slice)).rejects.toThrow(/invalid outcome/); + + const extraField = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(extraField.advance(async (digest) => ({ + ...loadedInventoryObject(snapshot.objects.get(digest)!), + unexpected: true, + }), slice)).rejects.toThrow(/unknown or missing fields/); + + const accessor = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(accessor.advance(async (digest) => { + const loaded = loadedInventoryObject(snapshot.objects.get(digest)!); + return Object.defineProperty({ ...loaded }, 'canonicalBytes', { + enumerable: true, + get: () => loaded.canonicalBytes, + }); + }, slice)).rejects.toThrow(/data properties/); + + const undercounted = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(undercounted.advance(async (digest) => { + const stored = snapshot.objects.get(digest)!; + return { + outcome: 'ok' as const, + objectKind: stored.objectKind, + canonicalBytes: stored.canonicalBytes, + wireBytes: 4 + stored.canonicalBytes.byteLength, + }; + }, slice)).rejects.toThrow(/over-cap object/); + + const subclassBytes = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + await expect(subclassBytes.advance(async (digest) => { + const loaded = loadedInventoryObject(snapshot.objects.get(digest)!); + return { + ...loaded, + canonicalBytes: new MisleadingSliceBytes(loaded.canonicalBytes), + }; + }, slice)).resolves.toMatchObject({ status: 'complete', requests: 1 }); + + const completed = createSystemRecordInventoryTraversalV1(snapshot.descriptor); + const first = await completed.advance(async (digest) => ( + loadedInventoryObject(snapshot.objects.get(digest)!) + ), slice); + expect(first.status).toBe('complete'); + (first.result!.objectDigests as Set).clear(); + const repeated = await completed.advance(async () => { + throw new Error('completed traversal must not load again'); + }, slice); + expect(repeated.result?.objectDigests.size).toBe(1); + }); + + it('rejects mixed child kinds before traversal', () => { + expect(() => canonicalizeSystemRecordInventoryInternalObjectV1({ + objectType: 'inventory-internal', + firstKeyHash: `0x${'01'.repeat(32)}`, + lastKeyHash: `0x${'02'.repeat(32)}`, + entries: [ + { separatorKeyHash: `0x${'01'.repeat(32)}`, childDigest: HEAD, childKind: 'inventory-leaf' }, + { separatorKeyHash: `0x${'02'.repeat(32)}`, childDigest: EVIDENCE, childKind: 'inventory-internal' }, + ], + }, true)).toThrow(/mix/); + }); + + it('rejects caller-owned behavior on persisted inventory arrays', () => { + const encoded = encodeInventoryRowBase64UrlV1(NETWORK, row(PEER_A)); + const rows = Object.assign([encoded], { map: () => [] }); + expect(() => canonicalizeSystemRecordInventoryLeafObjectV1({ + objectType: 'inventory-leaf', + firstKeyHash: computeSystemRecordStableKeyHashV1(NETWORK, PEER_A), + lastKeyHash: computeSystemRecordStableKeyHashV1(NETWORK, PEER_A), + rows, + }, NETWORK, true)).toThrow(/closed|non-index/); + + const entries = [{}, {}]; + Object.defineProperty(entries, Symbol.iterator, { value: function* () { yield {}; } }); + expect(() => canonicalizeSystemRecordInventoryInternalObjectV1({ + objectType: 'inventory-internal', + firstKeyHash: HEAD, + lastKeyHash: EVIDENCE, + entries, + } as unknown as SystemRecordInventoryInternalObjectV1, true)).toThrow(/dense closed array/); + + const builderRows = [row(PEER_A)]; + Object.defineProperty(builderRows, Symbol.iterator, { + value: function* () { yield row(PEER_A); yield row(PEER_B); }, + }); + expect(() => buildSystemRecordInventoryTreeV1(NETWORK, builderRows)).toThrow(/dense closed array/); + }); + + it('uses deterministic split/rebalance choices and enforces the COW budget', () => { + expect(chooseSystemRecordByteAwareSplitIndexV1([10, 10, 80, 10, 10], 2, 2)).toBe(2); + expect(chooseSystemRecordRebalanceV1(129, 200, 128)).toBe('borrow-left'); + expect(chooseSystemRecordRebalanceV1(128, 129, 128)).toBe('borrow-right'); + expect(chooseSystemRecordRebalanceV1(128, 128, 128)).toBe('merge-left'); + expect(() => assertSystemRecordInventoryCowUpdateBoundV1({ + leafObjects: 2, internalObjects: 2, rootObjects: 1, descriptorObjects: 1, + encodedBytes: 1024 * 1024, + })).not.toThrow(); + expect(() => assertSystemRecordInventoryCowUpdateBoundV1({ + leafObjects: 3, internalObjects: 2, rootObjects: 1, descriptorObjects: 1, + encodedBytes: 1024 * 1024, + })).toThrow(/six-object/); + + class MisleadingLengths extends Array { + override some(): boolean { return true; } + override reduce(): number { return Number.MAX_VALUE; } + override slice(): this { return new MisleadingLengths() as this; } + } + expect(chooseSystemRecordByteAwareSplitIndexV1( + new MisleadingLengths(10, 10, 80, 10, 10), + 2, + 2, + )).toBe(2); + const accounting = { + leafObjects: 2, internalObjects: 2, rootObjects: 1, descriptorObjects: 1, + encodedBytes: 1024 * 1024, + }; + const hostileAccounting = new Proxy(accounting, { + get: () => Number.MAX_SAFE_INTEGER, + }); + expect(() => assertSystemRecordInventoryCowUpdateBoundV1(hostileAccounting)).not.toThrow(); + }); + + it('performs localized immutable upsert/update/delete publications', async () => { + const peers = deterministicPeers(520); + let snapshot = buildSystemRecordInventoryTreeV1(NETWORK, peers.slice(0, 519).map(row)); + const firstRoot = snapshot.descriptor.treeRootDigest; + const insert = updateSystemRecordInventoryTreeV1(snapshot, { operation: 'upsert', row: row(peers[519]) }); + expect(insert.changed).toBe(true); + expect(insert.accounting.descriptorObjects).toBe(1); + expect(insert.accounting.leafObjects).toBeLessThanOrEqual(2); + expect(insert.accounting.encodedBytes).toBeLessThanOrEqual(1024 * 1024); + expect(insert.descriptor.treeRootDigest).not.toBe(firstRoot); + expect(insert.loadedObjectDigests.size).toBeLessThanOrEqual(4); + snapshot = applyUpdate(snapshot, insert); + + const changedRow = { ...row(peers[519]), version: '1' as const, headDigest: EVIDENCE }; + const update = updateSystemRecordInventoryTreeV1(snapshot, { operation: 'upsert', row: changedRow }); + expect(update.changed).toBe(true); + expect(update.writes.length + 1).toBeLessThanOrEqual(6); + snapshot = applyUpdate(snapshot, update); + + const deletion = updateSystemRecordInventoryTreeV1(snapshot, { + operation: 'delete', stableKeyHash: changedRow.stableKeyHash, peerId: changedRow.peerId, + }); + expect(deletion.changed).toBe(true); + expect(deletion.writes.length + 1).toBeLessThanOrEqual(6); + const deletionSnapshot = applyUpdate(snapshot, deletion); + const validated = await validateTree( + deletion.descriptor, + async (digest) => deletionSnapshot.objects.get(digest)?.object, + ); + expect(validated.totalRows).toBe(519); + }); + + it('pins the COW snapshot shell and ignores overridden map methods', () => { + const peers = deterministicPeers(3); + const source = buildSystemRecordInventoryTreeV1(NETWORK, peers.slice(0, 2).map(row)); + class MisleadingObjectMap extends Map< + `0x${string}`, + SystemRecordInventoryStoredObjectV1 + > { + override has(): boolean { return true; } + override get(): SystemRecordInventoryStoredObjectV1 | undefined { return undefined; } + } + const hostileMap = new MisleadingObjectMap(source.objects); + const hostileSnapshot = new Proxy({ ...source, objects: hostileMap }, { + get() { throw new Error('COW updater must not invoke snapshot getters'); }, + }); + const update = updateSystemRecordInventoryTreeV1(hostileSnapshot, { + operation: 'upsert', row: row(peers[2]), + }); + expect(update.changed).toBe(true); + expect(update.writes.length).toBeGreaterThan(0); + }); + + it('rejects a corrupt untouched sibling before publishing a reused root range', () => { + const peers = deterministicPeers(520); + const source = buildSystemRecordInventoryTreeV1(NETWORK, peers.map(row)); + const objects = new Map(source.objects); + const rootObject = objects.get(source.descriptor.treeRootDigest)?.object; + if (rootObject?.objectType !== 'inventory-internal') throw new Error('expected a multi-leaf root'); + const siblingDigest = rootObject.entries.at(-1)!.childDigest; + const sibling = objects.get(siblingDigest); + if (sibling?.object.objectType !== 'inventory-leaf') throw new Error('expected a leaf sibling'); + objects.set(siblingDigest, Object.freeze({ + ...sibling, + object: Object.freeze({ + ...sibling.object, + lastKeyHash: `0x${'ff'.repeat(32)}`, + }), + })); + const target = peers.map(row).sort((left, right) => + left.stableKeyHash.localeCompare(right.stableKeyHash))[0]; + expect(() => updateSystemRecordInventoryTreeV1({ ...source, objects }, { + operation: 'upsert', + row: { ...target, version: '1', headDigest: EVIDENCE }, + })).toThrow(/key range|digest/); + + const mismatchedBytes = new Map(source.objects); + const validSibling = mismatchedBytes.get(siblingDigest)!; + mismatchedBytes.set(siblingDigest, Object.freeze({ + ...validSibling, + canonicalBytes: Uint8Array.of(0), + })); + expect(() => updateSystemRecordInventoryTreeV1({ ...source, objects: mismatchedBytes }, { + operation: 'upsert', + row: { ...target, version: '1', headDigest: EVIDENCE }, + })).toThrow(/canonical bytes/); + }); + + it('keeps unchanged mutations path-bounded without enumerating the snapshot', () => { + const peers = deterministicPeers(513); + const snapshot = buildSystemRecordInventoryTreeV1(NETWORK, peers.map(row)); + const update = updateSystemRecordInventoryTreeV1(snapshot, { + operation: 'upsert', + row: row(peers[256]), + }); + + expect(update.changed).toBe(false); + expect(update.writes).toHaveLength(0); + expect(update.reusedObjectDigests.size).toBe(0); + expect(update.loadedObjectDigests.size).toBeLessThanOrEqual(3); + }); + + it('keeps split, borrow, and merge publications within the physical write bound', async () => { + const peers = deterministicPeers(513); + let snapshot = buildSystemRecordInventoryTreeV1(NETWORK, peers.slice(0, 512).map(row)); + const split = updateSystemRecordInventoryTreeV1(snapshot, { operation: 'upsert', row: row(peers[512]) }); + expect(split.accounting).toMatchObject({ leafObjects: 2, rootObjects: 1, descriptorObjects: 1 }); + const sorted = peers.slice(0, 257).map(row).sort((a, b) => a.stableKeyHash.localeCompare(b.stableKeyHash)); + const borrowSnapshot = twoLeafSnapshot(sorted.slice(0, 128), sorted.slice(128)); + const borrowed = updateSystemRecordInventoryTreeV1(borrowSnapshot, { + operation: 'delete', stableKeyHash: sorted[0].stableKeyHash, peerId: sorted[0].peerId, + }); + expect(leafRows(applyUpdate(borrowSnapshot, borrowed)).map((group) => group.length)).toEqual([128, 128]); + expect(borrowed.writes.length + 1).toBeLessThanOrEqual(6); + + const mergeSnapshot = twoLeafSnapshot(sorted.slice(0, 128), sorted.slice(128, 256)); + const finalUpdate = updateSystemRecordInventoryTreeV1(mergeSnapshot, { + operation: 'delete', stableKeyHash: sorted[0].stableKeyHash, peerId: sorted[0].peerId, + }); + snapshot = applyUpdate(mergeSnapshot, finalUpdate); + expect(finalUpdate.writes.length + 1).toBeLessThanOrEqual(6); + expect(snapshot.objects.get(snapshot.descriptor.treeRootDigest)?.objectKind).toBe('inventory-leaf'); + const validated = await validateTree( + snapshot.descriptor, + async (digest) => snapshot.objects.get(digest)?.object, + ); + expect(validated).toMatchObject({ leaves: 1, height: 1, totalRows: 255 }); + }); + + it('borrows from the right before merging left when both siblings exist', () => { + const sorted = deterministicPeers(385).map(row) + .sort((left, right) => left.stableKeyHash.localeCompare(right.stableKeyHash)); + const snapshot = multiLeafSnapshot([sorted.slice(0, 128), sorted.slice(128, 256), sorted.slice(256)]); + const target = sorted[128]; + const update = updateSystemRecordInventoryTreeV1(snapshot, { + operation: 'delete', stableKeyHash: target.stableKeyHash, peerId: target.peerId, + }); + const next = applyUpdate(snapshot, update); + expect(leafRows(next).map((group) => group.length)).toEqual([128, 128, 128]); + }); + + it('keeps ascending, descending, deterministic-random, and repeated mutations path-local', async () => { + const all = deterministicPeers(580).map(row) + .sort((left, right) => left.stableKeyHash.localeCompare(right.stableKeyHash)); + let snapshot = buildSystemRecordInventoryTreeV1(NETWORK, all.slice(0, 520)); + const mutations = [ + ...all.slice(520, 530).map((candidate) => ({ operation: 'upsert' as const, row: candidate })), + ...all.slice(0, 10).reverse().map((candidate, index) => ({ + operation: 'upsert' as const, + row: { ...candidate, version: '1' as const, headDigest: index % 2 === 0 ? EVIDENCE : HEAD }, + })), + ...all.slice(40, 50).sort((left, right) => left.peerId.localeCompare(right.peerId)).map((candidate) => ({ + operation: 'delete' as const, stableKeyHash: candidate.stableKeyHash, peerId: candidate.peerId, + })), + ]; + for (const mutation of mutations) { + const update = updateSystemRecordInventoryTreeV1(snapshot, mutation); + expect(update.writes.length + (update.changed ? 1 : 0)).toBeLessThanOrEqual(6); + expect(update.accounting.encodedBytes).toBeLessThanOrEqual(1024 * 1024); + if (update.changed && snapshot.objects.size > 2) expect(update.reusedObjectDigests.size).toBeGreaterThan(0); + snapshot = applyUpdate(snapshot, update); + await validateSnapshotTree(snapshot); + } + }, 30_000); + + it('refuses an insertion above the signed inventory record cap', () => { + const peers = deterministicPeers(521); + const initial = buildSystemRecordInventoryTreeV1(NETWORK, peers.slice(0, 520).map(row)); + const descriptor = { + ...initial.descriptor, + totalRows: SYSTEM_RECORD_MAX_INVENTORY_RECORDS.toString() as typeof initial.descriptor.totalRows, + }; + const saturated = { + ...initial, + descriptor, + descriptorDigest: computeSystemRecordRootDescriptorDigestV1(descriptor), + }; + expect(() => updateSystemRecordInventoryTreeV1(saturated, { + operation: 'upsert', row: row(peers[520]), + })).toThrow(/row cap/); + }, 30_000); + + it.runIf(process.env.DKG_SYSTEM_RECORD_EXHAUSTIVE === '1')( + 'handles complete height-three right-borrow then non-collapsing merge publications', async () => { + const borrowSnapshot = heightThreeSnapshot([128, 128, 128], { internalIndex: 1, leafIndex: 1 }); + expect(await validateSnapshotTree(borrowSnapshot)).toMatchObject({ + leaves: 384, height: 3, totalRows: 49_153, + }); + const initialRoot = borrowSnapshot.objects.get( + borrowSnapshot.descriptor.treeRootDigest, + )!.object as SystemRecordInventoryInternalObjectV1; + const borrowTarget = firstRowOfInternalLeaf(borrowSnapshot, 1, 0); + const borrowed = updateSystemRecordInventoryTreeV1(borrowSnapshot, { + operation: 'delete', stableKeyHash: borrowTarget.stableKeyHash, peerId: borrowTarget.peerId, + }); + expect(borrowed.accounting).toMatchObject({ + leafObjects: 2, internalObjects: 1, rootObjects: 1, descriptorObjects: 1, + }); + expect(borrowed.writes.length + 1).toBeLessThanOrEqual(6); + const borrowedSnapshot = applyUpdate(borrowSnapshot, borrowed); + expect(rootInternalChildCounts(borrowedSnapshot)).toEqual([128, 128, 128]); + expect(borrowed.reusedObjectDigests.has(initialRoot.entries[0].childDigest)).toBe(true); + expect(borrowed.descriptor.totalRows).toBe('49152'); + const borrowedRoot = borrowedSnapshot.objects.get( + borrowedSnapshot.descriptor.treeRootDigest, + )!.object as SystemRecordInventoryInternalObjectV1; + const mergeTarget = firstRowOfInternalLeaf(borrowedSnapshot, 1, 0); + const merged = updateSystemRecordInventoryTreeV1(borrowedSnapshot, { + operation: 'delete', stableKeyHash: mergeTarget.stableKeyHash, peerId: mergeTarget.peerId, + }); + expect(merged.accounting).toMatchObject({ internalObjects: 1, rootObjects: 1, descriptorObjects: 1 }); + expect(merged.writes.length + 1).toBeLessThanOrEqual(6); + const mergedSnapshot = applyUpdate(borrowedSnapshot, merged); + expect(rootInternalChildCounts(mergedSnapshot)).toEqual([255, 128]); + expect(merged.reusedObjectDigests.has(borrowedRoot.entries[2].childDigest)).toBe(true); + expect(merged.descriptor).toMatchObject({ + priorRootDigest: borrowedSnapshot.descriptorDigest, + treeRootDigest: mergedSnapshot.descriptor.treeRootDigest, + totalRows: '49151', + version: '2', + }); + expectCowPublicationArtifacts(borrowedSnapshot, merged); + const mergedLeaf = merged.writes.find((write) => write.role === 'leaf')! + .object as SystemRecordInventoryLeafObjectV1; + expect(mergedLeaf.rows).toHaveLength(255); + expect(mergedLeaf.rows).not.toContain(encodeInventoryRowBase64UrlV1(NETWORK, mergeTarget)); + }, 120_000, + ); +}); + +function row(peerId: string): SystemRecordInventoryRowV1 { + return { + stableKeyHash: computeSystemRecordStableKeyHashV1(NETWORK, peerId), + peerId, + authoritySequence: '0', + version: '0', + headDigest: HEAD, + tombstone: false, + quarantined: false, + }; +} + +function leafFor(rows: readonly SystemRecordInventoryRowV1[]): SystemRecordInventoryLeafObjectV1 { + return { + objectType: 'inventory-leaf', + firstKeyHash: rows[0].stableKeyHash, + lastKeyHash: rows[rows.length - 1].stableKeyHash, + rows: rows.map((row) => encodeInventoryRowBase64UrlV1(NETWORK, row)), + }; +} + + +const DETERMINISTIC_PEER_CACHE: string[] = []; + +function deterministicPeers(count: number): string[] { + while (DETERMINISTIC_PEER_CACHE.length < count) { + const index = DETERMINISTIC_PEER_CACHE.length; + const raw = new Uint8Array(32); + new DataView(raw.buffer).setUint32(28, index + 1, false); + const encodedEd25519PublicKey = new Uint8Array(36); + encodedEd25519PublicKey.set([0x08, 0x01, 0x12, 0x20]); + encodedEd25519PublicKey.set(raw, 4); + DETERMINISTIC_PEER_CACHE.push(peerIdFromMultihash( + createMultihashDigest(0x00, encodedEd25519PublicKey), + ).toString()); + } + return DETERMINISTIC_PEER_CACHE.slice(0, count); +} + +function leafRows(snapshot: ReturnType): SystemRecordInventoryRowV1[][] { + const root = snapshot.objects.get(snapshot.descriptor.treeRootDigest)!; + if (root.objectKind === 'inventory-leaf') { + return [(root.object as SystemRecordInventoryLeafObjectV1).rows.map((encoded) => + decodeSystemRecordInventoryRowV1(NETWORK, Uint8Array.from(Buffer.from(encoded, 'base64url'))))]; + } + return (root.object as import('../src/system-record-inventory-v1.js').SystemRecordInventoryInternalObjectV1) + .entries.map((entry) => { + const leaf = snapshot.objects.get(entry.childDigest)!.object as SystemRecordInventoryLeafObjectV1; + return leaf.rows.map((encoded) => decodeSystemRecordInventoryRowV1( + NETWORK, + Uint8Array.from(Buffer.from(encoded, 'base64url')), + )); + }); +} + +function twoLeafSnapshot( + leftRows: readonly SystemRecordInventoryRowV1[], + rightRows: readonly SystemRecordInventoryRowV1[], +): SystemRecordInventoryTreeSnapshotV1 { + const objects = new Map(); + const leaves = [leafFor(leftRows), leafFor(rightRows)].map((leaf) => { + const canonicalBytes = canonicalizeSystemRecordInventoryLeafObjectV1(leaf, NETWORK, false); + const digest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, false); + objects.set(digest, { objectKind: 'inventory-leaf', object: leaf, canonicalBytes }); + return { leaf, digest }; + }); + const root: SystemRecordInventoryInternalObjectV1 = { + objectType: 'inventory-internal', firstKeyHash: leaves[0].leaf.firstKeyHash!, + lastKeyHash: leaves[1].leaf.lastKeyHash!, entries: leaves.map(({ leaf, digest }) => ({ + separatorKeyHash: leaf.firstKeyHash!, childDigest: digest, childKind: 'inventory-leaf', + })), + }; + const rootBytes = canonicalizeSystemRecordInventoryInternalObjectV1(root, true); + const rootDigest = computeSystemRecordInventoryInternalDigestV1(root, true); + objects.set(rootDigest, { objectKind: 'inventory-internal', object: root, canonicalBytes: rootBytes }); + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, epoch: '0', version: '0', + treeRootDigest: rootDigest, totalRows: (leftRows.length + rightRows.length).toString(), + } as const; + return { networkId: NETWORK, descriptor, descriptorDigest: computeSystemRecordRootDescriptorDigestV1(descriptor), objects }; +} + +function multiLeafSnapshot( + groups: readonly (readonly SystemRecordInventoryRowV1[])[], +): SystemRecordInventoryTreeSnapshotV1 { + const objects = new Map(); + const leaves = groups.map((rows) => { + const leaf = leafFor(rows); + const canonicalBytes = canonicalizeSystemRecordInventoryLeafObjectV1(leaf, NETWORK, false); + const digest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, false); + objects.set(digest, { objectKind: 'inventory-leaf', object: leaf, canonicalBytes }); + return { leaf, digest }; + }); + const root: SystemRecordInventoryInternalObjectV1 = { + objectType: 'inventory-internal', firstKeyHash: leaves[0].leaf.firstKeyHash!, + lastKeyHash: leaves.at(-1)!.leaf.lastKeyHash!, entries: leaves.map(({ leaf, digest }) => ({ + separatorKeyHash: leaf.firstKeyHash!, childDigest: digest, childKind: 'inventory-leaf', + })), + }; + const rootBytes = canonicalizeSystemRecordInventoryInternalObjectV1(root, true); + const rootDigest = computeSystemRecordInventoryInternalDigestV1(root, true); + objects.set(rootDigest, { objectKind: 'inventory-internal', object: root, canonicalBytes: rootBytes }); + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, epoch: '0', version: '0', + treeRootDigest: rootDigest, totalRows: groups.reduce((sum, rows) => sum + rows.length, 0).toString(), + } as const; + return { networkId: NETWORK, descriptor, descriptorDigest: computeSystemRecordRootDescriptorDigestV1(descriptor), objects }; +} + +function heightThreeSnapshot( + internalLeafCounts: readonly number[], + oversizedLeaf?: Readonly<{ internalIndex: number; leafIndex: number }>, +): SystemRecordInventoryTreeSnapshotV1 { + const totalLeaves = internalLeafCounts.reduce((sum, count) => sum + count, 0); + const totalRows = totalLeaves * 128 + (oversizedLeaf === undefined ? 0 : 1); + const sorted = deterministicPeers(totalRows).map(row) + .sort((left, right) => left.stableKeyHash.localeCompare(right.stableKeyHash)); + const objects = new Map(); + let rowOffset = 0; + const internalRefs = internalLeafCounts.map((leafCount, internalIndex) => { + const entries = Array.from({ length: leafCount }, (_, leafIndex) => { + const rowCount = oversizedLeaf?.internalIndex === internalIndex + && oversizedLeaf.leafIndex === leafIndex ? 129 : 128; + const rows = sorted.slice(rowOffset, rowOffset + rowCount); + rowOffset += rowCount; + const leaf = leafFor(rows); + const canonicalBytes = canonicalizeSystemRecordInventoryLeafObjectV1(leaf, NETWORK, false); + const digest = computeSystemRecordInventoryLeafDigestV1(leaf, NETWORK, false); + objects.set(digest, { objectKind: 'inventory-leaf', object: leaf, canonicalBytes }); + return { + separatorKeyHash: leaf.firstKeyHash!, childDigest: digest, childKind: 'inventory-leaf' as const, + }; + }); + const lastLeaf = objects.get(entries.at(-1)!.childDigest)!.object as SystemRecordInventoryLeafObjectV1; + const internal: SystemRecordInventoryInternalObjectV1 = { + objectType: 'inventory-internal', firstKeyHash: entries[0].separatorKeyHash, + lastKeyHash: lastLeaf.lastKeyHash!, entries, + }; + const canonicalBytes = canonicalizeSystemRecordInventoryInternalObjectV1(internal, false); + const digest = computeSystemRecordInventoryInternalDigestV1(internal, false); + objects.set(digest, { objectKind: 'inventory-internal', object: internal, canonicalBytes }); + return { internal, digest }; + }); + const root: SystemRecordInventoryInternalObjectV1 = { + objectType: 'inventory-internal', firstKeyHash: internalRefs[0].internal.firstKeyHash, + lastKeyHash: internalRefs.at(-1)!.internal.lastKeyHash, + entries: internalRefs.map(({ internal, digest }) => ({ + separatorKeyHash: internal.firstKeyHash, childDigest: digest, childKind: 'inventory-internal', + })), + }; + const rootBytes = canonicalizeSystemRecordInventoryInternalObjectV1(root, true); + const rootDigest = computeSystemRecordInventoryInternalDigestV1(root, true); + objects.set(rootDigest, { objectKind: 'inventory-internal', object: root, canonicalBytes: rootBytes }); + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, epoch: '0', version: '0', + treeRootDigest: rootDigest, + totalRows: sorted.length.toString(), + } as const; + return { networkId: NETWORK, descriptor, descriptorDigest: computeSystemRecordRootDescriptorDigestV1(descriptor), objects }; +} + +function firstRowOfInternalLeaf( + snapshot: SystemRecordInventoryTreeSnapshotV1, + internalIndex: number, + leafIndex: number, +): SystemRecordInventoryRowV1 { + const root = snapshot.objects.get(snapshot.descriptor.treeRootDigest)!.object as SystemRecordInventoryInternalObjectV1; + const internal = snapshot.objects.get(root.entries[internalIndex].childDigest)!.object as SystemRecordInventoryInternalObjectV1; + const leaf = snapshot.objects.get(internal.entries[leafIndex].childDigest)!.object as SystemRecordInventoryLeafObjectV1; + return decodeSystemRecordInventoryRowV1(NETWORK, Uint8Array.from(Buffer.from(leaf.rows[0], 'base64url'))); +} + +function rootInternalChildCounts(snapshot: SystemRecordInventoryTreeSnapshotV1): number[] { + const root = snapshot.objects.get(snapshot.descriptor.treeRootDigest)!.object as SystemRecordInventoryInternalObjectV1; + return root.entries.map((entry) => ( + snapshot.objects.get(entry.childDigest)!.object as SystemRecordInventoryInternalObjectV1 + ).entries.length); +} + +function applyUpdate( + snapshot: SystemRecordInventoryTreeSnapshotV1, + update: SystemRecordInventoryCowUpdateV1, +): SystemRecordInventoryTreeSnapshotV1 { + const objects = new Map(snapshot.objects); + for (const write of update.writes) { + objects.set(write.digest, { + objectKind: write.objectKind, + object: write.object, + canonicalBytes: write.canonicalBytes, + }); + } + return { + networkId: snapshot.networkId, + descriptor: update.descriptor, + descriptorDigest: update.descriptorDigest, + objects, + }; +} + +function expectCowPublicationArtifacts( + snapshot: SystemRecordInventoryTreeSnapshotV1, + update: SystemRecordInventoryCowUpdateV1, +): void { + expect(update.descriptorDigest).toBe(computeSystemRecordRootDescriptorDigestV1(update.descriptor)); + expect(update.descriptorBytes).toEqual(canonicalizeSystemRecordRootDescriptorObjectV1(update.descriptor)); + expect(update.accounting.encodedBytes).toBe( + update.descriptorBytes!.byteLength + + update.writes.reduce((total, write) => total + write.canonicalBytes.byteLength, 0), + ); + + const overlay = new Map(snapshot.objects); + for (const write of update.writes) { + const root = write.role === 'root'; + const expectedBytes = write.objectKind === 'inventory-leaf' + ? canonicalizeSystemRecordInventoryLeafObjectV1( + write.object as SystemRecordInventoryLeafObjectV1, + snapshot.networkId, + root, + ) + : canonicalizeSystemRecordInventoryInternalObjectV1( + write.object as SystemRecordInventoryInternalObjectV1, + root, + ); + const expectedDigest = write.objectKind === 'inventory-leaf' + ? computeSystemRecordInventoryLeafDigestV1( + write.object as SystemRecordInventoryLeafObjectV1, + snapshot.networkId, + root, + ) + : computeSystemRecordInventoryInternalDigestV1( + write.object as SystemRecordInventoryInternalObjectV1, + root, + ); + expect(write.canonicalBytes).toEqual(expectedBytes); + expect(write.digest).toBe(expectedDigest); + overlay.set(write.digest, write); + } + + const root = overlay.get(update.descriptor.treeRootDigest); + expect(root).toBeDefined(); + for (const write of update.writes) { + if (write.objectKind !== 'inventory-internal') continue; + const internal = write.object as SystemRecordInventoryInternalObjectV1; + for (const entry of internal.entries) { + const child = overlay.get(entry.childDigest); + expect(child?.objectKind).toBe(entry.childKind); + expect(child?.object.firstKeyHash).toBe(entry.separatorKeyHash); + } + expect(internal.lastKeyHash).toBe( + overlay.get(internal.entries.at(-1)!.childDigest)?.object.lastKeyHash, + ); + } +} + +async function validateTree( + descriptor: SystemRecordInventoryTreeSnapshotV1['descriptor'], + load: (digest: `0x${string}`) => Promise< + SystemRecordInventoryLeafObjectV1 | SystemRecordInventoryInternalObjectV1 | undefined + >, + signal?: AbortSignal, +) { + return validateTreeArtifacts(descriptor, async (digest) => { + const object = await load(digest); + if (object === undefined) return undefined; + const root = digest === descriptor.treeRootDigest; + return object.objectType === 'inventory-leaf' + ? { + objectKind: 'inventory-leaf' as const, + canonicalBytes: canonicalizeSystemRecordInventoryLeafObjectV1(object, NETWORK, root), + } + : { + objectKind: 'inventory-internal' as const, + canonicalBytes: canonicalizeSystemRecordInventoryInternalObjectV1(object, root), + }; + }, signal); +} + +async function validateSnapshotTree(snapshot: SystemRecordInventoryTreeSnapshotV1) { + return validateTreeArtifacts(snapshot.descriptor, async (digest) => snapshot.objects.get(digest)); +} + +async function validateTreeArtifacts( + descriptor: SystemRecordInventoryTreeSnapshotV1['descriptor'], + load: (digest: `0x${string}`) => Promise, + signal?: AbortSignal, +) { + const traversal = createSystemRecordInventoryTraversalV1(descriptor); + while (true) { + const result = await traversal.advance(async (digest) => { + const stored = await load(digest); + return stored === undefined + ? undefined + : loadedInventoryObject(stored); + }, { + signal, + maxRequests: 12, + maxWireBytes: 2 * 1024 * 1024, + deadlineMs: 3_000, + nowMs: () => 0, + }); + if (result.status === 'complete') return result.result!; + } +} + +function loadedInventoryObject(stored: SystemRecordInventoryStoredObjectV1) { + return { + outcome: 'ok' as const, + objectKind: stored.objectKind, + canonicalBytes: stored.canonicalBytes, + wireBytes: 4 + 128 + stored.canonicalBytes.byteLength, + }; +} diff --git a/packages/core/test/system-record-limits-v1.test.ts b/packages/core/test/system-record-limits-v1.test.ts new file mode 100644 index 0000000000..5fa8ea9622 --- /dev/null +++ b/packages/core/test/system-record-limits-v1.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; + +import * as limits from '../src/system-record-limits-v1.js'; + +describe('frozen system-record V1 limits', () => { + it('pins transport, object, tree, closure, and applied-state ceilings', () => { + expect(limits.SYSTEM_RECORD_PROTOCOL_V1).toBe('/dkg/system-records/1.0.0'); + expect(limits.SYSTEM_RECORD_MAX_HEADER_BYTES).toBe(8 * 1024); + expect(limits.SYSTEM_RECORD_MAX_FRAME_PAYLOAD_BYTES).toBe(1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_FRAME_BYTES).toBe(1_056_772); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_PATH_DEPTH).toBe(2); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_CHILD_INDEX).toBe(255); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_RECORDS).toBe(262_144); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_LEAVES).toBe(2_048); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_OBJECTS).toBe(2_065); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_RECORDS).toBe( + limits.SYSTEM_RECORD_MAX_INVENTORY_LEAVES * limits.SYSTEM_RECORD_LEAF_MIN_ROWS, + ); + expect(limits.SYSTEM_RECORD_MAX_INVENTORY_OBJECTS).toBe( + limits.SYSTEM_RECORD_MAX_INVENTORY_LEAVES + + Math.ceil(limits.SYSTEM_RECORD_MAX_INVENTORY_LEAVES / limits.SYSTEM_RECORD_INTERNAL_MIN_ENTRIES) + + 1, + ); + expect(limits.SYSTEM_RECORD_MAX_TREE_HEIGHT).toBe(3); + expect(limits.SYSTEM_RECORD_MAX_TREE_UPDATE_OBJECTS).toBe(6); + expect(limits.SYSTEM_RECORD_MAX_TREE_UPDATE_BYTES).toBe(1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_CLOSURE_OBJECTS).toBe(32); + expect(limits.SYSTEM_RECORD_MAX_CLOSURE_BYTES).toBe(3 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_FRAME_BYTES).toBe( + 4 + limits.SYSTEM_RECORD_MAX_HEADER_BYTES + limits.SYSTEM_RECORD_MAX_FRAME_PAYLOAD_BYTES, + ); + expect(limits.SYSTEM_RECORD_MAX_APPLIED_STATE_BYTES).toBe(64 * 1024); + expect(limits.SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_BYTES).toBe(512 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_APPLIED_AGGREGATE_QUADS).toBe(5_000_000); + }); + + it('pins aggregate cache, activation, runtime, continuation, and journal ceilings', () => { + expect(limits.SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_OBJECTS).toBe(25_000); + expect(limits.SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_BYTES).toBe(1024 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES).toBe(262_144); + expect(limits.SYSTEM_RECORD_MAX_CONFLICT_SIDECARS).toBe(1_024); + expect(limits.SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_REFERENCES).toBe(17_408); + expect(limits.SYSTEM_RECORD_MAX_CONFLICT_SIDECAR_AGGREGATE_BYTES).toBe(128 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_ACTIVATION_RECORDS).toBe(512); + expect(limits.SYSTEM_RECORD_MAX_ACTIVATION_BUNDLE_BYTES).toBe(128 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_ACTIVATION_CLOSURE_BYTES).toBe(256 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_ACTIVATION_REFERENCES).toBe(32_768); + expect(limits.SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES).toBe(64 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_OBJECT_CACHE_BYTES).toBe(2 * 1024 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_OBJECT_CACHE_OBJECTS).toBe(50_000); + expect(limits.SYSTEM_RECORD_MAX_CONTINUATION_SLICES).toBe(512); + expect(limits.SYSTEM_RECORD_CONTINUATION_TIMEOUT_MS).toBe(30 * 60 * 1_000); + expect(limits.SYSTEM_RECORD_PROVIDER_EXCHANGE_TIMEOUT_MS).toBe(3_000); + expect(limits.SYSTEM_RECORD_MAX_PUBLICATION_JOURNAL_REFERENCES).toBe(110); + expect(limits.SYSTEM_RECORD_MAX_PUBLICATION_JOURNAL_BYTES).toBe(64 * 1024); + expect(limits.SYSTEM_RECORD_MAX_CACHE_LIVE_METADATA_BYTES).toBe(32 * 1024 * 1024); + expect(limits.SYSTEM_RECORD_MAX_CACHE_RESERVE_METADATA_BYTES).toBe(32 * 1024 * 1024); + }); +}); diff --git a/packages/core/test/system-record-objects-v1.test.ts b/packages/core/test/system-record-objects-v1.test.ts new file mode 100644 index 0000000000..e3d25738be --- /dev/null +++ b/packages/core/test/system-record-objects-v1.test.ts @@ -0,0 +1,1893 @@ +import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'; +import { createHash } from 'node:crypto'; +import { peerIdFromPublicKey } from '@libp2p/peer-id'; +import { signAsync as signEd25519 } from '@noble/ed25519'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { keccak256 } from '../src/crypto/keccak.js'; +import { describe, expect, it } from 'vitest'; + +import { + EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + assertCanonicalEip191SignatureV1, + assertCanonicalRfc3339SecondsV1, + assertAgentRootV1, + assertAgentProfileForkResolutionEvidenceV1, + assertOwnedSubjectTableObjectV1, + assertDerivedAgentEncryptionSubjectV1, + assertSystemRecordPeerBindingV1, + assertSystemRecordClosureAlgebraV1, + buildAgentProfileVerificationClosureV1, + buildSystemRecordSignatureMessageV1, + classifyAgentProfileOwnedSubjectV1, + canonicalizeAgentProfileAuthorityTransitionV1, + canonicalizeAgentProfileConflictEvidenceV1, + canonicalizeAgentProfileForkResolutionV1, + canonicalizeAgentProfileHeadObjectV1, + canonicalizeOwnedSubjectTableObjectV1, + canonicalizeSystemRecordRootCollisionEvidenceV1, + canonicalizeSignedSystemRecordEnvelopeV1, + computeAgentProfileAuthorityTransitionDigestV1, + computeAgentProfileForkResolutionDigestV1, + computeAgentProfileHeadObjectDigestV1, + computeOwnedSubjectTableDigestV1, + computeSignedSystemRecordEnvelopeDigestV1, + createSystemRecordCacheMetadataV1, + createSystemRecordCacheReferenceV1, + digestSystemRecordBytesV1, + eip191PersonalMessageHashV1, + evaluateAuthorityTransitionV1, + evaluateAuthorityTransitionAgainstAcceptedStateV1, + evaluateAgentProfileHeadAdvanceV1, + preflightSystemRecordCacheAccountingV1, + recoverEip191SignerV1, + parseCanonicalAgentProfileAuthorityTransitionV1, + parseCanonicalAgentProfileConflictEvidenceV1, + parseCanonicalAgentProfileForkResolutionV1, + parseCanonicalAgentProfileHeadObjectV1, + parseCanonicalSignedAgentProfileHeadEnvelopeV1, + verifySignedSystemRecordEnvelopeV1, + type AgentProfileActiveHeadObjectV1, + type AgentProfileAuthorityTransitionV1, + type AgentProfileForkResolutionV1, + type AgentProfileHeadObjectV1, + type SignedAgentProfileHeadEnvelopeV1, + type SystemRecordCacheRowAccountingV1, + type SystemRecordSignatureEntryV1, + type SignedSystemRecordEnvelopeV1, +} from '../src/system-record-objects-v1.js'; +import { + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_MAX_ACTIVATION_BUNDLE_BYTES, + SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES, + SYSTEM_RECORD_MAX_ATOMIC_BUNDLE_BYTES, + SYSTEM_RECORD_MAX_CONFLICT_SIDECARS, +} from '../src/system-record-limits-v1.js'; +import { + canonicalizeSignedSystemRecordRootDescriptorEnvelopeV1, + computeSystemRecordRootDescriptorDigestV1, +} from '../src/system-record-inventory-v1.js'; +import { verifySystemRecordResponsePayloadV1 } from '../src/system-record-wire-v1.js'; + +const DIGEST_A = `0x${'aa'.repeat(32)}` as const; +const DIGEST_B = `0x${'bb'.repeat(32)}` as const; +const DIGEST_C = `0x${'cc'.repeat(32)}` as const; +const NETWORK = 'otp:20430' as const; +const CLOSURE_BUNDLE = new TextEncoder().encode('adversarial-closure-bundle'); +const CLOSURE_BUNDLE_DIGEST = digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + CLOSURE_BUNDLE, +); + +describe('system-record V1 object codecs', () => { + it('round-trips exact active/tombstone, transition, fork, and conflict variants', async () => { + const fixture = await authorityFixture(); + const active = activeHead(fixture); + const parsedActive = parseCanonicalAgentProfileHeadObjectV1( + canonicalizeAgentProfileHeadObjectV1(active), + ); + expect(parsedActive).toEqual(active); + expect(Object.isFrozen(parsedActive.graphScopedAuthorSeal)).toBe(true); + + const tombstone: AgentProfileHeadObjectV1 = { + objectType: 'agent-profile-head', kind: 'agents', state: 'tombstone', + networkId: NETWORK, peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + authoritySequence: '0', version: '1', + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(active), + evmIssuer: fixture.evmIssuer, rootSubject: fixture.root, + projectionSchemaDigest: DIGEST_C, issuedAt: '2026-08-05T12:10:00Z', + ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + ownedSubjectCount: '0', projectionBytes: '0', projectionQuads: '0', + }; + expect(parseCanonicalAgentProfileHeadObjectV1(canonicalizeAgentProfileHeadObjectV1(tombstone))) + .toEqual(tombstone); + + const transition = authorityTransition(fixture, active); + expect(parseCanonicalAgentProfileAuthorityTransitionV1( + canonicalizeAgentProfileAuthorityTransitionV1(transition), + )).toEqual(transition); + + const fork = forkResolution(fixture, active); + const parsedFork = parseCanonicalAgentProfileForkResolutionV1( + canonicalizeAgentProfileForkResolutionV1(fork), + ); + expect(parsedFork).toEqual(fork); + expect(Object.isFrozen(parsedFork.evidenceHeadDigests)).toBe(true); + + const evidence = { + objectType: 'conflict-evidence', kind: 'agents', networkId: NETWORK, + peerId: fixture.peerId, + entries: [ + { type: 'fork', authoritySequence: '0', version: '0', objectDigests: [DIGEST_A, DIGEST_B] }, + { type: 'transition', priorAuthoritySequence: '0', nextAuthoritySequence: '1', objectDigests: [DIGEST_B, DIGEST_C] }, + ], + } as const; + const parsedEvidence = parseCanonicalAgentProfileConflictEvidenceV1( + canonicalizeAgentProfileConflictEvidenceV1(evidence), + ); + expect(parsedEvidence).toEqual(evidence); + expect(parsedEvidence.entries.every((entry) => Object.isFrozen(entry.objectDigests))).toBe(true); + const misleadingEntries = Object.assign([...evidence.entries], { map: () => [] }); + expect(() => canonicalizeAgentProfileConflictEvidenceV1({ + ...evidence, + entries: misleadingEntries, + } as unknown as typeof evidence)).toThrow(/closed|non-index/); + const digestIterator = [...evidence.entries[0].objectDigests] as string[]; + Object.defineProperty(digestIterator, Symbol.iterator, { + value: function* () { yield DIGEST_A; }, + }); + expect(() => canonicalizeAgentProfileConflictEvidenceV1({ + ...evidence, + entries: [{ ...evidence.entries[0], objectDigests: digestIterator }, evidence.entries[1]], + } as unknown as typeof evidence)).toThrow(/closed digests/); + }); + + it('rejects null optionals, wrong peer-key binding, unsafe timestamps, and high-s signatures', async () => { + const fixture = await authorityFixture(); + const active = activeHead(fixture); + expect(() => canonicalizeAgentProfileHeadObjectV1({ + ...active, + previousHeadDigest: null, + } as unknown as AgentProfileHeadObjectV1)).toThrow(/omit optional fields/); + expect(() => canonicalizeAgentProfileHeadObjectV1({ + ...active, + peerPublicKey: Buffer.alloc(32, 7).toString('base64url'), + })).toThrow(/does not derive peerId/); + expect(() => assertCanonicalRfc3339SecondsV1('2026-08-05T12:00:00.000Z')).toThrow(); + expect(() => assertAgentRootV1(`did:dkg:agent:0x${'0'.repeat(40)}`)).toThrow(/address/); + const highS = `0x${'01'.repeat(32)}${'ff'.repeat(32)}1b`; + expect(() => assertCanonicalEip191SignatureV1(highS)).toThrow(/low-s/); + expect(() => assertSystemRecordPeerBindingV1( + 'x'.repeat(257), + fixture.peerPublicKey, + )).toThrow(/byte bound/); + expect(classifyAgentProfileOwnedSubjectV1( + fixture.root, + `${fixture.root}/.well-known/genid/cap${'9'.repeat(256 * 1024)}`, + )).toBeNull(); + expect(() => assertOwnedSubjectTableObjectV1(fixture.root, [ + `${fixture.root}/.well-known/genid/cap1${'1'.repeat(140_000)}`, + `${fixture.root}/.well-known/genid/cap2${'2'.repeat(140_000)}`, + ])).toThrow(/byte cap/); + }); + + it('snapshots exact root-collision evidence and its incumbent tuple', async () => { + const fixture = await authorityFixture(); + const evidence = { + networkId: NETWORK, + root: fixture.root, + incumbentRecordKey: [NETWORK, fixture.peerId] as const, + contenderStableKey: DIGEST_A, + contenderHeadDigest: DIGEST_B, + }; + expect(canonicalizeSystemRecordRootCollisionEvidenceV1(evidence).byteLength).toBeGreaterThan(0); + const accessor = Object.defineProperty({ ...evidence }, 'networkId', { + enumerable: true, + get: () => NETWORK, + }); + expect(() => canonicalizeSystemRecordRootCollisionEvidenceV1(accessor as typeof evidence)) + .toThrow(/data properties/); + const tupleAccessor = Object.defineProperty([NETWORK, fixture.peerId], '0', { + enumerable: true, + get: () => NETWORK, + }); + expect(() => canonicalizeSystemRecordRootCollisionEvidenceV1({ + ...evidence, + incumbentRecordKey: tupleAccessor as unknown as typeof evidence.incumbentRecordKey, + })).toThrow(/closed two-item tuple/); + }); + + it('binds active head content/count/table fields to its public graph-scoped seal', async () => { + const fixture = await authorityFixture(); + const active = activeHead(fixture); + expect(() => canonicalizeAgentProfileHeadObjectV1({ ...active, contentDigest: DIGEST_C })) + .toThrow(/Merkle root/); + expect(() => canonicalizeAgentProfileHeadObjectV1({ ...active, projectionQuads: '4' })) + .toThrow(/public triple count/); + expect(() => canonicalizeAgentProfileHeadObjectV1({ + ...active, + graphScopedAuthorSeal: { + ...active.graphScopedAuthorSeal, + privateTripleCount: '1', + privateMerkleRoot: DIGEST_C, + }, + })).toThrow(/public-only/); + expect(() => canonicalizeAgentProfileHeadObjectV1({ + ...active, + graphScopedAuthorSeal: { + ...active.graphScopedAuthorSeal, + kaUal: `did:dkg:base:8453/${fixture.evmIssuer}/7`, + }, + })).toThrow(/UAL network/); + expect(() => canonicalizeAgentProfileHeadObjectV1({ + ...active, + graphScopedAuthorSeal: { + ...active.graphScopedAuthorSeal, + assertedAtChainId: '8453', + }, + })).toThrow(/asserted chain/); + expect(() => canonicalizeAgentProfileHeadObjectV1({ + ...active, + ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + })).toThrow(/nonempty/); + }); + + it('cryptographically verifies peer and EIP-191 role signatures', async () => { + const fixture = await authorityFixture(); + const object = activeHead(fixture); + const objectDigest = computeAgentProfileHeadObjectDigestV1(object); + const peerMessage = buildSystemRecordSignatureMessageV1(object, objectDigest, 'peer'); + const evmMessage = buildSystemRecordSignatureMessageV1(object, objectDigest, 'current-evm'); + const hostileObject = new Proxy(object, { + get(target, property, receiver) { + if (property === 'networkId') return 'otp:9999'; + return Reflect.get(target, property, receiver); + }, + }); + expect(buildSystemRecordSignatureMessageV1(hostileObject, objectDigest, 'peer')) + .toEqual(peerMessage); + class MisleadingMessage extends Uint8Array { + override get byteLength(): number { return 1; } + override subarray(): Uint8Array { return new Uint8Array(); } + } + expect(eip191PersonalMessageHashV1(new MisleadingMessage(evmMessage))) + .toEqual(eip191PersonalMessageHashV1(evmMessage)); + const peerSignature = Buffer.from( + await signEd25519(peerMessage, fixture.peerSecretSeed), + ).toString('base64url'); + const evmSignature = signEip191(evmMessage, fixture.evmPrivateKey); + const envelope: SignedAgentProfileHeadEnvelopeV1 = { + object, + objectDigest, + signatures: [ + { role: 'peer', suite: 'ed25519-v1', signer: fixture.peerId, evidence: { kind: 'none' }, signature: peerSignature }, + { role: 'current-evm', suite: 'eip191-personal-sign-digest-v1', signer: fixture.evmIssuer, evidence: { kind: 'none' }, signature: evmSignature }, + ], + }; + expect(await verifySignedSystemRecordEnvelopeV1(envelope)).toBe(true); + const personalHash = eip191PersonalMessageHashV1(evmMessage); + expect(recoverEip191SignerV1(evmSignature, new MisleadingMessage(personalHash))) + .toBe(fixture.evmIssuer); + const misleadingSignatures = Object.assign([...envelope.signatures], { map: () => [] }); + expect(() => canonicalizeSignedSystemRecordEnvelopeV1({ + ...envelope, + signatures: misleadingSignatures, + } as unknown as SignedAgentProfileHeadEnvelopeV1)).toThrow(/closed|non-index/); + let objectAccessorCalls = 0; + const accessorEnvelope = Object.defineProperty({ + objectDigest, + signatures: envelope.signatures, + }, 'object', { + enumerable: true, + get: () => { + objectAccessorCalls += 1; + return object; + }, + }); + expect(() => canonicalizeSignedSystemRecordEnvelopeV1( + accessorEnvelope as unknown as SignedAgentProfileHeadEnvelopeV1, + )).toThrow(/data properties/); + expect(objectAccessorCalls).toBe(0); + const tampered = { + ...envelope, + signatures: [envelope.signatures[0], { ...envelope.signatures[1], signature: signEip191(peerMessage, fixture.evmPrivateKey) }], + } as SignedAgentProfileHeadEnvelopeV1; + expect(await verifySignedSystemRecordEnvelopeV1(tampered)).toBe(false); + }); + + it('snapshots EIP-1271 evidence before awaited authority verification', async () => { + const fixture = await authorityFixture(); + const object = activeHead(fixture); + const objectDigest = computeAgentProfileHeadObjectDigestV1(object); + const evidence = { + kind: 'eip1271-current-finalized' as const, + chainId: '20430' as const, + contractAddress: fixture.evmIssuer, + finalizedBlockNumber: '1' as const, + finalizedBlockHash: DIGEST_A, + }; + const envelope: SignedAgentProfileHeadEnvelopeV1 = { + object, + objectDigest, + signatures: [ + { + role: 'peer', suite: 'ed25519-v1', signer: fixture.peerId, + evidence: { kind: 'none' }, + signature: Buffer.from(await signEd25519( + buildSystemRecordSignatureMessageV1(object, objectDigest, 'peer'), + fixture.peerSecretSeed, + )).toString('base64url'), + }, + { + role: 'current-evm', suite: 'eip1271-current-finalized-v1', + signer: fixture.evmIssuer, evidence, signature: '0x01', + }, + ], + }; + const mutatedAddress = '0x5555555555555555555555555555555555555555'; + let observedAddress: string | undefined; + const verification = verifySignedSystemRecordEnvelopeV1(envelope, { + verifyEip1271: (entry) => { + if (entry.evidence.kind !== 'eip1271-current-finalized') return false; + observedAddress = entry.evidence.contractAddress; + return entry.evidence.contractAddress === mutatedAddress; + }, + }); + (evidence as { contractAddress: string }).contractAddress = mutatedAddress; + + expect(await verification).toBe(false); + expect(observedAddress).toBe(fixture.evmIssuer); + (evidence as { contractAddress: string }).contractAddress = fixture.evmIssuer; + + const mutableOptions = { + verifyEip1271: () => false, + }; + const pinnedVerification = verifySignedSystemRecordEnvelopeV1(envelope, mutableOptions); + mutableOptions.verifyEip1271 = () => true; + expect(await pinnedVerification).toBe(false); + expect(await verifySignedSystemRecordEnvelopeV1(envelope, { + verifyEip1271: (() => ({ valid: false })) as never, + })).toBe(false); + }); + + it('enforces transition expiry and direct successor fork decisions', async () => { + const fixture = await authorityFixture(); + const current = activeHead(fixture); + const transition = authorityTransition(fixture, current, 'expired-prior'); + expect(evaluateAuthorityTransitionV1(transition, current, Date.parse(current.validUntil))) + .toMatchObject({ decision: 'reject' }); + expect(evaluateAuthorityTransitionV1( + transition, + current, + Date.parse(transition.issuedAt) + 5 * 60_000, + )).toEqual({ decision: 'accept' }); + const conflicting = { ...current, bundleDigest: DIGEST_C }; + const fork = forkResolution(fixture, current, [current, conflicting]); + const successor: AgentProfileActiveHeadObjectV1 = { + ...current, + version: '3', + forkResolutionDigest: computeAgentProfileForkResolutionDigestV1(fork), + }; + expect(evaluateAgentProfileHeadAdvanceV1( + { current, disposition: 'head-fork-quarantined', transitionLineage: [], historicalRoots: [], frontierConflictHeads: [current, conflicting] }, + successor, + { nowMs: Date.parse('2026-08-05T12:10:00Z'), forkResolution: fork, forkEvidenceHeads: [current, conflicting] }, + )).toEqual({ decision: 'accept' }); + const omittedLocalConflict = { ...current, bundleDigest: `0x${'dd'.repeat(32)}` as const }; + expect(evaluateAgentProfileHeadAdvanceV1( + { + current, + disposition: 'head-fork-quarantined', + transitionLineage: [], + historicalRoots: [], + frontierConflictHeads: [current, conflicting, omittedLocalConflict], + }, + successor, + { nowMs: Date.parse('2026-08-05T12:10:00Z'), forkResolution: fork, forkEvidenceHeads: [current, conflicting] }, + )).toEqual({ decision: 'accept' }); + expect(evaluateAgentProfileHeadAdvanceV1( + { current, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + conflicting, + { nowMs: Date.parse('2026-08-05T12:10:00Z') }, + )) + .toEqual({ decision: 'quarantine', reason: 'head-fork' }); + }); + + it('supports version-zero forks after rotation without changing transition lineage', async () => { + const fixture = await authorityFixture(); + const current = activeHead(fixture); + const transition = authorityTransition(fixture, current); + const transitionDigest = computeAgentProfileAuthorityTransitionDigestV1(transition); + const left = activeForIssuer(current, transition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: transitionDigest, + }); + const right = { ...left, bundleDigest: DIGEST_C }; + expect(() => canonicalizeAgentProfileHeadObjectV1(left)).not.toThrow(); + expect(() => canonicalizeAgentProfileHeadObjectV1(right)).not.toThrow(); + const resolution: AgentProfileForkResolutionV1 = { + objectType: 'fork-resolution', kind: 'agents', networkId: NETWORK, + peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + evmIssuer: transition.nextEvmIssuer, authoritySequence: '1', forkedVersion: '0', + resolutionVersion: '1', evidenceHeadDigests: [left, right].map(computeAgentProfileHeadObjectDigestV1).sort(), + issuedAt: '2026-08-05T12:05:00Z', + }; + expect(() => assertAgentProfileForkResolutionEvidenceV1(resolution, [right, left])).not.toThrow(); + const misleadingHeads = Object.assign([right, left], { map: () => [] }); + expect(() => assertAgentProfileForkResolutionEvidenceV1( + resolution, + misleadingHeads, + )).toThrow(/closed|non-index/); + const altered = { ...left, acceptedTransitionDigest: DIGEST_C }; + expect(() => assertAgentProfileForkResolutionEvidenceV1( + { + ...resolution, + evidenceHeadDigests: [right, altered].map(computeAgentProfileHeadObjectDigestV1).sort(), + }, + [right, altered], + )).toThrow(/transition lineage/); + const alternateTransition = { ...transition, issuedAt: '2026-08-05T12:00:01Z' }; + const alternateDigest = computeAgentProfileAuthorityTransitionDigestV1(alternateTransition); + const alternateLeft = { ...left, acceptedTransitionDigest: alternateDigest }; + const alternateRight = { ...alternateLeft, bundleDigest: DIGEST_C }; + const alternateResolution: AgentProfileForkResolutionV1 = { + ...resolution, + evidenceHeadDigests: [alternateLeft, alternateRight] + .map(computeAgentProfileHeadObjectDigestV1) + .sort(), + }; + const wrongLineageSuccessor = { + ...left, + version: '2' as const, + forkResolutionDigest: computeAgentProfileForkResolutionDigestV1(alternateResolution), + }; + expect(evaluateAgentProfileHeadAdvanceV1( + { + current: left, + disposition: 'head-fork-quarantined', + transitionLineage: [{ + priorAuthoritySequence: '0', nextAuthoritySequence: '1', transitionDigest, + }], + historicalRoots: [current.rootSubject], + }, + wrongLineageSuccessor, + { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + forkResolution: alternateResolution, + forkEvidenceHeads: [alternateLeft, alternateRight], + }, + )).toEqual({ decision: 'quarantine', reason: 'transition-equivocation' }); + const tooMany = Array.from({ length: 17 }, (_, index) => ({ + ...left, + bundleDigest: `0x${index.toString(16).padStart(64, '0')}` as const, + })); + expect(() => canonicalizeAgentProfileForkResolutionV1({ + ...resolution, + evidenceHeadDigests: tooMany.map(computeAgentProfileHeadObjectDigestV1).sort(), + })).toThrow(/2-16/); + }); + + it('rejects unproven lineage, wrong tombstones, resurrection, and late transition equivocation', async () => { + const fixture = await authorityFixture(); + const current = activeHead(fixture); + const transition = authorityTransition(fixture, current); + const next = activeForIssuer( + current, + transition.nextEvmIssuer, + '1', + '0', + { acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(transition) }, + ); + const state = { current, disposition: 'discoverable' as const, transitionLineage: [], historicalRoots: [] }; + expect(() => evaluateAgentProfileHeadAdvanceV1( + { ...state, disposition: 'unknown' } as never, + current, + { nowMs: Date.parse('2026-08-08T00:00:00Z') }, + )).toThrow(/disposition/); + expect(() => evaluateAuthorityTransitionAgainstAcceptedStateV1( + { ...state, disposition: 'unknown' } as never, + transition, + Date.parse('2026-08-08T00:00:00Z'), + )).toThrow(/disposition/); + expect(evaluateAgentProfileHeadAdvanceV1(state, next, { + nowMs: Date.parse('2026-08-08T00:00:00Z'), + })).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/verified active predecessor|exact accepted/) }); + expect(evaluateAgentProfileHeadAdvanceV1(state, next, { + nowMs: Date.parse('2026-08-08T00:00:00Z'), acceptedTransition: transition, + })).toEqual({ decision: 'accept' }); + + const tombstone: AgentProfileHeadObjectV1 = { + objectType: 'agent-profile-head', kind: 'agents', state: 'tombstone', + networkId: NETWORK, peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + authoritySequence: '0', version: '1', previousHeadDigest: DIGEST_C, + evmIssuer: fixture.evmIssuer, rootSubject: fixture.root, projectionSchemaDigest: DIGEST_C, + issuedAt: '2026-08-05T12:10:00Z', ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + ownedSubjectCount: '0', projectionBytes: '0', projectionQuads: '0', + }; + expect(evaluateAgentProfileHeadAdvanceV1(state, tombstone, { + nowMs: Date.parse('2026-08-05T12:11:00Z'), + })).toMatchObject({ + decision: 'reject', + reason: expect.stringMatching(/verified active predecessor|exact accepted/), + }); + const validTombstone = { ...tombstone, previousHeadDigest: computeAgentProfileHeadObjectDigestV1(current) }; + const resurrected = { + ...current, + version: '2' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(validTombstone), + issuedAt: '2026-08-05T12:15:00Z' as const, + }; + expect(evaluateAgentProfileHeadAdvanceV1( + { current: validTombstone, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + resurrected, + { nowMs: Date.parse('2026-08-05T12:20:00Z') }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/terminal/) }); + + const alternate = { ...transition, issuedAt: '2026-08-07T12:00:01Z' }; + expect(evaluateAuthorityTransitionAgainstAcceptedStateV1({ + current: next, + disposition: 'discoverable', + transitionLineage: [{ + priorAuthoritySequence: '0', nextAuthoritySequence: '1', + transitionDigest: computeAgentProfileAuthorityTransitionDigestV1(transition), + }], + historicalRoots: [current.rootSubject], + }, alternate, Date.parse('2026-08-08T00:00:00Z'))) + .toEqual({ decision: 'quarantine', reason: 'transition-equivocation' }); + }); + + it('rejects foreign records before honoring a retained transition quarantine', async () => { + const fixture = await authorityFixture(); + const other = await authorityFixture(17); + const current = activeHead(fixture); + const foreignHead = activeHead(other); + const foreignTransition = authorityTransition(other, foreignHead); + const quarantined = { + current, + disposition: 'transition-equivocation-quarantined' as const, + transitionLineage: [], + historicalRoots: [], + }; + + expect(evaluateAgentProfileHeadAdvanceV1( + quarantined, + foreignHead, + { nowMs: Date.parse('2026-08-05T12:10:00Z') }, + )).toEqual({ decision: 'reject', reason: 'stable record key changed' }); + expect(evaluateAuthorityTransitionAgainstAcceptedStateV1( + quarantined, + foreignTransition, + Date.parse('2026-08-08T00:00:00Z'), + )).toEqual({ decision: 'reject', reason: 'stable record key changed' }); + }); + + it('makes a verified tombstone dominant regardless of active-head delivery order', async () => { + const fixture = await authorityFixture(); + const initial = activeHead(fixture); + const tombstone = tombstoneHead(initial); + const activeAfterTombstone = { + ...initial, + version: '2' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(tombstone), + issuedAt: '2026-08-05T12:20:00Z' as const, + }; + const nowMs = Date.parse('2026-08-07T12:20:00Z'); + expect(evaluateAgentProfileHeadAdvanceV1( + { current: initial, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + tombstone, + { nowMs }, + )).toEqual({ decision: 'accept' }); + expect(evaluateAgentProfileHeadAdvanceV1( + { current: activeAfterTombstone, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + tombstone, + { nowMs, tombstonePredecessor: initial }, + )).toEqual({ decision: 'accept' }); + expect(evaluateAgentProfileHeadAdvanceV1( + { current: tombstone, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + activeAfterTombstone, + { nowMs }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/terminal/) }); + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + tombstone, + { nowMs, tombstonePredecessor: initial }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/verified authority closure/) }); + + const competingActive = { + ...initial, + version: '1' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(initial), + issuedAt: '2026-08-05T12:15:00Z' as const, + }; + expect(evaluateAgentProfileHeadAdvanceV1( + { current: competingActive, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + tombstone, + { nowMs, tombstonePredecessor: initial }, + )).toEqual({ decision: 'accept' }); + + const higherVersionTombstone = { + ...tombstone, + version: '2' as const, + issuedAt: '2026-08-07T11:55:00Z' as const, + }; + expect(evaluateAgentProfileHeadAdvanceV1( + { current: higherVersionTombstone, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + tombstone, + { nowMs, tombstonePredecessor: initial }, + )).toEqual({ decision: 'accept' }); + expect(evaluateAgentProfileHeadAdvanceV1( + { current: tombstone, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + higherVersionTombstone, + { nowMs, tombstonePredecessor: initial }, + )).toEqual({ decision: 'stale' }); + }); + + it('rejects tombstone fork evidence and reused or incomplete authority roots', async () => { + const fixture = await authorityFixture(); + const initial = activeHead(fixture); + const tombstone = tombstoneHead(initial); + const competing = { + ...initial, + version: '1' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(initial), + bundleDigest: DIGEST_C, + }; + const tombstoneFork: AgentProfileForkResolutionV1 = { + objectType: 'fork-resolution', kind: 'agents', networkId: NETWORK, + peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + evmIssuer: fixture.evmIssuer, authoritySequence: '0', forkedVersion: '1', + resolutionVersion: '2', forkBaseHeadDigest: computeAgentProfileHeadObjectDigestV1(initial), + evidenceHeadDigests: [tombstone, competing].map(computeAgentProfileHeadObjectDigestV1).sort(), + issuedAt: '2026-08-05T12:10:00Z', + }; + expect(() => assertAgentProfileForkResolutionEvidenceV1( + tombstoneFork, [tombstone, competing], initial, + )).toThrow(/tombstone evidence/); + const descendant = { + ...initial, + version: '2' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(tombstone), + issuedAt: '2026-08-05T12:20:00Z' as const, + }; + const otherDescendant = { ...descendant, bundleDigest: DIGEST_C }; + const tombstoneBaseFork: AgentProfileForkResolutionV1 = { + ...tombstoneFork, + forkedVersion: '2', + resolutionVersion: '3', + forkBaseHeadDigest: computeAgentProfileHeadObjectDigestV1(tombstone), + evidenceHeadDigests: [descendant, otherDescendant].map(computeAgentProfileHeadObjectDigestV1).sort(), + }; + expect(() => assertAgentProfileForkResolutionEvidenceV1( + tombstoneBaseFork, [descendant, otherDescendant], tombstone, + )).toThrow(/fork base/); + + const noOpTransition = { + ...authorityTransition(fixture, initial), + nextEvmIssuer: initial.evmIssuer, + nextRoot: initial.rootSubject, + }; + expect(() => canonicalizeAgentProfileAuthorityTransitionV1(noOpTransition)) + .toThrow(/new wallet root/); + + const firstTransition = authorityTransition(fixture, initial); + const firstDigest = computeAgentProfileAuthorityTransitionDigestV1(firstTransition); + const next = activeForIssuer(initial, firstTransition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: firstDigest, + }); + const backTransition = transitionFrom(fixture, next, '2', initial.evmIssuer); + const acceptedNext = { + current: next, + disposition: 'discoverable' as const, + transitionLineage: [{ + priorAuthoritySequence: '0' as const, + nextAuthoritySequence: '1' as const, + transitionDigest: firstDigest, + }], + historicalRoots: [initial.rootSubject], + }; + expect(evaluateAuthorityTransitionAgainstAcceptedStateV1( + acceptedNext, backTransition, Date.parse('2026-08-08T00:00:00Z'), + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/reuses a root/) }); + expect(evaluateAuthorityTransitionAgainstAcceptedStateV1({ + current: next, disposition: 'discoverable', transitionLineage: [], historicalRoots: [], + }, backTransition, Date.parse('2026-08-08T00:00:00Z'))) + .toMatchObject({ decision: 'reject', reason: expect.stringMatching(/incomplete/) }); + + const alternativeTransition = { ...firstTransition, issuedAt: '2026-08-07T12:00:01Z' }; + const alternativeHead = { + ...next, + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(alternativeTransition), + }; + expect(evaluateAgentProfileHeadAdvanceV1( + acceptedNext, + alternativeHead, + { nowMs: Date.parse('2026-08-08T00:00:00Z') }, + )).toEqual({ decision: 'quarantine', reason: 'transition-equivocation' }); + }); + + it('enforces the exact five-minute future boundary and discards historical resolutions', async () => { + const fixture = await authorityFixture(); + const now = Date.parse('2026-08-05T12:00:00Z'); + const atBoundary = { ...activeHead(fixture), issuedAt: '2026-08-05T12:05:00Z' }; + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, atBoundary, { nowMs: now }, + )).toEqual({ decision: 'accept' }); + const beyond = { ...atBoundary, issuedAt: '2026-08-05T12:05:01Z' }; + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, beyond, { nowMs: now }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/future/) }); + + const base = activeHead(fixture); + const current = { ...base, version: '10' as const, previousHeadDigest: DIGEST_C }; + const resolution = forkResolution(fixture, base); + const candidate = { + ...base, + version: '11' as const, + forkResolutionDigest: computeAgentProfileForkResolutionDigestV1(resolution), + }; + expect(evaluateAgentProfileHeadAdvanceV1( + { current, disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + candidate, + { nowMs: Date.parse('2026-08-05T12:10:00Z'), forkResolution: resolution, + forkEvidenceHeads: [base, { ...base, bundleDigest: DIGEST_C }] }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/historical|unsolicited/) }); + }); +}); + +describe('system-record owned subjects and verification closure', () => { + it('pins the empty table and rejects unsorted/external/oversized tables', async () => { + const fixture = await authorityFixture(); + expect(computeOwnedSubjectTableDigestV1(fixture.root, [])).toBe(EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1); + const valid = [fixture.root, `${fixture.root}/.well-known/genid/cap1`]; + assertOwnedSubjectTableObjectV1(fixture.root, valid); + expect(() => assertOwnedSubjectTableObjectV1(fixture.root, [...valid].reverse())).toThrow(/sorted/); + expect(() => assertOwnedSubjectTableObjectV1(fixture.root, ['https://evil.example/x'])).toThrow(); + expect(() => assertOwnedSubjectTableObjectV1(fixture.root, [ + `${'x'.repeat(fixture.root.length)}#x25519-${'a'.repeat(32)}`, + ])).toThrow(/owned/); + const publicKey = new Uint8Array(32).fill(9); + const keyId = `${fixture.root}#x25519-${createHash('sha256').update(publicKey).digest('hex').slice(0, 32)}`; + expect(() => assertDerivedAgentEncryptionSubjectV1(fixture.root, keyId, publicKey)).not.toThrow(); + expect(() => assertDerivedAgentEncryptionSubjectV1( + fixture.root, + `${fixture.root}#x25519-${'0'.repeat(32)}`, + publicKey, + )).toThrow(/not derived/); + class MisleadingPublicKey extends Uint8Array { + override get byteLength(): number { return 32; } + } + const shortKey = new MisleadingPublicKey([9]); + const shortKeyId = `${fixture.root}#x25519-${createHash('sha256') + .update(Uint8Array.of(9)).digest('hex').slice(0, 32)}`; + expect(() => assertDerivedAgentEncryptionSubjectV1( + fixture.root, + shortKeyId, + shortKey, + )).toThrow(/exactly 32 bytes/); + }); + + it('preflights exact closure/sidecar references without trusting caller counters', async () => { + class MisleadingAccountingBytes extends Uint8Array { + override get byteLength(): number { return 0; } + override slice(): Uint8Array { return new Uint8Array(); } + } + const metadata = (byteLength = 0) => createSystemRecordCacheMetadataV1(new Uint8Array(byteLength)); + const emptyMetadata = metadata(); + const references = Array.from({ length: 32 }, (_, index) => { + const canonicalBytes = new Uint8Array(64).fill(index + 1); + return createSystemRecordCacheReferenceV1( + 'profile-bundle', + digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, canonicalBytes), + canonicalBytes, + ); + }); + const accountedBytes = new Uint8Array(64).fill(42); + const accountedDigest = digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + accountedBytes, + ); + const accountedReference = createSystemRecordCacheReferenceV1( + 'profile-bundle', + accountedDigest, + new MisleadingAccountingBytes(accountedBytes), + ); + const accountedMetadata = createSystemRecordCacheMetadataV1( + new MisleadingAccountingBytes(accountedBytes), + ); + expect(preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [{ + closure: [accountedReference], + metadata: accountedMetadata, + }] })).toMatchObject({ cohortPhysicalBytes: 64, metadataBytes: 64 }); + expect(preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [ + { closure: references, metadata: metadata(512) }, + { closure: references, metadata: metadata(512) }, + ] })).toMatchObject({ cohortPhysicalObjects: 32, closureReferences: 64, cohortPhysicalBytes: 2048 }); + const misleadingRows = Object.assign([ + { closure: references, metadata: emptyMetadata }, + ], { map: () => [] }); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: misleadingRows, + })).toThrow(/closed bounds/); + const misleadingClosure = [...references]; + Object.defineProperty(misleadingClosure, Symbol.iterator, { + value: function* () { yield references[0]; }, + }); + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [ + { closure: misleadingClosure, metadata: emptyMetadata }, + ] })).toThrow(/closed bounds/); + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [ + { closure: [...references, references[0]], metadata: metadata(1) }, + ] })).toThrow(/cache row arrays|closed bounds/); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [{ + closure: [{ + objectKind: 'profile-bundle', + digest: references[0].digest, + cacheDigest: references[0].cacheDigest, + }], + metadata: emptyMetadata, + }], + })).toThrow(/not derived from canonical bytes/); + const ReferenceConstructor = Object.getPrototypeOf(references[0]).constructor as new ( + ...args: unknown[] + ) => typeof references[0]; + expect(() => new ReferenceConstructor( + references[0].digest, + references[0].cacheDigest, + references[0].objectKind, + { byteLength: 0, fingerprint: '0'.repeat(64) }, + )).toThrow(/factory-only/); + const forgedReference = Object.assign(Object.create(Object.getPrototypeOf(references[0])), { + digest: references[0].digest, + cacheDigest: references[0].cacheDigest, + objectKind: references[0].objectKind, + }) as typeof references[0]; + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [{ + closure: [forgedReference], metadata: emptyMetadata, + }] })).toThrow(/not derived from canonical bytes/); + let forgedReferenceReads = 0; + const proxiedReference = new Proxy(references[0], { + get(target, property, receiver) { + forgedReferenceReads += 1; + return Reflect.get(target, property, receiver); + }, + }); + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [{ + closure: [proxiedReference], metadata: emptyMetadata, + }] })).toThrow(/not derived from canonical bytes/); + expect(forgedReferenceReads).toBe(0); + const MetadataConstructor = Object.getPrototypeOf(emptyMetadata).constructor as new ( + ...args: unknown[] + ) => typeof emptyMetadata; + expect(() => new MetadataConstructor(0)).toThrow(/factory-only/); + const forgedMetadata = Object.create(Object.getPrototypeOf(emptyMetadata)) as typeof emptyMetadata; + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [{ + closure: [], metadata: forgedMetadata, + }] })).toThrow(/not derived from encoded bytes/); + let forgedMetadataReads = 0; + const proxiedMetadata = new Proxy(emptyMetadata, { + get(target, property, receiver) { + forgedMetadataReads += 1; + return Reflect.get(target, property, receiver); + }, + }); + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [{ + closure: [], metadata: proxiedMetadata, + }] })).toThrow(/not derived from encoded bytes/); + expect(forgedMetadataReads).toBe(0); + const fixture = await authorityFixture(); + const evidenceBytes = canonicalizeAgentProfileConflictEvidenceV1({ + objectType: 'conflict-evidence', kind: 'agents', networkId: NETWORK, + peerId: fixture.peerId, + entries: [{ + type: 'fork', authoritySequence: '0', version: '0', objectDigests: [DIGEST_A, DIGEST_B], + }], + }); + const sharedSidecar = [createSystemRecordCacheReferenceV1( + 'conflict-evidence', + digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.conflictEvidence, evidenceBytes), + evidenceBytes, + )]; + const deduplicated = preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [ + { closure: [], sidecar: sharedSidecar, metadata: emptyMetadata, sidecarMetadata: emptyMetadata }, + { closure: [], sidecar: sharedSidecar, metadata: emptyMetadata, sidecarMetadata: emptyMetadata }, + ] }); + expect(deduplicated.sidecarPhysicalBytes).toBe(evidenceBytes.byteLength); + expect(deduplicated.sidecarReferencedBytes).toBe(evidenceBytes.byteLength * 2); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'activation', + rows: Array.from({ length: 513 }, () => ({ closure: [], metadata: emptyMetadata })), + inventoryLeaves: [], + })).toThrow(/record bound|closed bounds/); + const leaf = (index: number) => { + const bytes = new TextEncoder().encode(`leaf-${index}`); + return createSystemRecordCacheReferenceV1( + 'inventory-leaf', + digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.inventoryLeaf, bytes), + bytes, + ); + }; + const activationLeaf = leaf(0); + expect(preflightSystemRecordCacheAccountingV1({ + mode: 'activation', rows: [], inventoryLeaves: [activationLeaf], + })).toMatchObject({ + activationInventoryLeaves: 1, + cohortPhysicalObjects: 1, + cohortPhysicalBytes: new TextEncoder().encode('leaf-0').byteLength, + }); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'activation', rows: [], inventoryLeaves: Array.from({ length: 5 }, (_, index) => leaf(index)), + })).toThrow(/leaf bound|closed bounds/); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [{ + closure: [], metadata: emptyMetadata, + sidecar: references.slice(0, 1), sidecarMetadata: emptyMetadata, + }], + })).toThrow(/one evidence object/); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [{ + closure: [], metadata: emptyMetadata, + sidecar: undefined, sidecarMetadata: emptyMetadata, + }], + })).toThrow(/sidecar must be an array|cache row arrays/); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [{ closure: [], metadata: emptyMetadata, sidecar: sharedSidecar }], + })).toThrow(/present together/); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [{ closure: [], metadata: {} as unknown as typeof emptyMetadata }], + })).toThrow(/not derived from encoded bytes/); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [{ closure: [], metadata: emptyMetadata, unknown: 1 }], + } as never)).toThrow(/unknown or missing fields/); + + const fullClosureRow = { closure: references, metadata: emptyMetadata }; + const overReferenceCap = Array.from({ + length: Math.floor(SYSTEM_RECORD_MAX_ADVERTISED_CLOSURE_REFERENCES / references.length) + 1, + }, () => fullClosureRow); + const trailingRow = Object.defineProperty( + { metadata: emptyMetadata }, + 'closure', + { + enumerable: true, + get: () => { throw new Error('preflight scanned past the aggregate reference cap'); }, + }, + ) as unknown as SystemRecordCacheRowAccountingV1; + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [...overReferenceCap, trailingRow], + })).toThrow(/aggregate cache accounting exceeds/); + const fullSidecarRow = { + closure: [], sidecar: sharedSidecar, + metadata: emptyMetadata, sidecarMetadata: emptyMetadata, + }; + const overSidecarCap = Array.from({ + length: SYSTEM_RECORD_MAX_CONFLICT_SIDECARS + 1, + }, () => fullSidecarRow); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'live', + rows: [...overSidecarCap, trailingRow], + })).toThrow(/aggregate cache accounting exceeds/); + }); + + it.runIf(process.env.DKG_SYSTEM_RECORD_EXHAUSTIVE === '1')( + 'accepts exactly 128 MiB of activation bundles and rejects one more MiB', () => { + const reusable = new Uint8Array(SYSTEM_RECORD_MAX_ATOMIC_BUNDLE_BYTES); + const references = Array.from({ length: 129 }, (_, index) => { + reusable[0] = index; + const digest = digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, reusable); + return createSystemRecordCacheReferenceV1('profile-bundle', digest, reusable); + }); + const emptyMetadata = createSystemRecordCacheMetadataV1(new Uint8Array()); + const rows = references.map((reference) => ({ closure: [reference], metadata: emptyMetadata })); + expect(preflightSystemRecordCacheAccountingV1({ + mode: 'activation', rows: rows.slice(0, 128), inventoryLeaves: [], + }).activationBundleBytes).toBe(SYSTEM_RECORD_MAX_ACTIVATION_BUNDLE_BYTES); + expect(() => preflightSystemRecordCacheAccountingV1({ + mode: 'activation', rows, inventoryLeaves: [], + })).toThrow(/activation cache accounting/); + }, 30_000, + ); + + it('separates signed semantic identity from exact physical envelope identity', async () => { + const fixture = await authorityFixture(); + const object = activeHead(fixture); + const semanticDigest = computeAgentProfileHeadObjectDigestV1(object); + const firstEnvelope = parseCanonicalSignedAgentProfileHeadEnvelopeV1(fakeEnvelopeBytes(object)); + const secondEnvelope: SignedAgentProfileHeadEnvelopeV1 = { + ...firstEnvelope, + signatures: [ + firstEnvelope.signatures[0], + { ...firstEnvelope.signatures[1], signature: '0x02' }, + ], + }; + const firstBytes = canonicalizeSignedSystemRecordEnvelopeV1(firstEnvelope); + const secondBytes = canonicalizeSignedSystemRecordEnvelopeV1(secondEnvelope); + expect(computeSignedSystemRecordEnvelopeDigestV1(firstEnvelope)) + .not.toBe(computeSignedSystemRecordEnvelopeDigestV1(secondEnvelope)); + + const first = createSystemRecordCacheReferenceV1('agent-profile-head', semanticDigest, firstBytes); + const second = createSystemRecordCacheReferenceV1('agent-profile-head', semanticDigest, secondBytes); + expect(first.digest).toBe(second.digest); + expect(first.cacheDigest).not.toBe(second.cacheDigest); + const metadata = createSystemRecordCacheMetadataV1(new Uint8Array()); + expect(preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [ + { closure: [first], metadata }, + { closure: [second], metadata }, + ] })).toMatchObject({ cohortPhysicalObjects: 2, closureReferences: 2 }); + expect(preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [ + { closure: [first], metadata }, + { closure: [first], metadata }, + ] })).toMatchObject({ cohortPhysicalObjects: 1, closureReferences: 2 }); + expect(() => preflightSystemRecordCacheAccountingV1({ mode: 'live', rows: [{ + closure: [{ ...first, cacheDigest: DIGEST_C } as unknown as typeof first], metadata, + }] })).toThrow(/not derived/); + + const descriptor = { + objectType: 'root-descriptor', kind: 'agents', networkId: NETWORK, + epoch: '0', version: '0', treeRootDigest: DIGEST_A, totalRows: '0', + } as const; + const descriptorDigest = computeSystemRecordRootDescriptorDigestV1(descriptor); + const descriptorBytes = canonicalizeSignedSystemRecordRootDescriptorEnvelopeV1({ + object: descriptor, + objectDigest: descriptorDigest, + providerPeerId: fixture.peerId, + signatureSuite: 'ed25519-v1', + signature: Buffer.alloc(64).toString('base64url'), + }); + const request = { + wireVersion: '1', requestId: '0123456789abcdef0123456789abcdef', + kind: 'agents', networkId: NETWORK, operation: 'get-root', payloadBytes: '0', + } as const; + const response = { + wireVersion: '1', requestId: request.requestId, status: 'ok', + objectKind: 'root-descriptor', objectDigest: descriptorDigest, + payloadBytes: String(descriptorBytes.byteLength), + } as const; + expect(() => verifySystemRecordResponsePayloadV1(request, response, descriptorBytes)).not.toThrow(); + const descriptorReference = createSystemRecordCacheReferenceV1( + 'root-descriptor', descriptorDigest, descriptorBytes, + ); + expect(descriptorReference).toMatchObject({ digest: descriptorDigest }); + expect(descriptorReference.cacheDigest).toBe(digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.signedRootDescriptorEnvelope, + descriptorBytes, + )); + expect(descriptorReference.cacheDigest).not.toBe(descriptorReference.digest); + }); + + it('derives a complete raw-artifact closure and fails closed on a missing dependency', async () => { + const fixture = await authorityFixture(); + const bundle = new TextEncoder().encode('canonical-profile-bundle'); + const object = { + ...activeHead(fixture), + bundleDigest: digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, bundle), + }; + const envelope = await signedHeadEnvelope(fixture, object); + const headBytes = canonicalizeSignedSystemRecordEnvelopeV1(envelope); + const artifacts = new Map([ + [`agent-profile-head:${envelope.objectDigest}`, { + objectKind: 'agent-profile-head' as const, digest: envelope.objectDigest, canonicalBytes: headBytes, + }], + [`profile-bundle:${object.bundleDigest}`, { + objectKind: 'profile-bundle' as const, digest: object.bundleDigest, canonicalBytes: bundle, + }], + ]); + const closure = await buildAgentProfileVerificationClosureV1(envelope.objectDigest, { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference) => artifacts.get(`${reference.objectKind}:${reference.digest}`), + verifyAuthorityEnvelope: (candidate) => verifySignedSystemRecordEnvelopeV1(candidate), + verifyCurrentBundle: (_head, bytes) => Buffer.from(bytes).equals(Buffer.from(bundle)), + }); + expect(closure).toMatchObject({ canonicalBytes: headBytes.byteLength + bundle.byteLength, rootClaims: 1 }); + expect(closure.objects.map((entry) => entry.digest)).toEqual( + [...closure.objects.map((entry) => entry.digest)].sort(), + ); + for (const entry of closure.objects) { + expect(entry.references.map((reference) => reference.digest)).toEqual( + [...entry.references.map((reference) => reference.digest)].sort(), + ); + } + await expect(buildAgentProfileVerificationClosureV1(envelope.objectDigest, { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference) => reference.objectKind === 'agent-profile-head' + ? artifacts.get(`${reference.objectKind}:${reference.digest}`) + : undefined, + verifyAuthorityEnvelope: () => true, + verifyCurrentBundle: () => true, + })).rejects.toThrow(/missing/); + }); + + it('snapshots resolver-owned artifact bytes before awaited closure verification', async () => { + class MisleadingSliceBytes extends Uint8Array { + override slice(): Uint8Array { return new Uint8Array(); } + } + const fixture = await authorityFixture(); + const bundle = new TextEncoder().encode('resolver-owned-profile-bundle'); + const expectedBundleBytes = bundle.slice(); + const object = { + ...activeHead(fixture), + bundleDigest: digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + bundle, + ), + }; + const envelope = await signedHeadEnvelope(fixture, object); + const headBytes = canonicalizeSignedSystemRecordEnvelopeV1(envelope); + const expectedHeadBytes = headBytes.slice(); + const resolverHeadBytes = new MisleadingSliceBytes(headBytes); + const resolverBundleBytes = new MisleadingSliceBytes(bundle); + const artifacts = new Map([ + [`agent-profile-head:${envelope.objectDigest}`, { + objectKind: 'agent-profile-head' as const, + digest: envelope.objectDigest, + canonicalBytes: resolverHeadBytes, + }], + [`profile-bundle:${object.bundleDigest}`, { + objectKind: 'profile-bundle' as const, + digest: object.bundleDigest, + canonicalBytes: resolverBundleBytes, + }], + ]); + let verificationStarted!: () => void; + const started = new Promise((resolve) => { verificationStarted = resolve; }); + let releaseVerification!: () => void; + const release = new Promise((resolve) => { releaseVerification = resolve; }); + const closurePromise = buildAgentProfileVerificationClosureV1(envelope.objectDigest, { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference) => artifacts.get(`${reference.objectKind}:${reference.digest}`), + verifyAuthorityEnvelope: async (candidate) => { + const valid = await verifySignedSystemRecordEnvelopeV1(candidate); + if (candidate.object.objectType === 'agent-profile-head' + && candidate.object.state === 'active') { + expect(Object.isFrozen(candidate.object.graphScopedAuthorSeal)).toBe(true); + } + verificationStarted(); + await release; + return valid; + }, + verifyCurrentBundle: (_head, bytes) => { + bytes.fill(0); + return true; + }, + }); + + await started; + resolverHeadBytes.fill(0); + releaseVerification(); + const closure = await closurePromise; + const retainedHead = closure.objects.find((entry) => entry.objectKind === 'agent-profile-head'); + expect(retainedHead?.canonicalBytes).toEqual(expectedHeadBytes); + const retainedBundle = closure.objects.find((entry) => entry.objectKind === 'profile-bundle'); + expect(retainedBundle?.canonicalBytes).toEqual(expectedBundleBytes); + }); + + it('pins closure capabilities, hides traversal context, and requires exact booleans', async () => { + const fixture = await authorityFixture(); + const bundle = new TextEncoder().encode('pinned-closure-bundle'); + const object = { + ...activeHead(fixture), + bundleDigest: digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + bundle, + ), + }; + const envelope = await signedHeadEnvelope(fixture, object); + const headBytes = canonicalizeSignedSystemRecordEnvelopeV1(envelope); + const artifacts = new Map([ + [`agent-profile-head:${envelope.objectDigest}`, { + objectKind: 'agent-profile-head' as const, + digest: envelope.objectDigest, + canonicalBytes: headBytes, + }], + [`profile-bundle:${object.bundleDigest}`, { + objectKind: 'profile-bundle' as const, + digest: object.bundleDigest, + canonicalBytes: bundle, + }], + ]); + const verifier = { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference: Readonly<{ objectKind: string; digest: string }>) => { + verifier.nowMs = Date.parse('2036-08-05T12:10:00Z'); + verifier.verifyAuthorityEnvelope = () => true; + verifier.verifyCurrentBundle = () => true; + return artifacts.get(`${reference.objectKind}:${reference.digest}`); + }, + verifyAuthorityEnvelope: () => false, + verifyCurrentBundle: () => false, + }; + await expect(buildAgentProfileVerificationClosureV1(envelope.objectDigest, verifier as never)) + .rejects.toThrow(/authority verification failed/); + const bundleVerifier = { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference: Readonly<{ objectKind: string; digest: string }>) => { + bundleVerifier.verifyCurrentBundle = () => true; + return artifacts.get(`${reference.objectKind}:${reference.digest}`); + }, + verifyAuthorityEnvelope: () => true, + verifyCurrentBundle: () => false, + }; + await expect(buildAgentProfileVerificationClosureV1( + envelope.objectDigest, + bundleVerifier as never, + )).rejects.toThrow(/bundle verification failed/); + + const closure = await buildAgentProfileVerificationClosureV1(envelope.objectDigest, { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference) => { + expect(Object.keys(reference).sort()).toEqual(['digest', 'objectKind']); + expect(Object.isFrozen(reference)).toBe(true); + try { + (reference as unknown as { purpose: string }).purpose = 'history'; + } catch { + // Frozen resolver DTOs intentionally reject mutation in strict ESM. + } + return artifacts.get(`${reference.objectKind}:${reference.digest}`); + }, + verifyAuthorityEnvelope: () => true, + verifyCurrentBundle: () => true, + }); + expect(closure.objects.some((candidate) => candidate.objectKind === 'profile-bundle')).toBe(true); + expect(closure.objects.every((candidate) => ( + candidate.references.every((reference) => Object.isFrozen(reference)) + ))).toBe(true); + + await expect(buildAgentProfileVerificationClosureV1(envelope.objectDigest, { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference) => artifacts.get(`${reference.objectKind}:${reference.digest}`), + verifyAuthorityEnvelope: (() => ({ valid: false })) as never, + verifyCurrentBundle: () => true, + })).rejects.toThrow(/authority verification failed/); + await expect(buildAgentProfileVerificationClosureV1(envelope.objectDigest, { + nowMs: Date.parse('2026-08-05T12:10:00Z'), + resolve: async (reference) => artifacts.get(`${reference.objectKind}:${reference.digest}`), + verifyAuthorityEnvelope: () => true, + verifyCurrentBundle: (() => 'yes') as never, + })).rejects.toThrow(/bundle verification failed/); + }); + + it('cryptographically verifies a cold rotated tombstone closure and rejects tampered history', async () => { + const fixture = await authorityFixture(); + const nextAuthority = await authorityFixture(31); + const initial = activeHead(fixture); + const transition = transitionFrom(fixture, initial, '1', nextAuthority.evmIssuer); + const transitionDigest = computeAgentProfileAuthorityTransitionDigestV1(transition); + const rotated = activeForIssuer(initial, nextAuthority.evmIssuer, '1', '0', { + acceptedTransitionDigest: transitionDigest, + }); + const tombstone = tombstoneHead(rotated); + const initialEnvelope = await signedHeadEnvelope(fixture, initial); + const rotatedEnvelope = await signedHeadEnvelope( + fixture, + rotated, + nextAuthority.evmPrivateKey, + ); + const tombstoneEnvelope = await signedHeadEnvelope( + fixture, + tombstone, + nextAuthority.evmPrivateKey, + ); + const transitionEnvelope = await signedTransitionEnvelope( + fixture, + transition, + fixture.evmPrivateKey, + nextAuthority.evmPrivateKey, + ); + const artifacts = closureArtifacts(tombstone, [rotated, initial], [transition]); + for (const envelope of [initialEnvelope, rotatedEnvelope, tombstoneEnvelope]) { + artifacts.set(`agent-profile-head:${envelope.objectDigest}`, { + objectKind: 'agent-profile-head', + digest: envelope.objectDigest, + canonicalBytes: canonicalizeSignedSystemRecordEnvelopeV1(envelope), + }); + } + artifacts.set(`authority-transition:${transitionEnvelope.objectDigest}`, { + objectKind: 'authority-transition', + digest: transitionEnvelope.objectDigest, + canonicalBytes: canonicalizeSignedSystemRecordEnvelopeV1(transitionEnvelope), + }); + const verifier = { + nowMs: Date.parse('2026-08-08T00:00:00Z'), + resolve: async (reference: { objectKind: string; digest: string }) => + artifacts.get(`${reference.objectKind}:${reference.digest}`), + verifyAuthorityEnvelope: (envelope: SignedSystemRecordEnvelopeV1< + AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1 + >) => verifySignedSystemRecordEnvelopeV1(envelope), + verifyCurrentBundle: () => true, + }; + + const closure = await buildAgentProfileVerificationClosureV1( + tombstoneEnvelope.objectDigest, + verifier, + ); + expect(closure.authoritySummary).toMatchObject({ + candidateHeadDigest: tombstoneEnvelope.objectDigest, + deletionTableDigest: rotated.ownedSubjectTableDigest, + historicalRoots: [initial.rootSubject], + }); + + const tamperedTransitionEnvelope = { + ...transitionEnvelope, + signatures: transitionEnvelope.signatures.map((signature) => signature.role === 'prior-evm' + ? { + ...signature, + signature: signEip191(new Uint8Array([1]), fixture.evmPrivateKey), + } + : signature), + } as SignedSystemRecordEnvelopeV1; + const tamperedArtifacts = new Map(artifacts); + tamperedArtifacts.set(`authority-transition:${transitionEnvelope.objectDigest}`, { + objectKind: 'authority-transition', + digest: transitionEnvelope.objectDigest, + canonicalBytes: canonicalizeSignedSystemRecordEnvelopeV1(tamperedTransitionEnvelope), + }); + await expect(buildAgentProfileVerificationClosureV1(tombstoneEnvelope.objectDigest, { + ...verifier, + resolve: async (reference) => tamperedArtifacts.get( + `${reference.objectKind}:${reference.digest}`, + ), + })).rejects.toThrow(/authority-transition verification failed/); + }); + + it('fails closed when authority verification rejects each closure control kind', async () => { + const fixture = await authorityFixture(); + const initial = { ...activeHead(fixture), bundleDigest: CLOSURE_BUNDLE_DIGEST }; + const initialDigest = computeAgentProfileHeadObjectDigestV1(initial); + await expect(buildClosure( + initial, + closureArtifacts(initial, [], []), + initialDigest, + )).rejects.toThrow(/head authority verification failed/); + + const transition = authorityTransition(fixture, initial); + const transitionDigest = computeAgentProfileAuthorityTransitionDigestV1(transition); + const rotated = { + ...activeForIssuer(initial, transition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: transitionDigest, + }), + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + const rotatedArtifacts = closureArtifacts(rotated, [initial], [transition]); + await expect(buildClosure(rotated, rotatedArtifacts, initialDigest)) + .rejects.toThrow(/head authority verification failed/); + await expect(buildClosure(rotated, rotatedArtifacts, transitionDigest)) + .rejects.toThrow(/authority-transition verification failed/); + + const conflicting = { ...initial, bundleDigest: DIGEST_C }; + const resolution = forkResolution(fixture, initial, [initial, conflicting]); + const successor = { + ...initial, + version: '3' as const, + forkResolutionDigest: computeAgentProfileForkResolutionDigestV1(resolution), + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + await expect(buildClosure( + successor, + closureArtifacts(successor, [initial, conflicting], [], [resolution]), + computeAgentProfileForkResolutionDigestV1(resolution), + )).rejects.toThrow(/fork-resolution verification failed/); + }); + + it('rejects cold cross-network and cross-peer transition reuse', async () => { + const fixture = await authorityFixture(); + const other = await authorityFixture(17); + for (const mismatch of ['network', 'peer'] as const) { + const prior = mismatch === 'network' + ? { + ...activeHead(fixture), + networkId: 'otp:9999' as const, + graphScopedAuthorSeal: { + ...activeHead(fixture).graphScopedAuthorSeal, + assertedAtChainId: '9999', + kaUal: `did:dkg:otp:9999/${fixture.evmIssuer}/7`, + }, + } + : activeHead(other); + const transition = { + ...authorityTransition(mismatch === 'network' ? fixture : other, prior), + ...(mismatch === 'network' ? { networkId: 'otp:9999' as const } : {}), + }; + const current = { + ...activeForIssuer( + activeHead(fixture), + transition.nextEvmIssuer, + '1', + '0', + { acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(transition) }, + ), + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + const artifacts = closureArtifacts(current, [prior], [transition]); + await expect(buildClosure(current, artifacts)).rejects.toThrow(/accepted authority transition/); + } + }); + + it('requires every historical tombstone predecessor during cold resurrection', async () => { + const fixture = await authorityFixture(); + const predecessor = activeHead(fixture); + const tombstone = tombstoneHead(predecessor); + const transition = transitionFrom(fixture, tombstone, '1', '0x5555555555555555555555555555555555555555'); + const current = { + ...activeForIssuer(predecessor, transition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(transition), + }), + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + const missing = closureArtifacts(current, [tombstone], [transition]); + await expect(buildClosure(current, missing)).rejects.toThrow(/missing/); + const complete = closureArtifacts(current, [tombstone, predecessor], [transition]); + const closure = await buildClosure(current, complete); + expect(closure).toMatchObject({ rootClaims: 2 }); + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + current, + { nowMs: Date.parse('2026-08-08T00:00:00Z'), verifiedAuthoritySummary: closure.authoritySummary }, + )).toEqual({ decision: 'accept' }); + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + current, + { nowMs: Date.parse('2026-08-08T00:00:00Z'), + verifiedAuthoritySummary: { + ...closure.authoritySummary, + } as unknown as typeof closure.authoritySummary }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/verified authority closure/) }); + const summaryPrototype = Object.getPrototypeOf(closure.authoritySummary); + const forgedSummary = Object.assign(Object.create(summaryPrototype), { + candidateHeadDigest: closure.authoritySummary.candidateHeadDigest, + transitionLineage: closure.authoritySummary.transitionLineage, + historicalRoots: closure.authoritySummary.historicalRoots, + }) as typeof closure.authoritySummary; + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + current, + { nowMs: Date.parse('2026-08-08T00:00:00Z'), verifiedAuthoritySummary: forgedSummary }, + )).toMatchObject({ decision: 'reject', reason: expect.stringMatching(/verified authority closure/) }); + const SummaryConstructor = summaryPrototype.constructor as new ( + ...args: unknown[] + ) => typeof closure.authoritySummary; + expect(() => new SummaryConstructor( + closure.authoritySummary.candidateHeadDigest, + closure.authoritySummary.transitionLineage, + closure.authoritySummary.historicalRoots, + )).toThrow(/factory-only/); + }); + + it('accepts a cold rotated tombstone only with its closure-minted deletion proof', async () => { + const fixture = await authorityFixture(); + const initial = activeHead(fixture); + const transition = authorityTransition(fixture, initial); + const middle = activeForIssuer(initial, transition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(transition), + }); + const tombstone = tombstoneHead(middle); + const closure = await buildClosure( + tombstone, + closureArtifacts(tombstone, [middle, initial], [transition]), + ); + + expect(evaluateAgentProfileHeadAdvanceV1( + { disposition: 'discoverable', transitionLineage: [], historicalRoots: [] }, + tombstone, + { nowMs: Date.parse('2026-08-08T00:00:00Z'), verifiedAuthoritySummary: closure.authoritySummary }, + )).toEqual({ decision: 'accept' }); + expect(closure.authoritySummary).toMatchObject({ + candidateHeadDigest: computeAgentProfileHeadObjectDigestV1(tombstone), + deletionTableDigest: middle.ownedSubjectTableDigest, + historicalRoots: [initial.rootSubject], + }); + }); + + it('keeps ordinary active closure bounded without replaying same-sequence version history', async () => { + const fixture = await authorityFixture(); + const initial = activeHead(fixture); + const tombstone = tombstoneHead(initial); + const hiddenDescendant = { + ...initial, + version: '2' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(tombstone), + issuedAt: '2026-08-05T12:20:00Z' as const, + }; + const current = { + ...hiddenDescendant, + version: '3' as const, + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(hiddenDescendant), + issuedAt: '2026-08-05T12:25:00Z' as const, + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + const closure = await buildClosure(current, closureArtifacts(current, [], [])); + + expect(closure.objects.map(({ objectKind }) => objectKind).sort()).toEqual( + ['agent-profile-head', 'profile-bundle'], + ); + expect(closure.objects).toHaveLength(2); + }); + + it('rejects transition equivocation even when a fork resolution selects one branch', async () => { + const fixture = await authorityFixture(); + const prior = activeHead(fixture); + const leftTransition = authorityTransition(fixture, prior); + const rightTransition = { ...leftTransition, issuedAt: '2026-08-07T12:00:01Z' }; + const left = activeForIssuer(prior, leftTransition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(leftTransition), + }); + const right = activeForIssuer({ ...prior, bundleDigest: DIGEST_C }, rightTransition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(rightTransition), + }); + const resolution: AgentProfileForkResolutionV1 = { + objectType: 'fork-resolution', kind: 'agents', networkId: NETWORK, + peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + evmIssuer: leftTransition.nextEvmIssuer, authoritySequence: '1', forkedVersion: '0', + resolutionVersion: '1', evidenceHeadDigests: [left, right].map(computeAgentProfileHeadObjectDigestV1).sort(), + issuedAt: '2026-08-07T12:05:00Z', + }; + const current = { + ...left, + version: '2' as const, + forkResolutionDigest: computeAgentProfileForkResolutionDigestV1(resolution), + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + const artifacts = closureArtifacts(current, [left, right, prior], [leftTransition, rightTransition], [resolution]); + await expect(buildClosure(current, artifacts)).rejects.toThrow(/transition lineage|equivocation/); + }); + + it('rejects a cold authority chain that returns to a historical wallet root', async () => { + const fixture = await authorityFixture(); + const initial = activeHead(fixture); + const firstTransition = authorityTransition(fixture, initial); + const middle = activeForIssuer(initial, firstTransition.nextEvmIssuer, '1', '0', { + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(firstTransition), + }); + const backTransition = transitionFrom(fixture, middle, '2', initial.evmIssuer); + const current = { + ...activeForIssuer(middle, initial.evmIssuer, '2', '0', { + acceptedTransitionDigest: computeAgentProfileAuthorityTransitionDigestV1(backTransition), + }), + bundleDigest: CLOSURE_BUNDLE_DIGEST, + }; + const artifacts = closureArtifacts( + current, + [middle, initial], + [backTransition, firstTransition], + ); + await expect(buildClosure(current, artifacts)).rejects.toThrow(/reuses a historical wallet root/); + }); + + it('pins the authority/fork closure edge equations', () => { + expect(assertSystemRecordClosureAlgebraV1(14n, 'active')).toBe(30); + expect(assertSystemRecordClosureAlgebraV1(14n, 'tombstone')).toBe(31); + expect(assertSystemRecordClosureAlgebraV1(13n, 'fork', 2)).toBe(32); + expect(() => assertSystemRecordClosureAlgebraV1(14n, 'fork', 2)).toThrow(/34 objects/); + }); +}); + +async function authorityFixture(offset = 0) { + const peerSecretSeed = Uint8Array.from({ length: 32 }, (_, index) => (index + 1 + offset) & 0xff); + const peerKey = await generateKeyPairFromSeed('Ed25519', peerSecretSeed); + const peerId = peerIdFromPublicKey(peerKey.publicKey).toString(); + const peerPublicKey = Buffer.from(peerKey.publicKey.raw).toString('base64url'); + const evmPrivateKey = Uint8Array.from( + { length: 32 }, + (_, index) => (index + 33 + offset) & 0xff, + ); + const publicKey = secp256k1.getPublicKey(evmPrivateKey, false); + const evmIssuer = `0x${Buffer.from(keccak256(publicKey.subarray(1)).subarray(12)).toString('hex')}`; + return { + peerSecretSeed, + peerId, + peerPublicKey, + evmPrivateKey, + evmIssuer, + root: `did:dkg:agent:${evmIssuer}`, + } as const; +} + +function tombstoneHead(active: AgentProfileActiveHeadObjectV1): AgentProfileHeadObjectV1 { + return { + objectType: 'agent-profile-head', kind: 'agents', state: 'tombstone', + networkId: active.networkId, peerId: active.peerId, peerPublicKey: active.peerPublicKey, + authoritySequence: active.authoritySequence, version: '1', + ...(active.acceptedTransitionDigest === undefined ? {} : { + acceptedTransitionDigest: active.acceptedTransitionDigest, + }), + previousHeadDigest: computeAgentProfileHeadObjectDigestV1(active), + evmIssuer: active.evmIssuer, rootSubject: active.rootSubject, + projectionSchemaDigest: active.projectionSchemaDigest, + issuedAt: '2026-08-07T11:50:00Z', + ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + ownedSubjectCount: '0', projectionBytes: '0', projectionQuads: '0', + }; +} + +function transitionFrom( + fixture: Awaited>, + prior: AgentProfileHeadObjectV1, + nextSequence: string, + nextIssuer: string, +): AgentProfileAuthorityTransitionV1 { + return { + objectType: 'authority-transition', kind: 'agents', mode: 'co-signed', + networkId: prior.networkId, peerId: prior.peerId, peerPublicKey: prior.peerPublicKey, + priorAuthoritySequence: prior.authoritySequence, nextAuthoritySequence: nextSequence, + priorHeadDigest: computeAgentProfileHeadObjectDigestV1(prior), priorEvmIssuer: prior.evmIssuer, + nextEvmIssuer: nextIssuer, nextRoot: `did:dkg:agent:${nextIssuer}`, + issuedAt: '2026-08-07T12:00:00Z', + }; +} + +function closureArtifacts( + current: AgentProfileHeadObjectV1, + historicalHeads: readonly AgentProfileHeadObjectV1[], + transitions: readonly AgentProfileAuthorityTransitionV1[], + resolutions: readonly AgentProfileForkResolutionV1[] = [], +) { + const artifacts = new Map(); + for (const head of [current, ...historicalHeads]) add('agent-profile-head', head); + for (const transition of transitions) add('authority-transition', transition); + for (const resolution of resolutions) add('fork-resolution', resolution); + if (current.state === 'active') { + artifacts.set(`profile-bundle:${current.bundleDigest}`, { + objectKind: 'profile-bundle', digest: current.bundleDigest, canonicalBytes: CLOSURE_BUNDLE, + }); + } else { + const predecessor = historicalHeads.find( + (head): head is AgentProfileActiveHeadObjectV1 => head.state === 'active' + && computeAgentProfileHeadObjectDigestV1(head) === current.previousHeadDigest, + ); + if (predecessor !== undefined) { + const canonicalBytes = canonicalizeOwnedSubjectTableObjectV1( + predecessor.rootSubject, + [predecessor.rootSubject], + ); + artifacts.set(`owned-subject-table:${predecessor.ownedSubjectTableDigest}`, { + objectKind: 'owned-subject-table', + digest: predecessor.ownedSubjectTableDigest, + canonicalBytes, + }); + } + } + return artifacts; + + function add( + objectKind: 'agent-profile-head' | 'authority-transition' | 'fork-resolution', + object: AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1, + ) { + const digest = object.objectType === 'agent-profile-head' + ? computeAgentProfileHeadObjectDigestV1(object) + : object.objectType === 'authority-transition' + ? computeAgentProfileAuthorityTransitionDigestV1(object) + : computeAgentProfileForkResolutionDigestV1(object); + artifacts.set(`${objectKind}:${digest}`, { + objectKind, + digest, + canonicalBytes: fakeEnvelopeBytes(object), + }); + } +} + +async function buildClosure( + current: AgentProfileHeadObjectV1, + artifacts: ReturnType, + rejectAuthorityDigest?: string, +) { + return buildAgentProfileVerificationClosureV1(computeAgentProfileHeadObjectDigestV1(current), { + nowMs: Date.parse('2026-08-08T00:00:00Z'), + resolve: async (reference) => artifacts.get(`${reference.objectKind}:${reference.digest}`), + verifyAuthorityEnvelope: (envelope) => envelope.objectDigest !== rejectAuthorityDigest, + verifyCurrentBundle: (_head, bytes) => Buffer.from(bytes).equals(Buffer.from(CLOSURE_BUNDLE)), + }); +} + +function fakeEnvelopeBytes( + object: AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1, +): Uint8Array { + const objectDigest = object.objectType === 'agent-profile-head' + ? computeAgentProfileHeadObjectDigestV1(object) + : object.objectType === 'authority-transition' + ? computeAgentProfileAuthorityTransitionDigestV1(object) + : computeAgentProfileForkResolutionDigestV1(object); + const roles = object.objectType === 'authority-transition' + ? object.mode === 'co-signed' ? ['peer', 'prior-evm', 'next-evm'] as const : ['peer', 'next-evm'] as const + : ['peer', 'current-evm'] as const; + const signatures = roles.map((role): SystemRecordSignatureEntryV1 => { + if (role === 'peer') { + return { + role, suite: 'ed25519-v1', signer: object.peerId, evidence: { kind: 'none' }, + signature: Buffer.alloc(64).toString('base64url'), + }; + } + const signer = role === 'prior-evm' + ? (object as AgentProfileAuthorityTransitionV1).priorEvmIssuer + : role === 'next-evm' + ? (object as AgentProfileAuthorityTransitionV1).nextEvmIssuer + : (object as AgentProfileHeadObjectV1 | AgentProfileForkResolutionV1).evmIssuer; + return { + role, + suite: 'eip1271-current-finalized-v1', + signer, + evidence: { + kind: 'eip1271-current-finalized', chainId: object.networkId.split(':').at(-1)!, + contractAddress: signer, finalizedBlockNumber: '1', finalizedBlockHash: DIGEST_A, + }, + signature: '0x01', + }; + }); + return canonicalizeSignedSystemRecordEnvelopeV1({ + object, + objectDigest, + signatures, + } as SignedSystemRecordEnvelopeV1); +} + +function activeHead(fixture: Awaited>): AgentProfileActiveHeadObjectV1 { + const seal = { + assertionMerkleRoot: DIGEST_A, + authorAddress: fixture.evmIssuer, + authorAttestationR: `0x${'11'.repeat(32)}`, + authorAttestationVS: `0x${'22'.repeat(32)}`, + authorSchemeVersion: '1', assertedAtChainId: '20430', + assertedAtKav10Address: '0x4444444444444444444444444444444444444444', + reservedKaId: ((BigInt(fixture.evmIssuer) << 96n) | 7n).toString(), + assertionFinalizedAt: '2026-08-05T11:59:59.000Z', + contentScopeVersion: '2', kaUal: `did:dkg:otp:20430/${fixture.evmIssuer}/7`, + assertionVersion: '1', publicTripleCount: '3', privateTripleCount: '0', + privateMerkleRoot: null, + } as const; + return { + objectType: 'agent-profile-head', kind: 'agents', state: 'active', + networkId: NETWORK, peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + authoritySequence: '0', version: '0', evmIssuer: fixture.evmIssuer, + rootSubject: fixture.root, projectionSchemaDigest: DIGEST_C, + issuedAt: '2026-08-05T12:00:00Z', validUntil: '2026-08-06T12:00:00Z', + ownedSubjectTableDigest: computeOwnedSubjectTableDigestV1(fixture.root, [fixture.root]), + ownedSubjectCount: '1', projectionBytes: '256', projectionQuads: '3', + assertionCoordinate: 'agent-profile-v1', graphScopedAuthorSeal: seal, + contentDigest: DIGEST_A, bundleDigest: DIGEST_B, + } as AgentProfileActiveHeadObjectV1; +} + +function activeForIssuer( + source: AgentProfileActiveHeadObjectV1, + issuer: string, + authoritySequence: string, + version: string, + history: Pick, +): AgentProfileActiveHeadObjectV1 { + const root = `did:dkg:agent:${issuer}`; + return { + ...source, + authoritySequence, + version, + ...history, + evmIssuer: issuer, + rootSubject: root, + ownedSubjectTableDigest: computeOwnedSubjectTableDigestV1(root, [root]), + graphScopedAuthorSeal: { + ...source.graphScopedAuthorSeal, + authorAddress: issuer, + kaUal: `did:dkg:otp:20430/${issuer}/7`, + reservedKaId: ((BigInt(issuer) << 96n) | 7n).toString(), + }, + } as AgentProfileActiveHeadObjectV1; +} + +function authorityTransition( + fixture: Awaited>, + active: AgentProfileActiveHeadObjectV1, + mode: 'co-signed' | 'expired-prior' = 'co-signed', +): AgentProfileAuthorityTransitionV1 { + const next = '0x5555555555555555555555555555555555555555'; + return { + objectType: 'authority-transition', kind: 'agents', mode, + networkId: NETWORK, peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + priorAuthoritySequence: '0', nextAuthoritySequence: '1', + priorHeadDigest: computeAgentProfileHeadObjectDigestV1(active), + priorEvmIssuer: fixture.evmIssuer, nextEvmIssuer: next, + nextRoot: `did:dkg:agent:${next}`, issuedAt: '2026-08-07T12:00:00Z', + ...(mode === 'expired-prior' ? { priorValidUntil: active.validUntil } : {}), + } as AgentProfileAuthorityTransitionV1; +} + +function forkResolution( + fixture: Awaited>, + _active: AgentProfileActiveHeadObjectV1, + conflicts: readonly AgentProfileHeadObjectV1[] = [], +): AgentProfileForkResolutionV1 { + const evidenceHeadDigests = conflicts.length === 0 + ? [DIGEST_A, DIGEST_B] + : conflicts.map(computeAgentProfileHeadObjectDigestV1).sort(); + return { + objectType: 'fork-resolution', kind: 'agents', networkId: NETWORK, + peerId: fixture.peerId, peerPublicKey: fixture.peerPublicKey, + evmIssuer: fixture.evmIssuer, authoritySequence: '0', forkedVersion: '0', + resolutionVersion: '2', evidenceHeadDigests, + issuedAt: '2026-08-05T12:05:00Z', + }; +} + +function signEip191(message: Uint8Array, privateKey: Uint8Array): string { + const recovered = secp256k1.sign(eip191PersonalMessageHashV1(message), privateKey, { + format: 'recovered', prehash: false, lowS: true, + }); + const compact = recovered.subarray(1); + const result = new Uint8Array(65); + result.set(compact); + result[64] = recovered[0] + 27; + return `0x${Buffer.from(result).toString('hex')}`; +} + +async function signedHeadEnvelope( + fixture: Awaited>, + object: AgentProfileHeadObjectV1, + evmPrivateKey = fixture.evmPrivateKey, +): Promise { + const objectDigest = computeAgentProfileHeadObjectDigestV1(object); + return { + object, + objectDigest, + signatures: [ + { + role: 'peer', suite: 'ed25519-v1', signer: fixture.peerId, evidence: { kind: 'none' }, + signature: Buffer.from(await signEd25519( + buildSystemRecordSignatureMessageV1(object, objectDigest, 'peer'), + fixture.peerSecretSeed, + )).toString('base64url'), + }, + { + role: 'current-evm', suite: 'eip191-personal-sign-digest-v1', signer: object.evmIssuer, + evidence: { kind: 'none' }, + signature: signEip191( + buildSystemRecordSignatureMessageV1(object, objectDigest, 'current-evm'), + evmPrivateKey, + ), + }, + ], + }; +} + +async function signedTransitionEnvelope( + fixture: Awaited>, + object: AgentProfileAuthorityTransitionV1, + priorEvmPrivateKey: Uint8Array, + nextEvmPrivateKey: Uint8Array, +): Promise> { + const objectDigest = computeAgentProfileAuthorityTransitionDigestV1(object); + return { + object, + objectDigest, + signatures: [ + { + role: 'peer', suite: 'ed25519-v1', signer: object.peerId, evidence: { kind: 'none' }, + signature: Buffer.from(await signEd25519( + buildSystemRecordSignatureMessageV1(object, objectDigest, 'peer'), + fixture.peerSecretSeed, + )).toString('base64url'), + }, + { + role: 'prior-evm', suite: 'eip191-personal-sign-digest-v1', + signer: object.priorEvmIssuer, evidence: { kind: 'none' }, + signature: signEip191( + buildSystemRecordSignatureMessageV1(object, objectDigest, 'prior-evm'), + priorEvmPrivateKey, + ), + }, + { + role: 'next-evm', suite: 'eip191-personal-sign-digest-v1', + signer: object.nextEvmIssuer, evidence: { kind: 'none' }, + signature: signEip191( + buildSystemRecordSignatureMessageV1(object, objectDigest, 'next-evm'), + nextEvmPrivateKey, + ), + }, + ], + }; +} diff --git a/packages/core/test/system-record-package-export-v1.mjs b/packages/core/test/system-record-package-export-v1.mjs new file mode 100644 index 0000000000..f3f60f3a2b --- /dev/null +++ b/packages/core/test/system-record-package-export-v1.mjs @@ -0,0 +1,17 @@ +const api = await import('@origintrail-official/dkg-core/system-record-v1'); + +const representativeExports = [ + 'SYSTEM_RECORD_KIND_V1', + 'computeAgentProfileHeadObjectDigestV1', + 'parseCanonicalSystemRecordAppliedStateV1', + 'computeSystemRecordRootDescriptorDigestV1', + 'verifySystemRecordResponsePayloadV1', +]; + +for (const name of representativeExports) { + if (!(name in api)) { + throw new Error(`system-record-v1 package export is missing ${name}`); + } +} + +console.log(`system-record-v1 package export ok (${representativeExports.length} representative symbols)`); diff --git a/packages/core/test/system-record-wire-v1.test.ts b/packages/core/test/system-record-wire-v1.test.ts new file mode 100644 index 0000000000..d9a16e8553 --- /dev/null +++ b/packages/core/test/system-record-wire-v1.test.ts @@ -0,0 +1,372 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +import { + canonicalizeAgentProfileConflictEvidenceV1, + canonicalizeSignedSystemRecordEnvelopeV1, + computeAgentProfileConflictEvidenceDigestV1, + digestSystemRecordBytesV1, + type AgentProfileAuthorityTransitionV1, + type AgentProfileForkResolutionV1, + type AgentProfileHeadObjectV1, + type SignedSystemRecordEnvelopeV1, +} from '../src/system-record-objects-v1.js'; +import { + canonicalizeSystemRecordInventoryInternalObjectV1, + canonicalizeSystemRecordInventoryLeafObjectV1, + canonicalizeSignedSystemRecordRootDescriptorEnvelopeV1, + computeSystemRecordInventoryInternalDigestV1, + computeSystemRecordInventoryLeafDigestV1, + computeSystemRecordRootDescriptorDigestV1, +} from '../src/system-record-inventory-v1.js'; +import { + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_MAX_FRAME_BYTES, + SYSTEM_RECORD_MAX_FRAME_PAYLOAD_BYTES, + SYSTEM_RECORD_MAX_HEADER_BYTES, + type SystemRecordObjectKindV1, +} from '../src/system-record-limits-v1.js'; +import { + decodeSystemRecordRequestFrameV1, + decodeSystemRecordResponseFrameV1, + decodeSystemRecordResponseHeaderV1, + encodeSystemRecordRequestFrameV1, + encodeSystemRecordResponseFrameV1, + readSystemRecordHeaderLengthV1, + verifySystemRecordResponsePayloadV1, + type SystemRecordRequestHeaderV1, +} from '../src/system-record-wire-v1.js'; + +const REQUEST_ID = '0123456789abcdef0123456789abcdef'; +const DIGEST = `0x${'aa'.repeat(32)}` as const; +const COMMON = { + wireVersion: '1', requestId: REQUEST_ID, kind: 'agents', networkId: 'otp:20430', + payloadBytes: '0', +} as const; +const PEER_ID = '12D3KooWDxBauQDeJjCmcvWiREFALfKsr5VfTzGUJbZJ6CUcc7aF'; +const vectors = JSON.parse(readFileSync( + new URL('./fixtures/system-record-v1/vectors.json', import.meta.url), + 'utf8', +)) as WireGoldenVectors; + +describe('system-record wire request framing', () => { + it('round-trips every request operation with exact omission branches', () => { + const requests: SystemRecordRequestHeaderV1[] = [ + { ...COMMON, operation: 'get-root' }, + { + ...COMMON, operation: 'get-inventory-object', rootDescriptorDigest: DIGEST, + path: [0, 255], objectKind: 'inventory-leaf', objectDigest: DIGEST, + }, + { ...COMMON, operation: 'get-control-object', objectKind: 'fork-resolution', objectDigest: DIGEST }, + { ...COMMON, operation: 'get-bundle', objectKind: 'profile-bundle', objectDigest: DIGEST }, + ]; + for (const request of requests) { + expect(decodeSystemRecordRequestFrameV1(encodeSystemRecordRequestFrameV1(request))) + .toEqual(request); + } + }); + + it('rejects malformed request IDs, unknown/null fields, paths, and payloads', () => { + expect(() => encodeSystemRecordRequestFrameV1({ + ...COMMON, requestId: `0x${REQUEST_ID}`, operation: 'get-root', + })).toThrow(/requestId/); + expect(() => encodeSystemRecordRequestFrameV1({ + ...COMMON, operation: 'get-root', objectDigest: null, + } as unknown as SystemRecordRequestHeaderV1)).toThrow(/omit optional|unknown or missing/); + expect(() => encodeSystemRecordRequestFrameV1({ + ...COMMON, operation: 'get-inventory-object', rootDescriptorDigest: DIGEST, + path: [0, 1, 2], objectKind: 'inventory-leaf', objectDigest: DIGEST, + })).toThrow(/path/); + const request = encodeSystemRecordRequestFrameV1({ ...COMMON, operation: 'get-root' }); + const withPayload = new Uint8Array(request.byteLength + 1); + withPayload.set(request); + expect(() => decodeSystemRecordRequestFrameV1(withPayload)).toThrow(/payload-free/); + }); + + it('snapshots request paths without trusting caller-owned array behavior', () => { + class MisleadingPath extends Array { + override some(): boolean { return true; } + } + const source = new MisleadingPath(0, 255); + const request = { + ...COMMON, + operation: 'get-inventory-object' as const, + rootDescriptorDigest: DIGEST, + path: source, + objectKind: 'inventory-leaf' as const, + objectDigest: DIGEST, + }; + const decoded = decodeSystemRecordRequestFrameV1(encodeSystemRecordRequestFrameV1(request)); + expect(decoded.operation).toBe('get-inventory-object'); + if (decoded.operation !== 'get-inventory-object') throw new Error('expected inventory request'); + expect(decoded.path).toEqual([0, 255]); + expect(decoded.path).not.toBe(source); + expect(Object.isFrozen(decoded.path)).toBe(true); + + const extra = Object.assign([0], { unexpected: true }); + expect(() => encodeSystemRecordRequestFrameV1({ ...request, path: extra })).toThrow(/closed|non-index/); + const accessor = Object.defineProperty([0], '0', { enumerable: true, get: () => 0 }); + expect(() => encodeSystemRecordRequestFrameV1({ ...request, path: accessor })).toThrow(/data elements/); + }); + + it('checks the four-byte header cap before body allocation', () => { + const prefix = new Uint8Array(4); + new DataView(prefix.buffer).setUint32(0, SYSTEM_RECORD_MAX_HEADER_BYTES + 1, false); + expect(() => readSystemRecordHeaderLengthV1(prefix)).toThrow(/preallocation/); + class MisleadingPrefix extends Uint8Array { + override get byteLength(): number { return 4; } + } + expect(() => readSystemRecordHeaderLengthV1(new MisleadingPrefix([0]))).toThrow(/four bytes/); + }); +}); + +describe('system-record wire response framing', () => { + it('round-trips and digest-verifies an exact bundle response', () => { + class MisleadingPayload extends Uint8Array { + override get byteLength(): number { return 999; } + override slice(): Uint8Array { return new Uint8Array(); } + } + const payload = new TextEncoder().encode('canonical bundle bytes'); + const objectDigest = digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, payload); + const request = { + ...COMMON, operation: 'get-bundle', objectKind: 'profile-bundle', objectDigest, + } as const; + const header = { + wireVersion: '1', requestId: REQUEST_ID, status: 'ok', objectKind: 'profile-bundle', + objectDigest, payloadBytes: String(payload.byteLength), + } as const; + const frame = encodeSystemRecordResponseFrameV1(header, payload); + const decoded = decodeSystemRecordResponseFrameV1(frame); + expect(decoded.header).toEqual(header); + expect(decoded.payload).toEqual(payload); + expect(() => verifySystemRecordResponsePayloadV1(request, decoded.header, decoded.payload)) + .not.toThrow(); + expect(frame.byteLength).toBeLessThanOrEqual(SYSTEM_RECORD_MAX_FRAME_BYTES); + expect(decoded.payload.buffer).toBe(frame.buffer); + const hostilePayload = new MisleadingPayload(payload); + expect(digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + hostilePayload, + )).toBe(objectDigest); + expect(decodeSystemRecordResponseFrameV1( + encodeSystemRecordResponseFrameV1(header, hostilePayload), + ).payload).toEqual(payload); + expect(() => verifySystemRecordResponsePayloadV1(request, header, hostilePayload)).not.toThrow(); + class MisleadingFrame extends Uint8Array { + override get byteLength(): number { return 1; } + override subarray(): Uint8Array { return new Uint8Array(); } + } + expect(decodeSystemRecordResponseFrameV1(new MisleadingFrame(frame)).payload).toEqual(payload); + }); + + it('accepts the exact payload ceiling, rejects +1, and rejects hostile JSON encodings', () => { + const payload = new Uint8Array(SYSTEM_RECORD_MAX_FRAME_PAYLOAD_BYTES); + const objectDigest = digestSystemRecordBytesV1(SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, payload); + const header = { + wireVersion: '1', requestId: REQUEST_ID, status: 'ok', objectKind: 'profile-bundle', + objectDigest, payloadBytes: String(payload.byteLength), + } as const; + expect(encodeSystemRecordResponseFrameV1(header, payload).byteLength) + .toBeLessThanOrEqual(SYSTEM_RECORD_MAX_FRAME_BYTES); + expect(() => encodeSystemRecordResponseFrameV1( + { ...header, payloadBytes: String(payload.byteLength + 1) }, + new Uint8Array(payload.byteLength + 1), + )).toThrow(/object cap/); + const duplicate = new TextEncoder().encode( + `{"objectDigest":"${DIGEST}","objectDigest":"${DIGEST}","objectKind":"profile-bundle","payloadBytes":"1","requestId":"${REQUEST_ID}","status":"ok","wireVersion":"1"}`, + ); + expect(() => decodeSystemRecordResponseHeaderV1(duplicate)).toThrow(/[Dd]uplicate|canonical/); + const bom = new Uint8Array([0xef, 0xbb, 0xbf, ...new TextEncoder().encode('{}')]); + expect(() => decodeSystemRecordResponseHeaderV1(bom)).toThrow(); + expect(readSystemRecordHeaderLengthV1(Uint8Array.of(0, 0, 0x20, 0))).toBe(SYSTEM_RECORD_MAX_HEADER_BYTES); + }); + + it('rejects status/error mismatches, declared over-cap bodies, and digest mismatch', () => { + expect(() => encodeSystemRecordResponseFrameV1({ + wireVersion: '1', requestId: REQUEST_ID, status: 'busy', payloadBytes: '0', + errorCode: 'internal', + }, new Uint8Array())).toThrow(/tuple/); + for (const inheritedStatus of ['toString', 'constructor']) { + expect(() => verifySystemRecordResponsePayloadV1( + { ...COMMON, operation: 'get-root' }, + { + wireVersion: '1', requestId: REQUEST_ID, status: inheritedStatus, + payloadBytes: '0', errorCode: Object.prototype[inheritedStatus as 'toString'], + } as never, + new Uint8Array(), + )).toThrow(/status/); + } + + const oversizedHeader = new TextEncoder().encode(JSON.stringify({ + objectDigest: DIGEST, objectKind: 'conflict-evidence', payloadBytes: '16385', + requestId: REQUEST_ID, status: 'ok', wireVersion: '1', + })); + expect(() => decodeSystemRecordResponseHeaderV1(oversizedHeader)).toThrow(/object cap/); + + const payload = new TextEncoder().encode('bytes'); + const request = { + ...COMMON, operation: 'get-bundle', objectKind: 'profile-bundle', objectDigest: DIGEST, + } as const; + const response = { + wireVersion: '1', requestId: REQUEST_ID, status: 'ok', objectKind: 'profile-bundle', + objectDigest: DIGEST, payloadBytes: String(payload.byteLength), + } as const; + expect(() => verifySystemRecordResponsePayloadV1(request, response, payload)).toThrow(/digest/); + }); + + it('rejects a valid root descriptor from a different requested network', () => { + const object = { + objectType: 'root-descriptor', kind: 'agents', networkId: 'otp:9999', + epoch: '0', version: '0', treeRootDigest: DIGEST, totalRows: '0', + } as const; + const objectDigest = computeSystemRecordRootDescriptorDigestV1(object); + const payload = canonicalizeSignedSystemRecordRootDescriptorEnvelopeV1({ + object, + objectDigest, + providerPeerId: '12D3KooWDxBauQDeJjCmcvWiREFALfKsr5VfTzGUJbZJ6CUcc7aF', + signatureSuite: 'ed25519-v1', + signature: Buffer.alloc(64).toString('base64url'), + }); + const request = { ...COMMON, operation: 'get-root' } as const; + const response = { + wireVersion: '1', requestId: REQUEST_ID, status: 'ok', objectKind: 'root-descriptor', + objectDigest, payloadBytes: String(payload.byteLength), + } as const; + + expect(() => verifySystemRecordResponsePayloadV1(request, response, payload)) + .toThrow(/requested kind\/network/); + }); + + it('verifies both inventory object payloads and binds root-path context', () => { + const leaf = { objectType: 'inventory-leaf', rows: [] } as const; + const internal = { + objectType: 'inventory-internal', firstKeyHash: DIGEST, + lastKeyHash: `0x${'cc'.repeat(32)}` as const, + entries: [ + { + separatorKeyHash: DIGEST, + childDigest: `0x${'bb'.repeat(32)}` as const, + childKind: 'inventory-leaf', + }, + { + separatorKeyHash: `0x${'cc'.repeat(32)}` as const, + childDigest: `0x${'dd'.repeat(32)}` as const, + childKind: 'inventory-leaf', + }, + ], + } as const; + const cases = [ + { + objectKind: 'inventory-leaf' as const, + payload: canonicalizeSystemRecordInventoryLeafObjectV1(leaf, COMMON.networkId, true), + objectDigest: computeSystemRecordInventoryLeafDigestV1(leaf, COMMON.networkId, true), + }, + { + objectKind: 'inventory-internal' as const, + payload: canonicalizeSystemRecordInventoryInternalObjectV1(internal, true), + objectDigest: computeSystemRecordInventoryInternalDigestV1(internal, true), + }, + ]; + + for (const value of cases) { + const request = { + ...COMMON, operation: 'get-inventory-object', rootDescriptorDigest: DIGEST, + path: [], objectKind: value.objectKind, objectDigest: value.objectDigest, + } as const; + const response = okResponse(value.objectKind, value.objectDigest, value.payload); + expect(() => verifySystemRecordResponsePayloadV1(request, response, value.payload)).not.toThrow(); + } + + const value = cases[0]; + const nonRootRequest = { + ...COMMON, operation: 'get-inventory-object', rootDescriptorDigest: DIGEST, + path: [0], objectKind: value.objectKind, objectDigest: value.objectDigest, + } as const; + expect(() => verifySystemRecordResponsePayloadV1( + nonRootRequest, + okResponse(value.objectKind, value.objectDigest, value.payload), + value.payload, + )).toThrow(/occupancy/); + }); + + it('verifies every control payload branch and rejects cross-network signed controls', () => { + const signed = [ + ['agent-profile-head', vectors.signed.activeEip191.envelope], + ['authority-transition', vectors.signed.coSignedTransitionEip191.envelope], + ['fork-resolution', vectors.signed.forkV0Eip191.envelope], + ] as const; + for (const [objectKind, envelope] of signed) { + const payload = canonicalizeSignedSystemRecordEnvelopeV1(envelope); + const request = { + ...COMMON, operation: 'get-control-object', objectKind, objectDigest: envelope.objectDigest, + } as const; + const response = okResponse(objectKind, envelope.objectDigest, payload); + expect(() => verifySystemRecordResponsePayloadV1(request, response, payload)).not.toThrow(); + expect(() => verifySystemRecordResponsePayloadV1( + { ...request, networkId: 'otp:9999' }, response, payload, + )).toThrow(/requested kind\/network/); + } + + const evidence = { + objectType: 'conflict-evidence', kind: 'agents', networkId: COMMON.networkId, + peerId: PEER_ID, + entries: [{ + type: 'fork', authoritySequence: '0', version: '0', + objectDigests: [DIGEST, `0x${'bb'.repeat(32)}` as const], + }], + } as const; + const evidencePayload = canonicalizeAgentProfileConflictEvidenceV1(evidence); + const evidenceDigest = computeAgentProfileConflictEvidenceDigestV1(evidence); + const evidenceRequest = { + ...COMMON, operation: 'get-control-object', objectKind: 'conflict-evidence', + objectDigest: evidenceDigest, + } as const; + expect(() => verifySystemRecordResponsePayloadV1( + evidenceRequest, + okResponse('conflict-evidence', evidenceDigest, evidencePayload), + evidencePayload, + )).not.toThrow(); + expect(() => verifySystemRecordResponsePayloadV1( + { ...evidenceRequest, networkId: 'otp:9999' }, + okResponse('conflict-evidence', evidenceDigest, evidencePayload), + evidencePayload, + )).toThrow(/requested kind\/network/); + + const tablePayload = new TextEncoder().encode('[]'); + const tableDigest = digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.ownedSubjectTable, + tablePayload, + ); + const tableRequest = { + ...COMMON, operation: 'get-control-object', objectKind: 'owned-subject-table', + objectDigest: tableDigest, + } as const; + expect(() => verifySystemRecordResponsePayloadV1( + tableRequest, + okResponse('owned-subject-table', tableDigest, tablePayload), + tablePayload, + )).not.toThrow(); + }); +}); + +function okResponse( + objectKind: SystemRecordObjectKindV1, + objectDigest: `0x${string}`, + payload: Uint8Array, +) { + return { + wireVersion: '1', requestId: REQUEST_ID, status: 'ok', objectKind, + objectDigest, payloadBytes: String(payload.byteLength), + } as const; +} + +type WireControlObject = + | AgentProfileHeadObjectV1 + | AgentProfileAuthorityTransitionV1 + | AgentProfileForkResolutionV1; + +interface WireGoldenVectors { + readonly signed: Readonly; + }>>; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1de90302e8..6abcfb821c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -731,6 +731,9 @@ importers: '@multiformats/multiaddr': specifier: ^13.0.3 version: 13.0.3 + '@noble/curves': + specifier: 2.2.0 + version: 2.2.0 '@noble/ed25519': specifier: ^3.1.0 version: 3.1.0 @@ -762,6 +765,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.1)(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + multiformats: + specifier: 14.0.0 + version: 14.0.0 vitest: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.1)(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)