Skip to content

Commit 5af39a1

Browse files
[ci] Added a test for zeroed balance values in bft scan connection.
Also added release notes. Signed-off-by: Raymond Roestenburg <[email protected]>
1 parent 029401d commit 5af39a1

File tree

3 files changed

+196
-59
lines changed

3 files changed

+196
-59
lines changed

apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala

Lines changed: 62 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ class SingleScanConnection private[client] (
9999
with ScanConnection
100100
with BackfillingScanConnection
101101
with HasUrl {
102+
import ScanRoundAggregatesDecoder.*
103+
102104
def url = config.adminApi.url
103105

104106
// cached DSO reference. Never changes.
@@ -392,64 +394,6 @@ class SingleScanConnection private[client] (
392394
}
393395
}
394396

395-
private def decodeRoundTotal(
396-
rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundTotals
397-
): Either[String, ScanAggregator.RoundTotals] = {
398-
(for {
399-
closedRoundEffectiveAt <- CantonTimestamp.fromInstant(rt.closedRoundEffectiveAt.toInstant)
400-
appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards)
401-
validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards)
402-
cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards)
403-
cumulativeValidatorRewards <- Codec
404-
.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards)
405-
} yield {
406-
// changeToInitialAmountAsOfRoundZero, changeToHoldingFeesRate, cumulativeChangeToInitialAmountAsOfRoundZero,
407-
// cumulativeChangeToHoldingFeesRate and totalAmuletBalance are intentionally left out
408-
// since these do not match up anymore because amulet expires are attributed to the closed round at a later stage
409-
// in scan_txlog_store, at a time that can easily differ between SVs.
410-
ScanAggregator.RoundTotals(
411-
closedRound = rt.closedRound,
412-
closedRoundEffectiveAt = closedRoundEffectiveAt,
413-
appRewards = appRewards,
414-
validatorRewards = validatorRewards,
415-
cumulativeAppRewards = cumulativeAppRewards,
416-
cumulativeValidatorRewards = cumulativeValidatorRewards,
417-
)
418-
})
419-
}
420-
421-
private def decodeRoundPartyTotals(
422-
rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundPartyTotals
423-
): Either[String, ScanAggregator.RoundPartyTotals] = {
424-
(for {
425-
appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards)
426-
validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards)
427-
trafficPurchasedCcSpent <- Codec.decode(Codec.BigDecimal)(rt.trafficPurchasedCcSpent)
428-
cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards)
429-
cumulativeValidatorRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards)
430-
cumulativeTrafficPurchasedCcSpent <- Codec
431-
.decode(Codec.BigDecimal)(rt.cumulativeTrafficPurchasedCcSpent)
432-
} yield {
433-
// cumulativeChangeToInitialAmountAsOfRoundZero and cumulativeChangeToHoldingFeesRate are intentionally left out
434-
// since these do not match up anymore because amulet expires are attributed to the closed round at a later stage
435-
// in scan_txlog_store, at a time that can easily differ between SVs.
436-
ScanAggregator.RoundPartyTotals(
437-
closedRound = rt.closedRound,
438-
party = rt.party,
439-
appRewards = appRewards,
440-
validatorRewards = validatorRewards,
441-
trafficPurchased = rt.trafficPurchased,
442-
trafficPurchasedCcSpent = trafficPurchasedCcSpent,
443-
trafficNumPurchases = rt.trafficNumPurchases,
444-
cumulativeAppRewards = cumulativeAppRewards,
445-
cumulativeValidatorRewards = cumulativeValidatorRewards,
446-
cumulativeTrafficPurchased = rt.cumulativeTrafficPurchased,
447-
cumulativeTrafficPurchasedCcSpent = cumulativeTrafficPurchasedCcSpent,
448-
cumulativeTrafficNumPurchases = rt.cumulativeTrafficNumPurchases,
449-
)
450-
})
451-
}
452-
453397
override def getMigrationSchedule()(implicit
454398
ec: ExecutionContext,
455399
tc: TraceContext,
@@ -866,3 +810,63 @@ class CachedScanConnection private[client] (
866810
)
867811
)
868812
}
813+
814+
object ScanRoundAggregatesDecoder {
815+
def decodeRoundTotal(
816+
rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundTotals
817+
): Either[String, ScanAggregator.RoundTotals] = {
818+
(for {
819+
closedRoundEffectiveAt <- CantonTimestamp.fromInstant(rt.closedRoundEffectiveAt.toInstant)
820+
appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards)
821+
validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards)
822+
cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards)
823+
cumulativeValidatorRewards <- Codec
824+
.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards)
825+
} yield {
826+
// changeToInitialAmountAsOfRoundZero, changeToHoldingFeesRate, cumulativeChangeToInitialAmountAsOfRoundZero,
827+
// cumulativeChangeToHoldingFeesRate and totalAmuletBalance are intentionally left out
828+
// since these do not match up anymore because amulet expires are attributed to the closed round at a later stage
829+
// in scan_txlog_store, at a time that can easily differ between SVs.
830+
ScanAggregator.RoundTotals(
831+
closedRound = rt.closedRound,
832+
closedRoundEffectiveAt = closedRoundEffectiveAt,
833+
appRewards = appRewards,
834+
validatorRewards = validatorRewards,
835+
cumulativeAppRewards = cumulativeAppRewards,
836+
cumulativeValidatorRewards = cumulativeValidatorRewards,
837+
)
838+
})
839+
}
840+
841+
def decodeRoundPartyTotals(
842+
rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundPartyTotals
843+
): Either[String, ScanAggregator.RoundPartyTotals] = {
844+
(for {
845+
appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards)
846+
validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards)
847+
trafficPurchasedCcSpent <- Codec.decode(Codec.BigDecimal)(rt.trafficPurchasedCcSpent)
848+
cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards)
849+
cumulativeValidatorRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards)
850+
cumulativeTrafficPurchasedCcSpent <- Codec
851+
.decode(Codec.BigDecimal)(rt.cumulativeTrafficPurchasedCcSpent)
852+
} yield {
853+
// cumulativeChangeToInitialAmountAsOfRoundZero and cumulativeChangeToHoldingFeesRate are intentionally left out
854+
// since these do not match up anymore because amulet expires are attributed to the closed round at a later stage
855+
// in scan_txlog_store, at a time that can easily differ between SVs.
856+
ScanAggregator.RoundPartyTotals(
857+
closedRound = rt.closedRound,
858+
party = rt.party,
859+
appRewards = appRewards,
860+
validatorRewards = validatorRewards,
861+
trafficPurchased = rt.trafficPurchased,
862+
trafficPurchasedCcSpent = trafficPurchasedCcSpent,
863+
trafficNumPurchases = rt.trafficNumPurchases,
864+
cumulativeAppRewards = cumulativeAppRewards,
865+
cumulativeValidatorRewards = cumulativeValidatorRewards,
866+
cumulativeTrafficPurchased = rt.cumulativeTrafficPurchased,
867+
cumulativeTrafficPurchasedCcSpent = cumulativeTrafficPurchasedCcSpent,
868+
cumulativeTrafficNumPurchases = rt.cumulativeTrafficNumPurchases,
869+
)
870+
})
871+
}
872+
}

apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import org.lfdecentralizedtrust.splice.environment.{
2929
RetryProvider,
3030
SpliceLedgerClient,
3131
}
32-
import org.lfdecentralizedtrust.splice.http.v0.definitions.ErrorResponse
32+
import org.lfdecentralizedtrust.splice.http.v0.definitions.{
33+
ErrorResponse,
34+
RoundPartyTotals as HttpRoundPartyTotals,
35+
RoundTotals as HttpRoundTotals,
36+
}
3337
import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection.Bft
3438
import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.{
3539
DomainScans,
@@ -40,6 +44,7 @@ import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationI
4044
import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState
4145
import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse
4246
import org.lfdecentralizedtrust.splice.util.{
47+
Codec,
4348
Contract,
4449
ContractWithState,
4550
DomainRecordTimeRange,
@@ -54,6 +59,7 @@ import org.slf4j.event.Level
5459
import java.time.{Duration, Instant}
5560
import java.util.concurrent.atomic.AtomicInteger
5661
import scala.concurrent.{ExecutionContext, Future}
62+
import scala.util.Random
5763

5864
// mock verification triggers this
5965
@SuppressWarnings(Array("com.digitalasset.canton.DiscardedFuture"))
@@ -922,6 +928,126 @@ class BftScanConnectionTest
922928
result shouldBe Some(roundAggregate)
923929
}
924930

931+
"get BFT round aggregates from scans that report having the round aggregate ignoring balance fields" in {
932+
val round = 0L
933+
def randomValue = BigDecimal(Random.nextInt(50) + 1)
934+
def mkRoundTotals() = RoundTotals(
935+
closedRound = round,
936+
closedRoundEffectiveAt = CantonTimestamp.MinValue,
937+
appRewards = BigDecimal(100),
938+
validatorRewards = BigDecimal(150),
939+
changeToInitialAmountAsOfRoundZero = randomValue,
940+
changeToHoldingFeesRate = randomValue,
941+
cumulativeAppRewards = BigDecimal(1100),
942+
cumulativeValidatorRewards = BigDecimal(1150),
943+
cumulativeChangeToInitialAmountAsOfRoundZero = randomValue,
944+
cumulativeChangeToHoldingFeesRate = randomValue,
945+
totalAmuletBalance = randomValue,
946+
)
947+
def encodeRoundTotals(rt: RoundTotals) = {
948+
HttpRoundTotals(
949+
closedRound = rt.closedRound,
950+
closedRoundEffectiveAt = Codec.OffsetDateTime.instance.encode(rt.closedRoundEffectiveAt),
951+
appRewards = Codec.encode(rt.appRewards),
952+
validatorRewards = Codec.encode(rt.validatorRewards),
953+
changeToInitialAmountAsOfRoundZero = Codec.encode(rt.changeToInitialAmountAsOfRoundZero),
954+
changeToHoldingFeesRate = Codec.encode(rt.changeToHoldingFeesRate),
955+
cumulativeAppRewards = Codec.encode(rt.cumulativeAppRewards),
956+
cumulativeValidatorRewards = Codec.encode(rt.cumulativeValidatorRewards),
957+
cumulativeChangeToInitialAmountAsOfRoundZero =
958+
Codec.encode(rt.cumulativeChangeToInitialAmountAsOfRoundZero),
959+
cumulativeChangeToHoldingFeesRate = Codec.encode(rt.cumulativeChangeToHoldingFeesRate),
960+
totalAmuletBalance = Codec.encode(rt.totalAmuletBalance),
961+
)
962+
}
963+
def encodeRoundPartyTotals(rpt: RoundPartyTotals) = {
964+
HttpRoundPartyTotals(
965+
closedRound = rpt.closedRound,
966+
party = rpt.party,
967+
appRewards = Codec.encode(rpt.appRewards),
968+
validatorRewards = Codec.encode(rpt.validatorRewards),
969+
trafficPurchased = rpt.trafficPurchased,
970+
trafficPurchasedCcSpent = Codec.encode(rpt.trafficPurchasedCcSpent),
971+
trafficNumPurchases = rpt.trafficNumPurchases,
972+
cumulativeAppRewards = Codec.encode(rpt.cumulativeAppRewards),
973+
cumulativeValidatorRewards = Codec.encode(rpt.cumulativeValidatorRewards),
974+
cumulativeChangeToInitialAmountAsOfRoundZero =
975+
Codec.encode(rpt.cumulativeChangeToInitialAmountAsOfRoundZero),
976+
cumulativeChangeToHoldingFeesRate = Codec.encode(rpt.cumulativeChangeToHoldingFeesRate),
977+
cumulativeTrafficPurchased = rpt.cumulativeTrafficPurchased,
978+
cumulativeTrafficPurchasedCcSpent = Codec.encode(rpt.cumulativeTrafficPurchasedCcSpent),
979+
cumulativeTrafficNumPurchases = rpt.cumulativeTrafficNumPurchases,
980+
)
981+
}
982+
def mkRoundPartyTotals() = RoundPartyTotals(
983+
closedRound = round,
984+
party = "party-id",
985+
appRewards = BigDecimal(10),
986+
validatorRewards = BigDecimal(20),
987+
trafficPurchased = 10L,
988+
trafficPurchasedCcSpent = BigDecimal(30),
989+
trafficNumPurchases = 30L,
990+
cumulativeAppRewards = BigDecimal(40),
991+
cumulativeValidatorRewards = BigDecimal(50),
992+
cumulativeChangeToInitialAmountAsOfRoundZero = randomValue,
993+
cumulativeChangeToHoldingFeesRate = randomValue,
994+
cumulativeTrafficPurchased = 50L,
995+
cumulativeTrafficPurchasedCcSpent = BigDecimal(70),
996+
cumulativeTrafficNumPurchases = 70L,
997+
)
998+
def mkRoundAggregateUsingDecoder() = RoundAggregate(
999+
ScanRoundAggregatesDecoder.decodeRoundTotal(encodeRoundTotals(mkRoundTotals())).value,
1000+
Vector(
1001+
ScanRoundAggregatesDecoder
1002+
.decodeRoundPartyTotals(encodeRoundPartyTotals(mkRoundPartyTotals()))
1003+
.value
1004+
),
1005+
)
1006+
def mkRoundAggregateWithoutDecoder() = RoundAggregate(
1007+
mkRoundTotals(),
1008+
Vector(mkRoundPartyTotals()),
1009+
)
1010+
val roundAggregateZeroBalanceValues = mkRoundAggregateWithoutDecoder().copy(
1011+
roundTotals = mkRoundAggregateWithoutDecoder().roundTotals.copy(
1012+
changeToInitialAmountAsOfRoundZero = zero,
1013+
changeToHoldingFeesRate = zero,
1014+
cumulativeChangeToInitialAmountAsOfRoundZero = zero,
1015+
cumulativeChangeToHoldingFeesRate = zero,
1016+
totalAmuletBalance = zero,
1017+
),
1018+
roundPartyTotals = mkRoundAggregateWithoutDecoder().roundPartyTotals.map(
1019+
_.copy(
1020+
cumulativeChangeToInitialAmountAsOfRoundZero = zero,
1021+
cumulativeChangeToHoldingFeesRate = zero,
1022+
)
1023+
),
1024+
)
1025+
1026+
def getConnections(roundAggregateResponse: () => RoundAggregate) = {
1027+
val connections = getMockedConnections(n = 10)
1028+
connections.foreach { mock =>
1029+
when(mock.getAggregatedRounds())
1030+
.thenReturn(Future.successful(Some(RoundRange(round, round))))
1031+
when(mock.getRoundAggregate(round))
1032+
.thenReturn(Future.successful(Some(roundAggregateResponse())))
1033+
}
1034+
connections
1035+
}
1036+
1037+
val bft = getBft(getConnections(() => mkRoundAggregateUsingDecoder()))
1038+
val con =
1039+
new ScanAggregatesConnection(bft, retryProvider, retryProvider.loggerFactory)
1040+
val result = con.getRoundAggregate(round).futureValue
1041+
result shouldBe Some(roundAggregateZeroBalanceValues)
1042+
1043+
// not using the decoder should fail on the randomized balance values.
1044+
val bftFail = getBft(getConnections(() => mkRoundAggregateWithoutDecoder()))
1045+
val conFail =
1046+
new ScanAggregatesConnection(bftFail, retryProvider, retryProvider.loggerFactory)
1047+
val resultFail = conFail.getRoundAggregate(round).failed.futureValue
1048+
resultFail shouldBe an[BftScanConnection.ConsensusNotReached]
1049+
}
1050+
9251051
"Not get round aggregates from scans that report having the round aggregate if too many fail" in {
9261052
val connections = getMockedConnections(n = 10)
9271053
connections.zipWithIndex.foreach { case (mock, index) =>

docs/src/release_notes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ Upcoming
2424

2525
- Fix bug that caused validators to fail on restoring participant users without rights during a synchronizer migration.
2626

27+
- Scan
28+
29+
- The round-based aggregates for balance values (changes to holding fees and initial amounts since round zero)
30+
have diverged between scans because of how amulets expire in the ``scan_txlog_store``.
31+
The balance values recorded in the round aggregates are effectively not used anymore, and are now set to zero to avoid consensus problems when an SV reads aggregates
32+
from the rest of the network.
33+
2734
0.5.1
2835
-----
2936

0 commit comments

Comments
 (0)