diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index b3bccf56..a1fcb9af 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -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{}) @@ -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{}) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/blockchain/common_test.go b/blockchain/common_test.go index 12badd3e..8c647c12 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -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, diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 2b499c4e..a17af451 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -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) { @@ -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, @@ -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() // --------------------------------------------------------------------- @@ -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) // --------------------------------------------------------------------- diff --git a/blockchain/fullblocktests/params.go b/blockchain/fullblocktests/params.go index 4679036f..2e36dcee 100644 --- a/blockchain/fullblocktests/params.go +++ b/blockchain/fullblocktests/params.go @@ -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 diff --git a/blockchain/regtest_test.go b/blockchain/regtest_test.go new file mode 100644 index 00000000..1f554a57 --- /dev/null +++ b/blockchain/regtest_test.go @@ -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(¶ms.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: ¶ms}, 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)) +} diff --git a/blockchain/utxocache_test.go b/blockchain/utxocache_test.go index 0f410cc9..9b3bc992 100644 --- a/blockchain/utxocache_test.go +++ b/blockchain/utxocache_test.go @@ -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) diff --git a/chaincfg/params.go b/chaincfg/params.go index 1abe7fc8..8aa6c110 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -481,9 +481,9 @@ var RegressionNetParams = Params{ PowLimitBits: 0x207fffff, PoWNoRetargeting: true, 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 @@ -540,6 +540,7 @@ var RegressionNetParams = Params{ DeploymentEnder: NewMedianTimeDeploymentEnder( time.Time{}, // Never expires ), + AlwaysActiveHeight: 1, }, DeploymentSegwit: { BitNumber: 1, @@ -549,6 +550,7 @@ var RegressionNetParams = Params{ DeploymentEnder: NewMedianTimeDeploymentEnder( time.Time{}, // Never expires. ), + AlwaysActiveHeight: 1, }, DeploymentTaproot: { BitNumber: 2, @@ -558,6 +560,8 @@ var RegressionNetParams = Params{ DeploymentEnder: NewMedianTimeDeploymentEnder( time.Time{}, // Never expires. ), + MinActivationHeight: 0, + AlwaysActiveHeight: 1, CustomActivationThreshold: 108, // Only needs 75% hash rate. }, }, diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 8f8b59a5..5d443d71 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -139,6 +139,16 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) { } defer r.TearDown() + // Short-circuit deployments that are configured as always active. + if deploymentID < uint32(len(r.ActiveNet.Deployments)) { + dep := &r.ActiveNet.Deployments[deploymentID] + if dep.AlwaysActiveHeight != 0 { + assertChainHeight(r, t, 0) + assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive) + return + } + } + // If the deployment is meant to be always active, then it should be // active from the very first block. if deploymentID == chaincfg.DeploymentTestDummyAlwaysActive {