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 8f2078af6a
  - 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.
This commit is contained in:
Sjors Provoost
2026-03-19 13:08:42 +01:00
parent 00d22328b0
commit 58eeab790d
13 changed files with 44 additions and 39 deletions

View File

@@ -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"},
},
};

View File

@@ -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);

View File

@@ -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.
*/

View File

@@ -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();

View File

@@ -390,7 +390,7 @@ TestChain100Setup::TestChain100Setup(
LOCK(::cs_main);
assert(
m_node.chainman->ActiveChain().Tip()->GetBlockHash().ToString() ==
"0c8c5f79505775a0f6aed6aca2350718ceb9c6f2c878667864d5c7a6d8ffa2a6");
"0ee6e270d6594249e548110619f7bd690695beb219b915da4a2e84e2b61ed60f");
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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']):

View File

@@ -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()

View File

@@ -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.

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)