Skip to content

Commit f9d738c

Browse files
committed
SV-app automation for VoteExecutionInstruction execution
Fixes to ReceiveFaucetCouponTrigger to ignore weight 0 licenses for ValidatorLivenessActivityRecord creation Added Integration tests to verify - Batched ValidatorLicense modification via vote - Check the rewards issuance with non-default ValidatorLicense weights - UnclaimedRewards creation with non-default weights - Confirm that weight 0 licenses still recorded activity without the coupon creation. PR reviewed in # 3297 Signed-off-by: Divam <dfordivam@gmail.com>
1 parent 3f72869 commit f9d738c

File tree

14 files changed

+723
-61
lines changed

14 files changed

+723
-61
lines changed

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package org.lfdecentralizedtrust.splice.integration.tests
22

33
import cats.syntax.parallel.*
4+
import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality
45
import com.digitalasset.canton.util.FutureInstances.*
56
import org.lfdecentralizedtrust.splice.codegen.java.splice
7+
import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.voteexecution.VoteExecutionInstruction
68
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.actionrequiringconfirmation.ARC_DsoRules
79
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.dsorules_actionrequiringconfirmation.{
810
SRARC_ConfirmSvOnboarding,
911
SRARC_SetConfig,
1012
}
13+
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.validatorlicensechange.VLC_ChangeWeight
1114
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{
1215
ActionRequiringConfirmation,
1316
DsoRulesConfig,
@@ -225,5 +228,57 @@ class SvTimeBasedOnboardingIntegrationTest
225228
},
226229
)
227230
}
231+
232+
clue("expire stale `VoteExecutionInstruction` contracts") {
233+
// Create a vote for weight change for a party not having a ValidatorLicense
234+
// The VoteExecutionInstruction cannot be run in this case and should be expired
235+
val newPartyWithoutLicense = allocateRandomSvParty("test-validator-expiry")
236+
237+
// Ignore the warnings issued by ExecuteVoteInstructionTrigger
238+
loggerFactory.assertLogsUnorderedOptional(
239+
{
240+
actAndCheck(
241+
"Modify validator licenses",
242+
modifyValidatorLicenses(
243+
sv1Backend,
244+
Seq(sv2Backend, sv3Backend),
245+
Seq(
246+
new VLC_ChangeWeight(
247+
newPartyWithoutLicense.toProtoPrimitive,
248+
BigDecimal(10.0).bigDecimal,
249+
)
250+
),
251+
),
252+
)(
253+
"VoteExecutionInstruction is created",
254+
_ =>
255+
sv1Backend.participantClientWithAdminToken.ledger_api_extensions.acs
256+
.filterJava(VoteExecutionInstruction.COMPANION)(
257+
dsoParty,
258+
_ => true,
259+
) should have length 1,
260+
)
261+
262+
// Advance time past the default timeout of 1 day
263+
val clockSkew = sv1Backend.config.automation.clockSkewAutomationDelay.asJava
264+
actAndCheck(
265+
"Advance time past the vote execution instruction timeout",
266+
advanceTime(JavaDuration.ofDays(2) plus clockSkew),
267+
)(
268+
"VoteExecutionInstruction is expired",
269+
_ => {
270+
sv1Backend.participantClientWithAdminToken.ledger_api_extensions.acs
271+
.filterJava(VoteExecutionInstruction.COMPANION)(
272+
dsoParty,
273+
_ => true,
274+
) shouldBe empty
275+
},
276+
)
277+
},
278+
LogEntryOptionality.OptionalMany -> (_.warningMessage should include(
279+
"ValidatorLicense not found for validator"
280+
)),
281+
)
282+
}
228283
}
229284
}

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvValidatorLicenseIntegrationTest.scala

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ package org.lfdecentralizedtrust.splice.integration.tests
66
import com.digitalasset.canton.logging.SuppressionRule
77
import com.digitalasset.canton.topology.PartyId
88
import org.lfdecentralizedtrust.splice.codegen.java.splice.validatorlicense.ValidatorLicense
9+
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.validatorlicensechange.{
10+
VLC_ChangeWeight,
11+
VLC_Withdraw,
12+
}
913
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
1014
import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.MergeValidatorLicenseContractsTrigger
1115
import org.lfdecentralizedtrust.splice.util.TriggerTestUtil
@@ -111,4 +115,71 @@ class SvValidatorLicenseIntegrationTest
111115
),
112116
)
113117
}
118+
119+
"can do batch modification of validator licenses" in { implicit env =>
120+
val info = sv1Backend.getDsoInfo()
121+
val dsoParty = info.dsoParty
122+
123+
def getLicenses(p: PartyId) = {
124+
aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs
125+
.filterJava(ValidatorLicense.COMPANION)(
126+
dsoParty,
127+
c => c.data.validator == p.toProtoPrimitive,
128+
)
129+
}
130+
131+
// Allocate two external parties on aliceValidator
132+
val OnboardingResult(newParty1, _, _) =
133+
onboardExternalParty(aliceValidatorBackend, Some("alice-test-party-1"))
134+
val OnboardingResult(newParty2, _, _) =
135+
onboardExternalParty(aliceValidatorBackend, Some("alice-test-party-2"))
136+
137+
// Grant licenses to both validators
138+
actAndCheck(
139+
"Grant validator license to first validator",
140+
sv1Backend.grantValidatorLicense(newParty1),
141+
)(
142+
"ValidatorLicense granted with default weight",
143+
_ => {
144+
val licenses = getLicenses(newParty1)
145+
licenses should have length 1
146+
licenses.head.data.weight.toScala shouldBe None
147+
},
148+
)
149+
150+
actAndCheck(
151+
"Grant validator license to second validator",
152+
sv1Backend.grantValidatorLicense(newParty2),
153+
)(
154+
"ValidatorLicense granted",
155+
_ => {
156+
val licenses = getLicenses(newParty2)
157+
licenses should have length 1
158+
licenses.head.data.weight.toScala shouldBe None
159+
},
160+
)
161+
162+
// Create a vote for batch modification, with both weight change and withdrawal
163+
actAndCheck(
164+
"Modify validator licenses",
165+
modifyValidatorLicenses(
166+
sv1Backend,
167+
svsToCastVotes = Seq.empty,
168+
Seq(
169+
new VLC_ChangeWeight(newParty1.toProtoPrimitive, BigDecimal(10.0).bigDecimal),
170+
new VLC_Withdraw(newParty2.toProtoPrimitive),
171+
),
172+
),
173+
)(
174+
"validator license modifications have been applied",
175+
_ => {
176+
val licenses1 = getLicenses(newParty1)
177+
licenses1 should have length 1
178+
licenses1.head.data.weight.toScala shouldBe Some(BigDecimal("10.0000000000").bigDecimal)
179+
180+
val licenses2 = getLicenses(newParty2)
181+
licenses2 shouldBe empty
182+
},
183+
)
184+
}
114185
}

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorLicenseMetadataTimeBasedIntegrationTest.scala

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.lfdecentralizedtrust.splice.integration.tests
22

33
import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.UnclaimedReward
4+
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.validatorlicensechange.VLC_ChangeWeight
45
import org.lfdecentralizedtrust.splice.codegen.java.splice.validatorlicense.*
56
import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{
67
ConfigurableApp,
@@ -9,7 +10,12 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{
910
import org.lfdecentralizedtrust.splice.environment.BuildInfo
1011
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
1112
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest
12-
import org.lfdecentralizedtrust.splice.util.{TimeTestUtil, TriggerTestUtil, WalletTestUtil}
13+
import org.lfdecentralizedtrust.splice.util.{
14+
SvTestUtil,
15+
TimeTestUtil,
16+
TriggerTestUtil,
17+
WalletTestUtil,
18+
}
1319
import org.lfdecentralizedtrust.splice.validator.automation.ReceiveFaucetCouponTrigger
1420
import org.lfdecentralizedtrust.splice.wallet.automation.CollectRewardsAndMergeAmuletsTrigger
1521
import com.digitalasset.canton.config.CantonRequireTypes.InstanceName
@@ -20,7 +26,8 @@ class ValidatorLicenseMetadataTimeBasedIntegrationTest
2026
extends IntegrationTest
2127
with WalletTestUtil
2228
with TimeTestUtil
23-
with TriggerTestUtil {
29+
with TriggerTestUtil
30+
with SvTestUtil {
2431

2532
override def environmentDefinition: SpliceEnvironmentDefinition =
2633
EnvironmentDefinition
@@ -134,67 +141,103 @@ class ValidatorLicenseMetadataTimeBasedIntegrationTest
134141
}
135142
}
136143

137-
"unclaimed ValidatorLivenessActivityRecord contracts should be expired" in { implicit env =>
138-
startAllSync(
139-
sv1Backend,
140-
sv1ScanBackend,
141-
aliceValidatorBackend,
142-
)
143-
144-
advanceTimeForRewardAutomationToRunForCurrentRound
145-
146-
eventually() {
147-
val validatorLivenessActivityRecordRounds =
148-
sv1Backend.participantClient.ledger_api_extensions.acs
149-
.filterJava(ValidatorLivenessActivityRecord.COMPANION)(
150-
dsoParty
151-
)
152-
.map(_.data.round.number)
153-
.toSet
144+
"expired ValidatorLivenessActivityRecord creates UnclaimedReward with correct weight" in {
145+
implicit env =>
146+
startAllSync(
147+
sv1Backend,
148+
sv1ScanBackend,
149+
aliceValidatorBackend,
150+
)
154151

155-
validatorLivenessActivityRecordRounds shouldBe Set(0L, 1L)
156-
}
157-
158-
// pause the trigger to avoid collecting further rewards
159-
setTriggersWithin(
160-
triggersToResumeAtStart = Seq.empty,
161-
triggersToPauseAtStart = Seq(
162-
aliceValidatorBackend.validatorAutomation.trigger[ReceiveFaucetCouponTrigger]
163-
),
164-
) {
152+
val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId()
165153

154+
// Change Alice's validator license weight to 10.0
155+
val aliceNewWeight = BigDecimal(10.0)
166156
actAndCheck(
167-
"Advance rounds so that issuing rounds 0 and 1 no longer exist",
168-
(1 to 5).foreach { _ =>
169-
advanceRoundsToNextRoundOpening
170-
},
157+
"Modify validator licenses",
158+
modifyValidatorLicenses(
159+
sv1Backend,
160+
svsToCastVotes = Seq.empty,
161+
Seq(new VLC_ChangeWeight(aliceValidatorParty.toProtoPrimitive, aliceNewWeight.bigDecimal)),
162+
),
171163
)(
172-
"ValidatorLivenessActivityRecord contracts for round 0 and 1 should be expired",
164+
"validator license modifications have been applied",
173165
_ => {
174-
val issuingRounds = sv1ScanBackend.getOpenAndIssuingMiningRounds()._2
175-
issuingRounds.map(_.payload.round.number).toSet shouldBe Set(2L, 3L, 4L)
176-
177-
val allValidatorLivenessActivityRecord =
178-
sv1Backend.participantClient.ledger_api_extensions.acs
179-
.filterJava(ValidatorLivenessActivityRecord.COMPANION)(
180-
dsoParty
166+
val licenses =
167+
aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs
168+
.filterJava(ValidatorLicense.COMPANION)(
169+
dsoParty,
170+
c => c.data.validator == aliceValidatorParty.toProtoPrimitive,
181171
)
182-
allValidatorLivenessActivityRecord shouldBe empty
183-
184-
// assuming this value is the same in that of round 0 and 1
185-
val issuancePerValidatorFaucetCoupon =
186-
issuingRounds.headOption.value.payload.optIssuancePerValidatorFaucetCoupon.toScala.value
187-
188-
val unclaimedRewards = sv1Backend.participantClient.ledger_api_extensions.acs
189-
.filterJava(UnclaimedReward.COMPANION)(
190-
dsoParty
191-
)
192-
.map(_.data.amount)
193-
194-
unclaimedRewards.count(_ == issuancePerValidatorFaucetCoupon) shouldBe 2
172+
licenses should have length 1
173+
licenses.head.data.weight.toScala.map(BigDecimal(_)) shouldBe Some(aliceNewWeight)
195174
},
196175
)
197-
succeed
198-
}
176+
177+
advanceTimeForRewardAutomationToRunForCurrentRound
178+
179+
// Verify liveness activity records are created for with correct weight
180+
val aliceActivityRecords = eventually() {
181+
val records = sv1Backend.participantClient.ledger_api_extensions.acs
182+
.filterJava(ValidatorLivenessActivityRecord.COMPANION)(
183+
dsoParty,
184+
c => c.data.validator == aliceValidatorParty.toProtoPrimitive,
185+
)
186+
records.map(_.data.round.number).toSet shouldBe Set(0L, 1L)
187+
records
188+
}
189+
190+
aliceActivityRecords.foreach { record =>
191+
record.data.weight.toScala.map(BigDecimal(_)) shouldBe Some(aliceNewWeight)
192+
}
193+
194+
val recordRounds = aliceActivityRecords.map(_.data.round.number).toSet
195+
196+
// pause the trigger to avoid collecting further rewards
197+
setTriggersWithin(
198+
triggersToResumeAtStart = Seq.empty,
199+
triggersToPauseAtStart = Seq(
200+
aliceValidatorBackend.validatorAutomation.trigger[ReceiveFaucetCouponTrigger]
201+
),
202+
) {
203+
actAndCheck(
204+
"Advance rounds so that issuing rounds 0 and 1 no longer exist",
205+
(1 to 5).foreach { _ =>
206+
advanceRoundsToNextRoundOpening
207+
},
208+
)(
209+
"ValidatorLivenessActivityRecord contracts for round 0 and 1 should be expired",
210+
_ => {
211+
// Verify activity records are expired
212+
val allValidatorLivenessActivityRecord =
213+
sv1Backend.participantClient.ledger_api_extensions.acs
214+
.filterJava(ValidatorLivenessActivityRecord.COMPANION)(
215+
dsoParty
216+
)
217+
allValidatorLivenessActivityRecord shouldBe empty
218+
219+
// Get issuance value from issuing rounds
220+
val issuingRounds = sv1ScanBackend.getOpenAndIssuingMiningRounds()._2
221+
issuingRounds should not be empty
222+
223+
val issuancePerValidatorFaucetCoupon =
224+
issuingRounds.headOption.value.payload.optIssuancePerValidatorFaucetCoupon.toScala.value
225+
226+
// Expected amount per record is weight * issuance
227+
val expectedAmountPerRecord =
228+
aliceNewWeight.bigDecimal.multiply(issuancePerValidatorFaucetCoupon)
229+
230+
val unclaimedRewards = sv1Backend.participantClient.ledger_api_extensions.acs
231+
.filterJava(UnclaimedReward.COMPANION)(dsoParty)
232+
.map(_.data.amount)
233+
234+
// Should have unclaimed rewards with the weighted amount for each round
235+
unclaimedRewards.count(
236+
_.compareTo(expectedAmountPerRecord) == 0
237+
) shouldBe recordRounds.size
238+
},
239+
)
240+
succeed
241+
}
199242
}
200243
}

0 commit comments

Comments
 (0)