Repository: kaspanet/kaspad
Pull request #1831 (2021-10-17 13:12:01)

Switch to a fixed block subsidy after a certain work threshold
This depends on https://github.com/kaspanet/kaspad/pull/1830
Do not merge this PR before that one

The switch function is outlined here: https://hashdag.medium.com/kaspa-launch-plan-9a63f4d754a6

codecov[bot]: # [Codecov](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) Report
> Merging [#1831](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) (1814c6a) into [v0.11.0-dev](https://codecov.io/gh/kaspanet/kaspad/commit/afaac28da1d799fd9f1740a1456d3cf9335e1ac7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) (afaac28) will **increase** coverage by `0.11%`.
> The diff coverage is `75.69%`.

[![Impacted file tree graph](https://codecov.io/gh/kaspanet/kaspad/pull/1831/graphs/tree.svg?width=650&height=150&src=pr&token=2FBIEEJTNU&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet)](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet)

```diff
@@               Coverage Diff               @@
##           v0.11.0-dev    #1831      +/-   ##
===============================================
+ Coverage        62.17%   62.28%   +0.11%     
===============================================
  Files              621      622       +1     
  Lines            28477    28651     +174     
===============================================
+ Hits             17705    17846     +141     
- Misses            8307     8317      +10     
- Partials          2465     2488      +23     
```


| [Impacted Files](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) | Coverage Δ | |
|---|---|---|
| [app/appmessage/p2p\_msgblockwithtrusteddata.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-YXBwL2FwcG1lc3NhZ2UvcDJwX21zZ2Jsb2Nrd2l0aHRydXN0ZWRkYXRhLmdv) | `50.00% <ø> (ø)` | |
| [cmd/kaspawallet/balance.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-Y21kL2thc3Bhd2FsbGV0L2JhbGFuY2UuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/kaspawallet/create\_unsigned\_tx.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-Y21kL2thc3Bhd2FsbGV0L2NyZWF0ZV91bnNpZ25lZF90eC5nbw==) | `0.00% <0.00%> (ø)` | |
| [...allet/daemon/server/create\_unsigned\_transaction.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-Y21kL2thc3Bhd2FsbGV0L2RhZW1vbi9zZXJ2ZXIvY3JlYXRlX3Vuc2lnbmVkX3RyYW5zYWN0aW9uLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/kaspawallet/send.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-Y21kL2thc3Bhd2FsbGV0L3NlbmQuZ28=) | `0.00% <0.00%> (ø)` | |
| [domain/dagconfig/params.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2RhZ2NvbmZpZy9wYXJhbXMuZ28=) | `88.88% <ø> (ø)` | |
| [infrastructure/config/network.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-aW5mcmFzdHJ1Y3R1cmUvY29uZmlnL25ldHdvcmsuZ28=) | `2.17% <0.00%> (ø)` | |
| [...s/processes/coinbasemanager/block\_reward\_switch.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvY29pbmJhc2VtYW5hZ2VyL2Jsb2NrX3Jld2FyZF9zd2l0Y2guZ28=) | `62.96% <62.96%> (ø)` | |
| [...nsensus/processes/pruningmanager/pruningmanager.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvcHJ1bmluZ21hbmFnZXIvcHJ1bmluZ21hbmFnZXIuZ28=) | `56.65% <71.42%> (+0.47%)` | :arrow_up: |
| [...ensus/processes/coinbasemanager/coinbasemanager.go](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet#diff-ZG9tYWluL2NvbnNlbnN1cy9wcm9jZXNzZXMvY29pbmJhc2VtYW5hZ2VyL2NvaW5iYXNlbWFuYWdlci5nbw==) | `75.00% <75.73%> (+1.21%)` | :arrow_up: |
| ... and [21 more](https://codecov.io/gh/kaspanet/kaspad/pull/1831/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet). Last update [afaac28...1814c6a](https://codecov.io/gh/kaspanet/kaspad/pull/1831?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kaspanet).


Patch:
diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go
index 78118ae7..06d1825d 100644
--- a/domain/consensus/factory.go
+++ b/domain/consensus/factory.go
@@ -205,11 +205,15 @@ func (f *factory) NewConsensus(config *Config, db infrastructuredatabase.Databas
 		config.SubsidyMergeSetRewardMultiplier,
 		config.CoinbasePayloadScriptPublicKeyMaxLength,
 		config.GenesisHash,
+		config.FixedSubsidySwitchPruningPointInterval,
+		config.FixedSubsidySwitchHashRateDifference,
 		dagTraversalManager,
 		ghostdagDataStore,
 		acceptanceDataStore,
 		daaBlocksStore,
-		blockStore)
+		blockStore,
+		pruningStore,
+		blockHeaderStore)
 	headerTipsManager := headersselectedtipmanager.New(dbManager, dagTopologyManager, dagTraversalManager,
 		ghostdagManager, headersSelectedTipStore, headersSelectedChainStore)
 	genesisHash := config.GenesisHash
diff --git a/domain/consensus/model/interface_processes_coinbasemanager.go b/domain/consensus/model/interface_processes_coinbasemanager.go
index e8635d99..42ef4720 100644
--- a/domain/consensus/model/interface_processes_coinbasemanager.go
+++ b/domain/consensus/model/interface_processes_coinbasemanager.go
@@ -6,7 +6,7 @@ import "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
 // coinbase transactions
 type CoinbaseManager interface {
 	ExpectedCoinbaseTransaction(stagingArea *StagingArea, blockHash *externalapi.DomainHash,
-		coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransaction, error)
-	CalcBlockSubsidy(stagingArea *StagingArea, blockHash *externalapi.DomainHash) (uint64, error)
+		coinbaseData *externalapi.DomainCoinbaseData, blockPruningPoint *externalapi.DomainHash) (*externalapi.DomainTransaction, error)
+	CalcBlockSubsidy(stagingArea *StagingArea, blockHash *externalapi.DomainHash, blockPruningPoint *externalapi.DomainHash) (uint64, error)
 	ExtractCoinbaseDataBlueScoreAndSubsidy(coinbaseTx *externalapi.DomainTransaction) (blueScore uint64, coinbaseData *externalapi.DomainCoinbaseData, subsidy uint64, err error)
 }
diff --git a/domain/consensus/processes/blockbuilder/block_builder.go b/domain/consensus/processes/blockbuilder/block_builder.go
index 26fb57cc..745bc880 100644
--- a/domain/consensus/processes/blockbuilder/block_builder.go
+++ b/domain/consensus/processes/blockbuilder/block_builder.go
@@ -103,13 +103,17 @@ func (bb *blockBuilder) buildBlock(stagingArea *model.StagingArea, coinbaseData
 		return nil, err
 	}
 
-	coinbase, err := bb.newBlockCoinbaseTransaction(stagingArea, coinbaseData)
+	newBlockPruningPoint, err := bb.newBlockPruningPoint(stagingArea, model.VirtualBlockHash)
+	if err != nil {
+		return nil, err
+	}
+	coinbase, err := bb.newBlockCoinbaseTransaction(stagingArea, coinbaseData, newBlockPruningPoint)
 	if err != nil {
 		return nil, err
 	}
 	transactionsWithCoinbase := append([]*externalapi.DomainTransaction{coinbase}, transactions...)
 
-	header, err := bb.buildHeader(stagingArea, transactionsWithCoinbase)
+	header, err := bb.buildHeader(stagingArea, transactionsWithCoinbase, newBlockPruningPoint)
 	if err != nil {
 		return nil, err
 	}
@@ -171,13 +175,13 @@ func (bb *blockBuilder) validateTransaction(
 }
 
 func (bb *blockBuilder) newBlockCoinbaseTransaction(stagingArea *model.StagingArea,
-	coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransaction, error) {
+	coinbaseData *externalapi.DomainCoinbaseData, blockPruningPoint *externalapi.DomainHash) (*externalapi.DomainTransaction, error) {
 
-	return bb.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, model.VirtualBlockHash, coinbaseData)
+	return bb.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, model.VirtualBlockHash, coinbaseData, blockPruningPoint)
 }
 
-func (bb *blockBuilder) buildHeader(stagingArea *model.StagingArea, transactions []*externalapi.DomainTransaction) (
-	externalapi.BlockHeader, error) {
+func (bb *blockBuilder) buildHeader(stagingArea *model.StagingArea, transactions []*externalapi.DomainTransaction,
+	newBlockPruningPoint *externalapi.DomainHash) (externalapi.BlockHeader, error) {
 
 	parents, err := bb.newBlockParents(stagingArea)
 	if err != nil {
@@ -212,10 +216,6 @@ func (bb *blockBuilder) buildHeader(stagingArea *model.StagingArea, transactions
 	if err != nil {
 		return nil, err
 	}
-	pruningPoint, err := bb.newBlockPruningPoint(stagingArea, model.VirtualBlockHash)
-	if err != nil {
-		return nil, err
-	}
 
 	return blockheader.NewImmutableBlockHeader(
 		constants.MaxBlockVersion,
@@ -229,7 +229,7 @@ func (bb *blockBuilder) buildHeader(stagingArea *model.StagingArea, transactions
 		daaScore,
 		blueScore,
 		blueWork,
-		pruningPoint,
+		newBlockPruningPoint,
 	), nil
 }
 
diff --git a/domain/consensus/processes/blockvalidator/block_body_in_context.go b/domain/consensus/processes/blockvalidator/block_body_in_context.go
index a652c8d8..538051c7 100644
--- a/domain/consensus/processes/blockvalidator/block_body_in_context.go
+++ b/domain/consensus/processes/blockvalidator/block_body_in_context.go
@@ -178,7 +178,7 @@ func (v *blockValidator) checkCoinbaseSubsidy(
 		return err
 	}
 
-	expectedSubsidy, err := v.coinbaseManager.CalcBlockSubsidy(stagingArea, blockHash)
+	expectedSubsidy, err := v.coinbaseManager.CalcBlockSubsidy(stagingArea, blockHash, block.Header.PruningPoint())
 	if err != nil {
 		return err
 	}
diff --git a/domain/consensus/processes/coinbasemanager/block_reward_switch.go b/domain/consensus/processes/coinbasemanager/block_reward_switch.go
new file mode 100644
index 00000000..d48dae9f
--- /dev/null
+++ b/domain/consensus/processes/coinbasemanager/block_reward_switch.go
@@ -0,0 +1,86 @@
+package coinbasemanager
+
+import (
+	"github.com/kaspanet/kaspad/domain/consensus/model"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+	"math/big"
+)
+
+func (c *coinbaseManager) isBlockRewardFixed(stagingArea *model.StagingArea, blockPruningPoint *externalapi.DomainHash) (bool, error) {
+	blockPruningPointIndex, found, err := c.findPruningPointIndex(stagingArea, blockPruningPoint)
+	if err != nil {
+		return false, err
+	}
+
+	// The given `pruningPointBlock` may only not be found under one circumstance:
+	// we're currently in the process of building the next pruning point. As such,
+	// we must manually set highIndex to currentIndex + 1 because the next pruning
+	// point is not yet stored in the database
+	highPruningPointIndex := blockPruningPointIndex
+	highPruningPointHash := blockPruningPoint
+	if !found {
+		currentPruningPointIndex, err := c.pruningStore.CurrentPruningPointIndex(c.databaseContext, stagingArea)
+		if err != nil {
+			return false, err
+		}
+		highPruningPointIndex = currentPruningPointIndex + 1
+	}
+
+	for {
+		if highPruningPointIndex <= c.fixedSubsidySwitchPruningPointInterval {
+			break
+		}
+
+		lowPruningPointIndex := highPruningPointIndex - c.fixedSubsidySwitchPruningPointInterval
+		lowPruningPointHash, err := c.pruningStore.PruningPointByIndex(c.databaseContext, stagingArea, lowPruningPointIndex)
+		if err != nil {
+			return false, err
+		}
+
+		highPruningPointHeader, err := c.blockHeaderStore.BlockHeader(c.databaseContext, stagingArea, highPruningPointHash)
+		if err != nil {
+			return false, err
+		}
+		lowPruningPointHeader, err := c.blockHeaderStore.BlockHeader(c.databaseContext, stagingArea, lowPruningPointHash)
+		if err != nil {
+			return false, err
+		}
+
+		blueWorkDifference := new(big.Int).Sub(highPruningPointHeader.BlueWork(), lowPruningPointHeader.BlueWork())
+		blueScoreDifference := new(big.Int).SetUint64(highPruningPointHeader.BlueScore() - lowPruningPointHeader.BlueScore())
+		estimatedAverageHashRate := new(big.Int).Div(blueWorkDifference, blueScoreDifference)
+		if estimatedAverageHashRate.Cmp(c.fixedSubsidySwitchHashRateDifference) >= 0 {
+			return true, nil
+		}
+
+		highPruningPointIndex--
+		highPruningPointHash, err = c.pruningStore.PruningPointByIndex(c.databaseContext, stagingArea, highPruningPointIndex)
+		if err != nil {
+			return false, err
+		}
+	}
+
+	return false, nil
+}
+
+func (c *coinbaseManager) findPruningPointIndex(stagingArea *model.StagingArea, pruningPointHash *externalapi.DomainHash) (uint64, bool, error) {
+	currentPruningPointHash, err := c.pruningStore.PruningPoint(c.databaseContext, stagingArea)
+	if err != nil {
+		return 0, false, err
+	}
+	currentPruningPointIndex, err := c.pruningStore.CurrentPruningPointIndex(c.databaseContext, stagingArea)
+	if err != nil {
+		return 0, false, err
+	}
+	for !currentPruningPointHash.Equal(pruningPointHash) && currentPruningPointIndex > 0 {
+		currentPruningPointIndex--
+		currentPruningPointHash, err = c.pruningStore.PruningPointByIndex(c.databaseContext, stagingArea, currentPruningPointIndex)
+		if err != nil {
+			return 0, false, err
+		}
+	}
+	if currentPruningPointIndex == 0 && !currentPruningPointHash.Equal(pruningPointHash) {
+		return 0, false, nil
+	}
+	return currentPruningPointIndex, true, nil
+}
diff --git a/domain/consensus/processes/coinbasemanager/coinbasemanager.go b/domain/consensus/processes/coinbasemanager/coinbasemanager.go
index 85af8f62..b642b86e 100644
--- a/domain/consensus/processes/coinbasemanager/coinbasemanager.go
+++ b/domain/consensus/processes/coinbasemanager/coinbasemanager.go
@@ -21,6 +21,8 @@ type coinbaseManager struct {
 	subsidyMergeSetRewardMultiplier         *big.Rat
 	coinbasePayloadScriptPublicKeyMaxLength uint8
 	genesisHash                             *externalapi.DomainHash
+	fixedSubsidySwitchPruningPointInterval  uint64
+	fixedSubsidySwitchHashRateDifference    *big.Int
 
 	databaseContext     model.DBReader
 	dagTraversalManager model.DAGTraversalManager
@@ -28,10 +30,12 @@ type coinbaseManager struct {
 	acceptanceDataStore model.AcceptanceDataStore
 	daaBlocksStore      model.DAABlocksStore
 	blockStore          model.BlockStore
+	pruningStore        model.PruningStore
+	blockHeaderStore    model.BlockHeaderStore
 }
 
 func (c *coinbaseManager) ExpectedCoinbaseTransaction(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash,
-	coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransaction, error) {
+	coinbaseData *externalapi.DomainCoinbaseData, blockPruningPoint *externalapi.DomainHash) (*externalapi.DomainTransaction, error) {
 
 	ghostdagData, err := c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, true)
 	if !database.IsNotFoundError(err) && err != nil {
@@ -79,7 +83,7 @@ func (c *coinbaseManager) ExpectedCoinbaseTransaction(stagingArea *model.Staging
 		txOuts = append(txOuts, txOut)
 	}
 
-	subsidy, err := c.CalcBlockSubsidy(stagingArea, blockHash)
+	subsidy, err := c.CalcBlockSubsidy(stagingArea, blockHash, blockPruningPoint)
 	if err != nil {
 		return nil, err
 	}
@@ -179,11 +183,21 @@ func acceptanceDataFromArrayToMap(acceptanceData externalapi.AcceptanceData) map
 // has the expected value.
 //
 // Further details: https://hashdag.medium.com/kaspa-launch-plan-9a63f4d754a6
-func (c *coinbaseManager) CalcBlockSubsidy(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (uint64, error) {
+func (c *coinbaseManager) CalcBlockSubsidy(stagingArea *model.StagingArea,
+	blockHash *externalapi.DomainHash, blockPruningPoint *externalapi.DomainHash) (uint64, error) {
+
 	if blockHash.Equal(c.genesisHash) {
 		return c.subsidyGenesisReward, nil
 	}
 
+	isBlockRewardFixed, err := c.isBlockRewardFixed(stagingArea, blockPruningPoint)
+	if err != nil {
+		return 0, err
+	}
+	if isBlockRewardFixed {
+		return c.subsidyGenesisReward, nil
+	}
+
 	averagePastSubsidy, err := c.calculateAveragePastSubsidy(stagingArea, blockHash)
 	if err != nil {
 		return 0, err
@@ -349,12 +363,16 @@ func New(
 	subsidyMergeSetRewardMultiplier *big.Rat,
 	coinbasePayloadScriptPublicKeyMaxLength uint8,
 	genesisHash *externalapi.DomainHash,
+	fixedSubsidySwitchPruningPointInterval uint64,
+	fixedSubsidySwitchHashRateDifference *big.Int,
 
 	dagTraversalManager model.DAGTraversalManager,
 	ghostdagDataStore model.GHOSTDAGDataStore,
 	acceptanceDataStore model.AcceptanceDataStore,
 	daaBlocksStore model.DAABlocksStore,
-	blockStore model.BlockStore) model.CoinbaseManager {
+	blockStore model.BlockStore,
+	pruningStore model.PruningStore,
+	blockHeaderStore model.BlockHeaderStore) model.CoinbaseManager {
 
 	return &coinbaseManager{
 		databaseContext: databaseContext,
@@ -366,11 +384,15 @@ func New(
 		subsidyMergeSetRewardMultiplier:         subsidyMergeSetRewardMultiplier,
 		coinbasePayloadScriptPublicKeyMaxLength: coinbasePayloadScriptPublicKeyMaxLength,
 		genesisHash:                             genesisHash,
+		fixedSubsidySwitchPruningPointInterval:  fixedSubsidySwitchPruningPointInterval,
+		fixedSubsidySwitchHashRateDifference:    fixedSubsidySwitchHashRateDifference,
 
 		dagTraversalManager: dagTraversalManager,
 		ghostdagDataStore:   ghostdagDataStore,
 		acceptanceDataStore: acceptanceDataStore,
 		daaBlocksStore:      daaBlocksStore,
 		blockStore:          blockStore,
+		pruningStore:        pruningStore,
+		blockHeaderStore:    blockHeaderStore,
 	}
 }
diff --git a/domain/consensus/processes/consensusstatemanager/verify_and_build_utxo.go b/domain/consensus/processes/consensusstatemanager/verify_and_build_utxo.go
index 490989a1..828245fb 100644
--- a/domain/consensus/processes/consensusstatemanager/verify_and_build_utxo.go
+++ b/domain/consensus/processes/consensusstatemanager/verify_and_build_utxo.go
@@ -162,8 +162,12 @@ func (csm *consensusStateManager) validateCoinbaseTransaction(stagingArea *model
 	}
 
 	log.Tracef("Calculating the expected coinbase transaction for the given coinbase data and block %s", blockHash)
+	header, err := csm.blockHeaderStore.BlockHeader(csm.databaseContext, stagingArea, blockHash)
+	if err != nil {
+		return err
+	}
 	expectedCoinbaseTransaction, err :=
-		csm.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, blockHash, coinbaseData)
+		csm.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, blockHash, coinbaseData, header.PruningPoint())
 	if err != nil {
 		return err
 	}
diff --git a/domain/dagconfig/consensus_defaults.go b/domain/dagconfig/consensus_defaults.go
index 3ca727fc..e7d7d360 100644
--- a/domain/dagconfig/consensus_defaults.go
+++ b/domain/dagconfig/consensus_defaults.go
@@ -46,11 +46,13 @@ const (
 	// Should be at least an order of magnitude smaller than defaultFinalityDuration/defaultTargetTimePerBlock.
 	// (Higher values make pruning attacks easier by a constant, lower values make merging after a split or a spike
 	// in block take longer)
-	defaultMergeSetSizeLimit                       = defaultGHOSTDAGK * 10
-	defaultSubsidyGenesisReward                    = 1 * constants.SompiPerKaspa
-	defaultMinSubsidy                              = 1 * constants.SompiPerKaspa
-	defaultMaxSubsidy                              = 1000 * constants.SompiPerKaspa
-	defaultCoinbasePayloadScriptPublicKeyMaxLength = 150
+	defaultMergeSetSizeLimit                              = defaultGHOSTDAGK * 10
+	defaultSubsidyGenesisReward                           = 1 * constants.SompiPerKaspa
+	defaultMinSubsidy                                     = 1 * constants.SompiPerKaspa
+	defaultMaxSubsidy                                     = 1000 * constants.SompiPerKaspa
+	defaultBaseSubsidy                                    = 50 * constants.SompiPerKaspa
+	defaultFixedSubsidySwitchPruningPointInterval  uint64 = 7
+	defaultCoinbasePayloadScriptPublicKeyMaxLength        = 150
 	// defaultDifficultyAdjustmentWindowSize is the number of blocks in a block's past used to calculate its difficulty
 	// target.
 	// The DAA should take the median of 2640 blocks, so in order to do that we need 2641 window size.
diff --git a/domain/dagconfig/params.go b/domain/dagconfig/params.go
index b7198c0f..49cf0684 100644
--- a/domain/dagconfig/params.go
+++ b/domain/dagconfig/params.go
@@ -179,6 +179,13 @@ type Params struct {
 
 	// PruningProofM is the 'm' constant in the pruning proof. For more details see: https://github.com/kaspanet/research/issues/3
 	PruningProofM uint64
+
+	// BaseSubsidy is the starting subsidy amount for mined blocks.
+	BaseSubsidy uint64
+
+	FixedSubsidySwitchPruningPointInterval uint64
+
+	FixedSubsidySwitchHashRateDifference *big.Int
 }
 
 // NormalizeRPCServerAddress returns addr with the current network default
@@ -254,6 +261,8 @@ var MainnetParams = Params{
 	MergeSetSizeLimit:                       defaultMergeSetSizeLimit,
 	CoinbasePayloadScriptPublicKeyMaxLength: defaultCoinbasePayloadScriptPublicKeyMaxLength,
 	PruningProofM:                           defaultPruningProofM,
+	FixedSubsidySwitchPruningPointInterval:  defaultFixedSubsidySwitchPruningPointInterval,
+	FixedSubsidySwitchHashRateDifference:    big.NewInt(1_000_000),
 }
 
 // TestnetParams defines the network parameters for the test Kaspa network.
@@ -314,6 +323,8 @@ var TestnetParams = Params{
 	MergeSetSizeLimit:                       defaultMergeSetSizeLimit,
 	CoinbasePayloadScriptPublicKeyMaxLength: defaultCoinbasePayloadScriptPublicKeyMaxLength,
 	PruningProofM:                           defaultPruningProofM,
+	FixedSubsidySwitchPruningPointInterval:  defaultFixedSubsidySwitchPruningPointInterval,
+	FixedSubsidySwitchHashRateDifference:    big.NewInt(1_000_000),
 }
 
 // SimnetParams defines the network parameters for the simulation test Kaspa
@@ -378,6 +389,8 @@ var SimnetParams = Params{
 	MergeSetSizeLimit:                       defaultMergeSetSizeLimit,
 	CoinbasePayloadScriptPublicKeyMaxLength: defaultCoinbasePayloadScriptPublicKeyMaxLength,
 	PruningProofM:                           defaultPruningProofM,
+	FixedSubsidySwitchPruningPointInterval:  defaultFixedSubsidySwitchPruningPointInterval,
+	FixedSubsidySwitchHashRateDifference:    big.NewInt(1_000_000),
 }
 
 // DevnetParams defines the network parameters for the development Kaspa network.
@@ -438,6 +451,8 @@ var DevnetParams = Params{
 	MergeSetSizeLimit:                       defaultMergeSetSizeLimit,
 	CoinbasePayloadScriptPublicKeyMaxLength: defaultCoinbasePayloadScriptPublicKeyMaxLength,
 	PruningProofM:                           defaultPruningProofM,
+	FixedSubsidySwitchPruningPointInterval:  defaultFixedSubsidySwitchPruningPointInterval,
+	FixedSubsidySwitchHashRateDifference:    big.NewInt(1_000_000),
 }
 
 var (


Test patch:
diff --git a/domain/consensus/processes/blockbuilder/test_block_builder.go b/domain/consensus/processes/blockbuilder/test_block_builder.go
index a798f53d..4796f9ea 100644
--- a/domain/consensus/processes/blockbuilder/test_block_builder.go
+++ b/domain/consensus/processes/blockbuilder/test_block_builder.go
@@ -200,7 +200,11 @@ func (bb *testBlockBuilder) buildBlockWithParents(stagingArea *model.StagingArea
 
 	bb.acceptanceDataStore.Stage(stagingArea, tempBlockHash, acceptanceData)
 
-	coinbase, err := bb.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, tempBlockHash, coinbaseData)
+	pruningPoint, err := bb.newBlockPruningPoint(stagingArea, tempBlockHash)
+	if err != nil {
+		return nil, nil, err
+	}
+	coinbase, err := bb.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, tempBlockHash, coinbaseData, pruningPoint)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/domain/consensus/processes/coinbasemanager/block_reward_switch_test.go b/domain/consensus/processes/coinbasemanager/block_reward_switch_test.go
new file mode 100644
index 00000000..cba6c62b
--- /dev/null
+++ b/domain/consensus/processes/coinbasemanager/block_reward_switch_test.go
@@ -0,0 +1,121 @@
+package coinbasemanager_test
+
+import (
+	"github.com/kaspanet/kaspad/domain/consensus"
+	"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
+	"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
+	"github.com/kaspanet/kaspad/util/difficulty"
+	"testing"
+	"time"
+)
+
+func TestBlockRewardSwitch(t *testing.T) {
+	testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
+		// Set the pruning depth to 10
+		consensusConfig.MergeSetSizeLimit = 1
+		consensusConfig.K = 1
+		consensusConfig.FinalityDuration = 1 * time.Second
+		consensusConfig.TargetTimePerBlock = 1 * time.Second
+
+		// Disable difficulty adjustment so that we could reason about blue work
+		consensusConfig.DisableDifficultyAdjustment = true
+
+		// Disable pruning so that we could have access to all the blocks
+		consensusConfig.IsArchival = true
+
+		// Set the interval to 10
+		consensusConfig.FixedSubsidySwitchPruningPointInterval = 10
+
+		// Set the hash rate difference such that the switch would trigger exactly
+		// on the `FixedSubsidySwitchPruningPointInterval + 1`th pruning point
+		workToAcceptGenesis := difficulty.CalcWork(consensusConfig.GenesisBlock.Header.Bits())
+		consensusConfig.FixedSubsidySwitchHashRateDifference = workToAcceptGenesis
+
+		// Set the min, max, and post-switch subsidies to values that would make it
+		// easy to tell whether the switch happened
+		consensusConfig.MinSubsidy = 2 * constants.SompiPerKaspa
+		consensusConfig.MaxSubsidy = 2 * constants.SompiPerKaspa
+		consensusConfig.SubsidyGenesisReward = 1 * constants.SompiPerKaspa
+
+		factory := consensus.NewFactory()
+		tc, teardown, err := factory.NewTestConsensus(consensusConfig, "TestBlockRewardSwitch")
+		if err != nil {
+			t.Fatalf("Error setting up consensus: %+v", err)
+		}
+		defer teardown(false)
+
+		// Make the pruning point move FixedSubsidySwitchPruningPointInterval times
+		tipHash := consensusConfig.GenesisHash
+		for i := uint64(0); i < consensusConfig.PruningDepth()+consensusConfig.FixedSubsidySwitchPruningPointInterval; i++ {
+			addedBlockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{tipHash}, nil, nil)
+			if err != nil {
+				t.Fatalf("AddBlock: %+v", err)
+			}
+			tipHash = addedBlockHash
+		}
+
+		// We expect to see `FixedSubsidySwitchPruningPointInterval` pruning points + the genesis
+		pruningPointHeaders, err := tc.PruningPointHeaders()
+		if err != nil {
+			t.Fatalf("PruningPointHeaders: %+v", pruningPointHeaders)
+		}
+		expectedPruningPointHeaderAmount := consensusConfig.FixedSubsidySwitchPruningPointInterval + 1
+		if uint64(len(pruningPointHeaders)) != expectedPruningPointHeaderAmount {
+			t.Fatalf("Unexpected amount of pruning point headers. "+
+				"Want: %d, got: %d", expectedPruningPointHeaderAmount, len(pruningPointHeaders))
+		}
+
+		// Make sure that all the headers thus far had a non-fixed subsidies
+		// Note that we skip the genesis, since that always has the post-switch
+		// value
+		for _, pruningPointHeader := range pruningPointHeaders[1:] {
+			pruningPointHash := consensushashing.HeaderHash(pruningPointHeader)
+			pruningPoint, err := tc.GetBlock(pruningPointHash)
+			if err != nil {
+				t.Fatalf("GetBlock: %+v", err)
+			}
+			pruningPointCoinbase := pruningPoint.Transactions[transactionhelper.CoinbaseTransactionIndex]
+			_, _, subsidy, err := tc.CoinbaseManager().ExtractCoinbaseDataBlueScoreAndSubsidy(pruningPointCoinbase)
+			if err != nil {
+				t.Fatalf("ExtractCoinbaseDataBlueScoreAndSubsidy: %+v", err)
+			}
+			if subsidy != consensusConfig.MinSubsidy {
+				t.Fatalf("Subsidy has unexpected value. Want: %d, got: %d", consensusConfig.MinSubsidy, subsidy)
+			}
+		}
+
+		// Add another block. We expect it to be another pruning point
+		lastPruningPointHash, _, err := tc.AddBlock([]*externalapi.DomainHash{tipHash}, nil, nil)
+		if err != nil {
+			t.Fatalf("AddBlock: %+v", err)
+		}
+
+		// Make sure that another pruning point had been added
+		pruningPointHeaders, err = tc.PruningPointHeaders()
+		if err != nil {
+			t.Fatalf("PruningPointHeaders: %+v", pruningPointHeaders)
+		}
+		expectedPruningPointHeaderAmount = expectedPruningPointHeaderAmount + 1
+		if uint64(len(pruningPointHeaders)) != expectedPruningPointHeaderAmount {
+			t.Fatalf("Unexpected amount of pruning point headers. "+
+				"Want: %d, got: %d", expectedPruningPointHeaderAmount, len(pruningPointHeaders))
+		}
+
+		// Make sure that the last pruning point has a post-switch subsidy
+		lastPruningPoint, err := tc.GetBlock(lastPruningPointHash)
+		if err != nil {
+			t.Fatalf("GetBlock: %+v", err)
+		}
+		lastPruningPointCoinbase := lastPruningPoint.Transactions[transactionhelper.CoinbaseTransactionIndex]
+		_, _, subsidy, err := tc.CoinbaseManager().ExtractCoinbaseDataBlueScoreAndSubsidy(lastPruningPointCoinbase)
+		if err != nil {
+			t.Fatalf("ExtractCoinbaseDataBlueScoreAndSubsidy: %+v", err)
+		}
+		if subsidy != consensusConfig.SubsidyGenesisReward {
+			t.Fatalf("Subsidy has unexpected value. Want: %d, got: %d", consensusConfig.SubsidyGenesisReward, subsidy)
+		}
+	})
+}