feat(rfc49): rotate the public _catalog commitment on curated KA updates#1198
feat(rfc49): rotate the public _catalog commitment on curated KA updates#1198branarakic wants to merge 1 commit into
Conversation
OT-RFC-49 follow-up to the catalog-sampling strip. The publish path wires the public `_catalog` commitment; the UPDATE path did not — `publisher.update()` ran none of the WS-D curated treatment (no catalog computed, not shipped inline, `newCatalogRoot=0`), so a curated update had the core storage-ACK decline and the contract update gate revert `IncompleteCatalogCommitment` (a curated KC must rotate its catalog commitment in lockstep). You could publish a curated KA but not update it. Port the publish-path catalog logic into `DKGPublisher.update()`: - partition the reloaded quads on the CG's canonical DID, recompute the catalog commitment via the shared `catalogCommittedLeaves` + `computeCatalogRoot` (the finalize-injected `_catalog` floor is already in the reloaded set, covered by the author seal), and thread `newCatalogRoot`/`newCatalogLeafCount` into the update ACK digest and `updateKnowledgeCollectionV10`. - for curated updates, ship the PUBLIC catalog N-quads inline (`stagingQuads`, `isEncryptedPayload`) so the core takes its curated-catalog ACK branch, and set the ACK/tx byteSize to the catalog footprint (one source, can't desync). - public CGs / curated CGs with no catalog entry are unchanged (newCatalogRoot stays bytes32(0)). - thread `encryptInlinePayload` through `agent.update()` + the update ACK provider so the publisher detects curated updates end-to-end. Member-side RE-ENCRYPTION of the updated private payload via the inline ACK is intentionally descoped (the inline ACK ships the PUBLIC catalog only; members get the updated private data via the normal SWM workspace gossip). A first pass added inline re-encryption but its chunked-AEAD nonce domain was constant per (session, kaId) — a two-time-pad reuse risk — so it was removed; member inline re-encryption should land separately with a per-attempt nonce domain. Validated: ka-update.test.ts curated-update cases (newCatalogRoot == independently computed catalog root, leafCount 4, ACK byteSize == tx byteSize, inline stagingQuads parse back to the same root; public update keeps catalog zero) + no regression on storage-ack/publish e2e (42 pass). Contract update path tested in v10-pca-lifecycle. Stacked on #1196. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| // core's curated ACK rebuild reproduces identical leaf hashes (`hashTripleV10` | ||
| // excludes the graph), exactly as the publish path does. | ||
| const useEncryptedInlineUpdate = typeof options.encryptInlinePayload === 'function'; | ||
| const { catalogQuads: updateCatalogQuads } = partitionCatalogQuads( |
There was a problem hiding this comment.
🔴 Bug: This only finds a catalog commitment when the caller already included the CG _catalog quads in allSkolemizedQuads, but the normal update flows do not guarantee that. In particular, the finalized-assertion update path still reloads SWM by seal.rootEntities, which excludes the CG DID catalog subject, so updateCatalogCommitment stays undefined and curated updates still submit newCatalogRoot=0/newCatalogLeafCount=0. Either append the catalog subject to update selections the same way publishFromSharedMemory does, or derive the catalog commitment from CG metadata instead of assuming it is present in user quads.
| // re-encryption/distribution of the updated private payload (the chunked | ||
| // AEAD fan-out the publish path runs) is a separate, independently-tested | ||
| // follow-up, so `encryptInlineChunked` is intentionally NOT threaded. | ||
| const updateEncryptInlinePayload = await this._resolveEncryptInlinePayload( |
There was a problem hiding this comment.
🔴 Bug: _resolveEncryptInlinePayload() does the full LU-5 curated bootstrap (agent-key lookup, recipient resolution, sender-key epoch setup) even though this update flow never calls the callback and only uses its presence as a curated/non-curated signal. That introduces new failure modes for curated updates from non-custodial authors or before sender-key state exists, even though no member-side payload encryption happens here yet. Please switch this to a lightweight curated-policy probe, or add a separate boolean resolver for updates so the real LU-5 checks only run once the update path actually redistributes private payload.
…commitment reverts The OT-RFC-49 WS-B proof-race rewrite of submitProof (reads the snapshotted challengeRoot/challengeLeafCount from the grown RandomSamplingLib.Challenge struct) changed RandomSampling's behavior but left _VERSION at 10.0.4 — identical to the live base_sepolia deployment, while its coupled storage contract already moved to 10.1.0. Bump so a redeploy is not read as a no-op. Also adds the previously-zero-assertion catalog-commitment integrity reverts (KnowledgeAssetsLifecycle), incl. IncompleteCatalogCommitment (the PR #1198 lifecycle regression): partial commitment + public-CG-with-catalog on BOTH the publish and update paths, and the already-committed zero-pair stranding guard.
|
Closing in favor of #1208. This PR is stacked on the old |
OT-RFC-49 — rotate the public
_catalogcommitment on curated KA updatesStacked follow-up to #1196 (the catalog-sampling strip). #1196 wires the public
_catalogcommitment on the publish path; this PR fixes the update path, which was the one remaining curated gap.The gap
DKGPublisher.update()ran none of the WS-D curated treatment — it never computed the catalog, never shipped it inline, and sentnewCatalogRoot=0. So a curated update would: (a) have the core storage-ACK decline (no catalog to verify) and (b) revert the contract's update gate withIncompleteCatalogCommitment(a curated KC must rotate its catalog commitment in lockstep). You could publish a curated KA but not update it.The fix (port the publish-path logic into
update())catalogCommittedLeaves+computeCatalogRoot(the finalize-injected_catalogfloor is already in the reloaded set and covered by the author seal), and threadnewCatalogRoot/newCatalogLeafCountinto the update ACK digest andupdateKnowledgeCollectionV10.stagingQuads+isEncryptedPayload) so the core takes its curated-catalog ACK branch, with ACK/txbyteSize= the catalog footprint (one source → can't desync).newCatalogRootstaysbytes32(0)).encryptInlinePayloadthroughagent.update()+ the update ACK provider so curated updates are detected end-to-end.Scope note (intentional)
Member-side re-encryption of the updated private payload via the inline ACK is descoped: the inline ACK ships the public catalog only; members get the updated private data via the normal SWM workspace gossip. A first pass added inline re-encryption but its chunked-AEAD nonce domain was constant per
(session, kaId)— a two-time-pad reuse risk — so it was removed. Member inline re-encryption should land separately with a per-attempt nonce domain.Validation
ka-update.test.tscurated-update cases:newCatalogRoot== an independently-computed catalog root,leafCount=4, ACKbyteSize== txbyteSize, and the inlinestagingQuadsparse back to the same committed root; a public update keeps the catalog zero.storage-ack-handler/ publish-e2e / signature-collection (42 pass, 1 pre-existing skip). Publisher + agent build + typecheck clean.v10-pca-lifecycle.test.tsin feat(rfc49): catalog-sampling rewire + ciphertext strip (WS-A..E) #1196.Note: the daemon KA-lifecycle update mechanics (
pull-from/ re-finalize) are intricate and validated here at the publisher harness level (mock adapter) rather than a full devnet lifecycle — orthogonal to this catalog fix.🤖 Generated with Claude Code