diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index c07029160..d191dd13e 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -18,9 +18,13 @@ #include #include #include +#include #include +#include +#include + namespace dgb::stratum { DGBWorkSource::DGBWorkSource(c2pool::dgb::HeaderChain& chain, @@ -148,11 +152,64 @@ void DGBWorkSource::update_stratum_worker(const std::string& session_id, nlohmann::json DGBWorkSource::get_current_work_template() const { - // Stage 4c: drive dgb::coin::TemplateBuilder over chain_ + mempool_ and - // shape its WorkData into the GBT-style JSON the stratum session expects - // (previousblockhash, bits, version, curtime, mintime, height, - // coinbasevalue, transactions[]). Scrypt nBits, not SHA256d. - return nlohmann::json::object(); + // Stage 4c (coinbasevalue wire): the full GBT-shaped template + // (previousblockhash, bits, version, curtime, mintime, transactions[]) + // is assembled by the embedded dgb::coin::TemplateBuilder in a following + // slice. What lands here is the consensus-bearing reward field: the + // coinbasevalue for the NEXT block, derived through the #207 SSOT + // (coinbase_value -> resolve_coinbase_value -> subsidy_func) keyed on the + // absolute next-block height from the #209 HeaderChain accessor + // (next_block_height() == tip_height()+1, or base_height for an empty + // chain). Embedded path: no external-daemon GBT value is plumbed in yet + // and mempool-fee aggregation is not wired, so total_fees = 0 here. Both + // compose in later slices WITHOUT changing this SSOT call (a present GBT + // coinbasevalue stays authoritative; fees add on top of subsidy). + const uint32_t next_h = chain_.next_block_height(); + const uint64_t coinbasevalue = + coinbase_value(next_h, /*total_fees=*/0, /*gbt_coinbasevalue=*/std::nullopt); + + // GBT-scaffold fields the embedded path can derive TRUTHFULLY from current + // chain state, ahead of a full dgb::coin::TemplateBuilder port (M3 TODO): + // version — BIP9 base | the DGB Scrypt algo nibble. A DGB block + // template MUST pin the Scrypt lane: the mining algo lives + // in 4 nVersion bits (coin/dgb_block_algo.hpp SSOT) and + // Scrypt is the all-zero codepoint (DGB_BLOCK_VERSION_SCRYPT + // == 0x0000); any other nibble is a non-Scrypt algo that is + // accept-by-continuity / V37 here, never a template we emit. + // curtime — current wall-clock; GBT's suggested header nTime. + // mintime — median_time_past()+1 (#209 accessor): DGB Core's + // ContextualCheckBlockHeader lower bound (nTime > MTP). An + // empty chain returns INT64_MIN (unconstrained) -> emit 0. + // transactions — empty array: embedded mempool tx SELECTION is not wired, + // 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 + // 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; } std::vector DGBWorkSource::get_stratum_merkle_branches() const diff --git a/src/impl/dgb/test/work_source_test.cpp b/src/impl/dgb/test/work_source_test.cpp index 6d0dc6f82..024c0e617 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -135,8 +135,10 @@ TEST(DgbWorkSource, WorkGenStubsReturnSafeDefaults) // 4a skeleton: every work-generation getter returns its documented // empty/default form (4c fills them in). EXPECT_TRUE(ws->get_current_gbt_prevhash().empty()); + // get_current_work_template() now emits height + coinbasevalue (Stage 4c + // coinbasevalue wire); its dedicated assertions live in + // WorkTemplateEmitsHeightAndCoinbaseValueViaSsot below. EXPECT_TRUE(ws->get_current_work_template().is_object()); - EXPECT_TRUE(ws->get_current_work_template().empty()); EXPECT_TRUE(ws->get_stratum_merkle_branches().empty()); auto parts = ws->get_coinbase_parts(); EXPECT_TRUE(parts.first.empty()); @@ -174,6 +176,61 @@ TEST(DgbWorkSource, ComputeShareDifficultyReturnsNotYetSentinel) EXPECT_DOUBLE_EQ(diff, 0.0); } +// 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 +// empty chain makes next_block_height() == base_height, so seeding an oracle +// era boundary pins the value unambiguously to the p2pool-dgb-scrypt subsidy. +TEST(DgbWorkSource, WorkTemplateEmitsHeightAndCoinbaseValueViaSsot) +{ + Fixture fx; + fx.chain.set_base_height(400000); // phase3 first block (oracle boundary) + auto ws = fx.make(); + auto tmpl = ws->get_current_work_template(); + ASSERT_TRUE(tmpl.is_object()); + ASSERT_TRUE(tmpl.contains("height")); + ASSERT_TRUE(tmpl.contains("coinbasevalue")); + // next_h = next_block_height() = base_height (empty chain) = 400000. + EXPECT_EQ(tmpl["height"].get(), 400000u); + // Zero embedded fees, no external GBT -> oracle subsidy at the boundary. + EXPECT_EQ(tmpl["coinbasevalue"].get(), 2434410000ULL); +} + +// Stage 4c GBT scaffold: alongside height + coinbasevalue, the work template +// now surfaces the GBT fields the embedded path can derive truthfully without +// a TemplateBuilder port -- version (Scrypt algo lane), curtime, mintime, and +// an (empty) transactions[]. previousblockhash + bits intentionally stay absent +// until HeaderSample carries the tip hash / next-target compact (later slices). +TEST(DgbWorkSource, WorkTemplateEmitsGbtScaffoldFields) +{ + Fixture fx; + fx.chain.set_base_height(400000); + auto ws = fx.make(); + auto tmpl = ws->get_current_work_template(); + ASSERT_TRUE(tmpl.is_object()); + + // version pins the DGB Scrypt lane: BIP9 base | algo nibble 0x0000. + ASSERT_TRUE(tmpl.contains("version")); + EXPECT_EQ(tmpl["version"].get(), 0x20000000u); + + // Empty chain -> median_time_past() == INT64_MIN -> mintime emitted as 0 + // (unconstrained), and curtime is a real wall-clock stamp (>= 0). + ASSERT_TRUE(tmpl.contains("mintime")); + EXPECT_EQ(tmpl["mintime"].get(), 0); + ASSERT_TRUE(tmpl.contains("curtime")); + EXPECT_GE(tmpl["curtime"].get(), 0); + + // No embedded tx selection yet -> transactions[] present but empty (no + // fabricated entries; consistent with the total_fees=0 coinbasevalue). + ASSERT_TRUE(tmpl.contains("transactions")); + EXPECT_TRUE(tmpl["transactions"].is_array()); + EXPECT_TRUE(tmpl["transactions"].empty()); + + // The two hash/difficulty fields are deliberately NOT emitted yet. + EXPECT_FALSE(tmpl.contains("previousblockhash")); + EXPECT_FALSE(tmpl.contains("bits")); +} + // ── Embedded coinbasevalue: first production caller of subsidy_func ────────── // One pin on each side of every DGB reward-era boundary (p2pool-dgb-scrypt // oracle vectors, test_dgb_subsidy.cpp).