Skip to content

Commit

Permalink
Boosted block reward (#3953)
Browse files Browse the repository at this point in the history
  • Loading branch information
phearnot authored Jul 1, 2024
1 parent 2cf9a00 commit b2e84b8
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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 = {
Expand Down
5 changes: 4 additions & 1 deletion node/src/main/scala/com/wavesplatform/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
3 changes: 2 additions & 1 deletion node/src/main/scala/com/wavesplatform/database/Caches.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand All @@ -56,7 +57,8 @@ object BlockchainFeatures {
BlockRewardDistribution,
CappedReward,
CeaseXtnBuyback,
LightNode
LightNode,
BoostBlockReward
).map(f => f.id -> f).toMap

val implemented: Set[Short] = dict.keySet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ 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)

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,
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions node/src/main/scala/com/wavesplatform/state/Blockchain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
128 changes: 126 additions & 2 deletions node/src/test/scala/com/wavesplatform/history/BlockRewardSpec.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {

Expand Down Expand Up @@ -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
}
}
Loading

0 comments on commit b2e84b8

Please sign in to comment.