[miner] omit dummy extraNonce via IPC

Previously the coinbase transaction generated by our miner code was
not used downstream, because the getblocktemplate RPC excludes it.

Since the Mining IPC interface was introduced in #30200 we do expose
this dummy coinbase transaction. In Stratum v2 several parts of it
are communicated downstream, including the scriptSig.

This commit removes the dummy extraNonce from the coinbase scriptSig
in block templates requested via IPC. This limits the scriptSig
to what is essential for consensus (BIP34) and removes the need for
external mining software to remove the dummy, or even ignore
the scriptSig we provide and generate it some other way. This
could cause problems if a future soft fork requires additional
data to be committed here.

A test is added to verify the new IPC behavior.

It achieves this by introducing an include_dummy_extranonce
option which defaults to false with all test code updated to
set it to true. Because this option is not exposed via IPC,
callers will no longer see it.

The caller needs to ensure that for blocks 1 through 16
they pad the scriptSig in order to avoid bad-cb-length.

Co-authored-by: Anthony Towns <aj@erisian.com.au>
This commit is contained in:
Sjors ProvoostandAnthony Towns committed 2026-01-27 14:45:35 +01:00
1 parent bf3b5d6d06
commit d511adb664
17 files changed
+55 -12

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ static void AssembleBlock(benchmark::Bench& bench)
witness.stack.push_back(WITNESS_STACK_ELEM_OP_TRUE); witness.stack.push_back(WITNESS_STACK_ELEM_OP_TRUE);
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = P2WSH_OP_TRUE; options.coinbase_output_script = P2WSH_OP_TRUE;
options.include_dummy_extranonce = true;
// Collect some loose transactions that spend the coinbases of our mined blocks // Collect some loose transactions that spend the coinbases of our mined blocks
constexpr size_t NUM_BLOCKS{200}; constexpr size_t NUM_BLOCKS{200};
+12 -5
View File
@@ -177,11 +177,17 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
coinbase_tx.block_reward_remaining = block_reward; coinbase_tx.block_reward_remaining = block_reward;
// Start the coinbase scriptSig with the block height as required by BIP34. // Start the coinbase scriptSig with the block height as required by BIP34.
// The trailing OP_0 (historically an extranonce) is optional padding and // Mining clients are expected to append extra data to this prefix, so
// could be removed without a consensus change. Mining clients are expected // increasing its length would reduce the space they can use and may break
// to append extra data to this prefix, so increasing its length would reduce // existing clients.
// the space they can use and may break existing clients. coinbaseTx.vin[0].scriptSig = CScript() << nHeight;
coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0; 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; coinbase_tx.script_sig_prefix = coinbaseTx.vin[0].scriptSig;
Assert(nHeight > 0); Assert(nHeight > 0);
coinbaseTx.nLockTime = static_cast<uint32_t>(nHeight - 1); coinbaseTx.nLockTime = static_cast<uint32_t>(nHeight - 1);
@@ -212,6 +218,7 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
pblock->nNonce = 0; pblock->nNonce = 0;
if (m_options.test_block_validity) { 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()) { 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())); throw std::runtime_error(strprintf("TestBlockValidity failed: %s", state.ToString()));
} }
+4
View File
@@ -67,6 +67,10 @@ struct BlockCreateOptions {
* coinbase_max_additional_weight and coinbase_output_max_additional_sigops. * coinbase_max_additional_weight and coinbase_output_max_additional_sigops.
*/ */
CScript coinbase_output_script{CScript() << OP_TRUE}; 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 { struct BlockWaitOptions {
+3 -3
View File
@@ -165,7 +165,7 @@ static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const
{ {
UniValue blockHashes(UniValue::VARR); UniValue blockHashes(UniValue::VARR);
while (nGenerate > 0 && !chainman.m_interrupt) { 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); CHECK_NONFATAL(block_template);
std::shared_ptr<const CBlock> block_out; std::shared_ptr<const CBlock> block_out;
@@ -376,7 +376,7 @@ static RPCHelpMan generateblock()
{ {
LOCK(chainman.GetMutex()); 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); CHECK_NONFATAL(block_template);
block = block_template->getBlock(); block = block_template->getBlock();
@@ -871,7 +871,7 @@ static RPCHelpMan getblocktemplate()
time_start = GetTime(); time_start = GetTime();
// Create new block // Create new block
block_template = miner.createNewBlock(); block_template = miner.createNewBlock({.include_dummy_extranonce = true});
CHECK_NONFATAL(block_template); CHECK_NONFATAL(block_template);
+1
View File
@@ -69,6 +69,7 @@ CBlock BuildChainTestingSetup::CreateBlock(const CBlockIndex* prev,
{ {
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey; 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(); std::unique_ptr<CBlockTemplate> pblocktemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock();
CBlock& block = pblocktemplate->block; CBlock& block = pblocktemplate->block;
block.hashPrevBlock = prev->GetBlockHash(); block.hashPrevBlock = prev->GetBlockHash();
+1
View File
@@ -46,6 +46,7 @@ void initialize_tx_pool()
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = P2WSH_EMPTY; options.coinbase_output_script = P2WSH_EMPTY;
options.include_dummy_extranonce = true;
for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) { for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
COutPoint prevout{MineBlock(g_setup->m_node, options)}; COutPoint prevout{MineBlock(g_setup->m_node, options)};
+3 -1
View File
@@ -41,7 +41,9 @@ void ResetChainman(TestingSetup& setup)
setup.m_make_chainman(); setup.m_make_chainman();
setup.LoadVerifyActivateChainstate(); setup.LoadVerifyActivateChainstate();
for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { 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(); setup.m_node.validation_signals->SyncWithValidationInterfaceQueue();
} }
+3 -1
View File
@@ -35,8 +35,10 @@ void ResetChainman(TestingSetup& setup)
setup.m_node.chainman.reset(); setup.m_node.chainman.reset();
setup.m_make_chainman(); setup.m_make_chainman();
setup.LoadVerifyActivateChainstate(); setup.LoadVerifyActivateChainstate();
node::BlockAssembler::Options options;
options.include_dummy_extranonce = true;
for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { for (int i = 0; i < 2 * COINBASE_MATURITY; i++) {
MineBlock(setup.m_node, {}); MineBlock(setup.m_node, options);
} }
setup.m_node.validation_signals->SyncWithValidationInterfaceQueue(); setup.m_node.validation_signals->SyncWithValidationInterfaceQueue();
} }
+2
View File
@@ -48,6 +48,7 @@ void initialize_tx_pool()
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = P2WSH_OP_TRUE; options.coinbase_output_script = P2WSH_OP_TRUE;
options.include_dummy_extranonce = true;
for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) { for (int i = 0; i < 2 * COINBASE_MATURITY; ++i) {
COutPoint prevout{MineBlock(g_setup->m_node, options)}; 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; BlockAssembler::Options options;
options.nBlockMaxWeight = fuzzed_data_provider.ConsumeIntegralInRange(0U, MAX_BLOCK_WEIGHT); options.nBlockMaxWeight = fuzzed_data_provider.ConsumeIntegralInRange(0U, MAX_BLOCK_WEIGHT);
options.blockMinFeeRate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)}; options.blockMinFeeRate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
options.include_dummy_extranonce = true;
auto assembler = BlockAssembler{chainstate, &tx_pool, options}; auto assembler = BlockAssembler{chainstate, &tx_pool, options};
auto block_template = assembler.CreateNewBlock(); auto block_template = assembler.CreateNewBlock();
Assert(block_template->block.vtx.size() >= 1); Assert(block_template->block.vtx.size() >= 1);
+1
View File
@@ -45,6 +45,7 @@ FUZZ_TARGET(utxo_total_supply)
}; };
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = CScript() << OP_FALSE; options.coinbase_output_script = CScript() << OP_FALSE;
options.include_dummy_extranonce = true;
const auto PrepareNextBlock = [&]() { const auto PrepareNextBlock = [&]() {
// Use OP_FALSE to avoid BIP30 check from hitting early // Use OP_FALSE to avoid BIP30 check from hitting early
auto block = PrepareBlock(node, options); auto block = PrepareBlock(node, options);
+4
View File
@@ -116,6 +116,7 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
auto mining{MakeMining()}; auto mining{MakeMining()};
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey; options.coinbase_output_script = scriptPubKey;
options.include_dummy_extranonce = true;
LOCK(tx_mempool.cs); LOCK(tx_mempool.cs);
BOOST_CHECK(tx_mempool.size() == 0); BOOST_CHECK(tx_mempool.size() == 0);
@@ -334,6 +335,7 @@ void MinerTestingSetup::TestBasicMining(const CScript& scriptPubKey, const std::
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey; options.coinbase_output_script = scriptPubKey;
options.include_dummy_extranonce = true;
{ {
CTxMemPool& tx_mempool{MakeMempool()}; CTxMemPool& tx_mempool{MakeMempool()};
@@ -660,6 +662,7 @@ void MinerTestingSetup::TestPrioritisedMining(const CScript& scriptPubKey, const
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey; options.coinbase_output_script = scriptPubKey;
options.include_dummy_extranonce = true;
CTxMemPool& tx_mempool{MakeMempool()}; CTxMemPool& tx_mempool{MakeMempool()};
LOCK(tx_mempool.cs); LOCK(tx_mempool.cs);
@@ -749,6 +752,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity)
CScript scriptPubKey = CScript() << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"_hex << OP_CHECKSIG; CScript scriptPubKey = CScript() << "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"_hex << OP_CHECKSIG;
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey; options.coinbase_output_script = scriptPubKey;
options.include_dummy_extranonce = true;
// Create and check a simple template // Create and check a simple template
std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options); std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options);
+3 -1
View File
@@ -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) static void mineBlock(const node::NodeContext& node, std::chrono::seconds block_time)
{ {
auto curr_time = GetTime<std::chrono::seconds>(); 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 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; while (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) ++block.nNonce;
block.fChecked = true; // little speedup block.fChecked = true; // little speedup
SetMockTime(curr_time); // process block at current time SetMockTime(curr_time); // process block at current time
+1
View File
@@ -35,6 +35,7 @@ BOOST_AUTO_TEST_CASE(MiningInterface)
BOOST_REQUIRE(mining); BOOST_REQUIRE(mining);
BlockAssembler::Options options; BlockAssembler::Options options;
options.include_dummy_extranonce = true;
std::unique_ptr<BlockTemplate> block_template; std::unique_ptr<BlockTemplate> block_template;
// Set node time a few minutes past the testnet4 genesis block // Set node time a few minutes past the testnet4 genesis block
+2
View File
@@ -29,6 +29,7 @@ COutPoint generatetoaddress(const NodeContext& node, const std::string& address)
assert(IsValidDestination(dest)); assert(IsValidDestination(dest));
BlockAssembler::Options assembler_options; BlockAssembler::Options assembler_options;
assembler_options.coinbase_output_script = GetScriptForDestination(dest); assembler_options.coinbase_output_script = GetScriptForDestination(dest);
assembler_options.include_dummy_extranonce = true;
return MineBlock(node, assembler_options); return MineBlock(node, assembler_options);
} }
@@ -139,6 +140,7 @@ std::shared_ptr<CBlock> PrepareBlock(const NodeContext& node, const CScript& coi
{ {
BlockAssembler::Options assembler_options; BlockAssembler::Options assembler_options;
assembler_options.coinbase_output_script = coinbase_scriptPubKey; assembler_options.coinbase_output_script = coinbase_scriptPubKey;
assembler_options.include_dummy_extranonce = true;
ApplyArgsManOptions(*node.args, assembler_options); ApplyArgsManOptions(*node.args, assembler_options);
return PrepareBlock(node, assembler_options); return PrepareBlock(node, assembler_options);
} }
+1
View File
@@ -404,6 +404,7 @@ CBlock TestChain100Setup::CreateBlock(
{ {
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = scriptPubKey; options.coinbase_output_script = scriptPubKey;
options.include_dummy_extranonce = true;
CBlock block = BlockAssembler{chainstate, nullptr, options}.CreateNewBlock()->block; CBlock block = BlockAssembler{chainstate, nullptr, options}.CreateNewBlock()->block;
Assert(block.vtx.size() == 1); Assert(block.vtx.size() == 1);
+2
View File
@@ -68,6 +68,7 @@ std::shared_ptr<CBlock> MinerTestingSetup::Block(const uint256& prev_hash)
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = CScript{} << i++ << OP_TRUE; 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 ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock();
auto pblock = std::make_shared<CBlock>(ptemplate->block); auto pblock = std::make_shared<CBlock>(ptemplate->block);
pblock->hashPrevBlock = prev_hash; pblock->hashPrevBlock = prev_hash;
@@ -336,6 +337,7 @@ BOOST_AUTO_TEST_CASE(witness_commitment_index)
pubKey << 1 << OP_TRUE; pubKey << 1 << OP_TRUE;
BlockAssembler::Options options; BlockAssembler::Options options;
options.coinbase_output_script = pubKey; options.coinbase_output_script = pubKey;
options.include_dummy_extranonce = true;
auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock(); auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock();
CBlock pblock = ptemplate->block; CBlock pblock = ptemplate->block;
+11 -1
View File
@@ -20,10 +20,14 @@ from test_framework.messages import (
ser_uint256, ser_uint256,
COIN, COIN,
) )
from test_framework.script import (
CScript,
CScriptNum,
)
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
assert_not_equal assert_not_equal,
) )
from test_framework.wallet import MiniWallet from test_framework.wallet import MiniWallet
from typing import Optional from typing import Optional
@@ -194,6 +198,12 @@ class IPCInterfaceTest(BitcoinTestFramework):
coinbase_tx.vin = [CTxIn()] coinbase_tx.vin = [CTxIn()]
coinbase_tx.vin[0].prevout = NULL_OUTPOINT coinbase_tx.vin[0].prevout = NULL_OUTPOINT
coinbase_tx.vin[0].nSequence = coinbase_res.sequence 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 # Typically a mining pool appends its name and an extraNonce
coinbase_tx.vin[0].scriptSig = coinbase_res.scriptSigPrefix coinbase_tx.vin[0].scriptSig = coinbase_res.scriptSigPrefix