Skip to content

Commit fe93a9f

Browse files
authored
canton: remove all contract keys (#39)
* canton: remove all contract keys; address contracts by cid * canton: direct emitter registration off a disclosed registry; no operator crank * canton: document cid addressing and aggregate the disclosure-service surface * canton: free replay roots; single-trie discipline is the consumer's own
1 parent ba90531 commit fe93a9f

10 files changed

Lines changed: 402 additions & 310 deletions

File tree

canton/README.md

Lines changed: 160 additions & 96 deletions
Large diffs are not rendered by default.

canton/core/daml/Wormhole/Core/Bytes.daml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ data RegistrarText = RegistrarText Text deriving (Eq, Show)
131131
data OwnerText = OwnerText Text deriving (Eq, Show)
132132
data RegistryId = RegistryId Int deriving (Eq, Show)
133133

134-
-- | Canonical 32-byte registry address, keyed off the components of a
135-
-- registry-allocated contract key:
134+
-- | Canonical 32-byte registry address, derived from the components of a
135+
-- registry-allocated identity:
136136
--
137137
-- > keccak256( utf8(tag) ‖ lp(utf8(registrar)) ‖ lp(utf8(owner)) ‖ uint64be(id) )
138138
--

canton/core/daml/Wormhole/Core/Replay.daml

Lines changed: 39 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,18 @@
3535
-- its subspace — no covering node, nothing verifies — fail-closed, the
3636
-- opposite of the key model.
3737
--
38-
-- The replay vector is therefore node RE-CREATION (archive-and-recreate, or a
39-
-- parallel shadow root), which the signatory design closes: the @operator@
40-
-- co-signs every node, so the consumer alone can neither archive nor create
41-
-- one, and roots are granted exactly once per consumer through the
42-
-- 'ReplayRootRegistry' — a positive, on-ledger one-root-per-consumer check
43-
-- linearized by consuming the registry (the 'EmitterRegistry' pattern, minus
44-
-- the key). Splits inherit both signatures from the archived parent, so the
45-
-- operator is not involved per consume. The irreducible remainder: consumer
46-
-- and operator COLLUDING can archive and rebuild a trie — signatories can
47-
-- always unmake their own contracts.
38+
-- The replay vector is therefore node RE-CREATION. Against THIRD PARTIES the
39+
-- signatory design closes it: the @operator@ co-signs every node (inherited
40+
-- from the 'ReplayRootRegistry' anchor at the root, from the archived parent
41+
-- at splits — the operator never signs per consume), and claiming a root for
42+
-- a consumer requires that consumer's authority, so nobody can archive,
43+
-- re-create, or fork someone ELSE'S replay scope. Against the consumer
44+
-- ITSELF there is deliberately no on-ledger defense: a consumer can mint
45+
-- parallel roots for its own scope (see 'ReplayRootRegistry'), which is
46+
-- self-harm — replay protection is the consumer's own safety property, and a
47+
-- compromised consumer key defeats the app more directly anyway. Same class
48+
-- as the irreducible signatory remainder: signatories can always unmake (or
49+
-- here, duplicate) their own state.
4850
--
4951
-- OFF-LEDGER: consumers (and their users) locate the covering node for a
5052
-- digest via a disclosure service — an ACS index over (consumer, prefix) that
@@ -53,7 +55,7 @@
5355
-- RETRY PROTOCOL: racing consumes on the same node conflict and exactly one
5456
-- commits; on rejection, re-resolve the covering node (the cid churns on every
5557
-- consume) and resubmit. A blind retry of a consume that actually committed
56-
-- hits the membership check and fails "VAA already consumed" — a clean
58+
-- hits the membership check and fails "digest already consumed" — a clean
5759
-- idempotency signal.
5860
module Wormhole.Core.Replay where
5961

@@ -123,7 +125,7 @@ template ReplayNode
123125
-- The replay check MUST precede the split branch: a split inserting a
124126
-- duplicate would be a silent Set.insert no-op — a replay that
125127
-- "succeeds".
126-
assertMsg "VAA already consumed" (not (Set.member suffix consumed))
128+
assertMsg "digest already consumed" (not (Set.member suffix consumed))
127129
if Set.size consumed < splitThreshold || T.length prefix == 63
128130
-- Depth-63 nodes never split: suffixes are single nibbles (≤16
129131
-- distinct, so the set is bounded), and children would be degenerate
@@ -143,52 +145,41 @@ template ReplayNode
143145
Some cid -> pure cid
144146
None -> abort "unreachable: split lost the covering child"
145147

146-
-- | Grants each consumer AT MOST ONE trie root — the positive, on-ledger
147-
-- uniqueness check that a contract key cannot provide. One registry per
148-
-- operator, created at setup ('mkInitialReplayRootRegistry'); every grant
149-
-- consumes it, so grants are linearized through the operator. The set grows by
150-
-- one Party per integrator app — dozens, not millions.
148+
-- | Hands out trie roots to any consumer that asks — a stateless O(1) anchor
149+
-- whose one nonconsuming choice lends the operator's INHERITED co-signature to
150+
-- consumer-submitted claims (the 'RegisterEmitter' pattern): no operator
151+
-- crank, no contention, no churn — the registry's disclosure blob is static
152+
-- and cacheable. One per operator, created at setup
153+
-- ('mkInitialReplayRootRegistry').
154+
--
155+
-- There is deliberately NO one-root-per-consumer enforcement: maintaining a
156+
-- single trie is the CONSUMER'S OWN responsibility (its disclosure service
157+
-- defines which trie its consumes resolve into). The controller is what keeps
158+
-- that sound against everyone else — a third party cannot fork someone else's
159+
-- replay scope, because claiming a root for @consumer@ requires @consumer@'s
160+
-- authority. Parallel roots can only be minted by the consumer itself (or its
161+
-- stolen key), which is self-harm of the same class as the signatory
162+
-- remainder above. Spam claims cost the spammer synchronizer traffic per
163+
-- transaction and the operator ~300 B of storage per worthless root; no
164+
-- shared contract grows. If consumer-key-compromise resilience is ever
165+
-- wanted, the hardening is trie-identity pinning: thread the original root's
166+
-- cid through successors (via @self@ in 'ConsumeDigest') and pin it in the
167+
-- app contract — documented, not built.
151168
template ReplayRootRegistry
152169
with
153170
operator : Party
154-
granted : Set Party
155171
where
156172
signatory operator
157173

158-
choice GrantReplayRoot : ContractId ReplayRootRegistry
159-
with
160-
consumer : Party
161-
controller operator
162-
do
163-
assertMsg "consumer already has a replay root" (not (Set.member consumer granted))
164-
create this with granted = Set.insert consumer granted
165-
166-
-- | The consumer's half of the root-creation ceremony: a one-time
167-
-- propose-accept per app (mirroring 'EmitterRequest'). The consumer's
168-
-- signature on this request is what authorizes its co-signature on the root;
169-
-- the operator accepts against its registry, which enforces
170-
-- one-root-per-consumer.
171-
template ReplayRootRequest
172-
with
173-
consumer : Party
174-
operator : Party
175-
splitThreshold : Int -- normally 'defaultSplitThreshold'; tiny in tests
176-
where
177-
signatory consumer
178-
observer operator
179-
180-
choice ApproveReplayRoot : (ContractId ReplayRootRegistry, ContractId ReplayNode)
174+
nonconsuming choice ClaimReplayRoot : ContractId ReplayNode
181175
with
182-
registryCid : ContractId ReplayRootRegistry -- passed by cid: no keys
183-
controller operator
176+
consumer : Party
177+
splitThreshold : Int -- normally 'defaultSplitThreshold'; tiny in tests
178+
controller consumer
184179
do
185-
reg <- fetch registryCid
186-
assertMsg "registry belongs to a different operator" (reg.operator == operator)
187-
regCid' <- exercise registryCid GrantReplayRoot with consumer
188-
rootCid <- create ReplayNode with
180+
create ReplayNode with
189181
consumer
190182
operator
191183
prefix = ""
192184
splitThreshold
193185
consumed = Set.empty
194-
pure (regCid', rootCid)

canton/core/daml/Wormhole/Core/Setup.daml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ mkInitialCoreState operator guardianGovernance govChain govContract initialGuard
5050

5151
-- | The emitter-id registry the operator creates alongside the 'CoreState' at
5252
-- setup. Emitter ids are allocated sequentially; it also carries
53-
-- @guardianObserver@ for 'ApproveEmitter' to thread into each 'Emitter'.
53+
-- @guardianObserver@ for 'RegisterEmitter' to thread into each 'Emitter'.
5454
mkInitialEmitterRegistry : Party -> Party -> EmitterRegistry
5555
mkInitialEmitterRegistry operator guardianObserver =
5656
EmitterRegistry with operator, guardianObserver, nextId = 0
5757

58-
-- | The replay-root registry the operator creates alongside the 'CoreState' at
59-
-- setup. Grants each integrator consumer at most one 'ReplayNode' trie root
60-
-- (see 'Wormhole.Core.Replay').
58+
-- | The replay-root registry (the stateless claim anchor) the operator
59+
-- creates alongside the 'CoreState' at setup. Hands a 'ReplayNode' trie root
60+
-- to any consumer that claims one (see 'Wormhole.Core.Replay').
6161
mkInitialReplayRootRegistry : Party -> ReplayRootRegistry
6262
mkInitialReplayRootRegistry operator =
63-
ReplayRootRegistry with operator, granted = Set.empty
63+
ReplayRootRegistry with operator

0 commit comments

Comments
 (0)