From 30a3c748b793706a4f489f596553109f88f6a0d9 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 09:33:03 +0000 Subject: [PATCH 01/10] write core of Daml --- .../daml/Splice/AmuletRules.daml | 6 + .../Splice/DecentralizedSynchronizer.daml | 2 + .../Splice/DSO/ResponseReimbursement.daml | 148 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 392e25fa9f..285f32cb60 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -233,6 +233,7 @@ template AmuletRules numPurchases = 1 amuletSpent = trafficCostAmulet usdSpent = trafficCostUsd + totalReimbursed = None -- return result return AmuletRules_BuyMemberTrafficResult with round = transferResult.round @@ -270,6 +271,11 @@ template AmuletRules numPurchases = acc.numPurchases + traffic.numPurchases usdSpent = acc.usdSpent + traffic.usdSpent amuletSpent = acc.amuletSpent + traffic.amuletSpent + totalReimbursed = case (acc.totalReimbursed, traffic.totalReimbursed) of + (None, None) -> None + (Some a, None) -> Some a + (None, Some b) -> Some b + (Some a, Some b) -> Some (a + b) ) (initialMemberTraffic dso first.memberId first.synchronizerId first.migrationId) traffics mergedTrafficCid <- create mergedTraffic return AmuletRules_MergeMemberTrafficContractsResult with .. diff --git a/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml b/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml index a26bbf5686..57cd63b62f 100644 --- a/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml +++ b/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml @@ -64,6 +64,7 @@ template MemberTraffic with numPurchases : Int -- ^ Number of times extra traffic has been purchased amuletSpent : Decimal -- ^ Total Amulet spent on extra traffic usdSpent : Decimal -- ^ Total USD spent on extra traffic + totalReimbursed : Optional Int -- ^ The number of bytes of response traffic reimbursed to this member. where signatory dso @@ -86,6 +87,7 @@ initialMemberTraffic dso memberId synchronizerId migrationId = MemberTraffic wit numPurchases = 0 amuletSpent = 0.0 usdSpent = 0.0 + totalReimbursed = None data ForMemberTraffic = ForMemberTraffic with dso : Party diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml new file mode 100644 index 0000000000..ad8361a0cd --- /dev/null +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -0,0 +1,148 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | All the response reimbursement workflows code that does not depend on `DsoRules`. +module Splice.DSO.ResponseReimbursement where + +import DA.Action +import DA.Foldable (forA_) +import DA.Optional +import DA.Text qualified as T + +import Splice.DecentralizedSynchronizer +import Splice.Util + +data ReimbursementInterval = ReimbursementInterval with + startExclusive : Time + endInclusive : Time + deriving (Eq, Show) + +-- TODO: consider using a fixed one nibble sharding prefix as that gives a 16x tx size reduction while being cheap to implement due to not being configurable. + +template ReimbursementWorkflowState + with + dso: Party + nextIntervalStart: Time + where + signatory dso + + choice ReimbursementWorkflowState_AddReimbursementInterval + : ReimbursementWorkflowState_AddReimbursementIntervalResult + with + nextIntervalEnd : Time + controller dso + do + intervalStateCid <- create ReimbursementIntervalState with + dso = dso + interval = ReimbursementInterval with + startExclusive = this.nextIntervalStart + endInclusive = nextIntervalEnd + reimbursementConfirmation = None + workflowStateCid <- create this with + nextIntervalStart = nextIntervalEnd + pure ReimbursementWorkflowState_AddReimbursementIntervalResult with + intervalStateCid + workflowStateCid + + +data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWorkflowState_AddReimbursementIntervalResult with + intervalStateCid : ContractId ReimbursementIntervalState + workflowStateCid : ContractId ReimbursementWorkflowState + deriving (Eq, Show) + + +template ReimbursementIntervalState + with + dso: Party + interval: ReimbursementInterval + reimbursementConfirmation : Optional ReimbursementConfirmation + where + signatory dso + + choice ReimbursementIntervalState_ConfirmReimbursement + : ContractId ReimbursementIntervalState -- FIXME + with + reimbursementConfirmation : ReimbursementConfirmation + controller dso + do + require "Not yet confirmed" (isNone this.responseTrafficDataHash) + create this with responseTrafficDataHash = Some responseTrafficDataHash + + choice ReimbursementIntervalState_ReimburseResponses + : ReimbursementIntervalState_ReimburseResponsesResult + with + responseTrafficData : ResponseTrafficData + controller dso + do + case reimbursementConfirmation of + None -> abort "Response traffic reimbursement hash not yet been confirmed" + Some confirmation -> do + let actualHash = hashResponseTrafficData responseTrafficData + let expectedHash = confirmation.responseTrafficDataHash + unless (actualHash == expectedHash) $ + abort $ T.unwords ["Response traffic data hash", actualHash.value, "does not match expected hash", expectedHash.value] + forA_ responseTrafficData.responseTrafficTotals \(ResponseTrafficTotal with memberId, totalTraffic) -> + create MemberTraffic with + dso = dso + memberId + synchronizerId = confirmation.synchronizerId + migrationId = confirmation.migrationId + totalReimbursed = Some totalTraffic + -- Set these fields to reflect that this reimbursement is not a traffic purchase. + -- Thereby the aggregation in AmuletRules_MergeMemberTrafficContracts works correctly. + totalPurchased = 0 + amuletSpent = 0.0 + usdSpent = 0.0 + numPurchases = 0 + + + +data ReimbursementIntervalState_ReimburseResponsesResult = ReimbursementIntervalState_ReimburseResponsesResult with + intervalStateCid : ContractId ReimbursementIntervalState + workflowStateCid : ContractId ReimbursementWorkflowState + deriving (Eq, Show) + + +data ReimbursementConfirmation = ReimbursementConfirmation with + synchronizerId : Text -- ^ Synchronizer on which the responses should be reimbursed. Always set to the global synchronizer-id. + migrationId : Int -- ^ Migration id on which the responses should be reimbursed. Always set to the latest migration id. + responseTrafficDataHash : Hash + deriving (Eq, Show) + +data ResponseTrafficData = ResponseTrafficData with + responseTrafficTotals : [ResponseTrafficTotal] + deriving (Eq, Show) + +data ResponseTrafficTotal = ResponseTrafficTotal with + memberId : Text + totalTraffic : Int + deriving (Eq, Show) + + +-- TODO: move into Splice.DSO.CryptoHash + +data Hash = Hash with value : Text + deriving (Eq, Show) + +hashInt : Int -> Hash +hashInt n = Hash $ T.sha256 (show n) + +hashText : Text -> Hash +hashText = Hash . T.sha256 + +hashListInternal : [Text] -> Hash +hashListInternal ts = Hash $ T.sha256 $ mconcat (show (length ts) :: "|" :: ts) + +hashRecord : [Hash] -> Hash +hashRecord = hashListInternal . map (.value) + +hashList : (a -> Hash) -> [a] -> Hash +hashList hashElem xs = hashListInternal [ (hashElem x).value | x <- xs ] + +hashResponseTrafficData : ResponseTrafficData -> Hash +hashResponseTrafficData (ResponseTrafficData with responseTrafficTotals) = + hashRecord [hashList hashResponseTrafficTotal responseTrafficTotals] + +hashResponseTrafficTotal : ResponseTrafficTotal -> Hash +hashResponseTrafficTotal (ResponseTrafficTotal with memberId, totalTraffic) = + hashRecord [hashText memberId, hashInt totalTraffic] From 9def18765552398ba32e4ab9f9ed770b2bc3cd17 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 10:16:47 +0000 Subject: [PATCH 02/10] package Daml hashing functions --- .../daml/Splice/DSO/CryptoHash.daml | 104 ++++++++++++++++++ .../Splice/DSO/ResponseReimbursement.daml | 32 ++---- 2 files changed, 111 insertions(+), 25 deletions(-) create mode 100644 daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml diff --git a/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml b/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml new file mode 100644 index 0000000000..f8baeecb19 --- /dev/null +++ b/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml @@ -0,0 +1,104 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | Utilities to compute cryptographic hashes of Daml data structures. +-- We use this for example for computing compact commitments for moving +-- off-ledger data shared by the SV nodes on-ledger. +module Splice.DSO.CryptoHash + ( + Hash(..), + Hashable(..), + hashRecord, + hashUpgradedRecord, + hashVariant, + hashUpgradedVariant, + ) where + +import DA.Optional (isNone) +import DA.Text qualified as T + +data Hash = Hash with value : Text + deriving (Eq, Show) + +-- | Compute the hash of a record. +hashRecord : [Hash] -> Hash +hashRecord = hashListInternal . map (.value) + +-- | Compute the hash of an upgraded record so that it agrees with the old record hash +-- when ignoring trailing None fields. +hashUpgradedRecord : [Hash] -> [Optional Hash] -> Hash +hashUpgradedRecord oldFieldHashes newFieldHashes = + hashListInternal $ + [ h.value | h <- oldFieldHashes ] ++ + [ (hashOptionalInternal optField).value | optField <- dropTrailingNones newFieldHashes ] + +-- | Compute the hash of a variant. +hashVariant : Text -> [Hash] -> Hash +hashVariant tag fieldHashes = hashVariantInternal tag [ h.value | h <- fieldHashes ] + +-- | Compute the hash of an upgraded variant so that it agrees with the old variant hash +-- when ignoring trailing None fields. +hashUpgradedVariant : Text -> [Hash] -> [Optional Hash] -> Hash +hashUpgradedVariant tag oldFieldHashes newFieldHashes = + hashVariantInternal tag $ + [ h.value | h <- oldFieldHashes ] ++ + [ (hashOptionalInternal optField).value | optField <- dropTrailingNones newFieldHashes ] + +class Hashable a where + hash : a -> Hash + +instance Hashable Int where + hash = hashInt + +instance Hashable Text where + hash = hashText + +instance Hashable a => Hashable (Optional a) where + hash = hashOptionalInternal . fmap hash + +instance Hashable a => Hashable [a] where + hash = hashList hash + + +-- internal helper functions +---------------------------- + +-- Design Note: we want these hashes to be easy to compute in many systems. +-- Therefore we essentially encode the data structure as an S-expression and hash that +-- one recursively. Concretely, we use the following rules: +-- +-- - hash scalars by hashing their string rendering +-- - hash lists by hashing the concatenation of the length and the element hashes +-- - hash records by hashing the list of field hashes +-- - hash variants by hashing the list of fields prefixed with tag for the variant constructor +-- +-- The length prefix on lists also serves as a tag to distinguish different tree structures. +-- We include the number of fields in the hash of a record, as the number of fields +-- can change as part of a Smart Contract Upgrades. +-- +-- Tags for variants must be unique within the scope where the hashes are used. + + +hashList : (a -> Hash) -> [a] -> Hash +hashList hashElem xs = hashListInternal [ (hashElem x).value | x <- xs ] + +hashInt : Int -> Hash +hashInt n = Hash $ T.sha256 (show n) + +hashText : Text -> Hash +hashText = Hash . T.sha256 + +hashListInternal : [Text] -> Hash +hashListInternal ts = Hash $ T.sha256 $ mconcat (show (length ts) :: "|" :: ts) + +hashVariantInternal : Text -> [Text] -> Hash +hashVariantInternal tag fieldValues = + Hash $ T.sha256 $ mconcat (tag :: "|" :: show (length fieldValues) :: "|" :: fieldValues) + +-- we view optionals as lists of length 0 or 1 to simplify the encoding in other systems +hashOptionalInternal : Optional Hash -> Hash +hashOptionalInternal None = hashListInternal [] +hashOptionalInternal (Some h) = hashListInternal [h.value] + +dropTrailingNones : [Optional Hash] -> [Optional Hash] +dropTrailingNones = reverse . dropWhile isNone . reverse diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index ad8361a0cd..9e5822da5d 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -11,6 +11,7 @@ import DA.Text qualified as T import Splice.DecentralizedSynchronizer import Splice.Util +import Splice.DSO.CryptoHash data ReimbursementInterval = ReimbursementInterval with startExclusive : Time @@ -119,30 +120,11 @@ data ResponseTrafficTotal = ResponseTrafficTotal with deriving (Eq, Show) --- TODO: move into Splice.DSO.CryptoHash +-- Hashing instances -data Hash = Hash with value : Text - deriving (Eq, Show) - -hashInt : Int -> Hash -hashInt n = Hash $ T.sha256 (show n) - -hashText : Text -> Hash -hashText = Hash . T.sha256 - -hashListInternal : [Text] -> Hash -hashListInternal ts = Hash $ T.sha256 $ mconcat (show (length ts) :: "|" :: ts) - -hashRecord : [Hash] -> Hash -hashRecord = hashListInternal . map (.value) - -hashList : (a -> Hash) -> [a] -> Hash -hashList hashElem xs = hashListInternal [ (hashElem x).value | x <- xs ] - -hashResponseTrafficData : ResponseTrafficData -> Hash -hashResponseTrafficData (ResponseTrafficData with responseTrafficTotals) = - hashRecord [hashList hashResponseTrafficTotal responseTrafficTotals] +instance Hashable ResponseTrafficData where + hash = hashRecord [hash responseTrafficTotals] -hashResponseTrafficTotal : ResponseTrafficTotal -> Hash -hashResponseTrafficTotal (ResponseTrafficTotal with memberId, totalTraffic) = - hashRecord [hashText memberId, hashInt totalTraffic] +instance Hashable ResponseTrafficTotal where + hash (ResponseTrafficTotal with memberId, totalTraffic) = + hashRecord [hashText memberId, hashInt totalTraffic] From 740cb21f73f2d0b83894c9bd94fa0155162b8e6f Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 10:31:01 +0000 Subject: [PATCH 03/10] polish core workflow templates --- .../daml/Splice/DSO/CryptoHash.daml | 4 + .../Splice/DSO/ResponseReimbursement.daml | 76 +++++++++++++------ 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml b/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml index f8baeecb19..9f258cda05 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/CryptoHash.daml @@ -47,6 +47,10 @@ hashUpgradedVariant tag oldFieldHashes newFieldHashes = class Hashable a where hash : a -> Hash +-- | Identity instance for Hash, which is useful for hash types like [Hash]. +instance Hashable Hash where + hash h = h + instance Hashable Int where hash = hashInt diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index 9e5822da5d..b69d92d34e 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -13,12 +13,25 @@ import Splice.DecentralizedSynchronizer import Splice.Util import Splice.DSO.CryptoHash + + + +-- TODO: consider using a fixed one nibble sharding prefix as that gives a 16x +-- tx size reduction while being cheap to implement due to not being +-- configurable. + + + + + +-- State of the overall reimbursement workflow. +----------------------------------------------- + data ReimbursementInterval = ReimbursementInterval with startExclusive : Time endInclusive : Time deriving (Eq, Show) --- TODO: consider using a fixed one nibble sharding prefix as that gives a 16x tx size reduction while being cheap to implement due to not being configurable. template ReimbursementWorkflowState with @@ -33,6 +46,10 @@ template ReimbursementWorkflowState nextIntervalEnd : Time controller dso do + require "Positive interval" (this.nextIntervalStart < nextIntervalEnd) + -- Note: additional constraints to ensure that the intervals are large + -- enough, and do not advance too quickly are enforced as part of the + -- DsoRules. intervalStateCid <- create ReimbursementIntervalState with dso = dso interval = ReimbursementInterval with @@ -52,6 +69,25 @@ data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWo deriving (Eq, Show) +-- State of reimbursing responses for a given interval. +------------------------------------------------------- + +data ReimbursementConfirmation = ReimbursementConfirmation with + synchronizerId : Text -- ^ Synchronizer on which the responses should be reimbursed. Always set to the global synchronizer-id. + migrationId : Int -- ^ Migration id on which the responses should be reimbursed. Always set to the latest migration id. + responseTrafficDataHash : Hash + deriving (Eq, Show) + +data ResponseTrafficData = ResponseTrafficData with + responseTrafficTotals : [ResponseTrafficTotal] + deriving (Eq, Show) + +data ResponseTrafficTotal = ResponseTrafficTotal with + memberId : Text + totalTraffic : Int + deriving (Eq, Show) + + template ReimbursementIntervalState with dso: Party @@ -61,13 +97,15 @@ template ReimbursementIntervalState signatory dso choice ReimbursementIntervalState_ConfirmReimbursement - : ContractId ReimbursementIntervalState -- FIXME + : ReimbursementIntervalState_ConfirmReimbursementResult with reimbursementConfirmation : ReimbursementConfirmation controller dso do - require "Not yet confirmed" (isNone this.responseTrafficDataHash) - create this with responseTrafficDataHash = Some responseTrafficDataHash + require "Not yet confirmed" (isNone this.reimbursementConfirmation) + intervalStateCid <- create this with reimbursementConfirmation = Some reimbursementConfirmation + pure ReimbursementIntervalState_ConfirmReimbursementResult with + intervalStateCid choice ReimbursementIntervalState_ReimburseResponses : ReimbursementIntervalState_ReimburseResponsesResult @@ -78,7 +116,7 @@ template ReimbursementIntervalState case reimbursementConfirmation of None -> abort "Response traffic reimbursement hash not yet been confirmed" Some confirmation -> do - let actualHash = hashResponseTrafficData responseTrafficData + let actualHash = hash responseTrafficData let expectedHash = confirmation.responseTrafficDataHash unless (actualHash == expectedHash) $ abort $ T.unwords ["Response traffic data hash", actualHash.value, "does not match expected hash", expectedHash.value] @@ -96,35 +134,29 @@ template ReimbursementIntervalState usdSpent = 0.0 numPurchases = 0 + -- Note: we intentionally do not return the created MemberTraffic contract IDs here + -- to avoid bloating the transaction size on the Ledger API. + pure ReimbursementIntervalState_ReimburseResponsesResult with + dummy = () - -data ReimbursementIntervalState_ReimburseResponsesResult = ReimbursementIntervalState_ReimburseResponsesResult with +data ReimbursementIntervalState_ConfirmReimbursementResult = ReimbursementIntervalState_ConfirmReimbursementResult with intervalStateCid : ContractId ReimbursementIntervalState - workflowStateCid : ContractId ReimbursementWorkflowState deriving (Eq, Show) - -data ReimbursementConfirmation = ReimbursementConfirmation with - synchronizerId : Text -- ^ Synchronizer on which the responses should be reimbursed. Always set to the global synchronizer-id. - migrationId : Int -- ^ Migration id on which the responses should be reimbursed. Always set to the latest migration id. - responseTrafficDataHash : Hash +data ReimbursementIntervalState_ReimburseResponsesResult = ReimbursementIntervalState_ReimburseResponsesResult with + dummy : () deriving (Eq, Show) -data ResponseTrafficData = ResponseTrafficData with - responseTrafficTotals : [ResponseTrafficTotal] - deriving (Eq, Show) -data ResponseTrafficTotal = ResponseTrafficTotal with - memberId : Text - totalTraffic : Int - deriving (Eq, Show) -- Hashing instances +-------------------- instance Hashable ResponseTrafficData where - hash = hashRecord [hash responseTrafficTotals] + hash (ResponseTrafficData with responseTrafficTotals) = + hashRecord [hash responseTrafficTotals] instance Hashable ResponseTrafficTotal where hash (ResponseTrafficTotal with memberId, totalTraffic) = - hashRecord [hashText memberId, hashInt totalTraffic] + hashRecord [hash memberId, hash totalTraffic] From 5de1a5303be437ec684050bb2d43259100ebfae5 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 10:45:53 +0000 Subject: [PATCH 04/10] start adding the DsoRules choices --- .../Splice/DSO/ResponseReimbursement.daml | 3 ++- .../daml/Splice/DsoRules.daml | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index b69d92d34e..4946f8e688 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -32,7 +32,8 @@ data ReimbursementInterval = ReimbursementInterval with endInclusive : Time deriving (Eq, Show) - +-- | The top-level state of the reimbursement workflow. We expect there to be exactly one +-- such contract per DSO. template ReimbursementWorkflowState with dso: Party diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 71990a32ea..8bac98975d 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -32,6 +32,7 @@ import Splice.Ans import Splice.SvOnboarding import Splice.DSO.AmuletPrice import Splice.DSO.DecentralizedSynchronizer +import Splice.DSO.ResponseReimbursement import Splice.DSO.SvState import Splice.Schedule import Splice.Util @@ -114,6 +115,12 @@ 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_StartResponseTrafficReimbursement DsoRules_StartResponseTrafficReimbursement + -- ^ Automated action to start reimbursing responses for a given interval. + | SRARC_AddResponseTrafficReimbursementInterval DsoRules_AddResponseTrafficReimbursementInterval + -- ^ Automated action to add a new confirmation response traffic reimbursement interval. + | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement + -- ^ Automated action to confirm the response traffic reimbursement data for a reimbursement interval. deriving (Eq, Show) data AnsEntryContext_ActionRequiringConfirmation @@ -294,6 +301,9 @@ data DsoRules_ExpireUnallocatedUnclaimedActivityRecordResult = DsoRules_ExpireUn data DsoRules_ExpireUnclaimedActivityRecordResult = DsoRules_ExpireUnclaimedActivityRecordResult with unclaimedRewardCid : ContractId UnclaimedReward +data DsoRules_ReimburseResponsesResult = DsoRules_ReimburseResponsesResult with + result : ReimbursementIntervalState_ReimburseResponsesResult + -- Workflow templates --------------------- @@ -1504,6 +1514,21 @@ template DsoRules with pure DsoRules_ExpireUnclaimedActivityRecordResult with unclaimedRewardCid + -- confirmation response reimbursement + nonconsuming choice DsoRules_ReimburseResponses : DsoRules_ReimburseResponsesResult + with + intervalStateCid : ContractId ReimbursementIntervalState + responseTrafficData : ResponseTrafficData + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise intervalStateCid ReimbursementIntervalState_ReimburseResponses with + responseTrafficData + pure DsoRules_ReimburseResponsesResult with result + + + pruneAtLeastOne : Ord t => t -> Schedule t a -> Optional (Schedule t a) pruneAtLeastOne now schedule = case reverse past of From ccd5d7edc15fb0097be77c3e3235eeec67fe7c93 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 14:37:50 +0000 Subject: [PATCH 05/10] partially through writing the whole flow --- .../Scripts/TestResponseReimbursement.daml | 79 +++++++++++++++++++ .../Splice/DSO/ResponseReimbursement.daml | 54 +++++++------ .../daml/Splice/DsoRules.daml | 50 +++++++++++- 3 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml new file mode 100644 index 0000000000..d38325759c --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml @@ -0,0 +1,79 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.TestResponseReimbursement where + +import DA.Assert +import DA.Foldable (forA_) +import DA.List +import qualified DA.Map as Map +import qualified DA.Set as Set +import Daml.Script +import DA.Time + +import Splice.Amulet +import Splice.AmuletRules +import Splice.Round +import Splice.Schedule +import Splice.DSO.DecentralizedSynchronizer +import Splice.Fees +import Splice.Issuance() + + +import Splice.DsoRules +import Splice.DSO.AmuletPrice +import Splice.AmuletConfig +import Splice.CometBft +import Splice.DecentralizedSynchronizer + +import Splice.Scripts.DsoTestUtils +import Splice.Testing.Registries.AmuletRegistry.Parameters + + +-- | Tests that reimbursement works. +test_confirmation_response_traffic_reimbursement : Script () +test_confirmation_response_traffic_reimbursement = do + (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet + + [(dsoRulesCid, dsoRules)] <- query @DsoRules dso + + -- setup demo data + let responseTrafficDataRaw = sortOn fst + [ ("PAR::1", 1000) + , ("PAR::2", 2000) + , ("PAR::3", 3000) + , ("PAR::4", 4000) + ] + let responseTrafficData = ResponseTrafficData with + responseTrafficTotals = map (uncurry ResponseTrafficTotal) responseTrafficDataRaw + let responseTrafficDataHash = hash responseTrafficData + let reimbursementConfirmation = ReimbursementConfirmation with + synchronizerId = dsoRules.decentralizedSynchronizer.activeSynchronizerId + migrationId = 0 + responseTrafficDataHash + + -- setup reimbursement workflow state + -- add a new interval + -- confirm + + -- check that there are no member traffic contracts yet + [] <- query @MemberTraffic dso + + -- reimburse responses + + -- check that the expected member traffic contracts were created + actualMemberTrafficContracts <- query @MemberTraffic dso + let expectedMemberTrafficContracts = do + (memberId, totalTraffic) <- responseTrafficDataRaw + pure MemberTraffic with + dso = dso + memberId + synchronizerId = reimbursementConfirmation.synchronizerId + migrationId = reimbursementConfirmation.migrationId + totalReimbursed = Some totalTraffic + totalPurchased = 0 + amuletSpent = 0.0 + usdSpent = 0.0 + numPurchases = 0 + sortOn (.memberId) (map snd actualMemberTrafficContracts) === + expectedMemberTrafficContracts diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index 4946f8e688..bc72392474 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -21,7 +21,7 @@ import Splice.DSO.CryptoHash -- configurable. - +-- TODO: add contract that makes it easy to test the hashing of the responseTrafficDataHash -- State of the overall reimbursement workflow. @@ -37,32 +37,12 @@ data ReimbursementInterval = ReimbursementInterval with template ReimbursementWorkflowState with dso: Party + earliestIntervalStart: Time + -- ^ The start time of the earliest interval that is still being processed. nextIntervalStart: Time where signatory dso - choice ReimbursementWorkflowState_AddReimbursementInterval - : ReimbursementWorkflowState_AddReimbursementIntervalResult - with - nextIntervalEnd : Time - controller dso - do - require "Positive interval" (this.nextIntervalStart < nextIntervalEnd) - -- Note: additional constraints to ensure that the intervals are large - -- enough, and do not advance too quickly are enforced as part of the - -- DsoRules. - intervalStateCid <- create ReimbursementIntervalState with - dso = dso - interval = ReimbursementInterval with - startExclusive = this.nextIntervalStart - endInclusive = nextIntervalEnd - reimbursementConfirmation = None - workflowStateCid <- create this with - nextIntervalStart = nextIntervalEnd - pure ReimbursementWorkflowState_AddReimbursementIntervalResult with - intervalStateCid - workflowStateCid - data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWorkflowState_AddReimbursementIntervalResult with intervalStateCid : ContractId ReimbursementIntervalState @@ -70,6 +50,28 @@ data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWo deriving (Eq, Show) +addReimbursementInterval : Party -> RelTime -> ContractId ReimbursementWorkflowState -> Time -> Update (ContractId ReimbursementIntervalState, ContractId ReimbursementWorkflowState) +addReimbursementInterval dso minIntervalDuration workflowStateCid nextIntervalEnd = do + workflowState <- fetchAndArchive (ForDso dso) workflowStateCid + -- ensure minimal duration + let intervalDuration = nextIntervalEnd `subTime` workflowState.nextIntervalStart + require "Minimal interval duration" (intervalDuration >= minIntervalDuration) + -- ensure that intervals are added at most `minIntervalDuration` ahead of time + let backdatedStart = workflowState.nextIntervalStart `addRelTime` (negate minIntervalDuration) + assertDeadlineExceeded "nextIntervalStart - minIntervalDuration" backdatedStart + + intervalStateCid <- create ReimbursementIntervalState with + dso = dso + interval = ReimbursementInterval with + startExclusive = workflowState.nextIntervalStart + endInclusive = nextIntervalEnd + reimbursementConfirmation = None + workflowStateCid <- create workflowState with + nextIntervalStart = nextIntervalEnd + pure (intervalStateCid, workflowStateCid) + + + -- State of reimbursing responses for a given interval. ------------------------------------------------------- @@ -149,6 +151,12 @@ data ReimbursementIntervalState_ReimburseResponsesResult = ReimbursementInterval deriving (Eq, Show) +confirmResponseTrafficReimbursement : Party -> ReimbursementConfirmation -> ContractId ReimbursementIntervalState -> Update (ContractId ReimbursementIntervalState) +confirmResponseTrafficReimbursement dso reimbursementConfirmation intervalStateCid = do + require "Not yet confirmed" (isNone this.reimbursementConfirmation) + intervalStateCid <- create this with reimbursementConfirmation = Some reimbursementConfirmation + pure ReimbursementIntervalState_ConfirmReimbursementResult with + intervalStateCid -- Hashing instances diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 8bac98975d..490e32f086 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -1514,8 +1514,46 @@ template DsoRules with pure DsoRules_ExpireUnclaimedActivityRecordResult with unclaimedRewardCid - -- confirmation response reimbursement - nonconsuming choice DsoRules_ReimburseResponses : DsoRules_ReimburseResponsesResult + -- confirmation response traffic reimbursement + ---------------------------------------------- + + nonconsuming choice DsoRules_StartResponseTrafficReimbursement : DsoRules_StartResponseTrafficReimbursementResult + with + sv : Party + controller dso + do + now <- getTime + workflowStateCid <- create ReimbursementWorkflowState with + dso + earliestIntervalStart = now + nextIntervalStart = now + pure DsoRules_StartResponseTrafficReimbursementResult with + workflowStateCid = workflowStateCid + + + nonconsuming choice DsoRules_AddResponseTrafficReimbursementInterval + : DsoRules_AddResponseTrafficReimbursementIntervalResult + with + workflowStateCid : ContractId ReimbursementWorkflowState + nextIntervalEnd : Time + controller dso + do + let minIntervalDuration = config.responseReimbursementMinIntervalDuration + (intervalStateCid, workflowStateCid) <- + ResponseReimbursement.addReimbursementInterval dso minIntervalDuration workflowStateCid nextIntervalEnd + + nonconsuming choice DsoRules_ConfirmResponseTrafficReimbursement + : DsoRules_ConfirmResponseTrafficReimbursementResult + with + intervalStateCid : ContractId ReimbursementIntervalState + confirmedResponseTrafficData : ReimbursementConfirmation + controller dso + do + result <- exercise intervalStateCid ReimbursementIntervalState_ConfirmResponses with + confirmedResponseTrafficData + pure DsoRules_ConfirmResponseTrafficReimbursementResult with result + + nonconsuming choice DsoRules_ReimburseResponseTraffic : DsoRules_ReimburseResponsesResult with intervalStateCid : ContractId ReimbursementIntervalState responseTrafficData : ResponseTrafficData @@ -1529,6 +1567,14 @@ template DsoRules with + | SRARC_StartResponseTrafficReimbursement DsoRules_StartResponseTrafficReimbursement + -- ^ Automated action to start reimbursing responses for a given interval. + | SRARC_AddResponseTrafficReimbursementInterval DsoRules_AddResponseTrafficReimbursementInterval + -- ^ Automated action to add a new confirmation response traffic reimbursement interval. + | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement + -- ^ Automated action to confirm the response traffic reimbursement data for a reimbursement interval. + + pruneAtLeastOne : Ord t => t -> Schedule t a -> Optional (Schedule t a) pruneAtLeastOne now schedule = case reverse past of From 85a537b1b78235dd2167bf59b5fcf7141b1e9b5a Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 14:51:38 +0000 Subject: [PATCH 06/10] get test to compile --- .../Scripts/TestResponseReimbursement.daml | 6 +- .../Splice/DSO/ResponseReimbursement.daml | 62 ++++++++++++------- .../daml/Splice/DsoRules.daml | 29 +++++---- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml index d38325759c..924aab7182 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml @@ -22,6 +22,8 @@ import Splice.Issuance() import Splice.DsoRules import Splice.DSO.AmuletPrice +import Splice.DSO.ResponseReimbursement -- FIXME: add Traffic to name +import Splice.DSO.CryptoHash import Splice.AmuletConfig import Splice.CometBft import Splice.DecentralizedSynchronizer @@ -48,11 +50,13 @@ test_confirmation_response_traffic_reimbursement = do responseTrafficTotals = map (uncurry ResponseTrafficTotal) responseTrafficDataRaw let responseTrafficDataHash = hash responseTrafficData let reimbursementConfirmation = ReimbursementConfirmation with - synchronizerId = dsoRules.decentralizedSynchronizer.activeSynchronizerId + synchronizerId = dsoRules.config.decentralizedSynchronizer.activeSynchronizerId migrationId = 0 responseTrafficDataHash -- setup reimbursement workflow state + + -- add a new interval -- confirm diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index bc72392474..d40aee9826 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -5,13 +5,16 @@ module Splice.DSO.ResponseReimbursement where import DA.Action +import DA.Assert import DA.Foldable (forA_) import DA.Optional import DA.Text qualified as T +import DA.Time import Splice.DecentralizedSynchronizer import Splice.Util import Splice.DSO.CryptoHash +import Splice.Types @@ -43,6 +46,32 @@ template ReimbursementWorkflowState where signatory dso + -- Only called from DsoRules. We keep it as its own choice to simplify reasoning about the logic. + nonconsuming choice ReimbursementWorkflowState_AddReimbursementInterval + : ReimbursementWorkflowState_AddReimbursementIntervalResult + with + minIntervalDuration : RelTime + nextIntervalEnd : Time + controller dso + do + -- ensure minimal duration + let intervalDuration = nextIntervalEnd `subTime` nextIntervalStart + require "Minimal interval duration" (intervalDuration >= minIntervalDuration) + -- ensure that intervals are added at most `minIntervalDuration` ahead of time + let backdatedStart = nextIntervalStart `addRelTime` (negate minIntervalDuration) + assertDeadlineExceeded "nextIntervalStart - minIntervalDuration" backdatedStart + intervalStateCid <- create ReimbursementIntervalState with + dso = dso + interval = ReimbursementInterval with + startExclusive = nextIntervalStart + endInclusive = nextIntervalEnd + reimbursementConfirmation = None + workflowStateCid <- create this with + nextIntervalStart = nextIntervalEnd + pure ReimbursementWorkflowState_AddReimbursementIntervalResult with + intervalStateCid + workflowStateCid + data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWorkflowState_AddReimbursementIntervalResult with intervalStateCid : ContractId ReimbursementIntervalState @@ -50,27 +79,6 @@ data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWo deriving (Eq, Show) -addReimbursementInterval : Party -> RelTime -> ContractId ReimbursementWorkflowState -> Time -> Update (ContractId ReimbursementIntervalState, ContractId ReimbursementWorkflowState) -addReimbursementInterval dso minIntervalDuration workflowStateCid nextIntervalEnd = do - workflowState <- fetchAndArchive (ForDso dso) workflowStateCid - -- ensure minimal duration - let intervalDuration = nextIntervalEnd `subTime` workflowState.nextIntervalStart - require "Minimal interval duration" (intervalDuration >= minIntervalDuration) - -- ensure that intervals are added at most `minIntervalDuration` ahead of time - let backdatedStart = workflowState.nextIntervalStart `addRelTime` (negate minIntervalDuration) - assertDeadlineExceeded "nextIntervalStart - minIntervalDuration" backdatedStart - - intervalStateCid <- create ReimbursementIntervalState with - dso = dso - interval = ReimbursementInterval with - startExclusive = workflowState.nextIntervalStart - endInclusive = nextIntervalEnd - reimbursementConfirmation = None - workflowStateCid <- create workflowState with - nextIntervalStart = nextIntervalEnd - pure (intervalStateCid, workflowStateCid) - - -- State of reimbursing responses for a given interval. ------------------------------------------------------- @@ -151,12 +159,24 @@ data ReimbursementIntervalState_ReimburseResponsesResult = ReimbursementInterval deriving (Eq, Show) +{- confirmResponseTrafficReimbursement : Party -> ReimbursementConfirmation -> ContractId ReimbursementIntervalState -> Update (ContractId ReimbursementIntervalState) confirmResponseTrafficReimbursement dso reimbursementConfirmation intervalStateCid = do require "Not yet confirmed" (isNone this.reimbursementConfirmation) intervalStateCid <- create this with reimbursementConfirmation = Some reimbursementConfirmation pure ReimbursementIntervalState_ConfirmReimbursementResult with intervalStateCid +-} + + +-- Checked fetch +---------------- + +instance HasCheckedFetch ReimbursementWorkflowState ForDso where + contractGroupId ReimbursementWorkflowState {..} = ForDso with dso + +instance HasCheckedFetch ReimbursementIntervalState ForDso where + contractGroupId ReimbursementIntervalState {..} = ForDso with dso -- Hashing instances diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 490e32f086..501fe951af 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -119,7 +119,8 @@ data DsoRules_ActionRequiringConfirmation -- ^ Automated action to start reimbursing responses for a given interval. | SRARC_AddResponseTrafficReimbursementInterval DsoRules_AddResponseTrafficReimbursementInterval -- ^ Automated action to add a new confirmation response traffic reimbursement interval. - | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement + -- FIXME + -- | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement -- ^ Automated action to confirm the response traffic reimbursement data for a reimbursement interval. deriving (Eq, Show) @@ -301,6 +302,12 @@ data DsoRules_ExpireUnallocatedUnclaimedActivityRecordResult = DsoRules_ExpireUn data DsoRules_ExpireUnclaimedActivityRecordResult = DsoRules_ExpireUnclaimedActivityRecordResult with unclaimedRewardCid : ContractId UnclaimedReward +data DsoRules_AddResponseTrafficReimbursementIntervalResult = DsoRules_AddResponseTrafficReimbursementIntervalResult with + result : ReimbursementWorkflowState_AddReimbursementIntervalResult + +data DsoRules_StartResponseTrafficReimbursementResult = DsoRules_StartResponseTrafficReimbursementResult with + workflowStateCid : ContractId ReimbursementWorkflowState + data DsoRules_ReimburseResponsesResult = DsoRules_ReimburseResponsesResult with result : ReimbursementIntervalState_ReimburseResponsesResult @@ -1530,7 +1537,6 @@ template DsoRules with pure DsoRules_StartResponseTrafficReimbursementResult with workflowStateCid = workflowStateCid - nonconsuming choice DsoRules_AddResponseTrafficReimbursementInterval : DsoRules_AddResponseTrafficReimbursementIntervalResult with @@ -1538,10 +1544,13 @@ template DsoRules with nextIntervalEnd : Time controller dso do - let minIntervalDuration = config.responseReimbursementMinIntervalDuration - (intervalStateCid, workflowStateCid) <- - ResponseReimbursement.addReimbursementInterval dso minIntervalDuration workflowStateCid nextIntervalEnd + _ <- fetchChecked (ForDso with dso) workflowStateCid + result <- exercise workflowStateCid ReimbursementWorkflowState_AddReimbursementInterval with + minIntervalDuration = hours 1 -- FIXME: config.responseReimbursementMinIntervalDuration + nextIntervalEnd + pure DsoRules_AddResponseTrafficReimbursementIntervalResult with result +{- nonconsuming choice DsoRules_ConfirmResponseTrafficReimbursement : DsoRules_ConfirmResponseTrafficReimbursementResult with @@ -1565,15 +1574,9 @@ template DsoRules with responseTrafficData pure DsoRules_ReimburseResponsesResult with result +-} - | SRARC_StartResponseTrafficReimbursement DsoRules_StartResponseTrafficReimbursement - -- ^ Automated action to start reimbursing responses for a given interval. - | SRARC_AddResponseTrafficReimbursementInterval DsoRules_AddResponseTrafficReimbursementInterval - -- ^ Automated action to add a new confirmation response traffic reimbursement interval. - | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement - -- ^ Automated action to confirm the response traffic reimbursement data for a reimbursement interval. - pruneAtLeastOne : Ord t => t -> Schedule t a -> Optional (Schedule t a) pruneAtLeastOne now schedule = @@ -1633,6 +1636,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 + SRARC_StartResponseTrafficReimbursement choiceArg -> void $ exercise dsoRulesCid choiceArg + SRARC_AddResponseTrafficReimbursementInterval choiceArg -> void $ exercise dsoRulesCid choiceArg ARC_AnsEntryContext with .. -> do void $ fetchChecked (ForDso with dso) ansEntryContextCid case ansEntryContextAction of From 77e3c36aa1382df4926cbcca3b166461c72f2f5e Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 15:05:57 +0000 Subject: [PATCH 07/10] more testing: adding a new interval works --- .../daml/Splice/Scripts/DsoTestUtils.daml | 13 +++++ .../Scripts/TestResponseReimbursement.daml | 50 ++++++++++++------- .../Splice/DSO/ResponseReimbursement.daml | 2 +- .../daml/Splice/DsoRules.daml | 2 - 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml index 17abe747c3..a1a6f00c03 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml @@ -293,6 +293,19 @@ confirmAWC_MiningRound_Archive app = do confirmer = sv pure () +-- | Convenience function to confirm and execute an action requiring confirmation. +confirmAndExecutionAction : AmuletApp -> ActionRequiringConfirmation -> Script () +confirmAndExecutionAction app action = do + [(dsoRulesCid, rules)] <- query @DsoRules app.dso + forA_ (Map.keys rules.svs) $ \sv -> do + -- mallory does not act + unless ("mallory" `T.isPrefixOf` partyToText sv) $ do + submitMulti [sv] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ConfirmAction with + action + confirmer = sv + pure () + executeAllConfirmedActions app + executeAllConfirmedActions : AmuletApp -> Script () executeAllConfirmedActions app = do [(amuletRulesCid, _)] <- query @AmuletRules app.dso diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml index 924aab7182..2556f5f535 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml @@ -28,6 +28,7 @@ import Splice.AmuletConfig import Splice.CometBft import Splice.DecentralizedSynchronizer +import Splice.Scripts.Util import Splice.Scripts.DsoTestUtils import Splice.Testing.Registries.AmuletRegistry.Parameters @@ -37,6 +38,8 @@ test_confirmation_response_traffic_reimbursement : Script () test_confirmation_response_traffic_reimbursement = do (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet + setTime demoTime + [(dsoRulesCid, dsoRules)] <- query @DsoRules dso -- setup demo data @@ -55,10 +58,21 @@ test_confirmation_response_traffic_reimbursement = do responseTrafficDataHash -- setup reimbursement workflow state - + confirmAndExecutionAction app ARC_DsoRules with + dsoAction = SRARC_StartResponseTrafficReimbursement + DsoRules_StartResponseTrafficReimbursement -- add a new interval - -- confirm + now <- getTime + [(workflowStateCid, _)] <- query @ReimbursementWorkflowState dso + confirmAndExecutionAction app ARC_DsoRules with + dsoAction = SRARC_AddResponseTrafficReimbursementInterval + DsoRules_AddResponseTrafficReimbursementInterval with + nextIntervalEnd = now `addRelTime` hours 1 + workflowStateCid + + + -- confirm data -- check that there are no member traffic contracts yet [] <- query @MemberTraffic dso @@ -66,18 +80,20 @@ test_confirmation_response_traffic_reimbursement = do -- reimburse responses -- check that the expected member traffic contracts were created - actualMemberTrafficContracts <- query @MemberTraffic dso - let expectedMemberTrafficContracts = do - (memberId, totalTraffic) <- responseTrafficDataRaw - pure MemberTraffic with - dso = dso - memberId - synchronizerId = reimbursementConfirmation.synchronizerId - migrationId = reimbursementConfirmation.migrationId - totalReimbursed = Some totalTraffic - totalPurchased = 0 - amuletSpent = 0.0 - usdSpent = 0.0 - numPurchases = 0 - sortOn (.memberId) (map snd actualMemberTrafficContracts) === - expectedMemberTrafficContracts +-- actualMemberTrafficContracts <- query @MemberTraffic dso +-- let expectedMemberTrafficContracts = do +-- (memberId, totalTraffic) <- responseTrafficDataRaw +-- pure MemberTraffic with +-- dso = dso +-- memberId +-- synchronizerId = reimbursementConfirmation.synchronizerId +-- migrationId = reimbursementConfirmation.migrationId +-- totalReimbursed = Some totalTraffic +-- totalPurchased = 0 +-- amuletSpent = 0.0 +-- usdSpent = 0.0 +-- numPurchases = 0 +-- sortOn (.memberId) (map snd actualMemberTrafficContracts) === +-- expectedMemberTrafficContracts + + pure () diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index d40aee9826..2aa314f93a 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -47,7 +47,7 @@ template ReimbursementWorkflowState signatory dso -- Only called from DsoRules. We keep it as its own choice to simplify reasoning about the logic. - nonconsuming choice ReimbursementWorkflowState_AddReimbursementInterval + choice ReimbursementWorkflowState_AddReimbursementInterval : ReimbursementWorkflowState_AddReimbursementIntervalResult with minIntervalDuration : RelTime diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 501fe951af..dc092964d3 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -1525,8 +1525,6 @@ template DsoRules with ---------------------------------------------- nonconsuming choice DsoRules_StartResponseTrafficReimbursement : DsoRules_StartResponseTrafficReimbursementResult - with - sv : Party controller dso do now <- getTime From 00d99c780fe50c46dbe1c574cc12500da9959e9e Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 15:11:29 +0000 Subject: [PATCH 08/10] add confirmation of data --- .../Scripts/TestResponseReimbursement.daml | 8 +++++++- .../daml/Splice/DsoRules.daml | 20 +++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml index 2556f5f535..ec448c0a92 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml @@ -71,8 +71,14 @@ test_confirmation_response_traffic_reimbursement = do nextIntervalEnd = now `addRelTime` hours 1 workflowStateCid - -- confirm data + [(workflowStateCid, _)] <- query @ReimbursementWorkflowState dso + [(intervalStateCid, _)] <- query @ReimbursementIntervalState dso + confirmAndExecutionAction app ARC_DsoRules with + dsoAction = SRARC_ConfirmResponseTrafficReimbursement + DsoRules_ConfirmResponseTrafficReimbursement with + intervalStateCid + reimbursementConfirmation -- check that there are no member traffic contracts yet [] <- query @MemberTraffic dso diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index dc092964d3..990532d562 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -119,8 +119,7 @@ data DsoRules_ActionRequiringConfirmation -- ^ Automated action to start reimbursing responses for a given interval. | SRARC_AddResponseTrafficReimbursementInterval DsoRules_AddResponseTrafficReimbursementInterval -- ^ Automated action to add a new confirmation response traffic reimbursement interval. - -- FIXME - -- | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement + | SRARC_ConfirmResponseTrafficReimbursement DsoRules_ConfirmResponseTrafficReimbursement -- ^ Automated action to confirm the response traffic reimbursement data for a reimbursement interval. deriving (Eq, Show) @@ -302,11 +301,14 @@ data DsoRules_ExpireUnallocatedUnclaimedActivityRecordResult = DsoRules_ExpireUn data DsoRules_ExpireUnclaimedActivityRecordResult = DsoRules_ExpireUnclaimedActivityRecordResult with unclaimedRewardCid : ContractId UnclaimedReward +data DsoRules_StartResponseTrafficReimbursementResult = DsoRules_StartResponseTrafficReimbursementResult with + workflowStateCid : ContractId ReimbursementWorkflowState + data DsoRules_AddResponseTrafficReimbursementIntervalResult = DsoRules_AddResponseTrafficReimbursementIntervalResult with result : ReimbursementWorkflowState_AddReimbursementIntervalResult -data DsoRules_StartResponseTrafficReimbursementResult = DsoRules_StartResponseTrafficReimbursementResult with - workflowStateCid : ContractId ReimbursementWorkflowState +data DsoRules_ConfirmResponseTrafficReimbursementResult = DsoRules_ConfirmResponseTrafficReimbursementResult with + result : ReimbursementIntervalState_ConfirmReimbursementResult data DsoRules_ReimburseResponsesResult = DsoRules_ReimburseResponsesResult with result : ReimbursementIntervalState_ReimburseResponsesResult @@ -1548,18 +1550,19 @@ template DsoRules with nextIntervalEnd pure DsoRules_AddResponseTrafficReimbursementIntervalResult with result -{- nonconsuming choice DsoRules_ConfirmResponseTrafficReimbursement : DsoRules_ConfirmResponseTrafficReimbursementResult with intervalStateCid : ContractId ReimbursementIntervalState - confirmedResponseTrafficData : ReimbursementConfirmation + reimbursementConfirmation : ReimbursementConfirmation controller dso do - result <- exercise intervalStateCid ReimbursementIntervalState_ConfirmResponses with - confirmedResponseTrafficData + result <- exercise intervalStateCid ReimbursementIntervalState_ConfirmReimbursement with + reimbursementConfirmation pure DsoRules_ConfirmResponseTrafficReimbursementResult with result + +{- nonconsuming choice DsoRules_ReimburseResponseTraffic : DsoRules_ReimburseResponsesResult with intervalStateCid : ContractId ReimbursementIntervalState @@ -1636,6 +1639,7 @@ executeActionRequiringConfirmation dso dsoRulesCid amuletRulesCid act = case act SRARC_CreateUnallocatedUnclaimedActivityRecord choiceArg -> void $ exercise dsoRulesCid choiceArg SRARC_StartResponseTrafficReimbursement choiceArg -> void $ exercise dsoRulesCid choiceArg SRARC_AddResponseTrafficReimbursementInterval choiceArg -> void $ exercise dsoRulesCid choiceArg + SRARC_ConfirmResponseTrafficReimbursement choiceArg -> void $ exercise dsoRulesCid choiceArg ARC_AnsEntryContext with .. -> do void $ fetchChecked (ForDso with dso) ansEntryContextCid case ansEntryContextAction of From c03f4ee2c931487c288603b87829891d1510e272 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 15:21:55 +0000 Subject: [PATCH 09/10] test reimbursement --- .../Scripts/TestResponseReimbursement.daml | 36 +++++++++++-------- .../daml/Splice/DsoRules.daml | 7 ++-- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml index ec448c0a92..f6989afd39 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml @@ -84,22 +84,28 @@ test_confirmation_response_traffic_reimbursement = do [] <- query @MemberTraffic dso -- reimburse responses + [(intervalStateCid, _)] <- query @ReimbursementIntervalState dso + [(dsoRulesCid, dsoRules)] <- query @DsoRules dso + submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ReimburseResponseTraffic with + intervalStateCid + responseTrafficData + sv = sv1 -- check that the expected member traffic contracts were created --- actualMemberTrafficContracts <- query @MemberTraffic dso --- let expectedMemberTrafficContracts = do --- (memberId, totalTraffic) <- responseTrafficDataRaw --- pure MemberTraffic with --- dso = dso --- memberId --- synchronizerId = reimbursementConfirmation.synchronizerId --- migrationId = reimbursementConfirmation.migrationId --- totalReimbursed = Some totalTraffic --- totalPurchased = 0 --- amuletSpent = 0.0 --- usdSpent = 0.0 --- numPurchases = 0 --- sortOn (.memberId) (map snd actualMemberTrafficContracts) === --- expectedMemberTrafficContracts + actualMemberTrafficContracts <- query @MemberTraffic dso + let expectedMemberTrafficContracts = do + (memberId, totalTraffic) <- responseTrafficDataRaw + pure MemberTraffic with + dso = dso + memberId + synchronizerId = reimbursementConfirmation.synchronizerId + migrationId = reimbursementConfirmation.migrationId + totalReimbursed = Some totalTraffic + totalPurchased = 0 + amuletSpent = 0.0 + usdSpent = 0.0 + numPurchases = 0 + sortOn (.memberId) (map snd actualMemberTrafficContracts) === + expectedMemberTrafficContracts pure () diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 990532d562..bcfb460835 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -1561,8 +1561,6 @@ template DsoRules with reimbursementConfirmation pure DsoRules_ConfirmResponseTrafficReimbursementResult with result - -{- nonconsuming choice DsoRules_ReimburseResponseTraffic : DsoRules_ReimburseResponsesResult with intervalStateCid : ContractId ReimbursementIntervalState @@ -1571,13 +1569,12 @@ template DsoRules with controller sv do _ <- getAndValidateSvParty this (Some sv) + -- FIXME: avoid this exercise as it leads to a full copy of the response traffic totals + -- in the Ledger API server result <- exercise intervalStateCid ReimbursementIntervalState_ReimburseResponses with responseTrafficData pure DsoRules_ReimburseResponsesResult with result --} - - pruneAtLeastOne : Ord t => t -> Schedule t a -> Optional (Schedule t a) pruneAtLeastOne now schedule = From 2c74bdd2a360de8a8605e710e02cca737e5a60f0 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Thu, 4 Dec 2025 15:35:06 +0000 Subject: [PATCH 10/10] add tracking of interval completion --- .../Scripts/TestResponseReimbursement.daml | 37 ++++++++--------- .../Splice/DSO/ResponseReimbursement.daml | 41 ++++++++++++------- .../daml/Splice/DsoRules.daml | 15 +++++++ 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml index f6989afd39..bf21902fa5 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestResponseReimbursement.daml @@ -4,39 +4,23 @@ module Splice.Scripts.TestResponseReimbursement where import DA.Assert -import DA.Foldable (forA_) import DA.List -import qualified DA.Map as Map -import qualified DA.Set as Set import Daml.Script import DA.Time -import Splice.Amulet -import Splice.AmuletRules -import Splice.Round -import Splice.Schedule -import Splice.DSO.DecentralizedSynchronizer -import Splice.Fees -import Splice.Issuance() - - import Splice.DsoRules -import Splice.DSO.AmuletPrice import Splice.DSO.ResponseReimbursement -- FIXME: add Traffic to name import Splice.DSO.CryptoHash -import Splice.AmuletConfig -import Splice.CometBft import Splice.DecentralizedSynchronizer import Splice.Scripts.Util import Splice.Scripts.DsoTestUtils -import Splice.Testing.Registries.AmuletRegistry.Parameters -- | Tests that reimbursement works. test_confirmation_response_traffic_reimbursement : Script () test_confirmation_response_traffic_reimbursement = do - (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet + (app, dso, (sv1, _, _, _)) <- initMainNet setTime demoTime @@ -64,15 +48,15 @@ test_confirmation_response_traffic_reimbursement = do -- add a new interval now <- getTime + let testIntervalEnd = now `addRelTime` hours 1 [(workflowStateCid, _)] <- query @ReimbursementWorkflowState dso confirmAndExecutionAction app ARC_DsoRules with dsoAction = SRARC_AddResponseTrafficReimbursementInterval DsoRules_AddResponseTrafficReimbursementInterval with - nextIntervalEnd = now `addRelTime` hours 1 + nextIntervalEnd = testIntervalEnd workflowStateCid -- confirm data - [(workflowStateCid, _)] <- query @ReimbursementWorkflowState dso [(intervalStateCid, _)] <- query @ReimbursementIntervalState dso confirmAndExecutionAction app ARC_DsoRules with dsoAction = SRARC_ConfirmResponseTrafficReimbursement @@ -85,7 +69,6 @@ test_confirmation_response_traffic_reimbursement = do -- reimburse responses [(intervalStateCid, _)] <- query @ReimbursementIntervalState dso - [(dsoRulesCid, dsoRules)] <- query @DsoRules dso submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ReimburseResponseTraffic with intervalStateCid responseTrafficData @@ -108,4 +91,18 @@ test_confirmation_response_traffic_reimbursement = do sortOn (.memberId) (map snd actualMemberTrafficContracts) === expectedMemberTrafficContracts + -- record the completion + [(intervalStateCid, _)] <- query @ReimbursementIntervalState dso + [(workflowStateCid, _)] <- query @ReimbursementWorkflowState dso + submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_RecordCompletedResponseTrafficReimbursement with + intervalStateCid + workflowStateCid + sv = sv1 + + [] <- query @ReimbursementIntervalState dso + [(_, workflowState)] <- query @ReimbursementWorkflowState dso + (workflowState.earliestIntervalStart, workflowState.nextIntervalStart) === + (testIntervalEnd, testIntervalEnd) + + pure () diff --git a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml index 2aa314f93a..9a228482c9 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/ResponseReimbursement.daml @@ -2,6 +2,9 @@ -- SPDX-License-Identifier: Apache-2.0 -- | All the response reimbursement workflows code that does not depend on `DsoRules`. +-- +-- FIXME: the names in this module are not yet as clean as they could be. Make a pass. +-- In particular "Confirmation" may be confusing, and "Traffic" is missing from some names. module Splice.DSO.ResponseReimbursement where import DA.Action @@ -66,18 +69,34 @@ template ReimbursementWorkflowState startExclusive = nextIntervalStart endInclusive = nextIntervalEnd reimbursementConfirmation = None + reimbursementComplete = False workflowStateCid <- create this with nextIntervalStart = nextIntervalEnd pure ReimbursementWorkflowState_AddReimbursementIntervalResult with intervalStateCid workflowStateCid + choice ReimbursementWorkflowState_RecordCompletedInterval + : ReimbursementWorkflowState_RecordCompletedIntervalResult + with + intervalStateCid : ContractId ReimbursementIntervalState + controller dso + do + intervalState <- fetchAndArchive (ForDso dso) intervalStateCid + require "Interval must be reimbursed" (intervalState.reimbursementComplete) + require "Interval must be the earliest" (intervalState.interval.startExclusive == earliestIntervalStart) + workflowStateCid <- create this with + earliestIntervalStart = intervalState.interval.endInclusive + pure ReimbursementWorkflowState_RecordCompletedIntervalResult with + workflowStateCid + data ReimbursementWorkflowState_AddReimbursementIntervalResult = ReimbursementWorkflowState_AddReimbursementIntervalResult with intervalStateCid : ContractId ReimbursementIntervalState workflowStateCid : ContractId ReimbursementWorkflowState - deriving (Eq, Show) +data ReimbursementWorkflowState_RecordCompletedIntervalResult = ReimbursementWorkflowState_RecordCompletedIntervalResult with + workflowStateCid : ContractId ReimbursementWorkflowState -- State of reimbursing responses for a given interval. @@ -104,6 +123,9 @@ template ReimbursementIntervalState dso: Party interval: ReimbursementInterval reimbursementConfirmation : Optional ReimbursementConfirmation + -- ^ The confirmation of the reimbursement data hash for this interval. + reimbursementComplete : Bool + -- ^ Whether the response traffic for this interval has been reimbursed. where signatory dso @@ -147,26 +169,15 @@ template ReimbursementIntervalState -- Note: we intentionally do not return the created MemberTraffic contract IDs here -- to avoid bloating the transaction size on the Ledger API. + intervalStateCid <- create this with reimbursementComplete = True pure ReimbursementIntervalState_ReimburseResponsesResult with - dummy = () + intervalStateCid data ReimbursementIntervalState_ConfirmReimbursementResult = ReimbursementIntervalState_ConfirmReimbursementResult with intervalStateCid : ContractId ReimbursementIntervalState - deriving (Eq, Show) data ReimbursementIntervalState_ReimburseResponsesResult = ReimbursementIntervalState_ReimburseResponsesResult with - dummy : () - deriving (Eq, Show) - - -{- -confirmResponseTrafficReimbursement : Party -> ReimbursementConfirmation -> ContractId ReimbursementIntervalState -> Update (ContractId ReimbursementIntervalState) -confirmResponseTrafficReimbursement dso reimbursementConfirmation intervalStateCid = do - require "Not yet confirmed" (isNone this.reimbursementConfirmation) - intervalStateCid <- create this with reimbursementConfirmation = Some reimbursementConfirmation - pure ReimbursementIntervalState_ConfirmReimbursementResult with - intervalStateCid --} + intervalStateCid : ContractId ReimbursementIntervalState -- Checked fetch diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index bcfb460835..371c172f5c 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -313,6 +313,8 @@ data DsoRules_ConfirmResponseTrafficReimbursementResult = DsoRules_ConfirmRespon data DsoRules_ReimburseResponsesResult = DsoRules_ReimburseResponsesResult with result : ReimbursementIntervalState_ReimburseResponsesResult +data DsoRules_RecordCompletedResponseTrafficReimbursementIntervalResult = DsoRules_RecordCompletedResponseTrafficReimbursementIntervalResult with + result : ReimbursementWorkflowState_RecordCompletedIntervalResult -- Workflow templates --------------------- @@ -1575,6 +1577,19 @@ template DsoRules with responseTrafficData pure DsoRules_ReimburseResponsesResult with result + nonconsuming choice DsoRules_RecordCompletedResponseTrafficReimbursement + : DsoRules_RecordCompletedResponseTrafficReimbursementIntervalResult + with + workflowStateCid : ContractId ReimbursementWorkflowState + intervalStateCid : ContractId ReimbursementIntervalState + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise workflowStateCid ReimbursementWorkflowState_RecordCompletedInterval with + intervalStateCid + pure DsoRules_RecordCompletedResponseTrafficReimbursementIntervalResult with result + pruneAtLeastOne : Ord t => t -> Schedule t a -> Optional (Schedule t a) pruneAtLeastOne now schedule =