PoC: buy registered-synchronizer traffic in CC (rung 2/2) - #2
Conversation
Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
51a8808 to
56e4666
Compare
05a8667 to
027008d
Compare
Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
… [ci] Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
The buy rung adds AmuletRules_BuyDedicatedSyncTraffic + DedicatedSyncTraffic to splice-amulet, so its DAR (and dependents that embed it) differ from the registration rung. Regenerated with 'sbt damlDarsLockFileUpdate' after merging the registration SCU + DAR-lock fix. [ci] Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
…al_daml_references) Splice's rename.sh no_illegal_daml_references check bans certain words in Daml (global, coin, domain, ...) and restricts DSO/member phrasing. Our PoC comments used several. Comment-only rewrite (no behaviour change): - global -> decentralized (the DSO-run synchronizer; matches the module name) - CC -> Amulet (the on-ledger token; 'Canton Coin' is also banned via 'coin') - DSO <x> -> 'DSO party' / 'DSO governance' (the only allowed forms) - reworded 'Dso'/'dso'/'splice-dso-governance' comment mentions Verified locally: scripts/rename.sh no_illegal_daml_references exits 0, and the full daml static-check suite passes. Rebuilt checked-in DARs + dars.lock. [ci] Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
…uy artifacts Reconcile buy onto @timwu20's registration work (c514673): - adopt his terminology wording for the shared files (DecentralizedSynchronizer RegisteredSynchronizer part, DsoRules, TestRegisterSynchronizer) - keep buy-only content (AmuletRules_BuyDedicatedSyncTraffic, DedicatedSyncTraffic, TestBuyDedicatedSyncTraffic) with the same approved terminology - regenerate checked-in DARs, dars.lock, and DarResources.scala for the buy rung (fixes the 'Verify no changes in SBT test files' static check) Verified locally: scripts/rename.sh no_illegal_daml_references exits 0; DARs + DarResources regenerated via damlDarsLockFileUpdate + updateDarResources. [ci] Signed-off-by: Sebastian Lindner <33971232+salindne@users.noreply.github.com>
|
The PoC's registration model is a set of free-standing, DSO-signed The scenario that makes this bite needs no attacker, just operator rotation: sync S hands over from operator A to B; governance registers (S, B); the old (S, A) contract stays active because nothing archives it. Buyers holding the old cid keep purchasing against it — the burn succeeds, the The template can't self-defend as written: choices can't query the ACS, and Daml 3.x has no contract keys — so uniqueness needs a serialization point. Proposal: make a singleton registry the only factory for registration children, keeping the operator-observer and disclosed-fetch mechanics of this PR untouched: Because every register/rotate consumes the singleton, concurrent registrations become a clean ledger conflict (one wins, one rejects) — uniqueness by UTXO semantics rather than by discipline. Rotation archives the predecessor in the same transaction, and since an archived cid can no longer be fetched, buyers holding a stale reference fail fast instead of burning into a void. Serialization through one contract is a non-cost at governance cadence, and a dedicated singleton avoids growing |
| context : TransferContext -- ^ standard transfer context (open mining round, etc.) | ||
| provider : Party -- ^ the buyer; burns Amulet from its own balance | ||
| memberId : Text -- ^ participant whose dedicated-sync traffic is credited (need not be `provider`) | ||
| migrationId : Int |
There was a problem hiding this comment.
Heads-up: migrationId is buyer-supplied with no validation and no on-ledger discovery path. The reconciler will only sum the current migration id, but for a dedicated sync that id is operator-managed — a buyer passing a stale one burns Amulet into a record we'll never count, silently. Upstream BuyMemberTraffic has the same shape, but there the top-up automation knows the gsync migration id; our buyers have no equivalent.
If we go the registry-singleton route (see the registry comment), the registry entry is the natural home for the sync's current migrationId — closes the validation gap and gives buyers discovery in one move. (memberId is similarly free text with only a non-empty check; lower stakes, same idea.)
There was a problem hiding this comment.
Agreed, and the registry-singleton fix is the right shape. Tracked: ChainSafe/canton-extending-mainnet#27 (hold the sync's current migrationId on the RegisteredSynchronizer entry, for validation + buyer discovery) and ChainSafe/canton-extending-mainnet#29 (validate the buy against it), which closes the silent-loss and discovery gaps in one move. We'll decide whether to fold it into this PoC now or take it next iteration.
There was a problem hiding this comment.
Still open under the reworked shape: migrationId remains buyer-supplied, now with exactly the same trust surface as the existing decentralized-sync path. The registration doesn't pin it. Options are registry-held migrationId (tracked in ChainSafe/canton-extending-mainnet#27) or accepting parity with the existing path — input welcome.
There was a problem hiding this comment.
Concrete proposal for this one after discussing internally: registry-held and operator-maintained. The registration gains a migrationId field (initial value set by the registering vote); the operator gets a consuming choice on their own entry to bump it (monotonic), no vote needed — a wrong bump only breaks their own buyers, so it's self-policing. The buy then validates the supplied migrationId against the disclosed registration, and the operator's reconciler reads the same entry, closing the silent-burn path (a stale id today burns CC into a record the ingestion filter drops before the reconciler ever sums it). Registration and revocation stay governance-gated; only the epoch is operator self-service. Tracked as ChainSafe/canton-extending-mainnet#55 — flagging for your read before we build it into the rung-1 rework.
…uyMemberTraffic (rung 2 rework) [ci] Rework the buy rung from a sibling path to the design-doc shape: extend the existing member-traffic purchase instead of duplicating it. - MemberTraffic: appended optional operator field (upgrade-safe), observed by the registered synchronizer's operator so its node can ingest purchases on-ledger - AmuletRules_BuyMemberTraffic: appended optional RegisteredSynchronizer arg; a disclosed registration authorizes the synchronizer id in place of the requiredSynchronizers gate (the min-topup floor still applies) - AmuletRules_MergeMemberTrafficContracts: merged contracts must agree on the operator, which carries through the merge - drop the sibling AmuletRules_BuyDedicatedSyncTraffic choice, DedicatedSyncTraffic template, and twin validator - rework the Daml Script tests to the new shape; fix Scala test fixtures for the new codegen arity - regenerate checked-in DARs, dars.lock, DarResources Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
|
Following up on the registration-model comment now that the rework has landed: still open, and slightly sharper — nothing stops a second |
Two parts here:
|
…onId=0 gate) Per review on PR #2: - fold all buy validation into validateBuyMemberTrafficInputs (registration passed in): min-topup floor in all cases, requiredSynchronizers gate without a registration, id-match + migrationId = 0 with one - registered synchronizers have no migration history; enforce migrationId 0 with a dedicated failure status (new negative test) - forA instead of the case block for the registration fetch - comment cleanups: drop appended-last notes; state the merge assert's defence-in-depth intent Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
Works for us on both counts. Dropping operator rotation from scope kills the nastiest path (stale registration after handover), and with registrations being vote-gated, an SV-UI duplicate check is a proportionate guard — we'll re-scope our tracking issue from the consume-once registry to that check. |
…edence [ci] The unified validator checked the minTopupAmount floor before the synchronizer authorization, changing which failure fires when a request is wrong in both ways; wallet tests pin the original precedence (unknown-synchronizer first). Authorization checks (requiredSynchronizers gate, registration id-match, migrationId = 0) now run before the floor, which still applies in all cases. Regenerate artifacts. Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
…or [ci] Per review: factor validateTopupAmount out of the case so it is visibly applied in all cases; authorization checks run first, preserving the failure precedence wallet tests pin. No behavior change. Regenerate artifacts. Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
PoC, rung 2 of 2, stacked on
multi-sync-poc-registration(#1).What this does
Adds the on-ledger purchase step: a buyer burns Canton Coin (Amulet) on the decentralized synchronizer to buy extra traffic for a registered synchronizer, through the existing
AmuletRules_BuyMemberTraffic.AmuletRules_BuyMemberTrafficgains an appended optional argument,optRegisteredSynchronizer:None(all existing callers): behavior is unchanged, byte for byte.Some cid: the disclosedRegisteredSynchronizer(the registration gate from rung 1) authorizes the synchronizer id in place of therequiredSynchronizerscheck; the id must match the registration, and theminTopupAmountfloor andexpectedDsoanti-tamper check still apply.computeSynchronizerFeesandsplitAndBurnunchanged, so the economics are identical to the decentralized-sync traffic buy. The result type is unchanged, so Scan/wallet transaction-log parsing is untouched.MemberTrafficgains an appended optionaloperatorfield, set from the registration and made an observer of the record — the operator's node picks up the purchase on-ledger (event-driven, no polling) and later grants the traffic on its own sequencer. Existing contracts carryNoneand behave as today; the appended-optional field and observer expression are upgrade-safe under Daml's smart-contract-upgrade rules.AmuletRules_MergeMemberTrafficContractsrequires all merged contracts to agree on the operator and carries it through the merge, so compaction reuses the existing machinery.An earlier draft of this rung explored a sibling path (
AmuletRules_BuyDedicatedSyncTraffic+ a separateDedicatedSyncTraffictemplate); it was dropped in favor of the shape above.How it's verified (
TestBuyRegisteredSyncTraffic, Daml Script)test_BuyRegisteredSyncTraffic(happy path): register, fund a buyer, run the buy with the registration; asserts a non-zero Amulet burn, aMemberTrafficwith the expecteddso/operator/synchronizerId/memberId/totalPurchased, and operator visibility.test_BuyMemberTraffic_withoutRegistrationUnchanged: without a registration the choice behaves exactly as before — the decentralized-sync buy succeeds with no operator observer; an unknown synchronizer id still fails therequiredSynchronizersgate.minTopupAmountrejected on the registered path; mismatchedexpectedDsorejected before any burn; emptysynchronizerIdrejected.test_MergeMemberTraffic_operatorPreserved: merging preserves the operator and sums the totals; merging contracts with disagreeing operators is rejected.The full amulet, wallet, and dso-governance Daml Script suites pass with the change.
Design notes
Reuse-first: no new economics, one record type for all traffic purchases, and the existing merge/reconcile/Scan machinery applies to registered-synchronizer purchases as-is. The registration gate means traffic can only be bought for a governance-registered synchronizer. All template/choice changes are additive (appended optional fields) and upgrade-safe.
Out of scope (next phase)
On-ledger only. This proves authorize, burn, record. The operator actually granting traffic on its sequencer (reconcile trigger + operator node) and a LocalNet register/buy/grant end-to-end are the next workstream.
Tracked in
Implements E1-3 (ChainSafe/canton-extending-mainnet#29; ChainSafe work breakdown), part of Epic E1 (#13). The on-ledger half of the merge follow-up is done here (existing merge reused, operator-aware); #34 now tracks the residual operator-aware trigger grouping. Registry-held
migrationIdvalidation and registry uniqueness are tracked as follow-ups (ChainSafe/canton-extending-mainnet#55 / #54).