Skip to content

Commit

Permalink
Merge pull request #22 from samay-kothari/headerValidation
Browse files Browse the repository at this point in the history
Header validation
  • Loading branch information
kcalvinalvin authored Aug 4, 2022
2 parents 7e21d3d + 4c2d0d2 commit 53dc2e1
Show file tree
Hide file tree
Showing 14 changed files with 562 additions and 12 deletions.
75 changes: 75 additions & 0 deletions blockchain/accept.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2018-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand All @@ -9,6 +10,7 @@ import (

"github.com/utreexo/utreexod/btcutil"
"github.com/utreexo/utreexod/database"
"github.com/utreexo/utreexod/wire"
)

// maybeAcceptBlock potentially accepts a block into the block chain and, if
Expand Down Expand Up @@ -90,3 +92,76 @@ func (b *BlockChain) maybeAcceptBlock(block *btcutil.Block, flags BehaviorFlags)

return isMainChain, nil
}

// maybeAcceptBlockHeader potentially accepts the header to the block index and,
// if accepted, returns the block node associated with the header. It performs
// several context independent checks as well as those which depend on its
// position within the chain.
//
// 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) {
// 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.
// However, it might have since been marked invalid either due to the
// associated block, or an ancestor, later failing validation.
hash := header.BlockHash()
if node := b.index.LookupNode(&hash); node != nil {
if err := b.checkKnownInvalidBlock(node); err != nil {
return nil, err
}

return node, nil
}

// Perform context-free sanity checks on the block header.
if checkHeaderSanity {
err := checkBlockHeaderSanity(header, b.chainParams.PowLimit, b.timeSource, BFNone)
if err != nil {
return nil, err
}
}
// Orphan headers are not allowed and this function should never be called
// with the genesis block.
prevHash := &header.PrevBlock
prevNode := b.index.LookupNode(prevHash)
if prevNode == nil {
str := fmt.Sprintf("previous block %s is not known", prevHash)
return nil, ruleError(ErrMissingParent, str)
}

// There is no need to validate the header if an ancestor is already known
// to be invalid.
prevNodeStatus := b.index.NodeStatus(prevNode)
if prevNodeStatus.KnownInvalid() {
str := fmt.Sprintf("previous block %s is known to be invalid", prevHash)
return nil, ruleError(ErrInvalidAncestorBlock, str)
}

// The block must pass all of the validation rules which depend on the
// position of the block within the block chain.
err := b.checkBlockHeaderContext(header, prevNode, BFNone)
if err != nil {
return nil, err
}

// Create a new block node for the block and add it to the block index.
//
// Note that the additional information for the actual transactions and
// witnesses in the block can't be populated until the full block data is
// known since that information is not available in the header.
newNode := newBlockNode(header, prevNode)
b.index.AddNode(newNode)
return newNode, nil
}
18 changes: 18 additions & 0 deletions blockchain/blockindex.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2015-2017 The btcsuite developers
// Copyright (c) 2018-2021 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 @@ -60,6 +61,22 @@ func (status blockStatus) KnownInvalid() bool {
return status&(statusValidateFailed|statusInvalidAncestor) != 0
}

// KnownInvalidAncestor returns whether the block is known to have an invalid
// ancestor. A return value of false in no way implies the block only has valid
// ancestors. Thus, this will return false for blocks with invalid ancestors
// that have not been proven invalid yet.
func (status blockStatus) KnownInvalidAncestor() bool {
return status&(statusInvalidAncestor) != 0
}

// KnownValidateFailed returns whether the block is known to have failed
// validation. A return value of false in no way implies the block is valid.
// Thus, this will return false for blocks that have not been proven to fail
// validation yet.
func (status blockStatus) KnownValidateFailed() bool {
return status&(statusValidateFailed) != 0
}

// blockNode represents a block within the block chain and is primarily used to
// aid in selecting the best chain to be the main chain. The main chain is
// stored into the block database.
Expand Down Expand Up @@ -114,6 +131,7 @@ func initBlockNode(node *blockNode, blockHeader *wire.BlockHeader, parent *block
nonce: blockHeader.Nonce,
timestamp: blockHeader.Timestamp.Unix(),
merkleRoot: blockHeader.MerkleRoot,
status: statusNone,
}
if parent != nil {
node.parent = parent
Expand Down
21 changes: 21 additions & 0 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,27 @@ func (b *BlockChain) BlockLocatorFromHash(hash *chainhash.Hash) BlockLocator {
return locator
}

// IndexLookupNode returns the block node identified by the provided hash. It will
// return nil if there is no entry for the hash.
//
// This function is safe for concurrent access.
func (b *BlockChain) IndexLookupNode(hash *chainhash.Hash) *blockNode {
b.chainLock.RLock()
node := b.index.LookupNode(hash)
b.chainLock.RUnlock()
return node
}

// IndexNodeStatus provides concurrent-safe access to the status field of a node.
//
// This function is safe for concurrent access.
func (b *BlockChain) IndexNodeStatus(node *blockNode) blockStatus {
b.chainLock.RLock()
status := b.index.NodeStatus(node)
b.chainLock.RUnlock()
return status
}

// LatestBlockLocator returns a block locator for the latest known tip of the
// main (best) chain.
//
Expand Down
2 changes: 1 addition & 1 deletion blockchain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestHaveBlock(t *testing.T) {
}

// Create a new database and chain instance to run tests against.
chain, teardownFunc, err := chainSetup("haveblock",
chain, teardownFunc, err := ChainSetup("haveblock",
&chaincfg.MainNetParams)
if err != nil {
t.Errorf("Failed to setup chain instance: %v", err)
Expand Down
4 changes: 2 additions & 2 deletions blockchain/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,10 @@ func loadBlocks(filename string) (blocks []*btcutil.Block, err error) {
return
}

// chainSetup is used to create a new db and chain instance with the genesis
// ChainSetup is used to create a new db and chain instance with the genesis
// block already inserted. In addition to the new chain instance, it returns
// a teardown function the caller should invoke when done testing to clean up.
func chainSetup(dbName string, params *chaincfg.Params) (*BlockChain, func(), error) {
func ChainSetup(dbName string, params *chaincfg.Params) (*BlockChain, func(), error) {
if !IsSupportedDbType(testDbType) {
return nil, nil, fmt.Errorf("unsupported db type %v", testDbType)
}
Expand Down
9 changes: 9 additions & 0 deletions blockchain/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ const (
// current chain tip. This is not a block validation rule, but is required
// for block proposals submitted via getblocktemplate RPC.
ErrPrevBlockNotBest

// ErrKnownInvalidBlock indicates that this block has previously failed
// validation.
ErrKnownInvalidBlock

// ErrMissingParent indicates that the block was an orphan.
ErrMissingParent
)

// Map of ErrorCode values back to their constant names for pretty printing.
Expand Down Expand Up @@ -267,6 +274,8 @@ var errorCodeStrings = map[ErrorCode]string{
ErrPreviousBlockUnknown: "ErrPreviousBlockUnknown",
ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock",
ErrPrevBlockNotBest: "ErrPrevBlockNotBest",
ErrKnownInvalidBlock: "ErrKnownInvalidBlock",
ErrMissingParent: "ErrMissingParent",
}

// String returns the ErrorCode as a human-readable name.
Expand Down
6 changes: 3 additions & 3 deletions blockchain/fullblocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ func isSupportedDbType(dbType string) bool {
return false
}

// chainSetup is used to create a new db and chain instance with the genesis
// ChainSetup is used to create a new db and chain instance with the genesis
// block already inserted. In addition to the new chain instance, it returns
// a teardown function the caller should invoke when done testing to clean up.
func chainSetup(dbName string, params *chaincfg.Params) (*blockchain.BlockChain, func(), error) {
func ChainSetup(dbName string, params *chaincfg.Params) (*blockchain.BlockChain, func(), error) {
if !isSupportedDbType(testDbType) {
return nil, nil, fmt.Errorf("unsupported db type %v", testDbType)
}
Expand Down Expand Up @@ -138,7 +138,7 @@ func TestFullBlocks(t *testing.T) {
}

// Create a new database and chain instance to run tests against.
chain, teardownFunc, err := chainSetup("fullblocktest",
chain, teardownFunc, err := ChainSetup("fullblocktest",
&chaincfg.RegressionNetParams)
if err != nil {
t.Errorf("Failed to setup chain instance: %v", err)
Expand Down
132 changes: 132 additions & 0 deletions blockchain/fullblocktests/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) 2022 The utreexod developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package fullblocktests

import (
"testing"

"github.com/utreexo/utreexod/blockchain"
)

type chaingenHarness struct {
*testGenerator

t *testing.T
chain *blockchain.BlockChain
}

// AcceptHeader processes the block header associated with the given name in the
// harness generator and expects it to be accepted, but not necessarily to the
// main chain. It also ensures the underlying block index is consistent with
// the result.
func (g *chaingenHarness) AcceptHeader(blockName string) {
g.t.Helper()

header := &g.blockByName(blockName).Header
blockHash := header.BlockHash()
g.t.Logf("Testing accept block header %q (hash %s)", blockName,
blockHash)

// Determine if the header is already known before attempting to process it.
alreadyHaveHeader := g.chain.IndexLookupNode(&blockHash) != nil
err := g.chain.ProcessBlockHeader(header)
if err != nil {
g.t.Fatalf("block header %q (hash %s) should have been "+
"accepted: %v", blockName, blockHash, err)
}

// Ensure the accepted header now exists in the block index.
node := g.chain.IndexLookupNode(&blockHash)
if node == nil {
g.t.Fatalf("accepted block header %q (hash %s) should have "+
"been added to the block index", blockName, blockHash)
}

// Ensure the accepted header is not marked as known valid when it was not
// previously known since that implies the block data is not yet available
// and therefore it can't possibly be known to be valid.
//
// Also, ensure the accepted header is not marked as known invalid, as
// having known invalid ancestors, or as known to have failed validation.
status := g.chain.IndexNodeStatus(node)
if !alreadyHaveHeader && status.KnownValid() {
g.t.Fatalf("accepted block header %q (hash %s) was not "+
"already known, but is marked as known valid", blockName, blockHash,
)
}
if status.KnownInvalid() {
g.t.Fatalf("accepted block header %q (hash %s) is marked "+
"as known invalid", blockName, blockHash)
}
if status.KnownInvalidAncestor() {
g.t.Fatalf("accepted block header %q (hash %s) is marked "+
"as having a known invalid ancestor", blockName, blockHash,
)
}
if status.KnownValidateFailed() {
g.t.Fatalf("accepted block header %q (hash %s) is marked "+
"as having known to fail validation", blockName, blockHash,
)
}
}

// RejectHeader expects the block header associated with the given name in the
// harness generator to be rejected with the provided error code and also
// ensures the underlying block index is consistent with the result.
func (g *chaingenHarness) RejectHeader(blockName string, code blockchain.ErrorCode) {
g.t.Helper()

header := &g.blockByName(blockName).Header
blockHash := header.BlockHash()
g.t.Logf("Testing reject block header %q (hash %s, reason %v)",
blockName, blockHash, code)

// Determine if the header is already known before attempting to process it.
alreadyHaveHeader := g.chain.IndexLookupNode(&blockHash) != nil

err := g.chain.ProcessBlockHeader(header)
if err == nil {
g.t.Fatalf("block header %q (hash %s) should not have been "+
"accepted", blockName, blockHash)
}

// Ensure the error matches the value specified in the test instance.
rerr, ok := err.(blockchain.RuleError)
if (!ok) || rerr.ErrorCode != code {
g.t.Fatalf("block header %q (hash %s) does not have "+
"expected reject code -- got %v, want %v", blockName, blockHash,
err, code)
}

// Ensure the rejected header was not added to the block index when it was
// not already previously successfully added and that it was not removed if
// it was already previously added.
node := g.chain.IndexLookupNode(&blockHash)
switch {
case !alreadyHaveHeader && node == nil:
// Header was not added as expected.
return

case !alreadyHaveHeader && node != nil:
g.t.Fatalf("rejected block header %q (hash %s) was added "+
"to the block index", blockName, blockHash)

case alreadyHaveHeader && node == nil:
g.t.Fatalf("rejected block header %q (hash %s) was removed "+
"from the block index", blockName, blockHash)
}

// The header was previously added, so ensure it is not reported as having
// been validated and that it is now known invalid.
status := g.chain.IndexNodeStatus(node)
if status.KnownValid() {
g.t.Fatalf("rejected block header %q (hash %s) is marked "+
"as known valid", blockName, blockHash)
}
if !status.KnownInvalid() {
g.t.Fatalf("rejected block header %q (hash %s) is NOT "+
"marked as known invalid", blockName, blockHash)
}
}
Loading

0 comments on commit 53dc2e1

Please sign in to comment.