blockchain: run BIP30 if we are on non-BIP34 fork
What changed, and why it matters
This commit fixes a subtle consensus bug in btcd, a Bitcoin implementation. BIP30 prevents two blocks from having the same special 'coinbase' transaction ID, and BIP34 makes duplicate coinbase IDs practically impossible once it activates. btcd was skipping the BIP30 check based only on block height, but on an alternate chain that has not actually activated BIP34 at that height, a duplicate coinbase could slip through. The fix mirrors Bitcoin Core by also checking that the known BIP34 activation block hash is present on the current chain before disabling BIP30.
Review and merge the fix, then ensure all deployed btcd nodes are upgraded. Nodes running older code risk consensus divergence on chains that lack the BIP34 activation block. Consider backporting to supported release branches.
Security signals we found
Consensus-rule divergence from Bitcoin Core
BIP30 duplicate-coinbase check could be skipped incorrectly on alternate/forked chains
Potential for duplicate transaction IDs if exploited
Fix mirrors Bitcoin Core PR #6931 behavior
Evidence from the diff
The patch changes bip0030CheckNeeded in blockchain/validate.go so that it no longer returns false solely when node.height > params.BIP0034Height. It now also verifies that an ancestor at the BIP34 activation height matches params.BIP0034Hash. A new BIP0034Hash field is added to chaincfg.Params and populated for MainNet and TestNet3 with the known activation block hashes. A unit test is added to confirm that a mismatched activation hash keeps BIP30 enforcement enabled.
Changed components
blockchain/validate.gochaincfg/params.goblockchain/bip30_test.goInspect captured patch +40 / −1
diff --git a/blockchain/bip30_test.go b/blockchain/bip30_test.go
index 520096c..c3a21ab 100644
--- a/blockchain/bip30_test.go
+++ b/blockchain/bip30_test.go
@@ -98,3 +98,28 @@ func TestBip0030CheckNeededAfterBIP34(t *testing.T) {
require.False(t, bip0030CheckNeeded(node, ¶ms))
}
+
+// TestBip0030CheckNeededMismatchedActivation verifies that if the block at the
+// recorded BIP34 activation height does not match, we continue to run the BIP30
+// check (this matches Bitcoin Core's behavior and guards against alternative
+// chains that have not activated BIP34 yet).
+func TestBip0030CheckNeededMismatchedActivation(t *testing.T) {
+ params := chaincfg.MainNetParams
+
+ ancestor := &blockNode{
+ height: params.BIP0034Height,
+ hash: mustHashFromStr(t, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
+ }
+ parent := &blockNode{
+ height: ancestor.height + 1,
+ hash: mustHashFromStr(t, "0000000000000000000000000000000000000000000000000000000000000004"),
+ parent: ancestor,
+ }
+ node := &blockNode{
+ height: parent.height + 1,
+ hash: mustHashFromStr(t, "0000000000000000000000000000000000000000000000000000000000000005"),
+ parent: parent,
+ }
+
+ require.True(t, bip0030CheckNeeded(node, ¶ms))
+}
diff --git a/blockchain/validate.go b/blockchain/validate.go
index cf98d6a..818fee2 100644
--- a/blockchain/validate.go
+++ b/blockchain/validate.go
@@ -220,7 +220,18 @@ func bip0030CheckNeeded(node *blockNode, params *chaincfg.Params) bool {
// Once BIP0034 is known to be active on this chain, duplicate coinbases
// can no longer occur, so the check can be omitted.
if node.height > params.BIP0034Height {
- return false
+ // Make sure that BIP0034 was activated. We need to make sure
+ // that there is a block with the hash we expect at the height
+ // BIP0034Height. If this is not the case, we might be on an
+ // alternate chain that hasn't activated BIP34 yet - even if its
+ // height is higher. In that case, BIP30 still applies.
+ if params.BIP0034Hash != nil && node.parent != nil {
+ ancestor := node.parent.Ancestor(params.BIP0034Height)
+ want := params.BIP0034Hash
+ if ancestor != nil && ancestor.hash.IsEqual(want) {
+ return false
+ }
+ }
}
return true
diff --git a/chaincfg/params.go b/chaincfg/params.go
index 4ddeb5d..1abe7fc 100644
--- a/chaincfg/params.go
+++ b/chaincfg/params.go
@@ -216,6 +216,7 @@ type Params struct {
// These fields define the block heights at which the specified softfork
// BIP became active.
BIP0034Height int32
+ BIP0034Hash *chainhash.Hash
BIP0065Height int32
BIP0066Height int32
@@ -322,6 +323,7 @@ var MainNetParams = Params{
PowLimit: mainPowLimit,
PowLimitBits: 0x1d00ffff,
BIP0034Height: 227931, // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8
+ BIP0034Hash: newHashFromStr("000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8"),
BIP0065Height: 388381, // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0
BIP0066Height: 363725, // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931
CoinbaseMaturity: 100,
@@ -600,6 +602,7 @@ var TestNet3Params = Params{
GenesisHash: &testNet3GenesisHash,
PowLimit: testNet3PowLimit,
PowLimitBits: 0x1d00ffff,
+ BIP0034Hash: newHashFromStr("0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8"),
BIP0034Height: 21111, // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8
BIP0065Height: 581885, // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6
BIP0066Height: 330776, // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182
Why this scored 72/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.