399 lines
14 KiB
Go

package coinbasemanager
import (
"encoding/binary"
"github.com/kaspanet/kaspad/domain/consensus/model"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/hashset"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
"github.com/kaspanet/kaspad/infrastructure/db/database"
"github.com/pkg/errors"
"math/big"
)
type coinbaseManager struct {
subsidyGenesisReward uint64
minSubsidy uint64
maxSubsidy uint64
subsidyPastRewardMultiplier *big.Rat
subsidyMergeSetRewardMultiplier *big.Rat
coinbasePayloadScriptPublicKeyMaxLength uint8
genesisHash *externalapi.DomainHash
fixedSubsidySwitchPruningPointInterval uint64
fixedSubsidySwitchHashRateThreshold *big.Int
databaseContext model.DBReader
dagTraversalManager model.DAGTraversalManager
ghostdagDataStore model.GHOSTDAGDataStore
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, blockPruningPoint *externalapi.DomainHash) (*externalapi.DomainTransaction, error) {
ghostdagData, err := c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, true)
if !database.IsNotFoundError(err) && err != nil {
return nil, err
}
// If there's ghostdag data with trusted data we prefer it because we need the original merge set non-pruned merge set.
if database.IsNotFoundError(err) {
ghostdagData, err = c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, false)
if err != nil {
return nil, err
}
}
acceptanceData, err := c.acceptanceDataStore.Get(c.databaseContext, stagingArea, blockHash)
if err != nil {
return nil, err
}
daaAddedBlocksSet, err := c.daaAddedBlocksSet(stagingArea, blockHash)
if err != nil {
return nil, err
}
txOuts := make([]*externalapi.DomainTransactionOutput, 0, len(ghostdagData.MergeSetBlues()))
acceptanceDataMap := acceptanceDataFromArrayToMap(acceptanceData)
for _, blue := range ghostdagData.MergeSetBlues() {
txOut, hasReward, err := c.coinbaseOutputForBlueBlock(stagingArea, blue, acceptanceDataMap[*blue], daaAddedBlocksSet)
if err != nil {
return nil, err
}
if hasReward {
txOuts = append(txOuts, txOut)
}
}
txOut, hasReward, err := c.coinbaseOutputForRewardFromRedBlocks(
stagingArea, ghostdagData, acceptanceData, daaAddedBlocksSet, coinbaseData)
if err != nil {
return nil, err
}
if hasReward {
txOuts = append(txOuts, txOut)
}
subsidy, err := c.CalcBlockSubsidy(stagingArea, blockHash, blockPruningPoint)
if err != nil {
return nil, err
}
payload, err := c.serializeCoinbasePayload(ghostdagData.BlueScore(), coinbaseData, subsidy)
if err != nil {
return nil, err
}
return &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: []*externalapi.DomainTransactionInput{},
Outputs: txOuts,
LockTime: 0,
SubnetworkID: subnetworks.SubnetworkIDCoinbase,
Gas: 0,
Payload: payload,
}, nil
}
func (c *coinbaseManager) daaAddedBlocksSet(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (
hashset.HashSet, error) {
daaAddedBlocks, err := c.daaBlocksStore.DAAAddedBlocks(c.databaseContext, stagingArea, blockHash)
if err != nil {
return nil, err
}
return hashset.NewFromSlice(daaAddedBlocks...), nil
}
// coinbaseOutputForBlueBlock calculates the output that should go into the coinbase transaction of blueBlock
// If blueBlock gets no fee - returns nil for txOut
func (c *coinbaseManager) coinbaseOutputForBlueBlock(stagingArea *model.StagingArea,
blueBlock *externalapi.DomainHash, blockAcceptanceData *externalapi.BlockAcceptanceData,
mergingBlockDAAAddedBlocksSet hashset.HashSet) (*externalapi.DomainTransactionOutput, bool, error) {
blockReward, err := c.calcMergedBlockReward(stagingArea, blueBlock, blockAcceptanceData, mergingBlockDAAAddedBlocksSet)
if err != nil {
return nil, false, err
}
if blockReward == 0 {
return nil, false, nil
}
// the ScriptPublicKey for the coinbase is parsed from the coinbase payload
_, coinbaseData, _, err := c.ExtractCoinbaseDataBlueScoreAndSubsidy(blockAcceptanceData.TransactionAcceptanceData[0].Transaction)
if err != nil {
return nil, false, err
}
txOut := &externalapi.DomainTransactionOutput{
Value: blockReward,
ScriptPublicKey: coinbaseData.ScriptPublicKey,
}
return txOut, true, nil
}
func (c *coinbaseManager) coinbaseOutputForRewardFromRedBlocks(stagingArea *model.StagingArea,
ghostdagData *externalapi.BlockGHOSTDAGData, acceptanceData externalapi.AcceptanceData, daaAddedBlocksSet hashset.HashSet,
coinbaseData *externalapi.DomainCoinbaseData) (*externalapi.DomainTransactionOutput, bool, error) {
acceptanceDataMap := acceptanceDataFromArrayToMap(acceptanceData)
totalReward := uint64(0)
for _, red := range ghostdagData.MergeSetReds() {
reward, err := c.calcMergedBlockReward(stagingArea, red, acceptanceDataMap[*red], daaAddedBlocksSet)
if err != nil {
return nil, false, err
}
totalReward += reward
}
if totalReward == 0 {
return nil, false, nil
}
return &externalapi.DomainTransactionOutput{
Value: totalReward,
ScriptPublicKey: coinbaseData.ScriptPublicKey,
}, true, nil
}
func acceptanceDataFromArrayToMap(acceptanceData externalapi.AcceptanceData) map[externalapi.DomainHash]*externalapi.BlockAcceptanceData {
acceptanceDataMap := make(map[externalapi.DomainHash]*externalapi.BlockAcceptanceData, len(acceptanceData))
for _, blockAcceptanceData := range acceptanceData {
acceptanceDataMap[*blockAcceptanceData.BlockHash] = blockAcceptanceData
}
return acceptanceDataMap
}
// CalcBlockSubsidy returns the subsidy amount a block at the provided blue score
// should have. This is mainly used for determining how much the coinbase for
// newly generated blocks awards as well as validating the coinbase for blocks
// has the expected value.
//
// Further details: https://hashdag.medium.com/kaspa-launch-plan-9a63f4d754a6
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
}
mergeSetSubsidySum, err := c.calculateMergeSetSubsidySum(stagingArea, blockHash)
if err != nil {
return 0, err
}
subsidyRandomVariable, err := c.calculateSubsidyRandomVariable(stagingArea, blockHash)
if err != nil {
return 0, err
}
pastSubsidy := new(big.Rat).Mul(averagePastSubsidy, c.subsidyPastRewardMultiplier)
mergeSetSubsidy := new(big.Rat).Mul(mergeSetSubsidySum, c.subsidyMergeSetRewardMultiplier)
// In order to avoid unsupported negative exponents in powInt64, flip
// the numerator and the denominator manually
subsidyRandom := new(big.Rat)
if subsidyRandomVariable >= 0 {
subsidyRandom = subsidyRandom.SetInt64(1 << subsidyRandomVariable)
} else {
subsidyRandom = subsidyRandom.SetFrac64(1, 1<<(-subsidyRandomVariable))
}
blockSubsidyBigRat := new(big.Rat).Add(mergeSetSubsidy, new(big.Rat).Mul(pastSubsidy, subsidyRandom))
blockSubsidyBigInt := new(big.Int).Div(blockSubsidyBigRat.Num(), blockSubsidyBigRat.Denom())
blockSubsidyUint64 := blockSubsidyBigInt.Uint64()
clampedBlockSubsidy := blockSubsidyUint64
if clampedBlockSubsidy < c.minSubsidy {
clampedBlockSubsidy = c.minSubsidy
} else if clampedBlockSubsidy > c.maxSubsidy {
clampedBlockSubsidy = c.maxSubsidy
}
return clampedBlockSubsidy, nil
}
func (c *coinbaseManager) calculateAveragePastSubsidy(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (*big.Rat, error) {
const subsidyPastWindowSize = 100
blockWindow, err := c.dagTraversalManager.BlockWindow(stagingArea, blockHash, subsidyPastWindowSize)
if err != nil {
return nil, err
}
if len(blockWindow) == 0 {
return new(big.Rat).SetFrac64(int64(c.subsidyGenesisReward), 1), nil
}
pastBlocks, err := c.blockStore.Blocks(c.databaseContext, stagingArea, blockWindow)
if err != nil {
return nil, err
}
pastBlockSubsidySum := int64(0)
for _, pastBlock := range pastBlocks {
coinbaseTransaction := pastBlock.Transactions[transactionhelper.CoinbaseTransactionIndex]
_, _, pastBlockSubsidy, err := c.ExtractCoinbaseDataBlueScoreAndSubsidy(coinbaseTransaction)
if err != nil {
return nil, err
}
pastBlockSubsidySum += int64(pastBlockSubsidy)
}
return big.NewRat(pastBlockSubsidySum, int64(len(blockWindow))), nil
}
func (c *coinbaseManager) calculateMergeSetSubsidySum(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (*big.Rat, error) {
ghostdagData, err := c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, true)
if !database.IsNotFoundError(err) && err != nil {
return nil, err
}
// If there's ghostdag data with trusted data we prefer it because we need the original merge set non-pruned merge set.
if database.IsNotFoundError(err) {
ghostdagData, err = c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, false)
if err != nil {
return nil, err
}
}
mergeSet := append(ghostdagData.MergeSetBlues(), ghostdagData.MergeSetReds()...)
mergeSetBlocks, err := c.blockStore.Blocks(c.databaseContext, stagingArea, mergeSet)
if err != nil {
return nil, err
}
mergeSetSubsidySum := int64(0)
for _, mergeSetBlock := range mergeSetBlocks {
coinbaseTransaction := mergeSetBlock.Transactions[transactionhelper.CoinbaseTransactionIndex]
_, _, mergeSetBlockSubsidy, err := c.ExtractCoinbaseDataBlueScoreAndSubsidy(coinbaseTransaction)
if err != nil {
return nil, err
}
mergeSetSubsidySum += int64(mergeSetBlockSubsidy)
}
return big.NewRat(mergeSetSubsidySum, 1), nil
}
func (c *coinbaseManager) calculateSubsidyRandomVariable(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (int64, error) {
ghostdagData, err := c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, false)
if err != nil {
return 0, err
}
selectedParentHash := ghostdagData.SelectedParent()
if selectedParentHash == nil {
return 0, nil
}
const binomialSteps = 10
binomialSum := int64(0)
// The first two bytes of a hash are a good deterministic source
// of randomness, so we use that instead of any rand implementation
firstTwoBytes := binary.LittleEndian.Uint16(selectedParentHash.ByteSlice()[:2])
for i := 0; i < binomialSteps; i++ {
step := firstTwoBytes & 1
firstTwoBytes >>= 1
binomialSum += int64(step)
}
return binomialSum - (binomialSteps / 2), nil
}
func (c *coinbaseManager) calcMergedBlockReward(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash,
blockAcceptanceData *externalapi.BlockAcceptanceData, mergingBlockDAAAddedBlocksSet hashset.HashSet) (uint64, error) {
if !blockHash.Equal(blockAcceptanceData.BlockHash) {
return 0, errors.Errorf("blockAcceptanceData.BlockHash is expected to be %s but got %s",
blockHash, blockAcceptanceData.BlockHash)
}
if !mergingBlockDAAAddedBlocksSet.Contains(blockHash) {
return 0, nil
}
totalFees := uint64(0)
for _, txAcceptanceData := range blockAcceptanceData.TransactionAcceptanceData {
if txAcceptanceData.IsAccepted {
totalFees += txAcceptanceData.Fee
}
}
block, err := c.blockStore.Block(c.databaseContext, stagingArea, blockHash)
if err != nil {
return 0, err
}
_, _, subsidy, err := c.ExtractCoinbaseDataBlueScoreAndSubsidy(block.Transactions[transactionhelper.CoinbaseTransactionIndex])
if err != nil {
return 0, err
}
return subsidy + totalFees, nil
}
// New instantiates a new CoinbaseManager
func New(
databaseContext model.DBReader,
subsidyGenesisReward uint64,
minSubsidy uint64,
maxSubsidy uint64,
subsidyPastRewardMultiplier *big.Rat,
subsidyMergeSetRewardMultiplier *big.Rat,
coinbasePayloadScriptPublicKeyMaxLength uint8,
genesisHash *externalapi.DomainHash,
fixedSubsidySwitchPruningPointInterval uint64,
fixedSubsidySwitchHashRateThreshold *big.Int,
dagTraversalManager model.DAGTraversalManager,
ghostdagDataStore model.GHOSTDAGDataStore,
acceptanceDataStore model.AcceptanceDataStore,
daaBlocksStore model.DAABlocksStore,
blockStore model.BlockStore,
pruningStore model.PruningStore,
blockHeaderStore model.BlockHeaderStore) model.CoinbaseManager {
return &coinbaseManager{
databaseContext: databaseContext,
subsidyGenesisReward: subsidyGenesisReward,
minSubsidy: minSubsidy,
maxSubsidy: maxSubsidy,
subsidyPastRewardMultiplier: subsidyPastRewardMultiplier,
subsidyMergeSetRewardMultiplier: subsidyMergeSetRewardMultiplier,
coinbasePayloadScriptPublicKeyMaxLength: coinbasePayloadScriptPublicKeyMaxLength,
genesisHash: genesisHash,
fixedSubsidySwitchPruningPointInterval: fixedSubsidySwitchPruningPointInterval,
fixedSubsidySwitchHashRateThreshold: fixedSubsidySwitchHashRateThreshold,
dagTraversalManager: dagTraversalManager,
ghostdagDataStore: ghostdagDataStore,
acceptanceDataStore: acceptanceDataStore,
daaBlocksStore: daaBlocksStore,
blockStore: blockStore,
pruningStore: pruningStore,
blockHeaderStore: blockHeaderStore,
}
}