Skip to content

Commit 8a1f479

Browse files
committed
dash(main): coin-P2P tip handler invalidates template cache — explicit fresh-payee re-source (robustness for the stale-payee window; #796)
Mirrors the fire_refresh trio (invalidate+bump+notify) so the served template always re-sources with the fresh-tip payee regardless of refresh_executor_ state. KATs in test_dash_stratum_work_source prove the window stays closed with refresh_executor_ set. Consensus-neutral.
1 parent d5d3766 commit 8a1f479

2 files changed

Lines changed: 104 additions & 6 deletions

File tree

src/c2pool/main_dash.cpp

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,12 +1575,21 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
15751575
// synced base (ZERO after a reorg = full snapshot); the
15761576
// new_mnlistdiff subscription advances sml_base on acceptance.
15771577
if (cp) cp->send_getmnlistd(*sml_base, new_tip);
1578-
// #739: event-driven stale-work notify. NOTE: the freshness gate
1579-
// now holds the embedded arm on the dashd fallback until the
1580-
// getmnlistd above lands and re-notifies (maintainer state-dirty),
1581-
// so this notify serves the reward-safe fallback, not a stale-SML
1582-
// embedded template.
1583-
if (ws) ws->bump_work_generation();
1578+
// #739 + stale-payee window close: event-driven stale-work
1579+
// notify. INVALIDATE the template cache FIRST (mirrors the
1580+
// #770/#772 fire_refresh trio: invalidate + bump + notify) so the
1581+
// next served job is ALWAYS re-sourced with the fresh-tip payee,
1582+
// regardless of refresh_executor_ state. Without the explicit
1583+
// invalidate the window only stays closed IMPLICITLY (the coin-P2P
1584+
// arm has no refresh_executor_, so cached_work() re-sources inline);
1585+
// if io-decouple is ever extended to this arm, cached_work() would
1586+
// serve the STALE cached template while refreshing async and a
1587+
// stale-payee job would go out. Make it robust, not implicit.
1588+
if (ws) {
1589+
ws->invalidate_template_cache(
1590+
"coin-P2P tip changed: fresh-payee re-source");
1591+
ws->bump_work_generation();
1592+
}
15841593
if (stratum_server) stratum_server->notify_all();
15851594
});
15861595

test/test_dash_stratum_work_source.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,4 +1411,93 @@ TEST(DashDashboardWiring, LocalStatsIdleWarningWhenNoWorkers)
14111411
EXPECT_TRUE(idle);
14121412
}
14131413

1414+
// ═════════════════════════════════════════════════════════════════════════════
1415+
// coin-P2P tip event closes the stale-payee window EXPLICITLY (robust under a
1416+
// future io-decouple-extended config where refresh_executor_ is SET).
1417+
// ═════════════════════════════════════════════════════════════════════════════
1418+
1419+
namespace {
1420+
// A DEFERRED refresh executor (models the rpc_pool background thread): jobs are
1421+
// queued, not run inline, so cached_work() takes the serve-stale-while-refreshing
1422+
// path — exactly the config where a bump-only tip event would REOPEN the window.
1423+
struct DeferredExecutor {
1424+
std::vector<std::function<void()>> jobs;
1425+
void operator()(std::function<void()> j) { jobs.push_back(std::move(j)); }
1426+
void run() { auto q = std::move(jobs); jobs.clear(); for (auto& j : q) j(); }
1427+
};
1428+
} // namespace
1429+
1430+
// THE FIX: invalidate_template_cache() on the coin-P2P tip event guarantees the
1431+
// next served job is re-sourced (fresh payee), never the stale cached one — even
1432+
// with a background refresh_executor_ set.
1433+
TEST(DashStratumCoinP2pTipInvalidate, InvalidateClosesStalePayeeWindowUnderRefreshExecutor)
1434+
{
1435+
auto current = std::make_shared<dash::coin::DashWorkData>(rich_work()); // tip A
1436+
auto fallback = [current]() -> dash::coin::DashWorkData { return *current; };
1437+
auto submit = [](const std::vector<unsigned char>&, uint32_t) { return true; };
1438+
dash::coin::NodeCoinState cs; // unpopulated -> fallback arm
1439+
1440+
dash::stratum::DASHWorkSource ws(cs, fallback, submit,
1441+
core::stratum::StratumConfig{}, /*is_testnet=*/false);
1442+
auto exec = std::make_shared<DeferredExecutor>();
1443+
ws.set_refresh_executor([exec](std::function<void()> j) { (*exec)(std::move(j)); });
1444+
1445+
// Prime the cache with tip A (dispatch the bg refresh, then run it).
1446+
ws.get_current_work_template(); // dispatches; returns set-gap
1447+
exec->run(); // bg refresh sources tip A -> cache = A
1448+
ASSERT_EQ(ws.get_current_work_template().value("previousblockhash", ""),
1449+
std::string(kPrevHashHex));
1450+
1451+
// The coin-P2P feed advances to tip B (a DIFFERENT masternode payee).
1452+
*current = rotated_work();
1453+
1454+
// The coin-P2P on_tip_changed handler fires: invalidate + bump (+ notify).
1455+
ws.invalidate_template_cache("coin-P2P tip changed: fresh-payee re-source");
1456+
ws.bump_work_generation();
1457+
1458+
// Under refresh_executor_ the served job must NEVER be the stale tip-A
1459+
// template: the invalidate dropped the cache -> honest set-gap until the bg
1460+
// refresh lands tip B.
1461+
auto served = ws.get_current_work_template();
1462+
if (!served.empty())
1463+
EXPECT_NE(served.value("previousblockhash", ""), std::string(kPrevHashHex))
1464+
<< "stale tip-A template must not be served after a coin-P2P tip event";
1465+
exec->run(); // the deferred bg refresh completes
1466+
auto fresh = ws.get_current_work_template();
1467+
ASSERT_FALSE(fresh.empty());
1468+
EXPECT_EQ(fresh.value("previousblockhash", ""), std::string(kRotatedPrevHashHex))
1469+
<< "the served job must carry the FRESH-tip (B) template/payee";
1470+
}
1471+
1472+
// CONTRAST (the window this fix closes): bump_work_generation() ALONE, under a
1473+
// refresh_executor_, serves the STALE cached tip-A template while refreshing
1474+
// async — the implicit-only path that reopens the stale-payee window.
1475+
TEST(DashStratumCoinP2pTipInvalidate, BumpAloneServesStaleUnderRefreshExecutor)
1476+
{
1477+
auto current = std::make_shared<dash::coin::DashWorkData>(rich_work());
1478+
auto fallback = [current]() -> dash::coin::DashWorkData { return *current; };
1479+
auto submit = [](const std::vector<unsigned char>&, uint32_t) { return true; };
1480+
dash::coin::NodeCoinState cs;
1481+
1482+
dash::stratum::DASHWorkSource ws(cs, fallback, submit,
1483+
core::stratum::StratumConfig{}, /*is_testnet=*/false);
1484+
auto exec = std::make_shared<DeferredExecutor>();
1485+
ws.set_refresh_executor([exec](std::function<void()> j) { (*exec)(std::move(j)); });
1486+
1487+
ws.get_current_work_template();
1488+
exec->run();
1489+
ASSERT_EQ(ws.get_current_work_template().value("previousblockhash", ""),
1490+
std::string(kPrevHashHex));
1491+
1492+
*current = rotated_work();
1493+
ws.bump_work_generation(); // NO invalidate — the pre-fix implicit path
1494+
1495+
// Serves the STALE tip-A template (cache still held; async refresh not landed).
1496+
auto served = ws.get_current_work_template();
1497+
ASSERT_FALSE(served.empty());
1498+
EXPECT_EQ(served.value("previousblockhash", ""), std::string(kPrevHashHex))
1499+
<< "bump-only under refresh_executor_ serves the STALE tip-A payee — the "
1500+
"window invalidate_template_cache() closes";
1501+
}
1502+
14141503
} // namespace

0 commit comments

Comments
 (0)