Skip to content

canton: remove all contract keys#39

Merged
bengtlofgren merged 4 commits into
integration/cantonfrom
cs/remove-keys
Jul 15, 2026
Merged

canton: remove all contract keys#39
bengtlofgren merged 4 commits into
integration/cantonfrom
cs/remove-keys

Conversation

@kcsongor

@kcsongor kcsongor commented Jul 10, 2026

Copy link
Copy Markdown

The problem

After the replay-protection redesign (#38), contract keys survived in the package only as indexes: CoreState keyed by the governance anchor, EmitterRegistry keyed by the operator, Emitter keyed by (operator, owner, emitterId). None of them carried authority or uniqueness — those never came from keys — but even the indexing role is not something the ledger underwrites: LF 2.3 key resolution happens once, on the submitting participant, against the submission's readers, so a key is just a submitter-local convenience wearing the syntax of a guarantee. Worse, the key-based authenticity story ("a lookup by the known governance party resolves only authentic CoreStates") quietly depended on that resolution being trustworthy, which it is not for anyone but the maintainer itself.

The keys also propped up a pattern worth naming: our propose-accept flows used the accept side for the operator's visibility (resolving the registry by key from its own view), not for its authority — which is inheritable. Keeping keys meant keeping cranks.

The solution

Contracts are addressed by contract-id, and authenticity moves to where it always actually lived: the payload. Signatories cannot be forged, so a fetched or disclosed CoreState naming the known governance anchor is authentic however its cid was found. Stakeholders resolve current cids from their own ACS (a consuming exercise delivers the successor's CreatedEvent to every stakeholder — publishers, the operator, and governance need no off-ledger help at all); non-stakeholders get cid + explicit disclosure from the disclosure service we already run for the replay trie. README §4.7 is the aggregation document: every resolution the service must serve, who needs an attachment, and churn rates — with the standing rule that the service is trusted for liveness only, since consumers authenticate payloads, never resolution paths.

With visibility outsourced and authority inherited, the cranks collapse into direct flows:

  • Emitter registration is permissionless and requester-submitted: RegisterEmitter on the disclosed registry allocates the id and creates the co-signed Emitter in one transaction — the operator's signature is inherited from the registry, matching EVM/Sui where anyone may emit. The request/approve templates are gone. (The registry holds only nextId : Int, so permissionless writes cannot bloat it.)
  • Replay-trie roots are free and consumer-claimed: ReplayRootRegistry shrinks to a stateless, never-churning anchor whose nonconsuming ClaimReplayRoot mints a co-signed root for whoever asks — for themselves. We deliberately dropped one-root-per-consumer enforcement after walking the alternatives (an on-ledger granted set is sybil-spammable past the request caps; gating disclosures promotes the service into a security control and leaks via witnessed successor blobs; operator-issued grants put a guardian-multisig signing ceremony on every onboarding). The load-bearing property is that claiming a root for a consumer requires that consumer's authority: nobody can fork someone else's replay scope. Parallel roots for your own scope are self-harm, detected off-ledger by the disclosure service (two covering nodes for one digest), and a stolen consumer key defeats an app more directly than replay anyway — with trie-identity pinning documented as the hardening if that trade ever changes. This is EVM parity: nobody grants replay storage there either.

One consequence worth calling out for integrators: a non-stakeholder cannot fetch a disclosed contract (fetch authorization requires a stakeholder among the authorizers — disclosure grants visibility, not fetch authority). Templates must opt in to third-party reads via a choice, so CoreState exposes a nonconsuming, flexible-controller GetOperator — its result is bound by validation to the exercised contract, making it the caller-side authenticity pin. The reference integrator asserts it against its configured operator before verifying anything, so a redeemer pointing at a fabricated CoreState (attacker guardian set) dies on the first line.

Operationally, the operator party's active signing surface after genesis is now empty: registration, root claims, splits, and governance transitions all reach its signature by inheritance. If the operator is stood up as a k-of-n guardian threshold party (the existing §9 external-party machinery), the interactive-submission ceremony is needed for genesis only.

🤖 Generated with Claude Code

Comment on lines +173 to +177
nonconsuming choice GetOperator : Party
with
reader : Party
controller reader
do pure operator

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I guess this doesnt protect against if the operator's keys got compromised .. but maybe we arent trying to protect against that. I wouldve thought we wanted a read choice on guardianGovernance as well

@bengtlofgren
bengtlofgren merged commit fe93a9f into integration/canton Jul 15, 2026
34 of 38 checks passed
@bengtlofgren
bengtlofgren deleted the cs/remove-keys branch July 15, 2026 11:47
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.

2 participants