Skip to content
Draft
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
8 changes: 8 additions & 0 deletions blockchain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,7 @@ func TestInvalidateBlock(t *testing.T) {
"invalidate-once")
// Grab the tip of the chain.
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)

// Create a chain with 11 blocks.
_, _, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{})
Expand All @@ -1407,6 +1408,7 @@ func TestInvalidateBlock(t *testing.T) {
chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-invalidate-twice")
// Grab the tip of the chain.
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)

// Create a chain with 11 blocks.
_, spendableOuts, err := addBlocks(11, chain, tip, []*testhelper.SpendableOut{})
Expand Down Expand Up @@ -1453,6 +1455,7 @@ func TestInvalidateBlock(t *testing.T) {
chainGen: func() (*BlockChain, []*chainhash.Hash, func()) {
chain, params, tearDown := utxoCacheTestChain("TestInvalidateBlock-invalidate-side-branch")
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)

// Grab the tip of the chain.
tip, err := chain.BlockByHash(&chain.bestChain.Tip().hash)
Expand Down Expand Up @@ -1635,6 +1638,7 @@ func TestReconsiderBlock(t *testing.T) {

// Create a chain with 101 blocks.
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)
_, _, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{})
if err != nil {
t.Fatal(err)
Expand All @@ -1657,6 +1661,7 @@ func TestReconsiderBlock(t *testing.T) {

// Create a chain with 101 blocks.
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)
_, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1689,6 +1694,7 @@ func TestReconsiderBlock(t *testing.T) {

// Create a chain with 101 blocks.
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)
_, spendableOuts, err := addBlocks(101, chain, tip, []*testhelper.SpendableOut{})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1718,6 +1724,7 @@ func TestReconsiderBlock(t *testing.T) {
chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-higher")

tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)
_, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1752,6 +1759,7 @@ func TestReconsiderBlock(t *testing.T) {
chain, params, tearDown := utxoCacheTestChain("TestReconsiderBlock-reconsider-an-invalid-side-branch-lower")

tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)
_, spendableOuts, err := addBlocks(6, chain, tip, []*testhelper.SpendableOut{})
if err != nil {
t.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion blockchain/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ func newBlock(chain *BlockChain, prev *btcutil.Block,
// SolveBlock.
block := btcutil.NewBlock(&wire.MsgBlock{
Header: wire.BlockHeader{
Version: 1,
Version: 4,
PrevBlock: *prev.Hash(),
MerkleRoot: calcMerkleRoot(txns),
Bits: chain.chainParams.PowLimitBits,
Expand Down
42 changes: 39 additions & 3 deletions blockchain/fullblocktests/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,30 @@ func replaceCoinbaseSigScript(script []byte) func(*wire.MsgBlock) {
}
}

// padCoinbaseSigScript returns a function that pads the existing coinbase
// signature script with OP_0 until it reaches the provided length. It keeps the
// existing prefix (including the serialized height) intact.
func padCoinbaseSigScript(targetLen int) func(*wire.MsgBlock) {
return func(b *wire.MsgBlock) {
sigScript := b.Transactions[0].TxIn[0].SignatureScript
if len(sigScript) > targetLen {
panic(fmt.Sprintf("padCoinbaseSigScript: script len "+
"%d > target %d", len(sigScript), targetLen))
}

if len(sigScript) == targetLen {
return
}

padding := bytes.Repeat(
[]byte{txscript.OP_0}, targetLen-len(sigScript),
)
b.Transactions[0].TxIn[0].SignatureScript = append(
sigScript, padding...,
)
}
}

// additionalTx returns a function that itself takes a block and modifies it by
// adding the provided transaction.
func additionalTx(tx *wire.MsgTx) func(*wire.MsgBlock) {
Expand Down Expand Up @@ -355,7 +379,7 @@ func (g *testGenerator) nextBlock(blockName string, spend *testhelper.SpendableO

block := wire.MsgBlock{
Header: wire.BlockHeader{
Version: 1,
Version: 4,
PrevBlock: g.tip.BlockHash(),
MerkleRoot: calcMerkleRoot(txns),
Bits: g.params.PowLimitBits,
Expand Down Expand Up @@ -1044,8 +1068,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
//
// ... -> b23(6) -> b30(7)
g.setTip("b23")
maxSizeCbScript := repeatOpcode(0x00, maxCoinbaseScriptLen)
g.nextBlock("b30", outs[7], replaceCoinbaseSigScript(maxSizeCbScript))
g.nextBlock("b30", outs[7], padCoinbaseSigScript(maxCoinbaseScriptLen))
accepted()

// ---------------------------------------------------------------------
Expand Down Expand Up @@ -1576,6 +1599,19 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
parent := g.blocks[b.Header.PrevBlock]
b.Transactions[0] = parent.Transactions[0]
})
rejected(blockchain.ErrBadCoinbaseHeight)

// Create block that duplicates a non-coinbase transaction from an
// earlier block to trigger BIP30 rejection (duplicate txid in UTXO).
//
// ... -> b60(17)
// \-> b61dup(18)
g.setTip("b60")
g.nextBlock("b61dup", outs[18], func(b *wire.MsgBlock) {
parent := g.blocks[b.Header.PrevBlock]
dupTx := parent.Transactions[1].Copy()
b.AddTransaction(dupTx)
})
rejected(blockchain.ErrOverwriteTx)

// ---------------------------------------------------------------------
Expand Down
6 changes: 3 additions & 3 deletions blockchain/fullblocktests/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ var regressionNetParams = &chaincfg.Params{
PowLimit: regressionPowLimit,
PowLimitBits: 0x207fffff,
CoinbaseMaturity: 100,
BIP0034Height: 100000000, // Not active - Permit ver 1 blocks
BIP0065Height: 1351, // Used by regression tests
BIP0066Height: 1251, // Used by regression tests
BIP0034Height: 1,
BIP0065Height: 1,
BIP0066Height: 1,
SubsidyReductionInterval: 150,
TargetTimespan: time.Hour * 24 * 14, // 14 days
TargetTimePerBlock: time.Minute * 10, // 10 minutes
Expand Down
218 changes: 218 additions & 0 deletions blockchain/regtest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package blockchain

import (
"testing"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
)

// stubChainCtx provides the minimal ChainCtx implementation needed for header
// context checks in tests.
type stubChainCtx struct {
params *chaincfg.Params
}

// ChainParams returns the active chain parameters.
func (s stubChainCtx) ChainParams() *chaincfg.Params {
return s.params
}

// BlocksPerRetarget returns the blocks per difficulty retarget.
func (s stubChainCtx) BlocksPerRetarget() int32 {
return int32(s.params.TargetTimespan / s.params.TargetTimePerBlock)
}

// MinRetargetTimespan returns the lower bound of the retarget timespan.
func (s stubChainCtx) MinRetargetTimespan() int64 {
return int64(
s.params.TargetTimespan /
time.Duration(s.params.RetargetAdjustmentFactor),
)
}

// MaxRetargetTimespan returns the upper bound of the retarget timespan.
func (s stubChainCtx) MaxRetargetTimespan() int64 {
return int64(
s.params.TargetTimespan *
time.Duration(s.params.RetargetAdjustmentFactor),
)
}

// VerifyCheckpoint reports whether a checkpoint matches.
func (s stubChainCtx) VerifyCheckpoint(_ int32, _ *chainhash.Hash) bool {
return true
}

// FindPreviousCheckpoint returns the last known checkpoint.
func (s stubChainCtx) FindPreviousCheckpoint() (HeaderCtx, error) {
return nil, nil
}

// regtestPrevNode returns a blockNode for the regtest genesis block to serve
// as the parent of height-1 test headers.
func regtestPrevNode(t *testing.T) *blockNode {
t.Helper()

params := chaincfg.RegressionNetParams

return newBlockNode(&params.GenesisBlock.Header, nil)
}

// TestRegtestBlockVersions ensures regtest enforces BIP34/66/65 version floors
// from height 1.
func TestRegtestBlockVersions(t *testing.T) {
params := chaincfg.RegressionNetParams
prevNode := regtestPrevNode(t)

testCases := []struct {
name string
version int32
wantErr ErrorCode
}{
{
name: "v1_rejected",
version: 1,
wantErr: ErrBlockVersionTooOld,
},
{
name: "v2_rejected",
version: 2,
wantErr: ErrBlockVersionTooOld,
},
{
name: "v3_rejected",
version: 3,
wantErr: ErrBlockVersionTooOld,
},
{
name: "v4_allowed",
version: 4,
wantErr: ErrorCode(0),
},
{
name: "vb_signal_allowed",
version: 0x20000000,
wantErr: ErrorCode(0),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
block := &wire.BlockHeader{
Version: tc.version,
PrevBlock: *params.GenesisHash,
Bits: params.PowLimitBits,
Timestamp: time.Unix(prevNode.Timestamp()+1, 0),
}

err := CheckBlockHeaderContext(
block, prevNode, BFFastAdd,
stubChainCtx{params: &params}, true,
)

if tc.wantErr == 0 {
require.NoError(t, err)

return
}

require.Error(t, err)

var rErr RuleError
require.ErrorAs(t, err, &rErr)
require.Equal(t, tc.wantErr, rErr.ErrorCode)
})
}
}

// TestRegtestBuriedDeploymentsAlwaysActive asserts CSV, SegWit, and Taproot
// are always active on regtest, mirroring Bitcoin Core.
func TestRegtestBuriedDeploymentsAlwaysActive(t *testing.T) {
chain := newFakeChain(&chaincfg.RegressionNetParams)
prevNode := chain.bestChain.Tip()

stateCSV, err := chain.deploymentState(prevNode, chaincfg.DeploymentCSV)
require.NoError(t, err)
require.Equal(t, ThresholdActive, stateCSV)

stateSegwit, err := chain.deploymentState(
prevNode, chaincfg.DeploymentSegwit,
)
require.NoError(t, err)
require.Equal(t, ThresholdActive, stateSegwit)

stateTaproot, err := chain.deploymentState(
prevNode, chaincfg.DeploymentTaproot,
)
require.NoError(t, err)
require.Equal(t, ThresholdActive, stateTaproot)
}

// TestRegtestRejectsCoinbaseMissingHeight ensures contextual validation
// enforces coinbase height serialization on regtest.
func TestRegtestRejectsCoinbaseMissingHeight(t *testing.T) {
chain := newFakeChain(&chaincfg.RegressionNetParams)
prevNode := chain.bestChain.Tip()

block := wire.MsgBlock{
Header: wire.BlockHeader{
Version: 4,
PrevBlock: prevNode.hash,
Bits: chain.chainParams.PowLimitBits,
Timestamp: time.Unix(prevNode.timestamp+1, 0),
},
}

coinbase := wire.NewMsgTx(wire.TxVersion)
coinbase.AddTxIn(&wire.TxIn{SignatureScript: []byte{}})
coinbase.AddTxOut(&wire.TxOut{Value: 0})
block.AddTransaction(coinbase)
block.Header.MerkleRoot = block.Transactions[0].TxHash()

btcBlock := btcutil.NewBlock(&block)

// The block without a height in coinbase is not considered valid.
err := chain.checkBlockContext(btcBlock, prevNode, BFNone)
require.Error(t, err)

// Make sure the error is in coinbase height record.
var rErr RuleError
require.ErrorAs(t, err, &rErr)
require.Equal(t, ErrMissingCoinbaseHeight, rErr.ErrorCode)
}

// TestRegtestAcceptsCoinbaseHeight ensures a properly encoded coinbase height
// passes contextual checks on regtest.
func TestRegtestAcceptsCoinbaseHeight(t *testing.T) {
chain := newFakeChain(&chaincfg.RegressionNetParams)
prevNode := chain.bestChain.Tip()

coinbaseScript, err := txscript.NewScriptBuilder().AddInt64(1).Script()
require.NoError(t, err)

block := wire.MsgBlock{
Header: wire.BlockHeader{
Version: 4,
PrevBlock: prevNode.hash,
Bits: chain.chainParams.PowLimitBits,
Timestamp: time.Unix(prevNode.timestamp+1, 0),
},
}

coinbase := wire.NewMsgTx(wire.TxVersion)
coinbase.AddTxIn(&wire.TxIn{SignatureScript: coinbaseScript})
coinbase.AddTxOut(&wire.TxOut{Value: 0})
block.AddTransaction(coinbase)
block.Header.MerkleRoot = block.Transactions[0].TxHash()

btcBlock := btcutil.NewBlock(&block)

// Make sure the block with the height in coinbase is considered valid.
require.NoError(t, chain.checkBlockContext(btcBlock, prevNode, BFNone))
}
1 change: 1 addition & 0 deletions blockchain/utxocache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ func TestUtxoCacheFlush(t *testing.T) {
defer tearDown()
cache := chain.utxoCache
tip := btcutil.NewBlock(params.GenesisBlock)
tip.SetHeight(0)

// The chainSetup init triggers the consistency status write.
err := assertConsistencyState(chain, params.GenesisHash)
Expand Down
Loading
Loading