PoC: buy dedicated-synchronizer traffic in CC (rung 2/2)#2
Conversation
Sibling of AmuletRules_BuyMemberTraffic that funds traffic for a governance-registered dedicated synchronizer (rung 1) by burning CC on the global synchronizer. Additive only, no MemberTraffic schema change. - DedicatedSyncTraffic template (splice-amulet): DSO-signed purchase record observed by the synchronizer operator. - AmuletRules_BuyDedicatedSyncTraffic: fetches the disclosed RegisteredSynchronizer, enforces the min-topup floor (no requiredSynchronizers gate), reuses computeSynchronizerFees + splitAndBurn, and records DedicatedSyncTraffic. - validateDedicatedSyncTopupAmount helper + result record. - TestBuyDedicatedSyncTraffic: happy path + below-min-topup negative. Not built locally (no dpm/nix env). Draft until CI runs sbt 'splice-amulet-test-daml/Test/damlTest'.
| -- 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.
The purchase record. Deliberately a separate template from MemberTraffic rather than adding an operator observer field to it: MemberTraffic has ~41 Scala/TS codegen consumers and no observers today, so an observer field is a breaking schema change. A sibling keeps rung 2 additive and CI green. signatory dso + observer operator (line below) is what lets the operator's node ingest purchases on-ledger.
| -- * the created record (`DedicatedSyncTraffic`) is OBSERVED by the synchronizer's operator so | ||
| -- the operator's node can ingest the purchase on-ledger. | ||
| -- CC burning and fee computation are reused unchanged (`splitAndBurn`, `computeSynchronizerFees`). | ||
| nonconsuming choice AmuletRules_BuyDedicatedSyncTraffic : AmuletRules_BuyDedicatedSyncTrafficResult |
There was a problem hiding this comment.
Sibling of AmuletRules_BuyMemberTraffic (just above). Same controller provider + nonconsuming shape and the same CC-burn machinery; the only real differences are how the target synchronizer is authorized and where the purchase is recorded -- see the two comments below.
| -- Read the registration (disclosed). Its `synchronizerId` is authoritative and its `operator` | ||
| -- becomes the observer. `RegisteredSynchronizer_Fetch` lets `provider` read a contract it is | ||
| -- not a stakeholder of, mirroring how `computeSynchronizerFees` reads the OpenMiningRound. | ||
| registered <- fetchPublicReferenceData (ForDso dso) registeredSynchronizerCid (RegisteredSynchronizer_Fetch provider) |
There was a problem hiding this comment.
Difference #1 vs BuyMemberTraffic: the target sync is authorized by a disclosed RegisteredSynchronizer (rung 1) rather than by requiredSynchronizers. fetchPublicReferenceData + the public RegisteredSynchronizer_Fetch choice let provider -- not a stakeholder -- read it, exactly as computeSynchronizerFees reads the OpenMiningRound. registered.synchronizerId/registered.operator are authoritative from here on.
| -- Only the min-topup floor applies here; the `requiredSynchronizers` gate is intentionally | ||
| -- skipped because the `RegisteredSynchronizer` is what authorizes this synchronizer id. | ||
| configUsd <- getValueAsOfLedgerTime configSchedule | ||
| case validateDedicatedSyncTopupAmount configUsd trafficAmount of |
There was a problem hiding this comment.
Difference #2: only the minTopupAmount floor is enforced (validateDedicatedSyncTopupAmount), NOT the requiredSynchronizers gate -- the registration is the authorization. Everything after this (computeSynchronizerFees, splitAndBurn) is reused unchanged, so the CC burn is identical to a member-traffic purchase.
| -- PoC (dedicated-synchronizer traffic): like `validateBuyMemberTrafficInputs` but WITHOUT the | ||
| -- `requiredSynchronizers` gate. `AmuletRules_BuyDedicatedSyncTraffic` authorizes the synchronizer via | ||
| -- a `RegisteredSynchronizer` contract instead, so only the `minTopupAmount` floor is enforced here. | ||
| validateDedicatedSyncTopupAmount : AmuletConfig Unit.USD -> Int -> Either FailureStatus () |
There was a problem hiding this comment.
Helper that drops the requiredSynchronizers check from validateBuyMemberTrafficInputs and keeps only the min-topup floor. Split out (rather than parameterizing the existing validator) so the member-traffic path is byte-for-byte untouched.
|
|
||
| -- | Result of `AmuletRules_BuyDedicatedSyncTraffic` (PoC). Same shape as | ||
| -- `AmuletRules_BuyMemberTrafficResult` but returns the `DedicatedSyncTraffic` cid. | ||
| data AmuletRules_BuyDedicatedSyncTrafficResult = AmuletRules_BuyDedicatedSyncTrafficResult with |
There was a problem hiding this comment.
Result record mirroring AmuletRules_BuyMemberTrafficResult; the only field change is dedicatedTraffic : ContractId DedicatedSyncTraffic in place of purchasedTraffic.
| -- visibility of the DSO-signed AmuletRules / OpenMiningRound / RegisteredSynchronizer in-script; | ||
| -- on a live ledger these are supplied via explicit disclosure. | ||
| result <- submit (actAs provider.primaryParty <> readAs app.dso) $ | ||
| exerciseCmd amuletRulesCid AmuletRules_BuyDedicatedSyncTraffic with |
There was a problem hiding this comment.
Exercises the buy for a synchronizer id that is intentionally NOT in requiredSynchronizers, submitted with readAs app.dso for in-script visibility (explicit disclosure on a live ledger). The assertions below check the CC was actually burned and the DedicatedSyncTraffic carries the operator/synchronizer from the registration -- and that the operator can see it as an observer.
| context <- getTransferContext app provider None | ||
| (amuletRulesCid, _) <- fetchAmuletRulesByKey app.dso | ||
|
|
||
| submitMustFail (actAs provider.primaryParty <> readAs app.dso) $ |
There was a problem hiding this comment.
Negative case: trafficAmount = 1 is below minTopupAmount, so the buy is rejected even though the requiredSynchronizers gate is skipped -- proves dropping that gate did not drop the floor.
…dedicated-sync tests - DsoRules.daml: import the RegisteredSynchronizer data constructor so DsoRules_RegisterSynchronizer can create it (fixes the damlBuild compile error). - TestBuyDedicatedSyncTraffic: add negatives (wrong expectedDso; RegisteredSynchronizer empty-id ensure). Both splice-amulet and splice-dso-governance damlBuild clean; all Daml Script tests green.
|
Status update — compiles + tested green.
Issues addressed: #21 (E1-3, Onboarding / full status: ChainSafe/canton-extending-mainnet#9. |
Dedicated-synchronizer traffic funding — PoC (rung 2 of 2)
Stacked on #1 (registration). Review that one first; this PR's base is
daml-poc-registration, so the diff here is only rung 2.Rung 1 lets the DSO register a dedicated (non-global) synchronizer. This rung lets anyone buy traffic for a registered synchronizer by burning CC on the global synchronizer.
What this PR adds
AmuletRules_BuyDedicatedSyncTraffic(splice-amulet,AmuletRules.daml): a sibling ofAmuletRules_BuyMemberTraffic. It reads a disclosedRegisteredSynchronizer(via the publicRegisteredSynchronizer_Fetchfrom rung 1), enforces theminTopupAmountfloor, reusescomputeSynchronizerFees+splitAndBurnunchanged to burn CC, and records the purchase.DedicatedSyncTraffic(splice-amulet,DecentralizedSynchronizer.daml): the purchase record. DSO-signed, observed by the synchronizer's operator so the operator's node can ingest it on-ledger.validateDedicatedSyncTopupAmount+AmuletRules_BuyDedicatedSyncTrafficResult.TestBuyDedicatedSyncTraffic: happy path (registered non-required sync → CC burned +DedicatedSyncTrafficcreated, operator observes it) and a negative (below-min-topup rejected).Key differences from
AmuletRules_BuyMemberTrafficrequiredSynchronizersRegisteredSynchronizerMemberTraffic(no observers)DedicatedSyncTraffic(operator = observer)splitAndBurn/computeSynchronizerFeesDesign notes
MemberTraffic,AmuletRules_BuyMemberTraffic, or the wallet bridge → no Scala/TS codegen churn.MemberTrafficobserver field: adding an observer toMemberTrafficis a breaking schema change touching ~41 Scala consumers. A sibling keeps rung 2 isolated and CI green.SetTrafficPurchasedmanually.Not yet verified
Written without a local build — no
dpm/nix env available. Please let CI (or a local nix build) runsbt 'splice-amulet-test-daml/Test/damlTest'. Draft until green.Inline comments walk through each hunk.