From d0460bad01290d09d2db8c26a6eeaf9e1e574373 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 17 Jul 2026 08:57:41 +0000 Subject: [PATCH 1/2] dgb(stratum): wire external-daemon GBT prevhash+bits fallback for empty embedded chain A freshly-stood-up c2pool-dgb :5025 node whose Scrypt-only HeaderChain has not yet ingested a tip (tip_hash()==nullopt) could supply NEITHER a previousblockhash NOR nBits: the header walk cannot reconstruct MultiShield-V4 (a 5-algo window == V37), and the embedded ingest that populates the tip lands later. Result: an EMPTY mining.notify prevhash on the live testnet. Add DGBWorkSource::set_gbt_tip_fn -- a std::function seam (mirrors set_pplns_inputs_fn and dash #726 dashd_fallback) bound in main_dgb.cpp to a lambda over NodeRPC::getblocktemplate. When the embedded chain has no tip, get_current_work_template() AND get_current_gbt_prevhash() (the notify-prevhash source) draw BOTH previousblockhash and the daemon-authoritative bits from ONE GBT snapshot (consistent height). A real embedded tip always wins; the RPC transport stays in main (stratum/ holds only the seam). Unbound / no digibyted creds / RPC failure -> both fields stay a truthful ABSENCE, byte-identical to the pre-wire path -- never a fabricated tip/bits. GBT previousblockhash is the u256_be_display_hex big-endian convention the embedded path emits, so it flows verbatim (width-guarded at the bind). TTL-cached (5s) so per-heartbeat template + prevhash polls do not each trigger a blocking getblocktemplate round-trip. External-daemon GBT fallback RETAINED (never removed), per V36. Scrypt-only; no shared-base / other-coin edit; per-coin isolation held. Tests: 3 new DgbWorkSource.GbtTip* (populate / unbound-absent / nullopt-absent) over the empty-chain fixture; full dgb_work_source_test 30/30 green. --- src/c2pool/main_dgb.cpp | 52 +++++++++++++++++++++ src/impl/dgb/stratum/work_source.cpp | 55 +++++++++++++++++++++- src/impl/dgb/stratum/work_source.hpp | 39 ++++++++++++++++ src/impl/dgb/test/work_source_test.cpp | 63 ++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) diff --git a/src/c2pool/main_dgb.cpp b/src/c2pool/main_dgb.cpp index 371cef78d..11f8c97cd 100644 --- a/src/c2pool/main_dgb.cpp +++ b/src/c2pool/main_dgb.cpp @@ -645,6 +645,58 @@ int run_node(const core::CoinParams& params, bool testnet, header_chain, mempool, testnet, std::move(stratum_submit_fn), params.subsidy_func); + // -- External-daemon GBT tip fallback bind (the fallback V36 mandates PERSIST) + // While the embedded HeaderChain is unfed (tip_hash()==nullopt -- the live + // state of a freshly-stood-up :5025 node) the Scrypt-only walk supplies + // NEITHER previousblockhash NOR the MultiShield-V4 bits. Bind the RPC arm so + // get_current_work_template() + get_current_gbt_prevhash() (the mining.notify + // prevhash source) draw both from digibyted getblocktemplate. The RPC + // transport stays in main (mirrors the #82 submit arm + dash's #726 + // dashd_fallback); stratum/ holds only the std::function seam. UNARMED (no + // digibyte.conf creds) -> seam left unbound -> truthful absence, byte-identical + // to the daemon-less default build. A real embedded tip always wins over this. + if (rpc) { + auto* rpc_ptr = rpc.get(); + work_source->set_gbt_tip_fn( + [rpc_ptr]() -> std::optional { + try { + // DGB GBT: "segwit" BIP9 rule; NodeRPC::getblocktemplate injects + // the mandatory separate Scrypt "algo" param (V36 Scrypt-only). + nlohmann::json tmpl = rpc_ptr->getblocktemplate({"segwit"}); + if (!tmpl.is_object()) + return std::nullopt; + auto ph = tmpl.find("previousblockhash"); + auto bt = tmpl.find("bits"); + if (ph == tmpl.end() || !ph->is_string() || + bt == tmpl.end() || !bt->is_string()) + return std::nullopt; + dgb::stratum::DGBWorkSource::GbtTip tip; + tip.previousblockhash = ph->get(); + tip.bits = bt->get(); + // GBT previousblockhash is 64-hex big-endian display -- the + // exact u256_be_display_hex convention the embedded path emits, + // so it flows verbatim (parity by the daemon's own GBT contract). + // Width-guard so a malformed reply is a truthful absence, never a + // fabricated short id. + if (tip.previousblockhash.size() != 64) + return std::nullopt; + return tip; + } catch (const std::exception& e) { + std::cerr << "[DGB-STRATUM] GBT tip fallback RPC failed: " + << e.what() << " -- prevhash/bits held absent" + << std::endl; + return std::nullopt; + } + }); + std::cout << "[DGB] stratum GBT tip fallback ARMED: previousblockhash+bits " + "<- digibyted getblocktemplate (embedded-empty-chain path)" + << std::endl; + } else { + std::cout << "[DGB] stratum GBT tip fallback UNARMED (no digibyted creds) " + "-- prevhash/bits truthful-absent until embedded chain feeds" + << std::endl; + } + // -- Phase-B producer bind: per-connection coinbase PPLNS inputs ---------- // Bind DGBWorkSource::set_pplns_inputs_fn -- the SOLE empty-jobs seam. While // unbound, build_connection_coinbase() returns an empty job (pre-wire stub); diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index d3a42d449..79d9d35d7 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -169,6 +169,14 @@ std::string DGBWorkSource::get_current_gbt_prevhash() const // embedded P2P header ingest) -- a truthful absence, never a fabricated id. if (auto th = chain_.tip_hash()) return u256_be_display_hex(*th); + // Embedded chain unfed -> external-daemon GBT fallback (the mining.notify + // prevhash path: stratum_server.cpp get_current_gbt_prevhash caller). Drawn + // from the SAME single GBT source as get_current_work_template()'s + // previousblockhash, so the dedicated getter and the assembled template can + // never diverge. Empty on unbound / no-creds / RPC-failure -- the server + // then falls back to the best-share hash (its documented empty-prevhash path). + if (auto tip = resolve_gbt_tip_fallback()) + return tip->previousblockhash; return {}; } @@ -320,8 +328,19 @@ nlohmann::json DGBWorkSource::get_current_work_template() const in.median_time_past = chain_.median_time_past(); in.curtime = static_cast(std::time(nullptr)); in.transactions = tx_sel.transactions; - if (auto th = chain_.tip_hash()) + if (auto th = chain_.tip_hash()) { + // Embedded chain carries a real tip -> source previousblockhash from it. + // bits stays absent here: the Scrypt-only walk cannot reconstruct the + // MultiShield-V4 5-algo window (== V37) -- unchanged truthful absence. in.previousblockhash = u256_be_display_hex(*th); + } else if (auto tip = resolve_gbt_tip_fallback()) { + // Embedded HeaderChain unfed -> external-daemon GBT fallback (persist per + // V36). Sources BOTH previousblockhash and the daemon-authoritative bits + // from ONE getblocktemplate snapshot (consistent height). Unbound / no + // creds / RPC failure -> both stay absent, byte-identical to before. + in.previousblockhash = tip->previousblockhash; + in.bits = tip->bits; + } return dgb::coin::build_work_template(in); } @@ -655,6 +674,40 @@ void DGBWorkSource::set_pplns_inputs_fn(PplnsInputsFn fn) pplns_inputs_fn_ = std::move(fn); } +void DGBWorkSource::set_gbt_tip_fn(GbtTipFn fn) +{ + std::lock_guard lk(gbt_tip_mutex_); + gbt_tip_fn_ = std::move(fn); + gbt_tip_cache_.reset(); // drop any stale cache when the seam is (re)bound + gbt_tip_cache_time_ = 0; +} + +std::optional DGBWorkSource::resolve_gbt_tip_fallback() const +{ + // Copy the seam + serve a fresh-enough cache under the lock; run the blocking + // RPC round-trip OUTSIDE the lock (single io_context thread today, but never + // hold a lock across network IO). TTL-bound so per-heartbeat template/prevhash + // polls do not each trigger a getblocktemplate round-trip. + GbtTipFn fn; + const int64_t now = static_cast(std::time(nullptr)); + { + std::lock_guard lk(gbt_tip_mutex_); + if (gbt_tip_cache_ && (now - gbt_tip_cache_time_) < GBT_TIP_TTL_SECONDS) + return gbt_tip_cache_; + fn = gbt_tip_fn_; // copy so a concurrent set_gbt_tip_fn() cannot tear it out + } + if (!fn) + return std::nullopt; // unbound (no digibyted creds armed) -> truthful absence + + std::optional tip = fn(); // blocking getblocktemplate; nullopt on RPC failure + if (tip) { + std::lock_guard lk(gbt_tip_mutex_); + gbt_tip_cache_ = tip; + gbt_tip_cache_time_ = now; + } + return tip; +} + uint256 DGBWorkSource::try_mint_share(const MintShareInputs& in) const { MintShareFn fn; diff --git a/src/impl/dgb/stratum/work_source.hpp b/src/impl/dgb/stratum/work_source.hpp index 73e0565fe..ea3b471d0 100644 --- a/src/impl/dgb/stratum/work_source.hpp +++ b/src/impl/dgb/stratum/work_source.hpp @@ -231,6 +231,28 @@ class DGBWorkSource : public core::stratum::IWorkSource const std::vector>>& merged_addrs)>; void set_pplns_inputs_fn(PplnsInputsFn fn); + /// External-daemon GBT tip fallback (the fallback V36 mandates PERSIST). + /// While the embedded HeaderChain is unfed -- tip_hash()==nullopt, which is + /// the live state of a freshly-stood-up :5025 node whose Scrypt-only header + /// ingest has not yet populated the chain -- the header walk can supply + /// NEITHER previousblockhash NOR the authoritative MultiShield-V4 bits (a + /// Scrypt-only walk cannot reconstruct the 5-algo retarget window == V37). + /// This seam lets get_current_work_template() AND get_current_gbt_prevhash() + /// source BOTH from the external digibyted getblocktemplate. Bound in + /// main_dgb.cpp to a lambda over NodeRPC::getblocktemplate -- the RPC + /// transport stays OUT of stratum/, mirroring set_pplns_inputs_fn and dash's + /// dashd_fallback closure (#726). Returns nullopt when the seam is unbound, + /// no digibyted creds are armed, or the RPC call fails -- both fields then + /// stay a truthful ABSENCE exactly as before (never a fabricated tip/bits). + /// Consumed ONLY when the embedded chain has no tip; a real embedded tip + /// always wins (single truthful source, never a prevhash/bits height split). + struct GbtTip { + std::string previousblockhash; ///< GBT big-endian display hex (u256_be_display_hex convention) + std::string bits; ///< GBT nBits compact hex, daemon-authoritative + }; + using GbtTipFn = std::function()>; + void set_gbt_tip_fn(GbtTipFn fn); + /// Dispatch one share-difficulty submission to the bound mint callback. /// The stage-4d mining_submit classify branch calls this on the /// "pow_hash <= share target" outcome. Returns the minted share hash, or a @@ -297,6 +319,23 @@ class DGBWorkSource : public core::stratum::IWorkSource mutable std::mutex pplns_inputs_mutex_; PplnsInputsFn pplns_inputs_fn_; + // External-daemon GBT tip fallback (set_gbt_tip_fn). Empty until bound in + // main_dgb; while empty the embedded-empty-chain path is byte-identical to + // the prior truthful-absence. A short TTL cache bounds how often the blocking + // getblocktemplate RPC round-trip runs on the single io_context thread (the + // template + prevhash accessors can be polled per stratum session heartbeat). + mutable std::mutex gbt_tip_mutex_; + GbtTipFn gbt_tip_fn_; + mutable std::optional gbt_tip_cache_; + mutable int64_t gbt_tip_cache_time_ = 0; + static constexpr int64_t GBT_TIP_TTL_SECONDS = 5; + + // Resolve the external-daemon GBT tip fallback (previousblockhash + bits), + // TTL-cached. Returns nullopt when no seam is bound / RPC unavailable / the + // call failed. Consumed by get_current_work_template() and + // get_current_gbt_prevhash() ONLY when chain_.tip_hash() is nullopt. + std::optional resolve_gbt_tip_fallback() const; + // Template cache (filled lazily; invalidated when work_generation_ bumps) // Stage 4c populates these. mutable std::mutex template_mutex_; diff --git a/src/impl/dgb/test/work_source_test.cpp b/src/impl/dgb/test/work_source_test.cpp index 4041b7806..6c1132145 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -575,6 +575,69 @@ dgb::coin::ConnCoinbasePplnsInputs sample_pplns_inputs() return in; } + +// ───────────────────────────────────────────────────────────────────────────── +// External-daemon GBT tip fallback (set_gbt_tip_fn) -- the empty-embedded-chain +// path that unblocks a freshly-stood-up :5025 node whose Scrypt-only HeaderChain +// has not yet ingested a tip. Fixture's HeaderChain is default-constructed +// (tip_hash()==nullopt), so these exercise exactly the live fallback condition. +// ───────────────────────────────────────────────────────────────────────────── + +using GbtTip = dgb::stratum::DGBWorkSource::GbtTip; + +TEST(DgbWorkSource, GbtTipFallbackPopulatesPrevhashAndBitsWhenChainEmpty) +{ + Fixture fx; + auto ws = fx.make(); + // Values mirror the live testnet GBT the integrator confirmed (h123 tip). + const std::string kPrev = + "ce2fd8d332d667c009e6e031fec6cba0e4d12c963d2c84f824d6c1ae676e7de9"; + const std::string kBits = "2003ffff"; + int calls = 0; + ws->set_gbt_tip_fn([&]() -> std::optional { + ++calls; + return GbtTip{kPrev, kBits}; + }); + + // mining.notify prevhash source (stratum_server get_current_gbt_prevhash). + EXPECT_EQ(ws->get_current_gbt_prevhash(), kPrev); + + // The assembled GBT template carries BOTH previousblockhash and the + // daemon-authoritative bits from the same fallback snapshot. + auto tmpl = ws->get_current_work_template(); + ASSERT_TRUE(tmpl.contains("previousblockhash")); + EXPECT_EQ(tmpl["previousblockhash"].get(), kPrev); + ASSERT_TRUE(tmpl.contains("bits")); + EXPECT_EQ(tmpl["bits"].get(), kBits); + + // TTL cache: the two accessors above must not each trigger a fresh RPC. + EXPECT_LE(calls, 1); +} + +TEST(DgbWorkSource, GbtTipFallbackAbsentWhenSeamUnbound) +{ + Fixture fx; + auto ws = fx.make(); + // No set_gbt_tip_fn -> truthful absence (byte-identical to pre-wire). + EXPECT_TRUE(ws->get_current_gbt_prevhash().empty()); + auto tmpl = ws->get_current_work_template(); + EXPECT_FALSE(tmpl.contains("previousblockhash")); + EXPECT_FALSE(tmpl.contains("bits")); +} + +TEST(DgbWorkSource, GbtTipFallbackNulloptStaysAbsent) +{ + Fixture fx; + auto ws = fx.make(); + // Seam bound but declining (RPC down / no template yet) -> same absence as + // unbound; never a fabricated prevhash/bits. + ws->set_gbt_tip_fn([]() -> std::optional { return std::nullopt; }); + EXPECT_TRUE(ws->get_current_gbt_prevhash().empty()); + auto tmpl = ws->get_current_work_template(); + EXPECT_FALSE(tmpl.contains("previousblockhash")); + EXPECT_FALSE(tmpl.contains("bits")); +} + } // namespace TEST(DgbWorkSource, ConnectionCoinbaseEmptyUntilPplnsInputsWired) From 2ccf4334ab89116a608f65b62871f7b6bdac7236 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 17 Jul 2026 09:39:35 +0000 Subject: [PATCH 2/2] dgb(stratum): score shares via scrypt digest in compute_share_difficulty (unstub 4b/4c) DGBWorkSource::compute_share_difficulty was a Stage-4b/4c stub returning 0.0. The coin-agnostic StratumServer calls it BEFORE mining_submit and silently rejects any share scoring below the vardiff floor (stratum_server.cpp: error 23 Low difficulty share, no server-side log), so every accepted-looking share was dropped before the stage-4d classify+broadcaster ever ran -- the pool could never advance its tip. Implement it as a byte-for-byte mirror of mining_submit reconstruct: coinb1||en1||en2||coinb2 -> sha256d txid -> ascend merkle branches -> 80-byte header -> scrypt_1024_1_1_256, then bridge the coin-space u256 into core uint256 via the u256_be_display_hex SSOT and return chain::target_to_difficulty (diff-1 0x1d00ffff) -- unit-identical to the required_difficulty / pool_difficulty the gate compares. Malformed reconstruct (header != 80B) keeps the documented 0.0 not-scored sentinel. KAT: valid header scores a finite positive difficulty; malformed input still returns 0.0. 31/31 dgb_work_source_test green. --- src/impl/dgb/stratum/work_source.cpp | 82 ++++++++++++++++++++++---- src/impl/dgb/test/work_source_test.cpp | 20 +++++++ 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index 79d9d35d7..37d82becd 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -31,6 +31,7 @@ #include #include // Hash (sha256d) for coinbase txid + merkle pair +#include // chain::target_to_difficulty (vardiff/pool unit parity) #include // address_to_script (share payout from username) #include // ParseHex @@ -630,20 +631,75 @@ nlohmann::json DGBWorkSource::mining_submit( } } double DGBWorkSource::compute_share_difficulty( - const std::string& /*coinb1*/, const std::string& /*coinb2*/, - const std::string& /*extranonce1*/, const std::string& /*extranonce2*/, - const std::string& /*ntime*/, const std::string& /*nonce*/, - uint32_t /*version*/, const std::string& /*prevhash_hex*/, - const std::string& /*nbits_hex*/, - const std::vector& /*merkle_branches*/) const + const std::string& coinb1, const std::string& coinb2, + const std::string& extranonce1, const std::string& extranonce2, + const std::string& ntime, const std::string& nonce, + uint32_t version, const std::string& prevhash_hex, + const std::string& nbits_hex, + const std::vector& merkle_branches) const { - // Stage 4b/4c: reconstruct the 80-byte header from (coinb1+en1+en2+coinb2) - // merkle-rooted with merkle_branches, then scrypt_1024_1_1_256(header) and - // return diff1 / pow_hash. Until then the coin-agnostic StratumServer must - // not credit pseudoshares it cannot score, so we return 0.0 — the documented - // parse-error / not-yet sentinel that the vardiff gate already treats as a - // hard reject (no garbage difficulty leaks into the rate monitor). - return 0.0; + // Stage 4b/4c LIVE: reconstruct the 80-byte header EXACTLY as mining_submit + // does (coinb1||en1||en2||coinb2 -> sha256d txid -> ascend merkle branches -> + // header), run the DGB-Scrypt digest (scrypt_1024_1_1_256, the sole algo V36 + // validates), and return the achieved difficulty in the SAME unit the coin- + // agnostic StratumServer vardiff/pool gate uses (chain::target_to_difficulty, + // diff-1 == 0x1d00ffff). A malformed reconstruct (bad hex -> header != 80B) + // returns the documented 0.0 not-scored sentinel, which the gate treats as a + // hard reject -- so no garbage difficulty leaks into the rate monitor. + + // 1. coinbase = coinb1 || extranonce1 || extranonce2 || coinb2 + auto coinb1_bytes = ParseHex(coinb1); + auto en1_bytes = ParseHex(extranonce1); + auto en2_bytes = ParseHex(extranonce2); + auto coinb2_bytes = ParseHex(coinb2); + + std::vector coinbase; + coinbase.reserve(coinb1_bytes.size() + en1_bytes.size() + + en2_bytes.size() + coinb2_bytes.size()); + coinbase.insert(coinbase.end(), coinb1_bytes.begin(), coinb1_bytes.end()); + coinbase.insert(coinbase.end(), en1_bytes.begin(), en1_bytes.end()); + coinbase.insert(coinbase.end(), en2_bytes.begin(), en2_bytes.end()); + coinbase.insert(coinbase.end(), coinb2_bytes.begin(), coinb2_bytes.end()); + + // 2. coinbase_txid = sha256d(coinbase) (non-witness txid for the merkle root) + uint256 coinbase_txid = Hash( + std::span(coinbase.data(), coinbase.size())); + + // 3. merkle_root = ascend the frozen stratum merkle branches (LE-internal). + uint256 merkle_root = coinbase_txid; + for (const auto& branch_hex : merkle_branches) { + uint256 b; + auto bb = ParseHex(branch_hex); + if (bb.size() == 32) std::memcpy(b.begin(), bb.data(), 32); + merkle_root = merkle_pair(merkle_root, b); + } + + // 4. header = version(LE) || prev(LE) || merkle_root || ntime(LE) + // || nbits(LE) || nonce(LE); prevhash arrives BE-display, so + // reverse to internal byte order (mirrors mining_submit byte-for-byte). + auto prevhash_be = ParseHex(prevhash_hex); + std::vector prevhash_le(prevhash_be.rbegin(), prevhash_be.rend()); + + std::vector header; + header.reserve(80); + push_u32_le(header, version); + header.insert(header.end(), prevhash_le.begin(), prevhash_le.end()); + header.insert(header.end(), merkle_root.data(), merkle_root.data() + 32); + push_u32_le(header, parse_be_hex_u32(ntime)); + push_u32_le(header, parse_be_hex_u32(nbits_hex)); // share target bits + push_u32_le(header, parse_be_hex_u32(nonce)); + + if (header.size() != 80) + return 0.0; // malformed reconstruct -> not-scored sentinel (hard reject). + + // 5. DGB-Scrypt PoW digest, then difficulty relative to diff-1. Bridge the + // coin-space u256 into core uint256 via the u256_be_display_hex SSOT so + // the number is unit-identical to required_difficulty / pool_difficulty + // (both computed through chain::target_to_difficulty on the core side). + dgb::coin::u256 pow_hash = dgb::coin::scrypt_pow_hash(header.data()); + uint256 pow_core; + pow_core.SetHex(dgb::coin::u256_be_display_hex(pow_hash)); + return chain::target_to_difficulty(pow_core); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/impl/dgb/test/work_source_test.cpp b/src/impl/dgb/test/work_source_test.cpp index 6c1132145..852133197 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +#include // dgb::stratum::DGBWorkSource — Stage 4a skeleton construction + contract test. // // Proves the work source instantiates against the live coin types @@ -355,6 +356,25 @@ TEST(DgbWorkSource, ComputeShareDifficultyReturnsNotYetSentinel) EXPECT_DOUBLE_EQ(diff, 0.0); } +// Stage 4b/4c live: a well-formed reconstruct (valid hex, header == 80B) is now +// SCORED -- scrypt_1024_1_1_256(header) bridged to core uint256 via the +// u256_be_display_hex SSOT and returned as chain::target_to_difficulty(pow), +// the SAME unit the coin-agnostic StratumServer vardiff/pool gate compares. A +// positive score is exactly what lets a real share clear the gate and reach +// mining_submit (the 0.0 stub silently rejected every share pre-fix). +TEST(DgbWorkSource, ComputeShareDifficultyScoresValidHeader) +{ + Fixture fx; + auto ws = fx.make(); + const std::string prevhash(64, '0'); // 32 zero bytes (BE display hex) + double diff = ws->compute_share_difficulty( + "01000000", "00000000", "00", "00", "5f5e1000", "0000abcd", + /*version=*/0x20000000u, prevhash, "1e0ffff0", + /*merkle_branches=*/{}); + EXPECT_GT(diff, 0.0); + EXPECT_TRUE(std::isfinite(diff)); +} + // Stage 4c coinbasevalue wire: the work template surfaces the NEXT-block // height and its coinbasevalue, the latter derived THROUGH the #207 SSOT // (subsidy_func) keyed on next_block_height() == tip.height + 1 (#209). An