Skip to content

Commit 62557c9

Browse files
committed
Merge #33819: mining: getCoinbase() returns struct instead of raw tx
48f57bb mining: add new getCoinbaseTx() returning a struct (Sjors Provoost) d59b4cd mining: rename getCoinbaseTx() to ..RawTx() (Sjors Provoost) Pull request description: The first commit renames `getCoinbaseTx()` to `getCoinbaseRawTx()` to reflect that it returns a serialised transaction. This does not impact IPC clients, because they do not use the function name. The second commit then introduces a replacement `getCoinbase()` that provides a struct with everything clients need to construct a coinbase. This avoids clients having to parse and manipulate our dummy transaction. Deprecate but don't remove `getCoinbaseRawTx()`, `getCoinbaseCommitment()` and `getWitnessCommitmentIndex()`. After this change we can drop these deprecated methods, which in turn would allow us to clear the dummy transaction from the `getBlock()` result. But that is left for a followup to keep this PR focussed. See Sjors#106 for an approach. Expand the `interface_ipc.py` functional test to document its usage. Can be tested using: - stratum-mining/sv2-tp#59 ACKs for top commit: ryanofsky: Code review ACK 48f57bb. Just rebased and addressed comments and dropped coinbase tx "template" suffix, which is a nice change ismaelsadeeq: code review ACK 48f57bb vasild: ACK 48f57bb Tree-SHA512: c4f1d752777fb3086a1a0b7b8b06e4205dbe2f3adb41f218855ad1dee952adccc263cf82acd3bf9300cc83c2c64cebd2b27f66a69beee32d325b9a85e3643b0d
2 parents 3c8d389 + 48f57bb commit 62557c9

9 files changed

Lines changed: 239 additions & 10 deletions

File tree

doc/release-notes-33819.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Mining IPC
2+
----------
3+
4+
- The `getCoinbaseTx()` method is renamed to `getCoinbaseRawTx()` and deprecated.
5+
IPC clients do not use the function name, so they're not affected. (#33819)
6+
- Adds `getCoinbaseTx()` which clients should use instead of `getCoinbaseRawTx()`. It
7+
contains all fields required to construct a coinbase transaction, and omits the
8+
dummy output which Bitcoin Core uses internally. (#33819)

src/interfaces/mining.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,29 @@ class BlockTemplate
4242
// Sigop cost per transaction, not including coinbase transaction.
4343
virtual std::vector<int64_t> getTxSigops() = 0;
4444

45-
virtual CTransactionRef getCoinbaseTx() = 0;
45+
/**
46+
* Return serialized dummy coinbase transaction.
47+
*
48+
* @note deprecated: use getCoinbaseTx()
49+
*/
50+
virtual CTransactionRef getCoinbaseRawTx() = 0;
51+
52+
/** Return fields needed to construct a coinbase transaction */
53+
virtual node::CoinbaseTx getCoinbaseTx() = 0;
54+
55+
/**
56+
* Return scriptPubKey with SegWit OP_RETURN.
57+
*
58+
* @note deprecated: use getCoinbaseTx()
59+
*/
4660
virtual std::vector<unsigned char> getCoinbaseCommitment() = 0;
61+
62+
/**
63+
* Return which output in the dummy coinbase contains the SegWit OP_RETURN.
64+
*
65+
* @note deprecated. Scan outputs from getCoinbaseTx() outputs field for the
66+
* SegWit marker.
67+
*/
4768
virtual int getWitnessCommitmentIndex() = 0;
4869

4970
/**

src/ipc/capnp/mining.capnp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
2727
getBlock @2 (context: Proxy.Context) -> (result: Data);
2828
getTxFees @3 (context: Proxy.Context) -> (result: List(Int64));
2929
getTxSigops @4 (context: Proxy.Context) -> (result: List(Int64));
30-
getCoinbaseTx @5 (context: Proxy.Context) -> (result: Data);
30+
getCoinbaseRawTx @5 (context: Proxy.Context) -> (result: Data);
31+
getCoinbaseTx @12 (context: Proxy.Context) -> (result: CoinbaseTx);
3132
getCoinbaseCommitment @6 (context: Proxy.Context) -> (result: Data);
3233
getWitnessCommitmentIndex @7 (context: Proxy.Context) -> (result: Int32);
3334
getCoinbaseMerklePath @8 (context: Proxy.Context) -> (result: List(Data));
@@ -51,3 +52,13 @@ struct BlockCheckOptions $Proxy.wrap("node::BlockCheckOptions") {
5152
checkMerkleRoot @0 :Bool $Proxy.name("check_merkle_root");
5253
checkPow @1 :Bool $Proxy.name("check_pow");
5354
}
55+
56+
struct CoinbaseTx $Proxy.wrap("node::CoinbaseTx") {
57+
version @0 :UInt32 $Proxy.name("version");
58+
sequence @1 :UInt32 $Proxy.name("sequence");
59+
scriptSigPrefix @2 :Data $Proxy.name("script_sig_prefix");
60+
witness @3 :Data $Proxy.name("witness");
61+
blockRewardRemaining @4 :Int64 $Proxy.name("block_reward_remaining");
62+
requiredOutputs @5 :List(Data) $Proxy.name("required_outputs");
63+
lockTime @6 :UInt32 $Proxy.name("lock_time");
64+
}

src/node/interfaces.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ using interfaces::WalletLoader;
8484
using kernel::ChainstateRole;
8585
using node::BlockAssembler;
8686
using node::BlockWaitOptions;
87+
using node::CoinbaseTx;
8788
using util::Join;
8889

8990
namespace node {
@@ -888,11 +889,16 @@ class BlockTemplateImpl : public BlockTemplate
888889
return m_block_template->vTxSigOpsCost;
889890
}
890891

891-
CTransactionRef getCoinbaseTx() override
892+
CTransactionRef getCoinbaseRawTx() override
892893
{
893894
return m_block_template->block.vtx[0];
894895
}
895896

897+
CoinbaseTx getCoinbaseTx() override
898+
{
899+
return m_block_template->m_coinbase_tx;
900+
}
901+
896902
std::vector<unsigned char> getCoinbaseCommitment() override
897903
{
898904
return m_block_template->vchCoinbaseCommitment;

src/node/miner.cpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,51 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock()
158158

159159
// Create coinbase transaction.
160160
CMutableTransaction coinbaseTx;
161+
162+
// Construct coinbase transaction struct in parallel
163+
CoinbaseTx& coinbase_tx{pblocktemplate->m_coinbase_tx};
164+
coinbase_tx.version = coinbaseTx.version;
165+
161166
coinbaseTx.vin.resize(1);
162167
coinbaseTx.vin[0].prevout.SetNull();
163168
coinbaseTx.vin[0].nSequence = CTxIn::MAX_SEQUENCE_NONFINAL; // Make sure timelock is enforced.
169+
coinbase_tx.sequence = coinbaseTx.vin[0].nSequence;
170+
171+
// Add an output that spends the full coinbase reward.
164172
coinbaseTx.vout.resize(1);
165173
coinbaseTx.vout[0].scriptPubKey = m_options.coinbase_output_script;
166-
coinbaseTx.vout[0].nValue = nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus());
174+
// Block subsidy + fees
175+
const CAmount block_reward{nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus())};
176+
coinbaseTx.vout[0].nValue = block_reward;
177+
coinbase_tx.block_reward_remaining = block_reward;
178+
179+
// Start the coinbase scriptSig with the block height as required by BIP34.
180+
// The trailing OP_0 (historically an extranonce) is optional padding and
181+
// could be removed without a consensus change. Mining clients are expected
182+
// to append extra data to this prefix, so increasing its length would reduce
183+
// the space they can use and may break existing clients.
167184
coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0;
185+
coinbase_tx.script_sig_prefix = coinbaseTx.vin[0].scriptSig;
168186
Assert(nHeight > 0);
169187
coinbaseTx.nLockTime = static_cast<uint32_t>(nHeight - 1);
188+
coinbase_tx.lock_time = coinbaseTx.nLockTime;
189+
170190
pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx));
171191
pblocktemplate->vchCoinbaseCommitment = m_chainstate.m_chainman.GenerateCoinbaseCommitment(*pblock, pindexPrev);
172192

193+
const CTransactionRef& final_coinbase{pblock->vtx[0]};
194+
if (final_coinbase->HasWitness()) {
195+
const auto& witness_stack{final_coinbase->vin[0].scriptWitness.stack};
196+
// Consensus requires the coinbase witness stack to have exactly one
197+
// element of 32 bytes.
198+
Assert(witness_stack.size() == 1 && witness_stack[0].size() == 32);
199+
coinbase_tx.witness = uint256(witness_stack[0]);
200+
}
201+
if (const int witness_index = GetWitnessCommitmentIndex(*pblock); witness_index != NO_WITNESS_COMMITMENT) {
202+
Assert(witness_index >= 0 && static_cast<size_t>(witness_index) < final_coinbase->vout.size());
203+
coinbase_tx.required_outputs.push_back(final_coinbase->vout[witness_index]);
204+
}
205+
173206
LogInfo("CreateNewBlock(): block weight: %u txs: %u fees: %ld sigops %d\n", GetBlockWeight(*pblock), nBlockTx, nFees, nBlockSigOpsCost);
174207

175208
// Fill in header
@@ -440,4 +473,5 @@ std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifi
440473
// avoid deadlocks.
441474
return GetTip(chainman);
442475
}
476+
443477
} // namespace node

src/node/miner.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ struct CBlockTemplate
5050
/* A vector of package fee rates, ordered by the sequence in which
5151
* packages are selected for inclusion in the block template.*/
5252
std::vector<FeePerVSize> m_package_feerates;
53+
/*
54+
* Template containing all coinbase transaction fields that are set by our
55+
* miner code.
56+
*/
57+
CoinbaseTx m_coinbase_tx;
5358
};
5459

5560
/** Generate a new block, without valid proof-of-work */
@@ -157,6 +162,7 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman);
157162
/* Waits for the connected tip to change until timeout has elapsed. During node initialization, this will wait until the tip is connected (regardless of `timeout`).
158163
* Returns the current tip, or nullopt if the node is shutting down. */
159164
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout);
165+
160166
} // namespace node
161167

162168
#endif // BITCOIN_NODE_MINER_H

src/node/types.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
#include <consensus/amount.h>
1717
#include <cstddef>
1818
#include <cstdint>
19+
#include <optional>
1920
#include <policy/policy.h>
21+
#include <primitives/transaction.h>
2022
#include <script/script.h>
2123
#include <uint256.h>
2224
#include <util/time.h>
25+
#include <vector>
2326

2427
namespace node {
2528
enum class TransactionError {
@@ -99,6 +102,53 @@ struct BlockCheckOptions {
99102
bool check_pow{true};
100103
};
101104

105+
/**
106+
* Template containing all coinbase transaction fields that are set by our
107+
* miner code. Clients are expected to add their own outputs and typically
108+
* also expand the scriptSig.
109+
*/
110+
struct CoinbaseTx {
111+
/* nVersion */
112+
uint32_t version;
113+
/* nSequence for the only coinbase transaction input */
114+
uint32_t sequence;
115+
/**
116+
* Prefix which needs to be placed at the beginning of the scriptSig.
117+
* Clients may append extra data to this as long as the overall scriptSig
118+
* size is 100 bytes or less, to avoid the block being rejected with
119+
* "bad-cb-length" error.
120+
*
121+
* Currently with BIP 34, the prefix is guaranteed to be less than 8 bytes,
122+
* but future soft forks could require longer prefixes.
123+
*/
124+
CScript script_sig_prefix;
125+
/**
126+
* The first (and only) witness stack element of the coinbase input.
127+
*
128+
* Omitted for block templates without witness data.
129+
*
130+
* This is currently the BIP 141 witness reserved value, and can be chosen
131+
* arbitrarily by the node, but future soft forks may constrain it.
132+
*/
133+
std::optional<uint256> witness;
134+
/**
135+
* Block subsidy plus fees, minus any non-zero required_outputs.
136+
*
137+
* Currently there are no non-zero required_outputs, so block_reward_remaining
138+
* is the entire block reward. See also required_outputs.
139+
*/
140+
CAmount block_reward_remaining;
141+
/*
142+
* To be included as the last outputs in the coinbase transaction.
143+
* Currently this is only the witness commitment OP_RETURN, but future
144+
* softforks or a custom mining patch could add more.
145+
*
146+
* The dummy output that spends the full reward is excluded.
147+
*/
148+
std::vector<CTxOut> required_outputs;
149+
uint32_t lock_time;
150+
};
151+
102152
/**
103153
* How to broadcast a local transaction.
104154
* Used to influence `BroadcastTransaction()` and its callers.

test/functional/interface_ipc.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,38 @@
66
import asyncio
77
import inspect
88
from contextlib import asynccontextmanager, AsyncExitStack
9+
from dataclasses import dataclass
910
from io import BytesIO
1011
from pathlib import Path
1112
import shutil
12-
from test_framework.messages import (CBlock, CTransaction, ser_uint256, COIN)
13+
from test_framework.blocktools import NULL_OUTPOINT
14+
from test_framework.messages import (
15+
CBlock,
16+
CTransaction,
17+
CTxIn,
18+
CTxOut,
19+
CTxInWitness,
20+
ser_uint256,
21+
COIN,
22+
)
1323
from test_framework.test_framework import BitcoinTestFramework
1424
from test_framework.util import (
1525
assert_equal,
1626
assert_not_equal
1727
)
1828
from test_framework.wallet import MiniWallet
29+
from typing import Optional
30+
31+
# Stores the result of getCoinbaseTx()
32+
@dataclass
33+
class CoinbaseTxData:
34+
version: int
35+
sequence: int
36+
scriptSigPrefix: bytes
37+
witness: Optional[bytes]
38+
blockRewardRemaining: int
39+
requiredOutputs: list[bytes]
40+
lockTime: int
1941

2042
# Test may be skipped and not have capnp installed
2143
try:
@@ -123,11 +145,32 @@ async def parse_and_deserialize_block(self, block_template, ctx):
123145
return block
124146

125147
async def parse_and_deserialize_coinbase_tx(self, block_template, ctx):
126-
coinbase_data = BytesIO((await block_template.getCoinbaseTx(ctx)).result)
148+
assert block_template is not None
149+
coinbase_data = BytesIO((await block_template.getCoinbaseRawTx(ctx)).result)
127150
tx = CTransaction()
128151
tx.deserialize(coinbase_data)
129152
return tx
130153

154+
async def parse_and_deserialize_coinbase(self, block_template, ctx) -> CoinbaseTxData:
155+
assert block_template is not None
156+
# Note: the template_capnp struct will be garbage-collected when this
157+
# method returns, so it is important to copy any Data fields from it
158+
# which need to be accessed later using the bytes() cast. Starting with
159+
# pycapnp v2.2.0, Data fields have type `memoryview` and are ephemeral.
160+
template_capnp = (await block_template.getCoinbaseTx(ctx)).result
161+
witness: Optional[bytes] = None
162+
if template_capnp._has("witness"):
163+
witness = bytes(template_capnp.witness)
164+
return CoinbaseTxData(
165+
version=int(template_capnp.version),
166+
sequence=int(template_capnp.sequence),
167+
scriptSigPrefix=bytes(template_capnp.scriptSigPrefix),
168+
witness=witness,
169+
blockRewardRemaining=int(template_capnp.blockRewardRemaining),
170+
requiredOutputs=[bytes(output) for output in template_capnp.requiredOutputs],
171+
lockTime=int(template_capnp.lockTime),
172+
)
173+
131174
def run_echo_test(self):
132175
self.log.info("Running echo test")
133176
async def async_routine():
@@ -142,6 +185,54 @@ async def async_routine():
142185
echo.destroy(ctx)
143186
asyncio.run(capnp.run(async_routine()))
144187

188+
async def build_coinbase_test(self, template, ctx, miniwallet):
189+
self.log.debug("Build coinbase transaction using getCoinbaseTx()")
190+
assert template is not None
191+
coinbase_res = await self.parse_and_deserialize_coinbase(template, ctx)
192+
coinbase_tx = CTransaction()
193+
coinbase_tx.version = coinbase_res.version
194+
coinbase_tx.vin = [CTxIn()]
195+
coinbase_tx.vin[0].prevout = NULL_OUTPOINT
196+
coinbase_tx.vin[0].nSequence = coinbase_res.sequence
197+
# Typically a mining pool appends its name and an extraNonce
198+
coinbase_tx.vin[0].scriptSig = coinbase_res.scriptSigPrefix
199+
200+
# We currently always provide a coinbase witness, even for empty
201+
# blocks, but this may change, so always check:
202+
has_witness = coinbase_res.witness is not None
203+
if has_witness:
204+
coinbase_tx.wit.vtxinwit = [CTxInWitness()]
205+
coinbase_tx.wit.vtxinwit[0].scriptWitness.stack = [coinbase_res.witness]
206+
207+
# First output is our payout
208+
coinbase_tx.vout = [CTxOut()]
209+
coinbase_tx.vout[0].scriptPubKey = miniwallet.get_output_script()
210+
coinbase_tx.vout[0].nValue = coinbase_res.blockRewardRemaining
211+
# Add SegWit OP_RETURN. This is currently always present even for
212+
# empty blocks, but this may change.
213+
found_witness_op_return = False
214+
# Compare SegWit OP_RETURN to getCoinbaseCommitment()
215+
coinbase_commitment = (await template.getCoinbaseCommitment(ctx)).result
216+
for output_data in coinbase_res.requiredOutputs:
217+
output = CTxOut()
218+
output.deserialize(BytesIO(output_data))
219+
coinbase_tx.vout.append(output)
220+
if output.scriptPubKey == coinbase_commitment:
221+
found_witness_op_return = True
222+
223+
assert_equal(has_witness, found_witness_op_return)
224+
225+
coinbase_tx.nLockTime = coinbase_res.lockTime
226+
227+
# Compare to dummy coinbase provided by the deprecated getCoinbaseTx()
228+
coinbase_legacy = await self.parse_and_deserialize_coinbase_tx(template, ctx)
229+
assert_equal(coinbase_legacy.vout[0].nValue, coinbase_res.blockRewardRemaining)
230+
# Swap dummy output for our own
231+
coinbase_legacy.vout[0].scriptPubKey = coinbase_tx.vout[0].scriptPubKey
232+
assert_equal(coinbase_tx.serialize().hex(), coinbase_legacy.serialize().hex())
233+
234+
return coinbase_tx
235+
145236
def run_mining_test(self):
146237
self.log.info("Running mining test")
147238
block_hash_size = 32
@@ -191,7 +282,7 @@ async def async_routine():
191282
assert_equal(len(txfees.result), 0)
192283
txsigops = await template.getTxSigops(ctx)
193284
assert_equal(len(txsigops.result), 0)
194-
coinbase_data = BytesIO((await template.getCoinbaseTx(ctx)).result)
285+
coinbase_data = BytesIO((await template.getCoinbaseRawTx(ctx)).result)
195286
coinbase = CTransaction()
196287
coinbase.deserialize(coinbase_data)
197288
assert_equal(coinbase.vin[0].prevout.hash, 0)
@@ -248,9 +339,9 @@ async def wait_for_block():
248339
check_opts = self.capnp_modules['mining'].BlockCheckOptions()
249340
async with destroying((await mining.createNewBlock(opts)).result, ctx) as template:
250341
block = await self.parse_and_deserialize_block(template, ctx)
251-
coinbase = await self.parse_and_deserialize_coinbase_tx(template, ctx)
252342
balance = miniwallet.get_balance()
253-
coinbase.vout[0].scriptPubKey = miniwallet.get_output_script()
343+
coinbase = await self.build_coinbase_test(template, ctx, miniwallet)
344+
# Reduce payout for balance comparison simplicity
254345
coinbase.vout[0].nValue = COIN
255346
block.vtx[0] = coinbase
256347
block.hashMerkleRoot = block.calc_merkle_root()

0 commit comments

Comments
 (0)