mining: only pad with OP_0 at heights <= 16
What changed, and why it matters
This Bitcoin Core change removes an unnecessary extra zero byte (OP_0) from newly mined coinbase transactions after block height 16. That byte was originally added to satisfy a minimum coinbase script length rule, but only heights 1-16 actually need it. The change is a cleanup that makes blocks one byte smaller and updates many hardcoded test hashes accordingly. It does not fix a vulnerability and does not appear exploitable.
No security action required. Reviewers and downstream integrators should expect changed deterministic block/UTXO hashes in regtest/test networks and update any external fixtures that depend on the old values. The deprecated include_dummy_extranonce option should be removed in a follow-up as noted by the TODO.
Security signals we found
No security-relevant bug is fixed; the change is a miner behavior cleanup.
Consensus rule (bad-cb-length) is still satisfied: heights <= 16 keep the OP_0 padding.
Block serialization size decreases by 1 byte for heights > 16, affecting deterministic test hashes and pruning thresholds.
include_dummy_extranonce is deprecated/ignored, not removed yet, to keep callers compiling.
Evidence from the diff
In CreateNewBlock(), the condition for appending OP_0 to the coinbase scriptSig is narrowed from (nHeight <= 16 || include_dummy_extranonce) to just (nHeight <= 16). The include_dummy_extranonce option is now ignored and will be removed later. Because the coinbase scriptSig becomes one byte shorter for blocks above height 16, block hashes, UTXO set hashes, assumeutxo snapshots, and pruning boundaries all shift. The commit regenerates the affected hardcoded values across unit and functional tests and updates the regtest assumeutxo snapshot in chainparams. The change is consensus-compatible because the old behavior was always valid; the new behavior is also valid and closer to the minimal required form.
Changed components
src/node/miner.cpp - CreateNewBlock coinbase scriptSig padding logicsrc/node/types.h - BlockCreateOptions include_dummy_extranonce documentationsrc/kernel/chainparams.cpp - regtest assumeutxo snapshot hashessrc/test/fuzz/utxo_total_supply.cpp - duplicate coinbase padding logicsrc/test/util/setup_common.cpp - deterministic test chain tip hashsrc/test/validation_tests.cpp - assumeutxo hash assertionstest/functional/feature_assumeutxo.pytest/functional/feature_index_prune.pytest/functional/feature_utxo_set_hash.pytest/functional/rpc_dumptxoutset.pytest/functional/rpc_getblockfrompeer.pytest/functional/tool_bitcoin_chainstate.pytest/functional/wallet_assumeutxo.pyInspect captured patch +44 / −39
diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp
index 02c7ae9d..969a838c 100644
--- a/src/kernel/chainparams.cpp
+++ b/src/kernel/chainparams.cpp
@@ -607,9 +607,9 @@ public:
m_assumeutxo_data = {
{ // For use by unit tests
.height = 110,
- .hash_serialized = AssumeutxoHash{uint256{"b952555c8ab81fec46f3d4253b7af256d766ceb39fb7752b9d18cdf4a0141327"}},
+ .hash_serialized = AssumeutxoHash{uint256{"86e9a1205b418b16dde3a18a78c730e30137e28466bda5dbf6b33ab8fc05447c"}},
.m_chain_tx_count = 111,
- .blockhash = uint256{"6affe030b7965ab538f820a56ef56c8149b7dc1d1c144af57113be080db7c397"},
+ .blockhash = uint256{"135eec25a6fb277884e5824e7aa7d052c4868161c99a5122170b5266f86c273d"},
},
{
// For use by fuzz target src/test/fuzz/utxo_snapshot.cpp
@@ -621,9 +621,9 @@ public:
{
// For use by test/functional/feature_assumeutxo.py and test/functional/tool_bitcoin_chainstate.py
.height = 299,
- .hash_serialized = AssumeutxoHash{uint256{"d2b051ff5e8eef46520350776f4100dd710a63447a8e01d917e92e79751a63e2"}},
+ .hash_serialized = AssumeutxoHash{uint256{"106b2c56233e378a824cf0d5ff2be42ed32c72f1605c9be288d00942908a40ac"}},
.m_chain_tx_count = 334,
- .blockhash = uint256{"7cc695046fec709f8c9394b6f928f81e81fd3ac20977bb68760fa1faa7916ea2"},
+ .blockhash = uint256{"0c552ced4721c249a389eb9b08cb8da261cd46f0e7b5f9d064d48f3113406853"},
},
};
diff --git a/src/node/miner.cpp b/src/node/miner.cpp
index 574e293a..7fc19dee 100644
--- a/src/node/miner.cpp
+++ b/src/node/miner.cpp
@@ -189,12 +189,11 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
// and in a typical setup a pool name or realistic extraNonce already makes
// the scriptSig long enough.
coinbase_tx.script_sig_prefix = coinbaseTx.vin[0].scriptSig;
- if (nHeight <= 16 || m_options.include_dummy_extranonce) {
+ if (nHeight <= 16) {
// For blocks at heights <= 16, the BIP34-encoded height alone is only
// one byte. Consensus requires coinbase scriptSigs to be at least two
// bytes long (bad-cb-length), so an OP_0 is always appended at those
- // heights. At greater heights it is only added when the caller
- // requests it (e.g. RPC and test code that rely on stable hashes).
+ // heights.
coinbaseTx.vin[0].scriptSig << OP_0;
}
Assert(nHeight > 0);
diff --git a/src/node/types.h b/src/node/types.h
index cc0c4048..f0debd45 100644
--- a/src/node/types.h
+++ b/src/node/types.h
@@ -75,6 +75,8 @@ struct BlockCreateOptions {
/**
* Whether to include an OP_0 as a dummy extraNonce in the template's coinbase
*
+ * This option is ignored and no longer has any effect.
+ *
* TODO: this can be dropped after regenerating hardcoded block and transaction
* hashes in the test suite.
*/
diff --git a/src/test/fuzz/utxo_total_supply.cpp b/src/test/fuzz/utxo_total_supply.cpp
index 1275fd45..9e7ee205 100644
--- a/src/test/fuzz/utxo_total_supply.cpp
+++ b/src/test/fuzz/utxo_total_supply.cpp
@@ -107,8 +107,12 @@ FUZZ_TARGET(utxo_total_supply)
// Assuming that the fuzzer will mine relatively short chains (less than 200 blocks), we want the duplicate coinbase to be not too high.
// Up to 300 seems reasonable.
int64_t duplicate_coinbase_height = fuzzed_data_provider.ConsumeIntegralInRange(0, 300);
- // Always pad with OP_0 at the end to avoid bad-cb-length error
- const CScript duplicate_coinbase_script = CScript() << duplicate_coinbase_height << OP_0;
+ // Avoid bad-cb-length error at heights <= 16. Pad the BIP34-encoded height
+ // with OP_0 to satisfy the minimum 2-byte coinbase scriptSig length.
+ CScript duplicate_coinbase_script = CScript() << duplicate_coinbase_height;
+ if (duplicate_coinbase_height <= 16) {
+ duplicate_coinbase_script << OP_0;
+ }
// Mine the first block with this duplicate
current_block = PrepareNextBlock();
StoreLastTxo();
diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp
index 39c691c3..c9511908 100644
--- a/src/test/util/setup_common.cpp
+++ b/src/test/util/setup_common.cpp
@@ -390,7 +390,7 @@ TestChain100Setup::TestChain100Setup(
LOCK(::cs_main);
assert(
m_node.chainman->ActiveChain().Tip()->GetBlockHash().ToString() ==
- "0c8c5f79505775a0f6aed6aca2350718ceb9c6f2c878667864d5c7a6d8ffa2a6");
+ "0ee6e270d6594249e548110619f7bd690695beb219b915da4a2e84e2b61ed60f");
}
}
diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp
index b1e204f1..268529fd 100644
--- a/src/test/validation_tests.cpp
+++ b/src/test/validation_tests.cpp
@@ -142,11 +142,11 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo)
}
const auto out110 = *params->AssumeutxoForHeight(110);
- BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "b952555c8ab81fec46f3d4253b7af256d766ceb39fb7752b9d18cdf4a0141327");
+ BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "86e9a1205b418b16dde3a18a78c730e30137e28466bda5dbf6b33ab8fc05447c");
BOOST_CHECK_EQUAL(out110.m_chain_tx_count, 111U);
- const auto out110_2 = *params->AssumeutxoForBlockhash(uint256{"6affe030b7965ab538f820a56ef56c8149b7dc1d1c144af57113be080db7c397"});
- BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "b952555c8ab81fec46f3d4253b7af256d766ceb39fb7752b9d18cdf4a0141327");
+ const auto out110_2 = *params->AssumeutxoForBlockhash(uint256{"135eec25a6fb277884e5824e7aa7d052c4868161c99a5122170b5266f86c273d"});
+ BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "86e9a1205b418b16dde3a18a78c730e30137e28466bda5dbf6b33ab8fc05447c");
BOOST_CHECK_EQUAL(out110_2.m_chain_tx_count, 111U);
}
diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py
index ae130ee2..8727a992 100755
--- a/test/functional/feature_assumeutxo.py
+++ b/test/functional/feature_assumeutxo.py
@@ -139,12 +139,12 @@ class AssumeutxoTest(BitcoinTestFramework):
self.log.info(" - snapshot file with alternated but parsable UTXO data results in different hash")
cases = [
# (content, offset, wrong_hash, custom_message)
- [b"\xff" * 32, 0, "77874d48d932a5cb7a7f770696f5224ff05746fdcf732a58270b45da0f665934", None], # wrong outpoint hash
- [(2).to_bytes(1, "little"), 32, None, "Bad snapshot format or truncated snapshot after deserializing 1 coins."], # wrong txid coins count
+ [b"\xff" * 32, 0, "43bb0fcc50f3789d50ce41891810bbc571d549a32e2bbaabfc036f73ae2a23f9", None], # wrong outpoint hash
+ [(2).to_bytes(1, "little"), 32, None, "Bad snapshot data after deserializing 2 coins."], # wrong txid coins count
[b"\xfd\xff\xff", 32, None, "Mismatch in coins count in snapshot metadata and actual snapshot data"], # txid coins count exceeds coins left
- [b"\x01", 33, "9f562925721e4f97e6fde5b590dbfede51e2204a68639525062ad064545dd0ea", None], # wrong outpoint index
- [b"\x82", 34, "161393f07f8ad71760b3910a914f677f2cb166e5bcf5354e50d46b78c0422d15", None], # wrong coin code VARINT
- [b"\x80", 34, "e6fae191ef851554467b68acff01ca09ad0a2e48c9b3dfea46cf7d35a7fd0ad0", None], # another wrong coin code
+ [b"\x01", 33, "0b0e96fd9a556157a68d9e6fe6e2c7562169c730347565ce0725032f824bea34", None], # wrong outpoint index
+ [b"\x83", 34, "52084c732f760fbcfb804d854815a9145111e93d7c7f3f7bfcb0ade1ea4f6b99", None], # wrong coin code VARINT
+ [b"\x80", 34, "c1a1308fcf04cbb14fc777467eb5e98e77a921b66eac4ad47877d0b6ed7b15b1", None], # another wrong coin code
[b"\x84\x58", 34, None, "Bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
[
# compressed txout value + scriptpubkey
@@ -163,12 +163,12 @@ class AssumeutxoTest(BitcoinTestFramework):
f.write(content)
f.write(valid_snapshot_contents[(5 + 2 + 4 + 32 + 8 + offset + len(content)):])
- msg = custom_message if custom_message is not None else f"Bad snapshot content hash: expected d2b051ff5e8eef46520350776f4100dd710a63447a8e01d917e92e79751a63e2, got {wrong_hash}."
+ msg = custom_message if custom_message is not None else f"Bad snapshot content hash: expected 106b2c56233e378a824cf0d5ff2be42ed32c72f1605c9be288d00942908a40ac, got {wrong_hash}."
expected_error(msg)
def test_headers_not_synced(self, valid_snapshot_path):
for node in self.nodes[1:]:
- msg = "Unable to load UTXO snapshot: The base block header (7cc695046fec709f8c9394b6f928f81e81fd3ac20977bb68760fa1faa7916ea2) must appear in the headers chain. Make sure all headers are syncing, and call loadtxoutset again."
+ msg = "Unable to load UTXO snapshot: The base block header (0c552ced4721c249a389eb9b08cb8da261cd46f0e7b5f9d064d48f3113406853) must appear in the headers chain. Make sure all headers are syncing, and call loadtxoutset again."
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, valid_snapshot_path)
def test_invalid_chainstate_scenarios(self):
@@ -228,7 +228,7 @@ class AssumeutxoTest(BitcoinTestFramework):
block_hash = node.getblockhash(height)
node.invalidateblock(block_hash)
assert_equal(node.getblockcount(), height - 1)
- msg = "Unable to load UTXO snapshot: The base block header (7cc695046fec709f8c9394b6f928f81e81fd3ac20977bb68760fa1faa7916ea2) is part of an invalid chain."
+ msg = "Unable to load UTXO snapshot: The base block header (0c552ced4721c249a389eb9b08cb8da261cd46f0e7b5f9d064d48f3113406853) is part of an invalid chain."
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
node.reconsiderblock(block_hash)
@@ -469,7 +469,7 @@ class AssumeutxoTest(BitcoinTestFramework):
def check_dump_output(output):
assert_equal(
output['txoutset_hash'],
- "d2b051ff5e8eef46520350776f4100dd710a63447a8e01d917e92e79751a63e2")
+ "106b2c56233e378a824cf0d5ff2be42ed32c72f1605c9be288d00942908a40ac")
assert_equal(output["nchaintx"], blocks[SNAPSHOT_BASE_HEIGHT].chain_tx)
check_dump_output(dump_output)
@@ -499,7 +499,7 @@ class AssumeutxoTest(BitcoinTestFramework):
dump_output4 = n0.dumptxoutset(path='utxos4.dat', rollback=prev_snap_height)
assert_equal(
dump_output4['txoutset_hash'],
- "45ac2777b6ca96588210e2a4f14b602b41ec37b8b9370673048cc0af434a1ec8")
+ "147d1789e3e29a62e3e15b80078b50603afb5e1e70ea07001c94666afbde38df")
assert_not_equal(sha256sum_file(dump_output['path']), sha256sum_file(dump_output4['path']))
# Use a hash instead of a height
diff --git a/test/functional/feature_index_prune.py b/test/functional/feature_index_prune.py
index 0654e04f..ba4907ac 100755
--- a/test/functional/feature_index_prune.py
+++ b/test/functional/feature_index_prune.py
@@ -100,7 +100,7 @@ class FeatureIndexPruneTest(BitcoinTestFramework):
pruneheight_new = node.pruneblockchain(400)
# the prune heights used here and below are magic numbers that are determined by the
# thresholds at which block files wrap, so they depend on disk serialization and default block file size.
- assert_equal(pruneheight_new, 248)
+ assert_equal(pruneheight_new, 249)
self.log.info("check if we can access the tips blockfilter and coinstats when we have pruned some blocks")
tip = self.nodes[0].getbestblockhash()
@@ -117,8 +117,8 @@ class FeatureIndexPruneTest(BitcoinTestFramework):
assert node.gettxoutsetinfo(hash_type="muhash", hash_or_height=height_hash)['muhash']
# mine and sync index up to a height that will later be the pruneheight
- self.generate(self.nodes[0], 51)
- self.sync_index(height=751)
+ self.generate(self.nodes[0], 54)
+ self.sync_index(height=754)
self.restart_without_indices()
@@ -130,12 +130,12 @@ class FeatureIndexPruneTest(BitcoinTestFramework):
msg = "Querying specific block heights requires coinstatsindex"
assert_raises_rpc_error(-8, msg, node.gettxoutsetinfo, "muhash", height_hash)
- self.generate(self.nodes[0], 749)
+ self.generate(self.nodes[0], 746)
self.log.info("prune exactly up to the indices best blocks while the indices are disabled")
for i in range(3):
pruneheight_2 = self.nodes[i].pruneblockchain(1000)
- assert_equal(pruneheight_2, 750)
+ assert_equal(pruneheight_2, 753)
# Restart the nodes again with the indices activated
self.restart_node(i, extra_args=self.extra_args[i])
@@ -195,7 +195,7 @@ class FeatureIndexPruneTest(BitcoinTestFramework):
for node in self.nodes[:2]:
with node.assert_debug_log(['limited pruning to height 2489']):
pruneheight_new = node.pruneblockchain(2500)
- assert_equal(pruneheight_new, 2005)
+ assert_equal(pruneheight_new, 2013)
self.log.info("ensure that prune locks don't prevent indices from failing in a reorg scenario")
with self.nodes[0].assert_debug_log(['basic block filter index prune lock moved back to 2480']):
diff --git a/test/functional/feature_utxo_set_hash.py b/test/functional/feature_utxo_set_hash.py
index 733b1fea..fb64439e 100755
--- a/test/functional/feature_utxo_set_hash.py
+++ b/test/functional/feature_utxo_set_hash.py
@@ -67,8 +67,8 @@ class UTXOSetHashTest(BitcoinTestFramework):
assert_equal(finalized[::-1].hex(), node_muhash)
self.log.info("Test deterministic UTXO set hash results")
- assert_equal(node.gettxoutsetinfo()['hash_serialized_3'], "e0b4c80f2880985fdf1adc331ed0735ac207588f986c91c7c05e8cf5fe6780f0")
- assert_equal(node.gettxoutsetinfo("muhash")['muhash'], "8739b878f23030ef39a5547edc7b57f88d50fdaaf47314ff0524608deb13067e")
+ assert_equal(node.gettxoutsetinfo()['hash_serialized_3'], "396058cfd7f3b4c05dcdfaf360be9986d859d2c8315082d3aadda6d2d16add25")
+ assert_equal(node.gettxoutsetinfo("muhash")['muhash'], "97f341d4226cb4451dd9da2856759fb97659cfc238a0b8d5452e64af6032bd3d")
def run_test(self):
self.test_muhash_implementation()
diff --git a/test/functional/rpc_dumptxoutset.py b/test/functional/rpc_dumptxoutset.py
index 80c4bf6f..47568c24 100755
--- a/test/functional/rpc_dumptxoutset.py
+++ b/test/functional/rpc_dumptxoutset.py
@@ -68,15 +68,15 @@ class DumptxoutsetTest(BitcoinTestFramework):
# Blockhash should be deterministic based on mocked time.
assert_equal(
out['base_hash'],
- '6885775faa46290bedfa071f22d0598c93f1d7e01f24607c4dedd69b9baa4a8f')
+ '220aee93f0f5409631f35488898258f0930952bd620063cb4d7d87f7c28a8f50')
# UTXO snapshot hash should be deterministic based on mocked time.
assert_equal(
sha256sum_file(str(expected_path)).hex(),
- 'd9506d541437f5e2892d6b6ea173f55233de11601650c157a27d8f2b9d08cb6f')
+ 'e8c59b1bc1f19061c67eb7a392f4ea17eea83af58646ea2909e270546699c36c')
assert_equal(
- out['txoutset_hash'], 'd4453995f4f20db7bb3a604afd10d7128e8ee11159cde56d5b2fd7f55be7c74c')
+ out['txoutset_hash'], '771d773b5c27b6f35f598ce764652a2cf28fbc268341eb1827844e416c629c7d')
assert_equal(out['nchaintx'], 101)
# Specifying a path to an existing or invalid file will fail.
diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py
index 841cde02..9adcd88c 100755
--- a/test/functional/rpc_getblockfrompeer.py
+++ b/test/functional/rpc_getblockfrompeer.py
@@ -128,7 +128,7 @@ class GetBlockFromPeerTest(BitcoinTestFramework):
self.generate(self.nodes[0], 400, sync_fun=self.no_op)
self.sync_blocks([self.nodes[0], pruned_node])
pruneheight = pruned_node.pruneblockchain(300)
- assert_equal(pruneheight, 248)
+ assert_equal(pruneheight, 249)
# Ensure the block is actually pruned
pruned_block = self.nodes[0].getblockhash(2)
assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)
@@ -144,14 +144,14 @@ class GetBlockFromPeerTest(BitcoinTestFramework):
self.log.info("Fetched block persists after next pruning event")
self.generate(self.nodes[0], 250, sync_fun=self.no_op)
self.sync_blocks([self.nodes[0], pruned_node])
- pruneheight += 251
+ pruneheight += 252
assert_equal(pruned_node.pruneblockchain(700), pruneheight)
assert_equal(pruned_node.getblock(pruned_block)["hash"], "196ee3a1a6db2353965081c48ef8e6b031cb2115d084bec6fec937e91a2c6277")
self.log.info("Fetched block can be pruned again when prune height exceeds the height of the tip at the time when the block was fetched")
self.generate(self.nodes[0], 250, sync_fun=self.no_op)
self.sync_blocks([self.nodes[0], pruned_node])
- pruneheight += 250
+ pruneheight += 251
assert_equal(pruned_node.pruneblockchain(1000), pruneheight)
assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)
diff --git a/test/functional/tool_bitcoin_chainstate.py b/test/functional/tool_bitcoin_chainstate.py
index c57da1fc..6b712889 100755
--- a/test/functional/tool_bitcoin_chainstate.py
+++ b/test/functional/tool_bitcoin_chainstate.py
@@ -20,7 +20,7 @@ from test_framework.wallet import MiniWallet
START_HEIGHT = 199
# Hardcoded in regtest chainparams
SNAPSHOT_BASE_BLOCK_HEIGHT = 299
-SNAPSHOT_BASE_BLOCK_HASH = "7cc695046fec709f8c9394b6f928f81e81fd3ac20977bb68760fa1faa7916ea2"
+SNAPSHOT_BASE_BLOCK_HASH = "0c552ced4721c249a389eb9b08cb8da261cd46f0e7b5f9d064d48f3113406853"
class BitcoinChainstateTest(BitcoinTestFramework):
diff --git a/test/functional/wallet_assumeutxo.py b/test/functional/wallet_assumeutxo.py
index a8c97be7..3f28e36c 100755
--- a/test/functional/wallet_assumeutxo.py
+++ b/test/functional/wallet_assumeutxo.py
@@ -172,7 +172,7 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(
dump_output['txoutset_hash'],
- "d2b051ff5e8eef46520350776f4100dd710a63447a8e01d917e92e79751a63e2")
+ "106b2c56233e378a824cf0d5ff2be42ed32c72f1605c9be288d00942908a40ac")
assert_equal(dump_output["nchaintx"], 334)
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
Why this scored 18/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.