Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/blockchain/agendas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
21 changes: 19 additions & 2 deletions internal/blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
Expand Down
96 changes: 54 additions & 42 deletions internal/blockchain/process.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Comment thread
davecgh marked this conversation as resolved.
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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
43 changes: 20 additions & 23 deletions internal/blockchain/process_test.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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")
Comment thread
davecgh marked this conversation as resolved.
g := newChaingenHarness(t, params)

// Shorter versions of useful params for convenience.
Expand Down Expand Up @@ -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++ {
Expand All @@ -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
Expand Down
Loading