Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/c2pool/main_dgb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,58 @@ int run_node(const core::CoinParams& params, bool testnet,
header_chain, mempool, testnet, std::move(stratum_submit_fn),
params.subsidy_func);

// -- External-daemon GBT tip fallback bind (the fallback V36 mandates PERSIST)
// While the embedded HeaderChain is unfed (tip_hash()==nullopt -- the live
// state of a freshly-stood-up :5025 node) the Scrypt-only walk supplies
// NEITHER previousblockhash NOR the MultiShield-V4 bits. Bind the RPC arm so
// get_current_work_template() + get_current_gbt_prevhash() (the mining.notify
// prevhash source) draw both from digibyted getblocktemplate. The RPC
// transport stays in main (mirrors the #82 submit arm + dash's #726
// dashd_fallback); stratum/ holds only the std::function seam. UNARMED (no
// digibyte.conf creds) -> seam left unbound -> truthful absence, byte-identical
// to the daemon-less default build. A real embedded tip always wins over this.
if (rpc) {
auto* rpc_ptr = rpc.get();
work_source->set_gbt_tip_fn(
[rpc_ptr]() -> std::optional<dgb::stratum::DGBWorkSource::GbtTip> {
try {
// DGB GBT: "segwit" BIP9 rule; NodeRPC::getblocktemplate injects
// the mandatory separate Scrypt "algo" param (V36 Scrypt-only).
nlohmann::json tmpl = rpc_ptr->getblocktemplate({"segwit"});
if (!tmpl.is_object())
return std::nullopt;
auto ph = tmpl.find("previousblockhash");
auto bt = tmpl.find("bits");
if (ph == tmpl.end() || !ph->is_string() ||
bt == tmpl.end() || !bt->is_string())
return std::nullopt;
dgb::stratum::DGBWorkSource::GbtTip tip;
tip.previousblockhash = ph->get<std::string>();
tip.bits = bt->get<std::string>();
// GBT previousblockhash is 64-hex big-endian display -- the
// exact u256_be_display_hex convention the embedded path emits,
// so it flows verbatim (parity by the daemon's own GBT contract).
// Width-guard so a malformed reply is a truthful absence, never a
// fabricated short id.
if (tip.previousblockhash.size() != 64)
return std::nullopt;
return tip;
} catch (const std::exception& e) {
std::cerr << "[DGB-STRATUM] GBT tip fallback RPC failed: "
<< e.what() << " -- prevhash/bits held absent"
<< std::endl;
return std::nullopt;
}
});
std::cout << "[DGB] stratum GBT tip fallback ARMED: previousblockhash+bits "
"<- digibyted getblocktemplate (embedded-empty-chain path)"
<< std::endl;
} else {
std::cout << "[DGB] stratum GBT tip fallback UNARMED (no digibyted creds) "
"-- prevhash/bits truthful-absent until embedded chain feeds"
<< std::endl;
}

// -- Phase-B producer bind: per-connection coinbase PPLNS inputs ----------
// Bind DGBWorkSource::set_pplns_inputs_fn -- the SOLE empty-jobs seam. While
// unbound, build_connection_coinbase() returns an empty job (pre-wire stub);
Expand Down
137 changes: 123 additions & 14 deletions src/impl/dgb/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

#include <core/log.hpp>
#include <core/hash.hpp> // Hash (sha256d) for coinbase txid + merkle pair
#include <core/target_utils.hpp> // chain::target_to_difficulty (vardiff/pool unit parity)
#include <core/address_utils.hpp> // address_to_script (share payout from username)

#include <btclibs/util/strencodings.h> // ParseHex
Expand Down Expand Up @@ -172,6 +173,14 @@ std::string DGBWorkSource::get_current_gbt_prevhash() const
// embedded P2P header ingest) -- a truthful absence, never a fabricated id.
if (auto th = chain_.tip_hash())
return u256_be_display_hex(*th);
// Embedded chain unfed -> external-daemon GBT fallback (the mining.notify
// prevhash path: stratum_server.cpp get_current_gbt_prevhash caller). Drawn
// from the SAME single GBT source as get_current_work_template()'s
// previousblockhash, so the dedicated getter and the assembled template can
// never diverge. Empty on unbound / no-creds / RPC-failure -- the server
// then falls back to the best-share hash (its documented empty-prevhash path).
if (auto tip = resolve_gbt_tip_fallback())
return tip->previousblockhash;
return {};
}

Expand Down Expand Up @@ -323,8 +332,19 @@ nlohmann::json DGBWorkSource::get_current_work_template() const
in.median_time_past = chain_.median_time_past();
in.curtime = static_cast<int64_t>(std::time(nullptr));
in.transactions = tx_sel.transactions;
if (auto th = chain_.tip_hash())
if (auto th = chain_.tip_hash()) {
// Embedded chain carries a real tip -> source previousblockhash from it.
// bits stays absent here: the Scrypt-only walk cannot reconstruct the
// MultiShield-V4 5-algo window (== V37) -- unchanged truthful absence.
in.previousblockhash = u256_be_display_hex(*th);
} else if (auto tip = resolve_gbt_tip_fallback()) {
// Embedded HeaderChain unfed -> external-daemon GBT fallback (persist per
// V36). Sources BOTH previousblockhash and the daemon-authoritative bits
// from ONE getblocktemplate snapshot (consistent height). Unbound / no
// creds / RPC failure -> both stay absent, byte-identical to before.
in.previousblockhash = tip->previousblockhash;
in.bits = tip->bits;
}

return dgb::coin::build_work_template(in);
}
Expand Down Expand Up @@ -614,20 +634,75 @@ nlohmann::json DGBWorkSource::mining_submit(
}
}
double DGBWorkSource::compute_share_difficulty(
const std::string& /*coinb1*/, const std::string& /*coinb2*/,
const std::string& /*extranonce1*/, const std::string& /*extranonce2*/,
const std::string& /*ntime*/, const std::string& /*nonce*/,
uint32_t /*version*/, const std::string& /*prevhash_hex*/,
const std::string& /*nbits_hex*/,
const std::vector<std::string>& /*merkle_branches*/) const
const std::string& coinb1, const std::string& coinb2,
const std::string& extranonce1, const std::string& extranonce2,
const std::string& ntime, const std::string& nonce,
uint32_t version, const std::string& prevhash_hex,
const std::string& nbits_hex,
const std::vector<std::string>& merkle_branches) const
{
// Stage 4b/4c: reconstruct the 80-byte header from (coinb1+en1+en2+coinb2)
// merkle-rooted with merkle_branches, then scrypt_1024_1_1_256(header) and
// return diff1 / pow_hash. Until then the coin-agnostic StratumServer must
// not credit pseudoshares it cannot score, so we return 0.0 — the documented
// parse-error / not-yet sentinel that the vardiff gate already treats as a
// hard reject (no garbage difficulty leaks into the rate monitor).
return 0.0;
// Stage 4b/4c LIVE: reconstruct the 80-byte header EXACTLY as mining_submit
// does (coinb1||en1||en2||coinb2 -> sha256d txid -> ascend merkle branches ->
// header), run the DGB-Scrypt digest (scrypt_1024_1_1_256, the sole algo V36
// validates), and return the achieved difficulty in the SAME unit the coin-
// agnostic StratumServer vardiff/pool gate uses (chain::target_to_difficulty,
// diff-1 == 0x1d00ffff). A malformed reconstruct (bad hex -> header != 80B)
// returns the documented 0.0 not-scored sentinel, which the gate treats as a
// hard reject -- so no garbage difficulty leaks into the rate monitor.

// 1. coinbase = coinb1 || extranonce1 || extranonce2 || coinb2
auto coinb1_bytes = ParseHex(coinb1);
auto en1_bytes = ParseHex(extranonce1);
auto en2_bytes = ParseHex(extranonce2);
auto coinb2_bytes = ParseHex(coinb2);

std::vector<uint8_t> coinbase;
coinbase.reserve(coinb1_bytes.size() + en1_bytes.size()
+ en2_bytes.size() + coinb2_bytes.size());
coinbase.insert(coinbase.end(), coinb1_bytes.begin(), coinb1_bytes.end());
coinbase.insert(coinbase.end(), en1_bytes.begin(), en1_bytes.end());
coinbase.insert(coinbase.end(), en2_bytes.begin(), en2_bytes.end());
coinbase.insert(coinbase.end(), coinb2_bytes.begin(), coinb2_bytes.end());

// 2. coinbase_txid = sha256d(coinbase) (non-witness txid for the merkle root)
uint256 coinbase_txid = Hash(
std::span<const uint8_t>(coinbase.data(), coinbase.size()));

// 3. merkle_root = ascend the frozen stratum merkle branches (LE-internal).
uint256 merkle_root = coinbase_txid;
for (const auto& branch_hex : merkle_branches) {
uint256 b;
auto bb = ParseHex(branch_hex);
if (bb.size() == 32) std::memcpy(b.begin(), bb.data(), 32);
merkle_root = merkle_pair(merkle_root, b);
}

// 4. header = version(LE) || prev(LE) || merkle_root || ntime(LE)
// || nbits(LE) || nonce(LE); prevhash arrives BE-display, so
// reverse to internal byte order (mirrors mining_submit byte-for-byte).
auto prevhash_be = ParseHex(prevhash_hex);
std::vector<uint8_t> prevhash_le(prevhash_be.rbegin(), prevhash_be.rend());

std::vector<uint8_t> header;
header.reserve(80);
push_u32_le(header, version);
header.insert(header.end(), prevhash_le.begin(), prevhash_le.end());
header.insert(header.end(), merkle_root.data(), merkle_root.data() + 32);
push_u32_le(header, parse_be_hex_u32(ntime));
push_u32_le(header, parse_be_hex_u32(nbits_hex)); // share target bits
push_u32_le(header, parse_be_hex_u32(nonce));

if (header.size() != 80)
return 0.0; // malformed reconstruct -> not-scored sentinel (hard reject).

// 5. DGB-Scrypt PoW digest, then difficulty relative to diff-1. Bridge the
// coin-space u256 into core uint256 via the u256_be_display_hex SSOT so
// the number is unit-identical to required_difficulty / pool_difficulty
// (both computed through chain::target_to_difficulty on the core side).
dgb::coin::u256 pow_hash = dgb::coin::scrypt_pow_hash(header.data());
uint256 pow_core;
pow_core.SetHex(dgb::coin::u256_be_display_hex(pow_hash));
return chain::target_to_difficulty(pow_core);
}

// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -658,6 +733,40 @@ void DGBWorkSource::set_pplns_inputs_fn(PplnsInputsFn fn)
pplns_inputs_fn_ = std::move(fn);
}

void DGBWorkSource::set_gbt_tip_fn(GbtTipFn fn)
{
std::lock_guard<std::mutex> lk(gbt_tip_mutex_);
gbt_tip_fn_ = std::move(fn);
gbt_tip_cache_.reset(); // drop any stale cache when the seam is (re)bound
gbt_tip_cache_time_ = 0;
}

std::optional<DGBWorkSource::GbtTip> DGBWorkSource::resolve_gbt_tip_fallback() const
{
// Copy the seam + serve a fresh-enough cache under the lock; run the blocking
// RPC round-trip OUTSIDE the lock (single io_context thread today, but never
// hold a lock across network IO). TTL-bound so per-heartbeat template/prevhash
// polls do not each trigger a getblocktemplate round-trip.
GbtTipFn fn;
const int64_t now = static_cast<int64_t>(std::time(nullptr));
{
std::lock_guard<std::mutex> lk(gbt_tip_mutex_);
if (gbt_tip_cache_ && (now - gbt_tip_cache_time_) < GBT_TIP_TTL_SECONDS)
return gbt_tip_cache_;
fn = gbt_tip_fn_; // copy so a concurrent set_gbt_tip_fn() cannot tear it out
}
if (!fn)
return std::nullopt; // unbound (no digibyted creds armed) -> truthful absence

std::optional<GbtTip> tip = fn(); // blocking getblocktemplate; nullopt on RPC failure
if (tip) {
std::lock_guard<std::mutex> lk(gbt_tip_mutex_);
gbt_tip_cache_ = tip;
gbt_tip_cache_time_ = now;
}
return tip;
}

uint256 DGBWorkSource::try_mint_share(const MintShareInputs& in) const
{
MintShareFn fn;
Expand Down
39 changes: 39 additions & 0 deletions src/impl/dgb/stratum/work_source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,28 @@ class DGBWorkSource : public core::stratum::IWorkSource
const std::vector<std::pair<uint32_t, std::vector<unsigned char>>>& merged_addrs)>;
void set_pplns_inputs_fn(PplnsInputsFn fn);

/// External-daemon GBT tip fallback (the fallback V36 mandates PERSIST).
/// While the embedded HeaderChain is unfed -- tip_hash()==nullopt, which is
/// the live state of a freshly-stood-up :5025 node whose Scrypt-only header
/// ingest has not yet populated the chain -- the header walk can supply
/// NEITHER previousblockhash NOR the authoritative MultiShield-V4 bits (a
/// Scrypt-only walk cannot reconstruct the 5-algo retarget window == V37).
/// This seam lets get_current_work_template() AND get_current_gbt_prevhash()
/// source BOTH from the external digibyted getblocktemplate. Bound in
/// main_dgb.cpp to a lambda over NodeRPC::getblocktemplate -- the RPC
/// transport stays OUT of stratum/, mirroring set_pplns_inputs_fn and dash's
/// dashd_fallback closure (#726). Returns nullopt when the seam is unbound,
/// no digibyted creds are armed, or the RPC call fails -- both fields then
/// stay a truthful ABSENCE exactly as before (never a fabricated tip/bits).
/// Consumed ONLY when the embedded chain has no tip; a real embedded tip
/// always wins (single truthful source, never a prevhash/bits height split).
struct GbtTip {
std::string previousblockhash; ///< GBT big-endian display hex (u256_be_display_hex convention)
std::string bits; ///< GBT nBits compact hex, daemon-authoritative
};
using GbtTipFn = std::function<std::optional<GbtTip>()>;
void set_gbt_tip_fn(GbtTipFn 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
Expand Down Expand Up @@ -297,6 +319,23 @@ class DGBWorkSource : public core::stratum::IWorkSource
mutable std::mutex pplns_inputs_mutex_;
PplnsInputsFn pplns_inputs_fn_;

// External-daemon GBT tip fallback (set_gbt_tip_fn). Empty until bound in
// main_dgb; while empty the embedded-empty-chain path is byte-identical to
// the prior truthful-absence. A short TTL cache bounds how often the blocking
// getblocktemplate RPC round-trip runs on the single io_context thread (the
// template + prevhash accessors can be polled per stratum session heartbeat).
mutable std::mutex gbt_tip_mutex_;
GbtTipFn gbt_tip_fn_;
mutable std::optional<GbtTip> gbt_tip_cache_;
mutable int64_t gbt_tip_cache_time_ = 0;
static constexpr int64_t GBT_TIP_TTL_SECONDS = 5;

// Resolve the external-daemon GBT tip fallback (previousblockhash + bits),
// TTL-cached. Returns nullopt when no seam is bound / RPC unavailable / the
// call failed. Consumed by get_current_work_template() and
// get_current_gbt_prevhash() ONLY when chain_.tip_hash() is nullopt.
std::optional<GbtTip> resolve_gbt_tip_fallback() const;

// Template cache (filled lazily; invalidated when work_generation_ bumps)
// Stage 4c populates these.
mutable std::mutex template_mutex_;
Expand Down
Loading
Loading