Skip to content

Commit d812921

Browse files
committed
Merge remote-tracking branch 'xyephy/support-multiple-coinbase-outputs'
2 parents e1f77aa + 9e68692 commit d812921

File tree

6 files changed

+105
-15
lines changed

6 files changed

+105
-15
lines changed

src/sv2/messages.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
#include <primitives/block.h>
55
#include <primitives/transaction.h>
66
#include <consensus/validation.h> // NO_WITNESS_COMMITMENT
7+
#include <script/script.h>
78

8-
node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, int witness_commitment_index, uint64_t template_id, bool future_template)
9+
node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template)
910
: m_template_id{template_id}, m_future_template{future_template}
1011
{
1112
m_version = header.nVersion;
@@ -17,11 +18,16 @@ node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTr
1718
// The coinbase nValue already contains the nFee + the Block Subsidy when built using CreateBlock().
1819
m_coinbase_tx_value_remaining = static_cast<uint64_t>(coinbase_tx->vout[0].nValue);
1920

20-
m_coinbase_tx_outputs_count = 0;
21-
if (witness_commitment_index != NO_WITNESS_COMMITMENT) {
22-
m_coinbase_tx_outputs_count = 1;
23-
m_coinbase_tx_outputs = {coinbase_tx->vout[witness_commitment_index]};
21+
// Extract only OP_RETURN coinbase outputs (witness commitment, merge mining, etc.)
22+
// Bitcoin Core adds a dummy output with the full reward that we must exclude,
23+
// otherwise the pool would create an invalid block trying to spend that amount again.
24+
m_coinbase_tx_outputs.clear();
25+
for (const auto& output : coinbase_tx->vout) {
26+
if (!output.scriptPubKey.empty() && output.scriptPubKey[0] == OP_RETURN) {
27+
m_coinbase_tx_outputs.push_back(output);
28+
}
2429
}
30+
m_coinbase_tx_outputs_count = m_coinbase_tx_outputs.size();
2531

2632
m_coinbase_tx_locktime = coinbase_tx->nLockTime;
2733

src/sv2/messages.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ struct Sv2NewTemplateMsg
314314
std::vector<uint256> m_merkle_path;
315315

316316
Sv2NewTemplateMsg() = default;
317-
explicit Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, int witness_commitment_index, uint64_t template_id, bool future_template);
317+
explicit Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector<uint256> coinbase_merkle_path, uint64_t template_id, bool future_template);
318318

319319
template <typename Stream>
320320
void Serialize(Stream& s) const
@@ -332,8 +332,12 @@ struct Sv2NewTemplateMsg
332332
// as [B0_64K](https://github.com/stratum-mining/sv2-spec/blob/main/03-Protocol-Overview.md#31-data-types-mapping)
333333
if (m_coinbase_tx_outputs_count > 0) {
334334
std::vector<uint8_t> outputs_bytes;
335-
// TODO: support more than 1 output
336-
VectorWriter{outputs_bytes, 0, m_coinbase_tx_outputs.at(0)};
335+
for (const auto& output : m_coinbase_tx_outputs) {
336+
VectorWriter{outputs_bytes, outputs_bytes.size(), output};
337+
}
338+
339+
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Serialized %zu coinbase output(s), total size: %zu bytes\n",
340+
m_coinbase_tx_outputs.size(), outputs_bytes.size());
337341

338342
s << static_cast<uint16_t>(outputs_bytes.size());
339343
s.write(MakeByteSpan(outputs_bytes));

src/sv2/template_provider.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,6 @@ bool Sv2TemplateProvider::SendWork(Sv2Client& client, uint64_t template_id, Bloc
558558
node::Sv2NewTemplateMsg new_template{header,
559559
block_template.getCoinbaseTx(),
560560
block_template.getCoinbaseMerklePath(),
561-
block_template.getWitnessCommitmentIndex(),
562561
template_id,
563562
future_template};
564563

src/test/sv2_messages_tests.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#include <boost/test/unit_test.hpp>
33
#include <sv2/messages.h>
44
#include <util/strencodings.h>
5+
#include <primitives/transaction.h>
6+
#include <primitives/block.h>
7+
#include <script/script.h>
58

69
BOOST_AUTO_TEST_SUITE(sv2_messages_tests)
710

@@ -153,6 +156,74 @@ BOOST_AUTO_TEST_CASE(Sv2NewTemplate_test)
153156
BOOST_CHECK_EQUAL(HexStr(ss), expected);
154157
}
155158

159+
BOOST_AUTO_TEST_CASE(Sv2NewTemplate_MultipleOutputs_test)
160+
{
161+
// NewTemplate with realistic coinbase: 1 dummy anyone-can-spend output + 3 OP_RETURN outputs
162+
// Tests that only OP_RETURN outputs are serialized (dummy output filtered out)
163+
//
164+
// U64 0100000000000000 template_id
165+
// BOOL 00 future_template
166+
// U32 00000030 version
167+
// U32 02000000 coinbase tx version
168+
// B0_255 04 coinbase_prefix len
169+
// 03012100 coinbase prefix
170+
// U32 ffffffff coinbase tx input sequence
171+
// U64 0040075af0750700 coinbase tx value remaining
172+
// U32 03000000 coinbase tx outputs count (3 OP_RETURN outputs, dummy filtered)
173+
// B0_64K 2100 coinbase_tx_outputs (33 bytes total)
174+
// 6400000000000000 output 1: 100 sats
175+
// 026a51 output 1: script (OP_RETURN OP_1)
176+
// c800000000000000 output 2: 200 sats
177+
// 026a52 output 2: script (OP_RETURN OP_2)
178+
// 2c01000000000000 output 3: 300 sats
179+
// 026a53 output 3: script (OP_RETURN OP_3)
180+
// U32 dbc80d00 coinbase lock time (height 903,387)
181+
// SEQ0_255[U256] 01 merkle path length
182+
// 1a6240823de4c8d6aaf826851bdf2b0e8d5acf7c31e8578cff4c394b5a32bd4e - merkle path
183+
std::string expected{"01000000000000000000000030020000000403012100ffffffff0040075af07507000300000021006400000000000000026a51c800000000000000026a522c01000000000000026a53dbc80d00011a6240823de4c8d6aaf826851bdf2b0e8d5acf7c31e8578cff4c394b5a32bd4e"};
184+
185+
// Create realistic coinbase transaction with dummy anyone-can-spend output
186+
CMutableTransaction coinbase_tx;
187+
coinbase_tx.version = 2;
188+
coinbase_tx.vin.resize(1);
189+
coinbase_tx.vin[0].prevout.SetNull();
190+
std::vector<uint8_t> coinbase_prefix_bytes{0x03, 0x01, 0x21, 0x00};
191+
coinbase_tx.vin[0].scriptSig = CScript(coinbase_prefix_bytes.begin(), coinbase_prefix_bytes.end());
192+
coinbase_tx.vin[0].nSequence = 4294967295;
193+
194+
coinbase_tx.vout.resize(4);
195+
// Output 0: Dummy anyone-can-spend output with full reward (will be filtered out by sv2-tp)
196+
coinbase_tx.vout[0].nValue = MAX_MONEY;
197+
coinbase_tx.vout[0].scriptPubKey = CScript() << OP_TRUE;
198+
199+
// Outputs 1-3: OP_RETURN outputs (will be included in NewTemplate message)
200+
coinbase_tx.vout[1] = CTxOut(100, CScript() << OP_RETURN << 1);
201+
coinbase_tx.vout[2] = CTxOut(200, CScript() << OP_RETURN << 2);
202+
coinbase_tx.vout[3] = CTxOut(300, CScript() << OP_RETURN << 3);
203+
204+
coinbase_tx.nLockTime = 903387;
205+
206+
CBlockHeader header;
207+
header.nVersion = 805306368;
208+
header.hashPrevBlock.SetNull();
209+
header.nTime = 0;
210+
header.nBits = 0;
211+
header.nNonce = 0;
212+
213+
std::vector<uint256> merkle_path;
214+
CMutableTransaction mtx_tx;
215+
CTransaction tx{mtx_tx};
216+
merkle_path.push_back(tx.GetHash().ToUint256());
217+
218+
// Use constructor which filters outputs to only OP_RETURN
219+
node::Sv2NewTemplateMsg new_template{header, MakeTransactionRef(coinbase_tx), merkle_path, 1, false};
220+
221+
DataStream ss{};
222+
ss << new_template;
223+
224+
BOOST_CHECK_EQUAL(HexStr(ss), expected);
225+
}
226+
156227
BOOST_AUTO_TEST_CASE(Sv2NetHeader_NewTemplate_test)
157228
{
158229
// 0000 - extension type

src/test/sv2_mock_mining.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,24 @@ MockBlockTemplate::MockBlockTemplate(std::shared_ptr<MockState> st, uint256 prev
2121
{
2222
// Simple internal consistency assertion: constructor sequence should not exceed state counter.
2323
assert(m_sequence <= state->chain.template_seq);
24-
// Build a dummy coinbase
24+
// Build a realistic coinbase with multiple outputs
2525
CMutableTransaction cb;
2626
cb.vin.resize(1);
2727
cb.vin[0].prevout.SetNull();
2828
cb.vin[0].scriptSig = CScript() << OP_0;
29-
cb.vout.resize(1);
29+
30+
cb.vout.resize(3);
31+
// Output 0: Dummy anyone-can-spend output with full reward (will be filtered out by sv2-tp)
3032
cb.vout[0].nValue = 50 * COIN;
31-
cb.vout[0].scriptPubKey = CScript() << OP_RETURN;
33+
cb.vout[0].scriptPubKey = CScript() << OP_TRUE;
34+
35+
// Output 1: Fake witness commitment (will be included)
36+
cb.vout[1].nValue = 0;
37+
cb.vout[1].scriptPubKey = CScript() << OP_RETURN << std::vector<unsigned char>(32, 0xaa);
38+
39+
// Output 2: Fake merge mining commitment (will be included)
40+
cb.vout[2].nValue = 0;
41+
cb.vout[2].scriptPubKey = CScript() << OP_RETURN << std::vector<unsigned char>{'M', 'M'};
3242

3343
block = CBlock{};
3444
block.vtx.clear();

src/test/sv2_template_provider_tests.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ BOOST_AUTO_TEST_CASE(client_tests)
6262
1 + // future_template
6363
4 + // version
6464
4 + // coinbase_tx_version
65-
2 + // coinbase_prefix (CompactSize(1) + 1-byte OP_0)
65+
2 + // coinbase_prefix (CompactSize(1) + 1-byte OP_0)
6666
4 + // coinbase_tx_input_sequence
6767
8 + // coinbase_tx_value_remaining
68-
4 + // coinbase_tx_outputs_count (0)
69-
2 + // B0_64K length for outputs array (0)
68+
4 + // coinbase_tx_outputs_count (2 - mock creates 3, only 2 OP_RETURN outputs pass filter)
69+
2 + 56 + // B0_64K: length prefix (2 bytes) + 2 outputs (witness commitment 43 bytes + merge mining 13 bytes)
7070
4 + // coinbase_tx_locktime
7171
1; // merkle_path count (CompactSize(0))
7272

0 commit comments

Comments
 (0)