mirror of
https://github.com/btcsuite/btcd.git
synced 2026-08-13 12:32:51 +02:00
regtest: align activations with Bitcoin Core
Set regtest buried heights to match Core: BIP34/65/66 at height 1 and make CSV/SegWit/Taproot always active. Added regtest coverage for header version floors, coinbase height enforcement, and deployment state to ensure we now reject the blocks Core rejects and accept the ones Core accepts. Updated package fullblocktests to generate BIP34-compliant blocks. Test helpers now set prev block height to 0 (not the default -1) so generated blocks start at height 1 and satisfy coinbase height rules.
This commit is contained in:
parent
6803c62599
commit
cd4e5426b2
8 changed files with 287 additions and 10 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
218
blockchain/regtest_test.go
Normal file
218
blockchain/regtest_test.go
Normal file
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue