From 1581dec50043e5f56205a3256d1d0c8e3d22be58 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 20 Jun 2026 09:14:25 +0000 Subject: [PATCH] dgb(#82): bind 5 won-block reconstruct seams to live tracker (decision a, deadlock-safe) Replace the interim nullopt reconstruct stub in main_dgb.cpp --run path with the faithful make_reconstruct_closure (#265), bound to the LIVE ShareTracker via 5 seams: share_fields / gentx_bytes / nth_parent / new_tx_hashes / known_txs. Adds a NON-LOCKING const-ref known_txs() accessor to dgb::NodeImpl (node.hpp). THREADING -- decision (a) (integrator + decisions, 2026-06-20): the prior approval assumed m_on_block_found fires lock-free on the IO thread and the seams would take a shared_lock snapshot. That premise was FALSIFIED: the callback fires on the COMPUTE thread, which ALREADY holds m_tracker_mutex EXCLUSIVELY (run_think node.cpp:1433 unique_lock -> think -> attempt_verify -> share_tracker.hpp:537). A shared_lock from the lock-owning thread on the non-recursive std::shared_mutex is UB/self-deadlock. Fix (a): drop the by-value snapshot_known_txs(), read chain + known-tx state DIRECTLY under the exclusive lock the compute thread already owns (non-locking accessor). HARD CONDITION verified (integrator): none of the 5 seam reads re-takes m_tracker_mutex -- grep confirms the mutex lives ONLY in NodeImpl (node.cpp/node.hpp); ShareTracker + ShareChain (chain.get_share, get_nth_parent_via_skip) and generate_share_transaction contain ZERO mutex/lock_guard/unique_lock/shared_lock. So (a) does NOT reintroduce the recursive self-deadlock one frame deeper. SHARE-VERSION GUARD: m_tx_info (new_transaction_hashes + transaction_hash_refs) is on-wire ONLY for share version < 34 (Share=17 / NewShare=33 -- the p2pool-dgb-scrypt oracle baseline). The generic ShareVariants::invoke must compile for all 5 variants, incl. v34+ segwit/merged which carry no tx refs in the DGB share format, so the two m_tx_info seams are if-constexpr requires-guarded and fail closed (throw -> caught -> nullopt, never a malformed broadcast) for any unexpected-for-DGB v34+ won share. Fenced to main_dgb.cpp + node.hpp. build_dgb green; #265 KAT 6/6 green; binary --help/selftest OK. The #82 dual-path broadcaster gate stays OPEN: the submitblock-RPC arm is still unarmed (coin_node rpc=nullptr, #163 seam guard = deferred-not-dropped, loud) and neither arm is verified until the live won-block-reaches-network test. HOLD merge (stacked on #265). --- src/c2pool/main_dgb.cpp | 102 +++++++++++++++++++++++++++++++++++++--- src/impl/dgb/node.hpp | 18 +++++++ 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/src/c2pool/main_dgb.cpp b/src/c2pool/main_dgb.cpp index 5acacfcb4..add5e3ca8 100644 --- a/src/c2pool/main_dgb.cpp +++ b/src/c2pool/main_dgb.cpp @@ -33,6 +33,7 @@ #include #include // make_mempool_tx_source (EmbeddedTxSource) #include +#include // #82 faithful make_reconstruct_closure (5 live tracker seams) #include #include #include @@ -274,14 +275,101 @@ int run_node(const core::CoinParams& params, bool testnet, // supplied (stays null otherwise -> sink no-ops, RPC fallback still fires). std::unique_ptr> coin_p2p; - p2p_node.tracker().m_on_block_found = dgb::coin::make_on_block_found( - /*reconstruct=*/[](const uint256& share_hash) - -> std::optional, std::string>> { - std::cout << "[DGB-POOL-BLOCK] won share " << share_hash.GetHex().substr(0, 16) - << " — reconstruct deferred (embedded gentx/known-tx feed " - "pending Phase B); not broadcast this build" << std::endl; - return std::nullopt; + // ── #82 faithful won-block reconstruct closure (5 live tracker seams) ── + // + // Replaces the interim `return nullopt` stub: make_reconstruct_closure + // (coin/reconstruct_closure.hpp, KAT'd) composes the share's gentx regen + + // skip-list ancestry walk + known-tx feed into a real p2pool Share.as_block() + // port, so a pool-won DGB block is actually reconstructed for both #82 arms. + // + // THREADING (integrator 2026-06-20, CORRECTED decision (a)): this closure is + // invoked from m_on_block_found, which fires on the COMPUTE thread while that + // thread ALREADY holds m_tracker_mutex EXCLUSIVELY (run_think node.cpp:1433 + // unique_lock -> attempt_verify -> share_tracker.hpp:537). The five seams + // therefore read tracker chain + known-tx state DIRECTLY with NO further + // locking -- a shared_lock from the lock-owning thread on the non-recursive + // shared_mutex would self-deadlock. VERIFIED (commit msg) that none of the + // seam reads re-takes m_tracker_mutex: get_share + get_nth_parent_via_skip are + // ShareChain methods (hash-map / skip-list lookups), the tx-hash and + // known_txs() reads are plain member access -- m_tracker_mutex lives only in + // NodeImpl and no seam touches it. + auto reconstruct_closure = dgb::coin::make_reconstruct_closure( + // share_fields_fn: chain.get_share(h) -> {min_header, merkle_link, refs}. + // get_share throws std::out_of_range on a miss -> closure fails closed. + [&p2p_node](const uint256& h) -> dgb::coin::ShareReconstructFields { + dgb::coin::ShareReconstructFields f; + p2p_node.tracker().chain.get_share(h).invoke([&](auto* obj) { + f.small_header = obj->m_min_header; + f.merkle_link = obj->m_merkle_link; + // m_tx_info (new_transaction_hashes + transaction_hash_refs) is + // on-wire ONLY for share version < 34 (Share=17 / NewShare=33 -- + // the p2pool-dgb-scrypt oracle baseline). v34+ segwit/merged + // shares carry no tx refs in the DGB share format, so a won block + // of one of those types fails closed here (caught -> nullopt, + // never a malformed broadcast). The generic invoke must still + // COMPILE for every variant, hence the constexpr guard. + if constexpr (requires { obj->m_tx_info; }) + f.refs = obj->m_tx_info.m_transaction_hash_refs; + else + throw std::runtime_error( + "reconstruct: won share version lacks m_tx_info " + "(v34+ carries no tx refs in DGB format) -- fail closed"); + }); + return f; + }, + // gentx_bytes_fn: regenerate the share's SSOT gentx and surface its + // non-witness bytes. v36_active=false per the documented binding + // (coin/reconstruct_closure.hpp). + [&p2p_node, ¶ms](const uint256& h) -> std::vector { + auto& tracker = p2p_node.tracker(); + std::vector bytes; + tracker.chain.get_share(h).invoke([&](auto* obj) { + dgb::coin::GentxCoinbase gc; + (void)dgb::generate_share_transaction(*obj, tracker, params, + /*dump_diag=*/false, + /*v36_active=*/false, &gc); + bytes = gc.bytes; + }); + return bytes; }, + // nth_parent_fn: O(log n) skip-list ancestry walk. + [&p2p_node](const uint256& h, uint64_t n) -> uint256 { + return p2p_node.tracker().chain.get_nth_parent_via_skip( + h, static_cast(n)); + }, + // new_tx_hashes_fn: ref into the share's m_new_transaction_hashes, read + // under the held exclusive lock (compute thread). + [&p2p_node](const uint256& h) -> const std::vector& { + const std::vector* out = nullptr; + p2p_node.tracker().chain.get_share(h).invoke([&](auto* obj) { + // Same v<34 guard as share_fields_fn: only Share/NewShare carry + // m_tx_info; v34+ leaves out null -> fail closed below. + if constexpr (requires { obj->m_tx_info; }) + out = &obj->m_tx_info.m_new_transaction_hashes; + }); + if (!out) throw std::out_of_range( + "new_tx_hashes: share missing or version lacks m_tx_info"); + return *out; + }, + // known_txs_fn: live known-tx pool (node.hpp known_txs(), non-locking) + // bridged coin::Transaction -> coin::MutableTransaction. Cached in a + // per-closure map so the returned pointer stays stable across the + // multiple lookups one reconstruct performs (won blocks are rare). A + // missing known-tx returns nullptr -> reconstruct throws -> fail closed. + [&p2p_node, cache = std::make_shared< + std::map>()] + (const uint256& h) -> const dgb::coin::MutableTransaction* { + auto cit = cache->find(h); + if (cit != cache->end()) return &cit->second; + const auto& known = p2p_node.known_txs(); + auto kit = known.find(h); + if (kit == known.end()) return nullptr; + auto ins = cache->emplace(h, dgb::coin::MutableTransaction(kit->second)); + return &ins.first->second; + }); + + p2p_node.tracker().m_on_block_found = dgb::coin::make_on_block_found( + /*reconstruct=*/std::move(reconstruct_closure), /*p2p_relay=*/[&ioc, &coin_p2p](const std::vector& block_bytes) { // #82 PRIMARY arm: relay the won block over the embedded coin-daemon // P2P producer. The sink fires from the compute thread, so post the diff --git a/src/impl/dgb/node.hpp b/src/impl/dgb/node.hpp index d1db5a9c8..7f7b54289 100644 --- a/src/impl/dgb/node.hpp +++ b/src/impl/dgb/node.hpp @@ -225,6 +225,24 @@ class NodeImpl : public pool::BaseNode /// or startup code (before compute thread exists). /// IO-thread code MUST use read_tracker() instead. ShareTracker& tracker() { return m_tracker; } + + /// #82 won-block CONSUME seam: a NON-LOCKING const-ref view of the live + /// known-tx pool the faithful reconstruct closure (coin/reconstruct_closure + /// .hpp) reads to resolve a won share's other_txs. + /// + /// LOCKING (integrator 2026-06-20, CORRECTED decision (a)): m_on_block_found + /// fires on the COMPUTE thread -- run_think (node.cpp:1433 takes + /// unique_lock(m_tracker_mutex), EXCLUSIVE) -> think -> attempt_verify -> + /// ShareTracker (share_tracker.hpp:537). That thread ALREADY holds + /// m_tracker_mutex exclusively, so a shared_lock from the SAME thread on this + /// non-recursive std::shared_mutex is UB/self-deadlock. The reconstruct path + /// therefore must NOT re-lock: it reads chain + known-tx state directly under + /// the exclusive lock the compute thread already owns. This accessor takes NO + /// lock and is ONLY safe from the compute thread (the won-block callback) or + /// single-threaded startup -- never from an IO thread without the lock held. + /// (Supersedes the by-value snapshot_known_txs() whose blocking shared_lock + /// would have self-deadlocked on this exact path.) + const std::map& known_txs() const { return m_known_txs; } const core::CoinParams& coin_params() const { return m_coin_params; } /// RAII guard for IO-thread tracker reads.