canton: remove all contract keys#39
Merged
Merged
Conversation
This was referenced Jul 13, 2026
bengtlofgren
approved these changes
Jul 15, 2026
Comment on lines
+173
to
+177
| nonconsuming choice GetOperator : Party | ||
| with | ||
| reader : Party | ||
| controller reader | ||
| do pure operator |
There was a problem hiding this comment.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
After the replay-protection redesign (#38), contract keys survived in the package only as indexes:
CoreStatekeyed by the governance anchor,EmitterRegistrykeyed by the operator,Emitterkeyed 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
CoreStatenaming 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'sCreatedEventto 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:
RegisterEmitteron the disclosed registry allocates the id and creates the co-signedEmitterin 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 onlynextId : Int, so permissionless writes cannot bloat it.)ReplayRootRegistryshrinks to a stateless, never-churning anchor whose nonconsumingClaimReplayRootmints 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
fetcha 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, soCoreStateexposes a nonconsuming, flexible-controllerGetOperator— 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 fabricatedCoreState(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