PoC (draft): buy registered-synchronizer traffic in CC (rung 2/2) - #2
PoC (draft): buy registered-synchronizer traffic in CC (rung 2/2)#2salindne wants to merge 10 commits into
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 |
| migrationId = first.migrationId | ||
| fetchAndArchive expectedTrafficContract trafficCid | ||
| let traffics = first::rest | ||
| -- all merged contracts must agree on the operator observer, which carries through the merge |
There was a problem hiding this comment.
When would the synchronizer ids match but the operators not match? Asked differently is this just defence in depth (which is totally reasonable) or do we actually expect it to trigger?
There was a problem hiding this comment.
Defence in depth — with unique registrations and no operator rotation it should never trigger. It exists because free-standing registrations don't structurally prevent two entries for one sync id with different operators; if that ever happens, refusing the merge keeps the mixed-operator set visible instead of silently blending it. Comment on the assert now says so (c61ca53).
| provider : Party | ||
| memberId : Text | ||
| synchronizerId : Text | ||
| migrationId : Int |
There was a problem hiding this comment.
I would suggest to enforce migrationId = 0 when registered synchronizer is set. Migration ids at this point are only a historical thing. We need to keep them for the global synchronizer but they make no sense for any new synchronizer.
There was a problem hiding this comment.
Agreed, and done in c61ca53: the buy now rejects a non-zero migrationId when a registration is supplied (dedicated failure status + negative test), folded into the unified validation per your other comment.
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. |
Draft 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).