package coinbasemanager import ( "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" ) type coinbaseManager struct { subsidyGenesisReward uint64 preDeflationaryPhaseBaseSubsidy uint64 coinbasePayloadScriptPublicKeyMaxLength uint8 genesisHash *externalapi.DomainHash deflationaryPhaseDaaScore uint64 deflationaryPhaseBaseSubsidy uint64 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) (expectedTransaction *externalapi.DomainTransaction, hasRedReward bool, err error) { ghostdagData, err := c.ghostdagDataStore.Get(c.databaseContext, stagingArea, blockHash, true) if !database.IsNotFoundError(err) && err != nil { return nil, false, 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, false, err } } acceptanceData, err := c.acceptanceDataStore.Get(c.databaseContext, stagingArea, blockHash) if err != nil { return nil, false, err } daaAddedBlocksSet, err := c.daaAddedBlocksSet(stagingArea, blockHash) if err != nil { return nil, false, 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, false, err } if hasReward { txOuts = append(txOuts, txOut) } } txOut, hasRedReward, err := c.coinbaseOutputForRewardFromRedBlocks( stagingArea, ghostdagData, acceptanceData, daaAddedBlocksSet, coinbaseData) if err != nil { return nil, false, err } if hasRedReward { txOuts = append(txOuts, txOut) } subsidy, err := c.CalcBlockSubsidy(stagingArea, blockHash) if err != nil { return nil, false, err } payload, err := c.serializeCoinbasePayload(ghostdagData.BlueScore(), coinbaseData, subsidy) if err != nil { return nil, false, err } return &externalapi.DomainTransaction{ Version: constants.MaxTransactionVersion, Inputs: []*externalapi.DomainTransactionInput{}, Outputs: txOuts, LockTime: 0, SubnetworkID: subnetworks.SubnetworkIDCoinbase, Gas: 0, Payload: payload, }, hasRedReward, 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. func (c *coinbaseManager) CalcBlockSubsidy(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (uint64, error) { if blockHash.Equal(c.genesisHash) { return c.subsidyGenesisReward, nil } blockDaaScore, err := c.daaBlocksStore.DAAScore(c.databaseContext, stagingArea, blockHash) if err != nil { return 0, err } if blockDaaScore < c.deflationaryPhaseDaaScore { return c.preDeflationaryPhaseBaseSubsidy, nil } blockSubsidy := c.calcDeflationaryPeriodBlockSubsidy(blockDaaScore) return blockSubsidy, nil } func (c *coinbaseManager) calcDeflationaryPeriodBlockSubsidy(blockDaaScore uint64) uint64 { // We define a year as 365.25 days and a month as 365.25 / 12 = 30.4375 // secondsPerMonth = 30.4375 * 24 * 60 * 60 const secondsPerMonth = 2629800 // Note that this calculation implicitly assumes that block per second = 1 (by assuming daa score diff is in second units). monthsSinceDeflationaryPhaseStarted := (blockDaaScore - c.deflationaryPhaseDaaScore) / secondsPerMonth // Return the pre-calculated value from subsidy-per-month table return c.getDeflationaryPeriodBlockSubsidyFromTable(monthsSinceDeflationaryPhaseStarted) } /* This table was pre-calculated by calling `calcDeflationaryPeriodBlockSubsidyFloatCalc` for all months until reaching 0 subsidy. To regenerate this table, run `TestBuildSubsidyTable` in coinbasemanager_test.go (note the `deflationaryPhaseBaseSubsidy` therein) */ var subsidyByDeflationaryMonthTable = []uint64{ 44000000000, 41530469757, 39199543598, 36999442271, 34922823143, 32962755691, 31112698372, 29366476791, 27718263097, 26162556530, 24694165062, 23308188075, 22000000000, 20765234878, 19599771799, 18499721135, 17461411571, 16481377845, 15556349186, 14683238395, 13859131548, 13081278265, 12347082531, 11654094037, 11000000000, 10382617439, 9799885899, 9249860567, 8730705785, 8240688922, 7778174593, 7341619197, 6929565774, 6540639132, 6173541265, 5827047018, 5500000000, 5191308719, 4899942949, 4624930283, 4365352892, 4120344461, 3889087296, 3670809598, 3464782887, 3270319566, 3086770632, 2913523509, 2750000000, 2595654359, 2449971474, 2312465141, 2182676446, 2060172230, 1944543648, 1835404799, 1732391443, 1635159783, 1543385316, 1456761754, 1375000000, 1297827179, 1224985737, 1156232570, 1091338223, 1030086115, 972271824, 917702399, 866195721, 817579891, 771692658, 728380877, 687500000, 648913589, 612492868, 578116285, 545669111, 515043057, 486135912, 458851199, 433097860, 408789945, 385846329, 364190438, 343750000, 324456794, 306246434, 289058142, 272834555, 257521528, 243067956, 229425599, 216548930, 204394972, 192923164, 182095219, 171875000, 162228397, 153123217, 144529071, 136417277, 128760764, 121533978, 114712799, 108274465, 102197486, 96461582, 91047609, 85937500, 81114198, 76561608, 72264535, 68208638, 64380382, 60766989, 57356399, 54137232, 51098743, 48230791, 45523804, 42968750, 40557099, 38280804, 36132267, 34104319, 32190191, 30383494, 28678199, 27068616, 25549371, 24115395, 22761902, 21484375, 20278549, 19140402, 18066133, 17052159, 16095095, 15191747, 14339099, 13534308, 12774685, 12057697, 11380951, 10742187, 10139274, 9570201, 9033066, 8526079, 8047547, 7595873, 7169549, 6767154, 6387342, 6028848, 5690475, 5371093, 5069637, 4785100, 4516533, 4263039, 4023773, 3797936, 3584774, 3383577, 3193671, 3014424, 2845237, 2685546, 2534818, 2392550, 2258266, 2131519, 2011886, 1898968, 1792387, 1691788, 1596835, 1507212, 1422618, 1342773, 1267409, 1196275, 1129133, 1065759, 1005943, 949484, 896193, 845894, 798417, 753606, 711309, 671386, 633704, 598137, 564566, 532879, 502971, 474742, 448096, 422947, 399208, 376803, 355654, 335693, 316852, 299068, 282283, 266439, 251485, 237371, 224048, 211473, 199604, 188401, 177827, 167846, 158426, 149534, 141141, 133219, 125742, 118685, 112024, 105736, 99802, 94200, 88913, 83923, 79213, 74767, 70570, 66609, 62871, 59342, 56012, 52868, 49901, 47100, 44456, 41961, 39606, 37383, 35285, 33304, 31435, 29671, 28006, 26434, 24950, 23550, 22228, 20980, 19803, 18691, 17642, 16652, 15717, 14835, 14003, 13217, 12475, 11775, 11114, 10490, 9901, 9345, 8821, 8326, 7858, 7417, 7001, 6608, 6237, 5887, 5557, 5245, 4950, 4672, 4410, 4163, 3929, 3708, 3500, 3304, 3118, 2943, 2778, 2622, 2475, 2336, 2205, 2081, 1964, 1854, 1750, 1652, 1559, 1471, 1389, 1311, 1237, 1168, 1102, 1040, 982, 927, 875, 826, 779, 735, 694, 655, 618, 584, 551, 520, 491, 463, 437, 413, 389, 367, 347, 327, 309, 292, 275, 260, 245, 231, 218, 206, 194, 183, 173, 163, 154, 146, 137, 130, 122, 115, 109, 103, 97, 91, 86, 81, 77, 73, 68, 65, 61, 57, 54, 51, 48, 45, 43, 40, 38, 36, 34, 32, 30, 28, 27, 25, 24, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, 7, 6, 6, 6, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, } func (c *coinbaseManager) getDeflationaryPeriodBlockSubsidyFromTable(month uint64) uint64 { if month >= uint64(len(subsidyByDeflationaryMonthTable)) { month = uint64(len(subsidyByDeflationaryMonthTable) - 1) } return subsidyByDeflationaryMonthTable[month] } func (c *coinbaseManager) calcDeflationaryPeriodBlockSubsidyFloatCalc(month uint64) uint64 { baseSubsidy := c.deflationaryPhaseBaseSubsidy subsidy := float64(baseSubsidy) / math.Pow(2, float64(month)/12) return uint64(subsidy) } 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, preDeflationaryPhaseBaseSubsidy uint64, coinbasePayloadScriptPublicKeyMaxLength uint8, genesisHash *externalapi.DomainHash, deflationaryPhaseDaaScore uint64, deflationaryPhaseBaseSubsidy uint64, 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, preDeflationaryPhaseBaseSubsidy: preDeflationaryPhaseBaseSubsidy, coinbasePayloadScriptPublicKeyMaxLength: coinbasePayloadScriptPublicKeyMaxLength, genesisHash: genesisHash, deflationaryPhaseDaaScore: deflationaryPhaseDaaScore, deflationaryPhaseBaseSubsidy: deflationaryPhaseBaseSubsidy, dagTraversalManager: dagTraversalManager, ghostdagDataStore: ghostdagDataStore, acceptanceDataStore: acceptanceDataStore, daaBlocksStore: daaBlocksStore, blockStore: blockStore, pruningStore: pruningStore, blockHeaderStore: blockHeaderStore, } }