Skip to content

Commit b91face

Browse files
committed
dgb(#82): recompute won-block header merkle_root over the actual tx vector
The tx-bearing reconstruct path (reconstruct_won_block_from_template) sources its body from the captured GBT template, but assemble_won_block derived the header merkle_root by walking the share merkle_link (check_merkle_link). That link only spans the txs it was built over, so a coinbase-only link with a funded tx in the body emitted a header root for the coinbase-only set -> daemon rejects bad-txnmrklroot (the #82 funded multi-tx soak FAIL on both arms). Recompute the root over [gentx_hash] ++ each other-tx txid via a canonical BuildMerkleRoot (build_block_merkle_root), set before serialize so the nonce-grind seam grinds the final header. Coinbase-only is unchanged (root == gentx_hash). +1 regression KAT; corrected Test 4 which had enshrined the buggy coinbase-only root with 2 other_txs. 8/8 green.
1 parent 9276c04 commit b91face

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

src/impl/dgb/coin/block_assembly.hpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "block.hpp"
4646
#include "../share_check.hpp" // dgb::check_merkle_link (SSOT merkle-branch walk)
4747
#include "../share_types.hpp" // dgb::MerkleLink
48+
#include "mempool.hpp" // dgb::coin::compute_txid (block-body txid SSOT)
4849

4950
namespace dgb
5051
{
@@ -72,6 +73,28 @@ reconstruct_block_header(const SmallBlockHeaderType& small_header,
7273
return header;
7374
}
7475

76+
// Build the canonical Bitcoin block merkle root over an ordered txid list
77+
// ([gentx_hash] ++ other-tx txids): duplicate-the-last on an odd count, then
78+
// sha256d each adjacent pair up to the single root. This is the BLOCK-BODY
79+
// SSOT for the header merkle_root -- it MUST be recomputed over the txs the
80+
// block actually carries, NOT walked up the share's merkle_link, or a
81+
// tx-bearing reconstruct emits a header root for the coinbase-only set and the
82+
// daemon rejects the block with bad-txnmrklroot (#82 funded-soak regression).
83+
inline uint256 build_block_merkle_root(std::vector<uint256> txids)
84+
{
85+
if (txids.empty()) return uint256();
86+
while (txids.size() > 1) {
87+
if (txids.size() % 2 == 1)
88+
txids.push_back(txids.back());
89+
std::vector<uint256> next;
90+
next.reserve(txids.size() / 2);
91+
for (size_t i = 0; i + 1 < txids.size(); i += 2)
92+
next.push_back(Hash(txids[i], txids[i + 1]));
93+
txids = std::move(next);
94+
}
95+
return txids[0];
96+
}
97+
7598
// Assemble the full serialized parent block for a won share.
7699
// small_header : share.m_min_header (version|prev|time|bits|nonce)
77100
// gentx : the reconstructed coinbase transaction (block tx 0)
@@ -105,6 +128,26 @@ assemble_won_block(const SmallBlockHeaderType& small_header,
105128
for (const auto& tx : other_txs)
106129
block.m_txs.push_back(tx);
107130

131+
// Recompute the header merkle_root over the ACTUAL block tx vector
132+
// ([gentx_hash] ++ each other-tx txid), overriding the merkle_link walk
133+
// reconstruct_block_header did. The share's merkle_link only spans the txs
134+
// it was built over; when the reconstruct sources its body from the captured
135+
// GBT template (reconstruct_won_block_from_template) that link can reflect a
136+
// different (coinbase-only) set, so trusting it emits a header root that
137+
// disagrees with the body -> daemon bad-txnmrklroot. Deriving the root from
138+
// the body keeps header and body consistent by construction (coinbase-only:
139+
// build_block_merkle_root([gentx_hash]) == gentx_hash, identical to the
140+
// empty-branch walk -- no regression). Set BEFORE serialize so the
141+
// nonce-grind seam (regrind_block.hpp) grinds the final, correct header.
142+
{
143+
std::vector<uint256> txids;
144+
txids.reserve(block.m_txs.size());
145+
txids.push_back(gentx_hash);
146+
for (const auto& tx : other_txs)
147+
txids.push_back(compute_txid(tx));
148+
block.m_merkle_root = build_block_merkle_root(std::move(txids));
149+
}
150+
108151
PackStream packed = pack<BlockType>(block);
109152
auto sp = packed.get_span();
110153
std::vector<unsigned char> bytes(

src/impl/dgb/test/block_assembly_test.cpp

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,16 @@ TEST(DgbBlockAssembly, AssembleRoundTripsCoinbaseFirst)
180180
EXPECT_EQ(blk.m_timestamp, sh.m_timestamp);
181181
EXPECT_EQ(blk.m_bits, sh.m_bits);
182182
EXPECT_EQ(blk.m_nonce, sh.m_nonce);
183-
EXPECT_EQ(blk.m_merkle_root, gtx_hash);
183+
// header merkle_root is recomputed over the ACTUAL block tx vector
184+
// ([gentx_hash] ++ other-tx txids), NOT the (empty here) share merkle_link:
185+
// with two other_txs it must differ from the coinbase-only root and equal
186+
// build_block_merkle_root over the real leaves (#82 bad-txnmrklroot fix).
187+
{
188+
std::vector<uint256> leaves = { gtx_hash,
189+
dgb::coin::compute_txid(other[0]), dgb::coin::compute_txid(other[1]) };
190+
EXPECT_EQ(blk.m_merkle_root, dgb::coin::build_block_merkle_root(leaves));
191+
EXPECT_NE(blk.m_merkle_root, gtx_hash);
192+
}
184193

185194
// txs = [gentx] ++ other_txs : coinbase at index 0, total = 1 + 2.
186195
ASSERT_EQ(blk.m_txs.size(), 3u);
@@ -295,4 +304,32 @@ TEST(DgbBlockAssembly, LegacyGentxEmitsLegacyBlock)
295304
EXPECT_EQ(hex, hex2);
296305
}
297306

307+
// --- Test 8: tx-bearing block header root == body root, != coinbase-only ------
308+
// Regression lock for the #82 funded multi-tx gate: a reconstructed block that
309+
// carries 1+ non-coinbase txs MUST publish a header merkle_root computed over
310+
// [gentx]++other_txs, so a peer/daemon validating the body accepts it. The
311+
// pre-fix path walked the share merkle_link (empty == coinbase-only => gentx_hash)
312+
// and the daemon rejected bad-txnmrklroot.
313+
TEST(DgbBlockAssembly, TxBearingRootMatchesBodyNotMerkleLink)
314+
{
315+
auto sh = make_small_header();
316+
auto gentx = make_gentx();
317+
auto gtx_hash = fixed_gentx_hash();
318+
::dgb::MerkleLink link; // empty link == the coinbase-only branch
319+
std::vector<MutableTransaction> other = { make_tx(42) };
320+
321+
auto [bytes, hex] = assemble_won_block(sh, gentx, gtx_hash, link, other);
322+
PackStream ps(bytes);
323+
BlockType blk;
324+
ps >> blk;
325+
326+
ASSERT_EQ(blk.m_txs.size(), 2u);
327+
const uint256 body_root = dgb::coin::build_block_merkle_root(
328+
{ gtx_hash, dgb::coin::compute_txid(other[0]) });
329+
EXPECT_EQ(blk.m_merkle_root, body_root);
330+
// the empty merkle_link would have yielded gentx_hash -- prove we did NOT.
331+
EXPECT_NE(blk.m_merkle_root, gtx_hash);
332+
EXPECT_NE(body_root, gtx_hash);
333+
}
334+
298335
} // namespace

0 commit comments

Comments
 (0)