Michael Sutton 21b82d7efc
Block template cache (#1994)
* minor text fix

* Implement a block template cache with template modification/reuse mechanism

* Fix compilation error

* Address review comments

* Added a through TestModifyBlockTemplate test

* Update header timestamp if possible

* Avoid copying the transactions when only the header changed

* go fmt
2022-03-31 16:37:48 +03:00

346 lines
12 KiB
Go

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
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,
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,
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)
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.MaxBlockVersion,
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) *externalapi.DomainHash {
return merkle.CalculateHashMerkleRoot(transactions)
}
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)
}