diff --git a/internal/blockchain/agendas.go b/internal/blockchain/agendas.go index 11ce9d6058..1ae8d432db 100644 --- a/internal/blockchain/agendas.go +++ b/internal/blockchain/agendas.go @@ -678,6 +678,29 @@ func (b *BlockChain) isAgendaActivePositional(prevNode *blockNode, agenda *conse return agendaActiveInfo{isValid: false} } +// isAgendaActivePositionalByID attempts to determine whether or not an agenda +// is active for the block AFTER the given block node using only information +// available that depends on its position within the block chain and having the +// headers of all ancestors available. It does not, and must not, rely on +// having the full block data of all ancestors available or deployment details +// associated with the agenda. +// +// See [BlockChain.isAgendaActivePositional] for more details. This only +// differs in that it is a convenience wrapper that looks up the agenda for the +// given ID and returns an error if the provided ID is unknown. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) isAgendaActivePositionalByID(prevNode *blockNode, agendaID string) (agendaActiveInfo, error) { + agenda, ok := b.agendas[agendaID] + if !ok { + str := fmt.Sprintf("agenda ID %s does not exist", agendaID) + return agendaActiveInfo{}, contextError(ErrUnknownAgendaID, str) + } + + info := b.isAgendaActivePositional(prevNode, agenda) + return info, nil +} + // isAgendaActive attempts to determine whether or not an agenda is active // for the block AFTER the given block node. // diff --git a/internal/blockchain/chain.go b/internal/blockchain/chain.go index 5e7aa35746..a344c4256d 100644 --- a/internal/blockchain/chain.go +++ b/internal/blockchain/chain.go @@ -48,6 +48,11 @@ const ( // contextCheckCacheSize is the number of recent successful contextual block // check results to keep in memory. contextCheckCacheSize = 25 + + // merkleCheckCacheSize is the number of recent blocks that have + // definitively proven the header commits to the received data for the block + // to keep in memory. + merkleCheckCacheSize = 25 ) // panicf is a convenience function that formats according to the given format @@ -196,8 +201,12 @@ type BlockChain struct { // recentContextChecks tracks recent blocks that have successfully passed // all contextual checks and is primarily used as an optimization to avoid // running the checks again when possible. + // + // recentMerkleChecks tracks recent blocks that have definitively proven the + // header commits to the received data for the block. recentBlocks *lru.Map[chainhash.Hash, *dcrutil.Block] recentContextChecks *lru.Set[chainhash.Hash] + recentMerkleChecks *lru.Set[chainhash.Hash] // These fields house a cached view that represents a block that votes // against its parent and therefore contains all changes as a result @@ -1197,8 +1206,8 @@ func (b *BlockChain) reorganizeChainInternal(target *blockNode) error { // The block must pass all of the validation rules which depend on // having the full block data for all of its ancestors available. if err := b.checkBlockContext(block, n.parent, BFNone); err != nil { - var rerr RuleError - if errors.As(err, &rerr) { + var rErr RuleError + if !errors.Is(err, ErrBadMerkleRoot) && errors.As(err, &rErr) { b.index.MarkBlockFailedValidation(n) } return err @@ -2116,6 +2125,13 @@ func newRecentContextChecksCache() *lru.Set[chainhash.Hash] { return lru.NewSet[chainhash.Hash](contextCheckCacheSize) } +// newRecentMerkleChecksCache returns a new LRU cache for tracking recent +// blocks that have definitively proven the header commits to the received data +// for the block. +func newRecentMerkleChecksCache() *lru.Set[chainhash.Hash] { + return lru.NewSet[chainhash.Hash](merkleCheckCacheSize) +} + // New returns a BlockChain instance using the provided configuration details. func New(ctx context.Context, config *Config) (*BlockChain, error) { // Enforce required config fields. @@ -2207,6 +2223,7 @@ func New(ctx context.Context, config *Config) (*BlockChain, error) { bestChain: newChainView(nil), recentBlocks: newRecentBlocksCache(), recentContextChecks: newRecentContextChecksCache(), + recentMerkleChecks: newRecentMerkleChecksCache(), isVoterMajorityVersionCache: make(map[[stakeMajorityCacheKeySize]byte]bool), isStakeMajorityVersionCache: make(map[[stakeMajorityCacheKeySize]byte]bool), calcPriorStakeVersionCache: make(map[[chainhash.HashSize]byte]uint32), diff --git a/internal/blockchain/process.go b/internal/blockchain/process.go index ca18e62293..1678fc83ba 100644 --- a/internal/blockchain/process.go +++ b/internal/blockchain/process.go @@ -1,5 +1,5 @@ // Copyright (c) 2013-2016 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -150,19 +150,13 @@ func (b *BlockChain) isAssumeValidAncestor(node *blockNode) bool { // them, so those fields are not included here. This provides support for full // headers-first semantics. // -// The flag for check header sanity allows the additional header sanity checks -// to be skipped which is useful for the full block processing path which checks -// the sanity of the entire block, including the header, before attempting to -// accept its header in order to quickly eliminate blocks that are obviously -// incorrect. -// // In the case the block header is already known, the associated block node is // examined to determine if the block is already known to be invalid, in which // case an appropriate error will be returned. Otherwise, the block node is // returned. // // This function MUST be called with the chain lock held (for writes). -func (b *BlockChain) maybeAcceptBlockHeader(header *wire.BlockHeader, checkHeaderSanity bool) (*blockNode, error) { +func (b *BlockChain) maybeAcceptBlockHeader(header *wire.BlockHeader) (*blockNode, error) { // Avoid validating the header again if its validation status is already // known. Invalid headers are never added to the block index, so if there // is an entry for the block hash, the header itself is known to be valid. @@ -178,11 +172,9 @@ func (b *BlockChain) maybeAcceptBlockHeader(header *wire.BlockHeader, checkHeade } // Perform context-free sanity checks on the block header. - if checkHeaderSanity { - err := checkBlockHeaderSanity(header, b.timeSource, BFNone, b.chainParams) - if err != nil { - return nil, err - } + err := checkBlockHeaderSanity(header, b.timeSource, BFNone, b.chainParams) + if err != nil { + return nil, err } // Orphan headers are not allowed and this function should never be called @@ -204,7 +196,7 @@ func (b *BlockChain) maybeAcceptBlockHeader(header *wire.BlockHeader, checkHeade // The block header must pass all of the validation rules which depend on // its position within the block chain. - err := b.checkBlockHeaderPositional(header, prevNode, BFNone) + err = b.checkBlockHeaderPositional(header, prevNode, BFNone) if err != nil { return nil, err } @@ -256,8 +248,7 @@ func (b *BlockChain) ProcessBlockHeader(header *wire.BlockHeader) error { // index, validate it according to both context free and context dependent // positional checks, and create a block index entry for it. b.chainLock.Lock() - const checkHeaderSanity = true - _, err := b.maybeAcceptBlockHeader(header, checkHeaderSanity) + _, err := b.maybeAcceptBlockHeader(header) if err != nil { b.chainLock.Unlock() return err @@ -307,12 +298,11 @@ func (b *BlockChain) maybeAcceptBlockData(node *blockNode, block *dcrutil.Block, b.index.PopulateTicketInfo(node, ticketInfo) // The block must pass all of the validation rules which depend on the - // position of the block within the block chain. Not that this only checks + // position of the block within the block chain. Note that this only checks // the block data, not including the header, because the header was already // checked when it was accepted to the block index. err := b.checkBlockDataPositional(block, node.parent, flags) if err != nil { - b.index.MarkBlockFailedValidation(node) return nil, err } @@ -376,7 +366,7 @@ func (b *BlockChain) maybeAcceptBlocks(curTip *blockNode, nodes []*blockNode, fl // having the full block data for all of its ancestors available. if err := b.checkBlockContext(linkedBlock, n.parent, flags); err != nil { var rErr RuleError - if errors.As(err, &rErr) { + if !errors.Is(err, ErrBadMerkleRoot) && errors.As(err, &rErr) { b.index.MarkBlockFailedValidation(n) } @@ -477,41 +467,53 @@ func (b *BlockChain) ProcessBlock(block *dcrutil.Block) (int64, error) { } } - // Perform preliminary sanity checks on the block and its transactions. - // This is done prior to any attempts to accept the block data and connect - // the block to quickly eliminate blocks that are obviously incorrect and - // significantly increase the cost to attackers. Of particular note is that - // the checks include proof-of-work validation which means a significant - // amount of work must have been done in order to pass this check. - err := checkBlockSanity(block, b.timeSource, BFNone, b.chainParams) - if err != nil { - // When there is a block index entry for the block, which will be the - // case if the header was previously seen and passed all validation, - // mark it as having failed validation and all of its descendants as - // having an invalid ancestor. - if node != nil { - b.index.MarkBlockFailedValidation(node) - } - return 0, err - } - // Potentially accept the header to the block index when it does not already // exist. // // This entails fully validating it according to both context independent - // and context dependent checks and creating a block index entry for it. + // and positional checks and creating a block index entry for it. // - // Note that the header sanity checks are skipped because they were just - // performed above as part of the full block sanity checks. + // Of particular note is that the checks include proof-of-work validation + // which means a significant amount of work must have been done in order to + // add the header to the index and pass this check. if node == nil { - const checkHeaderSanity = false + var err error header := &block.MsgBlock().Header - node, err = b.maybeAcceptBlockHeader(header, checkHeaderSanity) + node, err = b.maybeAcceptBlockHeader(header) if err != nil { return 0, err } } + // The block must pass all preconditions that are required before any + // further validation of the block data. Notably, the header must commit to + // the block data to ensure the data being validated is actually the data + // for the claimed header. + // + // Until [BlockChain.checkBlockContext] succeeds, it is only safe to + // attribute failures to the block hash when these checks pass and the + // returned flag indicates the data commitment has definitively been proven. + dataCommitProven, err := b.checkBlockDataPreconditions(block, node.parent) + if err != nil { + return 0, err + } + + // Perform preliminary sanity checks on the block and its transactions. + // This is done prior to any attempts to accept the block data and connect + // the block to quickly eliminate blocks that are obviously incorrect and + // significantly increase the cost to attackers. + err = checkBlockDataSanity(block, b.chainParams) + if err != nil { + // Mark the block as having failed validation and all of its descendants + // as having an invalid ancestor when it violates a consensus rule and + // the data commitment has definitively been proven. + var rErr RuleError + if dataCommitProven && errors.As(err, &rErr) { + b.index.MarkBlockFailedValidation(node) + } + return 0, err + } + // Enable skipping some of the more expensive validation checks when the // block is both an ancestor of the assumed valid block and an ancestor of // the best header. @@ -534,6 +536,13 @@ func (b *BlockChain) ProcessBlock(block *dcrutil.Block) (int64, error) { // are now eligible for validation. linkedNodes, err := b.maybeAcceptBlockData(node, block, flags) if err != nil { + // Mark the block as having failed validation and all of its descendants + // as having an invalid ancestor when it violates a consensus rule and + // the data commitment has definitively been proven. + var rErr RuleError + if dataCommitProven && errors.As(err, &rErr) { + b.index.MarkBlockFailedValidation(node) + } return 0, err } @@ -697,6 +706,7 @@ func (b *BlockChain) InvalidateBlock(hash *chainhash.Hash) error { // all of its descendants as having an invalid ancestor when it is not part // of the current best chain. b.recentContextChecks.Delete(node.hash) + b.recentMerkleChecks.Delete(node.hash) if !b.bestChain.Contains(node) { b.index.MarkBlockFailedValidation(node) b.chainLock.Lock() @@ -824,6 +834,7 @@ func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { } b.index.unsetStatusFlags(n, statusValidateFailed|statusInvalidAncestor) b.recentContextChecks.Delete(n.hash) + b.recentMerkleChecks.Delete(n.hash) } if b.index.canValidate(n) && n.workSum.GtEq(&curBestTip.workSum) { @@ -875,6 +886,7 @@ func (b *BlockChain) ReconsiderBlock(hash *chainhash.Hash) error { for n := finalNotKnownInvalidDescendant; n != vfNode; n = n.parent { b.index.unsetStatusFlags(n, statusInvalidAncestor) b.recentContextChecks.Delete(n.hash) + b.recentMerkleChecks.Delete(n.hash) if b.index.canValidate(n) && n.workSum.GtEq(&curBestTip.workSum) { b.index.addBestChainCandidate(n) } diff --git a/internal/blockchain/process_test.go b/internal/blockchain/process_test.go index 74311ac10d..33457461d8 100644 --- a/internal/blockchain/process_test.go +++ b/internal/blockchain/process_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2022 The Decred developers +// Copyright (c) 2019-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -220,6 +220,7 @@ func genSharedProcessTestBlocks(t *testing.T) *chaingen.Generator { // Create a new database and chain instance needed to create the generator // populated with the desired blocks. params := chaincfg.RegNetParams() + forceDeploymentResult(t, params, chaincfg.VoteIDHeaderCommitments, "no") g := newChaingenHarness(t, params) // Shorter versions of useful params for convenience. @@ -743,6 +744,17 @@ func TestProcessLogic(t *testing.T) { // ... bsv0 -> ... -> bsv# -> bbm0 -> ... -> bbm# // ------------------------------------------------------------------------- + // Ensure that block data which does not match a known valid header is + // rejected without marking the header invalid so the real data is still + // accepted afterwards. + // + // ... -> bfb (mismatched data rejected, real data accepted) + { + bfb := g.BlockByName("bfb") + bfb.Transactions[0].Version++ + g.RejectBlock("bfb", ErrBadMerkleRoot) + bfb.Transactions[0].Version-- + } g.AcceptBlock("bfb") g.RejectBlock("bfb", ErrDuplicateBlock) for i := uint16(0); i < coinbaseMaturity; i++ { @@ -767,39 +779,24 @@ func TestProcessLogic(t *testing.T) { // ------------------------------------------------------------------------- // Ensure that a block that has a context-free (aka sanity) failure is - // rejected as expected. - // - // Since invalid blocks that have not had their headers added separately are - // not added to the block index, attempting to process it again must return - // the specific failure reason as opposed to a known invalid block error. - // - // This also means that adding the header of a block that has been rejected - // due to a sanity error will succeed because the original failure is not - // tracked. Thus, ensure that is the case and that processing the bad block - // again fails as expected and is marked as failed such that future attempts - // to either add the header or the block will fail due to being a known - // invalid block. + // rejected as expected. Since the header is valid it will be added to the + // index and then end up marked as invalid, so attempting to process the + // header or block again is expected to return a known invalid block error. // // ... -> bbm# // \-> b1bad // ------------------------------------------------------------------------- g.RejectBlock("b1bad", ErrNotEnoughStake) - g.RejectBlock("b1bad", ErrNotEnoughStake) - g.ExpectBestInvalidHeader("bfbbadchild") - - g.AcceptHeader("b1bad") - g.RejectBlock("b1bad", ErrNotEnoughStake) - g.ExpectBestInvalidHeader("b1bad") g.RejectHeader("b1bad", ErrKnownInvalidBlock) g.RejectBlock("b1bad", ErrKnownInvalidBlock) + g.ExpectBestInvalidHeader("b1bad") // ------------------------------------------------------------------------- // Ensure that a block that has a positional failure is rejected as - // expected. Since the header is valid and the block sanity checks pass, - // the header will be added to the index and then end up marked as invalid, - // so attempting to process the block again is expected to return a known - // invalid block error. + // expected. Since the header is valid it will be added to the index and + // then end up marked as invalid, so attempting to process the block again + // is expected to return a known invalid block error. // // ... -> bbm# // \-> b1bada diff --git a/internal/blockchain/validate.go b/internal/blockchain/validate.go index d007bce386..a7defd4df5 100644 --- a/internal/blockchain/validate.go +++ b/internal/blockchain/validate.go @@ -875,55 +875,41 @@ func checkBlockHeaderSanity(header *wire.BlockHeader, timeSource MedianTimeSourc return nil } -// checkBlockSanity performs some preliminary checks on a block to ensure it is -// sane before continuing with block processing. These checks are context -// free. -// -// The flags do not modify the behavior of this function directly, however they -// are needed to pass along to checkBlockHeaderSanity. -func checkBlockSanity(block *dcrutil.Block, timeSource MedianTimeSource, flags BehaviorFlags, chainParams *chaincfg.Params) error { - msgBlock := block.MsgBlock() - header := &msgBlock.Header - err := checkBlockHeaderSanity(header, timeSource, flags, chainParams) - if err != nil { - return err - } - +// checkBlockDataSanity performs some preliminary checks on a block and its +// transactions to ensure it is sane before continuing with block processing. +// These checks are context free. +func checkBlockDataSanity(block *dcrutil.Block, chainParams *chaincfg.Params) error { // All ticket purchases must meet the difficulty specified by the block // header. - err = checkProofOfStake(block, chainParams.MinimumStakeDiff) + err := checkProofOfStake(block, chainParams.MinimumStakeDiff) if err != nil { return err } // A block must have at least one regular transaction. + msgBlock := block.MsgBlock() numTx := len(msgBlock.Transactions) if numTx == 0 { return ruleError(ErrNoTransactions, "block does not contain "+ "any transactions") } - // A block must not exceed the maximum allowed block payload when - // serialized. + // A block must not exceed the maximum allowed block payload when serialized + // and the block header must commit to the actual block size. // - // This is a quick and context-free sanity check of the maximum block - // size according to the wire protocol. Even though the wire protocol - // already prevents blocks bigger than this limit, there are other - // methods of receiving a block that might not have been checked - // already. A separate block size is enforced later that takes into - // account the network-specific block size and the results of block - // size votes. Typically that block size is more restrictive than this - // one. + // This is a quick and context-free sanity check of the maximum block size + // according to the wire protocol. A separate consensus-specific block size + // is enforced later that takes into account the network-specific block size + // and the results of block size votes. Typically that block size is more + // restrictive than this one. serializedSize := msgBlock.SerializeSize() - if serializedSize > wire.MaxBlockPayload { - str := fmt.Sprintf("serialized block is too big - got %d, "+ - "max %d", serializedSize, wire.MaxBlockPayload) - return ruleError(ErrBlockTooBig, str) + if err := checkBlockSizeSanity(int64(serializedSize)); err != nil { + return err } + header := &msgBlock.Header if header.Size != uint32(serializedSize) { str := fmt.Sprintf("serialized block is not size indicated in "+ - "header - got %d, expected %d", header.Size, - serializedSize) + "header - got %d, expected %d", header.Size, serializedSize) return ruleError(ErrWrongBlockSize, str) } @@ -978,9 +964,24 @@ func checkBlockSanity(block *dcrutil.Block, timeSource MedianTimeSource, flags B return nil } -// CheckBlockSanity performs some preliminary checks on a block to ensure it is -// sane before continuing with block processing. These checks are context -// free. +// checkBlockSanity performs some preliminary checks on a block (both its header +// and data) to ensure it is sane before continuing with block processing. +// These checks are context free. +// +// The flags do not modify the behavior of this function directly, however they +// are needed to pass along to [checkBlockHeaderSanity]. +func checkBlockSanity(block *dcrutil.Block, timeSource MedianTimeSource, flags BehaviorFlags, chainParams *chaincfg.Params) error { + header := &block.MsgBlock().Header + err := checkBlockHeaderSanity(header, timeSource, flags, chainParams) + if err != nil { + return err + } + return checkBlockDataSanity(block, chainParams) +} + +// CheckBlockSanity performs some preliminary checks on a block (both its header +// and data) to ensure it is sane before continuing with block processing. +// These checks are context free. func CheckBlockSanity(block *dcrutil.Block, timeSource MedianTimeSource, chainParams *chaincfg.Params) error { return checkBlockSanity(block, timeSource, BFNone, chainParams) } @@ -1848,26 +1849,90 @@ func checkTicketRedeemers(voteTicketHashes, revocationTicketHashes, winners, return nil } +// checkBlockSizeSanity validates the provided serialized block does not exceed +// the maximum block size according to the wire protocol. Even though the wire +// protocol already prevents blocks bigger than this limit, there are other +// methods of receiving a block that might not have been checked already. +// +// This is NOT the same as the maximum block size allowed by consensus. +// +// The consensus block size limit is an independent value that is enforced +// separately and takes into account the network-specific block size and the +// results of block size votes. Typically that block size is more restrictive +// than this one. +// +// The purpose of this limit is to prevent DoS vectors before the context needed +// to definitively determine the independent consensus block size limit is +// available. +// +// This check is context free. +func checkBlockSizeSanity(serializedSize int64) error { + if serializedSize > wire.MaxBlockPayload { + str := fmt.Sprintf("serialized block is too big - got %d, "+ + "max %d", serializedSize, wire.MaxBlockPayload) + return ruleError(ErrBlockTooBig, str) + } + + return nil +} + +// merkleRootVariant defines the available variants for merkle root +// calculations. +type merkleRootVariant uint8 + +const ( + // mrvOriginal specifies the original merkle root semantics that were in + // effect at initial launch. In particular, the normal merkle root field of + // the header commits to the regular transaction tree and the stake root + // (also referred to as the commitment root) field of the header commits to + // the stake transaction tree. + mrvOriginal merkleRootVariant = iota + + // mrvDCP0005 specifies the updated merkle root semantics specified by + // DCP0005. In particular, the merkle root field of the header commits to + // both the regular and stake transaction trees. + mrvDCP0005 +) + // checkMerkleRoots validates the merkle root(s) in the block header match the -// calculated value(s). +// calculated value(s) according to the provided merkle root variant. It panics +// if an invalid variant is passed. // -// Prior to the activation of the header commitments agenda, the regular -// transaction tree must match the merkle root field and the stake transaction -// tree must match the stake root field. +// For [mrvOriginal], the regular transaction tree must match the merkle root +// field and the stake transaction tree must match the stake root field. // -// Conversely, when the header commitments agenda is active, the merkle root -// field of the header is required to be the root of a merkle tree that has the -// individual merkle roots of the two transaction trees as leaves. +// For [mrvDCP0005], the merkle root field of the header is required to be the +// root of a merkle tree that has the individual merkle roots of the two +// transaction trees as leaves. // -// This function MUST be called with the chain state lock held (for writes). -func (b *BlockChain) checkMerkleRoots(block *wire.MsgBlock, prevNode *blockNode) error { +// This function is safe for concurrent access. +func checkMerkleRoots(block *wire.MsgBlock, variant merkleRootVariant) error { header := &block.Header - hdrCommitmentsActive, err := b.isHeaderCommitmentsAgendaActive(prevNode) - if err != nil { - return err - } - if hdrCommitmentsActive { + switch variant { + case mrvOriginal: + // Build merkle tree and ensure the calculated merkle root matches the + // entry in the block header. + wantMerkleRoot := standalone.CalcTxTreeMerkleRoot(block.Transactions) + if header.MerkleRoot != wantMerkleRoot { + str := fmt.Sprintf("block merkle root is invalid - block header "+ + "indicates %v, but calculated value is %v", header.MerkleRoot, + wantMerkleRoot) + return ruleError(ErrBadMerkleRoot, str) + } + + // Build the stake tx tree merkle root too and check it. + wantStakeRoot := standalone.CalcTxTreeMerkleRoot(block.STransactions) + if header.StakeRoot != wantStakeRoot { + str := fmt.Sprintf("block stake merkle root is invalid - block "+ + "header indicates %v, but calculated value is %v", + header.StakeRoot, wantStakeRoot) + return ruleError(ErrBadMerkleRoot, str) + } + + return nil + + case mrvDCP0005: // Build the two merkle trees and use their calculated merkle roots as // leaves to another merkle tree and ensure the final calculated merkle // root matches the entry in the block header. @@ -1883,28 +1948,133 @@ func (b *BlockChain) checkMerkleRoots(block *wire.MsgBlock, prevNode *blockNode) return nil } - // Fall back to the old behavior. + panic(fmt.Sprintf("unknown merkle root algorithm specified - %d", variant)) +} - // Build merkle tree and ensure the calculated merkle root matches the - // entry in the block header. - wantMerkleRoot := standalone.CalcTxTreeMerkleRoot(block.Transactions) - if header.MerkleRoot != wantMerkleRoot { - str := fmt.Sprintf("block merkle root is invalid - block header "+ - "indicates %v, but calculated value is %v", header.MerkleRoot, - wantMerkleRoot) - return ruleError(ErrBadMerkleRoot, str) +// checkBlockDataPreconditions performs checks that must be completed before any +// further validation of the block data. In particular, it ensures the +// serialized block is within the applicable size limit and that its header +// commits to the transaction data. The returned boolean indicates whether or +// not the data commitment is definitively known to be valid. +// +// These checks present a challenge by causing a circular dependency. They must +// be performed before further validation of the block data because failures in +// uncommitted data cannot safely be attributed to the block header. However, +// before [blockIndex.CanValidate] is true, the active agendas required to apply +// these checks cannot necessarily be determined because they depend on votes +// contained in ancestor block data. +// +// Since the activation results of all currently relevant agendas are fixed +// historical facts, the circular dependency is currently resolved by relying on +// the well-known activation blocks for each supported network. +// +// Any future consensus changes that affect checks performed by this function +// require great care to avoid introducing incorrect or exploitable behavior. +// In particular, when a check depends on an agenda whose state cannot yet be +// determined, this early validation path must conservatively account for every +// consensus rule that could apply. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) checkBlockDataPreconditions(block *dcrutil.Block, prevNode *blockNode) (bool, error) { + // A block must not exceed the maximum allowed block payload when + // serialized. + // + // This is a quick and context-free sanity check of the maximum block size + // according to the wire protocol. A separate consensus-specific block size + // is enforced later that takes into account the network-specific block size + // and the results of block size votes. Typically that block size is more + // restrictive than this one. + msgBlock := block.MsgBlock() + serializedSize := msgBlock.SerializeSize() + if err := checkBlockSizeSanity(int64(serializedSize)); err != nil { + return false, err } - // Build the stake tx tree merkle root too and check it. - wantStakeRoot := standalone.CalcTxTreeMerkleRoot(block.STransactions) - if header.StakeRoot != wantStakeRoot { - str := fmt.Sprintf("block stake merkle root is invalid - block header "+ - "indicates %v, but calculated value is %v", header.StakeRoot, - wantStakeRoot) - return ruleError(ErrBadMerkleRoot, str) + // As described by the function comment, the ability to determine whether or + // not the header commitments agenda is active is not guaranteed here. So, + // attempt to determine the agenda state from positional data. + // + // The positional agenda determination makes use of fixed historical facts + // which means that it will always resolve to a known state for the main and + // test networks. + // + // For other networks, it may or may not be able to determine the state and + // that case is handled below. + // + // When the agenda state can be definitively determined, validate the header + // commits to merkle root(s) of the transaction trees using the specific + // algorithm per the active status and return true to signal that the data + // is definitively known to be valid. + const agendaID = chaincfg.VoteIDHeaderCommitments + agendaInfo, err := b.isAgendaActivePositionalByID(prevNode, agendaID) + if err != nil { + return false, err } + if agendaInfo.isValid { + merkleVariant := mrvOriginal + if agendaInfo.isActive { + merkleVariant = mrvDCP0005 + } + if err := checkMerkleRoots(msgBlock, merkleVariant); err != nil { + return false, err + } - return nil + // Mark the block header as having definitively been proven to commit to + // the data to avoid computing and checking again when processing in the + // typical case. + // + // This must only be set when the agenda state is definitively known. + // + // Since the cache is limited in size, it is technically possible that + // entries that would otherwise be useful are evicted. The only effect + // in that case is having to validate the merkle roots again later. + blockHash := block.Hash() + b.recentMerkleChecks.Put(*blockHash) + + return true, nil + } + + // The agenda status could not be definitively determined, so permit both + // possibilities and allow the contextual checks that happen later to ensure + // validity for the correct algorithm as determined by the agenda state. + // + // The false return indicates the data commitment is not definitively known + // to be valid. That is, it is only provisionally valid when the error is + // nil. + err = checkMerkleRoots(msgBlock, mrvOriginal) + if err != nil { + err = checkMerkleRoots(msgBlock, mrvDCP0005) + } + return false, err +} + +// checkMerkleRootsContext validates the merkle root(s) in the block header +// match the calculated value(s). +// +// Prior to the activation of the header commitments agenda, the regular +// transaction tree must match the merkle root field and the stake transaction +// tree must match the stake root field. +// +// Conversely, when the header commitments agenda is active, the merkle root +// field of the header is required to be the root of a merkle tree that has the +// individual merkle roots of the two transaction trees as leaves. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) checkMerkleRootsContext(block *wire.MsgBlock, prevNode *blockNode) error { + // The expected merkle root(s) depend on the state of the header commitments + // agenda. An earlier positional check ensures that they match at least one + // of the possible algorithms. Ensure they actually match the correct + // algorithm now that the full context is available to determine the agenda + // status. + hdrCmtsActive, err := b.isHeaderCommitmentsAgendaActive(prevNode) + if err != nil { + return err + } + merkleVariant := mrvOriginal + if hdrCmtsActive { + merkleVariant = mrvDCP0005 + } + return checkMerkleRoots(block, merkleVariant) } // checkBlockContext performs several validation checks on the block which @@ -1938,7 +2108,8 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode } // No need to check the block again when it has already been checked. - if b.recentContextChecks.Contains(*block.Hash()) { + blockHash := block.Hash() + if b.recentContextChecks.Contains(*blockHash) { return nil } @@ -1951,6 +2122,26 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode return err } + // The calculated merkle root(s) of the transaction trees must match the + // associated entries in the header. + // + // This check must happen prior to any further checks of the block data to + // ensure the block data being validated is actually the data for the + // claimed header. + // + // No need to check the merkle roots again when they have already been + // proven valid. + if !b.recentMerkleChecks.Contains(*blockHash) { + if err := b.checkMerkleRootsContext(msgBlock, prevNode); err != nil { + return err + } + + // Mark the block header as having been definitively proven to commit to + // the data to avoid computing and checking the merkle roots again later + // during block processing. + b.recentMerkleChecks.Put(*blockHash) + } + // Create agenda flags for checking transactions based on which ones are // active as of the block being checked. checkTxFlags, err := b.determineCheckTxFlags(prevNode) @@ -2230,13 +2421,6 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode return ruleError(ErrBlockTooBig, str) } - // The calculated merkle root(s) of the transaction trees must match the - // associated entries in the header. - err = b.checkMerkleRoots(block.MsgBlock(), prevNode) - if err != nil { - return err - } - fastAdd := flags&BFFastAdd == BFFastAdd if !fastAdd { // Switch to using the past median time of the block prior to the block @@ -4491,6 +4675,12 @@ func (b *BlockChain) CheckConnectBlockTemplate(block *dcrutil.Block) error { return ruleError(ErrInvalidTemplateParent, str) } + // The block must pass all preconditions that are required before any + // further validation of the block data. + if _, err := b.checkBlockDataPreconditions(block, prevNode); err != nil { + return err + } + // Perform context-free sanity checks on the block and its transactions. err := checkBlockSanity(block, b.timeSource, flags, b.chainParams) if err != nil {