From fb8f91b5cf361242f7555deefd11ce525e74127f Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 14:59:01 +0000 Subject: [PATCH 1/3] dgb: HeaderChain tip_hash() accessor + wire previousblockhash into work template (Stage 4c) HeaderSample gains a sha256d block-id slot (block_hash, u256; 0 == not populated here, the same sentinel pow_hash uses) and HeaderChain gains a tip_hash() accessor returning the newest header's id or nullopt when the chain is empty / the tip carries no hash. get_current_work_template emits previousblockhash as GBT-conventional big-endian display hex ONLY when tip_hash() is present -- a truthful conditional, never a fabricated hash. The embedded P2P header-download -> validate_and_append ingest that populates block_hash lands in a following slice; until then tip_hash() is nullopt and previousblockhash is held back exactly as before. bits stays HELD BACK: the only embedded next-target source is the DigiShield damped multiply, which DGB Core runs as MultiShield V4 (a global window across all 5 algos == V37); a Scrypt-only walk cannot reconstruct it, so the ingest path demotes that gate to a no-op. Emitting a digishield-derived bits would be a known-wrong value. The authoritative bits is the external-daemon GBT value, not plumbed into this embedded path yet -- surfaced as [decision-needed]. Fenced to src/impl/dgb (4 files); test cases added to existing targets header_chain_test (31 -> 35) and dgb_work_source_test (15 -> 16), no new gtest target so the build.yml allowlist is unchanged. Both green. --- src/impl/dgb/coin/header_chain.hpp | 26 +++++++++-- src/impl/dgb/stratum/work_source.cpp | 53 +++++++++++++++++++--- src/impl/dgb/test/header_chain_test.cpp | 58 +++++++++++++++++++++++++ src/impl/dgb/test/work_source_test.cpp | 31 +++++++++++++ 4 files changed, 159 insertions(+), 9 deletions(-) diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index fde409b4f..7804d0a1c 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -75,10 +75,11 @@ using ::dgb::coin::u256; // == 0 is the default for "scrypt(header) not evaluated here" (trivially // satisfies any target); the daemon port fills it. struct HeaderSample { - int32_t n_version = 0; - int64_t n_time = 0; - u256 target = 0; // expanded PoW target (smaller == more work) - u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW + int32_t n_version = 0; + int64_t n_time = 0; + u256 target = 0; // expanded PoW target (smaller == more work) + u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW + u256 block_hash = 0; // sha256d(header) block id; 0 == not populated here }; // Outcome of validating + ingesting one header. @@ -358,6 +359,23 @@ class HeaderChain { return static_cast(m_base_height + (m_chain.size() - 1)); } + // Block hash of the newest header, or nullopt when the chain is empty OR + // the tip carries no hash. DGB's block id is sha256d over the 80-byte + // header (params.hpp block_hash_func == sha256d) -- distinct from pow_hash, + // which is the scrypt(header) PoW digest. HeaderSample stores it as a u256 + // and the work-template emitter renders the GBT-conventional big-endian + // display hex. block_hash == 0 is the "not populated here" sentinel (the + // SAME convention pow_hash uses): the embedded-daemon header-ingest port + // fills it at the validate_and_append boundary, so until that lands this + // returns nullopt and get_current_work_template holds previousblockhash + // back -- a truthful absence, never a fabricated hash. + std::optional tip_hash() const + { + if (m_chain.empty() || m_chain.back().block_hash.is_zero()) + return std::nullopt; + return m_chain.back().block_hash; + } + // Absolute height of the block the next template builds on top of the tip // == tip_height()+1, or m_base_height for an empty chain. This is the // template builder's `next_h` (btc template_builder.hpp: tip.height + 1). diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index d191dd13e..27e25d6fc 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -23,10 +23,33 @@ #include #include +#include #include namespace dgb::stratum { +namespace { + +// Render a u256 as the GBT-conventional big-endian block-hash display hex: +// most-significant limb first, 64 lowercase hex digits, no 0x prefix. Mirrors +// uint256::GetHex() ordering for a hash stored with limb[0] least-significant. +// Header-only u256 has no GetHex(), and this TU must not depend on btclibs' +// uint256, so we format the limbs directly. +std::string u256_be_display_hex(const dgb::coin::u256& v) +{ + static constexpr char H[] = "0123456789abcdef"; + std::string out; + out.reserve(64); + for (int li = 3; li >= 0; --li) { + const uint64_t w = v.limb[li]; + for (int sh = 60; sh >= 0; sh -= 4) + out.push_back(H[(w >> sh) & 0xF]); + } + return out; +} + +} // namespace + DGBWorkSource::DGBWorkSource(c2pool::dgb::HeaderChain& chain, dgb::coin::Mempool& mempool, bool is_testnet, @@ -184,11 +207,26 @@ nlohmann::json DGBWorkSource::get_current_work_template() const // so no transactions are fabricated and fees stay 0 // (consistent with the total_fees=0 coinbasevalue above). // - // Deliberately NOT emitted yet — they need accessors the Scrypt-only - // HeaderSample does not carry, and land in the following Stage 4c/4d slices: - // previousblockhash — the tip block hash (HeaderSample stores no hash yet) - // bits — the next-block compact target off the DigiShield - // retarget window + // previousblockhash — the tip block id. Emitted ONLY when the HeaderChain + // carries a real tip hash (tip_hash() accessor): the + // Scrypt-only HeaderSample now carries a block_hash slot, + // but the embedded P2P header-download -> validate_and_append + // ingest that POPULATES it lands in a following slice, so on + // today's chain state tip_hash() is nullopt and the field is + // held back -- a truthful absence, never a fabricated hash. + // bits — HELD BACK. The only embedded next-target source is the + // DigiShield/MultiShield damped multiply, which DGB Core + // runs as MultiShield V4: a GLOBAL window across all 5 algos + // with per-algo adjust + MTP deltas. A Scrypt-only header + // walk cannot reconstruct that window (== V37, 5-algo + // validation), so the ingest path deliberately demotes the + // retarget gate to a no-op (see header_chain.hpp). Emitting + // a digishield_next_target()-derived bits would be a + // KNOWN-WRONG value -- the same fabrication the empty + // transactions[] and total_fees=0 avoid. The authoritative + // bits is the external-daemon GBT value, which is not + // plumbed into this embedded template path yet; bits stays + // absent until then. [decision-needed] surfaced to integrator. // and the per-connection coinbase (gentx + ShareTracker ref_hash + PPLNS // payout map) assembles in build_connection_coinbase() — that output is // consensus-bearing and surfaces for an operator tap, not in this field wire. @@ -209,6 +247,11 @@ nlohmann::json DGBWorkSource::get_current_work_template() const tmpl["curtime"] = curtime; tmpl["mintime"] = mintime; tmpl["transactions"] = nlohmann::json::array(); + + // previousblockhash: truthful conditional emit (see field notes above). + if (auto th = chain_.tip_hash()) + tmpl["previousblockhash"] = u256_be_display_hex(*th); + return tmpl; } diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index e6edf44c1..f58bb21d8 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -727,3 +727,61 @@ TEST(HeaderChainBlockHeight, CheckpointSeedNumbersFirstHeader) EXPECT_EQ(hc.tip_height().value(), 12345u); EXPECT_EQ(hc.next_block_height(), 12346u); } + +// ───────────────────────────────────────────────────────────────────────────── +// tip_hash(): the Scrypt-only HeaderSample now carries a sha256d block-id slot, +// surfaced for the work template's previousblockhash. block_hash == 0 is the +// "not populated here" sentinel (same convention as pow_hash), so the accessor +// reports absence rather than a fabricated all-zero hash. +// ───────────────────────────────────────────────────────────────────────────── + +TEST(HeaderChainTipHash, EmptyChainHasNoTipHash) +{ + HeaderChain hc; + EXPECT_FALSE(hc.tip_hash().has_value()); +} + +TEST(HeaderChainTipHash, UnpopulatedHashStaysNullopt) +{ + // A header validated/appended WITHOUT a block_hash (the chain-helper path, + // exactly as every other test here) leaves block_hash == 0 -> nullopt, NOT + // a zero hash. Guards against previousblockhash being emitted as all-zeros. + HeaderChain hc; + ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 100}), + IngestResult::VALIDATED_SCRYPT); + EXPECT_TRUE(hc.tip_height().has_value()); // height advanced... + EXPECT_FALSE(hc.tip_hash().has_value()); // ...but no hash carried. +} + +TEST(HeaderChainTipHash, ReturnsNewestPopulatedHash) +{ + HeaderChain hc; + HeaderSample h1{SCRYPT, 1000, 100}; + h1.block_hash = dgb::coin::u256::from_u64(0x1111ull); + ASSERT_EQ(hc.validate_and_append(h1), IngestResult::VALIDATED_SCRYPT); + ASSERT_TRUE(hc.tip_hash().has_value()); + EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0x1111ull)); + + // A newer header's hash supersedes the prior tip's. + HeaderSample h2{SCRYPT, 1100, 100}; + h2.block_hash = dgb::coin::u256::from_u64(0x2222ull); + ASSERT_EQ(hc.validate_and_append(h2), IngestResult::VALIDATED_SCRYPT); + EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0x2222ull)); +} + +TEST(HeaderChainTipHash, ContinuityHeaderHashIsTip) +{ + // A non-Scrypt continuity header still extends the chain and becomes the + // tip, so its block_hash (if carried) is the tip hash -- previousblockhash + // tracks the actual newest block id regardless of algo. + HeaderChain hc; + HeaderSample s1{SCRYPT, 1000, 100}; + s1.block_hash = dgb::coin::u256::from_u64(0xaaaaull); + ASSERT_EQ(hc.validate_and_append(s1), IngestResult::VALIDATED_SCRYPT); + + HeaderSample c1{SHA256D, 1075, 999999}; + c1.block_hash = dgb::coin::u256::from_u64(0xbbbbull); + ASSERT_EQ(hc.validate_and_append(c1), IngestResult::ACCEPTED_CONTINUITY); + EXPECT_TRUE(hc.tip_hash().value() == dgb::coin::u256::from_u64(0xbbbbull)); +} + diff --git a/src/impl/dgb/test/work_source_test.cpp b/src/impl/dgb/test/work_source_test.cpp index 024c0e617..2faf2f43c 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -282,4 +282,35 @@ TEST(DgbWorkSource, CoinbaseValueHonorsGbtVerbatim) kGbt); } + +// previousblockhash: emitted as GBT-conventional big-endian display hex ONLY +// when the HeaderChain carries a real tip hash (tip_hash() accessor). With a +// known tip block_hash, the template surfaces it MSB-limb-first; bits stays +// HELD (no faithful embedded V36 next-target -- MultiShield V4 is V37). +TEST(DgbWorkSource, WorkTemplateEmitsPreviousBlockHashWhenTipCarriesHash) +{ + Fixture fx; + fx.chain.set_base_height(400000); + // Seed one Scrypt header carrying a distinctive block id. n_version with + // algo nibble 0x0000 is the Scrypt lane; target 100 with pow_hash 0 (<= + // target) clears the context-free PoW gate; empty-chain MTP is unconstrained. + c2pool::dgb::HeaderSample h; + h.n_version = 0x20000000; + h.n_time = 1000; + h.target = 100; + h.block_hash = dgb::coin::u256::from_u64(0x123456789abcdef0ULL); + ASSERT_EQ(fx.chain.validate_and_append(h), + c2pool::dgb::IngestResult::VALIDATED_SCRYPT); + + auto ws = fx.make(); + auto tmpl = ws->get_current_work_template(); + ASSERT_TRUE(tmpl.contains("previousblockhash")); + // limb[0] is least-significant -> renders as the trailing 16 hex digits, + // preceded by 48 zero digits (limbs 3..1 are zero). 64 chars total. + EXPECT_EQ(tmpl["previousblockhash"].get(), + std::string(48, '0') + "123456789abcdef0"); + // bits remains deliberately absent (V37 MultiShield V4 wall). + EXPECT_FALSE(tmpl.contains("bits")); +} + } // namespace From a36257d7eb37bfb53d0164f46c2205a1a3c38f29 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 15:31:56 +0000 Subject: [PATCH 2/3] dgb: wire get_current_gbt_prevhash() through tip_hash() SSOT (Stage 4b) The Stage-4b prevhash getter still returned {} while get_current_work_template emits previousblockhash from chain_.tip_hash() (#216). Route the getter through the SAME tip_hash() accessor and u256_be_display_hex formatter so the dedicated getter and the assembled template cannot silently diverge: one truthful source. Empty string when tip_hash() == nullopt (empty chain / unpopulated block_hash sentinel) -- a truthful absence, never a fabricated id. Non-consensus read-only getter, fenced to src/impl/dgb. +1 test asserting getter == template field on a seeded tip and joint-absence with no tip; dgb_work_source_test 17/17. --- src/impl/dgb/stratum/work_source.cpp | 11 ++++++++- src/impl/dgb/test/work_source_test.cpp | 32 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index 27e25d6fc..c0201b0c7 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -107,7 +107,16 @@ std::function DGBWorkSource::get_best_share_hash_fn() const std::string DGBWorkSource::get_current_gbt_prevhash() const { - // Stage 4b: read chain_.tip() and return BE-display-hex form. + // The tip block id as GBT-conventional big-endian display hex, drawn from + // the SAME source as get_current_work_template()'s previousblockhash field + // (chain_.tip_hash(), the #216 HeaderChain accessor) through the SAME + // u256_be_display_hex formatter -- ONE truthful source, so the dedicated + // getter and the assembled template can never silently diverge. Empty + // string when the chain carries no real tip hash (tip_hash() == nullopt: + // an empty chain, or the block_hash==0 sentinel from the not-yet-wired + // embedded P2P header ingest) -- a truthful absence, never a fabricated id. + if (auto th = chain_.tip_hash()) + return u256_be_display_hex(*th); return {}; } diff --git a/src/impl/dgb/test/work_source_test.cpp b/src/impl/dgb/test/work_source_test.cpp index 2faf2f43c..03605f6dc 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -313,4 +313,36 @@ TEST(DgbWorkSource, WorkTemplateEmitsPreviousBlockHashWhenTipCarriesHash) EXPECT_FALSE(tmpl.contains("bits")); } +// The dedicated prevhash getter and the assembled template draw the tip hash +// from ONE source (chain_.tip_hash() through u256_be_display_hex), so they can +// never diverge: with a real tip the getter returns the SAME BE-display-hex the +// template emits; with no tip BOTH are absent (getter empty string, template +// omits the field). +TEST(DgbWorkSource, GbtPrevhashGetterMatchesTemplateField) +{ + Fixture fx; + // No tip yet -> getter empty, template omits previousblockhash. + { + auto ws = fx.make(); + EXPECT_TRUE(ws->get_current_gbt_prevhash().empty()); + EXPECT_FALSE(ws->get_current_work_template().contains("previousblockhash")); + } + // Seed a Scrypt header carrying a distinctive block id (mirrors the + // previousblockhash emit test) -> getter == template field, BE-display-hex. + fx.chain.set_base_height(400000); + c2pool::dgb::HeaderSample h; + h.n_version = 0x20000000; + h.n_time = 1000; + h.target = 100; + h.block_hash = dgb::coin::u256::from_u64(0x123456789abcdef0ULL); + ASSERT_EQ(fx.chain.validate_and_append(h), + c2pool::dgb::IngestResult::VALIDATED_SCRYPT); + + auto ws = fx.make(); + const std::string expected = std::string(48, '0') + "123456789abcdef0"; + EXPECT_EQ(ws->get_current_gbt_prevhash(), expected); + EXPECT_EQ(ws->get_current_work_template()["previousblockhash"].get(), + expected); +} + } // namespace From d67ca9d0ca738d1f527226e524638be670200b60 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 16:04:55 +0000 Subject: [PATCH 3/3] dgb: extract work-template assembly into build_work_template SSOT (Stage 4c) Lift the GBT-shaped template assembly out of DGBWorkSource::get_current_work_template() into a free function dgb::coin::build_work_template() in coin/template_builder.hpp, so the stratum work source and the embedded path emit ONE template object and cannot diverge. Pure non-consensus shaping: the consensus-bearing coinbasevalue is still resolved by the caller through the #207 resolve_coinbase_value -> subsidy_func SSOT and passed in verbatim; the builder never recomputes or scales it. version (Scrypt lane pin via DGB_BLOCK_VERSION_SCRYPT), mintime (MTP+1, 0 on empty chain), curtime, empty transactions[] and the truthful conditional previousblockhash all move into the builder unchanged. bits stays held back (MultiShield V4 next-target is V37). Behaviour-preserving: dgb_work_source_test 17/17 unchanged (incl the prevhash getter == template field joint-absence assertion). New guard-weight dgb_template_builder_test 7/7 pins the invariants (Scrypt pin, coinbasevalue pass-through, mintime/empty-chain, empty transactions + no bits, prevhash conditional, determinism). Wired into ctest + both build.yml allowlists (#143 NOT_BUILT trap avoided). --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/template_builder.hpp | 77 +++++++++++++ src/impl/dgb/stratum/work_source.cpp | 35 +++--- src/impl/dgb/test/CMakeLists.txt | 12 ++ src/impl/dgb/test/template_builder_test.cpp | 121 ++++++++++++++++++++ 5 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 src/impl/dgb/test/template_builder_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 288b7a1c4..ba9d8921c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \ v37_test \ @@ -202,7 +202,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ diff --git a/src/impl/dgb/coin/template_builder.hpp b/src/impl/dgb/coin/template_builder.hpp index 4df325420..ccacfb00f 100644 --- a/src/impl/dgb/coin/template_builder.hpp +++ b/src/impl/dgb/coin/template_builder.hpp @@ -16,11 +16,16 @@ // matching config_coin.hpp (namespace dgb) and the btc::coin / ltc::coin // pattern the family-1 seam binds against. +#include +#include +#include #include +#include #include #include "rpc_data.hpp" +#include "dgb_block_algo.hpp" // DGB_BLOCK_VERSION_SCRYPT (Scrypt lane pin) namespace dgb { @@ -55,6 +60,78 @@ class CoinNodeInterface { // verbatim from btc once coin/block.hpp is ported. }; + +// ── Work-template assembly SSOT (Stage 4c) ────────────────────────────────── +// build_work_template() shapes the already-resolved field values into the +// GBT-compatible JSON template that DGBWorkSource::get_current_work_template() +// returns. Lifting the assembly here makes the stratum work source and the +// embedded path emit ONE template object -- they cannot diverge once both call +// this SSOT (the same intent as routing get_current_gbt_prevhash through +// tip_hash() in Stage 4b). +// +// NON-CONSENSUS: this function only SHAPES values; it never derives or alters +// the consensus-bearing coinbasevalue. That value is computed by the caller +// through the #207 resolve_coinbase_value -> subsidy_func SSOT and passed in +// verbatim. The builder fabricates nothing: transactions[] stays empty +// (embedded mempool tx selection is not wired, fees stay 0), previousblockhash +// is emitted ONLY when the caller supplies a real tip hash (truthful absence, +// never a fabricated id), and `bits` is held back entirely -- DGB Core's live +// next-target is MultiShield V4 (a global 5-algo window == V37), so a +// Scrypt-only walk would emit a known-wrong difficulty (the same fabrication +// the empty transactions[] deliberately avoids). bits becomes a GBT +// pass-through once the external-daemon path is plumbed in. +struct WorkTemplateInputs { + // Absolute height of the NEXT block (#209 next_block_height()). + uint32_t next_height = 0; + // Reward for the next block, already resolved via the #207 SSOT. Passed in + // verbatim; the builder never recomputes or scales it. + uint64_t coinbasevalue = 0; + // DGB Core ContextualCheckBlockHeader lower bound source (median-time-past). + // INT64_MIN means an empty chain (unconstrained) -> mintime emits 0. + int64_t median_time_past = std::numeric_limits::min(); + // GBT suggested header nTime. Injected by the caller (work source: wall + // clock) so the assembly is deterministically testable. + int64_t curtime = 0; + // Tip block id as GBT big-endian display hex, already formatted by the + // caller (work source: u256_be_display_hex). nullopt -> previousblockhash + // omitted from the template. + std::optional previousblockhash; +}; + +inline nlohmann::json build_work_template(const WorkTemplateInputs& in) +{ + // version: BIP9 base | DGB Scrypt algo nibble (dgb_block_algo.hpp SSOT). A + // DGB template MUST pin the Scrypt lane -- the mining algo lives in 4 + // nVersion bits and Scrypt is the all-zero codepoint (DGB_BLOCK_VERSION_SCRYPT + // == 0x0000); any other nibble is a non-Scrypt algo this V36 binary never + // emits a template for. + static constexpr uint32_t BIP9_BASE_VERSION = 0x20000000u; + const uint32_t version = + BIP9_BASE_VERSION | + static_cast(DGB_BLOCK_VERSION_SCRYPT); + + // mintime: median_time_past()+1 (DGB Core's nTime > MTP lower bound). An + // empty chain reports INT64_MIN (unconstrained) -> emit 0. + const int64_t mintime = + (in.median_time_past == std::numeric_limits::min()) + ? 0 : (in.median_time_past + 1); + + nlohmann::json tmpl = nlohmann::json::object(); + tmpl["height"] = in.next_height; + tmpl["coinbasevalue"] = in.coinbasevalue; + tmpl["version"] = version; + tmpl["curtime"] = in.curtime; + tmpl["mintime"] = mintime; + tmpl["transactions"] = nlohmann::json::array(); + + // previousblockhash: truthful conditional emit (see struct notes). + if (in.previousblockhash) + tmpl["previousblockhash"] = *in.previousblockhash; + + return tmpl; +} + + } // namespace coin } // namespace dgb diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index c0201b0c7..43cb2333a 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -239,29 +240,21 @@ nlohmann::json DGBWorkSource::get_current_work_template() const // and the per-connection coinbase (gentx + ShareTracker ref_hash + PPLNS // payout map) assembles in build_connection_coinbase() — that output is // consensus-bearing and surfaces for an operator tap, not in this field wire. - static constexpr uint32_t BIP9_BASE_VERSION = 0x20000000u; - const uint32_t version = - BIP9_BASE_VERSION | - static_cast(dgb::coin::DGB_BLOCK_VERSION_SCRYPT); - - const int64_t mtp = chain_.median_time_past(); - const int64_t mintime = (mtp == std::numeric_limits::min()) - ? 0 : (mtp + 1); - const int64_t curtime = static_cast(std::time(nullptr)); - - nlohmann::json tmpl = nlohmann::json::object(); - tmpl["height"] = next_h; - tmpl["coinbasevalue"] = coinbasevalue; - tmpl["version"] = version; - tmpl["curtime"] = curtime; - tmpl["mintime"] = mintime; - tmpl["transactions"] = nlohmann::json::array(); - - // previousblockhash: truthful conditional emit (see field notes above). + // Shape the truthfully-derivable fields into the GBT template via the + // dgb::coin::build_work_template SSOT so the embedded path and this work + // source emit one template (Stage 4c extraction). version (Scrypt lane + // pin), mintime (MTP+1 / 0 on empty chain), curtime, empty transactions[] + // and the conditional previousblockhash all live in the builder now; this + // method only resolves the chain-state inputs. + dgb::coin::WorkTemplateInputs in; + in.next_height = next_h; + in.coinbasevalue = coinbasevalue; + in.median_time_past = chain_.median_time_past(); + in.curtime = static_cast(std::time(nullptr)); if (auto th = chain_.tip_hash()) - tmpl["previousblockhash"] = u256_be_display_hex(*th); + in.previousblockhash = u256_be_display_hex(*th); - return tmpl; + return dgb::coin::build_work_template(in); } std::vector DGBWorkSource::get_stratum_merkle_branches() const diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 3a46490f7..45ee5270d 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -142,6 +142,18 @@ if (BUILD_TESTING AND GTest_FOUND) nlohmann_json::nlohmann_json) gtest_add_tests(dgb_work_source_test "" AUTO) + # dgb_template_builder_test: guards the build_work_template SSOT + # (Stage 4c extraction). Pure JSON-shaping function -> guard-weight + # linkage: the only out-of-line dep reachable through template_builder.hpp + # -> rpc_data.hpp -> core/uint256.hpp is declarations only (no uint256 + # method is invoked), so GTest + nlohmann_json suffice. MUST also be in the build.yml + # --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_template_builder_test template_builder_test.cpp) + target_link_libraries(dgb_template_builder_test PRIVATE + GTest::gtest_main GTest::gtest + nlohmann_json::nlohmann_json) + gtest_add_tests(dgb_template_builder_test "" AUTO) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test) add_executable(${dgb_guard} ${dgb_guard}.cpp) diff --git a/src/impl/dgb/test/template_builder_test.cpp b/src/impl/dgb/test/template_builder_test.cpp new file mode 100644 index 000000000..8b21422aa --- /dev/null +++ b/src/impl/dgb/test/template_builder_test.cpp @@ -0,0 +1,121 @@ +// dgb_template_builder_test — guards the dgb::coin::build_work_template SSOT +// (Stage 4c extraction). The work-template assembly was lifted out of +// DGBWorkSource::get_current_work_template() into coin/template_builder.hpp so +// the stratum work source and the embedded path emit ONE template object and +// cannot diverge. These tests pin the truthfulness invariants the assembly +// must hold: the Scrypt lane is pinned, the consensus coinbasevalue passes +// through verbatim, mintime tracks median-time-past (0 on an empty chain), +// transactions[] stays empty, bits is never emitted, and previousblockhash is +// a truthful conditional (present only when a real tip hash is supplied). +// +// Pure-shaping function -> guard-weight test (no chain/mempool fixture). + +#include +#include +#include +#include + +#include +#include + +#include +#include + +using dgb::coin::WorkTemplateInputs; +using dgb::coin::build_work_template; + +namespace { + +// A fully-populated set of inputs mirroring a mid-chain template build. +WorkTemplateInputs make_inputs() +{ + WorkTemplateInputs in; + in.next_height = 17722; + in.coinbasevalue = 625000000ULL; // arbitrary post-resolve reward + in.median_time_past = 1750000000; // a concrete MTP + in.curtime = 1750000075; // MTP + ~one block period + in.previousblockhash = + "00000000000000000a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778"; + return in; +} + +} // namespace + +// version MUST be BIP9 base OR'd with the all-zero Scrypt algo nibble. A DGB +// template that is not Scrypt-pinned would be a block this V36 binary never mines. +TEST(DgbTemplateBuilder, ScryptLanePinnedVersion) +{ + auto t = build_work_template(make_inputs()); + static constexpr uint32_t BIP9_BASE = 0x20000000u; + const uint32_t expect = + BIP9_BASE | static_cast(dgb::coin::DGB_BLOCK_VERSION_SCRYPT); + EXPECT_EQ(t.at("version").get(), expect); + EXPECT_EQ(expect, 0x20000000u); // Scrypt == all-zero codepoint + // The version nibble must classify back to Scrypt through the SSOT. + EXPECT_TRUE(dgb::coin::is_scrypt_header(t.at("version").get())); +} + +// The consensus-bearing reward is resolved upstream (#207 SSOT) and must pass +// through the builder byte-for-byte — the builder never recomputes or scales it. +TEST(DgbTemplateBuilder, CoinbaseValuePassesThroughVerbatim) +{ + auto in = make_inputs(); + in.coinbasevalue = 0xCAFEBABEDEADBEEFULL; + auto t = build_work_template(in); + EXPECT_EQ(t.at("coinbasevalue").get(), 0xCAFEBABEDEADBEEFULL); + EXPECT_EQ(t.at("height").get(), in.next_height); +} + +// mintime = median_time_past + 1 (DGB Core's nTime > MTP lower bound). +TEST(DgbTemplateBuilder, MintimeIsMtpPlusOne) +{ + auto in = make_inputs(); + in.median_time_past = 1750000000; + auto t = build_work_template(in); + EXPECT_EQ(t.at("mintime").get(), 1750000001); + EXPECT_EQ(t.at("curtime").get(), in.curtime); +} + +// Empty chain reports MTP == INT64_MIN (unconstrained) -> mintime emits 0, +// never INT64_MIN+1 (which would underflow-adjacent and be meaningless to GBT). +TEST(DgbTemplateBuilder, EmptyChainMintimeIsZero) +{ + auto in = make_inputs(); + in.median_time_past = std::numeric_limits::min(); + auto t = build_work_template(in); + EXPECT_EQ(t.at("mintime").get(), 0); +} + +// transactions[] is always an empty array (embedded tx selection unwired); +// bits is NEVER emitted (MultiShield V4 next-target is V37 — a Scrypt-only +// walk would be a known-wrong difficulty). +TEST(DgbTemplateBuilder, EmptyTransactionsAndNoBits) +{ + auto t = build_work_template(make_inputs()); + ASSERT_TRUE(t.at("transactions").is_array()); + EXPECT_TRUE(t.at("transactions").empty()); + EXPECT_FALSE(t.contains("bits")); +} + +// previousblockhash is a truthful conditional: emitted verbatim when a real +// tip hash is supplied, omitted entirely when absent (never a fabricated id). +TEST(DgbTemplateBuilder, PreviousblockhashConditionalEmit) +{ + auto with = build_work_template(make_inputs()); + ASSERT_TRUE(with.contains("previousblockhash")); + EXPECT_EQ(with.at("previousblockhash").get(), + make_inputs().previousblockhash.value()); + + auto in = make_inputs(); + in.previousblockhash = std::nullopt; + auto without = build_work_template(in); + EXPECT_FALSE(without.contains("previousblockhash")); +} + +// Divergence guard: identical inputs -> byte-identical template. This is the +// whole point of the SSOT — the work source and embedded path cannot diverge. +TEST(DgbTemplateBuilder, DeterministicForIdenticalInputs) +{ + EXPECT_EQ(build_work_template(make_inputs()).dump(), + build_work_template(make_inputs()).dump()); +}