package blockbuilder import ( "math/big" "sort" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/utils/blockheader" "github.com/pkg/errors" "github.com/kaspanet/kaspad/domain/consensus/model" "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/merkle" "github.com/kaspanet/kaspad/infrastructure/logger" "github.com/kaspanet/kaspad/util/mstime" ) type blockBuilder struct { databaseContext model.DBManager genesisHash *externalapi.DomainHash hfDAAScore uint64 difficultyManager model.DifficultyManager pastMedianTimeManager model.PastMedianTimeManager coinbaseManager model.CoinbaseManager consensusStateManager model.ConsensusStateManager ghostdagManager model.GHOSTDAGManager transactionValidator model.TransactionValidator finalityManager model.FinalityManager pruningManager model.PruningManager blockParentBuilder model.BlockParentBuilder acceptanceDataStore model.AcceptanceDataStore blockRelationStore model.BlockRelationStore multisetStore model.MultisetStore ghostdagDataStore model.GHOSTDAGDataStore daaBlocksStore model.DAABlocksStore } // New creates a new instance of a BlockBuilder func New( databaseContext model.DBManager, genesisHash *externalapi.DomainHash, hfDAAScore uint64, difficultyManager model.DifficultyManager, pastMedianTimeManager model.PastMedianTimeManager, coinbaseManager model.CoinbaseManager, consensusStateManager model.ConsensusStateManager, ghostdagManager model.GHOSTDAGManager, transactionValidator model.TransactionValidator, finalityManager model.FinalityManager, blockParentBuilder model.BlockParentBuilder, pruningManager model.PruningManager, acceptanceDataStore model.AcceptanceDataStore, blockRelationStore model.BlockRelationStore, multisetStore model.MultisetStore, ghostdagDataStore model.GHOSTDAGDataStore, daaBlocksStore model.DAABlocksStore, ) model.BlockBuilder { return &blockBuilder{ databaseContext: databaseContext, genesisHash: genesisHash, hfDAAScore: hfDAAScore, difficultyManager: difficultyManager, pastMedianTimeManager: pastMedianTimeManager, coinbaseManager: coinbaseManager, consensusStateManager: consensusStateManager, ghostdagManager: ghostdagManager, transactionValidator: transactionValidator, finalityManager: finalityManager, blockParentBuilder: blockParentBuilder, pruningManager: pruningManager, acceptanceDataStore: acceptanceDataStore, blockRelationStore: blockRelationStore, multisetStore: multisetStore, ghostdagDataStore: ghostdagDataStore, daaBlocksStore: daaBlocksStore, } } // BuildBlock builds a block over the current state, with the given // coinbaseData and the given transactions func (bb *blockBuilder) BuildBlock(coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (block *externalapi.DomainBlock, coinbaseHasRedReward bool, err error) { onEnd := logger.LogAndMeasureExecutionTime(log, "BuildBlock") defer onEnd() stagingArea := model.NewStagingArea() return bb.buildBlock(stagingArea, coinbaseData, transactions) } func (bb *blockBuilder) buildBlock(stagingArea *model.StagingArea, coinbaseData *externalapi.DomainCoinbaseData, transactions []*externalapi.DomainTransaction) (block *externalapi.DomainBlock, coinbaseHasRedReward bool, err error) { err = bb.validateTransactions(stagingArea, transactions) if err != nil { return nil, false, err } newBlockPruningPoint, err := bb.newBlockPruningPoint(stagingArea, model.VirtualBlockHash) if err != nil { return nil, false, err } coinbase, coinbaseHasRedReward, err := bb.newBlockCoinbaseTransaction(stagingArea, coinbaseData) if err != nil { return nil, false, err } transactionsWithCoinbase := append([]*externalapi.DomainTransaction{coinbase}, transactions...) header, err := bb.buildHeader(stagingArea, transactionsWithCoinbase, newBlockPruningPoint) if err != nil { return nil, false, err } return &externalapi.DomainBlock{ Header: header, Transactions: transactionsWithCoinbase, }, coinbaseHasRedReward, nil } func (bb *blockBuilder) validateTransactions(stagingArea *model.StagingArea, transactions []*externalapi.DomainTransaction) error { invalidTransactions := make([]ruleerrors.InvalidTransaction, 0) for _, transaction := range transactions { err := bb.validateTransaction(stagingArea, transaction) if err != nil { if !errors.As(err, &ruleerrors.RuleError{}) { return err } invalidTransactions = append(invalidTransactions, ruleerrors.InvalidTransaction{Transaction: transaction, Error: err}) } } if len(invalidTransactions) > 0 { return ruleerrors.NewErrInvalidTransactionsInNewBlock(invalidTransactions) } return nil } func (bb *blockBuilder) validateTransaction( stagingArea *model.StagingArea, transaction *externalapi.DomainTransaction) error { originalEntries := make([]externalapi.UTXOEntry, len(transaction.Inputs)) for i, input := range transaction.Inputs { originalEntries[i] = input.UTXOEntry input.UTXOEntry = nil } defer func() { for i, input := range transaction.Inputs { input.UTXOEntry = originalEntries[i] } }() err := bb.consensusStateManager.PopulateTransactionWithUTXOEntries(stagingArea, transaction) if err != nil { return err } virtualPastMedianTime, err := bb.pastMedianTimeManager.PastMedianTime(stagingArea, model.VirtualBlockHash) if err != nil { return err } err = bb.transactionValidator.ValidateTransactionInContextIgnoringUTXO(stagingArea, transaction, model.VirtualBlockHash, virtualPastMedianTime) if err != nil { return err } return bb.transactionValidator.ValidateTransactionInContextAndPopulateFee(stagingArea, transaction, model.VirtualBlockHash) } func (bb *blockBuilder) newBlockCoinbaseTransaction(stagingArea *model.StagingArea, coinbaseData *externalapi.DomainCoinbaseData) (expectedTransaction *externalapi.DomainTransaction, hasRedReward bool, err error) { return bb.coinbaseManager.ExpectedCoinbaseTransaction(stagingArea, model.VirtualBlockHash, coinbaseData) } func (bb *blockBuilder) buildHeader(stagingArea *model.StagingArea, transactions []*externalapi.DomainTransaction, newBlockPruningPoint *externalapi.DomainHash) (externalapi.BlockHeader, error) { daaScore, err := bb.newBlockDAAScore(stagingArea) if err != nil { return nil, err } parents, err := bb.newBlockParents(stagingArea, daaScore) if err != nil { return nil, err } timeInMilliseconds, err := bb.newBlockTime(stagingArea) if err != nil { return nil, err } bits, err := bb.newBlockDifficulty(stagingArea) if err != nil { return nil, err } hashMerkleRoot := bb.newBlockHashMerkleRoot(transactions, daaScore >= bb.hfDAAScore) acceptedIDMerkleRoot, err := bb.newBlockAcceptedIDMerkleRoot(stagingArea) if err != nil { return nil, err } utxoCommitment, err := bb.newBlockUTXOCommitment(stagingArea) if err != nil { return nil, err } blueWork, err := bb.newBlockBlueWork(stagingArea) if err != nil { return nil, err } blueScore, err := bb.newBlockBlueScore(stagingArea) if err != nil { return nil, err } return blockheader.NewImmutableBlockHeader( constants.BlockVersion, parents, hashMerkleRoot, acceptedIDMerkleRoot, utxoCommitment, timeInMilliseconds, bits, 0, daaScore, blueScore, blueWork, newBlockPruningPoint, ), nil } func (bb *blockBuilder) newBlockParents(stagingArea *model.StagingArea, daaScore uint64) ([]externalapi.BlockLevelParents, error) { virtualBlockRelations, err := bb.blockRelationStore.BlockRelation(bb.databaseContext, stagingArea, model.VirtualBlockHash) if err != nil { return nil, err } return bb.blockParentBuilder.BuildParents(stagingArea, daaScore, virtualBlockRelations.Parents) } func (bb *blockBuilder) newBlockTime(stagingArea *model.StagingArea) (int64, error) { // The timestamp for the block must not be before the median timestamp // of the last several blocks. Thus, choose the maximum between the // current time and one second after the past median time. The current // timestamp is truncated to a millisecond boundary before comparison since a // block timestamp does not supported a precision greater than one // millisecond. newTimestamp := mstime.Now().UnixMilliseconds() minTimestamp, err := bb.minBlockTime(stagingArea, model.VirtualBlockHash) if err != nil { return 0, err } if newTimestamp < minTimestamp { newTimestamp = minTimestamp } return newTimestamp, nil } func (bb *blockBuilder) minBlockTime(stagingArea *model.StagingArea, hash *externalapi.DomainHash) (int64, error) { pastMedianTime, err := bb.pastMedianTimeManager.PastMedianTime(stagingArea, hash) if err != nil { return 0, err } return pastMedianTime + 1, nil } func (bb *blockBuilder) newBlockDifficulty(stagingArea *model.StagingArea) (uint32, error) { return bb.difficultyManager.RequiredDifficulty(stagingArea, model.VirtualBlockHash) } func (bb *blockBuilder) newBlockHashMerkleRoot(transactions []*externalapi.DomainTransaction, postHF bool) *externalapi.DomainHash { return merkle.CalculateHashMerkleRoot(transactions, postHF) } func (bb *blockBuilder) newBlockAcceptedIDMerkleRoot(stagingArea *model.StagingArea) (*externalapi.DomainHash, error) { newBlockAcceptanceData, err := bb.acceptanceDataStore.Get(bb.databaseContext, stagingArea, model.VirtualBlockHash) if err != nil { return nil, err } return bb.calculateAcceptedIDMerkleRoot(newBlockAcceptanceData) } func (bb *blockBuilder) calculateAcceptedIDMerkleRoot(acceptanceData externalapi.AcceptanceData) (*externalapi.DomainHash, error) { var acceptedTransactions []*externalapi.DomainTransaction for _, blockAcceptanceData := range acceptanceData { for _, transactionAcceptance := range blockAcceptanceData.TransactionAcceptanceData { if !transactionAcceptance.IsAccepted { continue } acceptedTransactions = append(acceptedTransactions, transactionAcceptance.Transaction) } } sort.Slice(acceptedTransactions, func(i, j int) bool { acceptedTransactionIID := consensushashing.TransactionID(acceptedTransactions[i]) acceptedTransactionJID := consensushashing.TransactionID(acceptedTransactions[j]) return acceptedTransactionIID.Less(acceptedTransactionJID) }) return merkle.CalculateIDMerkleRoot(acceptedTransactions), nil } func (bb *blockBuilder) newBlockUTXOCommitment(stagingArea *model.StagingArea) (*externalapi.DomainHash, error) { newBlockMultiset, err := bb.multisetStore.Get(bb.databaseContext, stagingArea, model.VirtualBlockHash) if err != nil { return nil, err } newBlockUTXOCommitment := newBlockMultiset.Hash() return newBlockUTXOCommitment, nil } func (bb *blockBuilder) newBlockDAAScore(stagingArea *model.StagingArea) (uint64, error) { return bb.daaBlocksStore.DAAScore(bb.databaseContext, stagingArea, model.VirtualBlockHash) } func (bb *blockBuilder) newBlockBlueWork(stagingArea *model.StagingArea) (*big.Int, error) { virtualGHOSTDAGData, err := bb.ghostdagDataStore.Get(bb.databaseContext, stagingArea, model.VirtualBlockHash, false) if err != nil { return nil, err } return virtualGHOSTDAGData.BlueWork(), nil } func (bb *blockBuilder) newBlockBlueScore(stagingArea *model.StagingArea) (uint64, error) { virtualGHOSTDAGData, err := bb.ghostdagDataStore.Get(bb.databaseContext, stagingArea, model.VirtualBlockHash, false) if err != nil { return 0, err } return virtualGHOSTDAGData.BlueScore(), nil } func (bb *blockBuilder) newBlockPruningPoint(stagingArea *model.StagingArea, blockHash *externalapi.DomainHash) (*externalapi.DomainHash, error) { return bb.pruningManager.ExpectedHeaderPruningPoint(stagingArea, blockHash) }