From 029401dd2f170a97a574d157a19e7eb75a28c701 Mon Sep 17 00:00:00 2001 From: Raymond Roestenburg Date: Fri, 14 Nov 2025 09:40:41 +0000 Subject: [PATCH 1/4] [ci] Zero holding fee and initial amount values in responses so that they always agree. Signed-off-by: Raymond Roestenburg --- .../api/client/SingleScanConnection.scala | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala index a491b39fc3..2264e2d28f 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala @@ -399,30 +399,21 @@ class SingleScanConnection private[client] ( closedRoundEffectiveAt <- CantonTimestamp.fromInstant(rt.closedRoundEffectiveAt.toInstant) appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards) validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards) - changeToInitialAmountAsOfRoundZero <- Codec - .decode(Codec.BigDecimal)(rt.changeToInitialAmountAsOfRoundZero) - changeToHoldingFeesRate <- Codec.decode(Codec.BigDecimal)(rt.changeToHoldingFeesRate) cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards) cumulativeValidatorRewards <- Codec .decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards) - cumulativeChangeToInitialAmountAsOfRoundZero <- Codec - .decode(Codec.BigDecimal)(rt.cumulativeChangeToInitialAmountAsOfRoundZero) - cumulativeChangeToHoldingFeesRate <- Codec - .decode(Codec.BigDecimal)(rt.cumulativeChangeToHoldingFeesRate) - totalAmuletBalance <- Codec.decode(Codec.BigDecimal)(rt.totalAmuletBalance) } yield { + // changeToInitialAmountAsOfRoundZero, changeToHoldingFeesRate, cumulativeChangeToInitialAmountAsOfRoundZero, + // cumulativeChangeToHoldingFeesRate and totalAmuletBalance are intentionally left out + // since these do not match up anymore because amulet expires are attributed to the closed round at a later stage + // in scan_txlog_store, at a time that can easily differ between SVs. ScanAggregator.RoundTotals( closedRound = rt.closedRound, closedRoundEffectiveAt = closedRoundEffectiveAt, appRewards = appRewards, validatorRewards = validatorRewards, - changeToInitialAmountAsOfRoundZero = changeToInitialAmountAsOfRoundZero, - changeToHoldingFeesRate = changeToHoldingFeesRate, cumulativeAppRewards = cumulativeAppRewards, cumulativeValidatorRewards = cumulativeValidatorRewards, - cumulativeChangeToInitialAmountAsOfRoundZero = cumulativeChangeToInitialAmountAsOfRoundZero, - cumulativeChangeToHoldingFeesRate = cumulativeChangeToHoldingFeesRate, - totalAmuletBalance = totalAmuletBalance, ) }) } @@ -436,13 +427,12 @@ class SingleScanConnection private[client] ( trafficPurchasedCcSpent <- Codec.decode(Codec.BigDecimal)(rt.trafficPurchasedCcSpent) cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards) cumulativeValidatorRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards) - cumulativeChangeToInitialAmountAsOfRoundZero <- Codec - .decode(Codec.BigDecimal)(rt.cumulativeChangeToInitialAmountAsOfRoundZero) - cumulativeChangeToHoldingFeesRate <- Codec - .decode(Codec.BigDecimal)(rt.cumulativeChangeToHoldingFeesRate) cumulativeTrafficPurchasedCcSpent <- Codec .decode(Codec.BigDecimal)(rt.cumulativeTrafficPurchasedCcSpent) } yield { + // cumulativeChangeToInitialAmountAsOfRoundZero and cumulativeChangeToHoldingFeesRate are intentionally left out + // since these do not match up anymore because amulet expires are attributed to the closed round at a later stage + // in scan_txlog_store, at a time that can easily differ between SVs. ScanAggregator.RoundPartyTotals( closedRound = rt.closedRound, party = rt.party, @@ -453,8 +443,6 @@ class SingleScanConnection private[client] ( trafficNumPurchases = rt.trafficNumPurchases, cumulativeAppRewards = cumulativeAppRewards, cumulativeValidatorRewards = cumulativeValidatorRewards, - cumulativeChangeToInitialAmountAsOfRoundZero = cumulativeChangeToInitialAmountAsOfRoundZero, - cumulativeChangeToHoldingFeesRate = cumulativeChangeToHoldingFeesRate, cumulativeTrafficPurchased = rt.cumulativeTrafficPurchased, cumulativeTrafficPurchasedCcSpent = cumulativeTrafficPurchasedCcSpent, cumulativeTrafficNumPurchases = rt.cumulativeTrafficNumPurchases, From 23d08e3b2416f856caeb879a817abc608af936ea Mon Sep 17 00:00:00 2001 From: Raymond Roestenburg Date: Fri, 14 Nov 2025 12:10:56 +0000 Subject: [PATCH 2/4] [ci] Added a test for zeroed balance values in bft scan connection. Also added release notes. Signed-off-by: Raymond Roestenburg --- .../api/client/SingleScanConnection.scala | 120 ++++++++-------- .../api/client/BftScanConnectionTest.scala | 128 +++++++++++++++++- docs/src/release_notes.rst | 7 + 3 files changed, 196 insertions(+), 59 deletions(-) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala index 2264e2d28f..c6f43c6198 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala @@ -99,6 +99,8 @@ class SingleScanConnection private[client] ( with ScanConnection with BackfillingScanConnection with HasUrl { + import ScanRoundAggregatesDecoder.* + def url = config.adminApi.url // cached DSO reference. Never changes. @@ -392,64 +394,6 @@ class SingleScanConnection private[client] ( } } - private def decodeRoundTotal( - rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundTotals - ): Either[String, ScanAggregator.RoundTotals] = { - (for { - closedRoundEffectiveAt <- CantonTimestamp.fromInstant(rt.closedRoundEffectiveAt.toInstant) - appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards) - validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards) - cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards) - cumulativeValidatorRewards <- Codec - .decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards) - } yield { - // changeToInitialAmountAsOfRoundZero, changeToHoldingFeesRate, cumulativeChangeToInitialAmountAsOfRoundZero, - // cumulativeChangeToHoldingFeesRate and totalAmuletBalance are intentionally left out - // since these do not match up anymore because amulet expires are attributed to the closed round at a later stage - // in scan_txlog_store, at a time that can easily differ between SVs. - ScanAggregator.RoundTotals( - closedRound = rt.closedRound, - closedRoundEffectiveAt = closedRoundEffectiveAt, - appRewards = appRewards, - validatorRewards = validatorRewards, - cumulativeAppRewards = cumulativeAppRewards, - cumulativeValidatorRewards = cumulativeValidatorRewards, - ) - }) - } - - private def decodeRoundPartyTotals( - rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundPartyTotals - ): Either[String, ScanAggregator.RoundPartyTotals] = { - (for { - appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards) - validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards) - trafficPurchasedCcSpent <- Codec.decode(Codec.BigDecimal)(rt.trafficPurchasedCcSpent) - cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards) - cumulativeValidatorRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards) - cumulativeTrafficPurchasedCcSpent <- Codec - .decode(Codec.BigDecimal)(rt.cumulativeTrafficPurchasedCcSpent) - } yield { - // cumulativeChangeToInitialAmountAsOfRoundZero and cumulativeChangeToHoldingFeesRate are intentionally left out - // since these do not match up anymore because amulet expires are attributed to the closed round at a later stage - // in scan_txlog_store, at a time that can easily differ between SVs. - ScanAggregator.RoundPartyTotals( - closedRound = rt.closedRound, - party = rt.party, - appRewards = appRewards, - validatorRewards = validatorRewards, - trafficPurchased = rt.trafficPurchased, - trafficPurchasedCcSpent = trafficPurchasedCcSpent, - trafficNumPurchases = rt.trafficNumPurchases, - cumulativeAppRewards = cumulativeAppRewards, - cumulativeValidatorRewards = cumulativeValidatorRewards, - cumulativeTrafficPurchased = rt.cumulativeTrafficPurchased, - cumulativeTrafficPurchasedCcSpent = cumulativeTrafficPurchasedCcSpent, - cumulativeTrafficNumPurchases = rt.cumulativeTrafficNumPurchases, - ) - }) - } - override def getMigrationSchedule()(implicit ec: ExecutionContext, tc: TraceContext, @@ -866,3 +810,63 @@ class CachedScanConnection private[client] ( ) ) } + +object ScanRoundAggregatesDecoder { + def decodeRoundTotal( + rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundTotals + ): Either[String, ScanAggregator.RoundTotals] = { + (for { + closedRoundEffectiveAt <- CantonTimestamp.fromInstant(rt.closedRoundEffectiveAt.toInstant) + appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards) + validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards) + cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards) + cumulativeValidatorRewards <- Codec + .decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards) + } yield { + // changeToInitialAmountAsOfRoundZero, changeToHoldingFeesRate, cumulativeChangeToInitialAmountAsOfRoundZero, + // cumulativeChangeToHoldingFeesRate and totalAmuletBalance are intentionally left out + // since these do not match up anymore because amulet expires are attributed to the closed round at a later stage + // in scan_txlog_store, at a time that can easily differ between SVs. + ScanAggregator.RoundTotals( + closedRound = rt.closedRound, + closedRoundEffectiveAt = closedRoundEffectiveAt, + appRewards = appRewards, + validatorRewards = validatorRewards, + cumulativeAppRewards = cumulativeAppRewards, + cumulativeValidatorRewards = cumulativeValidatorRewards, + ) + }) + } + + def decodeRoundPartyTotals( + rt: org.lfdecentralizedtrust.splice.http.v0.definitions.RoundPartyTotals + ): Either[String, ScanAggregator.RoundPartyTotals] = { + (for { + appRewards <- Codec.decode(Codec.BigDecimal)(rt.appRewards) + validatorRewards <- Codec.decode(Codec.BigDecimal)(rt.validatorRewards) + trafficPurchasedCcSpent <- Codec.decode(Codec.BigDecimal)(rt.trafficPurchasedCcSpent) + cumulativeAppRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeAppRewards) + cumulativeValidatorRewards <- Codec.decode(Codec.BigDecimal)(rt.cumulativeValidatorRewards) + cumulativeTrafficPurchasedCcSpent <- Codec + .decode(Codec.BigDecimal)(rt.cumulativeTrafficPurchasedCcSpent) + } yield { + // cumulativeChangeToInitialAmountAsOfRoundZero and cumulativeChangeToHoldingFeesRate are intentionally left out + // since these do not match up anymore because amulet expires are attributed to the closed round at a later stage + // in scan_txlog_store, at a time that can easily differ between SVs. + ScanAggregator.RoundPartyTotals( + closedRound = rt.closedRound, + party = rt.party, + appRewards = appRewards, + validatorRewards = validatorRewards, + trafficPurchased = rt.trafficPurchased, + trafficPurchasedCcSpent = trafficPurchasedCcSpent, + trafficNumPurchases = rt.trafficNumPurchases, + cumulativeAppRewards = cumulativeAppRewards, + cumulativeValidatorRewards = cumulativeValidatorRewards, + cumulativeTrafficPurchased = rt.cumulativeTrafficPurchased, + cumulativeTrafficPurchasedCcSpent = cumulativeTrafficPurchasedCcSpent, + cumulativeTrafficNumPurchases = rt.cumulativeTrafficNumPurchases, + ) + }) + } +} diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala index 6799c45ad7..00c9d7582e 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala @@ -29,7 +29,11 @@ import org.lfdecentralizedtrust.splice.environment.{ RetryProvider, SpliceLedgerClient, } -import org.lfdecentralizedtrust.splice.http.v0.definitions.ErrorResponse +import org.lfdecentralizedtrust.splice.http.v0.definitions.{ + ErrorResponse, + RoundPartyTotals as HttpRoundPartyTotals, + RoundTotals as HttpRoundTotals, +} import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection.Bft import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.{ DomainScans, @@ -40,6 +44,7 @@ import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationI import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse import org.lfdecentralizedtrust.splice.util.{ + Codec, Contract, ContractWithState, DomainRecordTimeRange, @@ -54,6 +59,7 @@ import org.slf4j.event.Level import java.time.{Duration, Instant} import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.{ExecutionContext, Future} +import scala.util.Random // mock verification triggers this @SuppressWarnings(Array("com.digitalasset.canton.DiscardedFuture")) @@ -922,6 +928,126 @@ class BftScanConnectionTest result shouldBe Some(roundAggregate) } + "get BFT round aggregates from scans that report having the round aggregate ignoring balance fields" in { + val round = 0L + def randomValue = BigDecimal(Random.nextInt(50) + 1) + def mkRoundTotals() = RoundTotals( + closedRound = round, + closedRoundEffectiveAt = CantonTimestamp.MinValue, + appRewards = BigDecimal(100), + validatorRewards = BigDecimal(150), + changeToInitialAmountAsOfRoundZero = randomValue, + changeToHoldingFeesRate = randomValue, + cumulativeAppRewards = BigDecimal(1100), + cumulativeValidatorRewards = BigDecimal(1150), + cumulativeChangeToInitialAmountAsOfRoundZero = randomValue, + cumulativeChangeToHoldingFeesRate = randomValue, + totalAmuletBalance = randomValue, + ) + def encodeRoundTotals(rt: RoundTotals) = { + HttpRoundTotals( + closedRound = rt.closedRound, + closedRoundEffectiveAt = Codec.OffsetDateTime.instance.encode(rt.closedRoundEffectiveAt), + appRewards = Codec.encode(rt.appRewards), + validatorRewards = Codec.encode(rt.validatorRewards), + changeToInitialAmountAsOfRoundZero = Codec.encode(rt.changeToInitialAmountAsOfRoundZero), + changeToHoldingFeesRate = Codec.encode(rt.changeToHoldingFeesRate), + cumulativeAppRewards = Codec.encode(rt.cumulativeAppRewards), + cumulativeValidatorRewards = Codec.encode(rt.cumulativeValidatorRewards), + cumulativeChangeToInitialAmountAsOfRoundZero = + Codec.encode(rt.cumulativeChangeToInitialAmountAsOfRoundZero), + cumulativeChangeToHoldingFeesRate = Codec.encode(rt.cumulativeChangeToHoldingFeesRate), + totalAmuletBalance = Codec.encode(rt.totalAmuletBalance), + ) + } + def encodeRoundPartyTotals(rpt: RoundPartyTotals) = { + HttpRoundPartyTotals( + closedRound = rpt.closedRound, + party = rpt.party, + appRewards = Codec.encode(rpt.appRewards), + validatorRewards = Codec.encode(rpt.validatorRewards), + trafficPurchased = rpt.trafficPurchased, + trafficPurchasedCcSpent = Codec.encode(rpt.trafficPurchasedCcSpent), + trafficNumPurchases = rpt.trafficNumPurchases, + cumulativeAppRewards = Codec.encode(rpt.cumulativeAppRewards), + cumulativeValidatorRewards = Codec.encode(rpt.cumulativeValidatorRewards), + cumulativeChangeToInitialAmountAsOfRoundZero = + Codec.encode(rpt.cumulativeChangeToInitialAmountAsOfRoundZero), + cumulativeChangeToHoldingFeesRate = Codec.encode(rpt.cumulativeChangeToHoldingFeesRate), + cumulativeTrafficPurchased = rpt.cumulativeTrafficPurchased, + cumulativeTrafficPurchasedCcSpent = Codec.encode(rpt.cumulativeTrafficPurchasedCcSpent), + cumulativeTrafficNumPurchases = rpt.cumulativeTrafficNumPurchases, + ) + } + def mkRoundPartyTotals() = RoundPartyTotals( + closedRound = round, + party = "party-id", + appRewards = BigDecimal(10), + validatorRewards = BigDecimal(20), + trafficPurchased = 10L, + trafficPurchasedCcSpent = BigDecimal(30), + trafficNumPurchases = 30L, + cumulativeAppRewards = BigDecimal(40), + cumulativeValidatorRewards = BigDecimal(50), + cumulativeChangeToInitialAmountAsOfRoundZero = randomValue, + cumulativeChangeToHoldingFeesRate = randomValue, + cumulativeTrafficPurchased = 50L, + cumulativeTrafficPurchasedCcSpent = BigDecimal(70), + cumulativeTrafficNumPurchases = 70L, + ) + def mkRoundAggregateUsingDecoder() = RoundAggregate( + ScanRoundAggregatesDecoder.decodeRoundTotal(encodeRoundTotals(mkRoundTotals())).value, + Vector( + ScanRoundAggregatesDecoder + .decodeRoundPartyTotals(encodeRoundPartyTotals(mkRoundPartyTotals())) + .value + ), + ) + def mkRoundAggregateWithoutDecoder() = RoundAggregate( + mkRoundTotals(), + Vector(mkRoundPartyTotals()), + ) + val roundAggregateZeroBalanceValues = mkRoundAggregateWithoutDecoder().copy( + roundTotals = mkRoundAggregateWithoutDecoder().roundTotals.copy( + changeToInitialAmountAsOfRoundZero = zero, + changeToHoldingFeesRate = zero, + cumulativeChangeToInitialAmountAsOfRoundZero = zero, + cumulativeChangeToHoldingFeesRate = zero, + totalAmuletBalance = zero, + ), + roundPartyTotals = mkRoundAggregateWithoutDecoder().roundPartyTotals.map( + _.copy( + cumulativeChangeToInitialAmountAsOfRoundZero = zero, + cumulativeChangeToHoldingFeesRate = zero, + ) + ), + ) + + def getConnections(roundAggregateResponse: () => RoundAggregate) = { + val connections = getMockedConnections(n = 10) + connections.foreach { mock => + when(mock.getAggregatedRounds()) + .thenReturn(Future.successful(Some(RoundRange(round, round)))) + when(mock.getRoundAggregate(round)) + .thenReturn(Future.successful(Some(roundAggregateResponse()))) + } + connections + } + + val bft = getBft(getConnections(() => mkRoundAggregateUsingDecoder())) + val con = + new ScanAggregatesConnection(bft, retryProvider, retryProvider.loggerFactory) + val result = con.getRoundAggregate(round).futureValue + result shouldBe Some(roundAggregateZeroBalanceValues) + + // not using the decoder should fail on the randomized balance values. + val bftFail = getBft(getConnections(() => mkRoundAggregateWithoutDecoder())) + val conFail = + new ScanAggregatesConnection(bftFail, retryProvider, retryProvider.loggerFactory) + val resultFail = conFail.getRoundAggregate(round).failed.futureValue + resultFail shouldBe an[BftScanConnection.ConsensusNotReached] + } + "Not get round aggregates from scans that report having the round aggregate if too many fail" in { val connections = getMockedConnections(n = 10) connections.zipWithIndex.foreach { case (mock, index) => diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 3460f71642..5fc60808fe 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -24,6 +24,13 @@ Upcoming - Fix bug that caused validators to fail on restoring participant users without rights during a synchronizer migration. +- Scan + + - The round-based aggregates for balance values (changes to holding fees and initial amounts since round zero) + have diverged between scans because of how amulets expire in the ``scan_txlog_store``. + 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 + from the rest of the network when first joining. + 0.5.1 ----- From 35b6cf635df93c0975f5c9c10f71d52a8c648d6f Mon Sep 17 00:00:00 2001 From: Raymond Roestenburg Date: Fri, 14 Nov 2025 14:22:11 +0000 Subject: [PATCH 3/4] [ci] review, DRY. Signed-off-by: Raymond Roestenburg --- .../scan/admin/http/HttpScanHandler.scala | 87 ++++++++++--------- .../api/client/BftScanConnectionTest.scala | 49 ++--------- 2 files changed, 53 insertions(+), 83 deletions(-) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala index 3d97589410..92204732e0 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala @@ -95,6 +95,7 @@ import com.digitalasset.canton.util.ErrorUtil import org.lfdecentralizedtrust.splice.environment.TopologyAdminConnection.TopologyTransactionType.AuthorizedState import org.lfdecentralizedtrust.splice.scan.config.BftSequencerConfig import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.QueryAcsSnapshotResult +import org.lfdecentralizedtrust.splice.scan.store.db.ScanAggregator.{RoundPartyTotals, RoundTotals} import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.TxLogBackfillingState import org.lfdecentralizedtrust.splice.store.UpdateHistory.BackfillingState import org.lfdecentralizedtrust.splice.store.UpdateHistory @@ -125,7 +126,7 @@ class HttpScanHandler( with HttpVotesHandler with HttpValidatorLicensesHandler with HttpFeatureSupportHandler { - + import HttpScanHandler.* override protected val workflowId: String = this.getClass.getSimpleName override protected val votesStore: VotesStore = store override protected val validatorLicensesStore: AppStore = store @@ -1853,25 +1854,7 @@ class HttpScanHandler( ensureValidRange(request.startRound, request.endRound, 200) { for { roundTotals <- store.getRoundTotals(request.startRound, request.endRound) - entries = roundTotals.map { roundTotal => - definitions.RoundTotals( - closedRound = roundTotal.closedRound, - closedRoundEffectiveAt = java.time.OffsetDateTime - .ofInstant(roundTotal.closedRoundEffectiveAt.toInstant, ZoneOffset.UTC), - appRewards = Codec.encode(roundTotal.appRewards), - validatorRewards = Codec.encode(roundTotal.validatorRewards), - changeToInitialAmountAsOfRoundZero = - Codec.encode(roundTotal.changeToInitialAmountAsOfRoundZero), - changeToHoldingFeesRate = Codec.encode(roundTotal.changeToHoldingFeesRate), - cumulativeAppRewards = Codec.encode(roundTotal.cumulativeAppRewards), - cumulativeValidatorRewards = Codec.encode(roundTotal.cumulativeValidatorRewards), - cumulativeChangeToInitialAmountAsOfRoundZero = - Codec.encode(roundTotal.cumulativeChangeToInitialAmountAsOfRoundZero), - cumulativeChangeToHoldingFeesRate = - Codec.encode(roundTotal.cumulativeChangeToHoldingFeesRate), - totalAmuletBalance = Codec.encode(roundTotal.totalAmuletBalance), - ) - } + entries = roundTotals.map(encodeRoundTotals) } yield v0.ScanResource.ListRoundTotalsResponse.OK( definitions.ListRoundTotalsResponse(entries.toVector) ) @@ -1888,27 +1871,7 @@ class HttpScanHandler( ensureValidRange(request.startRound, request.endRound, 50) { for { roundPartyTotals <- store.getRoundPartyTotals(request.startRound, request.endRound) - entries = roundPartyTotals.map { roundPartyTotal => - definitions.RoundPartyTotals( - closedRound = roundPartyTotal.closedRound, - party = roundPartyTotal.party, - appRewards = Codec.encode(roundPartyTotal.appRewards), - validatorRewards = Codec.encode(roundPartyTotal.validatorRewards), - trafficPurchased = roundPartyTotal.trafficPurchased, - trafficPurchasedCcSpent = Codec.encode(roundPartyTotal.trafficPurchasedCcSpent), - trafficNumPurchases = roundPartyTotal.trafficNumPurchases, - cumulativeAppRewards = Codec.encode(roundPartyTotal.cumulativeAppRewards), - cumulativeValidatorRewards = Codec.encode(roundPartyTotal.cumulativeValidatorRewards), - cumulativeChangeToInitialAmountAsOfRoundZero = - Codec.encode(roundPartyTotal.cumulativeChangeToInitialAmountAsOfRoundZero), - cumulativeChangeToHoldingFeesRate = - Codec.encode(roundPartyTotal.cumulativeChangeToHoldingFeesRate), - cumulativeTrafficPurchased = roundPartyTotal.cumulativeTrafficPurchased, - cumulativeTrafficPurchasedCcSpent = - Codec.encode(roundPartyTotal.cumulativeTrafficPurchasedCcSpent), - cumulativeTrafficNumPurchases = roundPartyTotal.cumulativeTrafficNumPurchases, - ) - } + entries = roundPartyTotals.map(encodeRoundPartyTotals) } yield v0.ScanResource.ListRoundPartyTotalsResponse.OK( definitions.ListRoundPartyTotalsResponse(entries.toVector) ) @@ -2303,4 +2266,46 @@ object HttpScanHandler { // We expect a handful at most but want to somewhat guard against attacks // so we just hardcode a limit of 100. private val MAX_TRANSFER_COMMAND_CONTRACTS: Int = 100 + + def encodeRoundTotals(roundTotal: RoundTotals): definitions.RoundTotals = { + definitions.RoundTotals( + closedRound = roundTotal.closedRound, + closedRoundEffectiveAt = java.time.OffsetDateTime + .ofInstant(roundTotal.closedRoundEffectiveAt.toInstant, ZoneOffset.UTC), + appRewards = Codec.encode(roundTotal.appRewards), + validatorRewards = Codec.encode(roundTotal.validatorRewards), + changeToInitialAmountAsOfRoundZero = + Codec.encode(roundTotal.changeToInitialAmountAsOfRoundZero), + changeToHoldingFeesRate = Codec.encode(roundTotal.changeToHoldingFeesRate), + cumulativeAppRewards = Codec.encode(roundTotal.cumulativeAppRewards), + cumulativeValidatorRewards = Codec.encode(roundTotal.cumulativeValidatorRewards), + cumulativeChangeToInitialAmountAsOfRoundZero = + Codec.encode(roundTotal.cumulativeChangeToInitialAmountAsOfRoundZero), + cumulativeChangeToHoldingFeesRate = + Codec.encode(roundTotal.cumulativeChangeToHoldingFeesRate), + totalAmuletBalance = Codec.encode(roundTotal.totalAmuletBalance), + ) + } + + def encodeRoundPartyTotals(roundPartyTotal: RoundPartyTotals): definitions.RoundPartyTotals = { + definitions.RoundPartyTotals( + closedRound = roundPartyTotal.closedRound, + party = roundPartyTotal.party, + appRewards = Codec.encode(roundPartyTotal.appRewards), + validatorRewards = Codec.encode(roundPartyTotal.validatorRewards), + trafficPurchased = roundPartyTotal.trafficPurchased, + trafficPurchasedCcSpent = Codec.encode(roundPartyTotal.trafficPurchasedCcSpent), + trafficNumPurchases = roundPartyTotal.trafficNumPurchases, + cumulativeAppRewards = Codec.encode(roundPartyTotal.cumulativeAppRewards), + cumulativeValidatorRewards = Codec.encode(roundPartyTotal.cumulativeValidatorRewards), + cumulativeChangeToInitialAmountAsOfRoundZero = + Codec.encode(roundPartyTotal.cumulativeChangeToInitialAmountAsOfRoundZero), + cumulativeChangeToHoldingFeesRate = + Codec.encode(roundPartyTotal.cumulativeChangeToHoldingFeesRate), + cumulativeTrafficPurchased = roundPartyTotal.cumulativeTrafficPurchased, + cumulativeTrafficPurchasedCcSpent = + Codec.encode(roundPartyTotal.cumulativeTrafficPurchasedCcSpent), + cumulativeTrafficNumPurchases = roundPartyTotal.cumulativeTrafficNumPurchases, + ) + } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala index 00c9d7582e..325fa4f6d7 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala @@ -29,16 +29,14 @@ import org.lfdecentralizedtrust.splice.environment.{ RetryProvider, SpliceLedgerClient, } -import org.lfdecentralizedtrust.splice.http.v0.definitions.{ - ErrorResponse, - RoundPartyTotals as HttpRoundPartyTotals, - RoundTotals as HttpRoundTotals, -} +import org.lfdecentralizedtrust.splice.http.v0.definitions.ErrorResponse + import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection.Bft import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.{ DomainScans, DsoScan, } +import org.lfdecentralizedtrust.splice.scan.admin.http.HttpScanHandler import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationInfo import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState @@ -944,41 +942,6 @@ class BftScanConnectionTest cumulativeChangeToHoldingFeesRate = randomValue, totalAmuletBalance = randomValue, ) - def encodeRoundTotals(rt: RoundTotals) = { - HttpRoundTotals( - closedRound = rt.closedRound, - closedRoundEffectiveAt = Codec.OffsetDateTime.instance.encode(rt.closedRoundEffectiveAt), - appRewards = Codec.encode(rt.appRewards), - validatorRewards = Codec.encode(rt.validatorRewards), - changeToInitialAmountAsOfRoundZero = Codec.encode(rt.changeToInitialAmountAsOfRoundZero), - changeToHoldingFeesRate = Codec.encode(rt.changeToHoldingFeesRate), - cumulativeAppRewards = Codec.encode(rt.cumulativeAppRewards), - cumulativeValidatorRewards = Codec.encode(rt.cumulativeValidatorRewards), - cumulativeChangeToInitialAmountAsOfRoundZero = - Codec.encode(rt.cumulativeChangeToInitialAmountAsOfRoundZero), - cumulativeChangeToHoldingFeesRate = Codec.encode(rt.cumulativeChangeToHoldingFeesRate), - totalAmuletBalance = Codec.encode(rt.totalAmuletBalance), - ) - } - def encodeRoundPartyTotals(rpt: RoundPartyTotals) = { - HttpRoundPartyTotals( - closedRound = rpt.closedRound, - party = rpt.party, - appRewards = Codec.encode(rpt.appRewards), - validatorRewards = Codec.encode(rpt.validatorRewards), - trafficPurchased = rpt.trafficPurchased, - trafficPurchasedCcSpent = Codec.encode(rpt.trafficPurchasedCcSpent), - trafficNumPurchases = rpt.trafficNumPurchases, - cumulativeAppRewards = Codec.encode(rpt.cumulativeAppRewards), - cumulativeValidatorRewards = Codec.encode(rpt.cumulativeValidatorRewards), - cumulativeChangeToInitialAmountAsOfRoundZero = - Codec.encode(rpt.cumulativeChangeToInitialAmountAsOfRoundZero), - cumulativeChangeToHoldingFeesRate = Codec.encode(rpt.cumulativeChangeToHoldingFeesRate), - cumulativeTrafficPurchased = rpt.cumulativeTrafficPurchased, - cumulativeTrafficPurchasedCcSpent = Codec.encode(rpt.cumulativeTrafficPurchasedCcSpent), - cumulativeTrafficNumPurchases = rpt.cumulativeTrafficNumPurchases, - ) - } def mkRoundPartyTotals() = RoundPartyTotals( closedRound = round, party = "party-id", @@ -996,10 +959,12 @@ class BftScanConnectionTest cumulativeTrafficNumPurchases = 70L, ) def mkRoundAggregateUsingDecoder() = RoundAggregate( - ScanRoundAggregatesDecoder.decodeRoundTotal(encodeRoundTotals(mkRoundTotals())).value, + ScanRoundAggregatesDecoder + .decodeRoundTotal(HttpScanHandler.encodeRoundTotals(mkRoundTotals())) + .value, Vector( ScanRoundAggregatesDecoder - .decodeRoundPartyTotals(encodeRoundPartyTotals(mkRoundPartyTotals())) + .decodeRoundPartyTotals(HttpScanHandler.encodeRoundPartyTotals(mkRoundPartyTotals())) .value ), ) From be5e147888f469336cf216ac18c386b590f2232e Mon Sep 17 00:00:00 2001 From: Raymond Roestenburg <98821776+ray-roestenburg-da@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:24:30 +0100 Subject: [PATCH 4/4] [ci] Update docs/src/release_notes.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Oriol Muñoz Signed-off-by: Raymond Roestenburg <98821776+ray-roestenburg-da@users.noreply.github.com> Signed-off-by: Raymond Roestenburg --- .../splice/scan/admin/api/client/BftScanConnectionTest.scala | 3 +-- docs/src/release_notes.rst | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala index 325fa4f6d7..df116c8b43 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnectionTest.scala @@ -42,7 +42,6 @@ import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationI import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState import org.lfdecentralizedtrust.splice.store.UpdateHistory.UpdateHistoryResponse import org.lfdecentralizedtrust.splice.util.{ - Codec, Contract, ContractWithState, DomainRecordTimeRange, @@ -926,7 +925,7 @@ class BftScanConnectionTest result shouldBe Some(roundAggregate) } - "get BFT round aggregates from scans that report having the round aggregate ignoring balance fields" in { + "get BFT round aggregates from scans, ignoring balance fields" in { val round = 0L def randomValue = BigDecimal(Random.nextInt(50) + 1) def mkRoundTotals() = RoundTotals( diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 5fc60808fe..270857814d 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -27,8 +27,9 @@ Upcoming - Scan - The round-based aggregates for balance values (changes to holding fees and initial amounts since round zero) - have diverged between scans because of how amulets expire in the ``scan_txlog_store``. - 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 + have diverged between scans because of the way amulet expiration is counted in rounds. + The balance values recorded in the round aggregates are effectively not depended upon anymore by scan APIs, + and are now set to zero to avoid consensus problems when an SV reads aggregates from the rest of the network when first joining. 0.5.1