Skip to content

feat(rfc49): rotate the public _catalog commitment on curated KA updates#1198

Closed
branarakic wants to merge 1 commit into
feat/rfc49-catalog-sampling-stripfrom
feat/rfc49-curated-update-catalog
Closed

feat(rfc49): rotate the public _catalog commitment on curated KA updates#1198
branarakic wants to merge 1 commit into
feat/rfc49-catalog-sampling-stripfrom
feat/rfc49-curated-update-catalog

Conversation

@branarakic

Copy link
Copy Markdown
Contributor

OT-RFC-49 — rotate the public _catalog commitment on curated KA updates

Stacked follow-up to #1196 (the catalog-sampling strip). #1196 wires the public _catalog commitment 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 sent newCatalogRoot=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 with IncompleteCatalogCommitment (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())

  • 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 and 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, with ACK/tx byteSize = 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 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.ts curated-update cases: newCatalogRoot == an independently-computed catalog root, leafCount=4, ACK byteSize == tx byteSize, and the inline stagingQuads parse back to the same committed root; a public update keeps the catalog zero.
  • No regression on storage-ack-handler / publish-e2e / signature-collection (42 pass, 1 pre-existing skip). Publisher + agent build + typecheck clean.
  • The contract update path (with the catalog) is already covered by v10-pca-lifecycle.test.ts in 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

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

branarakic pushed a commit that referenced this pull request Jun 17, 2026
…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.
@branarakic

Copy link
Copy Markdown
Contributor Author

Closing in favor of #1208. This PR is stacked on the old feat/rfc49-catalog-sampling-strip base, so its diff carries the entire strip — which is now in main via #1203. Its net-new contribution, the curated UPDATE-path → catalog fix, is carried forward by #1208 (feat(rfc49): cut the curated UPDATE path over to the catalog model), the clean main-based version. Tracking the update-path work on #1208. Reopen if #1208 turns out to miss something here.

@branarakic branarakic closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant