diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d4976a00..3bb5a98eb 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 nmc_template_builder_test nmc_auxpow_wire_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 dgb_embedded_coin_node_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 nmc_template_builder_test nmc_auxpow_wire_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 dgb_embedded_coin_node_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/embedded_coin_node.hpp b/src/impl/dgb/coin/embedded_coin_node.hpp new file mode 100644 index 000000000..6fcc1f54c --- /dev/null +++ b/src/impl/dgb/coin/embedded_coin_node.hpp @@ -0,0 +1,112 @@ +#pragma once +// =========================================================================== +// dgb::coin::EmbeddedCoinNode -- embedded (no external digibyted) work source. +// +// Implements CoinNodeInterface::getwork() (coin/template_builder.hpp) by +// assembling a GBT-compatible work template ENTIRELY from in-process embedded +// chain state -- the HeaderChain (coin/header_chain.hpp) plus the coin's +// subsidy schedule -- with NO external RPC. This is the SECOND caller of +// dgb::coin::build_work_template(): the stratum work source +// (DGBWorkSource::get_current_work_template, stratum/work_source.cpp) is the +// first. Routing BOTH the miner-facing stratum path and the embedded +// CoinNodeInterface path through the one build_work_template SSOT is what makes +// the "the two paths cannot emit a divergent template" guarantee CONCRETE +// rather than theoretical (integrator directive, 2026-06-19). +// +// CONSENSUS DISCIPLINE (identical to build_work_template + the stratum caller): +// * coinbasevalue is resolved through the #207 resolve_coinbase_value SSOT. +// On the embedded path there is no external GBT figure, so it is derived +// as subsidy_func(height) + total_fees via embedded_coinbase_value.hpp. +// The external-daemon GBT fallback (NodeRPC::getwork) is a SEPARATE path +// and is untouched -- this node is the no-external-daemon source only. +// * version pins the Scrypt lane (build_work_template, DGB_BLOCK_VERSION_SCRYPT). +// * transactions[] stays empty and total_fees is 0: embedded mempool tx +// selection is not wired yet, so the template fabricates no transactions +// (the same truthful-absence discipline the stratum caller keeps). +// * previousblockhash is emitted ONLY when HeaderChain::tip_hash() carries a +// real tip id, rendered through the coin/hash_format.hpp SSOT so it is +// byte-identical to the stratum caller's previousblockhash. +// * bits is HELD BACK (MultiShield V4 next-target is a 5-algo global window +// == V37; a Scrypt-only walk would emit a known-wrong difficulty). It +// becomes a GBT pass-through once an external-daemon bits source is plumbed. +// +// Header-only + pure w.r.t. its inputs: make_inputs(curtime) is split out so +// the assembly is deterministically unit-testable without a wall clock or a +// running node; getwork() supplies std::time(nullptr) in production exactly as +// the stratum caller does. +// =========================================================================== + +#include +#include +#include + +#include // core::SubsidyFunc + +#include "header_chain.hpp" // c2pool::dgb::HeaderChain +#include "hash_format.hpp" // u256_be_display_hex SSOT +#include "embedded_coinbase_value.hpp" // resolve_coinbase_value SSOT (#207) +#include "template_builder.hpp" // build_work_template SSOT + CoinNodeInterface +#include "rpc_data.hpp" // rpc::WorkData + +namespace dgb::coin +{ + +class EmbeddedCoinNode : public CoinNodeInterface +{ +public: + EmbeddedCoinNode(c2pool::dgb::HeaderChain& chain, + core::SubsidyFunc subsidy_func) + : m_chain(chain), m_subsidy_func(std::move(subsidy_func)) + { + } + + // Assemble the build_work_template inputs from the live embedded chain + // state. Split from getwork() so the curtime is injectable for + // deterministic tests (the stratum caller does the same with std::time). + WorkTemplateInputs make_inputs(int64_t curtime) const + { + const uint32_t height = m_chain.next_block_height(); + + WorkTemplateInputs in; + in.next_height = height; + // Embedded path: no external GBT value -> derive subsidy(height)+fees. + // total_fees is 0 until embedded mempool tx selection is wired. + in.coinbasevalue = resolve_coinbase_value(m_subsidy_func, height, + /*total_fees=*/0, + /*gbt_coinbasevalue=*/std::nullopt); + in.median_time_past = m_chain.median_time_past(); + in.curtime = curtime; + if (auto th = m_chain.tip_hash()) + in.previousblockhash = u256_be_display_hex(*th); + return in; + } + + // CoinNodeInterface: produce a WorkData carrying the embedded template. + // m_hashes stays empty (transactions[] is empty), m_latency 0 (no RPC). + rpc::WorkData getwork() override + { + rpc::WorkData wd; + wd.m_data = build_work_template(make_inputs(std::time(nullptr))); + return wd; + } + + // Minimal chain-info shaped like getblockchaininfo's relevant fields. The + // embedded chain reports its absolute tip height (0 on an empty chain). + nlohmann::json getblockchaininfo() override + { + nlohmann::json info = nlohmann::json::object(); + info["blocks"] = m_chain.tip_height().value_or(0); + return info; + } + + // Not synced until the embedded P2P header-download ingest is wired (it is + // what populates HeaderSample.block_hash and gives real UTXO depth). A + // truthful false, never an optimistic claim of readiness. + bool is_synced() const override { return false; } + +private: + c2pool::dgb::HeaderChain& m_chain; + core::SubsidyFunc m_subsidy_func; +}; + +} // namespace dgb::coin diff --git a/src/impl/dgb/coin/hash_format.hpp b/src/impl/dgb/coin/hash_format.hpp new file mode 100644 index 000000000..641856d43 --- /dev/null +++ b/src/impl/dgb/coin/hash_format.hpp @@ -0,0 +1,41 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::u256_be_display_hex -- SSOT for rendering 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 (coin/dgb_arith256.hpp) has no GetHex(), and the consuming +// TUs must not depend on btclibs' uint256, so the limbs are formatted directly. +// +// SSOT rationale: the stratum work source (stratum/work_source.cpp, +// previousblockhash + get_current_gbt_prevhash) and the embedded work path +// (coin/embedded_coin_node.hpp) both render the tip hash for the GBT template. +// Drawing both from ONE formatter means the two callers of build_work_template +// can never emit a previousblockhash in a divergent byte-encoding -- the same +// "cannot diverge" intent build_work_template itself enforces for the template +// body. +// --------------------------------------------------------------------------- + +#include +#include + +#include "dgb_arith256.hpp" // dgb::coin::u256 + +namespace dgb::coin +{ + +inline std::string u256_be_display_hex(const 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 dgb::coin 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/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 d191dd13e..52b0e1dfb 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -19,14 +19,27 @@ #include #include #include +#include +#include #include #include +#include #include namespace dgb::stratum { +namespace { + +// previousblockhash big-endian display-hex rendering is the dgb::coin SSOT +// (coin/hash_format.hpp), shared with the embedded work path +// (coin/embedded_coin_node.hpp) so the two build_work_template callers +// cannot emit a divergent previousblockhash encoding. +using dgb::coin::u256_be_display_hex; + +} // namespace + DGBWorkSource::DGBWorkSource(c2pool::dgb::HeaderChain& chain, dgb::coin::Mempool& mempool, bool is_testnet, @@ -84,7 +97,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 {}; } @@ -184,32 +206,44 @@ 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. - 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(); - return tmpl; + // 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()) + in.previousblockhash = u256_be_display_hex(*th); + + 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..928d8a0ea 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -142,6 +142,32 @@ 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) + + # dgb_embedded_coin_node_test: guards EmbeddedCoinNode, the SECOND caller + # of the build_work_template SSOT (proves the embedded + stratum work + # paths cannot diverge). Pulls header_chain.hpp + embedded_coinbase_value.hpp + # + core/pow.hpp -> links the same proven SCC set as dgb_work_source_test. + # MUST also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap). + add_executable(dgb_embedded_coin_node_test embedded_coin_node_test.cpp) + target_link_libraries(dgb_embedded_coin_node_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb dgb_stratum + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain + nlohmann_json::nlohmann_json) + gtest_add_tests(dgb_embedded_coin_node_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/embedded_coin_node_test.cpp b/src/impl/dgb/test/embedded_coin_node_test.cpp new file mode 100644 index 000000000..6d6dd3e39 --- /dev/null +++ b/src/impl/dgb/test/embedded_coin_node_test.cpp @@ -0,0 +1,137 @@ +// --------------------------------------------------------------------------- +// dgb_embedded_coin_node_test -- guards dgb::coin::EmbeddedCoinNode, the +// SECOND caller of the build_work_template SSOT (the stratum DGBWorkSource is +// the first). Proving build_work_template has a real embedded caller is what +// turns its "the two work paths cannot diverge" claim from theoretical into +// concrete (integrator directive, 2026-06-19). +// +// What it pins: +// 1. SSOT routing -- getwork()/make_inputs() emit EXACTLY what +// build_work_template() emits for the same chain-derived inputs (the +// embedded node fabricates nothing of its own). +// 2. Consensus pass-through -- coinbasevalue is resolved via the #207 +// resolve_coinbase_value -> subsidy_func SSOT (embedded path, no GBT). +// 3. Truthful absence -- previousblockhash is held back on a chain with no +// real tip hash and emitted (byte-identical to the hash_format.hpp SSOT) +// once the tip carries a block_hash; transactions[] stays empty and bits +// is never present. +// +// Links the same proven SCC set as dgb_work_source_test (header_chain.hpp + +// embedded_coinbase_value.hpp + core/pow.hpp reach the core/dgb libs). MUST +// also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap). +// --------------------------------------------------------------------------- + +#include +#include +#include + +#include + +#include +#include +#include + +using c2pool::dgb::HeaderChain; +using c2pool::dgb::HeaderSample; +using dgb::coin::EmbeddedCoinNode; +using dgb::coin::WorkTemplateInputs; +using dgb::coin::build_work_template; +using dgb::coin::u256_be_display_hex; +using dgb::coin::DGB_BLOCK_VERSION_SCRYPT; + +namespace { + +// Header nVersion with the Scrypt algo nibble (PRIMARY default | Scrypt==0). +constexpr int32_t SCRYPT = 2 | DGB_BLOCK_VERSION_SCRYPT; + +// Deterministic stand-in for CoinParams::subsidy_func; the real schedule is +// oracle-conformed in test_dgb_subsidy.cpp. Here we only need a known mapping +// to prove the value flows through resolve_coinbase_value verbatim. +core::SubsidyFunc make_subsidy() { + return [](uint32_t height) -> uint64_t { return 1000ull + height; }; +} + +// Expected GBT template version (build_work_template's pin). +constexpr uint32_t EXPECTED_VERSION = + 0x20000000u | static_cast(DGB_BLOCK_VERSION_SCRYPT); + +} // namespace + +// 1. make_inputs() + build_work_template() == the node's emitted template. +TEST(DgbEmbeddedCoinNode, RoutesThroughBuildWorkTemplateSSOT) +{ + HeaderChain hc; + hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100}); // one Scrypt tip + + EmbeddedCoinNode node(hc, make_subsidy()); + + const int64_t curtime = 1718800000; + const WorkTemplateInputs in = node.make_inputs(curtime); + const nlohmann::json ssot = build_work_template(in); + + // The node, fed the same curtime, must reproduce the SSOT byte-for-byte. + EXPECT_EQ(node.make_inputs(curtime).next_height, in.next_height); + EXPECT_EQ(build_work_template(node.make_inputs(curtime)).dump(), ssot.dump()); +} + +// 2. Chain-derived fields + consensus pass-through. +TEST(DgbEmbeddedCoinNode, FieldsAndCoinbaseValueFromChainSSOT) +{ + HeaderChain hc; + hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100}); + + EmbeddedCoinNode node(hc, make_subsidy()); + const WorkTemplateInputs in = node.make_inputs(/*curtime=*/42); + const nlohmann::json t = build_work_template(in); + + const uint32_t h = hc.next_block_height(); + EXPECT_EQ(t["height"].get(), h); + EXPECT_EQ(t["coinbasevalue"].get(), 1000ull + h); // subsidy_func(h)+0 fees + EXPECT_EQ(t["version"].get(), EXPECTED_VERSION); + EXPECT_EQ(t["mintime"].get(), hc.median_time_past() + 1); + EXPECT_EQ(t["curtime"].get(), 42); + EXPECT_TRUE(t["transactions"].is_array()); + EXPECT_TRUE(t["transactions"].empty()); + EXPECT_FALSE(t.contains("bits")); // held back (MultiShield V4 == V37) +} + +// 3a. No real tip hash -> previousblockhash held back (truthful absence). +TEST(DgbEmbeddedCoinNode, PrevhashHeldBackWithoutRealTipHash) +{ + HeaderChain hc; // empty: tip_hash() == nullopt + EmbeddedCoinNode node(hc, make_subsidy()); + EXPECT_FALSE(build_work_template(node.make_inputs(0)).contains("previousblockhash")); + + // Appended tip with block_hash==0 sentinel is still "no real tip hash". + hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100}); + EXPECT_FALSE(build_work_template(node.make_inputs(0)).contains("previousblockhash")); +} + +// 3b. Real tip hash -> previousblockhash emitted via the hash_format SSOT. +TEST(DgbEmbeddedCoinNode, PrevhashEmittedByteIdenticalToHashFormatSSOT) +{ + HeaderChain hc; + HeaderSample s{SCRYPT, 1000, 100}; + s.block_hash = dgb::coin::u256(0xdeadbeefull); // populate a real tip id + hc.validate_and_append(s); + + EmbeddedCoinNode node(hc, make_subsidy()); + const nlohmann::json t = build_work_template(node.make_inputs(0)); + ASSERT_TRUE(t.contains("previousblockhash")); + EXPECT_EQ(t["previousblockhash"].get(), + u256_be_display_hex(dgb::coin::u256(0xdeadbeefull))); +} + +// 4. getwork() returns a non-fabricated WorkData (template body only). +TEST(DgbEmbeddedCoinNode, GetworkProducesTemplateWithNoFabrication) +{ + HeaderChain hc; + hc.validate_and_append(HeaderSample{SCRYPT, 1000, 100}); + + EmbeddedCoinNode node(hc, make_subsidy()); + const auto wd = node.getwork(); + EXPECT_EQ(wd.m_data["version"].get(), EXPECTED_VERSION); + EXPECT_TRUE(wd.m_data["transactions"].empty()); + EXPECT_TRUE(wd.m_hashes.empty()); // no tx hashes fabricated + EXPECT_FALSE(node.is_synced()); // truthful: not synced yet +} 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/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()); +} diff --git a/src/impl/dgb/test/work_source_test.cpp b/src/impl/dgb/test/work_source_test.cpp index 024c0e617..03605f6dc 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -282,4 +282,67 @@ 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")); +} + +// 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