|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +#pragma once |
| 3 | +// --------------------------------------------------------------------------- |
| 4 | +// btc::coin::reconstruct_won_block / make_reconstruct_closure -- the won-block |
| 5 | +// reconstructor's (#744 dispatch arc, slice 5/7) BODY + run-loop closure: the |
| 6 | +// single composition the dispatch handler (won_block_dispatch.hpp, |
| 7 | +// make_on_block_found) injects as its WonBlockReconstructor. It ties the landed |
| 8 | +// sub-slices into one C++ implementation of the 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 | +// Composition (each step is its own KAT'd SSOT slice): |
| 16 | +// 1. select_won_block_merkle_link (this file) |
| 17 | +// share_version + merkle_link + optional txid_merkle_link --segwit gate--> |
| 18 | +// the merkle link the framer must walk gentx_hash up to reproduce the |
| 19 | +// share-committed header root (share_check.hpp:674-692). SEGWIT LINK |
| 20 | +// SELECTION LIVES HERE (the caller), not in the framer -- block_assembly.hpp |
| 21 | +// takes an already-resolved MerkleLink so the sealed link math is reused |
| 22 | +// with NO new merkle path introduced downstream. |
| 23 | +// 2. unpack_gentx_coinbase (gentx_unpack.hpp, slice 1 / #822) |
| 24 | +// the share's SSOT non-witness gentx bytes --> {MutableTransaction, txid} |
| 25 | +// injected at block tx index 0. |
| 26 | +// 3. assemble_won_block (block_assembly.hpp, as_block FRAMING / #838) |
| 27 | +// small_header + gentx + resolved merkle_link + other_txs --> {bytes, hex} |
| 28 | +// (merkle_root recomputed from gentx_hash up the link; txs = [gentx] ++ |
| 29 | +// other_txs; body/header consistency guard fails closed; BlockType codec |
| 30 | +// is the live submitblock path). |
| 31 | +// |
| 32 | +// ---- BTC won-block tx source: the CAPTURED GBT TEMPLATE, not tx_hash_refs ---- |
| 33 | +// The broadcast block's non-coinbase tx set is the GBT template snapshot the |
| 34 | +// miner was handed at job hand-out time (template_capture.hpp, slice 3), NOT the |
| 35 | +// share's transaction_hash_refs. The ref-walk (DGB other_tx_resolver) is a |
| 36 | +// share-CHAIN peer-propagation mechanism; it is never the block-broadcast |
| 37 | +// source. That is why this path is version-AGNOSTIC: pre-v34 vs v34+/v36 makes |
| 38 | +// no difference, because the share never carried the block tx set in the first |
| 39 | +// place. The template txs are INJECTED as a callable (template_other_txs_fn), so |
| 40 | +// this whole reconstruction is unit-testable against a synthetic template set |
| 41 | +// with NO live TemplateCapture / mempool. On a capture MISS slice 3 replays an |
| 42 | +// EMPTY transactions[] -> other_txs = {} -> a valid coinbase-only block (the |
| 43 | +// full subsidy), which the body/header guard accepts iff the won share committed |
| 44 | +// to an empty merkle link (see block_assembly.hpp's template_capture note). |
| 45 | +// |
| 46 | +// FAIL-CLOSED (integrator 2026-06-19/20, REWARD-PATH CRITICAL): make_on_block_found |
| 47 | +// fires this callback from the compute thread on a won share. It NEVER throws out |
| 48 | +// of the callback and NEVER emits a partial/wrong block: ANY missing share, |
| 49 | +// gentx-unpack failure, or body/header merkle mismatch is caught, logged LOUDLY, |
| 50 | +// and yields std::nullopt -- the share is announced + audited, the submitblock |
| 51 | +// RPC fallback still attempts independently (block_broadcast.hpp), and no |
| 52 | +// malformed block reaches the network. A wrong reconstruction would hash to the |
| 53 | +// wrong merkle root and be daemon-rejected anyway, so failing closed here is |
| 54 | +// strictly safer than emitting it. |
| 55 | +// |
| 56 | +// SEAM-FIRST (mirrors every #744 sub-slice): the share-field lookup, the gentx |
| 57 | +// regeneration, and the template-tx snapshot are INJECTED as callables, so the |
| 58 | +// whole closure -- including its fail-closed posture -- is unit-testable against |
| 59 | +// a synthetic share set with NO live ShareTracker / TemplateCapture. In the |
| 60 | +// run-loop (main_btc, slice 7) they bind to: |
| 61 | +// share_fields_fn = chain.get_share(h) -> { m_min_header, VERSION, |
| 62 | +// m_merkle_link, m_segwit_data->m_txid_merkle_link } |
| 63 | +// gentx_bytes_fn = generate_share_transaction(share, ...).bytes (SSOT) |
| 64 | +// -> unpack_gentx_coinbase -> { tx, txid } |
| 65 | +// template_other_txs_fn = TemplateCapture replay for the won share's job |
| 66 | +// (slice 3), in template order |
| 67 | +// |
| 68 | +// Per-coin isolation: src/impl/btc/ only. p2pool-merged-v36 surface: NONE -- |
| 69 | +// pure composition of already-validated share_info + the captured template + the |
| 70 | +// proven BlockType serializer; no share format, PoW, coinbase commitment, or |
| 71 | +// PPLNS math is touched. |
| 72 | +// --------------------------------------------------------------------------- |
| 73 | + |
| 74 | +#include <exception> |
| 75 | +#include <functional> |
| 76 | +#include <optional> |
| 77 | +#include <string> |
| 78 | +#include <utility> |
| 79 | +#include <vector> |
| 80 | + |
| 81 | +#include <core/log.hpp> |
| 82 | +#include <core/uint256.hpp> |
| 83 | + |
| 84 | +#include "block_assembly.hpp" // assemble_won_block, SmallBlockHeaderType, MutableTransaction |
| 85 | +#include "gentx_unpack.hpp" // unpack_gentx_coinbase, UnpackedGentx |
| 86 | +#include "won_block_dispatch.hpp" // WonBlockReconstructor |
| 87 | +#include "../share_types.hpp" // btc::MerkleLink, btc::is_segwit_activated |
| 88 | + |
| 89 | +namespace btc |
| 90 | +{ |
| 91 | +namespace coin |
| 92 | +{ |
| 93 | + |
| 94 | +// The reconstructed parent block, ready for broadcast_block_for_connect's dual |
| 95 | +// path: |
| 96 | +// bytes : the pre-serialized blob the embedded P2P relay sends |
| 97 | +// hex : the same block for the external submitblock (RPC) fallback |
| 98 | +struct ReconstructedWonBlock |
| 99 | +{ |
| 100 | + std::vector<unsigned char> bytes; |
| 101 | + std::string hex; |
| 102 | +}; |
| 103 | + |
| 104 | +// The per-share inputs the closure resolves before framing -- everything that is |
| 105 | +// NOT the gentx or the template tx set. |
| 106 | +// small_header : share.m_min_header (version|prev|time|bits|nonce) |
| 107 | +// share_version : the share format VERSION (segwit-activation gate input) |
| 108 | +// merkle_link : share.m_merkle_link (legacy gentx->root branch) |
| 109 | +// txid_merkle_link : share.m_segwit_data->m_txid_merkle_link if the share |
| 110 | +// carried segwit_data, else std::nullopt |
| 111 | +struct WonShareReconstructFields |
| 112 | +{ |
| 113 | + SmallBlockHeaderType small_header; |
| 114 | + uint64_t share_version{0}; |
| 115 | + ::btc::MerkleLink merkle_link; |
| 116 | + std::optional<::btc::MerkleLink> txid_merkle_link; |
| 117 | +}; |
| 118 | + |
| 119 | +// Segwit merkle-link SELECTION SSOT -- mirrors share_check.hpp:674-692 verbatim |
| 120 | +// at runtime. A segwit-activated share that carried segwit_data committed its |
| 121 | +// header merkle_root over segwit_data.txid_merkle_link; a legacy share (or a |
| 122 | +// segwit-active share with no segwit_data sentinel) committed over merkle_link. |
| 123 | +// The framer must walk gentx_hash up the SAME link the share committed to, or |
| 124 | +// the reconstructed header hashes to a different root than the share's PoW-valid |
| 125 | +// header and the daemon rejects the block. Returns a reference into one of the |
| 126 | +// two arguments (no copy). |
| 127 | +inline const ::btc::MerkleLink& |
| 128 | +select_won_block_merkle_link(uint64_t share_version, |
| 129 | + const ::btc::MerkleLink& merkle_link, |
| 130 | + const std::optional<::btc::MerkleLink>& txid_merkle_link) |
| 131 | +{ |
| 132 | + if (::btc::is_segwit_activated(share_version) && txid_merkle_link.has_value()) |
| 133 | + return txid_merkle_link.value(); |
| 134 | + return merkle_link; |
| 135 | +} |
| 136 | + |
| 137 | +// Reconstruct the full serialized parent block for a won share. |
| 138 | +// small_header : share.m_min_header (version|prev|time|bits|nonce) |
| 139 | +// share_version : the share format VERSION (segwit-activation gate) |
| 140 | +// merkle_link : share.m_merkle_link (legacy path) |
| 141 | +// txid_merkle_link : share.m_segwit_data->m_txid_merkle_link, or nullopt |
| 142 | +// gentx : the share's ALREADY witness-committed coinbase (tx 0) |
| 143 | +// gentx_hash : its txid (double-SHA256 of the non-witness bytes) |
| 144 | +// template_other_txs : the captured template's non-coinbase txs, template order |
| 145 | +// Returns {bytes, hex} with hex == HexStr(bytes). Throws (via assemble_won_block) |
| 146 | +// if the assembled body's merkle root does not match the share-committed header |
| 147 | +// root -- fail-closed against a bad-txnmrklroot block. |
| 148 | +inline ReconstructedWonBlock |
| 149 | +reconstruct_won_block(const SmallBlockHeaderType& small_header, |
| 150 | + uint64_t share_version, |
| 151 | + const ::btc::MerkleLink& merkle_link, |
| 152 | + const std::optional<::btc::MerkleLink>& txid_merkle_link, |
| 153 | + const MutableTransaction& gentx, |
| 154 | + const uint256& gentx_hash, |
| 155 | + const std::vector<MutableTransaction>& template_other_txs) |
| 156 | +{ |
| 157 | + // 1. segwit-resolve the link the share committed its header root to. |
| 158 | + const ::btc::MerkleLink& link = |
| 159 | + select_won_block_merkle_link(share_version, merkle_link, txid_merkle_link); |
| 160 | + |
| 161 | + // 2. frame [gentx] ++ template_other_txs via the assemble_won_block SSOT |
| 162 | + // (recompute header root up `link`; body/header guard; BlockType codec). |
| 163 | + auto framed = assemble_won_block(small_header, gentx, gentx_hash, link, |
| 164 | + template_other_txs); |
| 165 | + return ReconstructedWonBlock{std::move(framed.first), std::move(framed.second)}; |
| 166 | +} |
| 167 | + |
| 168 | +// Build the run-loop WonBlockReconstructor the dispatch handler |
| 169 | +// (make_on_block_found) installs. See the header note for the run-loop bindings |
| 170 | +// of each injected seam. The returned callable is FAIL-CLOSED: it returns |
| 171 | +// std::nullopt (never throws, never a partial block) on ANY error -- an unknown |
| 172 | +// share, a malformed gentx serialization, or a body/header merkle mismatch. |
| 173 | +inline WonBlockReconstructor |
| 174 | +make_reconstruct_closure( |
| 175 | + std::function<WonShareReconstructFields(const uint256&)> share_fields_fn, |
| 176 | + std::function<std::vector<unsigned char>(const uint256&)> gentx_bytes_fn, |
| 177 | + std::function<std::vector<MutableTransaction>(const uint256&)> template_other_txs_fn) |
| 178 | +{ |
| 179 | + return [share_fields_fn = std::move(share_fields_fn), |
| 180 | + gentx_bytes_fn = std::move(gentx_bytes_fn), |
| 181 | + template_other_txs_fn = std::move(template_other_txs_fn)]( |
| 182 | + const uint256& share_hash) |
| 183 | + -> std::optional<std::pair<std::vector<unsigned char>, std::string>> |
| 184 | + { |
| 185 | + try |
| 186 | + { |
| 187 | + // 1. share-level fields (header / version / merkle links). |
| 188 | + const WonShareReconstructFields f = share_fields_fn(share_hash); |
| 189 | + |
| 190 | + // 2. regenerate + unpack the share's SSOT gentx (block tx index 0); |
| 191 | + // unpack throws on a malformed (trailing-byte) serialization. |
| 192 | + const UnpackedGentx ug = unpack_gentx_coinbase(gentx_bytes_fn(share_hash)); |
| 193 | + |
| 194 | + // 3. the captured-GBT template's non-coinbase set (template order; |
| 195 | + // empty on a capture miss => a valid coinbase-only block). |
| 196 | + const std::vector<MutableTransaction> other_txs = |
| 197 | + template_other_txs_fn(share_hash); |
| 198 | + |
| 199 | + // 4. select segwit link + frame the block (guard fails closed). |
| 200 | + ReconstructedWonBlock r = reconstruct_won_block( |
| 201 | + f.small_header, f.share_version, f.merkle_link, f.txid_merkle_link, |
| 202 | + ug.tx, ug.txid, other_txs); |
| 203 | + |
| 204 | + return std::make_pair(std::move(r.bytes), std::move(r.hex)); |
| 205 | + } |
| 206 | + catch (const std::exception& e) |
| 207 | + { |
| 208 | + // Fail closed: announce + audit, never broadcast a partial/wrong |
| 209 | + // block. The submitblock RPC fallback still attempts independently. |
| 210 | + LOG_WARNING << "[EMB-BTC] won share " << share_hash.GetHex().substr(0, 16) |
| 211 | + << " -- reconstruct FAILED CLOSED (" << e.what() |
| 212 | + << "); NOT broadcast on P2P arm (submitblock RPC fallback still attempts)."; |
| 213 | + return std::nullopt; |
| 214 | + } |
| 215 | + }; |
| 216 | +} |
| 217 | + |
| 218 | +} // namespace coin |
| 219 | +} // namespace btc |
0 commit comments