dash(coinbase): bind job to GBT snapshot + reconnect invalidation + submit-time payee guard (fix stale-payee bad-cb-payee)#751
Merged
Conversation
…ubmit-time payee guard (fix stale-payee bad-cb-payee)
DASH rotates the masternode (MN) payee EVERY block, so a coinbase is only
valid at the exact height its GBT template was fetched for. A won block
whose header names the current tip but whose coinbase carries a payee
frozen from an OLDER template snapshot is deterministically rejected by
dashd with bad-cb-payee -- the block reward is lost. Root cause is the
same template-vs-coinbase double-fetch staleness class as the core
send_notify_work path, aggravated by a "CoindRPC reconnecting" churn that
can serve a coinbase whose payee predates the reconnect.
Live-confirmed on DASH testnet dashd (.52, RPC 19998), each block mined
from c2pool's OWN coinbase serialization (dash::coinbase build + X11):
* coinbase with a STALE/wrong MN payee + valid X11 PoW
-> submitblock => {"result":"bad-cb-payee"} (the lost-reward bug)
* coinbase with the CORRECT current MN payee + valid X11 PoW
-> submitblock => {"result":null} ACCEPTED (3 real blocks)
* a slow (stale) correct-payee solve -> {"result":"bad-chainlock"}
(the sibling staleness class the steward saw at h1517187)
The live MN payee was observed rotating yeRZBWYfeNE4 -> yVXDAM73... across
consecutive blocks -- the per-block rotation this fix defends against.
The fix (fenced to src/impl/dash where possible; the core seam is a
default-off, byte-compatible flag so LTC/BTC/DGB are unchanged):
1. GBT-SNAPSHOT BINDING. build_connection_coinbase() now freezes the
header fields (prevhash/version/nbits/curtime/height) ATOMICALLY into
WorkSnapshot alongside the coinbase, branches and tx set. The stratum
session OVERRIDES its separately-fetched template header with these,
so an issued job is ONE frozen template unit -- its header can never
name a different height than its coinbase's MN payee.
2. RECONNECT INVALIDATION. NodeRPC gains an on-reconnect observer;
main_dash wires it to DASHWorkSource::invalidate_template_cache(),
which drops the cached template/payee and bumps work_generation so no
job is served or submitted from a snapshot predating the churn.
3. SUBMIT-TIME PAYEE GUARD. mining_submit() validates a won block's
coinbase against the CURRENT template's GBT-mandated payments
(submit_payee_guard.hpp). A same-height payee mismatch is rejected
LOUDLY and NOT submitted (never dispatch a doomed bad-cb-payee block);
a moved tip is an orphan-race candidate and still submits; a guard-side
parse failure never blocks a submission.
4. ZERO-HASH PRE-AUTH JOB SUPPRESSED. cached_work() treats an all-zero
prev template as an honest set-gap, and the core session (under the new
require_job_snapshot flag) refuses to serve a job with no atomic
snapshot or a zeroed prevhash.
5. CORE DOUBLE-FETCH. The has_header snapshot binding closes the
send_notify_work template-vs-coinbase window for DASH without a
behavioral change to other coins (the override and require_job_snapshot
gate are both default-off).
KATs (test_dash_stratum_work_source, already in the build.yml allowlist):
job frozen to its GBT snapshot; rotation between the two fetches yields a
self-consistent snapshot; reconnect invalidates the cache + bumps the
generation; submit-time guard classifies stale-payee vs tip-moved vs
unverifiable; a won block with a rotated same-height payee is locally
rejected (broadcaster not fired) while a tip-move still submits;
zero-prev template is a set-gap. 30 tests pass; dash + core stratum
regression suites green; LTC/BTC/DGB build clean.
frstrtr
force-pushed
the
fix/dash-coinbase-stale-payee
branch
from
July 19, 2026 10:02
9334118 to
9669c79
Compare
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug (release-blocking hotel reward-safety gate)
DASH rotates the masternode (MN) payee every block, so a coinbase is only valid at the exact height its GBT template was fetched for. A won block whose header names the current tip but whose coinbase carries a payee frozen from an older template snapshot is deterministically rejected by dashd with
bad-cb-payee— the block reward is lost. This is the same template-vs-coinbase double-fetch staleness class as the coresend_notify_workpath, aggravated by a "CoindRPC reconnecting" churn that can serve a coinbase whose payee predates the reconnect. PR #746 (net-mismatch) did not fix this.Live proof (real DASH testnet dashd .52, RPC 19998)
Each block mined from c2pool's own coinbase serialization (
dash::coinbasebuild + X11), submitted viasubmitblockto the real daemon:submitblockresultbad-cb-payee(the lost-reward bug)null= ACCEPTED (3 real blocks)bad-chainlock(sibling staleness class, cf. h1517187)The live MN payee was observed rotating
yeRZBWYfeNE4→yVXDAM73…across consecutive blocks — the per-block rotation this fix defends against. Thebad-cb-payeereject reproduces the steward's hex-confirmed h1517420 finding exactly.The fix
Fenced to
src/impl/dashwhere possible. The one core seam is a default-off, byte-compatible flag (StratumConfig::require_job_snapshot+WorkSnapshot::has_header), so LTC / BTC / DGB are unchanged (verified: they build clean and the core stratum suites stay green).build_connection_coinbase()freezes the header fields (prevhash/version/nbits/curtime/height) atomically intoWorkSnapshotalongside the coinbase, branches and tx set. The stratum session overrides its separately-fetched template header with these, so a job is one frozen template unit — its header can never name a different height than its coinbase's MN payee.NodeRPCgains an on-reconnect observer;main_dashwires it toDASHWorkSource::invalidate_template_cache(), which drops the cached template/payee and bumpswork_generationso no job is served or submitted from a snapshot predating the churn.mining_submit()validates a won block's coinbase against the current template's GBT-mandated payments (submit_payee_guard.hpp). A same-height payee mismatch is rejected loudly and NOT submitted (never dispatch a doomed block); a moved tip is an orphan-race candidate and still submits; a guard-side parse failure never blocks a submission.cached_work()treats an all-zero prev template as an honest set-gap, and the core session (underrequire_job_snapshot) refuses to serve a job with no atomic snapshot or a zeroed prevhash.has_headerbinding closes thesend_notify_worktemplate-vs-coinbase window for DASH with no behavioral change to other coins (both the override and the gate are default-off), so no cross-coin core surgery was needed.Tests
test_dash_stratum_work_source(already in the build.yml allowlist) gains KATs pinning: job frozen to its GBT snapshot; a rotation between the two fetches yields a self-consistent snapshot (with the hazard witness that a mixed job would have differed); reconnect invalidates the cache + bumps the generation; the submit-time guard classifies stale-payee vs tip-moved vs unverifiable; a won block with a rotated same-height payee is locally rejected (broadcaster not fired) while a tip-move still submits; zero-prev template is a set-gap. 30 tests pass. The dash + core stratum regression suites are green.