Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 160 additions & 96 deletions canton/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions canton/core/daml/Wormhole/Core/Bytes.daml
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ data RegistrarText = RegistrarText Text deriving (Eq, Show)
data OwnerText = OwnerText Text deriving (Eq, Show)
data RegistryId = RegistryId Int deriving (Eq, Show)

-- | Canonical 32-byte registry address, keyed off the components of a
-- registry-allocated contract key:
-- | Canonical 32-byte registry address, derived from the components of a
-- registry-allocated identity:
--
-- > keccak256( utf8(tag) ‖ lp(utf8(registrar)) ‖ lp(utf8(owner)) ‖ uint64be(id) )
--
Expand Down
87 changes: 39 additions & 48 deletions canton/core/daml/Wormhole/Core/Replay.daml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@
-- its subspace — no covering node, nothing verifies — fail-closed, the
-- opposite of the key model.
--
-- The replay vector is therefore node RE-CREATION (archive-and-recreate, or a
-- parallel shadow root), which the signatory design closes: the @operator@
-- co-signs every node, so the consumer alone can neither archive nor create
-- one, and roots are granted exactly once per consumer through the
-- 'ReplayRootRegistry' — a positive, on-ledger one-root-per-consumer check
-- linearized by consuming the registry (the 'EmitterRegistry' pattern, minus
-- the key). Splits inherit both signatures from the archived parent, so the
-- operator is not involved per consume. The irreducible remainder: consumer
-- and operator COLLUDING can archive and rebuild a trie — signatories can
-- always unmake their own contracts.
-- The replay vector is therefore node RE-CREATION. Against THIRD PARTIES the
-- signatory design closes it: the @operator@ co-signs every node (inherited
-- from the 'ReplayRootRegistry' anchor at the root, from the archived parent
-- at splits — the operator never signs per consume), and claiming a root for
-- a consumer requires that consumer's authority, so nobody can archive,
-- re-create, or fork someone ELSE'S replay scope. Against the consumer
-- ITSELF there is deliberately no on-ledger defense: a consumer can mint
-- parallel roots for its own scope (see 'ReplayRootRegistry'), which is
-- self-harm — replay protection is the consumer's own safety property, and a
-- compromised consumer key defeats the app more directly anyway. Same class
-- as the irreducible signatory remainder: signatories can always unmake (or
-- here, duplicate) their own state.
--
-- OFF-LEDGER: consumers (and their users) locate the covering node for a
-- digest via a disclosure service — an ACS index over (consumer, prefix) that
Expand All @@ -53,7 +55,7 @@
-- RETRY PROTOCOL: racing consumes on the same node conflict and exactly one
-- commits; on rejection, re-resolve the covering node (the cid churns on every
-- consume) and resubmit. A blind retry of a consume that actually committed
-- hits the membership check and fails "VAA already consumed" — a clean
-- hits the membership check and fails "digest already consumed" — a clean
-- idempotency signal.
module Wormhole.Core.Replay where

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

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

choice GrantReplayRoot : ContractId ReplayRootRegistry
with
consumer : Party
controller operator
do
assertMsg "consumer already has a replay root" (not (Set.member consumer granted))
create this with granted = Set.insert consumer granted

-- | The consumer's half of the root-creation ceremony: a one-time
-- propose-accept per app (mirroring 'EmitterRequest'). The consumer's
-- signature on this request is what authorizes its co-signature on the root;
-- the operator accepts against its registry, which enforces
-- one-root-per-consumer.
template ReplayRootRequest
with
consumer : Party
operator : Party
splitThreshold : Int -- normally 'defaultSplitThreshold'; tiny in tests
where
signatory consumer
observer operator

choice ApproveReplayRoot : (ContractId ReplayRootRegistry, ContractId ReplayNode)
nonconsuming choice ClaimReplayRoot : ContractId ReplayNode
with
registryCid : ContractId ReplayRootRegistry -- passed by cid: no keys
controller operator
consumer : Party
splitThreshold : Int -- normally 'defaultSplitThreshold'; tiny in tests
controller consumer
do
reg <- fetch registryCid
assertMsg "registry belongs to a different operator" (reg.operator == operator)
regCid' <- exercise registryCid GrantReplayRoot with consumer
rootCid <- create ReplayNode with
create ReplayNode with
consumer
operator
prefix = ""
splitThreshold
consumed = Set.empty
pure (regCid', rootCid)
10 changes: 5 additions & 5 deletions canton/core/daml/Wormhole/Core/Setup.daml
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ mkInitialCoreState operator guardianGovernance govChain govContract initialGuard

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

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