PoC (draft): buy dedicated-synchronizer traffic in CC (rung 2/2)#2
PoC (draft): buy dedicated-synchronizer traffic in CC (rung 2/2)#2salindne wants to merge 7 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 |
| -- operator's node can ingest purchases on-ledger (event-driven, no polling). It is a SEPARATE | ||
| -- template so the shared `MemberTraffic` (and its many Scala/TS codegen consumers) is untouched by | ||
| -- this PoC. Created by `AmuletRules_BuyDedicatedSyncTraffic`. | ||
| template DedicatedSyncTraffic with |
There was a problem hiding this comment.
Flagging for when this carries real traffic: we'll need a merge path for these records. Every purchase creates one (numPurchases stays 1) and nothing consumes them — at the auto-top-up cadence that's ~144/day per active member per sync. MemberTraffic handles this with DsoRules_MergeMemberTrafficContracts + the SV merge trigger + numMemberTrafficContractsThreshold; since this is deliberately a separate template we need the twin of that machinery. Without it: every SV participant stores every record forever (dso is sole signatory), the reconciler's exact sum per (memberId, synchronizerId, migrationId) is O(n) over an ever-growing set (the grant is an absolute total, so no windowing), and none of it is ever prunable.
One gotcha: the merged record must keep operator as observer, or our on-ledger ingestion goes blind right when compaction starts. Ties into Itai's note about scoping the merge trigger per sync id — same work item from this template's side.
There was a problem hiding this comment.
Agreed, this is required before it carries real traffic. Tracked in ChainSafe/canton-extending-mainnet#34, which I've expanded to cover the on-ledger merge choice (twin of DsoRules_MergeMemberTrafficContracts) + a threshold config + per-sync-id scoping, and I called out the operator-must-stay-observer constraint explicitly. Out of scope for this on-ledger PoC (it's in the next workstream), but captured. Thanks for the detailed flag.
| 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.
| senderChangeAmulet = transferResult.senderChangeAmulet | ||
| meta = Some meta | ||
|
|
||
| -- PoC (dedicated-synchronizer traffic): buy extra traffic for a DEDICATED (non-decentralized) |
There was a problem hiding this comment.
Tiny one: "non-decentralized" reads backwards — a BFT extension sync is decentralized. Maybe "separately-operated synchronizer"? Stays inside the vocabulary rules.
There was a problem hiding this comment.
Good catch, "non-decentralized" does read backwards. Will change to "separately-operated synchronizer" (stays within the vocabulary rules). Comment-only, so it rides the next DAR regen.
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 dedicated synchronizer, reusing Splice's existing traffic-fee and burn machinery unchanged.
AmuletRules_BuyDedicatedSyncTrafficchoice:RegisteredSynchronizer(supplied via explicit disclosure), the registration gate from rung 1;computeSynchronizerFees(bytes to USD to CC) andsplitAndBurnexactly as the existingAmuletRules_BuyMemberTrafficdoes, so the economics are identical to the decentralized-sync traffic buy;minTopupAmountfloor and anexpectedDsoanti-tamper check;DedicatedSyncTraffic.DedicatedSyncTraffictemplate: the purchase record. Signatory is the DSO party, and it is observed by the operator, so the operator's node can pick up the purchase on-ledger (event-driven, no polling) and later grant the traffic on its own sequencer. Separate template; noMemberTrafficschema change.How it's verified (
TestBuyDedicatedSyncTraffic, Daml Script)test_BuyDedicatedSyncTraffic(happy path): register, fund a buyer, run the buy; asserts a non-zero Amulet burn (amuletPaid > 0), aDedicatedSyncTrafficwith the expecteddso/operator/synchronizerId/memberId/totalPurchased, and operator visibility.minTopupAmountrejected; mismatchedexpectedDsorejected before any burn; emptysynchronizerIdrejected.Design notes
Additive and reuse-first: no new economics, no
MemberTrafficschema change, no change to the existingAmuletRules_BuyMemberTrafficpath. The registration gate means traffic can only be bought for a governance-registered synchronizer.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
Internal work breakdown in ChainSafe/canton-extending-mainnet (private): #29 (
AmuletRules_BuyDedicatedSyncTraffic+DedicatedSyncTraffic); part of Epic #13 (E1) and #12 (E0). Follow-ups from review: #34 (merge machinery), and registry-heldmigrationIdvalidation (#27 / #29).