Skip to content

Commit 3ca1ca9

Browse files
committed
btc(coin): faithful won-block reassembly framing — assemble_won_block (reconstructor slice 4/7)
Port the FRAMING half of p2pool data.py Share.as_block into btc::coin::assemble_won_block: reconstruct the full block header from the share's stored SmallBlockHeader + gentx txid walked up the (segwit-resolved) merkle link via the sealed btc::check_merkle_link, frame [gentx] ++ other_txs with the coinbase at index 0, and emit {bytes, hex} through the live BlockType TX_WITH_WITNESS conditional codec (the NodeRPC::submit_block wire path). BTC divergence from the DGB slice: BTC commits the BIP141 witness root inside the coinbase at gentx-BUILD time (share_check.hpp:2166/:2296), so slices 1-3 hand this framer an already-committed gentx. It therefore does NOT re-inject a witness commitment — doing so would mutate the coinbase, change its txid, invalidate the share-committed header root, and open a second merkle path the sealed merkle family (#570/#574/#579) forbids. The segwit-vs-legacy link selection stays in the caller (slice 5); this framer takes a resolved MerkleLink. Adds a fail-closed body/header guard: body_merkle_root folds [gentx_hash] ++ the non-witness other-tx txids through the sealed btc::coin::compute_merkle_root and assemble_won_block throws rather than emit a bad-txnmrklroot block. On a template_capture miss this passes for empty-link (coinbase-only) shares and correctly refuses the never-valid non-empty-link coinbase-only case. Slice cut off origin/master. btc-tree-only. btc_share_test 82/82 (10 new BtcBlockAssembly KATs, self-derived merkle branches).
1 parent cb4ba26 commit 3ca1ca9

3 files changed

Lines changed: 592 additions & 1 deletion

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
// ---------------------------------------------------------------------------
4+
// btc::coin::assemble_won_block -- the share->block "as_block" reassembly that
5+
// the BTC won-block reconstructor (#744 dispatch arc, slice 4/7) feeds to the
6+
// dual-path broadcaster (P2P relay + submitblock RPC fallback).
7+
//
8+
// This is the BTC port of the FRAMING half of p2pool data.py
9+
// Share.as_block(tracker, known_txs):
10+
//
11+
// gentx = self.check(tracker, known_txs) # the coinbase tx
12+
// other_txs = [known_txs[h] for h in transaction_hashes]
13+
// return dict(header=self.header, txs=[gentx]+other_txs)
14+
//
15+
// It mirrors the landed DGB slice (src/impl/dgb/coin/block_assembly.hpp) but is
16+
// deliberately NOT DGB-identical in one consensus-relevant respect -- see
17+
// "BTC divergence" below.
18+
//
19+
// Two consensus-relevant facts this captures (shared with DGB):
20+
// 1. p2pool stores only a SmallBlockHeaderType on the share (version|prev|
21+
// time|bits|nonce -- NO merkle_root). The full block header's merkle_root
22+
// is RECONSTRUCTED as check_merkle_link(gentx_hash, share merkle link),
23+
// exactly as verification recomputed the share's PoW root at check() time
24+
// (share_check.hpp:674-692 / :2500). We MUST reproduce it the same way, or
25+
// the block hashes to something other than the share's PoW-valid header and
26+
// the daemon rejects it.
27+
// 2. Block tx order is [gentx] ++ other_txs, gentx (coinbase) always index 0,
28+
// other_txs in the captured template's transactions[] order (the set the
29+
// share committed to, replayed by template_capture.hpp -- slice 3).
30+
//
31+
// ---- BTC divergence from the DGB slice (the reviewer-load-bearing part) ----
32+
// BTC commits the BIP141 witness root INSIDE the coinbase at gentx-BUILD time
33+
// (share_check.hpp:2166 / :2296 -- generate_share_transaction). So slices 1-3
34+
// hand this framer an ALREADY witness-committed gentx. We therefore do NOT
35+
// re-inject a witness commitment here. Re-injecting DGB-style
36+
// add_witness_commitment would MUTATE the coinbase, change its txid, invalidate
37+
// the share-committed header merkle_root, and open a SECOND merkle path -- which
38+
// the sealed BTC merkle family (#570 / #574 / #579) forbids. The DGB lane adds
39+
// its commitment downstream only because it does not commit at gentx-time; BTC
40+
// does, so the correct BTC action is: reuse the committed gentx verbatim. Do NOT
41+
// "fix" this back to the DGB add_witness_commitment shape.
42+
//
43+
// The segwit-vs-legacy merkle-LINK SELECTION (segwit shares use
44+
// segwit_data.txid_merkle_link, legacy shares use merkle_link -- share_check.hpp
45+
// :674-692) stays in the CALLER (the reconstruct closure, slice 5): this framer
46+
// takes an already-resolved MerkleLink, exactly like the DGB slice, so the
47+
// sealed link math is reused with NO new merkle path introduced here.
48+
//
49+
// Body/header self-check (item 3): because BTC hands us the full template tx
50+
// set, we can -- and do -- verify the assembled body against the share-committed
51+
// header BEFORE emitting bytes. body_merkle_root() folds [gentx_hash] ++ the
52+
// non-witness txids of other_txs through the sealed btc::coin::compute_merkle_root
53+
// SSOT (template_builder.hpp); for a body that matches the merkle link the share
54+
// committed to, this equals reconstruct_block_header()'s check_merkle_link root
55+
// by the definition of a coinbase (leaf-0) merkle branch. assemble_won_block
56+
// FAILS CLOSED (throws) on a mismatch rather than emit a bad-txnmrklroot block,
57+
// consistent with the whole slice arc's "never broadcast a malformed block"
58+
// posture (cf. gentx_unpack.hpp). This is a self-check over the EXISTING sealed
59+
// compute_merkle_root, NOT a new merkle path or a second commitment.
60+
//
61+
// Interaction with template_capture.hpp (slice 3): on a capture MISS slice 3
62+
// replays an EMPTY transactions[] -> other_txs = {}. If the won share mined a
63+
// coinbase-only template (empty merkle link), body_root == header_root == gentx
64+
// txid: the guard passes and the coinbase-only block broadcasts, carrying the
65+
// full subsidy exactly as template_capture documents. If instead the share
66+
// committed to fee txs (non-empty link), a coinbase-only body is a
67+
// bad-txnmrklroot block the daemon would reject anyway; the guard throws so the
68+
// reconstruct closure fails closed (announce + audit) instead of broadcasting a
69+
// doomed block. Nothing broadcastable is lost -- the guard only tightens the
70+
// non-empty-link miss case, which was never a valid block.
71+
//
72+
// Wire encoding is BlockType::Serialize -> TX_WITH_WITNESS(m_txs): the standard
73+
// Bitcoin-Core CONDITIONAL serializer -- it emits the per-tx witness marker/flag
74+
// iff some tx HasWitness(), legacy otherwise. The block's witness shape is thus
75+
// governed by whether the (already-committed) gentx carries a witness, NOT an
76+
// unconditional witness branch here. This is the identical path
77+
// NodeRPC::submit_block uses (rpc.cpp: pack<BlockType>(block) -> submitblock), so
78+
// the reconstructed block round-trips and the daemon accepts.
79+
//
80+
// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE --
81+
// block framing reuses BlockType + check_merkle_link + compute_merkle_root
82+
// verbatim; no share format, PoW hash, coinbase commitment, or PPLNS math is
83+
// touched.
84+
// ---------------------------------------------------------------------------
85+
86+
#include <stdexcept>
87+
#include <string>
88+
#include <utility>
89+
#include <vector>
90+
91+
#include <core/hash.hpp>
92+
#include <core/pack.hpp>
93+
#include <core/uint256.hpp>
94+
#include <btclibs/util/strencodings.h> // HexStr
95+
96+
#include "block.hpp" // SmallBlockHeaderType, BlockHeaderType, BlockType
97+
#include "transaction.hpp" // MutableTransaction, TX_NO_WITNESS
98+
#include "template_builder.hpp" // btc::coin::compute_merkle_root (sealed body-root SSOT)
99+
#include "../share_check.hpp" // btc::check_merkle_link (SSOT merkle-branch walk)
100+
#include "../share_types.hpp" // btc::MerkleLink
101+
102+
namespace btc
103+
{
104+
namespace coin
105+
{
106+
107+
// Reconstruct the full block header from the share's stored SmallBlockHeader
108+
// plus the gentx txid + (already segwit-resolved) merkle link. Mirrors the
109+
// merkle_root recomputation in share_check.hpp verification (SmallBlockHeader
110+
// omits merkle_root); the caller passes txid_merkle_link for segwit shares and
111+
// merkle_link for legacy ones (slice 5), so no link selection happens here.
112+
inline BlockHeaderType
113+
reconstruct_block_header(const SmallBlockHeaderType& small_header,
114+
const uint256& gentx_hash,
115+
const ::btc::MerkleLink& merkle_link)
116+
{
117+
BlockHeaderType header;
118+
header.m_version = small_header.m_version;
119+
header.m_previous_block = small_header.m_previous_block;
120+
header.m_timestamp = small_header.m_timestamp;
121+
header.m_bits = small_header.m_bits;
122+
header.m_nonce = small_header.m_nonce;
123+
// SmallBlockHeader has no merkle_root -- recompute it from the coinbase
124+
// (gentx) txid walked up the share's merkle branch, exactly as the share's
125+
// PoW hash was computed at verification time.
126+
header.m_merkle_root = ::btc::check_merkle_link(gentx_hash, merkle_link);
127+
return header;
128+
}
129+
130+
// The merkle leaf (txid) of an other-tx: double-SHA256 of its NON-WITNESS
131+
// serialization, identical to the gentx_hash derivation in gentx_unpack.hpp.
132+
// The BTC merkle tree (header root, and the segwit txid_merkle_link) commits to
133+
// TXIDs, never wtxids, so witness data is excluded here regardless of the tx's
134+
// witness shape.
135+
inline uint256 other_tx_txid(const MutableTransaction& tx)
136+
{
137+
return Hash(pack(TX_NO_WITNESS(tx)).get_span());
138+
}
139+
140+
// The body's merkle root: compute_merkle_root over [gentx_hash] ++ other-tx
141+
// txids, using the sealed btc::coin::compute_merkle_root SSOT (#570/#574/#579).
142+
// For a body that matches the merkle link the share committed to, this equals
143+
// reconstruct_block_header()'s check_merkle_link root.
144+
inline uint256
145+
body_merkle_root(const uint256& gentx_hash,
146+
const std::vector<MutableTransaction>& other_txs)
147+
{
148+
std::vector<uint256> txids;
149+
txids.reserve(1 + other_txs.size());
150+
txids.push_back(gentx_hash); // coinbase is leaf 0
151+
for (const auto& tx : other_txs)
152+
txids.push_back(other_tx_txid(tx));
153+
return compute_merkle_root(std::move(txids));
154+
}
155+
156+
// Assemble the full serialized parent block for a won share.
157+
// small_header : share.m_min_header (version|prev|time|bits|nonce)
158+
// gentx : the reconstructed, ALREADY witness-committed coinbase (tx 0)
159+
// gentx_hash : its txid (double-SHA256 of the non-witness bytes)
160+
// merkle_link : the share's segwit-resolved gentx->root branch (caller-chosen)
161+
// other_txs : the captured template's non-coinbase txs, in template order
162+
// Returns {block_bytes, block_hex}: block_bytes is the blob the embedded P2P
163+
// relay sends; block_hex is the same block for the external submitblock fallback
164+
// (block_hex == HexStr(block_bytes)). Throws std::runtime_error if the assembled
165+
// body's merkle root does not match the share-committed header root (fail-closed
166+
// against a bad-txnmrklroot block -- see the header comment's template_capture
167+
// interaction note).
168+
inline std::pair<std::vector<unsigned char>, std::string>
169+
assemble_won_block(const SmallBlockHeaderType& small_header,
170+
const MutableTransaction& gentx,
171+
const uint256& gentx_hash,
172+
const ::btc::MerkleLink& merkle_link,
173+
const std::vector<MutableTransaction>& other_txs)
174+
{
175+
BlockType block;
176+
static_cast<BlockHeaderType&>(block) =
177+
reconstruct_block_header(small_header, gentx_hash, merkle_link);
178+
179+
// Fail-closed body/header consistency guard: the share-committed header root
180+
// (the merkle_link walk) MUST equal the root the assembled body actually
181+
// hashes to, or the daemon rejects the block as bad-txnmrklroot. Reuses the
182+
// sealed compute_merkle_root SSOT -- NOT a new merkle path or a re-inject.
183+
const uint256 body_root = body_merkle_root(gentx_hash, other_txs);
184+
if (body_root != block.m_merkle_root)
185+
throw std::runtime_error(
186+
"assemble_won_block: assembled body merkle root " + body_root.GetHex() +
187+
" != share-committed header root " + block.m_merkle_root.GetHex() +
188+
" -- refusing to broadcast a bad-txnmrklroot block");
189+
190+
// txs = [gentx] ++ other_txs (coinbase first; data.py as_block ordering).
191+
block.m_txs.reserve(1 + other_txs.size());
192+
block.m_txs.push_back(gentx);
193+
for (const auto& tx : other_txs)
194+
block.m_txs.push_back(tx);
195+
196+
PackStream packed = pack<BlockType>(block);
197+
auto sp = packed.get_span();
198+
std::vector<unsigned char> bytes(
199+
reinterpret_cast<const unsigned char*>(sp.data()),
200+
reinterpret_cast<const unsigned char*>(sp.data()) + sp.size());
201+
std::string hex = HexStr(sp);
202+
return {std::move(bytes), std::move(hex)};
203+
}
204+
205+
} // namespace coin
206+
} // namespace btc

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp gentx_unpack_test.cpp)
4+
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp gentx_unpack_test.cpp block_assembly_test.cpp)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc

0 commit comments

Comments
 (0)