[miner] omit dummy extraNonce via IPC
What changed, and why it matters
This Bitcoin Core change tweaks the coinbase transaction—the special first transaction in a newly mined block—that is handed to external mining software through the new Mining IPC interface. Previously the template included an unnecessary dummy byte (OP_0) after the block height. The commit removes that dummy byte for IPC callers so downstream miners get only the consensus-required data. This is a cleanup and future-proofing measure, not a fix for an active exploit, but it reduces the chance that external mining software would mishandle or ignore the provided scriptSig if future consensus rules add more data there.
Treat as a hardening/miner-interface correctness change rather than an urgent security patch. Reviewers and operators using the Mining IPC interface should ensure their downstream mining software appends its own extraNonce/scriptSig data and pads appropriately for blocks at heights 1–16 to avoid bad-cb-length. No immediate network-wide action is required.
Security signals we found
Change to coinbase scriptSig generation in miner code
New boolean option include_dummy_extranonce with default false
IPC-exposed block template no longer includes dummy OP_0 extraNonce
All RPC and test paths opt back into legacy dummy byte
Added functional test asserting absence of dummy extraNonce in IPC template
Comment notes bad-cb-length risk for blocks 1–16 when dummy is omitted
Evidence from the diff
BlockAssembler::CreateNewBlock() previously always appended OP_0 to the coinbase scriptSig after the BIP34 height push. The commit adds an include_dummy_extranonce option defaulting to false. IPC/mining-interface callers therefore receive a coinbase scriptSig containing only the height push. All internal tests and RPC paths (generate, generateblock, getblocktemplate) explicitly set include_dummy_extranonce=true to preserve existing behavior and avoid bad-cb-length for blocks 1–16. A functional test verifies that the IPC coinbase scriptSig prefix equals CScript([height]) with no trailing OP_0. The change is defensive: it limits what Bitcoin Core commits to the scriptSig and avoids giving downstream Stratum v2 clients data they may discard or that could conflict with future soft-fork requirements.
Changed components
src/node/miner.cppsrc/node/types.hsrc/rpc/mining.cppMining IPC interface (BlockTemplate/coinbase scriptSigPrefix)test/functional/interface_ipc.pyInspect captured patch +55 / −12
diff --git a/src/bench/block_assemble.cpp b/src/bench/block_assemble.cpp
index 297465be..702f2c09 100644
--- a/src/bench/block_assemble.cpp
+++ b/src/bench/block_assemble.cpp
@@ -30,6 +30,7 @@ static void AssembleBlock(benchmark::Bench& bench)
witness.stack.push_back(WITNESS_STACK_ELEM_OP_TRUE);
BlockAssembler::Options options;
options.coinbase_output_script = P2WSH_OP_TRUE;
+ options.include_dummy_extranonce = true;
// Collect some loose transactions that spend the coinbases of our mined blocks
constexpr size_t NUM_BLOCKS{200};
diff --git a/src/node/miner.cpp b/src/node/miner.cpp
index 58b147ca..b5073054 100644
--- a/src/node/miner.cpp
+++ b/src/node/miner.cpp
@@ -177,11 +177,17 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
coinbase_tx.block_reward_remaining = block_reward;
// Start the coinbase scriptSig with the block height as required by BIP34.
- // The trailing OP_0 (historically an extranonce) is optional padding and
- // could be removed without a consensus change. Mining clients are expected
- // to append extra data to this prefix, so increasing its length would reduce
- // the space they can use and may break existing clients.
- coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0;
+ // Mining clients are expected to append extra data to this prefix, so
+ // increasing its length would reduce the space they can use and may break
+ // existing clients.
+ coinbaseTx.vin[0].scriptSig = CScript() << nHeight;
+ if (m_options.include_dummy_extranonce) {
+ // 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 tests and regtest include a dummy
+ // extraNonce (OP_0)
+ coinbaseTx.vin[0].scriptSig << OP_0;
+ }
coinbase_tx.script_sig_prefix = coinbaseTx.vin[0].scriptSig;
Assert(nHeight > 0);
coinbaseTx.nLockTime = static_cast<uint32_t>(nHeight - 1);
@@ -212,6 +218,7 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
pblock->nNonce = 0;
if (m_options.test_block_validity) {
+ // if nHeight <= 16, and include_dummy_extranonce=false this will fail due to bad-cb-length.
if (BlockValidationState state{TestBlockValidity(m_chainstate, *pblock, /*check_pow=*/false, /*check_merkle_root=*/false)}; !state.IsValid()) {
throw std::runtime_error(strprintf("TestBlockValidity failed: %s", state.ToString()));
}
diff --git a/src/node/types.h b/src/node/types.h
index 6930672f..deab1faa 100644
--- a/src/node/types.h
+++ b/src/node/types.h
@@ -67,6 +67,10 @@ struct BlockCreateOptions {
* coinbase_max_additional_weight and coinbase_output_max_additional_sigops.
*/
CScript coinbase_output_script{CScript() << OP_TRUE};
+ /**
+ * Whether to include an OP_0 as a dummy extraNonce in the template's coinbase
+ */
+ bool include_dummy_extranonce{false};
};
struct BlockWaitOptions {
diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp
index 9c357f87..d0efa84f 100644
--- a/src/rpc/mining.cpp
+++ b/src/rpc/mining.cpp
@@ -165,7 +165,7 @@ static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const
{
UniValue blockHashes(UniValue::VARR);
while (nGenerate > 0 && !chainman.m_interrupt) {
- std::unique_ptr<BlockTemplate> block_template(miner.createNewBlock({ .coinbase_output_script = coinbase_output_script }));
+ std::unique_ptr<BlockTemplate> block_template(miner.createNewBlock({ .coinbase_output_script = coinbase_output_script, .include_dummy_extranonce = true }));
CHECK_NONFATAL(block_template);
std::shared_ptr<const CBlock> block_out;
@@ -376,7 +376,7 @@ static RPCHelpMan generateblock()
{
LOCK(chainman.GetMutex());
{
- std::unique_ptr<BlockTemplate> block_template{miner.createNewBlock({.use_mempool = false, .coinbase_output_script = coinbase_output_script})};
+ std::unique_ptr<BlockTemplate> block_template{miner.createNewBlock({.use_mempool = false, .coinbase_output_script = coinbase_output_script, .include_dummy_extranonce = true})};
CHECK_NONFATAL(block_template);
block = block_template->getBlock();
@@ -871,7 +871,7 @@ static RPCHelpMan getblocktemplate()
time_start = GetTime();
// Create new block
- block_template = miner.createNewBlock();
+ block_template = miner.createNewBlock({.include_dummy_extranonce = true});
CHECK_NONFATAL(block_template);
diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp
index e970ae9c..d7d10dfb 100644
--- a/src/test/blockfilter_index_tests.cpp
+++ b/src/test/blockfilter_index_tests.cpp
@@ -69,6 +69,7 @@ CBlock BuildChainTestingSetup::CreateBlock(const CBlockIndex* prev,
{
BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey;
+ options.include_dummy_extranonce = true;
std::unique_ptr<CBlockTemplate> pblocktemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock();
CBlock& block = pblocktemplate->block;
block.hashPrevBlock = prev->GetBlockHash();
diff --git a/src/test/fuzz/package_eval.cpp b/src/test/fuzz/package_eval.cpp
index 1cc2caa3..93cd8ad6 100644
--- a/src/test/fuzz/package_eval.cpp
+++ b/src/test/fuzz/package_eval.cpp
@@ -46,6 +46,7 @@ void initialize_tx_pool()
BlockAssembler::Options options;
options.coinbase_output_script = P2WSH_EMPTY;
+ options.include_dummy_extranonce = true;
for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
COutPoint prevout{MineBlock(g_setup->m_node, options)};
diff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp
index ef8cb686..7a24c1de 100644
--- a/src/test/fuzz/process_message.cpp
+++ b/src/test/fuzz/process_message.cpp
@@ -41,7 +41,9 @@ void ResetChainman(TestingSetup& setup)
setup.m_make_chainman();
setup.LoadVerifyActivateChainstate();
for (int i = 0; i < 2 * COINBASE_MATURITY; i++) {
- MineBlock(setup.m_node, {});
+ node::BlockAssembler::Options options;
+ options.include_dummy_extranonce = true;
+ MineBlock(setup.m_node, options);
}
setup.m_node.validation_signals->SyncWithValidationInterfaceQueue();
}
diff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp
index f36f528b..28bee67d 100644
--- a/src/test/fuzz/process_messages.cpp
+++ b/src/test/fuzz/process_messages.cpp
@@ -35,8 +35,10 @@ void ResetChainman(TestingSetup& setup)
setup.m_node.chainman.reset();
setup.m_make_chainman();
setup.LoadVerifyActivateChainstate();
+ node::BlockAssembler::Options options;
+ options.include_dummy_extranonce = true;
for (int i = 0; i < 2 * COINBASE_MATURITY; i++) {
- MineBlock(setup.m_node, {});
+ MineBlock(setup.m_node, options);
}
setup.m_node.validation_signals->SyncWithValidationInterfaceQueue();
}
diff --git a/src/test/fuzz/tx_pool.cpp b/src/test/fuzz/tx_pool.cpp
index f70dd710..bb155527 100644
--- a/src/test/fuzz/tx_pool.cpp
+++ b/src/test/fuzz/tx_pool.cpp
@@ -48,6 +48,7 @@ void initialize_tx_pool()
BlockAssembler::Options options;
options.coinbase_output_script = P2WSH_OP_TRUE;
+ options.include_dummy_extranonce = true;
for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
COutPoint prevout{MineBlock(g_setup->m_node, options)};
@@ -97,6 +98,7 @@ void Finish(FuzzedDataProvider& fuzzed_data_provider, MockedTxPool& tx_pool, Cha
BlockAssembler::Options options;
options.nBlockMaxWeight = fuzzed_data_provider.ConsumeIntegralInRange(0U, MAX_BLOCK_WEIGHT);
options.blockMinFeeRate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
+ options.include_dummy_extranonce = true;
auto assembler = BlockAssembler{chainstate, &tx_pool, options};
auto block_template = assembler.CreateNewBlock();
Assert(block_template->block.vtx.size() >= 1);
diff --git a/src/test/fuzz/utxo_total_supply.cpp b/src/test/fuzz/utxo_total_supply.cpp
index d27ca347..cac9d4ce 100644
--- a/src/test/fuzz/utxo_total_supply.cpp
+++ b/src/test/fuzz/utxo_total_supply.cpp
@@ -45,6 +45,7 @@ FUZZ_TARGET(utxo_total_supply)
};
BlockAssembler::Options options;
options.coinbase_output_script = CScript() << OP_FALSE;
+ options.include_dummy_extranonce = true;
const auto PrepareNextBlock = [&]() {
// Use OP_FALSE to avoid BIP30 check from hitting early
auto block = PrepareBlock(node, options);
diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp
index a1f5a6d0..3b4beebe 100644
--- a/src/test/miner_tests.cpp
+++ b/src/test/miner_tests.cpp
@@ -116,6 +116,7 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
auto mining{MakeMining()};
BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey;
+ options.include_dummy_extranonce = true;
LOCK(tx_mempool.cs);
BOOST_CHECK(tx_mempool.size() == 0);
@@ -334,6 +335,7 @@ void MinerTestingSetup::TestBasicMining(const CScript& scriptPubKey, const std::
BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey;
+ options.include_dummy_extranonce = true;
{
CTxMemPool& tx_mempool{MakeMempool()};
@@ -660,6 +662,7 @@ void MinerTestingSetup::TestPrioritisedMining(const CScript& scriptPubKey, const
BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey;
+ options.include_dummy_extranonce = true;
CTxMemPool& tx_mempool{MakeMempool()};
LOCK(tx_mempool.cs);
@@ -749,6 +752,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
CScript scriptPubKey = CScript() << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"_hex << OP_CHECKSIG;
BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey;
+ options.include_dummy_extranonce = true;
// Create and check a simple template
std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options);
diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp
index 64b13fa3..e391e8b9 100644
--- a/src/test/peerman_tests.cpp
+++ b/src/test/peerman_tests.cpp
@@ -19,8 +19,10 @@ static constexpr int64_t NODE_NETWORK_LIMITED_ALLOW_CONN_BLOCKS = 144;
static void mineBlock(const node::NodeContext& node, std::chrono::seconds block_time)
{
auto curr_time = GetTime<std::chrono::seconds>();
+ node::BlockAssembler::Options options;
+ options.include_dummy_extranonce = true;
SetMockTime(block_time); // update time so the block is created with it
- CBlock block = node::BlockAssembler{node.chainman->ActiveChainstate(), nullptr, {}}.CreateNewBlock()->block;
+ CBlock block = node::BlockAssembler{node.chainman->ActiveChainstate(), nullptr, options}.CreateNewBlock()->block;
while (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) ++block.nNonce;
block.fChecked = true; // little speedup
SetMockTime(curr_time); // process block at current time
diff --git a/src/test/testnet4_miner_tests.cpp b/src/test/testnet4_miner_tests.cpp
index 5b3582ac..33e028a5 100644
--- a/src/test/testnet4_miner_tests.cpp
+++ b/src/test/testnet4_miner_tests.cpp
@@ -35,6 +35,7 @@ BOOST_AUTO_TEST_CASE(MiningInterface)
BOOST_REQUIRE(mining);
BlockAssembler::Options options;
+ options.include_dummy_extranonce = true;
std::unique_ptr<BlockTemplate> block_template;
// Set node time a few minutes past the testnet4 genesis block
diff --git a/src/test/util/mining.cpp b/src/test/util/mining.cpp
index 05f1be77..c4823bce 100644
--- a/src/test/util/mining.cpp
+++ b/src/test/util/mining.cpp
@@ -29,6 +29,7 @@ COutPoint generatetoaddress(const NodeContext& node, const std::string& address)
assert(IsValidDestination(dest));
BlockAssembler::Options assembler_options;
assembler_options.coinbase_output_script = GetScriptForDestination(dest);
+ assembler_options.include_dummy_extranonce = true;
return MineBlock(node, assembler_options);
}
@@ -139,6 +140,7 @@ std::shared_ptr<CBlock> PrepareBlock(const NodeContext& node, const CScript& coi
{
BlockAssembler::Options assembler_options;
assembler_options.coinbase_output_script = coinbase_scriptPubKey;
+ assembler_options.include_dummy_extranonce = true;
ApplyArgsManOptions(*node.args, assembler_options);
return PrepareBlock(node, assembler_options);
}
diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp
index 4e886639..e479168a 100644
--- a/src/test/util/setup_common.cpp
+++ b/src/test/util/setup_common.cpp
@@ -404,6 +404,7 @@ CBlock TestChain100Setup::CreateBlock(
{
BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey;
+ options.include_dummy_extranonce = true;
CBlock block = BlockAssembler{chainstate, nullptr, options}.CreateNewBlock()->block;
Assert(block.vtx.size() == 1);
diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp
index dfa66bb8..f91b30a3 100644
--- a/src/test/validation_block_tests.cpp
+++ b/src/test/validation_block_tests.cpp
@@ -68,6 +68,7 @@ std::shared_ptr<CBlock> MinerTestingSetup::Block(const uint256& prev_hash)
BlockAssembler::Options options;
options.coinbase_output_script = CScript{} << i++ << OP_TRUE;
+ options.include_dummy_extranonce = true;
auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock();
auto pblock = std::make_shared<CBlock>(ptemplate->block);
pblock->hashPrevBlock = prev_hash;
@@ -336,6 +337,7 @@ BOOST_AUTO_TEST_CASE(witness_commitment_index)
pubKey << 1 << OP_TRUE;
BlockAssembler::Options options;
options.coinbase_output_script = pubKey;
+ options.include_dummy_extranonce = true;
auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock();
CBlock pblock = ptemplate->block;
diff --git a/test/functional/interface_ipc.py b/test/functional/interface_ipc.py
index 47589cbc..6d007e88 100755
--- a/test/functional/interface_ipc.py
+++ b/test/functional/interface_ipc.py
@@ -20,10 +20,14 @@ from test_framework.messages import (
ser_uint256,
COIN,
)
+from test_framework.script import (
+ CScript,
+ CScriptNum,
+)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
- assert_not_equal
+ assert_not_equal,
)
from test_framework.wallet import MiniWallet
from typing import Optional
@@ -194,6 +198,12 @@ class IPCInterfaceTest(BitcoinTestFramework):
coinbase_tx.vin = [CTxIn()]
coinbase_tx.vin[0].prevout = NULL_OUTPOINT
coinbase_tx.vin[0].nSequence = coinbase_res.sequence
+
+ # Verify there's no dummy extraNonce in the coinbase scriptSig
+ current_block_height = self.nodes[0].getchaintips()[0]["height"]
+ expected_scriptsig = CScript([CScriptNum(current_block_height + 1)])
+ assert_equal(coinbase_res.scriptSigPrefix.hex(), expected_scriptsig.hex())
+
# Typically a mining pool appends its name and an extraNonce
coinbase_tx.vin[0].scriptSig = coinbase_res.scriptSigPrefix
Why this scored 32/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.