From b2e84b81edf9088182e6d3c114411d51c27d1d63 Mon Sep 17 00:00:00 2001 From: Sergey Nazarov Date: Mon, 1 Jul 2024 18:29:40 +0300 Subject: [PATCH] Boosted block reward (#3953) --- .../com/wavesplatform/events/events.scala | 3 +- .../events/BlockchainUpdatesSpec.scala | 29 +++- .../scala/com/wavesplatform/Application.scala | 5 +- .../com/wavesplatform/database/Caches.scala | 3 +- .../features/BlockchainFeature.scala | 8 +- .../settings/BlockchainSettings.scala | 9 +- .../state/BlockRewardCalculator.scala | 11 +- .../com/wavesplatform/state/Blockchain.scala | 7 + .../state/BlockchainUpdaterImpl.scala | 7 +- .../history/BlockRewardSpec.scala | 128 +++++++++++++++++- .../http/BlocksApiRouteSpec.scala | 77 ++++++++++- .../wavesplatform/http/UtilsRouteSpec.scala | 2 + .../smart/predef/ContextFunctionsTest.scala | 80 +++++++++++ 13 files changed, 351 insertions(+), 18 deletions(-) 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 62324eaaf1..614a643fb4 100644 --- a/grpc-server/src/main/scala/com/wavesplatform/events/events.scala +++ b/grpc-server/src/main/scala/com/wavesplatform/events/events.scala @@ -623,8 +623,7 @@ object BlockAppended { // updatedWavesAmount can change as a result of either genesis transactions or miner rewards val wavesAmount = blockchainBeforeWithReward.wavesAmount(height).toLong - val updatedWavesAmount = wavesAmount + reward.filter(_ => height > 0).getOrElse(0L) - + val updatedWavesAmount = wavesAmount + reward.filter(_ => height > 0).getOrElse(0L) * blockchainBeforeWithReward.blockRewardBoost(height + 1) val activatedFeatures = blockchainBeforeWithReward.activatedFeatures.collect { case (id, activationHeight) if activationHeight == height + 1 => id.toInt }.toSeq 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 8806ba561c..62ca697f79 100644 --- a/grpc-server/src/test/scala/com/wavesplatform/events/BlockchainUpdatesSpec.scala +++ b/grpc-server/src/test/scala/com/wavesplatform/events/BlockchainUpdatesSpec.scala @@ -27,8 +27,8 @@ import com.wavesplatform.lang.v1.compiler.Terms.FUNCTION_CALL import com.wavesplatform.lang.v1.compiler.TestCompiler import com.wavesplatform.protobuf.* import com.wavesplatform.protobuf.block.PBBlocks -import com.wavesplatform.protobuf.transaction.{DataEntry, InvokeScriptResult} import com.wavesplatform.protobuf.transaction.InvokeScriptResult.{Call, Invocation, Payment} +import com.wavesplatform.protobuf.transaction.{DataEntry, InvokeScriptResult} import com.wavesplatform.settings.{Constants, WavesSettings} import com.wavesplatform.state.{AssetDescription, BlockRewardCalculator, EmptyDataEntry, Height, LeaseBalance, StringDataEntry} import com.wavesplatform.test.* @@ -1093,6 +1093,33 @@ class BlockchainUpdatesSpec extends FreeSpec with WithBUDomain with ScalaFutures ) } } + + "should return correct updated_waves_amount when reward boost is active" in { + val settings = ConsensusImprovements + .setFeaturesHeight( + BlockchainFeatures.BlockReward -> 0, + BlockchainFeatures.BlockRewardDistribution -> 0, + BlockchainFeatures.BoostBlockReward -> 5 + ) + .configure(fs => + fs.copy(blockRewardBoostPeriod = 10) + ) + + withDomainAndRepo(settings) { case (d, repo) => + d.appendBlock() + val subscription = repo.createFakeObserver(SubscribeRequest.of(1, 0)) + + (1 to 15).foreach(_ => d.appendBlock()) + + + subscription + .fetchAllEvents(d.blockchain) + .map(_.getUpdate.getAppend.getBlock.updatedWavesAmount) shouldBe + (2 to 16).scanLeft(100_000_000.waves) { (total, height) => total + 6.waves * d.blockchain.blockRewardBoost(height) } + + + } + } } private def assertCommon(rollback: RollbackResult): Assertion = { diff --git a/node/src/main/scala/com/wavesplatform/Application.scala b/node/src/main/scala/com/wavesplatform/Application.scala index dd9f4341f1..886ac6b1a1 100644 --- a/node/src/main/scala/com/wavesplatform/Application.scala +++ b/node/src/main/scala/com/wavesplatform/Application.scala @@ -623,7 +623,10 @@ object Application extends ScorexLogging { .orElse(db.get(Keys.blockMetaAt(Height(height))).flatMap(BlockMeta.fromPb)) .map { blockMeta => val rewardShares = BlockRewardCalculator.getSortedBlockRewardShares(height, blockMeta.header.generator.toAddress, blockchainUpdater) - blockMeta.copy(rewardShares = rewardShares) + blockMeta.copy( + rewardShares = rewardShares, + reward = blockMeta.reward.map(_ * blockchainUpdater.blockRewardBoost(height)) + ) } def main(args: Array[String]): Unit = { diff --git a/node/src/main/scala/com/wavesplatform/database/Caches.scala b/node/src/main/scala/com/wavesplatform/database/Caches.scala index cff5d2b927..4ee6f9d097 100644 --- a/node/src/main/scala/com/wavesplatform/database/Caches.scala +++ b/node/src/main/scala/com/wavesplatform/database/Caches.scala @@ -245,7 +245,8 @@ abstract class Caches extends Blockchain with Storage { reward.getOrElse(0), if (block.header.version >= Block.ProtoBlockVersion) ByteString.copyFrom(hitSource.arr) else ByteString.EMPTY, ByteString.copyFrom(newScore.toByteArray), - current.meta.fold(settings.genesisSettings.initialBalance)(_.totalWavesAmount) + reward.getOrElse(0L) + current.meta.fold(settings.genesisSettings.initialBalance)(_.totalWavesAmount) + + (reward.getOrElse(0L) * this.blockRewardBoost(newHeight)) ) current = CurrentBlockInfo(Height(newHeight), Some(newMeta), block.transactionData) diff --git a/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala b/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala index 4667f77767..5a255eb3a4 100644 --- a/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala +++ b/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala @@ -26,10 +26,11 @@ object BlockchainFeatures { val CappedReward = BlockchainFeature(20, "Capped XTN buy-back & DAO amounts") val CeaseXtnBuyback = BlockchainFeature(21, "Cease XTN buy-back") val LightNode = BlockchainFeature(22, "Light Node") + val BoostBlockReward = BlockchainFeature(23, "Boost Block Reward") // Not exposed - val ContinuationTransaction = BlockchainFeature(23, "Continuation Transaction") - val LeaseExpiration = BlockchainFeature(24, "Lease Expiration") + val ContinuationTransaction = BlockchainFeature(24, "Continuation Transaction") + val LeaseExpiration = BlockchainFeature(25, "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!") @@ -56,7 +57,8 @@ object BlockchainFeatures { BlockRewardDistribution, CappedReward, CeaseXtnBuyback, - LightNode + LightNode, + BoostBlockReward ).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 5ac37553bd..2a0534c2df 100644 --- a/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala +++ b/node/src/main/scala/com/wavesplatform/settings/BlockchainSettings.scala @@ -77,7 +77,8 @@ case class FunctionalitySettings( daoAddress: Option[String] = None, xtnBuybackAddress: Option[String] = None, xtnBuybackRewardPeriod: Int = Int.MaxValue, - lightNodeBlockFieldsAbsenceInterval: Int = 1000 + lightNodeBlockFieldsAbsenceInterval: Int = 1000, + blockRewardBoostPeriod: Int = 1000 ) { val allowLeasedBalanceTransferUntilHeight: Int = blockVersion3AfterHeight val allowTemporaryNegativeUntil: Long = lastTimeBasedForkParameter @@ -130,7 +131,8 @@ object FunctionalitySettings { enforceTransferValidationAfter = 2959447, daoAddress = Some("3PEgG7eZHLFhcfsTSaYxgRhZsh4AxMvA4Ms"), xtnBuybackAddress = Some("3PFjHWuH6WXNJbwnfLHqNFBpwBS5dkYjTfv"), - xtnBuybackRewardPeriod = 100000 + xtnBuybackRewardPeriod = 100000, + blockRewardBoostPeriod = 300_000 ) val TESTNET: FunctionalitySettings = apply( @@ -145,7 +147,8 @@ object FunctionalitySettings { enforceTransferValidationAfter = 1698800, daoAddress = Some("3Myb6G8DkdBb8YcZzhrky65HrmiNuac3kvS"), xtnBuybackAddress = Some("3N13KQpdY3UU7JkWUBD9kN7t7xuUgeyYMTT"), - xtnBuybackRewardPeriod = 2000 + xtnBuybackRewardPeriod = 2000, + blockRewardBoostPeriod = 2_000 ) val STAGENET: FunctionalitySettings = apply( diff --git a/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala b/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala index 4b1f5c606e..124b5068a1 100644 --- a/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala +++ b/node/src/main/scala/com/wavesplatform/state/BlockRewardCalculator.scala @@ -8,7 +8,13 @@ import com.wavesplatform.state.diffs.BlockDiffer.Fraction object BlockRewardCalculator { - case class BlockRewardShares(miner: Long, daoAddress: Long, xtnBuybackAddress: Long) + case class BlockRewardShares(miner: Long, daoAddress: Long, xtnBuybackAddress: Long) { + private[BlockRewardCalculator] def multiply(by: Long): BlockRewardShares = BlockRewardShares( + miner = miner * by, + daoAddress = daoAddress * by, + xtnBuybackAddress = xtnBuybackAddress * by + ) + } val CurrentBlockRewardPart: Fraction = Fraction(1, 3) val RemaindRewardAddressPart: Fraction = Fraction(1, 2) @@ -16,6 +22,7 @@ object BlockRewardCalculator { val FullRewardInit: Long = 6 * Constants.UnitsInWave val MaxAddressReward: Long = 2 * Constants.UnitsInWave val GuaranteedMinerReward: Long = 2 * Constants.UnitsInWave + val RewardBoost = 10 def getBlockRewardShares( height: Int, @@ -50,7 +57,7 @@ object BlockRewardCalculator { calculateRewards(fullBlockReward, CurrentBlockRewardPart.apply(fullBlockReward), daoAddress, modifiedXtnBuybackAddress) } } else BlockRewardShares(fullBlockReward, 0, 0) - } + }.multiply(blockchain.blockRewardBoost(height)) def getSortedBlockRewardShares(height: Int, fullBlockReward: Long, generator: Address, blockchain: Blockchain): Seq[(Address, Long)] = { val daoAddress = blockchain.settings.functionalitySettings.daoAddressParsed.toOption.flatten diff --git a/node/src/main/scala/com/wavesplatform/state/Blockchain.scala b/node/src/main/scala/com/wavesplatform/state/Blockchain.scala index 3e7471e6fc..ab6283ea2a 100644 --- a/node/src/main/scala/com/wavesplatform/state/Blockchain.scala +++ b/node/src/main/scala/com/wavesplatform/state/Blockchain.scala @@ -226,5 +226,12 @@ object Blockchain { def supportsLightNodeBlockFields(height: Int = blockchain.height): Boolean = blockchain.featureActivationHeight(LightNode.id).exists(height >= _ + blockchain.settings.functionalitySettings.lightNodeBlockFieldsAbsenceInterval) + + def blockRewardBoost(height: Int): Int = + blockchain + .featureActivationHeight(BlockchainFeatures.BoostBlockReward.id) + .filter { boostHeight => + boostHeight <= height && height < boostHeight + blockchain.settings.functionalitySettings.blockRewardBoostPeriod + }.fold(1)(_ => BlockRewardCalculator.RewardBoost) } } diff --git a/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala b/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala index 4852c02108..d044515aba 100644 --- a/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala +++ b/node/src/main/scala/com/wavesplatform/state/BlockchainUpdaterImpl.scala @@ -347,7 +347,9 @@ class BlockchainUpdaterImpl( ) miner.scheduleMining(Some(tempBlockchain)) - log.trace(s"Persisting block ${referencedForgedBlock.id()}, discarded microblock refs: ${discarded.map(_._1.reference).mkString("[", ",", "]")}") + log.trace( + s"Persisting block ${referencedForgedBlock.id()}, discarded microblock refs: ${discarded.map(_._1.reference).mkString("[", ",", "]")}" + ) if (discarded.nonEmpty) { blockchainUpdateTriggers.onMicroBlockRollback(this, block.header.reference) @@ -629,7 +631,8 @@ class BlockchainUpdaterImpl( override def wavesAmount(height: Int): BigInt = readLock { ngState match { case Some(ng) if this.height == height => - rocksdb.wavesAmount(height - 1) + BigInt(ng.reward.getOrElse(0L)) + rocksdb.wavesAmount(height - 1) + + BigInt(ng.reward.getOrElse(0L)) * this.blockRewardBoost(height) case _ => rocksdb.wavesAmount(height) } diff --git a/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala b/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala index 0b5be51c21..a14ee7d4d3 100644 --- a/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala +++ b/node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala @@ -1,13 +1,14 @@ package com.wavesplatform.history import cats.syntax.option.* -import com.wavesplatform.account.KeyPair +import com.wavesplatform.account.{Address, KeyPair} import com.wavesplatform.api.http.RewardApiRoute import com.wavesplatform.block.Block import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.database.{Keys, DBExt} import com.wavesplatform.db.WithDomain +import com.wavesplatform.db.WithState.AddrWithBalance import com.wavesplatform.features.BlockchainFeatures import com.wavesplatform.features.BlockchainFeatures.{BlockReward, BlockRewardDistribution, ConsensusImprovements} import com.wavesplatform.history.Domain.BlockchainUpdaterExt @@ -16,12 +17,13 @@ import com.wavesplatform.mining.MiningConstraint import com.wavesplatform.settings.{Constants, FunctionalitySettings, RewardsSettings} import com.wavesplatform.state.diffs.BlockDiffer import com.wavesplatform.state.{BlockRewardCalculator, Blockchain, Height} -import com.wavesplatform.test.DomainPresets.{RideV6, WavesSettingsOps, BlockRewardDistribution as BlockRewardDistributionSettings} import com.wavesplatform.test.* +import com.wavesplatform.test.DomainPresets.{RideV6, WavesSettingsOps, BlockRewardDistribution as BlockRewardDistributionSettings} import com.wavesplatform.transaction.Asset.Waves import com.wavesplatform.transaction.transfer.TransferTransaction import com.wavesplatform.transaction.{GenesisTransaction, TxHelpers} import org.scalacheck.Gen +import org.scalactic.source.Position class BlockRewardSpec extends FreeSpec with WithDomain { @@ -1307,4 +1309,126 @@ class BlockRewardSpec extends FreeSpec with WithDomain { minerReward shouldBe fullBlockReward - daoAddressReward - xtnBuybackAddressReward } } + + private val daoAddress = TxHelpers.address(10002) + private val xtnBuybackAddress = TxHelpers.address(10003) + private val settingsWithRewardBoost = DomainPresets.BlockRewardDistribution + .setFeaturesHeight( + BlockchainFeatures.CappedReward -> 0, + BlockchainFeatures.BoostBlockReward -> 5 + ) + .configure(fs => + fs.copy( + blockRewardBoostPeriod = 10, + daoAddress = Some(daoAddress.toString), + xtnBuybackAddress = Some(xtnBuybackAddress.toString) + ) + ) + + private val blockMiner = TxHelpers.signer(10001) + private val initialMinerBalance = 100_000.waves + + private def assertBalances(blockchain: Blockchain, expectedBalances: (Address, Long)*)(implicit pos: Position): Unit = + expectedBalances.foreach { case (address, balance) => + withClue(address) { + blockchain.balance(address) shouldEqual balance + } + } + + "Boost block reward:" - { + "block reward is" - { + "increased after feature activation" in boostBlockRewardActivationScenario(0.5.waves, 2.waves) + "decreased after feature activation" in boostBlockRewardActivationScenario(-0.5.waves, 3.5.waves / 2) + "unchanged after feature activation" in boostBlockRewardActivationScenario(0, 2.waves) + } + } + + private def boostBlockRewardActivationScenario( + rewardDelta: Long, + addressShareAfterChange: Long + ): Unit = withDomain( + settingsWithRewardBoost.copy(blockchainSettings = + settingsWithRewardBoost.blockchainSettings.copy( + rewardsSettings = RewardsSettings(10, 10, 6.waves, 0.5.waves, 4) + ) + ), + Seq(AddrWithBalance(blockMiner.toAddress, initialMinerBalance)) + ) { d => + val minerRewardAfterChange = 2.waves + rewardDelta.max(0) + + (1 to 3).foreach(_ => d.appendKeyBlock(blockMiner)) + // height 4: before activation + d.blockchain.height shouldBe 4 + assertBalances( + d.blockchain, + blockMiner.toAddress -> (initialMinerBalance + 2.waves * 3), + daoAddress -> 2.waves * 3, + xtnBuybackAddress -> 2.waves * 3 + ) + + d.appendKeyBlock(blockMiner) + // height 5: activation height + val rewardAtActivationHeight = 2.waves * 3 + 2.waves * 10 + d.blockchain.height shouldBe 5 + assertBalances( + d.blockchain, + blockMiner.toAddress -> (initialMinerBalance + 2.waves * (3 + 10)), + daoAddress -> rewardAtActivationHeight, + xtnBuybackAddress -> rewardAtActivationHeight + ) + + d.appendKeyBlock(blockMiner) + // height 7: start voting + (1 to 3).foreach(_ => + d.appendBlock(d.createBlock(Block.RewardBlockVersion, Seq.empty, generator = blockMiner, rewardVote = 6.waves + rewardDelta)) + ) + d.blockchain.height shouldBe 9 + val rewardBeforeIncrease = rewardAtActivationHeight + 4 * 2.waves * 10 + assertBalances( + d.blockchain, + blockMiner.toAddress -> (initialMinerBalance + 2.waves * (3 + 10 * 5)), + daoAddress -> rewardBeforeIncrease, + xtnBuybackAddress -> rewardBeforeIncrease + ) + + // height 10: new base reward value = 65 waves + d.appendBlock(d.createBlock(Block.RewardBlockVersion, Seq.empty, generator = blockMiner, rewardVote = 7.waves)) + d.blockchain.height shouldBe 10 + val rewardAfterIncrease = rewardBeforeIncrease + addressShareAfterChange * 10 + assertBalances( + d.blockchain, + blockMiner.toAddress -> (initialMinerBalance + 2.waves * (3 + 10 * 5) + minerRewardAfterChange * 10), + daoAddress -> rewardAfterIncrease, + xtnBuybackAddress -> rewardAfterIncrease + ) + + (1 to 4).foreach(_ => d.appendKeyBlock(blockMiner)) + // height 14: before deactivation + d.blockchain.height shouldBe 14 + val rewardBeforeDeactivation = rewardAfterIncrease + 4 * addressShareAfterChange * 10 + assertBalances( + d.blockchain, + blockMiner.toAddress -> (initialMinerBalance + 2.waves * (3 + 10 * 5) + minerRewardAfterChange * 10 * 5), + daoAddress -> rewardBeforeDeactivation, + xtnBuybackAddress -> rewardBeforeDeactivation + ) + + d.appendKeyBlock(blockMiner) + // height 15: deactivation + d.blockchain.height shouldBe 15 + val rewardAfterDeactivation = rewardBeforeDeactivation + addressShareAfterChange + assertBalances( + d.blockchain, + blockMiner.toAddress -> (initialMinerBalance + 2.waves * (3 + 10 * 5) + minerRewardAfterChange * (10 * 5 + 1)), + daoAddress -> rewardAfterDeactivation, + xtnBuybackAddress -> rewardAfterDeactivation + ) + + d.blockchain.wavesAmount(15) shouldBe + BigInt(100_000_000.waves + // 1: genesis + 3 * 6.waves + // 2..4: before boost activation + 5 * 60.waves + // 5..9: boosted reward before change + 5 * (6.waves + rewardDelta) * 10 + // 10..14: boosted reward after change + 6.waves + rewardDelta) // 15: non-boosted after change + } } diff --git a/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala b/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala index 594765c03f..9a50e1c125 100644 --- a/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala +++ b/node/src/test/scala/com/wavesplatform/http/BlocksApiRouteSpec.scala @@ -19,10 +19,11 @@ import com.wavesplatform.state.{BlockRewardCalculator, Blockchain} import com.wavesplatform.test.* import com.wavesplatform.test.DomainPresets.* import com.wavesplatform.transaction.Asset.Waves -import com.wavesplatform.transaction.{TxHelpers, TxVersion} import com.wavesplatform.transaction.assets.exchange.{Order, OrderType} +import com.wavesplatform.transaction.{TxHelpers, TxVersion} import com.wavesplatform.utils.{SharedSchedulerMixin, SystemTime} import monix.reactive.Observable +import org.scalactic.source.Position import org.scalamock.scalatest.PathMockFactory import org.scalatest.Assertion import play.api.libs.json.* @@ -504,4 +505,78 @@ class BlocksApiRouteSpec .toMap shouldBe heightToResult } } + + "Boost block reward feature changes API response" in { + val miner = TxHelpers.signer(3001) + val daoAddress = TxHelpers.address(3002) + val xtnAddress = TxHelpers.address(3003) + + val settings = DomainPresets.ConsensusImprovements + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 0, + BlockchainFeatures.CappedReward -> 0, + BlockchainFeatures.BoostBlockReward -> 5, + BlockchainFeatures.CeaseXtnBuyback -> 0 + ) + .configure(fs => + fs.copy( + xtnBuybackRewardPeriod = 10, + blockRewardBoostPeriod = 10, + xtnBuybackAddress = Some(xtnAddress.toString), + daoAddress = Some(daoAddress.toString) + ) + ) + + withDomain(settings, Seq(AddrWithBalance(miner.toAddress, 100_000.waves))) { d => + val route = new BlocksApiRoute(d.settings.restAPISettings, d.blocksApi, SystemTime, new RouteTimeout(60.seconds)(sharedScheduler)).route + + def checkRewardAndShares(height: Int, expectedReward: Long, expectedMinerShare: Long, expectedDaoShare: Long, expectedXtnShare: Option[Long])( + implicit pos: Position + ): Unit = { + Seq("/headers/at/", "/at/").foreach { prefix => + val path = routePath(s"$prefix$height") + withClue(path) { + Get(path) ~> route ~> check { + val jsonResp = responseAs[JsObject] + withClue(" reward:") { + (jsonResp \ "reward").as[Long] shouldBe expectedReward + } + val shares = (jsonResp \ "rewardShares").as[JsObject] + withClue(" miner share: ") { + (shares \ miner.toAddress.toString).as[Long] shouldBe expectedMinerShare + } + withClue(" dao share: ") { + (shares \ daoAddress.toString).as[Long] shouldBe expectedDaoShare + } + withClue(" XTN share: ") { + (shares \ xtnAddress.toString).asOpt[Long] shouldBe expectedXtnShare + } + } + } + } + } + + (1 to 3).foreach(_ => d.appendKeyBlock(miner)) + d.blockchain.height shouldBe 4 + (1 to 3).foreach { h => + checkRewardAndShares(h + 1, 6.waves, 2.waves, 2.waves, Some(2.waves)) + } + + // reward boost activation + (1 to 5).foreach(_ => d.appendKeyBlock(miner)) + (1 to 5).foreach { h => + checkRewardAndShares(h + 4, 60.waves, 20.waves, 20.waves, Some(20.waves)) + } + + // cease XTN buyback + (1 to 5).foreach(_ => d.appendKeyBlock(miner)) + (1 to 5).foreach { h => + checkRewardAndShares(h + 9, 60.waves, 40.waves, 20.waves, None) + } + + d.appendKeyBlock(miner) + d.blockchain.height shouldBe 15 + checkRewardAndShares(15, 6.waves, 4.waves, 2.waves, None) + } + } } diff --git a/node/src/test/scala/com/wavesplatform/http/UtilsRouteSpec.scala b/node/src/test/scala/com/wavesplatform/http/UtilsRouteSpec.scala index 8f2f0d9261..a116946447 100644 --- a/node/src/test/scala/com/wavesplatform/http/UtilsRouteSpec.scala +++ b/node/src/test/scala/com/wavesplatform/http/UtilsRouteSpec.scala @@ -1,5 +1,6 @@ package com.wavesplatform.http +import akka.http.scaladsl.testkit.RouteTestTimeout import com.google.protobuf.ByteString import com.wavesplatform.api.http.ApiError.TooBigArrayAllocation import com.wavesplatform.api.http.requests.ScriptWithImportsRequest @@ -38,6 +39,7 @@ import scala.concurrent.duration.* class UtilsRouteSpec extends RouteSpec("/utils") with RestAPISettingsHelper with PropertyChecks with PathMockFactory with Inside with WithDomain { private val estimator = ScriptEstimatorV2 + protected override implicit val routeTestTimeout: RouteTestTimeout = RouteTestTimeout(20.seconds) private val timeBounded: SchedulerService = Schedulers.timeBoundedFixedPool( new HashedWheelTimer(), 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 87a67f9824..8530f2136e 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 @@ -15,6 +15,7 @@ import com.wavesplatform.lang.Testing.* import com.wavesplatform.lang.directives.values.* import com.wavesplatform.lang.directives.{DirectiveDictionary, DirectiveSet} import com.wavesplatform.lang.script.ContractScript +import com.wavesplatform.lang.v1.compiler.Terms.CONST_LONG import com.wavesplatform.lang.v1.compiler.{ContractCompiler, TestCompiler} import com.wavesplatform.lang.v1.estimator.v2.ScriptEstimatorV2 import com.wavesplatform.lang.v1.evaluator.ctx.impl.waves.WavesContext @@ -31,6 +32,7 @@ import com.wavesplatform.transaction.smart.script.ScriptCompiler import com.wavesplatform.transaction.{TxHelpers, TxVersion} import com.wavesplatform.utils.* import org.scalatest.Assertion +import org.scalatest.OptionValues.convertOptionToValuable import shapeless.Coproduct class ContextFunctionsTest extends PropSpec with WithDomain with EthHelpers { @@ -618,6 +620,84 @@ class ContextFunctionsTest extends PropSpec with WithDomain with EthHelpers { } } + property("Block reward boost is visible in dApp scripts") { + val daoAddress = TxHelpers.address(1003).toString + val xtnAddress = TxHelpers.address(1004).toString + val miner = TxHelpers.signer(1005) + val invoker = TxHelpers.signer(1006) + val dapp = TxHelpers.signer(1007) + val settings = ConsensusImprovements + .setFeaturesHeight( + BlockchainFeatures.BlockRewardDistribution -> 0, + BlockchainFeatures.CappedReward -> 0, + BlockchainFeatures.BoostBlockReward -> 5 + ) + .configure(fs => + fs.copy( + blockRewardBoostPeriod = 10, + daoAddress = Some(daoAddress), + xtnBuybackAddress = Some(xtnAddress) + ) + ) + + withDomain( + settings, + Seq( + AddrWithBalance(miner.toAddress, 100_000.waves), + AddrWithBalance(invoker.toAddress, 100.waves), + AddrWithBalance(dapp.toAddress, 100.waves) + ) + ) { d => + d.appendBlock( + TxHelpers.setScript( + dapp, + TestCompiler(V7).compileContract(s""" + func findRewardsFor(rewards: List[(Address, Int)], addr: Address) = { + func check(prev: Int|Unit, next: (Address, Int)) = match prev { + case _: Unit => + let (thisAddr, share) = next + if (thisAddr == addr) then share else unit + case _ => prev + } + + FOLD<3>(rewards, unit, check) + } + + @Callable(i) + func storeBlockInfo(height: Int) = { + let prefix = i.transactionId.toBase58String() + "_" + let blockInfo = blockInfoByHeight(height).value() + [ + IntegerEntry(prefix + "miner", findRewardsFor(blockInfo.rewards, blockInfo.generator).valueOrElse(0)), + IntegerEntry(prefix + "dao", findRewardsFor(blockInfo.rewards, addressFromStringValue("$daoAddress")).valueOrElse(0)), + IntegerEntry(prefix + "xtn", findRewardsFor(blockInfo.rewards, addressFromStringValue("$xtnAddress")).valueOrElse(0)) + ] + }""") + ) + ) + + def checkHeight(height: Int, minerShare: Long, daoShare: Long, xtnShare: Long): Unit = { + val invocation = TxHelpers.invoke(dapp.toAddress, Some("storeBlockInfo"), Seq(CONST_LONG(height)), invoker = invoker) + + d.appendBlock(d.createBlock(Block.RewardBlockVersion, Seq(invocation), generator = miner)) + + d.blockchain.accountData(dapp.toAddress, invocation.id().toString + "_miner").value.value shouldBe minerShare + d.blockchain.accountData(dapp.toAddress, invocation.id().toString + "_dao").value.value shouldBe daoShare + d.blockchain.accountData(dapp.toAddress, invocation.id().toString + "_xtn").value.value shouldBe xtnShare + } + + checkHeight(3, 2.waves, 2.waves, 2.waves) + d.appendBlock() + (1 to 10).foreach(i => checkHeight(i + 4, 20.waves, 20.waves, 20.waves)) + checkHeight(15, 2.waves, 2.waves, 2.waves) + + // check historic blocks again after deactivation + checkHeight(3, 2.waves, 2.waves, 2.waves) + checkHeight(5, 20.waves, 20.waves, 20.waves) + checkHeight(15, 2.waves, 2.waves, 2.waves) + } + } + property("transfer transaction by id") { val (masterAcc, _, genesis, setScriptTransactions, dataTransaction, transferTx, transfer2) = preconditionsAndPayments setScriptTransactions.foreach { setScriptTransaction =>