Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ceeb2b6
SvDsoStore: Ingest RewardCouponV2, CalculateRewardsV2, ProcessRewardsV2
dfordivam Apr 3, 2026
f0d49cd
Use provider as rewardParty in dso-store
dfordivam May 28, 2026
edede74
Handle stale CRARC_StartProcessingRewardsV2
dfordivam Apr 6, 2026
e96d847
Add listCalculateRewardsV2
dfordivam Apr 7, 2026
01846ca
yaml: archive-dry-runs API
dfordivam Apr 22, 2026
51a88b7
listDryRunRewardAccountingContractsByRounds
dfordivam Apr 22, 2026
145e263
archiveDryRunRewardAccountingContracts
dfordivam Apr 22, 2026
9771b7a
sv-app archiveDryRunRewardAccountingContracts
dfordivam Apr 24, 2026
538a8db
SvdsoStore: add listProcessRewardsV2 and listRewardCouponV2
dfordivam Apr 30, 2026
db48d3a
Add CalculateRewardsTrigger
dfordivam Apr 7, 2026
b1064ef
ProcessRewardsTrigger init
dfordivam Apr 9, 2026
51826df
scalatesttag SpliceAmulet_0_1_19
dfordivam Apr 27, 2026
92b6f36
RewardProcessingMetrics init
dfordivam May 6, 2026
3982dd0
Test: modify integ test
dfordivam May 8, 2026
ba59335
Test: Add SvApp trigger test
dfordivam May 8, 2026
60730fc
updateTestConfigForParallelRuns
dfordivam May 8, 2026
99667ae
Fix test assertions
dfordivam May 15, 2026
1a4d4a6
Fix test
dfordivam May 15, 2026
cd6ece7
Retry if getRootHash in undetermined, error on cannot-provide response
dfordivam May 15, 2026
01747f1
Do archive of round 0..5. use single SV topology
dfordivam May 18, 2026
ebdfa48
Fix setAmuletConfig voting by other SVs
dfordivam May 18, 2026
95fcdb8
Use 4 SVs in SvApp test
dfordivam May 18, 2026
3ee7356
Test: Ignore SV submitted txs in activity record computation
dfordivam May 20, 2026
d883a33
Test: Keep only DryRun and MintingTrafficBased flows
dfordivam May 20, 2026
487d0ba
Filter ProcessRewardsV2 contracts by dryRun at the source
dfordivam May 20, 2026
3721255
Just retry on scan connection failure
dfordivam May 20, 2026
b97875f
Throw FAILED_PRECONDITION when waiting on scan
dfordivam May 20, 2026
c7aae0b
Add TODOs for BFT read
dfordivam May 20, 2026
8f23637
Use 'mining_round' as it has index
dfordivam May 20, 2026
e7192e1
Bump DbSvDsoStore version
dfordivam May 20, 2026
196ecd9
Test: add TODO
dfordivam May 20, 2026
4399da2
Test: remove wrong assertions
dfordivam May 20, 2026
58fa7ad
Add listConfirmationsByConfirmer
dfordivam May 21, 2026
9f87a9e
Filter confirmed actions in retrieveTasks and isStaleTask
dfordivam May 21, 2026
ae09ba1
Improve success message
dfordivam May 21, 2026
b61716a
Add comment
dfordivam May 21, 2026
3c05f11
Add ScanConnection in SvDsoAutomationService for use in triggers
dfordivam May 22, 2026
025cd67
Fixes per suggestions
dfordivam May 28, 2026
7823e8a
[ci]
dfordivam May 28, 2026
1f7a70e
[ci] fix closeAsync
dfordivam May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,16 @@ class SvAppBackendReference(
}
}

@Help.Summary(
"Archive dry-run CalculateRewardsV2 and ProcessRewardsV2 contracts for the given rounds (via admin API)"
)
def archiveDryRunRewardAccountingContracts(rounds: Seq[Long]): Unit =
consoleEnvironment.run {
httpCommand(
HttpSvOperatorAppClient.ArchiveDryRunRewardAccountingContracts(rounds)
)
}

@Help.Summary("Get the CometBFT node debug dump")
def cometBftNodeDump(): definitions.CometBftNodeDumpResponse =
consoleEnvironment.run {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package org.lfdecentralizedtrust.splice.integration.tests

import com.digitalasset.canton.HasExecutionContext
import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.topology.PartyId
import java.time.Duration
import java.util.Optional
import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletconfig.{
AmuletConfig,
RewardConfig,
RewardVersion,
USD,
}
import org.lfdecentralizedtrust.splice.config.ConfigTransforms
import org.lfdecentralizedtrust.splice.http.v0.definitions
import definitions.GetRewardAccountingBatchResponse
import definitions.GetRewardAccountingRootHashResponse
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{
IntegrationTestWithIsolatedEnvironment,
SpliceTestConsoleEnvironment,
}
import org.lfdecentralizedtrust.splice.sv.automation.confirmation.{
CalculateRewardsDryRunTrigger,
CalculateRewardsTrigger,
}
import org.lfdecentralizedtrust.splice.sv.config.InitialRewardConfig
import org.lfdecentralizedtrust.splice.util.{
AmuletConfigSchedule,
AmuletConfigUtil,
TimeTestUtil,
TriggerTestUtil,
WalletTestUtil,
}

// This test focuses on the SV app side triggers testing
// - Turning on/off of dry-run and minting-version in rewardConfig
// And confirming that rewards processing works.
//
// Later this test would be extended to cover unhide, expire, etc
@org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceAmulet_0_1_19
class TrafficBasedRewardsSvAppTimeBasedIntegrationTest
extends IntegrationTestWithIsolatedEnvironment
with HasExecutionContext
with WalletTestUtil
with TriggerTestUtil
with TimeTestUtil
with AmuletConfigUtil {

override def environmentDefinition: SpliceEnvironmentDefinition =
EnvironmentDefinition
.simpleTopology4SvsWithSimTime(this.getClass.getSimpleName)
.addConfigTransform((_, config) =>
ConfigTransforms.withRewardConfig(
InitialRewardConfig(
dryRunVersion = None,
appRewardCouponThreshold = BigDecimal("0"),
)
)(config)
)

"Enable, disable of dryRunVersion/mintingVersion take effect at round closure" in {
implicit env =>
val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend)
val bobParty = onboardWalletUser(bobWalletClient, bobValidatorBackend)

aliceWalletClient.tap(20000)

grantFeaturedAppRight(aliceWalletClient)
grantFeaturedAppRight(bobWalletClient)

for (round <- 1 to 3) {
advanceRoundsToNextRoundOpening
assertOldestOpenRound(round.toLong)
}

// oldest=3: rounds 3,4,5 open.
// Next round to open is R6, it will have dryRun enabled
clue("vote to enable dryRunVersion") {
changeRewardConfig(enableDryRun = true)
}

advanceRoundsToNextRoundOpening
assertOldestOpenRound(4)
doTransfer(bobParty)

// oldest=4: rounds 4,5,6 open.
// R7 will have the disabled config.
clue("vote to disable dryRunVersion") {
changeRewardConfig(enableDryRun = false)
}

advanceRoundsToNextRoundOpening
assertOldestOpenRound(5)
doTransfer(bobParty)

// oldest=5: rounds 5,6,7 open. R8 will have
// both dryRunVersion and mintingVersion set.
clue("vote to enable dryRunVersion + mintingVersion") {
changeRewardConfig(enableDryRun = true, enableMinting = true)
}

val svBackends = Seq(sv1Backend, sv2Backend, sv3Backend, sv4Backend)
val calculateRewardsDryRunTriggers =
svBackends.map(_.dsoAutomation.trigger[CalculateRewardsDryRunTrigger])
val calculateRewardsTriggers =
svBackends.map(_.dsoAutomation.trigger[CalculateRewardsTrigger])

// Create activity for 6, 7, and 8 and confirm creation of CalculateRewardsV2
setTriggersWithin(
triggersToPauseAtStart = calculateRewardsDryRunTriggers ++ calculateRewardsTriggers
) {
advanceRoundsToNextRoundOpening
assertOldestOpenRound(6)
doTransfer(bobParty)

advanceRoundsToNextRoundOpening
assertOldestOpenRound(7)
doTransfer(bobParty)

advanceRoundsToNextRoundOpening
assertOldestOpenRound(8)
doTransfer(bobParty)

advanceRoundsToNextRoundOpening
assertOldestOpenRound(9)
doTransfer(bobParty)

clue("CalculateRewardsV2 are created for rounds, 6 and 8") {
eventually() {
val v2s = sv1Backend.appState.dsoStore.listCalculateRewardsV2().futureValue
v2s.map(_.payload.round.number) should contain(6L)
v2s.map(_.payload.round.number) should not contain 7L
v2s
.filter(_.payload.round.number == 8L)
.map(_.payload.dryRun)
.toSet shouldBe Set(true, false)
}
}
}

clue("Alice and Bob have minting allowances for R6") {
eventually() {
val hash = inside(sv1ScanBackend.getRewardAccountingRootHash(6L)) {
case GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(h) =>
h.rootHash
}
val providers = walkBatch(6L, hash).map(_.provider)
providers should contain(aliceParty.toProtoPrimitive)
providers should contain(bobParty.toProtoPrimitive)
}
}

clue("Alice and Bob have minting allowances for R8") {
eventually() {
val hash = inside(sv1ScanBackend.getRewardAccountingRootHash(8L)) {
case GetRewardAccountingRootHashResponse.members.RewardAccountingRootHashOk(h) =>
h.rootHash
}
val providers = walkBatch(8L, hash).map(_.provider)
providers should contain(aliceParty.toProtoPrimitive)
providers should contain(bobParty.toProtoPrimitive)
}
}

clue("All CalculateRewardsV2 and ProcessRewardsV2 contracts consumed") {
eventually() {
sv1Backend.appState.dsoStore.listCalculateRewardsV2().futureValue shouldBe empty
sv1Backend.appState.dsoStore.listProcessRewardsV2().futureValue shouldBe empty
}
}

clue("Alice and Bob received RewardCouponV2 for R8") {
eventually() {
val coupons = sv1Backend.appState.dsoStore.listRewardCouponsV2().futureValue
coupons.filter(c =>
c.payload.round.number == 8L && c.payload.provider == aliceParty.toProtoPrimitive
) should not be empty
coupons.filter(c =>
c.payload.round.number == 8L && c.payload.provider == bobParty.toProtoPrimitive
) should not be empty
}
}
}

private def doTransfer(
bobParty: PartyId
)(implicit env: SpliceTestConsoleEnvironment): Unit = {
val offerCid = aliceWalletClient.createTransferOffer(
bobParty,
BigDecimal(10.0),
"activity",
CantonTimestamp.now().plus(Duration.ofMinutes(1)),
s"transfer-${scala.util.Random.nextInt()}",
)
bobWalletClient.acceptTransferOffer(offerCid)
}

private def walkBatch(
round: Long,
hash: String,
)(implicit
env: SpliceTestConsoleEnvironment
): Seq[definitions.RewardAccountingMintingAllowance] =
sv1ScanBackend.getRewardAccountingBatch(round, hash).toList.flatMap {
case GetRewardAccountingBatchResponse.members.RewardAccountingBatchOfBatches(b) =>
b.childHashes.flatMap(h => walkBatch(round, h))
case GetRewardAccountingBatchResponse.members.RewardAccountingBatchOfMintingAllowances(b) =>
b.mintingAllowances.toSeq
}

private def assertOldestOpenRound(
expected: Long
)(implicit env: SpliceTestConsoleEnvironment): Unit = {
clue(s"Asserting oldest open round=$expected") {
eventually() {
val (openRounds, _) = sv1ScanBackend.getOpenAndIssuingMiningRounds()
val roundNumbers = openRounds.map(_.contract.payload.round.number.toLong).sorted
roundNumbers should have size 3
roundNumbers.head shouldBe expected
}
}
}

private def changeRewardConfig(
enableDryRun: Boolean,
enableMinting: Boolean = false,
)(implicit env: SpliceTestConsoleEnvironment): Unit = {
val amuletRules = sv1Backend.getDsoInfo().amuletRules
val existing = AmuletConfigSchedule(amuletRules).getConfigAsOf(env.environment.clock.now)
val rc = existing.rewardConfig.get()
val newRc = new RewardConfig(
if (enableMinting) RewardVersion.REWARDVERSION_TRAFFICBASEDAPPREWARDS
else rc.mintingVersion,
if (enableDryRun) Optional.of(RewardVersion.REWARDVERSION_TRAFFICBASEDAPPREWARDS)
else Optional.empty[RewardVersion](),
rc.batchSize,
rc.rewardCouponTimeToLive,
rc.appRewardCouponThreshold,
)
val newConfig = new AmuletConfig[USD](
existing.transferConfig,
existing.issuanceCurve,
existing.decentralizedSynchronizer,
existing.tickDuration,
existing.packageConfig,
existing.transferPreapprovalFee,
existing.featuredAppActivityMarkerAmount,
existing.optDevelopmentFundManager,
existing.externalPartyConfigStateTickDuration,
Optional.of(newRc),
)
setAmuletConfig(Seq((None, newConfig, existing)))
eventually() {
sv1Backend.listVoteRequests() shouldBe empty
}
}

}
Loading
Loading