From 58eeab790d9825a777f907e3e912a2da78cbc76d Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 19 Mar 2026 13:08:42 +0100 Subject: [PATCH] mining: only pad with OP_0 at heights <= 16 Drop the include_dummy_extranonce branch from the OP_0 padding condition in CreateNewBlock(), so that the dummy extraNonce is only appended when consensus actually requires it (heights <= 16, where the BIP34 height push alone would yield a 1-byte scriptSig and trigger bad-cb-length). The include_dummy_extranonce option struct field is now unused by the miner and is removed in the next commit. Callers still set it, so that this commit compiles. Regenerate the hardcoded coinbase / block hashes throughout the unit and functional test suites and update the regtest assumeutxo snapshot in chainparams. Additional side-effects: - Without the dummy extranonce, coinbase scriptSigs are 1 byte shorter at heights > 16, making every block 1 byte smaller. This shifts where block files wrap and therefore where pruning boundaries land. - feature_assumeutxo malleation cases: - case 1: error message changes due to UTXO reordering, similar to 8f2078af6a55448c003b3f7f3021955fbb351caa - case 4: the corruption byte is swapped from \x82 to \x83 because \x82 happened to be the actual value at that offset in the new snapshot. --- src/kernel/chainparams.cpp | 8 ++++---- src/node/miner.cpp | 5 ++--- src/node/types.h | 2 ++ src/test/fuzz/utxo_total_supply.cpp | 8 ++++++-- src/test/util/setup_common.cpp | 2 +- src/test/validation_tests.cpp | 6 +++--- test/functional/feature_assumeutxo.py | 20 ++++++++++---------- test/functional/feature_index_prune.py | 12 ++++++------ test/functional/feature_utxo_set_hash.py | 4 ++-- test/functional/rpc_dumptxoutset.py | 6 +++--- test/functional/rpc_getblockfrompeer.py | 6 +++--- test/functional/tool_bitcoin_chainstate.py | 2 +- test/functional/wallet_assumeutxo.py | 2 +- 13 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 02c7ae9d220..969a838cd22 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 574e293ac11..7fc19deeb79 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -189,12 +189,11 @@ std::unique_ptr 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 cc0c404892f..f0debd459a3 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 1275fd45c43..9e7ee205063 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 39c691c3364..c9511908b9e 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 b1e204f1741..268529fd802 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 ae130ee2035..8727a9925ec 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 0654e04f586..ba4907acdcf 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 733b1fea185..fb64439ecc5 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 80c4bf6fe6a..47568c248e0 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 841cde02197..9adcd88c9ee 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 c57da1fc376..6b7128898d2 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 a8c97be778f..3f28e36c805 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)