From 0914ecc088a675169b47d0802a892a9acc0ce7e9 Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 15:22:47 +0000 Subject: [PATCH 01/12] gitignore: ignore IDEA files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0e1de2f7..3b742cea 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ btcutil/psbt/coverage.txt *.swo /.vim +#IDE +.idea + # Binaries produced by "make build" /addblock /btcctl From 3e3c7791e7d00f03ae87fbdf0fb679acd9dec89a Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 15:30:48 +0000 Subject: [PATCH 02/12] multi: fix typos --- blockchain/validate.go | 2 +- cmd/addblock/config.go | 2 +- params.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blockchain/validate.go b/blockchain/validate.go index 5e24405e..1470a41c 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -1230,7 +1230,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block *btcutil.Block, vi if csvState == ThresholdActive { // If the CSV soft-fork is now active, then modify the // scriptFlags to ensure that the CSV op code is properly - // validated during the script checks bleow. + // validated during the script checks below. scriptFlags |= txscript.ScriptVerifyCheckSequenceVerify // We obtain the MTP of the *previous* block in order to diff --git a/cmd/addblock/config.go b/cmd/addblock/config.go index 51cd6c7e..3f1c7c93 100644 --- a/cmd/addblock/config.go +++ b/cmd/addblock/config.go @@ -31,7 +31,7 @@ var ( activeNetParams = &chaincfg.MainNetParams ) -// config defines the configuration options for findcheckpoint. +// config defines the configuration options for addblock. // // See loadConfig for details on the configuration load process. type config struct { diff --git a/params.go b/params.go index b4d1453d..a3e51640 100644 --- a/params.go +++ b/params.go @@ -21,7 +21,7 @@ type params struct { } // mainNetParams contains parameters specific to the main network -// (wire.MainNet). NOTE: The RPC port is intentionally different than the +// (wire.MainNet). NOTE: The RPC port is intentionally different from the // reference implementation because btcd does not handle wallet requests. The // separate wallet process listens on the well-known port and forwards requests // it does not handle on to btcd. This approach allows the wallet process @@ -41,7 +41,7 @@ var regressionNetParams = params{ } // testNet3Params contains parameters specific to the test network (version 3) -// (wire.TestNet3). NOTE: The RPC port is intentionally different than the +// (wire.TestNet3). NOTE: The RPC port is intentionally different from the // reference implementation - see the mainNetParams comment for details. var testNet3Params = params{ Params: &chaincfg.TestNet3Params, From ac1399f0d32d87cff826a8a2883b6a6fc19d68b3 Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 15:39:29 +0000 Subject: [PATCH 03/12] wire: add TestNet4 --- wire/protocol.go | 4 ++++ wire/protocol_test.go | 1 + 2 files changed, 5 insertions(+) diff --git a/wire/protocol.go b/wire/protocol.go index b6e5ea92..65d19f52 100644 --- a/wire/protocol.go +++ b/wire/protocol.go @@ -179,6 +179,9 @@ const ( // TestNet3 represents the test network (version 3). TestNet3 BitcoinNet = 0x0709110b + // TestNet4 represents the test network (version 4). + TestNet4 BitcoinNet = 0x283f161c + // SigNet represents the public default SigNet. For custom signets, // see CustomSignetParams. SigNet BitcoinNet = 0x40CF030A @@ -193,6 +196,7 @@ var bnStrings = map[BitcoinNet]string{ MainNet: "MainNet", TestNet: "TestNet", TestNet3: "TestNet3", + TestNet4: "TestNet4", SigNet: "SigNet", SimNet: "SimNet", } diff --git a/wire/protocol_test.go b/wire/protocol_test.go index 5ab5b9dd..46498849 100644 --- a/wire/protocol_test.go +++ b/wire/protocol_test.go @@ -49,6 +49,7 @@ func TestBitcoinNetStringer(t *testing.T) { {MainNet, "MainNet"}, {TestNet, "TestNet"}, {TestNet3, "TestNet3"}, + {TestNet4, "TestNet4"}, {SigNet, "SigNet"}, {SimNet, "SimNet"}, {0xffffffff, "Unknown BitcoinNet (4294967295)"}, From 70a9a8e58a4b8e4577295ef5534dacf4e7a36b0e Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 15:47:31 +0000 Subject: [PATCH 04/12] chaincfg: add TestNet4 params --- chaincfg/genesis.go | 71 +++++++++++++++++++++++ chaincfg/genesis_test.go | 67 ++++++++++++++++++++++ chaincfg/params.go | 117 ++++++++++++++++++++++++++++++++++++++ chaincfg/register_test.go | 56 ++++++++++++++++++ 4 files changed, 311 insertions(+) diff --git a/chaincfg/genesis.go b/chaincfg/genesis.go index 73d28610..f734feb2 100644 --- a/chaincfg/genesis.go +++ b/chaincfg/genesis.go @@ -143,6 +143,77 @@ var testNet3GenesisBlock = wire.MsgBlock{ Transactions: []*wire.MsgTx{&genesisCoinbaseTx}, } +// testNet4GenesisTx is the transaction for the genesis blocks for test network (version 4). +var testNet4GenesisTx = wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{ + // Message: `03/May/2024 000000000000000000001ebd58c244970b3aa9d783bb001011fbe8ea8e98e00e` + 0x4, 0xff, 0xff, 0x0, 0x1d, 0x1, 0x4, 0x4c, + 0x4c, 0x30, 0x33, 0x2f, 0x4d, 0x61, 0x79, 0x2f, + 0x32, 0x30, 0x32, 0x34, 0x20, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x31, 0x65, 0x62, 0x64, 0x35, 0x38, 0x63, + 0x32, 0x34, 0x34, 0x39, 0x37, 0x30, 0x62, 0x33, + 0x61, 0x61, 0x39, 0x64, 0x37, 0x38, 0x33, 0x62, + 0x62, 0x30, 0x30, 0x31, 0x30, 0x31, 0x31, 0x66, + 0x62, 0x65, 0x38, 0x65, 0x61, 0x38, 0x65, 0x39, + 0x38, 0x65, 0x30, 0x30, 0x65}, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0x12a05f200, + PkScript: []byte{ + 0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0xac}, + }, + }, + LockTime: 0, +} + +// testNet4GenesisHash is the hash of the first block in the block chain for the +// test network (version 4). +var testNet4GenesisHash = chainhash.Hash([chainhash.HashSize]byte{ + 0x43, 0xf0, 0x8b, 0xda, 0xb0, 0x50, 0xe3, 0x5b, + 0x56, 0x7c, 0x86, 0x4b, 0x91, 0xf4, 0x7f, 0x50, + 0xae, 0x72, 0x5a, 0xe2, 0xde, 0x53, 0xbc, 0xfb, + 0xba, 0xf2, 0x84, 0xda, 0x00, 0x00, 0x00, 0x00}) + +// testNet4GenesisMerkleRoot is the hash of the first transaction in the genesis +// block for the test network (version 4). It is the same as the merkle root +// for the main network. +var testNet4GenesisMerkleRoot = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy. + 0x4e, 0x7b, 0x2b, 0x91, 0x28, 0xfe, 0x02, 0x91, + 0xdb, 0x06, 0x93, 0xaf, 0x2a, 0xe4, 0x18, 0xb7, + 0x67, 0xe6, 0x57, 0xcd, 0x40, 0x7e, 0x80, 0xcb, + 0x14, 0x34, 0x22, 0x1e, 0xae, 0xa7, 0xa0, 0x7a, +}) + +// testNet4GenesisBlock defines the genesis block of the block chain which +// serves as the public transaction ledger for the test network (version 3). +var testNet4GenesisBlock = wire.MsgBlock{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: chainhash.Hash{}, // 0000000000000000000000000000000000000000000000000000000000000000 + MerkleRoot: testNet4GenesisMerkleRoot, // 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b + Timestamp: time.Unix(1714777860, 0), // 2024-05-03 23:11:00 +0000 UTC + Bits: 0x1d00ffff, // 486604799 [00000000ffff0000000000000000000000000000000000000000000000000000] + Nonce: 0x17780cbb, // 393743547 + }, + Transactions: []*wire.MsgTx{&testNet4GenesisTx}, +} + // simNetGenesisHash is the hash of the first block in the block chain for the // simulation test network. var simNetGenesisHash = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy. diff --git a/chaincfg/genesis_test.go b/chaincfg/genesis_test.go index 1daf8479..b2975ffb 100644 --- a/chaincfg/genesis_test.go +++ b/chaincfg/genesis_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" ) // TestGenesisBlock tests the genesis block of the main network for validity by @@ -91,6 +92,34 @@ func TestTestNet3GenesisBlock(t *testing.T) { } } +// TestTestNet4GenesisBlock tests the genesis block of the test network (version +// 4) for validity by checking the encoded bytes and hashes. +func TestTestNet4GenesisBlock(t *testing.T) { + // Encode the genesis block to raw bytes. + var buf bytes.Buffer + err := TestNet4Params.GenesisBlock.Serialize(&buf) + require.NoError(t, err) + + // Ensure the encoded block matches the expected bytes. + if !bytes.Equal(buf.Bytes(), testNet4GenesisBlockBytes) { + t.Fatalf("TestTestNet4GenesisBlock: Genesis block does not "+ + "appear valid - got %v, want %v", + spew.Sdump(buf.Bytes()), + spew.Sdump(testNet4GenesisBlockBytes)) + } + + // Check hash of the block against expected hash. + hash := TestNet4Params.GenesisBlock.BlockHash() + if !TestNet4Params.GenesisHash.IsEqual(&hash) { + t.Fatalf("TestTestNet4GenesisBlock: Genesis block hash does "+ + "not appear valid - got %v, want %v", spew.Sdump(hash), + spew.Sdump(TestNet4Params.GenesisHash)) + } + expectedHash := "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be3" + + "50b0da8bf043" + require.Equal(t, expectedHash, hash.String()) +} + // TestSimNetGenesisBlock tests the genesis block of the simulation test network // for validity by checking the encoded bytes and hashes. func TestSimNetGenesisBlock(t *testing.T) { @@ -268,6 +297,44 @@ var testNet3GenesisBlockBytes = []byte{ 0xac, 0x00, 0x00, 0x00, 0x00, /* |.....| */ } +// testNet4GenesisBlockBytes are the wire encoded bytes for the genesis block of +// the test network (version 4) +var testNet4GenesisBlockBytes = []byte{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x4e, 0x7b, 0x2b, 0x91, /* |....N{+.| */ + 0x28, 0xfe, 0x02, 0x91, 0xdb, 0x06, 0x93, 0xaf, /* |(.......| */ + 0x2a, 0xe4, 0x18, 0xb7, 0x67, 0xe6, 0x57, 0xcd, /* |*...g.W.| */ + 0x40, 0x7e, 0x80, 0xcb, 0x14, 0x34, 0x22, 0x1e, /* |@~...4".| */ + 0xae, 0xa7, 0xa0, 0x7a, 0x04, 0x6f, 0x35, 0x66, /* |...z.o5f| */ + 0xff, 0xff, 0x00, 0x1d, 0xbb, 0x0c, 0x78, 0x17, /* |......x.| */ + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, /* |........| */ + 0xff, 0xff, 0x55, 0x04, 0xff, 0xff, 0x00, 0x1d, /* |..U.....| */ + 0x01, 0x04, 0x4c, 0x4c, 0x30, 0x33, 0x2f, 0x4d, /* |..LL03/M| */ + 0x61, 0x79, 0x2f, 0x32, 0x30, 0x32, 0x34, 0x20, /* |ay/2024 | */ + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, /* |00000000| */ + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, /* |00000000| */ + 0x30, 0x30, 0x30, 0x30, 0x31, 0x65, 0x62, 0x64, /* |00001ebd| */ + 0x35, 0x38, 0x63, 0x32, 0x34, 0x34, 0x39, 0x37, /* |58c24497| */ + 0x30, 0x62, 0x33, 0x61, 0x61, 0x39, 0x64, 0x37, /* |0b3aa9d7| */ + 0x38, 0x33, 0x62, 0x62, 0x30, 0x30, 0x31, 0x30, /* |83bb0010| */ + 0x31, 0x31, 0x66, 0x62, 0x65, 0x38, 0x65, 0x61, /* |11fbe8ea| */ + 0x38, 0x65, 0x39, 0x38, 0x65, 0x30, 0x30, 0x65, /* |8e98e00e| */ + 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0xf2, 0x05, /* |........| */ + 0x2a, 0x01, 0x00, 0x00, 0x00, 0x23, 0x21, 0x00, /* |*....#!.| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* |........| */ + 0xac, 0x00, 0x00, 0x00, 0x00, /* |..... | */ +} + // simNetGenesisBlockBytes are the wire encoded bytes for the genesis block of // the simulation test network as of protocol version 70002. var simNetGenesisBlockBytes = []byte{ diff --git a/chaincfg/params.go b/chaincfg/params.go index 1c329cb5..d97353f5 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -189,6 +189,10 @@ type Params struct { // regtest like networks. PoWNoRetargeting bool + // EnforceBIP94 enforces timewarp attack mitigation and on testnet4 + // this also enforces the block storm mitigation. + EnforceBIP94 bool + // These fields define the block heights at which the specified softfork // BIP became active. BIP0034Height int32 @@ -673,6 +677,118 @@ var TestNet3Params = Params{ HDCoinType: 1, } +// TestNet4Params defines the network parameters for the test Bitcoin network +// (version 4). +var TestNet4Params = Params{ + Name: "testnet4", + Net: wire.TestNet4, + DefaultPort: "48333", + DNSSeeds: []DNSSeed{ + {"seed.testnet4.bitcoin.sprovoost.nl", true}, + {"seed.testnet4.wiz.biz", true}, + }, + + // Chain parameters + GenesisBlock: &testNet4GenesisBlock, + GenesisHash: &testNet4GenesisHash, + PowLimit: testNet3PowLimit, + PowLimitBits: 0x1d00ffff, + EnforceBIP94: true, + BIP0034Height: 1, + BIP0065Height: 1, + BIP0066Height: 1, + CoinbaseMaturity: 100, + SubsidyReductionInterval: 210000, + TargetTimespan: time.Hour * 24 * 14, // 14 days + TargetTimePerBlock: time.Minute * 10, // 10 minutes + RetargetAdjustmentFactor: 4, // 25% less, 400% more + ReduceMinDifficulty: true, + MinDiffReductionTime: time.Minute * 20, // TargetTimePerBlock * 2 + GenerateSupported: false, + + // Checkpoints ordered from oldest to newest. + Checkpoints: []Checkpoint{}, + + // Consensus rule change deployments. + // + // The miner confirmation window is defined as: + // target proof of work timespan / target proof of work spacing + RuleChangeActivationThreshold: 1512, // 75% of MinerConfirmationWindow + MinerConfirmationWindow: 2016, + Deployments: [DefinedDeployments]ConsensusDeployment{ + DeploymentTestDummy: { + BitNumber: 28, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Unix(1199145601, 0), // January 1, 2008 UTC + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Unix(1230767999, 0), // December 31, 2008 UTC + ), + }, + DeploymentTestDummyMinActivation: { + BitNumber: 22, + CustomActivationThreshold: 1815, // Only needs 90% hash rate. + MinActivationHeight: 10_0000, // Can only activate after height 10k. + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + }, + DeploymentCSV: { + BitNumber: 29, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + }, + DeploymentSegwit: { + BitNumber: 29, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + }, + DeploymentTaproot: { + BitNumber: 2, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + MinActivationHeight: 0, + }, + }, + + // Mempool parameters + RelayNonStdTxs: true, + + // Human-readable part for Bech32 encoded segwit addresses, as defined in + // BIP 173. + Bech32HRPSegwit: "tb", // always tb for test net + + // Address encoding magics + PubKeyHashAddrID: 0x6f, // starts with m or n + ScriptHashAddrID: 0xc4, // starts with 2 + WitnessPubKeyHashAddrID: 0x03, // starts with QW + WitnessScriptHashAddrID: 0x28, // starts with T7n + PrivateKeyID: 0xef, // starts with 9 (uncompressed) or c (compressed) + + // BIP32 hierarchical deterministic extended key magics + HDPrivateKeyID: [4]byte{0x04, 0x35, 0x83, 0x94}, // starts with tprv + HDPublicKeyID: [4]byte{0x04, 0x35, 0x87, 0xcf}, // starts with tpub + + // BIP44 coin type used in the hierarchical deterministic path for + // address generation. + HDCoinType: 1, +} + // SimNetParams defines the network parameters for the simulation test Bitcoin // network. This network is similar to the normal test network except it is // intended for private use within a group of individuals doing simulation @@ -1075,6 +1191,7 @@ func init() { // Register all default networks when the package is initialized. mustRegister(&MainNetParams) mustRegister(&TestNet3Params) + mustRegister(&TestNet4Params) mustRegister(&RegressionNetParams) mustRegister(&SimNetParams) } diff --git a/chaincfg/register_test.go b/chaincfg/register_test.go index bcb5b3c6..db254415 100644 --- a/chaincfg/register_test.go +++ b/chaincfg/register_test.go @@ -68,6 +68,11 @@ func TestRegister(t *testing.T) { params: &TestNet3Params, err: ErrDuplicateNet, }, + { + name: "duplicate testnet4", + params: &TestNet4Params, + err: ErrDuplicateNet, + }, { name: "duplicate simnet", params: &SimNetParams, @@ -83,6 +88,10 @@ func TestRegister(t *testing.T) { magic: TestNet3Params.PubKeyHashAddrID, valid: true, }, + { + magic: TestNet4Params.PubKeyHashAddrID, + valid: true, + }, { magic: RegressionNetParams.PubKeyHashAddrID, valid: true, @@ -109,6 +118,10 @@ func TestRegister(t *testing.T) { magic: TestNet3Params.ScriptHashAddrID, valid: true, }, + { + magic: TestNet4Params.ScriptHashAddrID, + valid: true, + }, { magic: RegressionNetParams.ScriptHashAddrID, valid: true, @@ -135,6 +148,10 @@ func TestRegister(t *testing.T) { prefix: TestNet3Params.Bech32HRPSegwit + "1", valid: true, }, + { + prefix: TestNet4Params.Bech32HRPSegwit + "1", + valid: true, + }, { prefix: RegressionNetParams.Bech32HRPSegwit + "1", valid: true, @@ -175,6 +192,11 @@ func TestRegister(t *testing.T) { want: TestNet3Params.HDPublicKeyID[:], err: nil, }, + { + priv: TestNet4Params.HDPrivateKeyID[:], + want: TestNet4Params.HDPublicKeyID[:], + err: nil, + }, { priv: RegressionNetParams.HDPrivateKeyID[:], want: RegressionNetParams.HDPublicKeyID[:], @@ -217,6 +239,10 @@ func TestRegister(t *testing.T) { magic: TestNet3Params.PubKeyHashAddrID, valid: true, }, + { + magic: TestNet4Params.PubKeyHashAddrID, + valid: true, + }, { magic: RegressionNetParams.PubKeyHashAddrID, valid: true, @@ -243,6 +269,10 @@ func TestRegister(t *testing.T) { magic: TestNet3Params.ScriptHashAddrID, valid: true, }, + { + magic: TestNet4Params.ScriptHashAddrID, + valid: true, + }, { magic: RegressionNetParams.ScriptHashAddrID, valid: true, @@ -269,6 +299,10 @@ func TestRegister(t *testing.T) { prefix: TestNet3Params.Bech32HRPSegwit + "1", valid: true, }, + { + prefix: TestNet4Params.Bech32HRPSegwit + "1", + valid: true, + }, { prefix: RegressionNetParams.Bech32HRPSegwit + "1", valid: true, @@ -324,6 +358,11 @@ func TestRegister(t *testing.T) { params: &TestNet3Params, err: ErrDuplicateNet, }, + { + name: "duplicate testnet4", + params: &TestNet4Params, + err: ErrDuplicateNet, + }, { name: "duplicate simnet", params: &SimNetParams, @@ -344,6 +383,10 @@ func TestRegister(t *testing.T) { magic: TestNet3Params.PubKeyHashAddrID, valid: true, }, + { + magic: TestNet4Params.PubKeyHashAddrID, + valid: true, + }, { magic: RegressionNetParams.PubKeyHashAddrID, valid: true, @@ -370,6 +413,10 @@ func TestRegister(t *testing.T) { magic: TestNet3Params.ScriptHashAddrID, valid: true, }, + { + magic: TestNet4Params.ScriptHashAddrID, + valid: true, + }, { magic: RegressionNetParams.ScriptHashAddrID, valid: true, @@ -396,6 +443,10 @@ func TestRegister(t *testing.T) { prefix: TestNet3Params.Bech32HRPSegwit + "1", valid: true, }, + { + prefix: TestNet4Params.Bech32HRPSegwit + "1", + valid: true, + }, { prefix: RegressionNetParams.Bech32HRPSegwit + "1", valid: true, @@ -436,6 +487,11 @@ func TestRegister(t *testing.T) { want: TestNet3Params.HDPublicKeyID[:], err: nil, }, + { + priv: TestNet4Params.HDPrivateKeyID[:], + want: TestNet4Params.HDPublicKeyID[:], + err: nil, + }, { priv: RegressionNetParams.HDPrivateKeyID[:], want: RegressionNetParams.HDPublicKeyID[:], From b31d0554cccdf9b0f1df19dbc329e1ec79f1e7f8 Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 16:56:46 +0000 Subject: [PATCH 05/12] blockchain: add BIP94 support --- blockchain/difficulty.go | 10 +++++++- blockchain/error.go | 4 +++ blockchain/validate.go | 53 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/blockchain/difficulty.go b/blockchain/difficulty.go index b1e39b9d..56de7780 100644 --- a/blockchain/difficulty.go +++ b/blockchain/difficulty.go @@ -191,12 +191,20 @@ func calcNextRequiredDifficulty(lastNode HeaderCtx, newBlockTime time.Time, adjustedTimespan = c.MaxRetargetTimespan() } + // Special difficulty rule for Testnet4 + oldTarget := CompactToBig(lastNode.Bits()) + if c.ChainParams().EnforceBIP94 { + // Here we use the first block of the difficulty period. This way + // the real difficulty is always preserved in the first block as + // it is not allowed to use the min-difficulty exception. + oldTarget = CompactToBig(firstNode.Bits()) + } + // Calculate new target difficulty as: // currentDifficulty * (adjustedTimespan / targetTimespan) // The result uses integer division which means it will be slightly // rounded down. Bitcoind also uses integer division to calculate this // result. - oldTarget := CompactToBig(lastNode.Bits()) newTarget := new(big.Int).Mul(oldTarget, big.NewInt(adjustedTimespan)) targetTimeSpan := int64(c.ChainParams().TargetTimespan / time.Second) newTarget.Div(newTarget, big.NewInt(targetTimeSpan)) diff --git a/blockchain/error.go b/blockchain/error.go index dc402222..8a7d4a7d 100644 --- a/blockchain/error.go +++ b/blockchain/error.go @@ -220,6 +220,10 @@ const ( // current chain tip. This is not a block validation rule, but is required // for block proposals submitted via getblocktemplate RPC. ErrPrevBlockNotBest + + // ErrTimewarpAttack indicates a timewarp attack i.e. + // when block's timestamp is too early on diff adjustment block. + ErrTimewarpAttack ) // Map of ErrorCode values back to their constant names for pretty printing. diff --git a/blockchain/validate.go b/blockchain/validate.go index 1470a41c..1cc0ec33 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -46,6 +46,11 @@ const ( // coinbaseHeightAllocSize is the amount of bytes that the // ScriptBuilder will allocate when validating the coinbase height. coinbaseHeightAllocSize = 5 + + // maxTimeWarp is a maximum number of seconds that the timestamp of the first + // block of a difficulty adjustment period is allowed to + // be earlier than the last block of the previous period (BIP94). + maxTimeWarp = 600 * time.Second ) var ( @@ -684,6 +689,12 @@ func compareScript(height int32, script []byte) error { func CheckBlockHeaderContext(header *wire.BlockHeader, prevNode HeaderCtx, flags BehaviorFlags, c ChainCtx, skipCheckpoint bool) error { + // The height of this block is one more than the referenced previous + // block. + blockHeight := prevNode.Height() + 1 + + params := c.ChainParams() + fastAdd := flags&BFFastAdd == BFFastAdd if !fastAdd { // Ensure the difficulty specified in the block header matches @@ -710,16 +721,24 @@ func CheckBlockHeaderContext(header *wire.BlockHeader, prevNode HeaderCtx, str = fmt.Sprintf(str, header.Timestamp, medianTime) return ruleError(ErrTimeTooOld, str) } - } - // The height of this block is one more than the referenced previous - // block. - blockHeight := prevNode.Height() + 1 + // Testnet4 only: Check timestamp against prev for + // difficulty-adjustment blocks to prevent timewarp attacks. + if params.EnforceBIP94 { + err := assertNoTimeWarp( + blockHeight, c.BlocksPerRetarget(), + header.Timestamp, + time.Unix(prevNode.Timestamp(), 0), + ) + if err != nil { + return err + } + } + } // Reject outdated block versions once a majority of the network // has upgraded. These were originally voted on by BIP0034, // BIP0065, and BIP0066. - params := c.ChainParams() if header.Version < 2 && blockHeight >= params.BIP0034Height || header.Version < 3 && blockHeight >= params.BIP0066Height || header.Version < 4 && blockHeight >= params.BIP0065Height { @@ -761,6 +780,30 @@ func CheckBlockHeaderContext(header *wire.BlockHeader, prevNode HeaderCtx, return nil } +// assertNoTimeWarp checks the timestamp of the block against the previous +// block's timestamp for the first block of each difficulty adjustment interval +// to prevent timewarp attacks. This is defined in BIP-0094. +func assertNoTimeWarp(blockHeight, blocksPerReTarget int32, headerTimestamp, + prevBlockTimestamp time.Time) error { + + // If this isn't the first block of the difficulty adjustment interval, + // then we can exit early. + if blockHeight%blocksPerReTarget != 0 { + return nil + } + + // Check timestamp for the first block of each difficulty adjustment + // interval, except the genesis block. + if headerTimestamp.Before(prevBlockTimestamp.Add(-maxTimeWarp)) { + str := "block's timestamp %v is too early on diff adjustment " + + "block %v" + str = fmt.Sprintf(str, headerTimestamp, prevBlockTimestamp) + return ruleError(ErrTimewarpAttack, str) + } + + return nil +} + // checkBlockContext performs several validation checks on the block which depend // on its position within the block chain. // From 9b258bd3ccb7fd9a612622d4c91bff8c8488206f Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 17:01:14 +0000 Subject: [PATCH 06/12] cmd: add TestNet4 config param --- cmd/addblock/config.go | 7 ++++++- cmd/btcctl/config.go | 13 ++++++++++++- cmd/findcheckpoint/config.go | 7 ++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/addblock/config.go b/cmd/addblock/config.go index 3f1c7c93..5f479003 100644 --- a/cmd/addblock/config.go +++ b/cmd/addblock/config.go @@ -42,7 +42,8 @@ type config struct { Progress int `short:"p" long:"progress" description:"Show a progress message each time this number of seconds have passed -- Use 0 to disable progress announcements"` RegressionTest bool `long:"regtest" description:"Use the regression test network"` SimNet bool `long:"simnet" description:"Use the simulation test network"` - TestNet3 bool `long:"testnet" description:"Use the test network"` + TestNet3 bool `long:"testnet" description:"Use the test network (version 3)"` + TestNet4 bool `long:"testnet4" description:"Use the test network (version 4)"` TxIndex bool `long:"txindex" description:"Build a full hash-based transaction index which makes all transactions available via the getrawtransaction RPC"` } @@ -108,6 +109,10 @@ func loadConfig() (*config, []string, error) { numNets++ activeNetParams = &chaincfg.TestNet3Params } + if cfg.TestNet4 { + numNets++ + activeNetParams = &chaincfg.TestNet4Params + } if cfg.RegressionTest { numNets++ activeNetParams = &chaincfg.RegressionNetParams diff --git a/cmd/btcctl/config.go b/cmd/btcctl/config.go index 44d28c60..023dd93a 100644 --- a/cmd/btcctl/config.go +++ b/cmd/btcctl/config.go @@ -106,7 +106,8 @@ type config struct { RPCUser string `short:"u" long:"rpcuser" description:"RPC username"` SimNet bool `long:"simnet" description:"Connect to the simulation test network"` TLSSkipVerify bool `long:"skipverify" description:"Do not verify tls certificates (not recommended!)"` - TestNet3 bool `long:"testnet" description:"Connect to testnet"` + TestNet3 bool `long:"testnet" description:"Connect to testnet (version 3)"` + TestNet4 bool `long:"testnet4" description:"Connect to testnet (version 4)"` SigNet bool `long:"signet" description:"Connect to signet"` ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` Wallet bool `long:"wallet" description:"Connect to wallet"` @@ -125,6 +126,12 @@ func normalizeAddress(addr string, chain *chaincfg.Params, useWallet bool) (stri } else { defaultPort = "18334" } + case &chaincfg.TestNet4Params: + if useWallet { + defaultPort = "48332" + } else { + defaultPort = "48334" + } case &chaincfg.SimNetParams: if useWallet { defaultPort = "18554" @@ -272,6 +279,10 @@ func loadConfig() (*config, []string, error) { numNets++ network = &chaincfg.TestNet3Params } + if cfg.TestNet4 { + numNets++ + network = &chaincfg.TestNet4Params + } if cfg.SimNet { numNets++ network = &chaincfg.SimNetParams diff --git a/cmd/findcheckpoint/config.go b/cmd/findcheckpoint/config.go index fad01a03..d4ae5dfc 100644 --- a/cmd/findcheckpoint/config.go +++ b/cmd/findcheckpoint/config.go @@ -42,7 +42,8 @@ type config struct { NumCandidates int `short:"n" long:"numcandidates" description:"Max num of checkpoint candidates to show {1-20}"` RegressionTest bool `long:"regtest" description:"Use the regression test network"` SimNet bool `long:"simnet" description:"Use the simulation test network"` - TestNet3 bool `long:"testnet" description:"Use the test network"` + TestNet3 bool `long:"testnet" description:"Use the test network (version 3)"` + TestNet4 bool `long:"testnet4" description:"Use the test network (version 4)"` } // validDbType returns whether or not dbType is a supported database type. @@ -96,6 +97,10 @@ func loadConfig() (*config, []string, error) { numNets++ activeNetParams = &chaincfg.TestNet3Params } + if cfg.TestNet4 { + numNets++ + activeNetParams = &chaincfg.TestNet4Params + } if cfg.RegressionTest { numNets++ activeNetParams = &chaincfg.RegressionNetParams From 47fbe7a242fc47b1349933d29e50c0f87aa7142e Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 17:01:44 +0000 Subject: [PATCH 07/12] dbtool: add TestNet4 config param --- database/cmd/dbtool/globalconfig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database/cmd/dbtool/globalconfig.go b/database/cmd/dbtool/globalconfig.go index db7f1324..bcea56a2 100644 --- a/database/cmd/dbtool/globalconfig.go +++ b/database/cmd/dbtool/globalconfig.go @@ -37,6 +37,7 @@ type config struct { RegressionTest bool `long:"regtest" description:"Use the regression test network"` SimNet bool `long:"simnet" description:"Use the simulation test network"` TestNet3 bool `long:"testnet" description:"Use the test network"` + TestNet4 bool `long:"testnet4" description:"Use the test network (version 4)"` } // fileExists reports whether the named file or directory exists. @@ -84,6 +85,10 @@ func setupGlobalConfig() error { numNets++ activeNetParams = &chaincfg.TestNet3Params } + if cfg.TestNet4 { + numNets++ + activeNetParams = &chaincfg.TestNet4Params + } if cfg.RegressionTest { numNets++ activeNetParams = &chaincfg.RegressionNetParams From 029a730319f6e562544d745640d634404366862e Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 17:03:02 +0000 Subject: [PATCH 08/12] rpcclient: add TestNet4 config param support --- rpcclient/infrastructure.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index f26d2c56..373ffad4 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -1529,6 +1529,8 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error client.chainParams = &chaincfg.MainNetParams case chaincfg.TestNet3Params.Name: client.chainParams = &chaincfg.TestNet3Params + case chaincfg.TestNet4Params.Name: + client.chainParams = &chaincfg.TestNet4Params case chaincfg.RegressionNetParams.Name: client.chainParams = &chaincfg.RegressionNetParams case chaincfg.SigNetParams.Name: From fe4db21e6cba85033601fdd44f930c9fdd8b6749 Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Mon, 10 Feb 2025 17:06:09 +0000 Subject: [PATCH 09/12] btcd: add TestNet4 config param --- config.go | 7 ++++++- params.go | 8 ++++++++ rpcserver.go | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 8e88fd59..04f926c2 100644 --- a/config.go +++ b/config.go @@ -171,7 +171,8 @@ type config struct { SigNet bool `long:"signet" description:"Use the signet test network"` SigNetChallenge string `long:"signetchallenge" description:"Connect to a custom signet network defined by this challenge instead of using the global default signet test network -- Can be specified multiple times"` SigNetSeedNode []string `long:"signetseednode" description:"Specify a seed node for the signet network instead of using the global default signet network seed nodes"` - TestNet3 bool `long:"testnet" description:"Use the test network"` + TestNet3 bool `long:"testnet" description:"Use the test network (version 3)"` + TestNet4 bool `long:"testnet4" description:"Use the test network (version 4)"` TorIsolation bool `long:"torisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection."` TrickleInterval time.Duration `long:"trickleinterval" description:"Minimum time between attempts to send new inventory to a connected peer"` UtxoCacheMaxSizeMiB uint `long:"utxocachemaxsize" description:"The maximum size in MiB of the UTXO cache"` @@ -548,6 +549,10 @@ func loadConfig() (*config, []string, error) { numNets++ activeNetParams = &testNet3Params } + if cfg.TestNet4 { + numNets++ + activeNetParams = &testNet4Params + } if cfg.RegressionTest { numNets++ activeNetParams = ®ressionNetParams diff --git a/params.go b/params.go index a3e51640..30daec8d 100644 --- a/params.go +++ b/params.go @@ -48,6 +48,14 @@ var testNet3Params = params{ rpcPort: "18334", } +// testNet4Params contains parameters specific to the test network (version 4) +// (wire.TestNet4). NOTE: The RPC port is intentionally different from the +// reference implementation - see the mainNetParams comment for details. +var testNet4Params = params{ + Params: &chaincfg.TestNet4Params, + rpcPort: "48334", +} + // simNetParams contains parameters specific to the simulation test network // (wire.SimNet). var simNetParams = params{ diff --git a/rpcserver.go b/rpcserver.go index e9fec435..7483011c 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2358,7 +2358,7 @@ func handleGetInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in Connections: s.cfg.ConnMgr.ConnectedCount(), Proxy: cfg.Proxy, Difficulty: getDifficultyRatio(best.Bits, s.cfg.ChainParams), - TestNet: cfg.TestNet3, + TestNet: cfg.TestNet3 || cfg.TestNet4, RelayFee: cfg.minRelayTxFee.ToBTC(), } @@ -2413,7 +2413,7 @@ func handleGetMiningInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{ HashesPerSec: s.cfg.CPUMiner.HashesPerSecond(), NetworkHashPS: networkHashesPerSec, PooledTx: uint64(s.cfg.TxMemPool.Count()), - TestNet: cfg.TestNet3, + TestNet: cfg.TestNet3 || cfg.TestNet4, } return &result, nil } From bf23b34cd20146ea54af0a7a027a66e694b7378c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 6 Mar 2025 15:41:20 -0800 Subject: [PATCH 10/12] build: add rapid as a dep --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 04110578..f8b8e95f 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 golang.org/x/crypto v0.22.0 golang.org/x/sys v0.19.0 + pgregory.net/rapid v1.2.0 ) require ( diff --git a/go.sum b/go.sum index b92ab408..19f62db8 100644 --- a/go.sum +++ b/go.sum @@ -139,3 +139,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= From 9429f7def5a12959129fd4b8f5cd5f868048aaa3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 6 Mar 2025 15:44:43 -0800 Subject: [PATCH 11/12] blockchain: add property-based tests for assertNoTimeWarp Add robust property-based tests for the assertNoTimeWarp function using the rapid testing library. The tests verify the following scenarios: - Basic property tests: - Only retarget blocks (block height divisible by blocksPerRetarget) are checked - Valid timestamps (within maxTimeWarp of previous block) pass validation - Invalid timestamps (too early) fail with appropriate ErrTimewarpAttack - Correct boundary behavior (exactly at maxTimeWarp limit) - Invariant tests: - Function never panics with valid inputs - Non-retarget blocks always return nil regardless of timestamps - Security tests: - All retarget blocks are protected from timewarp attacks - Non-retarget blocks are not affected by the timewarp check --- blockchain/validate_rapid_test.go | 339 ++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 blockchain/validate_rapid_test.go diff --git a/blockchain/validate_rapid_test.go b/blockchain/validate_rapid_test.go new file mode 100644 index 00000000..11b13571 --- /dev/null +++ b/blockchain/validate_rapid_test.go @@ -0,0 +1,339 @@ +package blockchain + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// TestAssertNoTimeWarpProperties uses property-based testing to verify that +// the assertNoTimeWarp function correctly implements the BIP-94 rule. This +// helps catch edge cases that might be missed with regular unit tests. +func TestAssertNoTimeWarpProperties(t *testing.T) { + t.Parallel() + + // Define constant for blocks per retarget (similar to Bitcoin's 2016). + const blocksPerRetarget = 2016 + + // Rapid test that only the retarget blocks are checked. + t.Run("only_checks_retarget_blocks", rapid.MakeCheck(func(t *rapid.T) { + // Generate block height that is not a retarget block. + height := rapid.Int32Range( + 1, 1000000, + ).Filter(func(h int32) bool { + return h%blocksPerRetarget != 0 + }).Draw(t, "height") + + // Even with an "extreme" time warp, the function should return + // nil because it only applies the check to retarget blocks. + // Define headerTime as the Unix epoch start. + headerTime := time.Unix(0, 0) + + // Define prevBlockTime as the current time (creating an + // extreme gap). + prevBlockTime := time.Now() + + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.NoError( + t, err, "expected nil error for non-retarget block "+ + "but got: %v.", err, + ) + })) + + // Rapid test that retarget blocks with acceptable timestamps pass + // validation. + t.Run("valid_timestamps_pass", rapid.MakeCheck(func(t *rapid.T) { + // Generate block height that is a retarget block + height := rapid.Int32Range(blocksPerRetarget, 1000000). + Filter(func(h int32) bool { + return h%blocksPerRetarget == 0 + }).Draw(t, "height") + + // Generate a previous block timestamp. + prevTimeUnix := rapid.Int64Range( + 1000000, 2000000000, + ).Draw(t, "prev_time") + prevBlockTime := time.Unix(prevTimeUnix, 0) + + // Generate a header timestamp that is not more than + // maxTimeWarp earlier than the previous block timestamp. + minValidHeaderTime := prevBlockTime.Add( + -maxTimeWarp, + ).Add(time.Second) + + // Generate any valid header time between the minimum valid + // time and prevBlockTime to ensure it passes the time warp + // check. + minTimeUnix := minValidHeaderTime.Unix() + maxTimeUnix := prevBlockTime.Unix() + + // Ensure min is always less than max. + if minTimeUnix >= maxTimeUnix { + // If a valid range cannot be generated, use the + // previous block time which is guaranteed to pass the + // test. + headerTime := prevBlockTime + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.NoError(t, err, "expected valid timestamps to "+ + "pass but got: %v.") + return + } + + headerTimeUnix := rapid.Int64Range( + minTimeUnix, maxTimeUnix, + ).Draw(t, "header_time_unix") + headerTime := time.Unix(headerTimeUnix, 0) + + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.NoError(t, err, "expected valid timestamps to pass but "+ + "got: %v.") + })) + + // Rapid test that retarget blocks with invalid timestamps fail + t.Run("invalid_timestamps_fail", rapid.MakeCheck(func(t *rapid.T) { + // validation. + // Generate block height that is a retarget block. + height := rapid.Int32Range(blocksPerRetarget, 1000000). + Filter(func(h int32) bool { + return h%blocksPerRetarget == 0 + }).Draw(t, "height") + + // Generate a previous block timestamp. + prevTimeUnix := rapid.Int64Range( + 1000000, 2000000000, + ).Draw(t, "prev_time") + prevBlockTime := time.Unix(prevTimeUnix, 0) + + // Invalid header timestamp: more than maxTimeWarp earlier than + // prevBlockTime Ensure we generate a time that is definitely + // beyond the maxTimeWarp (which is 600 seconds) by using at + // least 601 seconds. + invalidDelta := time.Duration( + -rapid.Int64Range(601, 86400).Draw(t, "invalid_delta"), + ) * time.Second + headerTime := prevBlockTime.Add(invalidDelta) + + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.Error(t, err, "expected error for time-warped header but got nil.") + + // Verify the correct error type is returned. + require.IsType( + t, RuleError{}, err, "expected RuleError but got: %T.", err, + ) + + // Verify it's the expected ErrTimewarpAttack error. + ruleErr, ok := err.(RuleError) + require.True(t, ok, "expected RuleError but got: %T.", err) + require.Equal( + t, ErrTimewarpAttack, ruleErr.ErrorCode, "expected "+ + "ErrTimewarpAttack but got: %v.", ruleErr.ErrorCode, + ) + })) + + // Test the edge case right at the boundary of maxTimeWarp. + t.Run("boundary_timestamps", rapid.MakeCheck(func(t *rapid.T) { + // Generate block height that is a retarget block. + height := rapid.Int32Range(blocksPerRetarget, 1000000). + Filter(func(h int32) bool { + return h%blocksPerRetarget == 0 + }).Draw(t, "height") + + // Generate a previous block timestamp with enough padding + // to avoid time.Time precision issues. + prevTimeUnix := rapid.Int64Range( + 1000000, 2000000000, + ).Draw(t, "prev_time") + prevBlockTime := time.Unix(prevTimeUnix, 0) + + // Test exact boundary: headerTime is exactly maxTimeWarp earlier. + headerTime := prevBlockTime.Add(-maxTimeWarp) + + // Check the actual implementation (looking at + // validate.go:797-798) The comparison is + // "headerTimestamp.Before(prevBlockTimestamp.Add(-maxTimeWarp))" + // This means at exact boundary (headerTime == + // prevBlockTime.Add(-maxTimeWarp)) it should NOT fail, since + // Before() is strict < not <=. + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.NoError( + t, err, "expected no error at exact boundary but "+ + "got: %v.", + ) + + // Test 1 nanosecond BEYOND the boundary (which should fail). + headerTime = prevBlockTime.Add(-maxTimeWarp).Add( + -time.Nanosecond, + ) + + // This should fail as it is just beyond the maxTimeWarp limit. + err = assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.Error( + t, err, "expected error just beyond boundary but "+ + "got nil.", + ) + })) +} + +// TestAssertNoTimeWarpInvariants uses property-based testing to verify the +// invariants of the assertNoTimeWarp function regardless of inputs. +func TestAssertNoTimeWarpInvariants(t *testing.T) { + t.Parallel() + + // Invariant: The function should never panic regardless of input. + t.Run("never_panics", rapid.MakeCheck(func(t *rapid.T) { + // Generate any possible inputs + height := rapid.Int32().Draw(t, "height") + blocksPerRetarget := rapid.Int32Range( + 1, 10000, + ).Draw(t, "blocks_per_retarget") + headerTimeUnix := rapid.Int64().Draw(t, "header_time") + prevTimeUnix := rapid.Int64().Draw(t, "prev_time") + + headerTime := time.Unix(headerTimeUnix, 0) + prevBlockTime := time.Unix(prevTimeUnix, 0) + + // The function should never panic regardless of input + _ = assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + })) + + // Invariant: For non-retarget blocks, the function always returns nil. + // nolint:lll. + t.Run("non_retarget_blocks_return_nil", rapid.MakeCheck(func(t *rapid.T) { + // Generate height and blocksPerRetarget such that height is + // not a multiple of blocksPerRetarget. + blocksPerRetarget := rapid.Int32Range(2, 10000).Draw( + t, "blocks_per_retarget", + ) + + // Ensure height is not a multiple of blocksPerRetarget. + remainders := rapid.Int32Range(1, blocksPerRetarget-1).Draw( + t, "remainder", + ) + height := rapid.Int32Range(0, 1000000).Draw( + t, "base", + )*blocksPerRetarget + remainders + + // Generate any timestamps, even invalid ones. + headerTime := time.Unix(rapid.Int64().Draw(t, "header_time"), 0) + prevBlockTime := time.Unix( + rapid.Int64().Draw(t, "prev_time"), 0, + ) + + // For non-retarget blocks, should always return nil. + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.NoError( + t, err, "expected nil for non-retarget block "+ + "(height=%d, blocks_per_retarget=%d) but "+ + "got: %v.", height, blocksPerRetarget, err, + ) + })) +} + +// TestAssertNoTimeWarpSecurity tests the security properties of the +// assertNoTimeWarp function. This verifies that the function properly prevents +// "time warp" attacks where miners might attempt to manipulate timestamps for +// difficulty adjustment blocks. +func TestAssertNoTimeWarpSecurity(t *testing.T) { + t.Parallel() + + const blocksPerRetarget = 2016 + + // Test that all difficulty adjustment blocks are protected from timewarp. + t.Run("all_retarget_blocks_protected", rapid.MakeCheck(func(t *rapid.T) { //nolint:lll + // Generate any retarget block height (multiples of + // blocksPerRetarget). + multiplier := rapid.Int32Range(1, 1000).Draw(t, "multiplier") + height := multiplier * blocksPerRetarget + + // Generate a reasonable previous block timestamp. + prevTimeUnix := rapid.Int64Range( + 1000000, 2000000000, + ).Draw(t, "prev_time") + prevBlockTime := time.Unix(prevTimeUnix, 0) + + // Generate a test header timestamp that's significantly before + // the previous timestamp This should always be rejected for + // retarget blocks. + timeDiff := rapid.Int64Range( + int64(maxTimeWarp+time.Second), + int64(maxTimeWarp+time.Hour*24*7), + ).Draw(t, "warp_amount") + invalidDelta := time.Duration(-timeDiff) + headerTime := prevBlockTime.Add(invalidDelta) + + // This should always fail with ErrTimewarpAttack for any retarget block. + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.Error( + t, err, "security vulnerability: Time warp attack not "+ + "detected for height %d.", height, + ) + + // Verify it's the expected error type. + ruleErr, ok := err.(RuleError) + require.True(t, ok, "expected RuleError but got: %T.", err) + require.Equal( + t, ErrTimewarpAttack, ruleErr.ErrorCode, + "expected ErrTimewarpAttack but got: %v.", + ruleErr.ErrorCode, + ) + })) + + // Test that non-adjustment blocks are not subject to the same check. + // nolint:lll. + t.Run("non_retarget_blocks_not_affected", rapid.MakeCheck(func(t *rapid.T) { + // Generate any non-retarget block height. + baseHeight := rapid.Int32Range(0, 1000).Draw( + t, "base_height", + ) * blocksPerRetarget + offset := rapid.Int32Range(1, blocksPerRetarget-1).Draw( + t, "offset", + ) + height := baseHeight + offset + + // Generate a reasonable previous block timestamp. + prevTimeUnix := rapid.Int64Range(1000000, 2000000000).Draw( + t, "prev_time", + ) + prevBlockTime := time.Unix(prevTimeUnix, 0) + + // Generate a test header timestamp that's significantly before + // the previous timestamp. Even though this would be rejected + // for retarget blocks, it shouldn't matter here. + timeDiff := rapid.Int64Range( + int64(maxTimeWarp+time.Second), + int64(maxTimeWarp+time.Hour*24*7), + ).Draw(t, "warp_amount") + invalidDelta := time.Duration(-timeDiff) + headerTime := prevBlockTime.Add(invalidDelta) + + // This should NOT fail for non-retarget blocks, even with + // extreme timewarp. + err := assertNoTimeWarp( + height, blocksPerRetarget, headerTime, prevBlockTime, + ) + require.NoError( + t, err, "non-retarget blocks should not be affected "+ + "by time warp check, but got: %v.", err, + ) + })) +} From 987745ed255862faaaccad20d25ebfd7828df1cf Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 10 Mar 2025 16:38:57 -0500 Subject: [PATCH 12/12] blockchain: implement AlwaysActiveHeight for forced deployment activation This commit introduces the concept of `AlwaysActiveHeight` to the deployment mechanism, allowing a deployment to be forced into the active state if the next block's height meets or exceeds this threshold. This is intended primarily to be used alongside the new Testnet4 deployment, as the past major soft forks are meant to be active from the very first block height. --- .gitignore | 1 + blockchain/thresholdstate.go | 17 ++++++- blockchain/thresholdstate_test.go | 4 ++ blockchain/versionbits.go | 41 +++++++++++---- chaincfg/params.go | 83 +++++++++++++++++++++++++++++++ integration/bip0009_test.go | 9 ++++ rpcserver.go | 3 ++ 7 files changed, 148 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 3b742cea..acfb8c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ btcutil/psbt/coverage.txt /gencerts .DS_Store +.aider* diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index d62c2de3..88031019 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -102,6 +102,11 @@ type thresholdConditionChecker interface { // not the bit associated with the condition is set, but can be more // complex as needed. Condition(*blockNode) (bool, error) + + // ForceActive returns if the deployment should be forced to transition + // to the active state. This is useful on certain testnet, where we + // we'd like for a deployment to always be active. + ForceActive(*blockNode) bool } // thresholdStateCache provides a type to cache the threshold states of each @@ -279,7 +284,17 @@ func thresholdStateTransition(state ThresholdState, prevNode *blockNode, // threshold states for previous windows are only calculated once. // // This function MUST be called with the chain state lock held (for writes). -func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdConditionChecker, cache *thresholdStateCache) (ThresholdState, error) { +func (b *BlockChain) thresholdState(prevNode *blockNode, + checker thresholdConditionChecker, + cache *thresholdStateCache) (ThresholdState, error) { + + // If the deployment has a nonzero AlwaysActiveHeight and the next + // block’s height is at or above that threshold, then force the state + // to Active. + if checker.ForceActive(prevNode) { + return ThresholdActive, nil + } + // The threshold state for the window that contains the genesis block is // defined by definition. confirmationWindow := int32(checker.MinerConfirmationWindow()) diff --git a/blockchain/thresholdstate_test.go b/blockchain/thresholdstate_test.go index 8d527137..28f417a1 100644 --- a/blockchain/thresholdstate_test.go +++ b/blockchain/thresholdstate_test.go @@ -175,6 +175,10 @@ func (c customDeploymentChecker) Condition(_ *blockNode) (bool, error) { return c.conditionTrue, nil } +func (c customDeploymentChecker) ForceActive(_ *blockNode) bool { + return false +} + // TestThresholdStateTransition tests that the thresholdStateTransition // properly implements the BIP 009 state machine, along with the speedy trial // augments. diff --git a/blockchain/versionbits.go b/blockchain/versionbits.go index 371d4f20..493787a7 100644 --- a/blockchain/versionbits.go +++ b/blockchain/versionbits.go @@ -134,6 +134,13 @@ func (c bitConditionChecker) IsSpeedy() bool { return false } +// ForceActive returns if the deployment should be forced to transition to the +// active state. This is useful on certain testnet, where we we'd like for a +// deployment to always be active. +func (c bitConditionChecker) ForceActive(node *blockNode) bool { + return false +} + // deploymentChecker provides a thresholdConditionChecker which can be used to // test a specific deployment rule. This is required for properly detecting // and activating consensus rule changes. @@ -207,15 +214,9 @@ func (c deploymentChecker) MinerConfirmationWindow() uint32 { } // EligibleToActivate returns true if a custom deployment can transition from -// the LockedIn to the Active state. For normal deployments, this always -// returns true. However, some deployments add extra rules like a minimum -// activation height, which can be abstracted into a generic arbitrary check at -// the final state via this method. -// -// This implementation always returns true, unless a minimum activation height -// is specified. -// -// This is part of the thresholdConditionChecker interface implementation. +// the LockedIn to the Active state. In addition to the traditional minimum +// activation height (MinActivationHeight), an optional AlwaysActiveHeight can +// force the deployment to be active after a specified height. func (c deploymentChecker) EligibleToActivate(blkNode *blockNode) bool { // No activation height, so it's always ready to go. if c.deployment.MinActivationHeight == 0 { @@ -249,6 +250,28 @@ func (c deploymentChecker) Condition(node *blockNode) (bool, error) { nil } +// ForceActive returns if the deployment should be forced to transition to the +// active state. This is useful on certain testnet, where we we'd like for a +// deployment to always be active. +func (c deploymentChecker) ForceActive(node *blockNode) bool { + if node == nil { + return false + } + + // If the deployment has a nonzero AlwaysActiveHeight and the next + // block’s height is at or above that threshold, then force the state + // to Active. + effectiveHeight := c.deployment.EffectiveAlwaysActiveHeight() + if uint32(node.height)+1 >= effectiveHeight { + log.Debugf("Force activating deployment: next block "+ + "height %d >= EffectiveAlwaysActiveHeight %d", + uint32(node.height)+1, effectiveHeight) + return true + } + + return false +} + // calcNextBlockVersion calculates the expected version of the block after the // passed previous block node based on the state of started and locked in // rule change deployments. diff --git a/chaincfg/params.go b/chaincfg/params.go index d97353f5..eb4f062e 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "encoding/hex" "errors" + "math" "math/big" "strings" "time" @@ -78,6 +79,16 @@ type Checkpoint struct { Hash *chainhash.Hash } +// EffectiveAlwaysActiveHeight returns the effective activation height for the +// deployment. If AlwaysActiveHeight is unset (i.e. zero), it returns +// the maximum uint32 value to indicate that it does not force activation. +func (d *ConsensusDeployment) EffectiveAlwaysActiveHeight() uint32 { + if d.AlwaysActiveHeight == 0 { + return math.MaxUint32 + } + return d.AlwaysActiveHeight +} + // DNSSeed identifies a DNS seed. type DNSSeed struct { // Host defines the hostname of the seed. @@ -108,6 +119,11 @@ type ConsensusDeployment struct { // activation. A value of 1815 block denotes a 90% threshold. CustomActivationThreshold uint32 + // AlwaysActiveHeight defines an optional block threshold at which the + // deployment is forced to be active. If unset (0), it defaults to + // math.MaxUint32, meaning the deployment does not force activation. + AlwaysActiveHeight uint32 + // DeploymentStarter is used to determine if the given // ConsensusDeployment has started or not. DeploymentStarter ConsensusDeploymentStarter @@ -146,6 +162,10 @@ const ( // the deployment of BIPS 340, 341 and 342. DeploymentTaproot + // DeploymentTestDummyAlwaysActive is a dummy deployment that is meant + // to always be active. + DeploymentTestDummyAlwaysActive + // NOTE: DefinedDeployments must always come last since it is used to // determine how many defined deployments there currently are. @@ -379,6 +399,16 @@ var MainNetParams = Params{ time.Time{}, // Never expires ), }, + DeploymentTestDummyAlwaysActive: { + BitNumber: 30, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + AlwaysActiveHeight: 1, + }, DeploymentCSV: { BitNumber: 0, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -490,6 +520,16 @@ var RegressionNetParams = Params{ time.Time{}, // Never expires ), }, + DeploymentTestDummyAlwaysActive: { + BitNumber: 30, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + AlwaysActiveHeight: 1, + }, DeploymentCSV: { BitNumber: 0, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -624,6 +664,16 @@ var TestNet3Params = Params{ time.Time{}, // Never expires ), }, + DeploymentTestDummyAlwaysActive: { + BitNumber: 30, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + AlwaysActiveHeight: 1, + }, DeploymentCSV: { BitNumber: 0, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -736,6 +786,16 @@ var TestNet4Params = Params{ time.Time{}, // Never expires ), }, + DeploymentTestDummyAlwaysActive: { + BitNumber: 30, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + AlwaysActiveHeight: 1, + }, DeploymentCSV: { BitNumber: 29, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -744,6 +804,7 @@ var TestNet4Params = Params{ DeploymentEnder: NewMedianTimeDeploymentEnder( time.Time{}, // Never expires ), + AlwaysActiveHeight: 1, }, DeploymentSegwit: { BitNumber: 29, @@ -753,6 +814,7 @@ var TestNet4Params = Params{ DeploymentEnder: NewMedianTimeDeploymentEnder( time.Time{}, // Never expires ), + AlwaysActiveHeight: 1, }, DeploymentTaproot: { BitNumber: 2, @@ -763,6 +825,7 @@ var TestNet4Params = Params{ time.Time{}, // Never expires ), MinActivationHeight: 0, + AlwaysActiveHeight: 1, }, }, @@ -877,6 +940,16 @@ var SimNetParams = Params{ ), CustomActivationThreshold: 75, // Only needs 75% hash rate. }, + DeploymentTestDummyAlwaysActive: { + BitNumber: 29, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + AlwaysActiveHeight: 1, + }, }, // Mempool parameters @@ -977,6 +1050,16 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params { time.Time{}, // Never expires ), }, + DeploymentTestDummyAlwaysActive: { + BitNumber: 30, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // Never expires + ), + AlwaysActiveHeight: 1, + }, DeploymentCSV: { BitNumber: 29, DeploymentStarter: NewMedianTimeDeploymentStarter( diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 5b644804..8f8b59a5 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -139,6 +139,14 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) { } defer r.TearDown() + // If the deployment is meant to be always active, then it should be + // active from the very first block. + if deploymentID == chaincfg.DeploymentTestDummyAlwaysActive { + assertChainHeight(r, t, 0) + assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive) + return + } + // *** ThresholdDefined *** // // Assert the chain height is the expected value and the soft fork @@ -340,6 +348,7 @@ func TestBIP0009(t *testing.T) { testBIP0009(t, "dummy", chaincfg.DeploymentTestDummy) testBIP0009(t, "dummy-min-activation", chaincfg.DeploymentTestDummyMinActivation) + testBIP0009(t, "dummy-always-active", chaincfg.DeploymentTestDummyAlwaysActive) testBIP0009(t, "segwit", chaincfg.DeploymentSegwit) } diff --git a/rpcserver.go b/rpcserver.go index 7483011c..736f1459 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1258,6 +1258,9 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str case chaincfg.DeploymentTestDummyMinActivation: forkName = "dummy-min-activation" + case chaincfg.DeploymentTestDummyAlwaysActive: + forkName = "dummy-always-active" + case chaincfg.DeploymentCSV: forkName = "csv"