Skip to content

Commit 128a8cc

Browse files
authored
Merge pull request #770 from frstrtr/dash/tip-notify-fallback-arm
dash(stratum): event-driven tip refresh on dashd-fallback arm + issued-diff hashrate credit
2 parents 2e2fa23 + e1d2906 commit 128a8cc

5 files changed

Lines changed: 137 additions & 3 deletions

File tree

src/c2pool/main_dash.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,65 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
14811481
}
14821482
}
14831483

1484+
// ── Fallback-arm event-driven tip refresh ────────────────────────────
1485+
// On the dashd-fallback arm (no embedded coin-P2P tip source) the template
1486+
// cache only re-sources on the 30 s staleness TTL (work_source.cpp
1487+
// kStaleAfter) — there is NO tip-change signal like the embedded arm's
1488+
// header_chain->set_on_tip_changed callback. DASH blocks arrive ~every
1489+
// 150 s, so up to ~30 s of stale-tip mining can occur per block (accepted
1490+
// pseudoshares that can no longer win the current block). Poll dashd for
1491+
// its best-block hash every 3 s; on a change, drop the cached template +
1492+
// bump work-generation + notify every session (clean_jobs) — the SAME
1493+
// refresh pair the embedded arm fires. Gated on the fallback arm (coin_p2p
1494+
// null) AND an armed rpc AND a live stratum acceptor. getbestblockhash is a
1495+
// trivial RPC; failures are swallowed so a daemon hiccup never crashes the
1496+
// run-loop (retry next tick). Reuses the existing NodeRPC client — no new
1497+
// dependency, no dashd config change, no new notify mechanism.
1498+
if (!coin_p2p && rpc && stratum_server) {
1499+
auto tip_timer = std::make_shared<io::steady_timer>(ioc);
1500+
auto last_tip = std::make_shared<std::string>();
1501+
auto tip_tick = std::make_shared<
1502+
std::function<void(const boost::system::error_code&)>>();
1503+
*tip_tick = [rpc = rpc.get(), ws = work_source.get(),
1504+
ss = stratum_server.get(), tip_timer, last_tip, tip_tick](
1505+
const boost::system::error_code& ec) {
1506+
if (ec) return; // cancelled at shutdown
1507+
try {
1508+
const std::string tip = rpc->getbestblockhash();
1509+
if (!tip.empty() && *last_tip != tip) {
1510+
const bool first_seen = last_tip->empty();
1511+
*last_tip = tip;
1512+
// Skip the notify on the very first observation (baseline);
1513+
// only a genuine tip CHANGE forces a refresh.
1514+
if (!first_seen) {
1515+
ws->invalidate_template_cache(
1516+
"tip-poll: dashd best-block changed");
1517+
ws->bump_work_generation();
1518+
ss->notify_all();
1519+
LOG_INFO << "[Stratum] tip-poll: new tip "
1520+
<< tip.substr(0, 16)
1521+
<< ", forcing template refresh + notify";
1522+
std::cout << "[Stratum] tip-poll: new tip "
1523+
<< tip.substr(0, 16)
1524+
<< ", forcing template refresh + notify\n";
1525+
}
1526+
}
1527+
} catch (const std::exception& e) {
1528+
LOG_WARNING << "[Stratum] tip-poll getbestblockhash failed "
1529+
"(non-fatal, retry next tick): " << e.what();
1530+
} catch (...) {
1531+
// swallow — never crash the run-loop on a tip probe
1532+
}
1533+
tip_timer->expires_after(std::chrono::seconds(3));
1534+
tip_timer->async_wait(*tip_tick);
1535+
};
1536+
tip_timer->expires_after(std::chrono::seconds(3));
1537+
tip_timer->async_wait(*tip_tick);
1538+
std::cout << "[run] fallback-arm tip-poll ARMED (dashd getbestblockhash "
1539+
"every 3 s -> event-driven template refresh + clean_jobs "
1540+
"notify on tip change)\n";
1541+
}
1542+
14841543
std::cout << "[run] run-loop up (Ctrl-C to stop); won blocks relay DUAL-PATH:\n"
14851544
"[run] ARM A embedded coin-P2P relay (primary, daemonless) = "
14861545
<< (p2p_relay ? "ARMED" : (no_p2p_relay ? "SUPPRESSED (--no-p2p-relay)"

src/core/stratum_server.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,18 +1178,26 @@ nlohmann::json StratumSession::handle_submit(const nlohmann::json& params, const
11781178

11791179
// ── Pseudoshare accepted — record in RateMonitor ──
11801180
// p2pool: work = target_to_average_attempts(effective_target)
1181-
// For us: work = vardiff_difficulty × 2^32 (equivalent unit conversion)
1181+
// For us: work = credited_difficulty × 2^32 (equivalent unit conversion)
11821182
++accepted_shares_;
11831183
static constexpr double TWO_32 = 4294967296.0;
1184-
double work = vardiff_difficulty * TWO_32;
1184+
// #766/#762: credit the hashrate estimate at the difficulty THIS job was
1185+
// actually issued/mined at, not the live vardiff. A vardiff retarget
1186+
// between job-issue and submit must not skew the EWMA/RateMonitor input —
1187+
// the share represents work at its own job's target. Falls back to the live
1188+
// vardiff when issued_difficulty is unset (0). Estimate/stats only; the
1189+
// acceptance gate above is unchanged.
1190+
double credited_difficulty =
1191+
(job.issued_difficulty > 0.0) ? job.issued_difficulty : vardiff_difficulty;
1192+
double work = credited_difficulty * TWO_32;
11851193
bool is_dead = (job.stale_info != 0);
11861194

11871195
// Record in global RateMonitor (for get_local_addr_rates)
11881196
if (server_)
11891197
server_->record_pseudoshare(work, pubkey_hash_, username_, is_dead);
11901198

11911199
// Record in per-connection tracker (for VARDIFF adjustment + per-session stats)
1192-
hashrate_tracker_.record_mining_share_submission(vardiff_difficulty, true);
1200+
hashrate_tracker_.record_mining_share_submission(credited_difficulty, true);
11931201

11941202
// Check if VARDIFF changed → send new difficulty AND new work to miner.
11951203
// p2pool (stratum.py:594-595): after adjusting target, calls _send_work()

src/impl/dash/coin/rpc.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,18 @@ nlohmann::json NodeRPC::getmininginfo()
444444
return CallAPIMethod("getmininginfo");
445445
}
446446

447+
// Trivial tip probe -- `getbestblockhash` returns the best-block hash as a bare
448+
// JSON string. Returns "" if the daemon result is null/absent (never throws on
449+
// a well-formed-but-empty result; transport errors still propagate to the
450+
// caller, which swallows them). No dashd config change is required.
451+
std::string NodeRPC::getbestblockhash()
452+
{
453+
auto result = CallAPIMethod("getbestblockhash");
454+
if (result.is_string())
455+
return result.get<std::string>();
456+
return {};
457+
}
458+
447459
// verbose: true -- json result, false -- hex-encode result;
448460
nlohmann::json NodeRPC::getblockheader(uint256 header, bool verbose)
449461
{

src/impl/dash/coin/rpc.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ class NodeRPC : public jsonrpccxx::IClientConnector
126126
nlohmann::json getnetworkinfo();
127127
nlohmann::json getblockchaininfo();
128128
nlohmann::json getmininginfo();
129+
// Trivial tip probe: the current best-block hash as a hex string. Used by
130+
// the fallback-arm tip poller (main_dash.cpp) to drive event-driven
131+
// template refresh without waiting on the 30 s staleness TTL. Empty string
132+
// on a null/absent result.
133+
std::string getbestblockhash();
129134
// verbose: true -- json result, false -- hex-encode result;
130135
nlohmann::json getblockheader(uint256 header, bool verbose = true);
131136
// verbosity: 0 for hex-encoded data, 1 for a json object, and 2 for json object with transaction data

test/test_dash_stratum_work_source.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,56 @@ TEST(DashStratumWorkSource, InvalidateTemplateCacheForcesRefetchAndBumpsGenerati
741741
EXPECT_EQ(fallback_calls, 2);
742742
}
743743

744+
// Fallback-arm event-driven tip refresh pin: on the dashd-fallback arm the
745+
// template cache only re-sources on the 30 s staleness TTL, so a new DASH block
746+
// can leave miners hashing a stale tip for up to ~30 s (accepted pseudoshares
747+
// that can no longer win the current block). The run-path tip poller
748+
// (main_dash.cpp) closes that window by firing the SAME refresh pair on a
749+
// dashd best-block change that the embedded arm fires from its header-chain
750+
// tip-changed callback: invalidate_template_cache() + bump_work_generation(),
751+
// then notify_all() pushes a clean_jobs mining.notify. This KAT pins the
752+
// work-source contract that pair relies on: on a tip change the served template
753+
// must SWAP to the new tip (no stale-tip mining) AND the work generation must
754+
// bump (the clean_jobs/notify signal every session re-pulls on). Fallback arm
755+
// only: an UNPOPULATED coin-state, so get_work routes to the dashd fallback.
756+
TEST(DashStratumWorkSource, FallbackArmTipChangeRefreshesTemplateAndBumpsGeneration)
757+
{
758+
Fixture fx(true); // unpopulated coin-state -> fallback arm
759+
ASSERT_FALSE(fx.coin_state.populated());
760+
auto ws = fx.make();
761+
762+
// Baseline: the fallback serves the pre-tip-change template, cached at the
763+
// old tip. This is what an idle miner would keep hashing until the TTL.
764+
auto tmpl_before = ws->get_current_work_template();
765+
ASSERT_FALSE(tmpl_before.empty());
766+
EXPECT_EQ(tmpl_before.value("previousblockhash", ""), std::string(kPrevHashHex));
767+
const uint64_t gen_before = ws->get_work_generation();
768+
769+
// A new DASH block arrives: dashd's best-block hash changes. The fallback
770+
// now sources a new-tip template (rotated prev + rotated payee), exactly as
771+
// getbestblockhash flips for the run-path poller.
772+
fx.fallback_work = rotated_work();
773+
774+
// The poller's refresh action on an observed tip change (the SAME pair the
775+
// embedded header-chain tip-changed callback fires).
776+
ws->invalidate_template_cache("kat: fallback-arm dashd best-block changed");
777+
ws->bump_work_generation();
778+
779+
// (a) clean_jobs/notify signal: work generation advanced, so notify_all()
780+
// will push a fresh mining.notify to every session.
781+
EXPECT_GT(ws->get_work_generation(), gen_before);
782+
783+
// (b) no more stale-tip mining: the next served template is the NEW tip,
784+
// not the cached pre-change one.
785+
auto tmpl_after = ws->get_current_work_template();
786+
ASSERT_FALSE(tmpl_after.empty());
787+
EXPECT_EQ(tmpl_after.value("previousblockhash", ""),
788+
std::string(kRotatedPrevHashHex));
789+
EXPECT_NE(tmpl_after.value("previousblockhash", ""),
790+
tmpl_before.value("previousblockhash", ""));
791+
EXPECT_EQ(tmpl_after.value("height", 0u), 424243u);
792+
}
793+
744794
// Fix 3 pin (work-source side of the zero-hash pre-auth job_0 defect): a
745795
// template with a zeroed prev is NOT mineable work — honest set-gap, never a
746796
// zero-prev job.

0 commit comments

Comments
 (0)