What changed, and why it matters
This commit fixes a subtle off-by-one bug in btcd's blockchain validation. BIP30 is a rule that prevents duplicate coinbase transactions, which could otherwise allow an attacker to overwrite existing Bitcoin funds. BIP34 later made duplicate coinbases impossible by requiring unique block heights in coinbase data. btcd was skipping the BIP30 duplicate check starting exactly at the BIP34 activation block, but Bitcoin Core still runs the BIP30 check for that one activation block. The change aligns btcd with Core by moving the skip point one block later, so the duplicate-coinbase check is still enforced at the BIP34 activation height.
Treat as a consensus-correctness fix worth backporting to maintained release branches. Nodes should upgrade to avoid potential chain-split risk if a block at the BIP34 activation height were crafted to trigger the divergent rule. Review whether any other height-based BIP activation checks have similar off-by-one discrepancies with Bitcoin Core.
Security signals we found
Consensus-rule divergence from Bitcoin Core
Off-by-one height comparison in validation logic
BIP30 duplicate-coinbase enforcement gap at BIP34 activation block
Added regression test for activation-height behavior
Evidence from the diff
The patch changes bip0030CheckNeeded in blockchain/validate.go from node.height >= params.BIP0034Height to node.height > params.BIP0034Height. Previously, btcd disabled the BIP30 duplicate-coinbase check for the activation block itself. Bitcoin Core only disables BIP30 once the previous block’s ancestor chain already contains the BIP34 activation block; for the activation block, pindex->pprev->GetAncestor(BIP34Height) is null, so the flag remains set and BIP30 is still enforced. The commit syncs btcd with that behavior and adds a unit test verifying the activation block still requires the check.
Changed components
blockchain/validate.go:bip0030CheckNeededblockchain/bip30_test.goInspect captured patch +15 / −1
diff --git a/blockchain/bip30_test.go b/blockchain/bip30_test.go
index c741b32..520096c 100644
--- a/blockchain/bip30_test.go
+++ b/blockchain/bip30_test.go
@@ -61,6 +61,20 @@ func TestBip0030CheckNeededBeforeBIP34(t *testing.T) {
require.True(t, bip0030CheckNeeded(node, ¶ms))
}
+// TestBip0030CheckNeededAtActivationHeight validates that the activation block
+// itself still triggers the check.
+func TestBip0030CheckNeededAtActivationHeight(t *testing.T) {
+ params := chaincfg.MainNetParams
+
+ node := &blockNode{
+ height: params.BIP0034Height,
+ hash: mustHashFromStr(t, "000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8"),
+ parent: &blockNode{height: params.BIP0034Height - 1},
+ }
+
+ require.True(t, bip0030CheckNeeded(node, ¶ms))
+}
+
// TestBip0030CheckNeededAfterBIP34 covers the happy-path where we are on a
// chain that contains the recorded BIP34 activation block. In that case the
// expensive duplicate-coinbase check can be skipped.
diff --git a/blockchain/validate.go b/blockchain/validate.go
index f650fa9..cf98d6a 100644
--- a/blockchain/validate.go
+++ b/blockchain/validate.go
@@ -219,7 +219,7 @@ 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 {
+ if node.height > params.BIP0034Height {
return false
}
Why this scored 60/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.