diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala index 02f35b1048..5bda861b44 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala @@ -4241,7 +4241,7 @@ object DarResources { ) val dsoGovernance_0_1_28 = DarResource( "splice-dso-governance-0.1.28.dar", - "d1ceb0493b95818b6edb8018370b0231ef8a6237bd03e896803130e16dfd3b7f", + "fbe0681d6c1f55e269be120f11f416c975707dbe704168c6929d087cbf9655d0", PackageMetadata( PackageName.assertFromString("splice-dso-governance"), PackageVersion.assertFromString("0.1.28"), @@ -4305,7 +4305,7 @@ object DarResources { ) val dsoGovernance_current = DarResource( "splice-dso-governance-current.dar", - "d1ceb0493b95818b6edb8018370b0231ef8a6237bd03e896803130e16dfd3b7f", + "fbe0681d6c1f55e269be120f11f416c975707dbe704168c6929d087cbf9655d0", PackageMetadata( PackageName.assertFromString("splice-dso-governance"), PackageVersion.assertFromString("0.1.28"), diff --git a/daml/dars.lock b/daml/dars.lock index 1c885c649d..2afeb267e9 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -86,7 +86,7 @@ splice-dso-governance 0.1.24 4974c654485d4ecaa6b5caf8ef3c2679efa8195c4b50d4965a8 splice-dso-governance 0.1.25 dfe102514acd41f79945b8363ca84d8243be97b2b235522ce77c754a36bb8479 splice-dso-governance 0.1.26 45099e955ce443f7895125097a62c509b9ad297091c91d6377ffe94f183c0e3e splice-dso-governance 0.1.27 014473d35514cb36583bdeca269386e46ae4e2283fe9b04beb2f53cdb408e1a2 -splice-dso-governance 0.1.28 d1ceb0493b95818b6edb8018370b0231ef8a6237bd03e896803130e16dfd3b7f +splice-dso-governance 0.1.28 fbe0681d6c1f55e269be120f11f416c975707dbe704168c6929d087cbf9655d0 splice-dso-governance 0.1.3 b0ae3cc03e418790305a3c15f761fe495572de5827f8d322fb8b96996b783c13 splice-dso-governance 0.1.4 dc24fd18b4d151cd1e0ff6bfb7438bafb2f50fe076d0f16f50565e60b153a0be splice-dso-governance 0.1.5 9e3ca1d22ad495dfabf3d61acae3dc1a7718f527f02092280b58cf69edfdc84c @@ -94,7 +94,7 @@ splice-dso-governance 0.1.6 4e7653cfbf7ca249de4507aca9cd3b91060e5489042a522c589d splice-dso-governance 0.1.7 d406eba1132d464605f4dae3edf8cf5ecbbb34bd8edef0e047e7e526d328718c splice-dso-governance 0.1.8 1790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948 splice-dso-governance 0.1.9 9ee83bfd872f91e659b8a8439c5b4eaf240bcf6f19698f884d7d7993ab48c401 -splice-dso-governance-test 0.1.34 c1f017c15bb39f1f43128c03767579907e74f43b7950a9f6877b8333362c7dcd +splice-dso-governance-test 0.1.34 5c792944831e9257a35d9f55c12d4a76f57e948b872163f06398ee72e2104c2d splice-test-token-v1 1.0.0 aaa0b576b5a3db49b3a4f7a4710fe6f8ae462aabbde4da89f639eb87fd62e90c splice-test-token-v1-test 1.0.1 397e9016f18a08c7b28abc5f26d10e6ed4bfb4593321e21c396992642061e84f splice-test-token-v2 1.0.0 a38a96b6f46c14c599b2763bc4fc68911a9cada90f89c599a1401e8e3df685e1 diff --git a/daml/dars/splice-dso-governance-0.1.28.dar b/daml/dars/splice-dso-governance-0.1.28.dar index dce9492e07..9ccc4b43be 100644 Binary files a/daml/dars/splice-dso-governance-0.1.28.dar and b/daml/dars/splice-dso-governance-0.1.28.dar differ diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestExtensionRewardVote.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestExtensionRewardVote.daml index e941b20ca4..9611d1b876 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestExtensionRewardVote.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestExtensionRewardVote.daml @@ -27,7 +27,7 @@ import Splice.Scripts.DsoTestUtils test_ExtensionRewards_viaVote : Script () test_ExtensionRewards_viaVote = do - (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet + (app, _dso, (sv1, sv2, sv3, sv4)) <- initMainNet operator <- allocateParty "dedicatedSyncOperator" featured <- allocateParty "featuredAppOnExtension" @@ -43,14 +43,10 @@ test_ExtensionRewards_viaVote = do -- The vote-created registration authorizes reward processing below. [(regCid, _)] <- query @RegisteredSynchronizer operator - -- Reporting state (created directly as the DSO party in this PoC; production wiring of the - -- state creation is part of the registration follow-ups). - stateCid <- submit dso $ createCmd DedicatedSynchronizerState with - dso - synchronizerId = dedicatedSyncId - operator - lastReportedRound = None - totalActivityReported = 0.0 + -- Registration created the reporting state; the operator reports against it. + [(stateCid, st)] <- query @DedicatedSynchronizerState operator + st.synchronizerId === dedicatedSyncId + st.totalActivityReported === 0.0 -- The operator reports a single-leaf tree of activity weights for round 1. let leaf = BatchOfMintingAllowances [MintingAllowance with provider = featured; amount = 25.0] diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml index 0f1bfcf91b..1ba2d8cc87 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestRegisterSynchronizer.daml @@ -17,7 +17,10 @@ import Daml.Script import Splice.DsoRules -- The registry contract created by the vote. It lives in splice-amulet (AmuletRules must be -- able to fetch it), and this test package data-depends on splice-amulet. -import Splice.DecentralizedSynchronizer (RegisteredSynchronizer) +import Splice.Amulet.CryptoHash qualified as CryptoHash +import Splice.Amulet.RewardAccountingV2 (Batch(..)) +import Splice.DecentralizedSynchronizer +import Splice.Types (Round(..)) -- Shared helpers that spin up a 4-SV network and drive a vote end-to-end. import Splice.Scripts.DsoTestUtils @@ -54,3 +57,52 @@ test_RegisterSynchronizer_viaVote = do -- The operator is an observer, so it can see its own registration. operatorView <- query @RegisteredSynchronizer operator length operatorView === 1 + + -- Registration also created the synchronizer's reporting state (extension reward flow). + states <- query @DedicatedSynchronizerState operator + case states of + [(_cid, st)] -> do + st.synchronizerId === dedicatedSyncId + st.operator === operator + st.totalActivityReported === 0.0 + _ -> abort $ "expected exactly one DedicatedSynchronizerState, got " <> show (length states) + +-- | Offboard a registered synchronizer via a supermajority vote: the registration is archived, +-- which closes both economic paths (the buy needs the disclosed registration; reward +-- processing requires the live registration - see test_StartProcessing_registrationGate in +-- splice-amulet-test). The reporting state deliberately survives the vote (its contract id +-- changes with every report, so a vote must not pin it); it is inert residue, cleaned up by a +-- separate archive by DSO automation, off the vote's critical path. +test_ArchiveSynchronizerRegistration_viaVote : Script () +test_ArchiveSynchronizerRegistration_viaVote = do + (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet + operator <- allocateParty "dedicatedSyncOperator" + let dedicatedSyncId = "dedicated-sync::1220acacacacacacacacacacacacacacacacacacacacacacacacacacacacacac" + + initiateAndAcceptVote app [sv1, sv2, sv3, sv4] $ + ARC_DsoRules with + dsoAction = SRARC_RegisterSynchronizer DsoRules_RegisterSynchronizer with + synchronizerId = dedicatedSyncId + operator + [(regCid, _)] <- query @RegisteredSynchronizer operator + [(stateCid, _)] <- query @DedicatedSynchronizerState operator + + initiateAndAcceptVote app [sv1, sv2, sv3, sv4] $ + ARC_DsoRules with + dsoAction = SRARC_ArchiveSynchronizerRegistration DsoRules_ArchiveSynchronizerRegistration with + registeredSynchronizerCid = regCid + + -- the registration is gone: nothing to disclose to a buy, nothing to authorize processing + [] <- query @RegisteredSynchronizer operator + + -- the state survives and reporting still runs, but the reports are inert: without a live + -- registration they can never be turned into minting (the start-processing gate) + result <- submit operator $ exerciseCmd stateCid DedicatedSynchronizerState_ReportActivity with + round = Round 1 + activityRootHash = CryptoHash.hash (BatchOfMintingAllowances []) + totalActivity = 0.0 + + -- cleanup is a plain archive by DSO automation, retried freely against the current state cid + submit dso $ archiveCmd result.stateCid + [] <- query @DedicatedSynchronizerState operator + pure () diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 7856cfde8c..a688c07969 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(..), DedicatedSynchronizerState(..)) import qualified Splice.CometBft as CometBft import Splice.Ans @@ -132,6 +132,9 @@ data DsoRules_ActionRequiringConfirmation -- (one run by its own operator) by binding its id to an operator party, so its traffic can -- be funded in Amulet. Executed via DSO governance; see the `DsoRules_RegisterSynchronizer` -- choice. + | SRARC_ArchiveSynchronizerRegistration DsoRules_ArchiveSynchronizerRegistration + -- ^ PoC (dedicated-synchronizer traffic): voted action to offboard a registered synchronizer, + -- archiving its registration and reporting state. deriving (Eq, Show) data AnsEntryContext_ActionRequiringConfirmation @@ -346,6 +349,9 @@ data DsoRules_CreateUnallocatedUnclaimedActivityRecordResult = DsoRules_CreateUn -- | Result of `DsoRules_RegisterSynchronizer`: the cid of the created registration. data DsoRules_RegisterSynchronizerResult = DsoRules_RegisterSynchronizerResult with registeredSynchronizerCid : ContractId RegisteredSynchronizer + dedicatedSynchronizerStateCid : ContractId DedicatedSynchronizerState + +data DsoRules_ArchiveSynchronizerRegistrationResult = DsoRules_ArchiveSynchronizerRegistrationResult data DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult = DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult with unclaimedActivityRecordCid : ContractId UnclaimedActivityRecord @@ -1707,8 +1713,35 @@ template DsoRules with do require "synchronizerId is non-empty" (not (T.isEmpty synchronizerId)) registeredSynchronizerCid <- create RegisteredSynchronizer with dso; synchronizerId; operator + -- reporting state for the extension reward flow (see DedicatedSynchronizerState), + -- created at registration so the operator can report from its first round + dedicatedSynchronizerStateCid <- create DedicatedSynchronizerState with + dso + synchronizerId + operator + lastReportedRound = None + totalActivityReported = 0.0 pure $ DsoRules_RegisterSynchronizerResult with .. + nonconsuming choice DsoRules_ArchiveSynchronizerRegistration : DsoRules_ArchiveSynchronizerRegistrationResult + -- ^ PoC (dedicated-synchronizer traffic): governed offboarding of a registered synchronizer + -- by archiving its registration. That closes both economic paths structurally: future + -- purchases are impossible (an archived RegisteredSynchronizer cannot be disclosed to the + -- buy choice) and reward processing is impossible (start-processing requires the live + -- registration). The reporting state is deliberately not archived here: it is inert + -- without the registration, and its contract id changes with every report, so pinning it + -- in a vote's arguments would let the operator invalidate the vote by reporting while the + -- vote is open. Cleaning up the leftover state is a separate archive by DSO automation, safe to + -- retry off the vote's critical path. Records of past purchases (MemberTraffic) and reward + -- processing already authorized for past rounds are untouched: authorized work completes, + -- new work stops. + with + registeredSynchronizerCid : ContractId RegisteredSynchronizer + controller dso + do + void $ fetchAndArchive (ForDso with dso) registeredSynchronizerCid + pure DsoRules_ArchiveSynchronizerRegistrationResult + nonconsuming choice DsoRules_AllocateUnallocatedUnclaimedActivityRecord : DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult with unallocatedUnclaimedActivityRecordCid : ContractId UnallocatedUnclaimedActivityRecord @@ -1848,6 +1881,7 @@ executeActionRequiringConfirmation dso dsoRulesCid amuletRulesCid act = case act 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_ArchiveSynchronizerRegistration choiceArg -> void $ exercise dsoRulesCid choiceArg SRARC_CreateBootstrapExternalPartyConfigStateInstruction choiceArg -> void $ exercise dsoRulesCid choiceArg ARC_AnsEntryContext with .. -> do void $ fetchChecked (ForDso with dso) ansEntryContextCid