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
7 changes: 3 additions & 4 deletions cmd/addblock/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (

"github.com/blinklabs-io/handshake-node/blockchain"
"github.com/blinklabs-io/handshake-node/blockchain/indexers"
"github.com/blinklabs-io/handshake-node/hnsutil"
"github.com/blinklabs-io/handshake-node/chaincfg/chainhash"
"github.com/blinklabs-io/handshake-node/database"
"github.com/blinklabs-io/handshake-node/hnsutil"
"github.com/blinklabs-io/handshake-node/wire"
)

Expand Down Expand Up @@ -127,10 +127,9 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) {
}
}

// Ensure the blocks follows all of the chain rules and match up to the
// Ensure the block follows all of the chain rules and matches up to the
// known checkpoints.
isMainChain, isOrphan, err := bi.chain.ProcessBlock(block,
blockchain.BFFastAdd)
isMainChain, isOrphan, err := bi.chain.ProcessBlock(block, blockchain.BFNone)
if err != nil {
return false, err
}
Expand Down
127 changes: 127 additions & 0 deletions cmd/addblock/import_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) 2026 Blink Labs Software
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package main

import (
"errors"
"math"
"path/filepath"
"testing"
"time"

"github.com/blinklabs-io/handshake-node/blockchain"
"github.com/blinklabs-io/handshake-node/chaincfg"
"github.com/blinklabs-io/handshake-node/chaincfg/chainhash"
"github.com/blinklabs-io/handshake-node/database"
_ "github.com/blinklabs-io/handshake-node/database/ffldb"
"github.com/blinklabs-io/handshake-node/hnsutil"
"github.com/blinklabs-io/handshake-node/txscript"
"github.com/blinklabs-io/handshake-node/wire"
)

func solveImportTestBlock(t *testing.T, header *wire.BlockHeader,
params *chaincfg.Params) {

t.Helper()
target := blockchain.CompactToBig(params.PowLimitBits)
for nonce := uint32(0); nonce < math.MaxUint32; nonce++ {
header.Nonce = nonce
hash := header.BlockHash()
if blockchain.HashToBig(&hash).Cmp(target) <= 0 {
return
}
}
t.Fatal("failed to solve import test block")
}

func invalidImportCoinbaseBlock(t *testing.T,
params *chaincfg.Params) *hnsutil.Block {

t.Helper()
height := int32(1)
coinbase := wire.NewMsgTx(wire.TxVersion)
coinbase.LockTime = uint32(height + 1)
heightScript, err := txscript.NewScriptBuilder().
AddInt64(int64(height)).
AddOp(txscript.OP_0).
Script()
if err != nil {
t.Fatalf("coinbase height script: %v", err)
}
coinbase.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: wire.MaxPrevOutIndex,
},
Sequence: wire.MaxTxInSequenceNum,
Witness: wire.TxWitness{heightScript},
})
coinbase.AddTxOut(&wire.TxOut{
Value: blockchain.CalcBlockSubsidy(height, params),
Address: wire.Address{
Version: 0,
Hash: make([]byte, 20),
},
})

txns := []*hnsutil.Tx{hnsutil.NewTx(coinbase)}
header := wire.BlockHeader{
Version: 2,
PrevBlock: *params.GenesisHash,
MerkleRoot: blockchain.CalcMerkleRoot(txns, false),
WitnessRoot: blockchain.CalcMerkleRoot(txns, true),
Timestamp: params.GenesisBlock.Header.Timestamp.Add(time.Minute),
Bits: params.PowLimitBits,
}
solveImportTestBlock(t, &header, params)

return hnsutil.NewBlock(&wire.MsgBlock{
Header: header,
Transactions: []*wire.MsgTx{coinbase},
})
}

func TestProcessBlockFullyValidatesImports(t *testing.T) {
params := chaincfg.RegressionNetParams
params.Checkpoints = nil
dbPath := filepath.Join(t.TempDir(), "ffldb")
db, err := database.Create("ffldb", dbPath, params.Net)
if err != nil {
t.Fatalf("database.Create: %v", err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("database close: %v", err)
}
})

chain, err := blockchain.New(&blockchain.Config{
DB: db,
ChainParams: &params,
TimeSource: blockchain.NewMedianTime(),
UtxoCacheMaxSize: 16 * 1024 * 1024,
})
if err != nil {
t.Fatalf("blockchain.New: %v", err)
}
block := invalidImportCoinbaseBlock(t, &params)
serialized, err := block.Bytes()
if err != nil {
t.Fatalf("serialize block: %v", err)
}

imported, err := (&blockImporter{chain: chain}).processBlock(serialized)
if imported {
t.Fatal("consensus-invalid block was imported")
}
var ruleErr blockchain.RuleError
if !errors.As(err, &ruleErr) {
t.Fatalf("processBlock error = %v, want blockchain.RuleError", err)
}
if ruleErr.ErrorCode != blockchain.ErrBadCoinbaseHeight {
t.Fatalf("processBlock error code = %v, want %v",
ruleErr.ErrorCode, blockchain.ErrBadCoinbaseHeight)
}
}
11 changes: 11 additions & 0 deletions netsync/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,17 @@ func (sm *SyncManager) checkHeadersList(blockHash *chainhash.Hash) (
return false, blockchain.BFNone
}

// A checkpoint can only prove the validity of blocks on its ancestor path.
// Since IsValidHeader requires a header to be in the linear best-header
// view, requiring both the block and checkpoint to be in that view proves
// the block is an ancestor of the checkpoint. Do not fast-add while the
// future checkpoint is still unknown, since a peer could otherwise end a
// valid-PoW header chain before the checkpoint and supply invalid block
// bodies that bypass full validation.
if !sm.chain.IsValidHeader(checkpoint.Hash) {
return false, blockchain.BFNone
}

behaviorFlags |= blockchain.BFFastAdd
if blockHash.IsEqual(checkpoint.Hash) {
isCheckpointBlock = true
Expand Down
21 changes: 19 additions & 2 deletions netsync/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,9 @@ func TestCheckHeadersList(t *testing.T) {
sm, tearDown := makeMockSyncManager(t, &params)
defer tearDown()

// Setup SyncManager with headers processed.
for _, block := range blocks[:checkpointHeight] {
// Process headers up to, but not including, the checkpoint. A future
// checkpoint that is not known yet must not authorize fast-add.
for _, block := range blocks[:checkpointHeight-1] {
isMainChain, err := sm.chain.ProcessBlockHeader(
&block.MsgBlock().Header, blockchain.BFNone, false)
if err != nil {
Expand All @@ -437,6 +438,22 @@ func TestCheckHeadersList(t *testing.T) {
block.Hash())
}
}
sm.ibdMode = true
isCheckpoint, gotFlags := sm.checkHeadersList(
blocks[checkpointHeight-2].Hash(),
)
require.False(t, isCheckpoint)
require.Equal(t, blockchain.BFNone, gotFlags)

// Once the checkpoint itself is known on the same best-header path, it
// anchors all earlier headers on that path and fast-add is safe.
for _, block := range blocks[checkpointHeight-1:] {
isMainChain, err := sm.chain.ProcessBlockHeader(
&block.MsgBlock().Header, blockchain.BFNone, false)
require.NoError(t, err)
require.True(t, isMainChain,
"expected block header %v to be in the main chain", block.Hash())
}

tests := []struct {
hash *chainhash.Hash
Expand Down