From 24ee0e75e59a9693b8163d334db360420f99ca3b Mon Sep 17 00:00:00 2001 From: Ivan Mashonskii Date: Wed, 7 Jun 2023 19:06:24 +0300 Subject: [PATCH] NODE-2595 Change block reward distribution (#3853) --- docker/private/waves.custom.conf | 4 + .../api/grpc/BlocksApiGrpcImpl.scala | 10 +- .../events/BlockchainUpdates.scala | 10 +- .../scala/com/wavesplatform/events/Repo.scala | 10 +- .../com/wavesplatform/events/events.scala | 40 +- .../events/protobuf/serde/package.scala | 7 +- .../events/repo/LiquidState.scala | 1 + .../api/grpc/test/BlocksApiGrpcSpec.scala | 185 ++++- .../events/BlockchainUpdatesSpec.scala | 188 +++++- node-it/src/test/resources/template.conf | 1 + .../main/resources/swagger-ui/openapi.yaml | 33 + .../scala/com/wavesplatform/Application.scala | 10 +- .../com/wavesplatform/api/BlockMeta.scala | 37 +- .../wavesplatform/api/http/CustomJson.scala | 18 +- .../api/http/RewardApiRoute.scala | 14 +- .../com/wavesplatform/database/Keys.scala | 16 +- .../database/LevelDBWriter.scala | 14 +- .../com/wavesplatform/database/package.scala | 1 + .../events/BlockchainUpdateTriggers.scala | 22 +- .../features/BlockchainFeature.scala | 10 +- .../settings/BlockchainSettings.scala | 32 +- .../state/BlockRewardCalculator.scala | 82 +++ .../com/wavesplatform/state/Blockchain.scala | 2 +- .../state/BlockchainUpdaterImpl.scala | 10 +- .../com/wavesplatform/state/NgState.scala | 33 +- .../state/diffs/BlockDiffer.scala | 38 +- .../transaction/smart/WavesEnvironment.scala | 31 +- .../history/BlockRewardSpec.scala | 632 +++++++++++++++++- .../com/wavesplatform/history/Domain.scala | 10 +- .../http/BlocksApiRouteSpec.scala | 145 +++- .../http/RewardApiRouteSpec.scala | 92 ++- .../BlockchainSettingsSpecification.scala | 128 ++-- .../smart/predef/ContextFunctionsTest.scala | 122 +++- project/Dependencies.scala | 2 +- 34 files changed, 1724 insertions(+), 266 deletions(-) create mode 100644 node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala diff --git a/docker/private/waves.custom.conf b/docker/private/waves.custom.conf index 5251a5c1d7d..aa1fb090f46 100755 --- a/docker/private/waves.custom.conf +++ b/docker/private/waves.custom.conf @@ -39,6 +39,9 @@ waves { 15 = 0 16 = 0 17 = 0 + 18 = 0 + 19 = 0 + 20 = 0 } double-features-periods-after-height = 1000000000 max-transaction-time-back-offset = 120m @@ -59,6 +62,7 @@ waves { } rewards { term = 6 + term-after-capped-reward-feature = 3 initial = 600000000 min-increment = 50000000 voting-interval = 3 diff --git a/grpc-server/src/main/scala/com/wavesplatform/api/grpc/BlocksApiGrpcImpl.scala b/grpc-server/src/main/scala/com/wavesplatform/api/grpc/BlocksApiGrpcImpl.scala index bbf6cc61f5f..65cf64b14ed 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/api/grpc/BlocksApiGrpcImpl.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/api/grpc/BlocksApiGrpcImpl.scala @@ -76,10 +76,16 @@ object BlocksApiGrpcImpl { BlockWithHeight( Some(PBBlock(Some(blockMeta.header.toPBHeader), blockMeta.signature.toByteString, txs.map(_._2.toPB))), blockMeta.height, - blockMeta.vrf.fold(ByteString.EMPTY)(_.toByteString) + blockMeta.vrf.fold(ByteString.EMPTY)(_.toByteString), + blockMeta.rewardShares.map { case (addr, reward) => RewardShare(ByteString.copyFrom(addr.bytes), reward) } ) } private def toBlockWithHeight(m: BlockMeta) = - BlockWithHeight(Some(PBBlock(Some(m.header.toPBHeader), m.signature.toByteString)), m.height, m.vrf.fold(ByteString.EMPTY)(_.toByteString)) + BlockWithHeight( + Some(PBBlock(Some(m.header.toPBHeader), m.signature.toByteString)), + m.height, + m.vrf.fold(ByteString.EMPTY)(_.toByteString), + m.rewardShares.map { case (addr, reward) => RewardShare(ByteString.copyFrom(addr.bytes), reward) } + ) } diff --git a/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala b/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala index e3b22500ab9..263ebc66bb9 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/BlockchainUpdates.scala @@ -95,18 +95,18 @@ class BlockchainUpdates(private val context: Context) extends Extension with Sco override def onProcessBlock( block: Block, diff: BlockDiffer.DetailedDiff, - minerReward: Option[Long], + reward: Option[Long], hitSource: ByteStr, - blockchainBeforeWithMinerReward: Blockchain - ): Unit = repo.onProcessBlock(block, diff, minerReward, hitSource, blockchainBeforeWithMinerReward) + blockchainBeforeWithReward: Blockchain + ): Unit = repo.onProcessBlock(block, diff, reward, hitSource, blockchainBeforeWithReward) override def onProcessMicroBlock( microBlock: MicroBlock, diff: BlockDiffer.DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, totalBlockId: ByteStr, totalTransactionsRoot: ByteStr - ): Unit = repo.onProcessMicroBlock(microBlock, diff, blockchainBeforeWithMinerReward, totalBlockId, totalTransactionsRoot) + ): Unit = repo.onProcessMicroBlock(microBlock, diff, blockchainBeforeWithReward, totalBlockId, totalTransactionsRoot) override def onRollback(blockchainBefore: Blockchain, toBlockId: ByteStr, toHeight: Int): Unit = repo.onRollback(blockchainBefore, toBlockId, toHeight) diff --git a/grpc-server/src/main/scala/com/wavesplatform/events/Repo.scala b/grpc-server/src/main/scala/com/wavesplatform/events/Repo.scala index 9151d73ee15..ee6781452a1 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/Repo.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/Repo.scala @@ -61,9 +61,9 @@ class Repo(db: DB, blocksApi: CommonBlocksApi)(implicit s: Scheduler) extends Bl override def onProcessBlock( block: Block, diff: BlockDiffer.DetailedDiff, - minerReward: Option[Long], + reward: Option[Long], hitSource: ByteStr, - blockchainBeforeWithMinerReward: Blockchain + blockchainBeforeWithReward: Blockchain ): Unit = monitor.synchronized { require( liquidState.forall(_.totalBlockId == block.header.reference), @@ -74,7 +74,7 @@ class Repo(db: DB, blocksApi: CommonBlocksApi)(implicit s: Scheduler) extends Bl db.put(keyForHeight(ls.keyBlock.height), ls.solidify().protobuf.update(_.append.block.optionalBlock := None).toByteArray) ) - val ba = BlockAppended.from(block, diff, blockchainBeforeWithMinerReward, minerReward, hitSource) + val ba = BlockAppended.from(block, diff, blockchainBeforeWithReward, reward, hitSource) liquidState = Some(LiquidState(ba, Seq.empty)) handlers.forEach(_.handleUpdate(ba)) } @@ -82,7 +82,7 @@ class Repo(db: DB, blocksApi: CommonBlocksApi)(implicit s: Scheduler) extends Bl override def onProcessMicroBlock( microBlock: MicroBlock, diff: BlockDiffer.DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, totalBlockId: ByteStr, totalTransactionsRoot: ByteStr ): Unit = monitor.synchronized { @@ -92,7 +92,7 @@ class Repo(db: DB, blocksApi: CommonBlocksApi)(implicit s: Scheduler) extends Bl s"Microblock reference ${microBlock.reference} does not match last block id ${liquidState.get.totalBlockId}" ) - val mba = MicroBlockAppended.from(microBlock, diff, blockchainBeforeWithMinerReward, totalBlockId, totalTransactionsRoot) + val mba = MicroBlockAppended.from(microBlock, diff, blockchainBeforeWithReward, totalBlockId, totalTransactionsRoot) liquidState = Some(ls.copy(microBlocks = ls.microBlocks :+ mba)) handlers.forEach(_.handleUpdate(mba)) diff --git a/grpc-server/src/main/scala/com/wavesplatform/events/events.scala b/grpc-server/src/main/scala/com/wavesplatform/events/events.scala index 48d9abed37a..c6322b28aca 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/events.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/events.scala @@ -529,32 +529,33 @@ object StateUpdate { .flatMap(id => blockchain.assetDescription(IssuedAsset(id)).map(ad => AssetInfo(id, ad.decimals, ad.name.toStringUtf8))) def container( - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, diff: DetailedDiff, minerAddress: Address ): (StateUpdate, Seq[StateUpdate], Seq[TransactionMetadata], Seq[AssetInfo]) = { val DetailedDiff(parentDiff, txsDiffs) = diff - val parentStateUpdate = atomic(blockchainBeforeWithMinerReward, parentDiff) + val parentStateUpdate = atomic(blockchainBeforeWithReward, parentDiff) - // miner reward is already in the blockchainBeforeWithMinerReward + // miner reward is already in the blockchainBeforeWithReward // if miner balance has been changed in parentDiff, it is already included in balance updates // if it has not, it needs to be manually requested from the blockchain and added to balance updates + // TODO: remove val parentStateUpdateWithMinerReward = parentStateUpdate.balances.find(_.address == minerAddress) match { case Some(_) => parentStateUpdate case None => - val minerBalance = blockchainBeforeWithMinerReward.balance(minerAddress, Waves) - val reward = blockchainBeforeWithMinerReward.blockReward(blockchainBeforeWithMinerReward.height).getOrElse(0L) + val minerBalance = blockchainBeforeWithReward.balance(minerAddress, Waves) + val reward = blockchainBeforeWithReward.blockReward(blockchainBeforeWithReward.height).getOrElse(0L) parentStateUpdate.copy(balances = parentStateUpdate.balances :+ BalanceUpdate(minerAddress, Waves, minerBalance - reward, minerBalance)) } val (txsStateUpdates, totalDiff) = txsDiffs.reverse .foldLeft((Seq.empty[StateUpdate], parentDiff)) { case ((updates, accDiff), txDiff) => ( - updates :+ atomic(CompositeBlockchain(blockchainBeforeWithMinerReward, accDiff), txDiff), + updates :+ atomic(CompositeBlockchain(blockchainBeforeWithReward, accDiff), txDiff), accDiff.combineF(txDiff).explicitGet() ) } - val blockchainAfter = CompositeBlockchain(blockchainBeforeWithMinerReward, totalDiff) + val blockchainAfter = CompositeBlockchain(blockchainBeforeWithReward, totalDiff) val metadata = transactionsMetadata(blockchainAfter, totalDiff) val refAssets = referencedAssets(blockchainAfter, txsStateUpdates) (parentStateUpdateWithMinerReward, txsStateUpdates, metadata, refAssets) @@ -593,6 +594,7 @@ final case class BlockAppended( updatedWavesAmount: Long, vrf: Option[ByteStr], activatedFeatures: Seq[Int], + rewardShares: Seq[(Address, Long)], blockStateUpdate: StateUpdate, transactionStateUpdates: Seq[StateUpdate], transactionMetadata: Seq[TransactionMetadata], @@ -608,22 +610,25 @@ object BlockAppended { def from( block: Block, diff: DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, - minerReward: Option[Long], + blockchainBeforeWithReward: Blockchain, + reward: Option[Long], hitSource: ByteStr ): BlockAppended = { - val height = blockchainBeforeWithMinerReward.height + val height = blockchainBeforeWithReward.height val (blockStateUpdate, txsStateUpdates, txsMetadata, refAssets) = - StateUpdate.container(blockchainBeforeWithMinerReward, diff, block.sender.toAddress) + StateUpdate.container(blockchainBeforeWithReward, diff, block.sender.toAddress) // updatedWavesAmount can change as a result of either genesis transactions or miner rewards - val wavesAmount = blockchainBeforeWithMinerReward.wavesAmount(height).toLong - val updatedWavesAmount = wavesAmount + minerReward.filter(_ => height > 0).getOrElse(0L) + val wavesAmount = blockchainBeforeWithReward.wavesAmount(height).toLong + val updatedWavesAmount = wavesAmount + reward.filter(_ => height > 0).getOrElse(0L) - val activatedFeatures = blockchainBeforeWithMinerReward.activatedFeatures.collect { + val activatedFeatures = blockchainBeforeWithReward.activatedFeatures.collect { case (id, activationHeight) if activationHeight == height + 1 => id.toInt }.toSeq + val rewardShares = + BlockRewardCalculator.getSortedBlockRewardShares(height + 1, reward.getOrElse(0L), block.header.generator.toAddress, blockchainBeforeWithReward) + BlockAppended( block.id(), height + 1, @@ -631,6 +636,7 @@ object BlockAppended { updatedWavesAmount, if (block.header.version >= Block.ProtoBlockVersion) Some(hitSource) else None, activatedFeatures, + rewardShares, blockStateUpdate, txsStateUpdates, txsMetadata, @@ -662,16 +668,16 @@ object MicroBlockAppended { def from( microBlock: MicroBlock, diff: DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, totalBlockId: ByteStr, totalTransactionsRoot: ByteStr ): MicroBlockAppended = { val (microBlockStateUpdate, txsStateUpdates, txsMetadata, refAssets) = - StateUpdate.container(blockchainBeforeWithMinerReward, diff, microBlock.sender.toAddress) + StateUpdate.container(blockchainBeforeWithReward, diff, microBlock.sender.toAddress) MicroBlockAppended( totalBlockId, - blockchainBeforeWithMinerReward.height, + blockchainBeforeWithReward.height, microBlock, microBlockStateUpdate, txsStateUpdates, diff --git a/grpc-server/src/main/scala/com/wavesplatform/events/protobuf/serde/package.scala b/grpc-server/src/main/scala/com/wavesplatform/events/protobuf/serde/package.scala index 614a043f122..8ee9907bbc6 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/protobuf/serde/package.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/protobuf/serde/package.scala @@ -2,6 +2,8 @@ package com.wavesplatform.events.protobuf import cats.Monoid import com.google.protobuf.ByteString +import com.wavesplatform.account.Address +import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.events.StateUpdate.AssetInfo import com.wavesplatform.events.protobuf.BlockchainUpdated.Append.Body import com.wavesplatform.events.protobuf.BlockchainUpdated.{Append, Rollback, Update} @@ -27,6 +29,7 @@ package object serde { updatedWavesAmount, vrf, activatedFeatures, + rewardShares, blockStateUpdate, transactionStateUpdates, transactionsMetadata, @@ -49,7 +52,8 @@ package object serde { block = Some(PBBlocks.protobuf(block)), updatedWavesAmount = updatedWavesAmount, activatedFeatures = activatedFeatures, - vrf = vrf.fold(ByteString.EMPTY)(_.toByteString) + vrf = vrf.fold(ByteString.EMPTY)(_.toByteString), + rewardShares = rewardShares.map { case (addr, reward) => RewardShare(ByteString.copyFrom(addr.bytes), reward) } ) ) ) @@ -138,6 +142,7 @@ package object serde { updatedWavesAmount = body.updatedWavesAmount, vrf = Option.unless(body.vrf.isEmpty)(body.vrf.toByteStr), activatedFeatures = body.activatedFeatures, + rewardShares = body.rewardShares.map { rs => (Address.fromBytes(rs.address.toByteArray).explicitGet(), rs.reward) }, blockStateUpdate = append.stateUpdate.fold(Monoid[ve.StateUpdate].empty)(_.vanilla.get), transactionStateUpdates = append.transactionStateUpdates.map(_.vanilla.get), transactionMetadata = append.transactionsMetadata, diff --git a/grpc-server/src/main/scala/com/wavesplatform/events/repo/LiquidState.scala b/grpc-server/src/main/scala/com/wavesplatform/events/repo/LiquidState.scala index 6730cbbb4f9..56044d2559e 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/repo/LiquidState.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/repo/LiquidState.scala @@ -37,6 +37,7 @@ object LiquidState { updatedWavesAmount = keyBlock.updatedWavesAmount, vrf = keyBlock.vrf, activatedFeatures = keyBlock.activatedFeatures, + rewardShares = keyBlock.rewardShares, blockStateUpdate = blockStateUpdate, transactionStateUpdates = transactionStateUpdates, transactionMetadata = transactionMetadata, diff --git a/grpc-server/src/test/scala/com/wavesplatform/api/grpc/test/BlocksApiGrpcSpec.scala b/grpc-server/src/test/scala/com/wavesplatform/api/grpc/test/BlocksApiGrpcSpec.scala index a42da31e545..f5c47e3d1fc 100644 --- a/grpc-server/src/test/scala/com/wavesplatform/api/grpc/test/BlocksApiGrpcSpec.scala +++ b/grpc-server/src/test/scala/com/wavesplatform/api/grpc/test/BlocksApiGrpcSpec.scala @@ -1,27 +1,32 @@ package com.wavesplatform.api.grpc.test import com.google.protobuf.ByteString -import com.wavesplatform.account.KeyPair +import com.wavesplatform.account.{Address, KeyPair} import com.wavesplatform.api.grpc.{BlockRangeRequest, BlockRequest, BlockWithHeight, BlocksApiGrpcImpl} import com.wavesplatform.block.Block +import com.wavesplatform.common.state.ByteStr import com.wavesplatform.db.WithDomain import com.wavesplatform.db.WithState.AddrWithBalance +import com.wavesplatform.features.BlockchainFeatures import com.wavesplatform.history.Domain import com.wavesplatform.protobuf.* import com.wavesplatform.protobuf.block.PBBlocks -import com.wavesplatform.test.FreeSpec +import com.wavesplatform.state.BlockRewardCalculator +import com.wavesplatform.test.DomainPresets.* +import com.wavesplatform.test.* import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.utils.DiffMatchers import monix.execution.Scheduler.Implicits.global -import org.scalatest.BeforeAndAfterAll +import org.scalatest.{Assertion, BeforeAndAfterAll} import scala.concurrent.Await -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{DurationInt, FiniteDuration} class BlocksApiGrpcSpec extends FreeSpec with BeforeAndAfterAll with DiffMatchers with WithDomain with GrpcApiHelpers { - val sender: KeyPair = TxHelpers.signer(1) - val recipient: KeyPair = TxHelpers.signer(2) + val sender: KeyPair = TxHelpers.signer(1) + val recipient: KeyPair = TxHelpers.signer(2) + val timeout: FiniteDuration = 2.minutes "GetBlock should work" in withDomain(DomainPresets.RideV6, AddrWithBalance.enoughBalances(sender)) { d => val grpcApi = getGrpcApi(d) @@ -31,18 +36,23 @@ class BlocksApiGrpcSpec extends FreeSpec with BeforeAndAfterAll with DiffMatcher d.liquidAndSolidAssert { () => val vrf = getBlockVrfPB(d, block) vrf.isEmpty shouldBe false - val expectedResult = BlockWithHeight.of(Some(PBBlocks.protobuf(block)), 2, vrf) + val expectedResult = BlockWithHeight.of( + Some(PBBlocks.protobuf(block)), + 2, + vrf, + Seq(RewardShare(ByteString.copyFrom(block.sender.toAddress.bytes), d.blockchain.settings.rewardsSettings.initial)) + ) val resultById = Await.result( grpcApi.getBlock(BlockRequest.of(BlockRequest.Request.BlockId(block.id().toByteString), includeTransactions = true)), - Duration.Inf + timeout ) resultById shouldBe expectedResult val resultByHeight = Await.result( grpcApi.getBlock(BlockRequest.of(BlockRequest.Request.Height(2), includeTransactions = true)), - Duration.Inf + timeout ) resultByHeight shouldBe expectedResult @@ -65,14 +75,169 @@ class BlocksApiGrpcSpec extends FreeSpec with BeforeAndAfterAll with DiffMatcher result.runSyncUnsafe() shouldBe blocks.zipWithIndex.map { case (block, idx) => val vrf = getBlockVrfPB(d, block) vrf.isEmpty shouldBe false - BlockWithHeight.of(Some(PBBlocks.protobuf(block)), idx + 2, vrf) + BlockWithHeight.of( + Some(PBBlocks.protobuf(block)), + idx + 2, + vrf, + Seq(RewardShare(ByteString.copyFrom(block.sender.toAddress.bytes), d.blockchain.settings.rewardsSettings.initial)) + ) } } } + "NODE-844. GetBlock should return correct rewardShares" in { + blockRewardSharesTestCase { case (daoAddress, xtnBuybackAddress, d, grpcApi) => + val miner = d.appendBlock().sender.toAddress + val blockBeforeBlockRewardDistr = d.appendBlock() + val heightToBlock = (3 to 5).map { h => + h -> d.appendBlock().id() + }.toMap + d.appendBlock() + + // reward distribution features not activated + checkBlockRewards( + blockBeforeBlockRewardDistr.id(), + 2, + Seq(RewardShare(ByteString.copyFrom(miner.bytes), d.blockchain.settings.rewardsSettings.initial)) + )(grpcApi) + + // BlockRewardDistribution activated + val configAddrReward3 = d.blockchain.settings.rewardsSettings.initial / 3 + val minerReward3 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward3 + + checkBlockRewards( + heightToBlock(3), + 3, + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward3), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward3), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward3) + ).sortBy(_.address.toByteStr) + )(grpcApi) + + // CappedReward activated + val configAddrReward4 = BlockRewardCalculator.MaxAddressReward + val minerReward4 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward4 + + checkBlockRewards( + heightToBlock(4), + 4, + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward4), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward4), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward4) + ).sortBy(_.address.toByteStr) + )(grpcApi) + + // CeaseXTNBuyback activated with expired XTN buyback reward period + val configAddrReward5 = BlockRewardCalculator.MaxAddressReward + val minerReward5 = d.blockchain.settings.rewardsSettings.initial - configAddrReward5 + + checkBlockRewards( + heightToBlock(5), + 5, + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward5), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward5) + ).sortBy(_.address.toByteStr) + )(grpcApi) + } + } + + "NODE-845. GetBlockRange should return correct rewardShares" in { + blockRewardSharesTestCase { case (daoAddress, xtnBuybackAddress, d, grpcApi) => + val miner = d.appendBlock().sender.toAddress + d.appendBlock() + + (3 to 5).foreach(_ => d.appendBlock()) + d.appendBlock() + + val (observer, result) = createObserver[BlockWithHeight] + grpcApi.getBlockRange( + BlockRangeRequest.of(2, 5, BlockRangeRequest.Filter.Empty, includeTransactions = true), + observer + ) + val blocks = result.runSyncUnsafe() + + // reward distribution features not activated + blocks.head.rewardShares shouldBe Seq(RewardShare(ByteString.copyFrom(miner.bytes), d.blockchain.settings.rewardsSettings.initial)) + + // BlockRewardDistribution activated + val configAddrReward3 = d.blockchain.settings.rewardsSettings.initial / 3 + val minerReward3 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward3 + + blocks(1).rewardShares shouldBe Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward3), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward3), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward3) + ).sortBy(_.address.toByteStr) + + // CappedReward activated + val configAddrReward4 = BlockRewardCalculator.MaxAddressReward + val minerReward4 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward4 + + blocks(2).rewardShares shouldBe Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward4), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward4), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward4) + ).sortBy(_.address.toByteStr) + + // CeaseXTNBuyback activated with expired XTN buyback reward period + val configAddrReward5 = BlockRewardCalculator.MaxAddressReward + val minerReward5 = d.blockchain.settings.rewardsSettings.initial - configAddrReward5 + + blocks(3).rewardShares shouldBe Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward5), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward5) + ).sortBy(_.address.toByteStr) + } + } + private def getBlockVrfPB(d: Domain, block: Block): ByteString = d.blocksApi.block(block.id()).flatMap(_._1.vrf).map(_.toByteString).getOrElse(ByteString.EMPTY) private def getGrpcApi(d: Domain) = new BlocksApiGrpcImpl(d.blocksApi) + + private def checkBlockRewards(blockId: ByteStr, height: Int, expected: Seq[RewardShare])(api: BlocksApiGrpcImpl): Assertion = { + Await + .result( + api.getBlock(BlockRequest.of(BlockRequest.Request.BlockId(blockId.toByteString), includeTransactions = false)), + timeout + ) + .rewardShares shouldBe expected + + Await + .result( + api.getBlock(BlockRequest.of(BlockRequest.Request.Height(height), includeTransactions = false)), + timeout + ) + .rewardShares shouldBe expected + } + + private def blockRewardSharesTestCase(checks: (Address, Address, Domain, BlocksApiGrpcImpl) => Unit): Unit = { + val daoAddress = TxHelpers.address(3) + val xtnBuybackAddress = TxHelpers.address(4) + + val settings = DomainPresets.ConsensusImprovements + val settingsWithFeatures = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString), xtnBuybackRewardPeriod = 1), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 3, + BlockchainFeatures.CappedReward -> 4, + BlockchainFeatures.CeaseXtnBuyback -> 5 + ) + + withDomain(settingsWithFeatures) { d => + val grpcApi = getGrpcApi(d) + + checks(daoAddress, xtnBuybackAddress, d, grpcApi) + } + } } diff --git a/grpc-server/src/test/scala/com/wavesplatform/events/BlockchainUpdatesSpec.scala b/grpc-server/src/test/scala/com/wavesplatform/events/BlockchainUpdatesSpec.scala index a92cb5c25fb..0c11c55ec1e 100644 --- a/grpc-server/src/test/scala/com/wavesplatform/events/BlockchainUpdatesSpec.scala +++ b/grpc-server/src/test/scala/com/wavesplatform/events/BlockchainUpdatesSpec.scala @@ -18,7 +18,7 @@ import com.wavesplatform.events.StateUpdate.{ LeasingBalanceUpdate, ScriptUpdate } -import com.wavesplatform.events.api.grpc.protobuf.{GetBlockUpdatesRangeRequest, SubscribeRequest} +import com.wavesplatform.events.api.grpc.protobuf.{GetBlockUpdateRequest, GetBlockUpdatesRangeRequest, SubscribeRequest} import com.wavesplatform.events.protobuf.BlockchainUpdated.Rollback.RollbackType import com.wavesplatform.events.protobuf.BlockchainUpdated.Update import com.wavesplatform.events.protobuf.serde.* @@ -36,7 +36,7 @@ import com.wavesplatform.protobuf.transaction.DataTransactionData.DataEntry import com.wavesplatform.protobuf.transaction.InvokeScriptResult import com.wavesplatform.protobuf.transaction.InvokeScriptResult.{Call, Invocation, Payment} import com.wavesplatform.settings.{Constants, WavesSettings} -import com.wavesplatform.state.{AssetDescription, EmptyDataEntry, Height, LeaseBalance, StringDataEntry} +import com.wavesplatform.state.{AssetDescription, BlockRewardCalculator, EmptyDataEntry, Height, LeaseBalance, StringDataEntry} import com.wavesplatform.test.* import com.wavesplatform.test.DomainPresets.* import com.wavesplatform.transaction.Asset.Waves @@ -852,6 +852,147 @@ class BlockchainUpdatesSpec extends FreeSpec with WithBUDomain with ScalaFutures ) } } + + "should return correct rewardShares for GetBlockUpdate (NODE-838)" in { + blockUpdatesRewardSharesTestCase { case (miner, daoAddress, xtnBuybackAddress, d, r) => + // reward distribution features not activated + checkBlockUpdateRewards( + 2, + Seq(RewardShare(ByteString.copyFrom(miner.bytes), d.blockchain.settings.rewardsSettings.initial)) + )(r) + + // BlockRewardDistribution activated + val configAddrReward3 = d.blockchain.settings.rewardsSettings.initial / 3 + val minerReward3 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward3 + + checkBlockUpdateRewards( + 3, + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward3), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward3), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward3) + ).sortBy(_.address.toByteStr) + )(r) + + // CappedReward activated + val configAddrReward4 = BlockRewardCalculator.MaxAddressReward + val minerReward4 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward4 + + checkBlockUpdateRewards( + 4, + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward4), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward4), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward4) + ).sortBy(_.address.toByteStr) + )(r) + + // CeaseXTNBuyback activated with expired XTN buyback reward period + val configAddrReward5 = BlockRewardCalculator.MaxAddressReward + val minerReward5 = d.blockchain.settings.rewardsSettings.initial - configAddrReward5 + + checkBlockUpdateRewards( + 5, + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward5), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward5) + ).sortBy(_.address.toByteStr) + )(r) + } + } + + "should return correct rewardShares for GetBlockUpdatesRange (NODE-839)" in { + blockUpdatesRewardSharesTestCase { case (miner, daoAddress, xtnBuybackAddress, d, r) => + val updates = r.getBlockUpdatesRange(GetBlockUpdatesRangeRequest(2, 5)).futureValue.updates + + // reward distribution features not activated + checkBlockUpdateRewards(updates.head, Seq(RewardShare(ByteString.copyFrom(miner.bytes), d.blockchain.settings.rewardsSettings.initial))) + + // BlockRewardDistribution activated + val configAddrReward3 = d.blockchain.settings.rewardsSettings.initial / 3 + val minerReward3 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward3 + + checkBlockUpdateRewards( + updates(1), + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward3), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward3), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward3) + ).sortBy(_.address.toByteStr) + ) + + // CappedReward activated + val configAddrReward4 = BlockRewardCalculator.MaxAddressReward + val minerReward4 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward4 + + checkBlockUpdateRewards( + updates(2), + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward4), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward4), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward4) + ).sortBy(_.address.toByteStr) + ) + + // CeaseXTNBuyback activated with expired XTN buyback reward period + val configAddrReward5 = BlockRewardCalculator.MaxAddressReward + val minerReward5 = d.blockchain.settings.rewardsSettings.initial - configAddrReward5 + + checkBlockUpdateRewards( + updates(3), + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward5), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward5) + ).sortBy(_.address.toByteStr) + ) + } + } + + "should return correct rewardShares for Subscribe (NODE-840)" in { + blockUpdatesRewardSharesTestCase { case (miner, daoAddress, xtnBuybackAddress, d, r) => + val subscription = r.createFakeObserver(SubscribeRequest.of(2, 0)) + + val rewardShares = subscription.fetchAllEvents(d.blockchain).map(_.getUpdate.getAppend.body.block.map(_.rewardShares)) + + // reward distribution features not activated + rewardShares.head shouldBe Some(Seq(RewardShare(ByteString.copyFrom(miner.bytes), d.blockchain.settings.rewardsSettings.initial))) + + // BlockRewardDistribution activated + val configAddrReward3 = d.blockchain.settings.rewardsSettings.initial / 3 + val minerReward3 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward3 + + rewardShares(1) shouldBe Some( + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward3), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward3), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward3) + ).sortBy(_.address.toByteStr) + ) + + // CappedReward activated + val configAddrReward4 = BlockRewardCalculator.MaxAddressReward + val minerReward4 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward4 + + rewardShares(2) shouldBe Some( + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward4), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward4), + RewardShare(ByteString.copyFrom(xtnBuybackAddress.bytes), configAddrReward4) + ).sortBy(_.address.toByteStr) + ) + + // CeaseXTNBuyback activated with expired XTN buyback reward period + val configAddrReward5 = BlockRewardCalculator.MaxAddressReward + val minerReward5 = d.blockchain.settings.rewardsSettings.initial - configAddrReward5 + + rewardShares(3) shouldBe Some( + Seq( + RewardShare(ByteString.copyFrom(miner.bytes), minerReward5), + RewardShare(ByteString.copyFrom(daoAddress.bytes), configAddrReward5) + ).sortBy(_.address.toByteStr) + ) + } + } } private def assertCommon(rollback: RollbackResult): Assertion = { @@ -931,4 +1072,47 @@ class BlockchainUpdatesSpec extends FreeSpec with WithBUDomain with ScalaFutures ) case _ => throw new IllegalArgumentException("Not a microblock rollback") } + + private def blockUpdatesRewardSharesTestCase(checks: (Address, Address, Address, Domain, Repo) => Unit): Unit = { + val daoAddress = TxHelpers.address(3) + val xtnBuybackAddress = TxHelpers.address(4) + + val settings = DomainPresets.ConsensusImprovements + val settingsWithFeatures = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString), xtnBuybackRewardPeriod = 1), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 3, + BlockchainFeatures.CappedReward -> 4, + BlockchainFeatures.CeaseXtnBuyback -> 5 + ) + + withDomainAndRepo(settingsWithFeatures) { case (d, r) => + val miner = d.appendBlock().sender.toAddress + d.appendBlock() + (3 to 5).foreach(_ => d.appendBlock()) + d.appendBlock() + + checks(miner, daoAddress, xtnBuybackAddress, d, r) + } + } + + private def checkBlockUpdateRewards(height: Int, expected: Seq[RewardShare])(repo: Repo): Assertion = + Await + .result( + repo.getBlockUpdate(GetBlockUpdateRequest(height)), + Duration.Inf + ) + .getUpdate + .update + .append + .flatMap(_.body.block.map(_.rewardShares)) shouldBe Some(expected) + + private def checkBlockUpdateRewards(bu: protobuf.BlockchainUpdated, expected: Seq[RewardShare]): Assertion = + bu.getAppend.body.block.map(_.rewardShares) shouldBe Some(expected) } diff --git a/node-it/src/test/resources/template.conf b/node-it/src/test/resources/template.conf index 37e0f4f4c21..bb6f251f339 100644 --- a/node-it/src/test/resources/template.conf +++ b/node-it/src/test/resources/template.conf @@ -52,6 +52,7 @@ waves { } rewards { term = 100000 + term-after-capped-reward-feature = 50000 initial = 600000000 min-increment = 50000000 voting-interval = 10000 diff --git a/node/src/main/resources/swagger-ui/openapi.yaml b/node/src/main/resources/swagger-ui/openapi.yaml index b6bc276856d..1620b2beda1 100644 --- a/node/src/main/resources/swagger-ui/openapi.yaml +++ b/node/src/main/resources/swagger-ui/openapi.yaml @@ -3881,6 +3881,15 @@ components: reward: type: integer format: int64 + rewardShares: + type: object + additionalProperties: + type: integer + format: int64 + description: map of address <-> reward + example: + 3MtmVzUQQj1keBsr3Pq5DYkutZzu5H8wdgA: 200000000 + 3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS: 100000000 desiredReward: type: integer format: int64 @@ -3949,6 +3958,14 @@ components: format: int32 reward: type: string + rewardShares: + type: object + additionalProperties: + type: string + description: map of address <-> reward + example: + 3MtmVzUQQj1keBsr3Pq5DYkutZzu5H8wdgA: '200000000' + 3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS: '100000000' desiredReward: type: string VRF: @@ -4302,6 +4319,10 @@ components: decrease: type: integer format: int32 + daoAddress: + type: string + xtnBuybackAddress: + type: string RewardStatusLSF: required: - currentReward @@ -4347,6 +4368,10 @@ components: decrease: type: integer format: int32 + daoAddress: + type: string + xtnBuybackAddress: + type: string Signed: type: object properties: @@ -5413,6 +5438,9 @@ components: transactionCount: 10 features: [0] reward: '30000' + rewardShares: + 3MtmVzUQQj1keBsr3Pq5DYkutZzu5H8wdgA: '200000000' + 3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS: '100000000' desiredReward: '30000' VRF: 'some_string_should_be_here' transactionsRoot: 'some_string_should_be_here' @@ -5460,6 +5488,9 @@ components: transactionCount: 10 features: [0] reward: '30000' + rewardShares: + 3MtmVzUQQj1keBsr3Pq5DYkutZzu5H8wdgA: '200000000' + 3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS: '100000000' desiredReward: '30000' VRF: 'some_string_should_be_here' transactionsRoot: 'some_string_should_be_here' @@ -5477,6 +5508,8 @@ components: votes: increase: 0 decrease: 0 + daoAddress: '3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS' + xtnBuybackAddress: '3N13KQpdY3UU7JkWUBD9kN7t7xuUgeyYMTT' largeSignificandTransaction: value: type: 7 diff --git a/node/src/main/scala/com/wavesplatform/Application.scala b/node/src/main/scala/com/wavesplatform/Application.scala index 9eea5e835ae..eca23af1570 100644 --- a/node/src/main/scala/com/wavesplatform/Application.scala +++ b/node/src/main/scala/com/wavesplatform/Application.scala @@ -31,7 +31,7 @@ import com.wavesplatform.mining.{Miner, MinerDebugInfo, MinerImpl} import com.wavesplatform.network.* import com.wavesplatform.settings.WavesSettings import com.wavesplatform.state.appender.{BlockAppender, ExtensionAppender, MicroblockAppender} -import com.wavesplatform.state.{Blockchain, BlockchainUpdaterImpl, Diff, Height, TxMeta} +import com.wavesplatform.state.{BlockRewardCalculator, Blockchain, BlockchainUpdaterImpl, Diff, Height, TxMeta} import com.wavesplatform.transaction.TxValidationError.GenericError import com.wavesplatform.transaction.smart.script.trace.TracedResult import com.wavesplatform.transaction.{Asset, DiscardedBlocks, Transaction} @@ -597,10 +597,14 @@ object Application extends ScorexLogging { } private[wavesplatform] def loadBlockMetaAt(db: DB, blockchainUpdater: BlockchainUpdaterImpl)(height: Int): Option[BlockMeta] = { - val result = blockchainUpdater.liquidBlockMeta + blockchainUpdater.liquidBlockMeta .filter(_ => blockchainUpdater.height == height) .orElse(db.get(Keys.blockMetaAt(Height(height)))) - result + .map { blockMeta => + val rewardShares = BlockRewardCalculator.getSortedBlockRewardShares(height, blockMeta.header.generator.toAddress, blockchainUpdater) + + blockMeta.copy(rewardShares = rewardShares) + } } def main(args: Array[String]): Unit = { diff --git a/node/src/main/scala/com/wavesplatform/api/BlockMeta.scala b/node/src/main/scala/com/wavesplatform/api/BlockMeta.scala index 5b557396be1..a9c86e1f0f6 100644 --- a/node/src/main/scala/com/wavesplatform/api/BlockMeta.scala +++ b/node/src/main/scala/com/wavesplatform/api/BlockMeta.scala @@ -1,5 +1,6 @@ package com.wavesplatform.api +import com.wavesplatform.account.Address import com.wavesplatform.block.Block.protoHeaderHash import com.wavesplatform.block.serialization.BlockHeaderSerializer import com.wavesplatform.block.{Block, BlockHeader, SignedBlockHeader} @@ -16,6 +17,7 @@ case class BlockMeta( transactionCount: Int, totalFeeInWaves: Long, reward: Option[Long], + rewardShares: Seq[(Address, Long)], vrf: Option[ByteStr] ) { def toSignedHeader: SignedBlockHeader = SignedBlockHeader(header, signature) @@ -24,22 +26,31 @@ case class BlockMeta( val json: Coeval[JsObject] = Coeval.evalOnce { BlockHeaderSerializer.toJson(header, size, transactionCount, signature) ++ Json.obj("height" -> height, "totalFee" -> totalFeeInWaves) ++ - reward.fold(Json.obj())(r => Json.obj("reward" -> r)) ++ - vrf.fold(Json.obj())(v => Json.obj("VRF" -> v.toString)) ++ + reward.fold(Json.obj())(r => + Json.obj( + "reward" -> r, + "rewardShares" -> Json.obj(rewardShares.map[(String, Json.JsValueWrapper)] { case (addrName, reward) => + addrName.toString -> reward + }*) + ) + ) ++ + vrf.fold(Json.obj())(v => Json.obj("VRF" -> v.toString)) ++ headerHash.fold(Json.obj())(h => Json.obj("id" -> h.toString)) } } object BlockMeta { - def fromBlock(block: Block, height: Int, totalFee: Long, reward: Option[Long], vrf: Option[ByteStr]): BlockMeta = BlockMeta( - block.header, - block.signature, - if (block.header.version >= Block.ProtoBlockVersion) Some(protoHeaderHash(block.header)) else None, - height, - block.bytes().length, - block.transactionData.length, - totalFee, - reward, - vrf - ) + def fromBlock(block: Block, height: Int, totalFee: Long, reward: Option[Long], vrf: Option[ByteStr]): BlockMeta = + BlockMeta( + block.header, + block.signature, + if (block.header.version >= Block.ProtoBlockVersion) Some(protoHeaderHash(block.header)) else None, + height, + block.bytes().length, + block.transactionData.length, + totalFee, + reward, + Seq.empty, + vrf + ) } diff --git a/node/src/main/scala/com/wavesplatform/api/http/CustomJson.scala b/node/src/main/scala/com/wavesplatform/api/http/CustomJson.scala index 91e07e69980..21c8f051988 100644 --- a/node/src/main/scala/com/wavesplatform/api/http/CustomJson.scala +++ b/node/src/main/scala/com/wavesplatform/api/http/CustomJson.scala @@ -32,6 +32,7 @@ object NumberAsStringSerializer extends JsonSerializer[JsValue] { "quantity", "regular", "reward", + "rewardShares", "sellMatcherFee", "sponsorBalance", "totalAmount", @@ -41,15 +42,19 @@ object NumberAsStringSerializer extends JsonSerializer[JsValue] { ) override def serialize(value: JsValue, json: JsonGenerator, provider: SerializerProvider): Unit = + serializeWithNumberAsStrings(value, json, provider, insideStringifiedField = false) + + private def serializeWithNumberAsStrings(value: JsValue, json: JsonGenerator, provider: SerializerProvider, insideStringifiedField: Boolean): Unit = value match { - case JsNumber(v) => json.writeNumber(v.bigDecimal) - case JsString(v) => json.writeString(v) - case v: JsBoolean => json.writeBoolean(v.value) + case JsNumber(v) if insideStringifiedField => json.writeString(v.bigDecimal.toPlainString) + case JsNumber(v) => json.writeNumber(v.bigDecimal) + case JsString(v) => json.writeString(v) + case v: JsBoolean => json.writeBoolean(v.value) case JsArray(elements) => json.writeStartArray() elements.foreach { t => - serialize(t, json, provider) + serializeWithNumberAsStrings(t, json, provider, insideStringifiedField) } json.writeEndArray() @@ -58,9 +63,12 @@ object NumberAsStringSerializer extends JsonSerializer[JsValue] { values.foreach { case (name, JsNumber(v)) if fieldNamesToTranslate(name) => json.writeStringField(name, v.bigDecimal.toPlainString) + case (name, jsv) if fieldNamesToTranslate(name) => + json.writeFieldName(name) + serializeWithNumberAsStrings(jsv, json, provider, insideStringifiedField = true) case (name, jsv) => json.writeFieldName(name) - serialize(jsv, json, provider) + serializeWithNumberAsStrings(jsv, json, provider, insideStringifiedField) } json.writeEndObject() diff --git a/node/src/main/scala/com/wavesplatform/api/http/RewardApiRoute.scala b/node/src/main/scala/com/wavesplatform/api/http/RewardApiRoute.scala index 1172fe66855..6be41e9ba23 100644 --- a/node/src/main/scala/com/wavesplatform/api/http/RewardApiRoute.scala +++ b/node/src/main/scala/com/wavesplatform/api/http/RewardApiRoute.scala @@ -31,19 +31,23 @@ case class RewardApiRoute(blockchain: Blockchain) extends ApiRoute { .filter(_ <= height) .toRight(GenericError("Block reward feature is not activated yet")) reward <- blockchain.blockReward(height).toRight(GenericError(s"No information about rewards at height = $height")) - amount = blockchain.wavesAmount(height) - rewardsSettings = blockchain.settings.rewardsSettings - funcSettings = blockchain.settings.functionalitySettings - nextCheck = rewardsSettings.nearestTermEnd(activatedAt, height) + amount = blockchain.wavesAmount(height) + rewardsSettings = blockchain.settings.rewardsSettings + funcSettings = blockchain.settings.functionalitySettings + nextCheck = rewardsSettings.nearestTermEnd(activatedAt, height, blockchain.isFeatureActivated(BlockchainFeatures.CappedReward, height)) votingIntervalStart = nextCheck - rewardsSettings.votingInterval + 1 votingThreshold = rewardsSettings.votingInterval / 2 + 1 votes = blockchain.blockRewardVotes(height).filter(_ >= 0) + term = + if (blockchain.isFeatureActivated(BlockchainFeatures.CappedReward, height)) + rewardsSettings.termAfterCappedRewardFeature + else rewardsSettings.term } yield RewardStatus( height, amount, reward, rewardsSettings.minIncrement, - rewardsSettings.term, + term, nextCheck, votingIntervalStart, rewardsSettings.votingInterval, diff --git a/node/src/main/scala/com/wavesplatform/database/Keys.scala b/node/src/main/scala/com/wavesplatform/database/Keys.scala index dce34e5aca1..bdb9f578206 100644 --- a/node/src/main/scala/com/wavesplatform/database/Keys.scala +++ b/node/src/main/scala/com/wavesplatform/database/Keys.scala @@ -15,7 +15,13 @@ import com.wavesplatform.utils.* object Keys { import KeyHelpers.* - import KeyTags.{AddressId as AddressIdTag, EthereumTransactionMeta as EthereumTransactionMetaTag, InvokeScriptResult as InvokeScriptResultTag, LeaseDetails as LeaseDetailsTag, *} + import KeyTags.{ + AddressId as AddressIdTag, + EthereumTransactionMeta as EthereumTransactionMetaTag, + InvokeScriptResult as InvokeScriptResultTag, + LeaseDetails as LeaseDetailsTag, + * + } val version: Key[Int] = intKey(Version, default = 1) val height: Key[Int] = intKey(Height) @@ -140,14 +146,6 @@ object Keys { _.toByteArray ) - def blockTransactionsFee(height: Int): Key[Long] = - Key( - BlockTransactionsFee, - h(height), - Longs.fromByteArray, - Longs.toByteArray - ) - def invokeScriptResult(height: Int, txNum: TxNum): Key[Option[InvokeScriptResult]] = Key.opt(InvokeScriptResultTag, hNum(height, txNum), InvokeScriptResult.fromBytes, InvokeScriptResult.toBytes) diff --git a/node/src/main/scala/com/wavesplatform/database/LevelDBWriter.scala b/node/src/main/scala/com/wavesplatform/database/LevelDBWriter.scala index 440af49bcd8..9aeca8428c9 100644 --- a/node/src/main/scala/com/wavesplatform/database/LevelDBWriter.scala +++ b/node/src/main/scala/com/wavesplatform/database/LevelDBWriter.scala @@ -400,7 +400,13 @@ abstract class LevelDBWriter private[database] ( rw.put( Keys.blockMetaAt(Height(height)), Some( - BlockMeta.fromBlock(block, height, totalFee, reward, if (block.header.version >= Block.ProtoBlockVersion) Some(hitSource) else None) + BlockMeta.fromBlock( + block, + height, + totalFee, + reward, + if (block.header.version >= Block.ProtoBlockVersion) Some(hitSource) else None + ) ) ) rw.put(Keys.heightOf(block.id()), Some(height)) @@ -554,8 +560,6 @@ abstract class LevelDBWriter private[database] ( rw.put(Keys.carryFee(height), carry) expiredKeys += Keys.carryFee(threshold - 1).keyBytes - rw.put(Keys.blockTransactionsFee(height), totalFee) - if (dbSettings.storeInvokeScriptResults) scriptResults.foreach { case (txId, result) => val (txHeight, txNum) = transactions .get(TransactionId(txId)) @@ -737,7 +741,6 @@ abstract class LevelDBWriter private[database] ( rw.delete(Keys.changedAddresses(currentHeight)) rw.delete(Keys.heightOf(discardedMeta.id)) rw.delete(Keys.carryFee(currentHeight)) - rw.delete(Keys.blockTransactionsFee(currentHeight)) rw.delete(Keys.blockReward(currentHeight)) rw.delete(Keys.wavesAmount(currentHeight)) rw.delete(Keys.stateHash(currentHeight)) @@ -949,8 +952,9 @@ abstract class LevelDBWriter private[database] ( override def blockRewardVotes(height: Int): Seq[Long] = readOnly { db => activatedFeatures.get(BlockchainFeatures.BlockReward.id) match { case Some(activatedAt) if activatedAt <= height => + val modifyTerm = activatedFeatures.get(BlockchainFeatures.CappedReward.id).exists(_ <= height) settings.rewardsSettings - .votingWindow(activatedAt, height) + .votingWindow(activatedAt, height, modifyTerm) .flatMap { h => db.get(Keys.blockMetaAt(Height(h))) .map(_.header.rewardVote) diff --git a/node/src/main/scala/com/wavesplatform/database/package.scala b/node/src/main/scala/com/wavesplatform/database/package.scala index 83948f985fa..e738f9a38c3 100644 --- a/node/src/main/scala/com/wavesplatform/database/package.scala +++ b/node/src/main/scala/com/wavesplatform/database/package.scala @@ -352,6 +352,7 @@ package object database { pbbm.transactionCount, pbbm.totalFeeInWaves, Option(pbbm.reward).filter(_ >= 0), + Seq.empty, Option(pbbm.vrf).collect { case bs if !bs.isEmpty => bs.toByteStr } ) } diff --git a/node/src/main/scala/com/wavesplatform/events/BlockchainUpdateTriggers.scala b/node/src/main/scala/com/wavesplatform/events/BlockchainUpdateTriggers.scala index ffc96a20a5b..3b7bb321f62 100644 --- a/node/src/main/scala/com/wavesplatform/events/BlockchainUpdateTriggers.scala +++ b/node/src/main/scala/com/wavesplatform/events/BlockchainUpdateTriggers.scala @@ -10,14 +10,14 @@ trait BlockchainUpdateTriggers { def onProcessBlock( block: Block, diff: DetailedDiff, - minerReward: Option[Long], + reward: Option[Long], hitSource: ByteStr, - blockchainBeforeWithMinerReward: Blockchain + blockchainBeforeWithReward: Blockchain ): Unit def onProcessMicroBlock( microBlock: MicroBlock, diff: DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, totalBlockId: ByteStr, totalTransactionsRoot: ByteStr ): Unit @@ -30,14 +30,14 @@ object BlockchainUpdateTriggers { override def onProcessBlock( block: Block, diff: DetailedDiff, - minerReward: Option[Long], + reward: Option[Long], hitSource: ByteStr, - blockchainBeforeWithMinerReward: Blockchain + blockchainBeforeWithReward: Blockchain ): Unit = {} override def onProcessMicroBlock( microBlock: MicroBlock, diff: DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, totalBlockId: ByteStr, totalTransactionsRoot: ByteStr ): Unit = {} @@ -49,20 +49,20 @@ object BlockchainUpdateTriggers { override def onProcessBlock( block: Block, diff: BlockDiffer.DetailedDiff, - minerReward: Option[Long], + reward: Option[Long], hitSource: ByteStr, - blockchainBeforeWithMinerReward: Blockchain + blockchainBeforeWithReward: Blockchain ): Unit = - triggers.foreach(_.onProcessBlock(block, diff, minerReward, hitSource, blockchainBeforeWithMinerReward)) + triggers.foreach(_.onProcessBlock(block, diff, reward, hitSource, blockchainBeforeWithReward)) override def onProcessMicroBlock( microBlock: MicroBlock, diff: BlockDiffer.DetailedDiff, - blockchainBeforeWithMinerReward: Blockchain, + blockchainBeforeWithReward: Blockchain, totalBlockId: ByteStr, totalTransactionsRoot: ByteStr ): Unit = - triggers.foreach(_.onProcessMicroBlock(microBlock, diff, blockchainBeforeWithMinerReward, totalBlockId, totalTransactionsRoot)) + triggers.foreach(_.onProcessMicroBlock(microBlock, diff, blockchainBeforeWithReward, totalBlockId, totalTransactionsRoot)) override def onRollback(blockchainBefore: Blockchain, toBlockId: ByteStr, toHeight: Int): Unit = triggers.foreach(_.onRollback(blockchainBefore, toBlockId, toHeight)) diff --git a/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala b/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala index 4ce973931f6..9b954ec6fab 100644 --- a/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala +++ b/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala @@ -23,10 +23,12 @@ object BlockchainFeatures { val RideV6 = BlockchainFeature(17, "Ride V6, MetaMask support") val ConsensusImprovements = BlockchainFeature(18, "Consensus and MetaMask updates") val BlockRewardDistribution = BlockchainFeature(19, "Block Reward Distribution") + val CappedReward = BlockchainFeature(20, "Capped XTN buy-back & DAO amounts") + val CeaseXtnBuyback = BlockchainFeature(21, "Cease XTN buy-back") // Not exposed - val ContinuationTransaction = BlockchainFeature(20, "Continuation Transaction") - val LeaseExpiration = BlockchainFeature(21, "Lease Expiration") + val ContinuationTransaction = BlockchainFeature(22, "Continuation Transaction") + val LeaseExpiration = BlockchainFeature(23, "Lease Expiration") // When next fork-parameter is created, you must replace all uses of the DummyFeature with the new one. val Dummy = BlockchainFeature(-1, "Non Votable!") @@ -50,7 +52,9 @@ object BlockchainFeatures { SynchronousCalls, RideV6, ConsensusImprovements, - BlockRewardDistribution + BlockRewardDistribution, + CappedReward, + CeaseXtnBuyback ).map(f => f.id -> f).toMap val implemented: Set[Short] = dict.keySet diff --git a/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala b/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala index 7f5a5f647ac..0b37e2be9b6 100644 --- a/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala +++ b/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala @@ -13,6 +13,7 @@ import scala.concurrent.duration.* case class RewardsSettings( term: Int, + termAfterCappedRewardFeature: Int, initial: Long, minIncrement: Long, votingInterval: Int @@ -22,16 +23,22 @@ case class RewardsSettings( require(term > 0, "term must be greater than 0") require(votingInterval > 0, "votingInterval must be greater than 0") require(votingInterval <= term, s"votingInterval must be less than or equal to term($term)") + require(termAfterCappedRewardFeature > 0, "termAfterCappedRewardFeature must be greater than 0") + require( + votingInterval <= termAfterCappedRewardFeature, + s"votingInterval must be less than or equal to termAfterCappedRewardFeature($termAfterCappedRewardFeature)" + ) - def nearestTermEnd(activatedAt: Int, height: Int): Int = { + def nearestTermEnd(activatedAt: Int, height: Int, modifyTerm: Boolean): Int = { require(height >= activatedAt) - val diff = height - activatedAt + 1 - val mul = math.ceil(diff.toDouble / term).toInt - activatedAt + mul * term - 1 + val diff = height - activatedAt + 1 + val modifiedTerm = if (modifyTerm) termAfterCappedRewardFeature else term + val mul = math.ceil(diff.toDouble / modifiedTerm).toInt + activatedAt + mul * modifiedTerm - 1 } - def votingWindow(activatedAt: Int, height: Int): Range = { - val end = nearestTermEnd(activatedAt, height) + def votingWindow(activatedAt: Int, height: Int, modifyTerm: Boolean): Range = { + val end = nearestTermEnd(activatedAt, height, modifyTerm) val start = end - votingInterval + 1 if (height >= start) Range.inclusive(start, height) else Range(0, 0) @@ -41,6 +48,7 @@ case class RewardsSettings( object RewardsSettings { val MAINNET, TESTNET, STAGENET = apply( 100000, + 50000, 6 * Constants.UnitsInWave, 50000000, 10000 @@ -67,7 +75,8 @@ case class FunctionalitySettings( enforceTransferValidationAfter: Int = 0, ethInvokePaymentsCheckHeight: Int = 0, daoAddress: Option[String] = None, - xtnBuybackAddress: Option[String] = None + xtnBuybackAddress: Option[String] = None, + xtnBuybackRewardPeriod: Int = Int.MaxValue ) { val allowLeasedBalanceTransferUntilHeight: Int = blockVersion3AfterHeight val allowTemporaryNegativeUntil: Long = lastTimeBasedForkParameter @@ -119,7 +128,8 @@ object FunctionalitySettings { estimatorSumOverflowFixHeight = 2897510, enforceTransferValidationAfter = 2959447, daoAddress = Some("3PEgG7eZHLFhcfsTSaYxgRhZsh4AxMvA4Ms"), - xtnBuybackAddress = Some("3PFjHWuH6WXNJbwnfLHqNFBpwBS5dkYjTfv") + xtnBuybackAddress = Some("3PFjHWuH6WXNJbwnfLHqNFBpwBS5dkYjTfv"), + xtnBuybackRewardPeriod = 100000 ) val TESTNET: FunctionalitySettings = apply( @@ -133,7 +143,8 @@ object FunctionalitySettings { estimatorSumOverflowFixHeight = 1832520, enforceTransferValidationAfter = 1698800, daoAddress = Some("3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS"), - xtnBuybackAddress = Some("3N13KQpdY3UU7JkWUBD9kN7t7xuUgeyYMTT") + xtnBuybackAddress = Some("3N13KQpdY3UU7JkWUBD9kN7t7xuUgeyYMTT"), + xtnBuybackRewardPeriod = 2000 ) val STAGENET: FunctionalitySettings = apply( @@ -146,7 +157,8 @@ object FunctionalitySettings { estimatorSumOverflowFixHeight = 1097419, ethInvokePaymentsCheckHeight = 1311110, daoAddress = Some("3MaFVH1vTv18FjBRugSRebx259D7xtRh9ic"), - xtnBuybackAddress = Some("3MbhiRiLFLJ1EVKNP9npRszcLLQDjwnFfZM") + xtnBuybackAddress = Some("3MbhiRiLFLJ1EVKNP9npRszcLLQDjwnFfZM"), + xtnBuybackRewardPeriod = 1000 ) } diff --git a/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala b/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala new file mode 100644 index 00000000000..9fdf2c69ac3 --- /dev/null +++ b/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala @@ -0,0 +1,82 @@ +package com.wavesplatform.state + +import com.wavesplatform.account.Address +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.features.BlockchainFeatures +import com.wavesplatform.settings.Constants +import com.wavesplatform.state.diffs.BlockDiffer.Fraction + +object BlockRewardCalculator { + + case class BlockRewardShares(miner: Long, daoAddress: Long, xtnBuybackAddress: Long) + + val CurrentBlockRewardPart: Fraction = Fraction(1, 3) + val RemaindRewardAddressPart: Fraction = Fraction(1, 2) + + val FullRewardInit: Long = 6 * Constants.UnitsInWave + val MaxAddressReward: Long = 2 * Constants.UnitsInWave + val GuaranteedMinerReward: Long = 2 * Constants.UnitsInWave + + def getBlockRewardShares( + height: Int, + fullBlockReward: Long, + daoAddress: Option[Address], + xtnBuybackAddress: Option[Address], + blockchain: Blockchain + ): BlockRewardShares = { + val blockRewardDistributionHeight = blockchain.featureActivationHeight(BlockchainFeatures.BlockRewardDistribution.id).getOrElse(Int.MaxValue) + val cappedRewardHeight = blockchain.featureActivationHeight(BlockchainFeatures.CappedReward.id).getOrElse(Int.MaxValue) + val ceaseXtnBuybackHeight = blockchain.featureActivationHeight(BlockchainFeatures.CeaseXtnBuyback.id).getOrElse(Int.MaxValue) + + if (height >= blockRewardDistributionHeight) { + val modifiedXtnBuybackAddress = xtnBuybackAddress.filter { _ => + height < ceaseXtnBuybackHeight || + height < blockRewardDistributionHeight + blockchain.settings.functionalitySettings.xtnBuybackRewardPeriod + } + if (height >= cappedRewardHeight) { + if (fullBlockReward < GuaranteedMinerReward) { + BlockRewardShares(fullBlockReward, 0, 0) + } else if (fullBlockReward < FullRewardInit) { + calculateRewards( + fullBlockReward, + RemaindRewardAddressPart.apply(fullBlockReward - GuaranteedMinerReward), + daoAddress, + modifiedXtnBuybackAddress + ) + } else { + calculateRewards(fullBlockReward, MaxAddressReward, daoAddress, modifiedXtnBuybackAddress) + } + } else { + calculateRewards(fullBlockReward, CurrentBlockRewardPart.apply(fullBlockReward), daoAddress, modifiedXtnBuybackAddress) + } + } else BlockRewardShares(fullBlockReward, 0, 0) + } + + def getSortedBlockRewardShares(height: Int, fullBlockReward: Long, generator: Address, blockchain: Blockchain): Seq[(Address, Long)] = { + val daoAddress = blockchain.settings.functionalitySettings.daoAddressParsed.toOption.flatten + val xtnBuybackAddress = blockchain.settings.functionalitySettings.xtnBuybackAddressParsed.toOption.flatten + + val rewardShares = getBlockRewardShares(height, fullBlockReward, daoAddress, xtnBuybackAddress, blockchain) + + (Seq(generator -> rewardShares.miner) ++ + daoAddress.map(_ -> rewardShares.daoAddress) ++ + xtnBuybackAddress.map(_ -> rewardShares.xtnBuybackAddress)) + .filter(_._2 > 0) + .sortBy { case (addr, _) => ByteStr(addr.bytes) } + } + + def getSortedBlockRewardShares(height: Int, generator: Address, blockchain: Blockchain): Seq[(Address, Long)] = { + val fullBlockReward = blockchain.blockReward(height).getOrElse(0L) + getSortedBlockRewardShares(height, fullBlockReward, generator, blockchain) + } + + private def calculateRewards(blockReward: Long, addressReward: Long, daoAddress: Option[Address], xtnBuybackAddress: Option[Address]) = { + val daoAddressReward = daoAddress.fold(0L) { _ => addressReward } + val xtnBuybackReward = xtnBuybackAddress.fold(0L) { _ => addressReward } + BlockRewardShares( + blockReward - daoAddressReward - xtnBuybackReward, + daoAddressReward, + xtnBuybackReward + ) + } +} diff --git a/node/src/main/scala/com/wavesplatform/state/Blockchain.scala b/node/src/main/scala/com/wavesplatform/state/Blockchain.scala index 0566ac6ad2a..36bf632c418 100644 --- a/node/src/main/scala/com/wavesplatform/state/Blockchain.scala +++ b/node/src/main/scala/com/wavesplatform/state/Blockchain.scala @@ -81,7 +81,7 @@ object Blockchain { def isEmpty: Boolean = blockchain.height == 0 def isSponsorshipActive: Boolean = blockchain.height >= Sponsorship.sponsoredFeesSwitchHeight(blockchain) - def isNGActive: Boolean = blockchain.isFeatureActivated(BlockchainFeatures.NG, blockchain.height - 1) + def isNGActive: Boolean = blockchain.isFeatureActivated(BlockchainFeatures.NG, blockchain.height - 1) def parentHeader(block: BlockHeader, back: Int = 1): Option[BlockHeader] = blockchain diff --git a/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala b/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala index 16050903436..ded8d3ff10c 100644 --- a/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala +++ b/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala @@ -162,9 +162,14 @@ class BlockchainUpdaterImpl( .flatMap { activatedAt => val mayBeReward = lastBlockReward val mayBeTimeToVote = nextHeight - activatedAt + val modifiedTerm = if (leveldb.isFeatureActivated(BlockchainFeatures.CappedReward, this.height)) { + settings.termAfterCappedRewardFeature + } else { + settings.term + } mayBeReward match { - case Some(reward) if mayBeTimeToVote > 0 && mayBeTimeToVote % settings.term == 0 => + case Some(reward) if mayBeTimeToVote > 0 && mayBeTimeToVote % modifiedTerm == 0 => Some((blockRewardVotes(this.height).filter(_ >= 0), reward)) case None if mayBeTimeToVote >= 0 => Some((Seq(), settings.initial)) @@ -577,7 +582,8 @@ class BlockchainUpdaterImpl( case None => leveldb.blockRewardVotes(height) case Some(ng) => val innerVotes = leveldb.blockRewardVotes(height) - if (height == this.height && settings.rewardsSettings.votingWindow(activatedAt, height).contains(height)) + val modifyTerm = activatedFeatures.get(BlockchainFeatures.CappedReward.id).exists(_ <= height) + if (height == this.height && settings.rewardsSettings.votingWindow(activatedAt, height, modifyTerm).contains(height)) innerVotes :+ ng.base.header.rewardVote else innerVotes } diff --git a/node/src/main/scala/com/wavesplatform/state/NgState.scala b/node/src/main/scala/com/wavesplatform/state/NgState.scala index 0e8baa84c4a..5a39fbbbee9 100644 --- a/node/src/main/scala/com/wavesplatform/state/NgState.scala +++ b/node/src/main/scala/com/wavesplatform/state/NgState.scala @@ -63,7 +63,7 @@ case class NgState( .toList .foldLeft[Either[String, Diff]](Right(diff)) { case (Right(d1), d2) => d1.combineF(d2) - case (r, _) => r + case (r, _) => r } def microBlockIds: Seq[BlockId] = microBlocks.map(_.totalBlockId) @@ -74,7 +74,8 @@ case class NgState( (baseBlockDiff, baseBlockCarry, baseBlockTotalFee) else internalCaches.blockDiffCache.get( - totalResBlockRef, { () => + totalResBlockRef, + { () => microBlocks.find(_.idEquals(totalResBlockRef)) match { case Some(MicroBlockInfo(blockId, current)) => val (prevDiff, prevCarry, prevTotalFee) = this.diffFor(current.reference) @@ -113,10 +114,9 @@ case class NgState( } def totalDiffOf(id: BlockId): Option[(Block, Diff, Long, Long, DiscardedMicroBlocks)] = - forgeBlock(id).map { - case (block, discarded) => - val (diff, carry, totalFee) = this.diffFor(id) - (block, diff, carry, totalFee, discarded) + forgeBlock(id).map { case (block, discarded) => + val (diff, carry, totalFee) = this.diffFor(id) + (block, diff, carry, totalFee, discarded) } def bestLiquidDiffAndFees: (Diff, Long, Long) = diffFor(microBlocks.headOption.fold(base.id())(_.totalBlockId)) @@ -177,14 +177,20 @@ case class NgState( private[this] def forgeBlock(blockId: BlockId): Option[(Block, DiscardedMicroBlocks)] = internalCaches.forgedBlockCache.get( - blockId, { () => + blockId, + { () => val microBlocksAsc = microBlocks.reverse if (base.id() == blockId) { - Some((base, microBlocksAsc.toVector.map { mb => - val diff = microDiffs(mb.totalBlockId).diff - (mb.microBlock, diff) - })) + Some( + ( + base, + microBlocksAsc.toVector.map { mb => + val diff = microDiffs(mb.totalBlockId).diff + (mb.microBlock, diff) + } + ) + ) } else if (!microBlocksAsc.exists(_.idEquals(blockId))) None else { val (accumulatedTxs, maybeFound) = microBlocksAsc.foldLeft((Vector.empty[Transaction], Option.empty[(ByteStr, DiscardedMicroBlocks)])) { @@ -200,9 +206,8 @@ case class NgState( (accumulated ++ mb.transactionData, None) } - maybeFound.map { - case (sig, discarded) => - (Block.create(base, base.transactionData ++ accumulatedTxs, sig), discarded) + maybeFound.map { case (sig, discarded) => + (Block.create(base, base.transactionData ++ accumulatedTxs, sig), discarded) } } } diff --git a/node/src/main/scala/com/wavesplatform/state/diffs/BlockDiffer.scala b/node/src/main/scala/com/wavesplatform/state/diffs/BlockDiffer.scala index 266fa7ab877..c252e77640c 100644 --- a/node/src/main/scala/com/wavesplatform/state/diffs/BlockDiffer.scala +++ b/node/src/main/scala/com/wavesplatform/state/diffs/BlockDiffer.scala @@ -23,8 +23,7 @@ object BlockDiffer { def apply(l: Long): Long = l / divider * dividend } - val CurrentBlockFeePart: Fraction = Fraction(2, 5) - val CurrentBlockRewardPart: Fraction = Fraction(1, 3) + val CurrentBlockFeePart: Fraction = Fraction(2, 5) def fromBlock( blockchain: Blockchain, @@ -44,32 +43,31 @@ object BlockDiffer { hitSource: ByteStr, verify: Boolean ): TracedResult[ValidationError, Result] = { - val stateHeight = blockchain.height + val stateHeight = blockchain.height + val heightWithNewBlock = stateHeight + 1 // height switch is next after activation - val ngHeight = blockchain.featureActivationHeight(BlockchainFeatures.NG.id).getOrElse(Int.MaxValue) - val sponsorshipHeight = Sponsorship.sponsoredFeesSwitchHeight(blockchain) - val blockRewardDistributionHeight = blockchain.featureActivationHeight(BlockchainFeatures.BlockRewardDistribution.id).getOrElse(Int.MaxValue) - - val blockReward = blockchain.lastBlockReward.fold(Portfolio.empty)(Portfolio.waves) + val ngHeight = blockchain.featureActivationHeight(BlockchainFeatures.NG.id).getOrElse(Int.MaxValue) + val sponsorshipHeight = Sponsorship.sponsoredFeesSwitchHeight(blockchain) val addressRewardsE = for { daoAddress <- blockchain.settings.functionalitySettings.daoAddressParsed xtnBuybackAddress <- blockchain.settings.functionalitySettings.xtnBuybackAddressParsed } yield { - if (stateHeight + 1 >= blockRewardDistributionHeight) { - val daoAddressReward = daoAddress.fold(Portfolio.empty) { _ => - blockReward.multiply(CurrentBlockRewardPart) - } - val xtnBuybackReward = xtnBuybackAddress.fold(Portfolio.empty) { _ => - blockReward.multiply(CurrentBlockRewardPart) - } - ( - blockReward.minus(daoAddressReward).minus(xtnBuybackReward), - daoAddress.fold(Diff.empty)(addr => Diff(portfolios = Map(addr -> daoAddressReward))), - xtnBuybackAddress.fold(Diff.empty)(addr => Diff(portfolios = Map(addr -> xtnBuybackReward))) + val blockRewardShares = BlockRewardCalculator.getBlockRewardShares( + heightWithNewBlock, + blockchain.lastBlockReward.getOrElse(0L), + daoAddress, + xtnBuybackAddress, + blockchain + ) + ( + Portfolio.waves(blockRewardShares.miner), + daoAddress.fold(Diff.empty)(addr => Diff(portfolios = Map(addr -> Portfolio.waves(blockRewardShares.daoAddress)).filter(_._2.balance > 0))), + xtnBuybackAddress.fold(Diff.empty)(addr => + Diff(portfolios = Map(addr -> Portfolio.waves(blockRewardShares.xtnBuybackAddress)).filter(_._2.balance > 0)) ) - } else (blockReward, Diff.empty, Diff.empty) + ) } val feeFromPreviousBlockE = diff --git a/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala b/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala index 4f477894546..9c04d3c645c 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala @@ -19,7 +19,7 @@ import com.wavesplatform.lang.v1.traits.* import com.wavesplatform.lang.v1.traits.domain.* import com.wavesplatform.lang.v1.traits.domain.Recipient.* import com.wavesplatform.state.* -import com.wavesplatform.state.diffs.BlockDiffer.CurrentBlockRewardPart +import com.wavesplatform.state.BlockRewardCalculator.CurrentBlockRewardPart import com.wavesplatform.state.diffs.invoke.{InvokeScript, InvokeScriptDiff, InvokeScriptTransactionLike} import com.wavesplatform.state.reader.CompositeBlockchain import com.wavesplatform.transaction.Asset.* @@ -248,17 +248,26 @@ class WavesEnvironment( reentrant: Boolean ): Coeval[(Either[ValidationError, (EVALUATED, Log[Id])], Int)] = ??? - private def getRewards(generator: PublicKey, height: Int): List[(Address, Long)] = { - blockchain.blockReward(height).fold(List.empty[(Address, Long)]) { reward => - val configAddressesReward = - (blockchain.settings.functionalitySettings.daoAddressParsed.toList.flatten ++ - blockchain.settings.functionalitySettings.xtnBuybackAddressParsed.toList.flatten).map { addr => - Address(ByteStr(addr.bytes)) -> CurrentBlockRewardPart.apply(reward) - } - - val minerReward = Address(ByteStr(generator.toAddress.bytes)) -> (reward - configAddressesReward.map(_._2).sum) + private def getRewards(generator: PublicKey, height: Int): Seq[(Address, Long)] = { + if (blockchain.isFeatureActivated(BlockchainFeatures.CappedReward)) { + val rewardShares = BlockRewardCalculator.getSortedBlockRewardShares(height, generator.toAddress, blockchain) - (configAddressesReward :+ minerReward).sortBy(_._1.bytes) + rewardShares.map { case (addr, reward) => + Address(ByteStr(addr.bytes)) -> reward + } + } else { + val daoAddress = blockchain.settings.functionalitySettings.daoAddressParsed.toOption.flatten + val xtnBuybackAddress = blockchain.settings.functionalitySettings.xtnBuybackAddressParsed.toOption.flatten + + blockchain.blockReward(height).fold(Seq.empty[(Address, Long)]) { fullBlockReward => + val configAddressesReward = + (daoAddress.toSeq ++ xtnBuybackAddress).map { addr => + Address(ByteStr(addr.bytes)) -> CurrentBlockRewardPart.apply(fullBlockReward) + } + val minerReward = Address(ByteStr(generator.toAddress.bytes)) -> (fullBlockReward - configAddressesReward.map(_._2).sum) + + (configAddressesReward :+ minerReward).sortBy(_._1.bytes) + } } } } diff --git a/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala b/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala index f88cb45c413..e948e9b7f1e 100644 --- a/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala +++ b/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala @@ -15,9 +15,9 @@ import com.wavesplatform.lagonaki.mocks.TestBlock import com.wavesplatform.mining.MiningConstraint import com.wavesplatform.settings.{Constants, FunctionalitySettings, RewardsSettings} import com.wavesplatform.state.diffs.BlockDiffer -import com.wavesplatform.state.{Blockchain, Height} +import com.wavesplatform.state.{BlockRewardCalculator, Blockchain, Height} import com.wavesplatform.test.DomainPresets.{RideV6, WavesSettingsOps, BlockRewardDistribution as BlockRewardDistributionSettings} -import com.wavesplatform.test.FreeSpec +import com.wavesplatform.test.* import com.wavesplatform.transaction.Asset.Waves import com.wavesplatform.transaction.transfer.TransferTransaction import com.wavesplatform.transaction.{GenesisTransaction, TxHelpers} @@ -42,6 +42,7 @@ class BlockRewardSpec extends FreeSpec with WithDomain { ), rewardsSettings = RewardsSettings( 10, + 5, InitialReward, 1 * Constants.UnitsInWave, 4 @@ -106,7 +107,8 @@ class BlockRewardSpec extends FreeSpec with WithDomain { b16 = Range .inclusive(secondTermStart + 1, secondTermStart + rewardSettings.blockchainSettings.rewardsSettings.term) .foldLeft(Seq(b15)) { - case (prev, i) if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i).contains(i) => + case (prev, i) + if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i, modifyTerm = false).contains(i) => prev :+ mkEmptyBlockDecReward(prev.last.id(), miner) case (prev, _) => prev :+ mkEmptyBlock(prev.last.id(), miner) } @@ -115,7 +117,8 @@ class BlockRewardSpec extends FreeSpec with WithDomain { b17 = Range .inclusive(thirdTermStart + 1, thirdTermStart + rewardSettings.blockchainSettings.rewardsSettings.term) .foldLeft(Seq(b16.last)) { - case (prev, i) if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i).contains(i) => + case (prev, i) + if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i, modifyTerm = false).contains(i) => prev :+ mkEmptyBlockReward(prev.last.id(), miner, -1L) case (prev, _) => prev :+ mkEmptyBlock(prev.last.id(), miner) } @@ -124,7 +127,8 @@ class BlockRewardSpec extends FreeSpec with WithDomain { b18 = Range .inclusive(fourthTermStart + 1, fourthTermStart + rewardSettings.blockchainSettings.rewardsSettings.term) .foldLeft(Seq(b17.last)) { - case (prev, i) if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i).contains(i) => + case (prev, i) + if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i, modifyTerm = false).contains(i) => prev :+ mkEmptyBlockReward(prev.last.id(), miner, 0) case (prev, _) => prev :+ mkEmptyBlock(prev.last.id(), miner) } @@ -395,7 +399,8 @@ class BlockRewardSpec extends FreeSpec with WithDomain { b6s = Range .inclusive(BlockRewardActivationHeight + 1, BlockRewardActivationHeight + rewardSettings.blockchainSettings.rewardsSettings.term) .foldLeft(Seq(b5)) { - case (prev, i) if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i).contains(i) => + case (prev, i) + if rewardSettings.blockchainSettings.rewardsSettings.votingWindow(BlockRewardActivationHeight, i, modifyTerm = false).contains(i) => prev :+ mkEmptyBlockIncReward(prev.last.id(), if (i % 2 == 0) miner2 else miner1) case (prev, i) => prev :+ mkEmptyBlock(prev.last.id(), if (i % 2 == 0) miner2 else miner1) } @@ -447,7 +452,7 @@ class BlockRewardSpec extends FreeSpec with WithDomain { ), doubleFeaturesPeriodsAfterHeight = Int.MaxValue ), - rewardsSettings = RewardsSettings(12, 6 * Constants.UnitsInWave, 1 * Constants.UnitsInWave, 6) + rewardsSettings = RewardsSettings(12, 6, 6 * Constants.UnitsInWave, 1 * Constants.UnitsInWave, 6) ) ) @@ -481,8 +486,8 @@ class BlockRewardSpec extends FreeSpec with WithDomain { d.blockchainUpdater.height shouldBe 15 val calcSettings = calcRewardSettings.blockchainSettings.rewardsSettings - calcSettings.nearestTermEnd(4, 9) shouldBe 15 - calcSettings.nearestTermEnd(4, 10) shouldBe 15 + calcSettings.nearestTermEnd(4, 9, modifyTerm = false) shouldBe 15 + calcSettings.nearestTermEnd(4, 10, modifyTerm = false) shouldBe 15 val route = RewardApiRoute(d.blockchainUpdater) @@ -510,7 +515,7 @@ class BlockRewardSpec extends FreeSpec with WithDomain { ), doubleFeaturesPeriodsAfterHeight = Int.MaxValue ), - rewardsSettings = RewardsSettings(3, 6 * Constants.UnitsInWave, 1 * Constants.UnitsInWave, 2) + rewardsSettings = RewardsSettings(3, 2, 6 * Constants.UnitsInWave, 1 * Constants.UnitsInWave, 2) ) ) @@ -747,4 +752,611 @@ class BlockRewardSpec extends FreeSpec with WithDomain { d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackBalance + newConfigAddrReward } } + + s"NODE-815. XTN buyback and dao addresses should get 2 WAVES when full block reward >= 6 WAVES after ${BlockchainFeatures.CappedReward} activation" in { + Seq(6.waves, 7.waves).foreach { fullBlockReward => + cappedRewardFeatureTestCase( + fullBlockReward, + Some(_ => BlockRewardCalculator.MaxAddressReward), + Some(_ => BlockRewardCalculator.MaxAddressReward) + ) + } + } + + s"NODE-816. XTN buyback and dao addresses should get max((R - 2)/2, 0) WAVES when full block reward < 6 WAVES after ${BlockchainFeatures.CappedReward} activation" in { + Seq(1.waves, 2.waves, 3.waves).foreach { fullBlockReward => + cappedRewardFeatureTestCase( + fullBlockReward, + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)), + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)) + ) + } + } + + s"NODE-817. Single XTN buyback or dao address should get 2 WAVES when full block reward >= 6 WAVES after ${BlockchainFeatures.CappedReward} activation" in { + Seq(6.waves, 7.waves).foreach { fullBlockReward => + // only daoAddress defined + cappedRewardFeatureTestCase( + fullBlockReward, + Some(_ => BlockRewardCalculator.MaxAddressReward), + None + ) + + // only xtnBuybackAddress defined + cappedRewardFeatureTestCase( + fullBlockReward, + None, + Some(_ => BlockRewardCalculator.MaxAddressReward) + ) + } + } + + s"NODE-818. Single XTN buyback or dao address should get max((R - 2)/2, 0) WAVES when full block reward < 6 WAVES after ${BlockchainFeatures.CappedReward} activation" in { + Seq(1.waves, 2.waves, 3.waves).foreach { fullBlockReward => + // only daoAddress defined + cappedRewardFeatureTestCase( + fullBlockReward, + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)), + None + ) + + // only xtnBuybackAddress defined + cappedRewardFeatureTestCase( + fullBlockReward, + None, + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)) + ) + } + } + + s"NODE-820. Miner should get full block reward when daoAddress and xtnBuybackAddress are not defined after ${BlockchainFeatures.CappedReward} activation" in { + Seq(1.waves, 2.waves, 3.waves, 6.waves, 7.waves).foreach { fullBlockReward => + cappedRewardFeatureTestCase(fullBlockReward, None, None) + } + } + + s"NODE-821. Miner should get full block reward after ${BlockchainFeatures.CappedReward} activation if ${BlockchainFeatures.BlockRewardDistribution} is not activated" in { + Seq(1.waves, 2.waves, 3.waves, 6.waves, 7.waves).foreach { fullBlockReward => + // both addresses defined + cappedRewardFeatureTestCase(fullBlockReward, Some(_ => 0L), Some(_ => 0L), blockRewardDistributionActivated = false) + + // only daoAddress defined + cappedRewardFeatureTestCase(fullBlockReward, Some(_ => 0L), None, blockRewardDistributionActivated = false) + + // only xtnBuybackAddress defined + cappedRewardFeatureTestCase(fullBlockReward, None, Some(_ => 0L), blockRewardDistributionActivated = false) + + // both addresses not defined + cappedRewardFeatureTestCase(fullBlockReward, None, None, blockRewardDistributionActivated = false) + } + } + + s"NODE-822. termAfterCappedRewardFeature option should be used instead of term option after ${BlockchainFeatures.CappedReward} activation" in { + val votingInterval = 1 + val term = 10 + val termAfterCappedRewardFeature = 5 + + Seq(5 -> true, 6 -> false, 10 -> true).foreach { case (cappedRewardActivationHeight, rewardChanged) => + val settings = BlockRewardDistributionSettings + .copy(blockchainSettings = + BlockRewardDistributionSettings.blockchainSettings.copy( + functionalitySettings = BlockRewardDistributionSettings.blockchainSettings.functionalitySettings + .copy(daoAddress = None, xtnBuybackAddress = None), + rewardsSettings = BlockRewardDistributionSettings.blockchainSettings.rewardsSettings + .copy(votingInterval = votingInterval, term = term, termAfterCappedRewardFeature = termAfterCappedRewardFeature) + ) + ) + .setFeaturesHeight(BlockchainFeatures.CappedReward -> cappedRewardActivationHeight, BlockchainFeatures.BlockReward -> 1) + + withDomain(settings) { d => + val initReward = d.settings.blockchainSettings.rewardsSettings.initial + val rewardDelta = d.settings.blockchainSettings.rewardsSettings.minIncrement + val miner = d.appendBlock().sender.toAddress + (1 until cappedRewardActivationHeight - 1).foreach { _ => + val prevMinerBalance = d.balance(miner) + + d.appendBlock(d.createBlock(Block.ProtoBlockVersion, Seq.empty, rewardVote = initReward - 1)) + + d.balance(miner) shouldBe prevMinerBalance + initReward + } + + // activation height, if it == last voting interval height then reward for next block will be changed + d.appendBlock( + d.createBlock(Block.ProtoBlockVersion, Seq.empty, rewardVote = initReward - 1) + ) + + val prevMinerBalance = d.balance(miner) + val newReward = if (rewardChanged) initReward - rewardDelta else initReward + + d.appendBlock() + + d.balance(miner) shouldBe prevMinerBalance + newReward + } + } + } + + s"NODE-825, NODE-828, NODE-841. XTN buyback reward should be cancelled when ${BlockchainFeatures.CeaseXtnBuyback} activated after xtnBuybackRewardPeriod blocks starting from ${BlockchainFeatures.BlockRewardDistribution} activation height (full reward >= 6 WAVES)" in { + Seq(6.waves, 7.waves).foreach { fullBlockReward => + // daoAddress is defined + ceaseXtnBuybackFeatureTestCase( + fullBlockReward, + Some(_ => BlockRewardCalculator.MaxAddressReward), + Some(_ => BlockRewardCalculator.MaxAddressReward) + ) + + // daoAddress not defined + ceaseXtnBuybackFeatureTestCase( + fullBlockReward, + None, + Some(_ => BlockRewardCalculator.MaxAddressReward) + ) + } + } + + s"NODE-826, NODE-828, NODE-841. XTN buyback reward should be cancelled when ${BlockchainFeatures.CeaseXtnBuyback} activated after xtnBuybackRewardPeriod blocks starting from ${BlockchainFeatures.BlockRewardDistribution} activation height (full reward < 6 WAVES)" in { + Seq(1.waves, 2.waves, 3.waves).foreach { fullBlockReward => + // daoAddress is defined + ceaseXtnBuybackFeatureTestCase( + fullBlockReward, + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)), + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)) + ) + + // daoAddress not defined + ceaseXtnBuybackFeatureTestCase( + fullBlockReward, + None, + Some(r => Math.max((r - BlockRewardCalculator.GuaranteedMinerReward) / 2, 0)) + ) + } + } + + s"NODE-829. Miner should get full block reward if daoAddress and xtnBuybackAddress are not defined after ${BlockchainFeatures.CeaseXtnBuyback} activation" in { + Seq(1.waves, 2.waves, 3.waves, 6.waves, 7.waves).foreach { fullBlockReward => + ceaseXtnBuybackFeatureTestCase(fullBlockReward, None, None) + } + } + + s"NODE-830. Block reward distribution should not change after ${BlockchainFeatures.CeaseXtnBuyback} activation if ${BlockchainFeatures.CappedReward}/${BlockchainFeatures.CappedReward} and ${BlockchainFeatures.BlockRewardDistribution} not activated" in { + Seq(1.waves, 2.waves, 3.waves, 6.waves, 7.waves).foreach { fullBlockReward => + // both addresses defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, Some(r => r / 3), Some(r => r / 3), cappedRewardActivated = false) + + // only xtnBuybackAddress defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, None, Some(r => r / 3), cappedRewardActivated = false) + + // only daoAddress defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, Some(r => r / 3), None, cappedRewardActivated = false) + + // both addresses not defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, None, None, cappedRewardActivated = false) + + // both addresses defined + ceaseXtnBuybackFeatureTestCase( + fullBlockReward, + Some(_ => 0), + Some(_ => 0), + blockRewardDistributionActivated = false, + cappedRewardActivated = false + ) + + // only xtnBuybackAddress defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, None, Some(_ => 0), blockRewardDistributionActivated = false, cappedRewardActivated = false) + + // only daoAddress defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, Some(_ => 0), None, blockRewardDistributionActivated = false, cappedRewardActivated = false) + + // both addresses not defined + ceaseXtnBuybackFeatureTestCase(fullBlockReward, None, None, blockRewardDistributionActivated = false, cappedRewardActivated = false) + } + } + + s"NODE-858. Rollback on height before ${BlockchainFeatures.BlockRewardDistribution} activation should be correct" in { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + val settings = DomainPresets.ConsensusImprovements + val rewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString)), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 4 + ) + + withDomain(rewardSettings) { d => + val fullReward = d.blockchain.settings.rewardsSettings.initial + + val miner = d.appendBlock().sender.toAddress + d.appendBlock() // rollback height + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddress = d.balance(xtnBuybackAddress) + val prevMinerBalance = d.balance(miner) + + prevDaoAddressBalance shouldBe 0 + prevXtnBuybackAddress shouldBe 0 + prevMinerBalance shouldBe fullReward + + d.appendBlock() + d.appendBlock() // block reward distribution activation height + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + 2 * (fullReward / 3) + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + 2 * (fullReward / 3) + d.balance(miner) shouldBe prevMinerBalance + fullReward + 2 * (fullReward - 2 * (fullReward / 3)) + + d.appendBlock() + d.rollbackTo(2) + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + d.balance(miner) shouldBe prevMinerBalance + + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + d.balance(miner) shouldBe prevMinerBalance + fullReward + + d.appendBlock() // block reward distribution activation height + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + fullReward / 3 + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + fullReward / 3 + d.balance(miner) shouldBe prevMinerBalance + fullReward + fullReward - 2 * (fullReward / 3) + } + } + + s"NODE-859. Rollback on height after ${BlockchainFeatures.BlockRewardDistribution} activation should be correct" in { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + val settings = DomainPresets.ConsensusImprovements + val rewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString)), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 2 + ) + + withDomain(rewardSettings) { d => + val fullReward = d.blockchain.settings.rewardsSettings.initial + + val miner = d.appendBlock().sender.toAddress + d.appendBlock() // block reward distribution activation height + d.appendBlock() // rollback height + + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddress = d.balance(xtnBuybackAddress) + val prevMinerBalance = d.balance(miner) + + prevDaoAddressBalance shouldBe 2 * fullReward / 3 + prevXtnBuybackAddress shouldBe 2 * fullReward / 3 + prevMinerBalance shouldBe 2 * (fullReward - (2 * fullReward / 3)) + + d.appendBlock() + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + 2 * (fullReward / 3) + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + 2 * (fullReward / 3) + d.balance(miner) shouldBe prevMinerBalance + 2 * (fullReward - 2 * (fullReward / 3)) + + d.appendBlock() + d.rollbackTo(3) + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + d.balance(miner) shouldBe prevMinerBalance + + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + fullReward / 3 + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + fullReward / 3 + d.balance(miner) shouldBe prevMinerBalance + fullReward - 2 * (fullReward / 3) + } + } + + s"NODE-860. Rollback on height before ${BlockchainFeatures.CappedReward} activation should be correct" in { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + val settings = DomainPresets.ConsensusImprovements + val rewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString)), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 2, + BlockchainFeatures.CappedReward -> 5 + ) + + withDomain(rewardSettings) { d => + val fullReward = d.blockchain.settings.rewardsSettings.initial + + val miner = d.appendBlock().sender.toAddress + d.appendBlock() // block reward distribution activation height + d.appendBlock() // rollback height + + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddress = d.balance(xtnBuybackAddress) + val prevMinerBalance = d.balance(miner) + + prevDaoAddressBalance shouldBe 2 * fullReward / 3 + prevXtnBuybackAddress shouldBe 2 * fullReward / 3 + prevMinerBalance shouldBe 2 * (fullReward - (2 * fullReward / 3)) + + d.appendBlock() + d.appendBlock() // capped reward activation height + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + fullReward / 3 + 2 * BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + fullReward / 3 + 2 * BlockRewardCalculator.MaxAddressReward + d.balance(miner) shouldBe prevMinerBalance + fullReward - 2 * (fullReward / 3) + 2 * (fullReward - 2 * BlockRewardCalculator.MaxAddressReward) + + d.appendBlock() + d.rollbackTo(3) + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + d.balance(miner) shouldBe prevMinerBalance + + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + fullReward / 3 + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + fullReward / 3 + d.balance(miner) shouldBe prevMinerBalance + fullReward - 2 * (fullReward / 3) + + d.appendBlock() // capped reward activation height + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + fullReward / 3 + BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + fullReward / 3 + BlockRewardCalculator.MaxAddressReward + d.balance(miner) shouldBe prevMinerBalance + fullReward - 2 * (fullReward / 3) + fullReward - 2 * BlockRewardCalculator.MaxAddressReward + } + } + + s"NODE-861. Rollback on height after ${BlockchainFeatures.CappedReward} activation should be correct" in { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + val settings = DomainPresets.ConsensusImprovements + val rewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString)), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 2, + BlockchainFeatures.CappedReward -> 2 + ) + + withDomain(rewardSettings) { d => + val fullReward = d.blockchain.settings.rewardsSettings.initial + + val miner = d.appendBlock().sender.toAddress + d.appendBlock() // block reward distribution and capped reward activation height + d.appendBlock() // rollback height + + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddress = d.balance(xtnBuybackAddress) + val prevMinerBalance = d.balance(miner) + + prevDaoAddressBalance shouldBe 2 * BlockRewardCalculator.MaxAddressReward + prevXtnBuybackAddress shouldBe 2 * BlockRewardCalculator.MaxAddressReward + prevMinerBalance shouldBe 2 * (fullReward - 2 * BlockRewardCalculator.MaxAddressReward) + + d.appendBlock() + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + 2 * BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + 2 * BlockRewardCalculator.MaxAddressReward + d.balance(miner) shouldBe prevMinerBalance + 2 * (fullReward - 2 * BlockRewardCalculator.MaxAddressReward) + + d.appendBlock() + d.rollbackTo(3) + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + d.balance(miner) shouldBe prevMinerBalance + + d.appendBlock() + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + BlockRewardCalculator.MaxAddressReward + d.balance(miner) shouldBe prevMinerBalance + fullReward - 2 * BlockRewardCalculator.MaxAddressReward + } + } + + s"NODE-862. Rollback on height before ${BlockchainFeatures.CeaseXtnBuyback} activation should be correct" in { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + val settings = DomainPresets.ConsensusImprovements + val rewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString), xtnBuybackRewardPeriod = 3), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 2, + BlockchainFeatures.CappedReward -> 2, + BlockchainFeatures.CeaseXtnBuyback -> 5 + ) + + withDomain(rewardSettings) { d => + val fullReward = d.blockchain.settings.rewardsSettings.initial + + val miner = d.appendBlock().sender.toAddress + d.appendBlock() // block reward distribution and capped reward activation height + d.appendBlock() // rollback height + + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddress = d.balance(xtnBuybackAddress) + val prevMinerBalance = d.balance(miner) + + prevDaoAddressBalance shouldBe 2 * BlockRewardCalculator.MaxAddressReward + prevXtnBuybackAddress shouldBe 2 * BlockRewardCalculator.MaxAddressReward + prevMinerBalance shouldBe 2 * (fullReward - 2 * BlockRewardCalculator.MaxAddressReward) + + d.appendBlock() // last block of XTN buyback reward period + d.appendBlock() // cease XTN buyback activation height + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + 2 * BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + BlockRewardCalculator.MaxAddressReward + d.balance( + miner + ) shouldBe prevMinerBalance + fullReward - 2 * BlockRewardCalculator.MaxAddressReward + fullReward - BlockRewardCalculator.MaxAddressReward + + d.appendBlock() + d.rollbackTo(3) + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + d.balance(miner) shouldBe prevMinerBalance + + d.appendBlock() // last block of XTN buyback reward period + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + BlockRewardCalculator.MaxAddressReward + d.balance(miner) shouldBe prevMinerBalance + fullReward - 2 * BlockRewardCalculator.MaxAddressReward + + d.appendBlock() // cease XTN buyback activation height + + d.balance(daoAddress) shouldBe prevDaoAddressBalance + 2 * BlockRewardCalculator.MaxAddressReward + d.balance(xtnBuybackAddress) shouldBe prevXtnBuybackAddress + BlockRewardCalculator.MaxAddressReward + d.balance( + miner + ) shouldBe prevMinerBalance + fullReward - 2 * BlockRewardCalculator.MaxAddressReward + fullReward - BlockRewardCalculator.MaxAddressReward + } + } + + private def ceaseXtnBuybackFeatureTestCase( + fullBlockReward: Long, + daoAddressRewardF: Option[Long => Long], + xtnBuybackAddressRewardF: Option[Long => Long], + blockRewardDistributionActivated: Boolean = true, + cappedRewardActivated: Boolean = true + ): Unit = { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + + val settings = DomainPresets.ConsensusImprovements + val maybeBlockRewardDistributionHeight = 1 + val blockRewardDistributionActivationHeight = if (blockRewardDistributionActivated) maybeBlockRewardDistributionHeight else Int.MaxValue - 100 + val cappedRewardActivationHeight = if (cappedRewardActivated) maybeBlockRewardDistributionHeight else Int.MaxValue - 100 + val xtnBuybackRewardPeriod = 5 + + // feature activation before, at and after last block with XTN buyback address reward + (xtnBuybackRewardPeriod - maybeBlockRewardDistributionHeight - 1 to xtnBuybackRewardPeriod - maybeBlockRewardDistributionHeight + 3) + .foreach { ceaseXtnBuybackActivationHeight => + val modifiedRewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = fullBlockReward), + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy( + daoAddress = Some(daoAddress.toString).filter(_ => daoAddressRewardF.isDefined), + xtnBuybackAddress = Some(xtnBuybackAddress.toString).filter(_ => xtnBuybackAddressRewardF.isDefined), + xtnBuybackRewardPeriod = xtnBuybackRewardPeriod + ) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> blockRewardDistributionActivationHeight, + BlockchainFeatures.CappedReward -> cappedRewardActivationHeight, + BlockchainFeatures.CeaseXtnBuyback -> ceaseXtnBuybackActivationHeight + ) + + withDomain(modifiedRewardSettings) { d => + val firstBlock = d.appendBlock() + val miner = firstBlock.sender.toAddress + + (1 to maybeBlockRewardDistributionHeight + xtnBuybackRewardPeriod + 1).foreach { idx => + val prevMinerBalance = d.balance(miner) + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddressBalance = d.balance(xtnBuybackAddress) + + d.appendBlock() + + val daoAddressReward = d.balance(daoAddress) - prevDaoAddressBalance + val xtnBuybackAddressReward = d.balance(xtnBuybackAddress) - prevXtnBuybackAddressBalance + + daoAddressReward shouldBe daoAddressRewardF.map(_.apply(fullBlockReward)).getOrElse(0L) + val expectedXtnBuybackAddressReward = + if (ceaseXtnBuybackActivationHeight <= idx + 1 && blockRewardDistributionActivationHeight + xtnBuybackRewardPeriod <= idx + 1) 0 + else xtnBuybackAddressRewardF.map(_.apply(fullBlockReward)).getOrElse(0L) + xtnBuybackAddressReward shouldBe expectedXtnBuybackAddressReward + d.balance( + miner + ) - prevMinerBalance shouldBe d.settings.blockchainSettings.rewardsSettings.initial - daoAddressReward - xtnBuybackAddressReward + } + } + } + } + + private def cappedRewardFeatureTestCase( + fullBlockReward: Long, + daoAddressRewardF: Option[Long => Long], + xtnBuybackAddressRewardF: Option[Long => Long], + blockRewardDistributionActivated: Boolean = true + ) = { + val daoAddress = TxHelpers.address(1) + val xtnBuybackAddress = TxHelpers.address(2) + + val settings = DomainPresets.ConsensusImprovements + val blockRewardDistributionHeight = if (blockRewardDistributionActivated) 0 else Int.MaxValue + val modifiedRewardSettings = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = fullBlockReward), + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy( + daoAddress = Some(daoAddress.toString).filter(_ => daoAddressRewardF.isDefined), + xtnBuybackAddress = Some(xtnBuybackAddress.toString).filter(_ => xtnBuybackAddressRewardF.isDefined) + ) + ) + ) + .setFeaturesHeight(BlockchainFeatures.BlockRewardDistribution -> blockRewardDistributionHeight, BlockchainFeatures.CappedReward -> 3) + + withDomain(modifiedRewardSettings) { d => + val firstBlock = d.appendBlock() + val miner = firstBlock.sender.toAddress + + d.balance(daoAddress) shouldBe 0 + d.balance(xtnBuybackAddress) shouldBe 0 + d.balance(miner) shouldBe 0 + + d.appendBlock() + + val prevDaoAddressBalance = d.balance(daoAddress) + val prevXtnBuybackAddressBalance = d.balance(xtnBuybackAddress) + val prevMinerBalance = d.balance(miner) + + prevDaoAddressBalance shouldBe (if (daoAddressRewardF.isDefined && blockRewardDistributionActivated) fullBlockReward / 3 else 0L) + prevXtnBuybackAddressBalance shouldBe (if (xtnBuybackAddressRewardF.isDefined && blockRewardDistributionActivated) fullBlockReward / 3 else 0L) + prevMinerBalance shouldBe fullBlockReward - prevDaoAddressBalance - prevXtnBuybackAddressBalance + + d.appendBlock() + + val daoAddressReward = d.balance(daoAddress) - prevDaoAddressBalance + val xtnBuybackAddressReward = d.balance(xtnBuybackAddress) - prevXtnBuybackAddressBalance + val minerReward = d.balance(miner) - prevMinerBalance + + daoAddressReward shouldBe daoAddressRewardF.map(_.apply(fullBlockReward)).getOrElse(0L) + xtnBuybackAddressReward shouldBe xtnBuybackAddressRewardF.map(_.apply(fullBlockReward)).getOrElse(0L) + minerReward shouldBe fullBlockReward - daoAddressReward - xtnBuybackAddressReward + } + } } diff --git a/node/src/test/scala/com/wavesplatform/history/Domain.scala b/node/src/test/scala/com/wavesplatform/history/Domain.scala index 58152d1cdbd..72250f12dde 100644 --- a/node/src/test/scala/com/wavesplatform/history/Domain.scala +++ b/node/src/test/scala/com/wavesplatform/history/Domain.scala @@ -24,7 +24,7 @@ import com.wavesplatform.transaction.{BlockchainUpdater, *} import com.wavesplatform.utils.{EthEncoding, SystemTime} import com.wavesplatform.utx.UtxPoolImpl import com.wavesplatform.wallet.Wallet -import com.wavesplatform.{Application, TestValues, crypto, database} +import com.wavesplatform.{Application, TestValues, crypto} import monix.execution.Scheduler.Implicits.global import org.iq80.leveldb.DB import org.scalatest.matchers.should.Matchers.* @@ -340,16 +340,12 @@ case class Domain(db: DB, blockchainUpdater: BlockchainUpdaterImpl, levelDBWrite val blocksApi: CommonBlocksApi = { def loadBlockMetaAt(db: DB, blockchainUpdater: BlockchainUpdaterImpl)(height: Int): Option[BlockMeta] = - blockchainUpdater.liquidBlockMeta.filter(_ => blockchainUpdater.height == height).orElse(db.get(Keys.blockMetaAt(Height(height)))) + Application.loadBlockMetaAt(db, blockchainUpdater)(height) def loadBlockInfoAt(db: DB, blockchainUpdater: BlockchainUpdaterImpl)( height: Int ): Option[(BlockMeta, Seq[(TxMeta, Transaction)])] = - loadBlockMetaAt(db, blockchainUpdater)(height).map { meta => - meta -> blockchainUpdater - .liquidTransactions(meta.id) - .getOrElse(db.readOnly(ro => database.loadTransactions(Height(height), ro))) - } + Application.loadBlockInfoAt(db, blockchainUpdater)(height) CommonBlocksApi(blockchainUpdater, loadBlockMetaAt(db, blockchainUpdater), loadBlockInfoAt(db, blockchainUpdater)) } diff --git a/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala b/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala index 6927dca4110..7219b146893 100644 --- a/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala +++ b/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala @@ -1,17 +1,22 @@ package com.wavesplatform.http import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers.Accept +import akka.http.scaladsl.server.Route import com.wavesplatform.TestWallet import com.wavesplatform.api.BlockMeta import com.wavesplatform.api.common.CommonBlocksApi import com.wavesplatform.api.http.ApiError.TooBigArrayAllocation -import com.wavesplatform.api.http.{BlocksApiRoute, RouteTimeout} +import com.wavesplatform.api.http.{BlocksApiRoute, CustomJson, RouteTimeout} import com.wavesplatform.block.serialization.BlockHeaderSerializer import com.wavesplatform.block.{Block, BlockHeader} import com.wavesplatform.common.state.ByteStr import com.wavesplatform.db.WithDomain +import com.wavesplatform.features.BlockchainFeatures import com.wavesplatform.lagonaki.mocks.TestBlock -import com.wavesplatform.state.Blockchain +import com.wavesplatform.state.{BlockRewardCalculator, Blockchain} +import com.wavesplatform.test.* +import com.wavesplatform.test.DomainPresets.* import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.utils.{SharedSchedulerMixin, SystemTime} import monix.reactive.Observable @@ -37,7 +42,13 @@ class BlocksApiRouteSpec private val testBlock2 = TestBlock.create(Nil, Block.ProtoBlockVersion) private val testBlock1Json = testBlock1.json() ++ Json.obj("height" -> 1, "totalFee" -> 0L) - private val testBlock2Json = testBlock2.json() ++ Json.obj("height" -> 2, "totalFee" -> 0L, "reward" -> 5, "VRF" -> testBlock2.id().toString) + private val testBlock2Json = testBlock2.json() ++ Json.obj( + "height" -> 2, + "totalFee" -> 0L, + "reward" -> 5, + "rewardShares" -> Json.obj(testBlock2.header.generator.toAddress.toString -> 5), + "VRF" -> testBlock2.id().toString + ) private val testBlock1HeaderJson = BlockHeaderSerializer.toJson(testBlock1.header, testBlock1.bytes().length, 0, testBlock1.signature) ++ Json.obj( "height" -> 1, @@ -45,14 +56,16 @@ class BlocksApiRouteSpec ) private val testBlock2HeaderJson = BlockHeaderSerializer.toJson(testBlock2.header, testBlock2.bytes().length, 0, testBlock2.signature) ++ Json.obj( - "height" -> 2, - "totalFee" -> 0L, - "reward" -> 5, - "VRF" -> testBlock2.id().toString + "height" -> 2, + "totalFee" -> 0L, + "reward" -> 5, + "rewardShares" -> Json.obj(testBlock2.header.generator.toAddress.toString -> 5), + "VRF" -> testBlock2.id().toString ) private val testBlock1Meta = BlockMeta.fromBlock(testBlock1, 1, 0L, None, None) - private val testBlock2Meta = BlockMeta.fromBlock(testBlock2, 2, 0L, Some(5), Some(testBlock2.id())) + private val testBlock2Meta = + BlockMeta.fromBlock(testBlock2, 2, 0L, Some(5), Some(testBlock2.id())).copy(rewardShares = Seq(testBlock2.header.generator.toAddress -> 5)) private val invalidBlockId = ByteStr(new Array[Byte](32)) (blocksApi.block _).expects(invalidBlockId).returning(None).anyNumberOfTimes() @@ -244,7 +257,7 @@ class BlocksApiRouteSpec def metaAt(height: Int): Option[BlockMeta] = if (height >= 1 && height <= 3) - Some(BlockMeta(blocks(height - 1).header, ByteStr.empty, None, 1, 0, 0, 0, None, None)) + Some(BlockMeta(blocks(height - 1).header, ByteStr.empty, None, 1, 0, 0, 0, None, Seq.empty, None)) else None val blocksApi = CommonBlocksApi(blockchain, metaAt, _ => None) @@ -282,7 +295,7 @@ class BlocksApiRouteSpec if (height < 1 || height > blocks.size) None else { val block = blocks(height - 1) - Some(BlockMeta(block.header, block.signature, None, height, 1, 0, 0L, None, None)) + Some(BlockMeta(block.header, block.signature, None, height, 1, 0, 0L, None, Seq.empty, None)) } } blocksApi @@ -350,4 +363,116 @@ class BlocksApiRouteSpec } } } + + "NODE-857. Block meta response should contain correct rewardShares field" in { + val daoAddress = TxHelpers.address(3) + val xtnBuybackAddress = TxHelpers.address(4) + + val settings = DomainPresets.ConsensusImprovements + val settingsWithFeatures = settings + .copy(blockchainSettings = + settings.blockchainSettings.copy( + functionalitySettings = settings.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString), xtnBuybackRewardPeriod = 1), + rewardsSettings = settings.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 3, + BlockchainFeatures.CappedReward -> 4, + BlockchainFeatures.CeaseXtnBuyback -> 5 + ) + + withDomain(settingsWithFeatures) { d => + val route = new BlocksApiRoute(d.settings.restAPISettings, d.blocksApi, SystemTime, new RouteTimeout(60.seconds)(sharedScheduler)).route + + val miner = d.appendBlock().sender.toAddress + + // BlockRewardDistribution activated + val configAddrReward3 = d.blockchain.settings.rewardsSettings.initial / 3 + val minerReward3 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward3 + + // CappedReward activated + val configAddrReward4 = BlockRewardCalculator.MaxAddressReward + val minerReward4 = d.blockchain.settings.rewardsSettings.initial - 2 * configAddrReward4 + + // CeaseXTNBuyback activated with expired XTN buyback reward period + val configAddrReward5 = BlockRewardCalculator.MaxAddressReward + val minerReward5 = d.blockchain.settings.rewardsSettings.initial - configAddrReward5 + + val heightToResult = Map( + 2 -> Map(miner.toString -> d.blockchain.settings.rewardsSettings.initial), + 3 -> Map(miner.toString -> minerReward3, daoAddress.toString -> configAddrReward3, xtnBuybackAddress.toString -> configAddrReward3), + 4 -> Map(miner.toString -> minerReward4, daoAddress.toString -> configAddrReward4, xtnBuybackAddress.toString -> configAddrReward4), + 5 -> Map(miner.toString -> minerReward5, daoAddress.toString -> configAddrReward5) + ) + + val heightToBlock = (2 to 5).map { h => + val block = d.appendBlock() + + Seq(true, false).foreach { lsf => + checkRewardSharesBlock(route, "/last", heightToResult(h), lsf) + checkRewardSharesBlock(route, "/headers/last", heightToResult(h), lsf) + } + + h -> block + }.toMap + d.appendBlock() + + Seq(true, false).foreach { lsf => + heightToResult.foreach { case (h, expectedResult) => + checkRewardSharesBlock(route, s"/at/$h", expectedResult, lsf) + checkRewardSharesBlock(route, s"/headers/at/$h", expectedResult, lsf) + checkRewardSharesBlock(route, s"/headers/${heightToBlock(h).id()}", expectedResult, lsf) + checkRewardSharesBlock(route, s"/${heightToBlock(h).id()}", expectedResult, lsf) + } + checkRewardSharesBlockSeq(route, "/seq", 2, 5, heightToResult, lsf) + checkRewardSharesBlockSeq(route, "/headers/seq", 2, 5, heightToResult, lsf) + checkRewardSharesBlockSeq(route, s"/address/${miner.toString}", 2, 5, heightToResult, lsf) + } + } + } + + private def checkRewardSharesBlock(route: Route, path: String, expected: Map[String, Long], largeSignificandFormat: Boolean) = { + val maybeWithLsf = + if (largeSignificandFormat) + Get(routePath(path)) ~> Accept(CustomJson.jsonWithNumbersAsStrings) + else Get(routePath(path)) + + maybeWithLsf ~> route ~> check { + (responseAs[JsObject] \ "rewardShares") + .as[JsObject] + .value + .view + .mapValues { rewardJson => if (largeSignificandFormat) rewardJson.as[String].toLong else rewardJson.as[Long] } + .toMap shouldBe expected + } + } + + private def checkRewardSharesBlockSeq( + route: Route, + prefix: String, + start: Int, + end: Int, + heightToResult: Map[Int, Map[String, Long]], + largeSignificandFormat: Boolean + ) = { + val maybeWithLsf = + if (largeSignificandFormat) + Get(routePath(s"$prefix/$start/$end")) ~> Accept(CustomJson.jsonWithNumbersAsStrings) + else Get(routePath(s"$prefix/$start/$end")) + maybeWithLsf ~> route ~> check { + responseAs[Seq[JsObject]] + .zip(start to end) + .map { case (obj, h) => + h -> (obj \ "rewardShares") + .as[JsObject] + .value + .view + .mapValues { rewardJson => if (largeSignificandFormat) rewardJson.as[String].toLong else rewardJson.as[Long] } + .toMap + } + .toMap shouldBe heightToResult + } + } } diff --git a/node/src/test/scala/com/wavesplatform/http/RewardApiRouteSpec.scala b/node/src/test/scala/com/wavesplatform/http/RewardApiRouteSpec.scala index b96e759a9f5..eaa56c17aed 100644 --- a/node/src/test/scala/com/wavesplatform/http/RewardApiRouteSpec.scala +++ b/node/src/test/scala/com/wavesplatform/http/RewardApiRouteSpec.scala @@ -3,9 +3,10 @@ package com.wavesplatform.http import com.wavesplatform.account.Address import com.wavesplatform.api.http.RewardApiRoute import com.wavesplatform.db.WithDomain +import com.wavesplatform.features.BlockchainFeatures import com.wavesplatform.history.Domain import com.wavesplatform.settings.WavesSettings -import com.wavesplatform.test.DomainPresets.RideV6 +import com.wavesplatform.test.DomainPresets.* import com.wavesplatform.transaction.TxHelpers import play.api.libs.json.JsValue @@ -36,18 +37,87 @@ class RewardApiRouteSpec extends RouteSpec("/blockchain") with WithDomain { ) ) - routePath("/rewards") in { + val blockRewardActivationHeight = 1 + val settingsWithVoteParams: WavesSettings = ConsensusImprovements + .copy(blockchainSettings = + ConsensusImprovements.blockchainSettings + .copy(rewardsSettings = + ConsensusImprovements.blockchainSettings.rewardsSettings.copy(term = 100, termAfterCappedRewardFeature = 50, votingInterval = 10) + ) + ) + .setFeaturesHeight(BlockchainFeatures.BlockReward -> blockRewardActivationHeight, BlockchainFeatures.CappedReward -> 3) + + routePath("/rewards (NODE-855)") in { checkWithSettings(settingsWithoutAddresses) checkWithSettings(settingsWithOnlyDaoAddress) checkWithSettings(settingsWithOnlyXtnBuybackAddress) checkWithSettings(settingsWithBothAddresses) + + withDomain(settingsWithVoteParams) { d => + d.appendBlock() + d.appendBlock() + + checkVoteParams( + d, + d.blockchain.settings.rewardsSettings.term, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.term - d.blockchain.settings.rewardsSettings.votingInterval, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.term - 1 + ) + + d.appendBlock() // activation height, vote parameters should be changed + checkVoteParams( + d, + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - d.blockchain.settings.rewardsSettings.votingInterval, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - 1 + ) + + d.appendBlock() + checkVoteParams( + d, + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - d.blockchain.settings.rewardsSettings.votingInterval, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - 1 + ) + } } - routePath("/rewards/{height}") in { + routePath("/rewards/{height} (NODE-856)") in { checkWithSettings(settingsWithoutAddresses, Some(1)) checkWithSettings(settingsWithOnlyDaoAddress, Some(1)) checkWithSettings(settingsWithOnlyXtnBuybackAddress, Some(1)) checkWithSettings(settingsWithBothAddresses, Some(1)) + + withDomain(settingsWithVoteParams) { d => + d.appendBlock() + d.appendBlock() + d.appendBlock() // activation height, vote parameters should be changed + d.appendBlock() + + checkVoteParams( + d, + d.blockchain.settings.rewardsSettings.term, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.term - d.blockchain.settings.rewardsSettings.votingInterval, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.term - 1, + Some(2) + ) + + checkVoteParams( + d, + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - d.blockchain.settings.rewardsSettings.votingInterval, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - 1, + Some(3) + ) + + checkVoteParams( + d, + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - d.blockchain.settings.rewardsSettings.votingInterval, + blockRewardActivationHeight + d.blockchain.settings.rewardsSettings.termAfterCappedRewardFeature - 1, + Some(4) + ) + } } private def checkWithSettings(settings: WavesSettings, height: Option[Int] = None) = @@ -71,9 +141,9 @@ class RewardApiRouteSpec extends RouteSpec("/blockchain") with WithDomain { | "currentReward" : ${d.blockchain.settings.rewardsSettings.initial}, | "minIncrement" : ${d.blockchain.settings.rewardsSettings.minIncrement}, | "term" : ${d.blockchain.settings.rewardsSettings.term}, - | "nextCheck" : ${d.blockchain.settings.rewardsSettings.nearestTermEnd(0, 1)}, + | "nextCheck" : ${d.blockchain.settings.rewardsSettings.nearestTermEnd(0, 1, modifyTerm = false)}, | "votingIntervalStart" : ${d.blockchain.settings.rewardsSettings - .nearestTermEnd(0, 1) - d.blockchain.settings.rewardsSettings.votingInterval + 1}, + .nearestTermEnd(0, 1, modifyTerm = false) - d.blockchain.settings.rewardsSettings.votingInterval + 1}, | "votingInterval" : ${d.blockchain.settings.rewardsSettings.votingInterval}, | "votingThreshold" : ${d.blockchain.settings.rewardsSettings.votingInterval / 2 + 1}, | "votes" : { @@ -84,4 +154,16 @@ class RewardApiRouteSpec extends RouteSpec("/blockchain") with WithDomain { | "xtnBuybackAddress" : ${d.blockchain.settings.functionalitySettings.xtnBuybackAddress.fold("null")(addr => s"\"$addr\"")} |} |""".stripMargin + + private def checkVoteParams(d: Domain, expectedTerm: Int, expectedVotingIntervalStart: Int, expectedNextCheck: Int, height: Option[Int] = None) = { + val route = RewardApiRoute(d.blockchain).route + val pathSuffix = height.fold("")(h => s"/$h") + + Get(routePath(s"/rewards$pathSuffix")) ~> route ~> check { + val response = responseAs[JsValue] + (response \ "term").as[Int] shouldBe expectedTerm + (response \ "votingIntervalStart").as[Int] shouldBe expectedVotingIntervalStart + (response \ "nextCheck").as[Int] shouldBe expectedNextCheck + } + } } diff --git a/node/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala b/node/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala index 412d3f9a6be..d3d17179ed1 100644 --- a/node/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala +++ b/node/src/test/scala/com/wavesplatform/settings/BlockchainSettingsSpecification.scala @@ -8,48 +8,53 @@ import scala.concurrent.duration._ class BlockchainSettingsSpecification extends FlatSpec { "BlockchainSettings" should "read custom values" in { - val config = loadConfig(ConfigFactory.parseString("""waves { - | directory = "/waves" - | data-directory = "/waves/data" - | blockchain { - | type = CUSTOM - | custom { - | address-scheme-character = "C" - | functionality { - | feature-check-blocks-period = 10000 - | blocks-for-feature-activation = 9000 - | generation-balance-depth-from-50-to-1000-after-height = 4 - | block-version-3-after-height = 18 - | pre-activated-features { - | 19 = 100 - | 20 = 200 - | } - | double-features-periods-after-height = 21 - | max-transaction-time-back-offset = 55s - | max-transaction-time-forward-offset = 12d - | lease-expiration = 1000000 - | } - | rewards { - | term = 100000 - | initial = 600000000 - | min-increment = 50000000 - | voting-interval = 10000 - | } - | genesis { - | timestamp = 1460678400000 - | block-timestamp = 1460678400000 - | signature = "BASE58BLKSGNATURE" - | initial-balance = 100000000000000 - | initial-base-target = 153722867 - | average-block-delay = 60s - | transactions = [ - | {recipient = "BASE58ADDRESS1", amount = 50000000000001}, - | {recipient = "BASE58ADDRESS2", amount = 49999999999999} - | ] - | } - | } - | } - |}""".stripMargin)) + val config = loadConfig( + ConfigFactory.parseString( + """waves { + | directory = "/waves" + | data-directory = "/waves/data" + | blockchain { + | type = CUSTOM + | custom { + | address-scheme-character = "C" + | functionality { + | feature-check-blocks-period = 10000 + | blocks-for-feature-activation = 9000 + | generation-balance-depth-from-50-to-1000-after-height = 4 + | block-version-3-after-height = 18 + | pre-activated-features { + | 19 = 100 + | 20 = 200 + | } + | double-features-periods-after-height = 21 + | max-transaction-time-back-offset = 55s + | max-transaction-time-forward-offset = 12d + | lease-expiration = 1000000 + | } + | rewards { + | term = 100000 + | term-after-capped-reward-feature = 50000 + | initial = 600000000 + | min-increment = 50000000 + | voting-interval = 10000 + | } + | genesis { + | timestamp = 1460678400000 + | block-timestamp = 1460678400000 + | signature = "BASE58BLKSGNATURE" + | initial-balance = 100000000000000 + | initial-base-target = 153722867 + | average-block-delay = 60s + | transactions = [ + | {recipient = "BASE58ADDRESS1", amount = 50000000000001}, + | {recipient = "BASE58ADDRESS2", amount = 49999999999999} + | ] + | } + | } + | } + |}""".stripMargin + ) + ) val settings = BlockchainSettings.fromRootConfig(config) settings.addressSchemeCharacter should be('C') @@ -64,6 +69,7 @@ class BlockchainSettingsSpecification extends FlatSpec { settings.rewardsSettings.initial should be(600000000) settings.rewardsSettings.minIncrement should be(50000000) settings.rewardsSettings.term should be(100000) + settings.rewardsSettings.termAfterCappedRewardFeature should be(50000) settings.rewardsSettings.votingInterval should be(10000) settings.genesisSettings.blockTimestamp should be(1460678400000L) settings.genesisSettings.timestamp should be(1460678400000L) @@ -77,13 +83,17 @@ class BlockchainSettingsSpecification extends FlatSpec { } it should "read testnet settings" in { - val config = loadConfig(ConfigFactory.parseString("""waves { - | directory = "/waves" - | data-directory = "/waves/data" - | blockchain { - | type = TESTNET - | } - |}""".stripMargin)) + val config = loadConfig( + ConfigFactory.parseString( + """waves { + | directory = "/waves" + | data-directory = "/waves/data" + | blockchain { + | type = TESTNET + | } + |}""".stripMargin + ) + ) val settings = BlockchainSettings.fromRootConfig(config) settings.addressSchemeCharacter should be('T') @@ -94,6 +104,7 @@ class BlockchainSettingsSpecification extends FlatSpec { settings.rewardsSettings.initial should be(600000000) settings.rewardsSettings.minIncrement should be(50000000) settings.rewardsSettings.term should be(100000) + settings.rewardsSettings.termAfterCappedRewardFeature should be(50000) settings.rewardsSettings.votingInterval should be(10000) settings.genesisSettings.blockTimestamp should be(1460678400000L) settings.genesisSettings.timestamp should be(1478000000000L) @@ -114,13 +125,17 @@ class BlockchainSettingsSpecification extends FlatSpec { } it should "read mainnet settings" in { - val config = loadConfig(ConfigFactory.parseString("""waves { - | directory = "/waves" - | data-directory = "/waves/data" - | blockchain { - | type = MAINNET - | } - |}""".stripMargin)) + val config = loadConfig( + ConfigFactory.parseString( + """waves { + | directory = "/waves" + | data-directory = "/waves/data" + | blockchain { + | type = MAINNET + | } + |}""".stripMargin + ) + ) val settings = BlockchainSettings.fromRootConfig(config) settings.addressSchemeCharacter should be('W') @@ -130,6 +145,7 @@ class BlockchainSettingsSpecification extends FlatSpec { settings.rewardsSettings.initial should be(600000000) settings.rewardsSettings.minIncrement should be(50000000) settings.rewardsSettings.term should be(100000) + settings.rewardsSettings.termAfterCappedRewardFeature should be(50000) settings.rewardsSettings.votingInterval should be(10000) settings.genesisSettings.blockTimestamp should be(1460678400000L) settings.genesisSettings.timestamp should be(1465742577614L) diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/ContextFunctionsTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/ContextFunctionsTest.scala index 040fb361614..19939375368 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/ContextFunctionsTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/ContextFunctionsTest.scala @@ -1,6 +1,7 @@ package com.wavesplatform.state.diffs.smart.predef import cats.syntax.semigroup.* +import com.wavesplatform.account.Address import com.wavesplatform.block.Block import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.{Base58, Base64, EitherExt2} @@ -31,6 +32,7 @@ import com.wavesplatform.transaction.smart.SetScriptTransaction import com.wavesplatform.transaction.smart.script.ScriptCompiler import com.wavesplatform.transaction.{TxHelpers, TxVersion} import com.wavesplatform.utils.* +import org.scalatest.Assertion import shapeless.Coproduct class ContextFunctionsTest extends PropSpec with WithDomain with EthHelpers { @@ -521,30 +523,11 @@ class ContextFunctionsTest extends PropSpec with WithDomain with EthHelpers { .setFeaturesHeight(BlockchainFeatures.BlockRewardDistribution -> 4) Seq("value(blockInfoByHeight(2)).rewards", "lastBlock.rewards").foreach { getRewardsCode => - def script(version: StdLibVersion): String = - s""" - |{-# STDLIB_VERSION ${version.id} #-} - |{-# CONTENT_TYPE DAPP #-} - |{-# SCRIPT_TYPE ACCOUNT #-} - | - | @Callable(i) - | func foo() = { - | let r = $getRewardsCode - | [ - | IntegerEntry("size", r.size()), - | BinaryEntry("addr1", r[0]._1.bytes), - | IntegerEntry("reward1", r[0]._2), - | BinaryEntry("addr2", r[1]._1.bytes), - | IntegerEntry("reward2", r[1]._2), - | BinaryEntry("addr3", r[2]._1.bytes), - | IntegerEntry("reward3", r[2]._2) - | ] - | } - |""".stripMargin - - Seq(V4, V5, V6).foreach(v => TestCompiler(v).compile(script(v)) should produce("Undefined field `rewards` of variable of type `BlockInfo`")) + Seq(V4, V5, V6).foreach(v => + TestCompiler(v).compile(blockInfoScript(v, getRewardsCode)) should produce("Undefined field `rewards` of variable of type `BlockInfo`") + ) - val compiledDapp = TestCompiler(V7).compile(script(V7)) + val compiledDapp = TestCompiler(V7).compile(blockInfoScript(V7, getRewardsCode)) compiledDapp should beRight withDomain(rideV6Settings, balances = AddrWithBalance.enoughBalances(invoker, dApp)) { d => @@ -577,6 +560,81 @@ class ContextFunctionsTest extends PropSpec with WithDomain with EthHelpers { } } + property( + s"NODE-842, NODE-843. blockInfoByHeight(height) should return correct rewards after ${BlockchainFeatures.CappedReward} activation" + ) { + val invoker = TxHelpers.signer(1) + val dApp = TxHelpers.signer(2) + val daoAddress = TxHelpers.address(3) + val xtnBuybackAddress = TxHelpers.address(4) + + val settings = ConsensusImprovements + .copy(blockchainSettings = + ConsensusImprovements.blockchainSettings.copy( + functionalitySettings = ConsensusImprovements.blockchainSettings.functionalitySettings + .copy(daoAddress = Some(daoAddress.toString), xtnBuybackAddress = Some(xtnBuybackAddress.toString)), + rewardsSettings = ConsensusImprovements.blockchainSettings.rewardsSettings.copy(initial = BlockRewardCalculator.FullRewardInit + 1.waves) + ) + ) + .setFeaturesHeight(BlockchainFeatures.BlockRewardDistribution -> 3, BlockchainFeatures.CappedReward -> 5) + + val dAppBeforeBlockRewardDistribution = TestCompiler(V7).compileContract(blockInfoScript(V7, "value(blockInfoByHeight(2)).rewards")) + val dAppAfterBlockRewardDistribution = TestCompiler(V7).compileContract(blockInfoScript(V7, "value(blockInfoByHeight(3)).rewards")) + val dAppAfterCappedReward = TestCompiler(V7).compileContract(blockInfoScript(V7, "value(blockInfoByHeight(5)).rewards")) + + withDomain(settings, balances = AddrWithBalance.enoughBalances(invoker, dApp)) { d => + def checkAfterBlockRewardDistrResult(miner: Address, configAddressReward: Long): Assertion = { + val expectedResAfterBlockRewardDistribution = Seq( + ByteStr(miner.bytes) -> (d.blockchain.settings.rewardsSettings.initial - 2 * configAddressReward), + ByteStr(daoAddress.bytes) -> configAddressReward, + ByteStr(xtnBuybackAddress.bytes) -> configAddressReward + ).sortBy(_._1) + d.blockchain.accountData(dApp.toAddress, "size") shouldBe Some(IntegerDataEntry("size", 3)) + (1 to 3).map { idx => + ( + d.blockchain.accountData(dApp.toAddress, s"addr$idx").get.asInstanceOf[BinaryDataEntry].value, + d.blockchain.accountData(dApp.toAddress, s"reward$idx").get.asInstanceOf[IntegerDataEntry].value + ) + } shouldBe expectedResAfterBlockRewardDistribution + } + + val invoke = () => TxHelpers.invoke(dApp.toAddress, Some("foo"), invoker = invoker) + val cleanData = () => + TxHelpers.data( + dApp, + Seq(EmptyDataEntry("size")) ++ (1 to 3).flatMap(idx => Seq(EmptyDataEntry(s"addr$idx"), EmptyDataEntry(s"reward$idx"))), + version = TxVersion.V2 + ) + + val miner = d.appendBlock().sender.toAddress + d.appendBlock(TxHelpers.setScript(dApp, dAppBeforeBlockRewardDistribution)) // BlockRewardDistribution activation + d.appendBlockE(invoke()) should beRight + checkAfterBlockRewardDistrResult(miner, d.blockchain.settings.rewardsSettings.initial / 3) + + d.appendBlockE(cleanData(), invoke()) should beRight // CappedReward activation + d.blockchain.accountData(dApp.toAddress, "size") shouldBe Some(IntegerDataEntry("size", 1)) + d.blockchain.accountData(dApp.toAddress, "addr1").get.asInstanceOf[BinaryDataEntry].value shouldBe ByteStr(miner.bytes) + d.blockchain + .accountData(dApp.toAddress, "reward1") + .get + .asInstanceOf[IntegerDataEntry] + .value shouldBe d.blockchain.settings.rewardsSettings.initial + + (2 to 3).map { idx => + d.blockchain.accountData(dApp.toAddress, s"addr$idx") shouldBe None + d.blockchain.accountData(dApp.toAddress, s"reward$idx") shouldBe None + } + + d.appendBlockE(TxHelpers.setScript(dApp, dAppAfterBlockRewardDistribution), cleanData()) should beRight + d.appendBlockE(invoke()) should beRight + checkAfterBlockRewardDistrResult(miner, d.blockchain.settings.rewardsSettings.initial / 3) + + d.appendBlockE(TxHelpers.setScript(dApp, dAppAfterCappedReward), cleanData()) should beRight + d.appendBlockE(invoke()) should beRight + checkAfterBlockRewardDistrResult(miner, BlockRewardCalculator.MaxAddressReward) + } + } + property("transfer transaction by id") { val (masterAcc, _, genesis, setScriptTransactions, dataTransaction, transferTx, transfer2) = preconditionsAndPayments setScriptTransactions.foreach { setScriptTransaction => @@ -867,4 +925,22 @@ class ContextFunctionsTest extends PropSpec with WithDomain with EthHelpers { d.appendBlockE(transfer(signer(3))) should produce("TransactionNotAllowedByScript") } } + + private def blockInfoScript(version: StdLibVersion, getRewardsCode: String): String = + s""" + |{-# STDLIB_VERSION ${version.id} #-} + |{-# CONTENT_TYPE DAPP #-} + |{-# SCRIPT_TYPE ACCOUNT #-} + | + | @Callable(i) + | func foo() = { + | let r = $getRewardsCode + | let s = r.size() + | let first = if (s >= 1) then [BinaryEntry("addr1", r[0]._1.bytes), IntegerEntry("reward1", r[0]._2)] else [] + | let second = if (s >= 2) then [BinaryEntry("addr2", r[1]._1.bytes), IntegerEntry("reward2", r[1]._2)] else [] + | let third = if (s >= 3) then [BinaryEntry("addr3", r[2]._1.bytes), IntegerEntry("reward3", r[2]._2)] else [] + | + | [IntegerEntry("size", s)] ++ first ++ second ++ third + | } + |""".stripMargin } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 1fb3496f83e..6b28e153754 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,7 +6,7 @@ import sbt.{Def, _} object Dependencies { // Node protobuf schemas private[this] val protoSchemasLib = - "com.wavesplatform" % "protobuf-schemas" % "1.4.4" classifier "protobuf-src" intransitive () + "com.wavesplatform" % "protobuf-schemas" % "1.4.5-80-SNAPSHOT" classifier "protobuf-src" intransitive () def akkaModule(module: String): ModuleID = "com.typesafe.akka" %% s"akka-$module" % "2.6.20"