From 30a6d78dccfaf6e4c07d4ba68a135953a3aa8ead Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 14:59:01 +0000 Subject: [PATCH] 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