From 40e099608f45083634db75ccb665fa213afc76b8 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:16:43 -0600 Subject: [PATCH 1/2] feat(daml-poc): register dedicated synchronizers via governance PoC rung 1 of dedicated-synchronizer traffic funding. - splice-amulet: RegisteredSynchronizer template (dso-signed, operator observer) + public RegisteredSynchronizer_Fetch choice, binding a dedicated synchronizer id to its operator party. - splice-dso-governance: SRARC_RegisterSynchronizer / DsoRules_RegisterSynchronizer governed create action + dispatch, so a DSO vote populates the registry. - test: TestRegisterSynchronizer registers a synchronizer via a supermajority vote and asserts the RegisteredSynchronizer contract. Not built locally (no dpm/nix env available); needs sbt damlTest / CI to verify. --- .../Splice/DecentralizedSynchronizer.daml | 45 +++++++++++++++ .../Scripts/TestRegisterSynchronizer.daml | 56 +++++++++++++++++++ .../daml/Splice/DsoRules.daml | 30 +++++++++- 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml diff --git a/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml b/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml index a26bbf5686..0240faef17 100644 --- a/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml +++ b/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml @@ -100,6 +100,51 @@ instance HasCheckedFetch MemberTraffic ForDso where instance HasCheckedFetch MemberTraffic ForMemberTraffic where contractGroupId MemberTraffic{..} = ForMemberTraffic with .. +-- Dedicated-synchronizer registration (PoC) +------------------------------------------------ +-- +-- A "dedicated synchronizer" is a non-global synchronizer (stood up with the Splice software) +-- whose traffic we want to fund by burning CC on the global synchronizer. Before any CC can be +-- burned for such a synchronizer it is registered once, by DSO governance, via +-- `DsoRules_RegisterSynchronizer` (splice-dso-governance), which creates this contract. +-- +-- The registration binds a stable synchronizer id to the `operator` party that runs it. Two +-- consumers read it: +-- * `AmuletRules_BuyDedicatedSyncTraffic` (splice-amulet) authorizes a CC burn for a +-- synchronizer id only if a matching `RegisteredSynchronizer` is supplied (via explicit +-- disclosure), and uses `operator` as the observer of the resulting traffic record. +-- * the `operator` itself, which observes this contract and thus learns it is registered. +-- +-- This template lives in splice-amulet (not splice-dso-governance) so that `AmuletRules` can +-- `fetch` it: splice-dso-governance depends on splice-amulet, not the reverse. +template RegisteredSynchronizer with + dso : Party + -- ^ The DSO of the global synchronizer; sole signatory. A DSO vote creates this contract. + synchronizerId : Text + -- ^ Id of the dedicated synchronizer this registration authorizes CC-funded traffic for. + operator : Party + -- ^ The party operating the dedicated synchronizer. Observes this registration and every + -- traffic purchase for its synchronizer (see `DedicatedSyncTraffic`). + where + signatory dso + observer operator + + ensure not (T.isEmpty synchronizerId) + + -- | Public fetch choice, mirroring `OpenMiningRound_Fetch`: lets a buyer who is neither + -- signatory nor observer read this contract inside `AmuletRules_BuyDedicatedSyncTraffic` + -- when it is passed via explicit disclosure. Returns the contract unchanged. + nonconsuming choice RegisteredSynchronizer_Fetch : RegisteredSynchronizer + with + p : Party -- ^ the reader (the buyer/provider); authorizes the fetch + controller p + do pure this + +-- Group-id check for `fetchPublicReferenceData`: a `RegisteredSynchronizer` belongs to its DSO, +-- so reads are validated against the expected DSO party (mirrors the `MemberTraffic`/`ForDso` instance). +instance HasCheckedFetch RegisteredSynchronizer ForDso where + contractGroupId RegisteredSynchronizer{..} = ForDso with .. + instance Patchable BaseRateTrafficLimits where patch new base current = BaseRateTrafficLimits with burstAmount = patch new.burstAmount base.burstAmount current.burstAmount diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml new file mode 100644 index 0000000000..46e727b582 --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml @@ -0,0 +1,56 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | PoC (dedicated-synchronizer traffic): register a dedicated synchronizer through a DSO +-- governance vote and assert the resulting `RegisteredSynchronizer` contract. +-- +-- This is the first rung of the ladder: it only proves that governance can create the +-- registry entry that a later CC-funded traffic purchase will read. Buying traffic against a +-- registered synchronizer is exercised in the splice-amulet test (next PR). +module Splice.Scripts.TestRegisterSynchronizer where + +import DA.Assert + +import Daml.Script + +-- The governance action + its argument record. +import Splice.DsoRules +-- The registry contract created by the vote. It lives in splice-amulet (AmuletRules must be +-- able to fetch it), and splice-dso-governance-test data-depends on splice-amulet. +import Splice.DecentralizedSynchronizer (RegisteredSynchronizer) + +-- Shared helpers that spin up a 4-SV network and drive a vote end-to-end. +import Splice.Scripts.DsoTestUtils + +-- | Register a dedicated synchronizer via a supermajority SV vote, then verify the registration: +-- signed by the DSO, naming the operator, and observed by that operator. +test_RegisterSynchronizer_viaVote : Script () +test_RegisterSynchronizer_viaVote = do + -- 4-SV network; `dso` is the DSO party, `sv1..sv4` are the Super Validators. + (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet + + -- The party that will operate the dedicated synchronizer. + operator <- allocateParty "dedicatedSyncOperator" + let dedicatedSyncId = "dedicated-sync::1220dededededededededededededededededededededededededededededede" + + -- Supermajority SV vote to register the dedicated synchronizer. `initiateAndAcceptVote` routes + -- through DsoRules_RequestVote -> DsoRules_CastVote (x3) -> DsoRules_CloseVoteRequest, which on + -- acceptance calls executeActionRequiringConfirmation, dispatching to DsoRules_RegisterSynchronizer. + initiateAndAcceptVote app [sv1, sv2, sv3, sv4] $ + ARC_DsoRules with + dsoAction = SRARC_RegisterSynchronizer DsoRules_RegisterSynchronizer with + synchronizerId = dedicatedSyncId + operator + + -- The registration now exists on-ledger with exactly the fields we voted for. + registrations <- query @RegisteredSynchronizer dso + case registrations of + [(_cid, reg)] -> do + reg.dso === dso + reg.synchronizerId === dedicatedSyncId + reg.operator === operator + _ -> abort $ "expected exactly one RegisteredSynchronizer, got " <> show (length registrations) + + -- The operator is an observer, so it can see its own registration. + operatorView <- query @RegisteredSynchronizer operator + length operatorView === 1 diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index ff4829a8cd..7d939927e3 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -27,7 +27,9 @@ import Splice.Round import Splice.Types import Splice.ValidatorLicense import Splice.Wallet.Subscriptions -import Splice.DecentralizedSynchronizer (MemberTraffic) +-- `RegisteredSynchronizer` (PoC): created by `DsoRules_RegisterSynchronizer` below; the template +-- itself lives in splice-amulet because AmuletRules must be able to fetch it. +import Splice.DecentralizedSynchronizer (MemberTraffic, RegisteredSynchronizer) import qualified Splice.CometBft as CometBft import Splice.Ans @@ -117,6 +119,10 @@ data DsoRules_ActionRequiringConfirmation -- ^ Create TransferCommandCounter contract for the given sender if it does not already exist | SRARC_CreateUnallocatedUnclaimedActivityRecord DsoRules_CreateUnallocatedUnclaimedActivityRecord -- ^ Voted action to create an UnallocatedUnclaimedActivityRecord contract. + | SRARC_RegisterSynchronizer DsoRules_RegisterSynchronizer + -- ^ PoC (dedicated-synchronizer traffic): voted action to register a dedicated (non-global) + -- synchronizer by binding its id to an operator party, so its traffic can be funded in CC. + -- Executed via a DSO vote; see the `DsoRules_RegisterSynchronizer` choice. | SRARC_CreateBootstrapExternalPartyConfigStateInstruction DsoRules_CreateBootstrapExternalPartyConfigStateInstruction -- ^ Create BootstrapExternalPartyConfigStateInstruction | SRARC_UpdateFeaturedAppRight DsoRules_UpdateFeaturedAppRight @@ -332,6 +338,10 @@ data DsoRules_AmuletRules_ConvertFeaturedAppActivityMarkersResult = DsoRules_Amu data DsoRules_CreateUnallocatedUnclaimedActivityRecordResult = DsoRules_CreateUnallocatedUnclaimedActivityRecordResult with unallocatedUnclaimedActivityRecordCid : ContractId UnallocatedUnclaimedActivityRecord +-- | Result of `DsoRules_RegisterSynchronizer`: the cid of the created registration. +data DsoRules_RegisterSynchronizerResult = DsoRules_RegisterSynchronizerResult with + registeredSynchronizerCid : ContractId RegisteredSynchronizer + data DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult = DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult with unclaimedActivityRecordCid : ContractId UnclaimedActivityRecord optUnclaimedRewardCid : Optional (ContractId UnclaimedReward) @@ -1680,6 +1690,22 @@ template DsoRules with expiresAt pure $ DsoRules_CreateUnallocatedUnclaimedActivityRecordResult with .. + -- | PoC (dedicated-synchronizer traffic): governed registration of a dedicated synchronizer. + -- Creates a `RegisteredSynchronizer` (in splice-amulet) binding `synchronizerId` to its + -- `operator`. This is the only way the registry is populated; `AmuletRules_BuyDedicatedSyncTraffic` + -- then reads that contract (via explicit disclosure) to authorize CC burns for the synchronizer. + -- `controller dso` + `nonconsuming`: a vote reaches this via `executeActionRequiringConfirmation`, + -- and the `DsoRules` contract is not archived by exercising it. + nonconsuming choice DsoRules_RegisterSynchronizer : DsoRules_RegisterSynchronizerResult + with + synchronizerId : Text -- ^ stable id of the dedicated synchronizer to register + operator : Party -- ^ the party operating that synchronizer + controller dso + do + require "synchronizerId is non-empty" (not (T.isEmpty synchronizerId)) + registeredSynchronizerCid <- create RegisteredSynchronizer with dso; synchronizerId; operator + pure $ DsoRules_RegisterSynchronizerResult with .. + nonconsuming choice DsoRules_AllocateUnallocatedUnclaimedActivityRecord : DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult with unallocatedUnclaimedActivityRecordCid : ContractId UnallocatedUnclaimedActivityRecord @@ -1816,6 +1842,8 @@ executeActionRequiringConfirmation dso dsoRulesCid amuletRulesCid act = case act SRARC_CreateExternalPartyAmuletRules choiceArg -> void $ exercise dsoRulesCid choiceArg SRARC_CreateTransferCommandCounter choiceArg -> void $ exercise dsoRulesCid choiceArg SRARC_CreateUnallocatedUnclaimedActivityRecord choiceArg -> void $ exercise dsoRulesCid choiceArg + -- PoC: an accepted vote to register a dedicated synchronizer runs the choice on DsoRules. + SRARC_RegisterSynchronizer choiceArg -> void $ exercise dsoRulesCid choiceArg SRARC_CreateBootstrapExternalPartyConfigStateInstruction choiceArg -> void $ exercise dsoRulesCid choiceArg ARC_AnsEntryContext with .. -> do void $ fetchChecked (ForDso with dso) ansEntryContextCid From 09c054f7923c373ecfac86c3d1d5c0c6529649fc Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:20:31 -0600 Subject: [PATCH 2/2] fix(daml-poc): import RegisteredSynchronizer(..) so DsoRules_RegisterSynchronizer can create it --- daml/splice-dso-governance/daml/Splice/DsoRules.daml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 7d939927e3..251ce9808b 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -29,7 +29,7 @@ import Splice.ValidatorLicense import Splice.Wallet.Subscriptions -- `RegisteredSynchronizer` (PoC): created by `DsoRules_RegisterSynchronizer` below; the template -- itself lives in splice-amulet because AmuletRules must be able to fetch it. -import Splice.DecentralizedSynchronizer (MemberTraffic, RegisteredSynchronizer) +import Splice.DecentralizedSynchronizer (MemberTraffic, RegisteredSynchronizer(..)) import qualified Splice.CometBft as CometBft import Splice.Ans