Skip to content

PoC: buy dedicated-synchronizer traffic in CC (rung 2/2)#2

Open
salindne wants to merge 2 commits into
daml-poc-registrationfrom
daml-poc-buy-traffic
Open

PoC: buy dedicated-synchronizer traffic in CC (rung 2/2)#2
salindne wants to merge 2 commits into
daml-poc-registrationfrom
daml-poc-buy-traffic

Conversation

@salindne

Copy link
Copy Markdown

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 of AmuletRules_BuyMemberTraffic. It reads a disclosed RegisteredSynchronizer (via the public RegisteredSynchronizer_Fetch from rung 1), enforces the minTopupAmount floor, reuses computeSynchronizerFees + splitAndBurn unchanged 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 + DedicatedSyncTraffic created, operator observes it) and a negative (below-min-topup rejected).

Key differences from AmuletRules_BuyMemberTraffic

BuyMemberTraffic BuyDedicatedSyncTraffic
authorizes the target sync via membership in requiredSynchronizers a disclosed RegisteredSynchronizer
records into MemberTraffic (no observers) DedicatedSyncTraffic (operator = observer)
CC burn / fee calc splitAndBurn / computeSynchronizerFees same, unchanged

Design notes

  • Additive only. No change to MemberTraffic, AmuletRules_BuyMemberTraffic, or the wallet bridge → no Scala/TS codegen churn.
  • Why a separate template, not a MemberTraffic observer field: adding an observer to MemberTraffic is a breaking schema change touching ~41 Scala consumers. A sibling keeps rung 2 isolated and CI green.
  • Reconcile-to-sequencer is out of scope (a later Scala trigger). This rung stops at the on-ledger record; the LocalNet demo will play SetTrafficPurchased manually.

Not yet verified

Written without a local build — no dpm/nix env available. Please let CI (or a local nix build) run sbt 'splice-amulet-test-daml/Test/damlTest'. Draft until green.

Inline comments walk through each hunk.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) $

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@salindne

Copy link
Copy Markdown
Author

Status update — compiles + tested green.

splice-amulet builds clean (splice-amulet-0.1.22.dar); all buy-path Daml Script tests pass: test_BuyDedicatedSyncTraffic (happy), test_BuyDedicatedSyncTraffic_belowMinTopup, test_BuyDedicatedSyncTraffic_wrongExpectedDso, and test_RegisteredSynchronizer_ensureNonEmpty (negatives).

Issues addressed: #21 (E1-3, AmuletRules_BuyDedicatedSyncTraffic + DedicatedSyncTraffic). Contributes to #15 (compile) and #16 (Daml Script tests) — both green. Stacked on #1 (registration).

Onboarding / full status: ChainSafe/canton-extending-mainnet#9.

@salindne
salindne marked this pull request as ready for review July 17, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant