Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/interfaces/mining.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <consensus/amount.h>
#include <interfaces/types.h>
#include <sv2/block_options.h>
#include <sv2/coinbase_template.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
#include <uint256.h>
Expand Down Expand Up @@ -38,8 +39,29 @@ class BlockTemplate
// Sigop cost per transaction, not including coinbase transaction.
virtual std::vector<int64_t> getTxSigops() = 0;

virtual CTransactionRef getCoinbaseTx() = 0;
/** Return fields needed to construct a coinbase transaction */
virtual node::CoinbaseTxTemplate getCoinbaseTx() = 0;

/**
* Return serialized dummy coinbase transaction.
*
* @note deprecated: use getCoinbaseTx()
*/
virtual CTransactionRef getCoinbaseRawTx() = 0;

/**
* Return scriptPubKey with SegWit OP_RETURN.
*
* @note deprecated: use getCoinbaseRawTx()
*/
virtual std::vector<unsigned char> getCoinbaseCommitment() = 0;

/**
* Return which output in the dummy coinbase contains the SegWit OP_RETURN.
*
* @note deprecated. Scan outputs from getCoinbaseRawTx() outputs field for the
* SegWit marker.
*/
virtual int getWitnessCommitmentIndex() = 0;

/**
Expand Down
1 change: 1 addition & 0 deletions src/ipc/capnp/mining-types.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <ipc/capnp/common-types.h>
#include <ipc/capnp/mining.capnp.proxy.h>
#include <sv2/block_options.h>
#include <sv2/coinbase_template.h>

namespace mp {
// Custom serializations
Expand Down
13 changes: 12 additions & 1 deletion src/ipc/capnp/mining.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
getBlock @2 (context: Proxy.Context) -> (result: Data);
getTxFees @3 (context: Proxy.Context) -> (result: List(Int64));
getTxSigops @4 (context: Proxy.Context) -> (result: List(Int64));
getCoinbaseTx @5 (context: Proxy.Context) -> (result: Data);
getCoinbaseTx @12 (context: Proxy.Context) -> (result: CoinbaseTxTemplate);
getCoinbaseRawTx @5 (context: Proxy.Context) -> (result: Data);
getCoinbaseCommitment @6 (context: Proxy.Context) -> (result: Data);
getWitnessCommitmentIndex @7 (context: Proxy.Context) -> (result: Int32);
getCoinbaseMerklePath @8 (context: Proxy.Context) -> (result: List(Data));
Expand All @@ -51,3 +52,13 @@ struct BlockCheckOptions $Proxy.wrap("node::BlockCheckOptions") {
checkMerkleRoot @0 :Bool $Proxy.name("check_merkle_root");
checkPow @1 :Bool $Proxy.name("check_pow");
}

struct CoinbaseTxTemplate $Proxy.wrap("node::CoinbaseTxTemplate") {
version @0 :UInt32 $Proxy.name("version");
sequence @1 :UInt32 $Proxy.name("sequence");
scriptSigPrefix @2 :Data $Proxy.name("script_sig_prefix");
witness @3 :Data $Proxy.name("witness");
blockRewardRemaining @4 :Int64 $Proxy.name("block_reward_remaining");
requiredOutputs @5 :List(Data) $Proxy.name("required_outputs");
lockTime @6 :UInt32 $Proxy.name("lock_time");
}
63 changes: 63 additions & 0 deletions src/sv2/coinbase_template.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_SV2_COINBASE_TEMPLATE_H
#define BITCOIN_SV2_COINBASE_TEMPLATE_H

#include <consensus/amount.h>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <script/script.h>
#include <primitives/transaction.h>
#include <util/time.h>
#include <vector>

namespace node {

struct CoinbaseTxTemplate {
/* nVersion */
uint32_t version;
/* nSequence for the only coinbase transaction input */
uint32_t sequence;
/**
* Prefix which needs to be placed at the beginning of the scriptSig.
* Clients may append extra data to this as long as the overall scriptSig
* size is 100 bytes or less, to avoid the block being rejected with
* "bad-cb-length" error.
*
* Currently with BIP 34, the prefix is guaranteed to be less than 8 bytes,
* but future soft forks could require longer prefixes.
*/
CScript script_sig_prefix;
/**
* The first (and only) witness stack element of the coinbase input.
*
* Omitted for block templates without witness data.
*
* This is currently the BIP 141 witness reserved value, and can be chosen
* arbitrarily by the node, but future soft forks may constrain it.
*/
std::optional<uint256> witness;
/**
* Block subsidy plus fees, minus any non-zero required_outputs.
*
* Currently there are no non-zero required_outputs, so block_reward_remaining
* is the entire block reward. See also required_outputs.
*/
CAmount block_reward_remaining;
/*
* To be included as the last outputs in the coinbase transaction.
* Currently this is only the witness commitment OP_RETURN, but future
* softforks or a custom mining patch could add more.
*
* The dummy output that spends the full reward is excluded.
*/
std::vector<CTxOut> required_outputs;
uint32_t lock_time;
};

} // namespace node

#endif // BITCOIN_SV2_COINBASE_TEMPLATE_H
67 changes: 56 additions & 11 deletions src/sv2/messages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,82 @@
#include <consensus/validation.h> // NO_WITNESS_COMMITMENT
#include <script/script.h>

node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template)
node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const node::CoinbaseTxTemplate coinbase, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template)
: m_template_id{template_id}, m_future_template{future_template}
{
m_version = header.nVersion;

m_coinbase_tx_version = coinbase_tx->CURRENT_VERSION;
m_coinbase_prefix = coinbase_tx->vin[0].scriptSig;
m_coinbase_tx_input_sequence = coinbase_tx->vin[0].nSequence;
m_coinbase_tx_version = coinbase.version;
m_coinbase_prefix = coinbase.script_sig_prefix;
m_coinbase_tx_input_sequence = coinbase.sequence;

// The coinbase nValue already contains the nFee + the Block Subsidy when built using CreateBlock().
m_coinbase_tx_value_remaining = static_cast<uint64_t>(coinbase_tx->vout[0].nValue);
m_coinbase_tx_value_remaining = static_cast<uint64_t>(coinbase.block_reward_remaining);

// Extract only OP_RETURN coinbase outputs (witness commitment, merge mining, etc.)
// Bitcoin Core adds a dummy output with the full reward that we must exclude,
// otherwise the pool would create an invalid block trying to spend that amount again.
m_coinbase_tx_outputs.clear();
for (const auto& output : coinbase_tx->vout) {
if (!output.scriptPubKey.empty() && output.scriptPubKey[0] == OP_RETURN) {
m_coinbase_tx_outputs.push_back(output);
}
for (const auto& output : coinbase.required_outputs) {
m_coinbase_tx_outputs.push_back(output);
Comment on lines +25 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the new constructor the loop just pushes all required_outputs:
This trusts that getCoinbase() already filtered to OP_RETURN outputs. The fallback path does
the filtering in ExtractCoinbaseTemplate(). This is correct assuming Bitcoin Core's
getCoinbase() returns pre-filtered outputs - just worth double-checking in Bitcoin Core.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trusts that getCoinbase() already filtered to OP_RETURN outputs.

Indeed. I think it's better that Bitcoin Core handles this filtering. This means we no longer have to assume only OP_RETURN outputs are possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can say the PR is ready for merging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I plan to do that after bitcoin/bitcoin#33819 lands.

}
m_coinbase_tx_outputs_count = m_coinbase_tx_outputs.size();
m_coinbase_tx_outputs_count = coinbase.required_outputs.size();

m_coinbase_tx_locktime = coinbase_tx->nLockTime;
m_coinbase_tx_locktime = coinbase.lock_time;

for (const auto& hash : coinbase_merkle_path) {
m_merkle_path.push_back(hash);
}

}

node::CoinbaseTxTemplate ExtractCoinbaseTxTemplate(const CTransactionRef coinbase_tx)
{
node::CoinbaseTxTemplate coinbase{};

coinbase.version = coinbase_tx->version;
Assert(coinbase_tx->vin.size() == 1);
coinbase.script_sig_prefix = coinbase_tx->vin[0].scriptSig;
// The CoinbaseTxTemplate interface guarantees a size limit. Raising it (e.g.
// if a future softfork needs to commit more than BIP34) is a
// (potentially silent) breaking change for clients.
if (!Assume(coinbase.script_sig_prefix.size() <= 8)) {
LogWarning("Unexpected %d byte scriptSig prefix size.",
coinbase.script_sig_prefix.size());
}

if (coinbase_tx->HasWitness()) {
const auto& witness_stack{coinbase_tx->vin[0].scriptWitness.stack};
// Consensus requires the coinbase witness stack to have exactly one
// element of 32 bytes.
Assert(witness_stack.size() == 1 && witness_stack[0].size() == 32);
coinbase.witness = uint256(witness_stack[0]);
}

coinbase.sequence = coinbase_tx->vin[0].nSequence;

// Extract only OP_RETURN coinbase outputs (witness commitment, merge
// mining, etc). BlockAssembler::CreateNewBlock adds a dummy output with
// the full reward that we must exclude.
for (const auto& output : coinbase_tx->vout) {
if (!output.scriptPubKey.empty() && output.scriptPubKey[0] == OP_RETURN) {
coinbase.required_outputs.push_back(output);
} else {
// The (single) dummy coinbase output produced by CreateBlock() has
// an nValue set to nFee + the Block Subsidy.
Assume(coinbase.block_reward_remaining == 0);
coinbase.block_reward_remaining = output.nValue;
}
}

coinbase.lock_time = coinbase_tx->nLockTime;

return coinbase;
}

node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template) :
node::Sv2NewTemplateMsg(header, ExtractCoinbaseTxTemplate(coinbase_tx), coinbase_merkle_path, template_id, future_template) {};

node::Sv2SetNewPrevHashMsg::Sv2SetNewPrevHashMsg(const CBlockHeader& header, uint64_t template_id) : m_template_id{template_id}
{
m_prev_hash = header.hashPrevBlock;
Expand Down
14 changes: 13 additions & 1 deletion src/sv2/messages.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <primitives/transaction.h>
#include <script/script.h>
#include <span.h>
#include <sv2/coinbase_template.h>
#include <streams.h>
#include <string>
#include <vector>
Expand Down Expand Up @@ -304,7 +305,7 @@ struct Sv2NewTemplateMsg
std::vector<CTxOut> m_coinbase_tx_outputs;

/**
* The locktime field in the coinbase transaction.
* The lock_time field in the coinbase transaction.
*/
uint32_t m_coinbase_tx_locktime;

Expand All @@ -315,6 +316,7 @@ struct Sv2NewTemplateMsg

Sv2NewTemplateMsg() = default;
explicit Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template);
explicit Sv2NewTemplateMsg(const CBlockHeader& header, const node::CoinbaseTxTemplate coinbase, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template);

template <typename Stream>
void Serialize(Stream& s) const
Expand Down Expand Up @@ -738,4 +740,14 @@ class Sv2NetMsg

}

/*
* Extract relevant fields from the dummy coinbase transaction in the template.
*
* Extracts only OP_RETURN coinbase outputs, i.e. the witness commitment. If
* BlockAssembler::CreateNewBlock() is patched to add more OP_RETURN outputs
* e.g. for merge mining, those will be included. The dummy output that spends
* the full reward is excluded.
*/
node::CoinbaseTxTemplate ExtractCoinbaseTxTemplate(const CTransactionRef coinbase_tx);

#endif // BITCOIN_SV2_MESSAGES_H
25 changes: 20 additions & 5 deletions src/sv2/template_provider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -575,11 +575,26 @@ bool Sv2TemplateProvider::SendWork(Sv2Client& client, uint64_t template_id, Bloc
return false;
}

node::Sv2NewTemplateMsg new_template{header,
block_template.getCoinbaseTx(),
block_template.getCoinbaseMerklePath(),
template_id,
future_template};
node::Sv2NewTemplateMsg new_template;
try {
node::CoinbaseTxTemplate coinbase{block_template.getCoinbaseTx()};

new_template = node::Sv2NewTemplateMsg{header,
coinbase,
block_template.getCoinbaseMerklePath(),
template_id,
future_template};
} catch (const std::exception&) {
// Fall back to getCoinbaseRawTx()
CTransactionRef coinbase_tx{block_template.getCoinbaseRawTx()};

new_template = node::Sv2NewTemplateMsg{header,
coinbase_tx,
block_template.getCoinbaseMerklePath(),
template_id,
future_template};

}

LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x71 NewTemplate id=%lu future=%d to client id=%zu\n", template_id, future_template, client.m_id);
{
Expand Down
4 changes: 3 additions & 1 deletion src/test/sv2_mock_mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <sync.h>
#include <cassert>
#include <logging.h>
#include <sv2/messages.h>

namespace {
static inline uint256 HashFromHeight(uint64_t h)
Expand Down Expand Up @@ -56,7 +57,8 @@ CBlockHeader MockBlockTemplate::getBlockHeader() { return block.GetBlockHeader()
CBlock MockBlockTemplate::getBlock() { return block; }
std::vector<CAmount> MockBlockTemplate::getTxFees() { return {}; }
std::vector<int64_t> MockBlockTemplate::getTxSigops() { return {}; }
CTransactionRef MockBlockTemplate::getCoinbaseTx() { return block.vtx[0]; }
node::CoinbaseTxTemplate MockBlockTemplate::getCoinbaseTx() { return ExtractCoinbaseTxTemplate(block.vtx[0]); }
CTransactionRef MockBlockTemplate::getCoinbaseRawTx() { return block.vtx[0]; }
std::vector<unsigned char> MockBlockTemplate::getCoinbaseCommitment() { return {}; }
int MockBlockTemplate::getWitnessCommitmentIndex() { return -1; }
std::vector<uint256> MockBlockTemplate::getCoinbaseMerklePath() { return {}; }
Expand Down
3 changes: 2 additions & 1 deletion src/test/sv2_mock_mining.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ class MockBlockTemplate : public interfaces::BlockTemplate {
CBlock getBlock() override;
std::vector<CAmount> getTxFees() override;
std::vector<int64_t> getTxSigops() override;
CTransactionRef getCoinbaseTx() override;
node::CoinbaseTxTemplate getCoinbaseTx() override;
CTransactionRef getCoinbaseRawTx() override;
std::vector<unsigned char> getCoinbaseCommitment() override;
int getWitnessCommitmentIndex() override;
std::vector<uint256> getCoinbaseMerklePath() override;
Expand Down
Loading