diff --git a/src/impl/dgb/stratum/work_source.cpp b/src/impl/dgb/stratum/work_source.cpp index af4465f79..2aa5b009c 100644 --- a/src/impl/dgb/stratum/work_source.cpp +++ b/src/impl/dgb/stratum/work_source.cpp @@ -25,6 +25,7 @@ #include #include // scrypt_pow_hash (DGB-Scrypt PoW SSOT) #include // classify_submission (Stage-4d decision SSOT) +#include // build_connection_coinbase_from_pplns SSOT #include // compact_to_target (compact bits -> u256) #include @@ -333,16 +334,49 @@ std::pair DGBWorkSource::get_coinbase_parts() const } core::stratum::CoinbaseResult DGBWorkSource::build_connection_coinbase( - const uint256& /*prev_share_hash*/, - const std::string& /*extranonce1_hex*/, - const std::vector& /*payout_script*/, - const std::vector>>& /*merged_addrs*/) const + const uint256& prev_share_hash, + const std::string& extranonce1_hex, + const std::vector& payout_script, + const std::vector>>& merged_addrs) const { - // Stage 4c: build per-connection coinbase using the SSOT gentx assembler - // (coin/gentx_coinbase.hpp) + ShareTracker ref_hash + PPLNS payout map. - // For now return an empty result; sessions calling this will get an empty - // job and skip pushing work, which is safe but non-functional. - return {}; + // Phase B live-wire: delegate to the PPLNS->coinbase SSOT + // (build_connection_coinbase_from_pplns), which itself routes through the + // single compute_pplns_payout_split() the verifier uses -- so the coinbase a + // miner hashes here is byte-identical to the one generate_share_transaction() + // enforces, by construction (no second payout implementation to keep in sync). + // + // The PPLNS inputs (weight map walked from the ShareTracker, ref_hash, + // subsidy, donation script) are produced by the seam bound in main_dgb.cpp + // -- the tracker walk lives there, not in the work source. While the seam is + // UNBOUND (or returns nullopt) this is byte-identical to the pre-wire stub: + // an empty result, so the session pushes no work (safe, non-functional). + PplnsInputsFn fn; + { + std::lock_guard lk(pplns_inputs_mutex_); + fn = pplns_inputs_fn_; // copy under lock; a concurrent set_*_fn() cannot + // tear it out mid-call. + } + if (!fn) + return {}; // unbound: pre-wire behavior (empty job). + + std::optional inputs = + fn(prev_share_hash, extranonce1_hex, payout_script, merged_addrs); + if (!inputs) + return {}; // producer declined (e.g. tip not yet known) -> safe empty job. + + dgb::coin::ConnCoinbaseParts parts = + dgb::coin::build_connection_coinbase_from_pplns(*inputs); + + core::stratum::CoinbaseResult out; + out.coinb1 = std::move(parts.coinb1); + out.coinb2 = std::move(parts.coinb2); + // Freeze the consensus-bearing ref fields the submit path (mining_submit) + // re-derives the won-block reconstruct from; the remaining snapshot fields + // (merkle_branches / segwit) are populated by the template-cache follow-on. + out.snapshot.subsidy = inputs->subsidy; + out.snapshot.frozen_ref.ref_hash = inputs->ref_hash; + out.snapshot.frozen_ref.last_txout_nonce = inputs->last_txout_nonce; + return out; } // ───────────────────────────────────────────────────────────────────────────── @@ -610,6 +644,12 @@ void DGBWorkSource::set_fallback_payout_fn(FallbackPayoutFn fn) fallback_payout_fn_ = std::move(fn); } +void DGBWorkSource::set_pplns_inputs_fn(PplnsInputsFn fn) +{ + std::lock_guard lk(pplns_inputs_mutex_); + pplns_inputs_fn_ = std::move(fn); +} + 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 c4a592d9b..c73228ef3 100644 --- a/src/impl/dgb/stratum/work_source.hpp +++ b/src/impl/dgb/stratum/work_source.hpp @@ -42,6 +42,7 @@ #include #include #include // core::SubsidyFunc — embedded coinbasevalue SSOT feed +#include // ConnCoinbasePplnsInputs (PPLNS->coinbase SSOT) #include #include @@ -209,6 +210,26 @@ class DGBWorkSource : public core::stratum::IWorkSource using FallbackPayoutFn = std::function()>; void set_fallback_payout_fn(FallbackPayoutFn fn); + /// Producer seam for the per-connection coinbase (Phase B live-wire). + /// Given the share-chain tip a miner builds ON TOP OF, plus this session's + /// extranonce1 and resolved payout/merged scripts, returns the fully + /// populated PPLNS inputs (weight map sourced from the ShareTracker, ref_hash, + /// subsidy, donation script). Bound once at startup in main_dgb.cpp where the + /// ShareTracker + CoinParams are in scope; the tracker walk lives THERE so no + /// sharechain logic leaks into the work source. While UNBOUND, + /// build_connection_coinbase() returns an empty result (byte-identical to the + /// pre-wire stub -- no behavior change until the producer is bound). Returning + /// std::nullopt (e.g. tip not yet known) also yields an empty, safe job. The + /// emitted coinbase is byte-identical to the verifier's + /// generate_share_transaction() BY CONSTRUCTION: both delegate to the single + /// compute_pplns_payout_split() SSOT via build_connection_coinbase_from_pplns. + using PplnsInputsFn = std::function( + const uint256& prev_share_hash, + const std::string& extranonce1_hex, + const std::vector& payout_script, + const std::vector>>& merged_addrs)>; + void set_pplns_inputs_fn(PplnsInputsFn 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 @@ -269,6 +290,12 @@ class DGBWorkSource : public core::stratum::IWorkSource mutable std::mutex fallback_payout_mutex_; FallbackPayoutFn fallback_payout_fn_; + // Per-connection coinbase PPLNS-inputs producer (#327/#330 live-wire). + // Empty until set_pplns_inputs_fn() bound in main_dgb; while empty the + // per-connection coinbase path returns an empty job (pre-wire behavior). + mutable std::mutex pplns_inputs_mutex_; + PplnsInputsFn pplns_inputs_fn_; + // 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 89294104e..941d65344 100644 --- a/src/impl/dgb/test/work_source_test.cpp +++ b/src/impl/dgb/test/work_source_test.cpp @@ -16,6 +16,7 @@ #include #include #include +#include // build_connection_coinbase_from_pplns (SSOT under test) #include // dgb::CoinParams::subsidy (oracle SSOT) #include // core::SubsidyFunc @@ -24,6 +25,7 @@ #include +#include #include #include @@ -520,4 +522,89 @@ TEST(DgbWorkSource, GbtPrevhashGetterMatchesTemplateField) expected); } + +// ── Per-connection coinbase live-wire (set_pplns_inputs_fn / build_connection_coinbase) ── +// The producer half of the Phase-B coinbase wire: build_connection_coinbase +// delegates to the build_connection_coinbase_from_pplns SSOT (which the verifier +// also calls). These pin three contracts: (1) UNBOUND -> empty job (pre-wire +// byte-identical no-op), (2) bound -> coinb1/coinb2 byte-identical to the SSOT +// called directly with the same inputs (proving build_connection_coinbase is a +// pure pass-through, not a second payout implementation), (3) producer nullopt +// -> empty job. +namespace { + +using Script = std::vector; + +// A fixed, fully-populated PPLNS input set (two payout scripts, v36 no-finder). +dgb::coin::ConnCoinbasePplnsInputs sample_pplns_inputs() +{ + dgb::coin::ConnCoinbasePplnsInputs in; + in.coinbase_script = Script{0x03, 0x01, 0x02, 0x03}; // BIP34-ish scriptSig + in.weights = { {Script{0x76, 0xa9, 0x14, 0xaa}, uint288(3)}, + {Script{0x76, 0xa9, 0x14, 0xbb}, uint288(1)} }; + in.total_weight = uint288(4); + in.subsidy = 1234567; + in.use_v36_pplns = true; + in.donation_script = Script{0xa9, 0x14, 0xcc}; + in.ref_hash = uint256(std::vector(32, 0xab)); + in.last_txout_nonce = 0x0102030405060708ULL; + return in; +} + +} // namespace + +TEST(DgbWorkSource, ConnectionCoinbaseEmptyUntilPplnsInputsWired) +{ + Fixture fx; + auto ws = fx.make(); + auto r = ws->build_connection_coinbase( + uint256::ZERO, "deadbeef", Script{}, {}); + EXPECT_TRUE(r.coinb1.empty()); + EXPECT_TRUE(r.coinb2.empty()); +} + +TEST(DgbWorkSource, ConnectionCoinbaseDelegatesToPplnsSsotByteIdentical) +{ + Fixture fx; + auto ws = fx.make(); + const auto inputs = sample_pplns_inputs(); + + // Bind a producer that hands back the fixed inputs regardless of args. + ws->set_pplns_inputs_fn( + [&](const uint256&, const std::string&, const Script&, + const std::vector>&) + -> std::optional { + return inputs; + }); + + auto wired = ws->build_connection_coinbase( + uint256(std::vector(32, 0x11)), "cafef00d", Script{0x01}, {}); + + // The SSOT called directly with the same inputs is the oracle. + auto oracle = dgb::coin::build_connection_coinbase_from_pplns(inputs); + + EXPECT_EQ(wired.coinb1, oracle.coinb1); + EXPECT_EQ(wired.coinb2, oracle.coinb2); + EXPECT_FALSE(wired.coinb1.empty()); + // Consensus ref fields are frozen onto the snapshot for the submit path. + EXPECT_EQ(wired.snapshot.frozen_ref.ref_hash, inputs.ref_hash); + EXPECT_EQ(wired.snapshot.frozen_ref.last_txout_nonce, inputs.last_txout_nonce); + EXPECT_EQ(wired.snapshot.subsidy, inputs.subsidy); +} + +TEST(DgbWorkSource, ConnectionCoinbaseProducerNulloptYieldsEmptyJob) +{ + Fixture fx; + auto ws = fx.make(); + ws->set_pplns_inputs_fn( + [&](const uint256&, const std::string&, const Script&, + const std::vector>&) + -> std::optional { + return std::nullopt; // tip not yet known + }); + auto r = ws->build_connection_coinbase(uint256::ZERO, "00", Script{}, {}); + EXPECT_TRUE(r.coinb1.empty()); + EXPECT_TRUE(r.coinb2.empty()); +} + } // namespace