Skip to content

dash(coinbase): bind job to GBT snapshot + reconnect invalidation + submit-time payee guard (fix stale-payee bad-cb-payee)#751

Merged
frstrtr merged 1 commit into
masterfrom
fix/dash-coinbase-stale-payee
Jul 19, 2026
Merged

dash(coinbase): bind job to GBT snapshot + reconnect invalidation + submit-time payee guard (fix stale-payee bad-cb-payee)#751
frstrtr merged 1 commit into
masterfrom
fix/dash-coinbase-stale-payee

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 19, 2026

Copy link
Copy Markdown
Owner

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 core send_notify_work path, 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::coinbase build + X11), submitted via submitblock to the real daemon:

scenario PoW dashd submitblock result
coinbase with stale/wrong MN payee valid bad-cb-payee (the lost-reward bug)
coinbase with correct current MN payee valid null = ACCEPTED (3 real blocks)
correct payee but slow/stale solve valid bad-chainlock (sibling staleness class, cf. h1517187)

The live MN payee was observed rotating yeRZBWYfeNE4yVXDAM73… across consecutive blocks — the per-block rotation this fix defends against. The bad-cb-payee reject reproduces the steward's hex-confirmed h1517420 finding exactly.

The fix

Fenced to src/impl/dash where 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).

  1. GBT-snapshot bindingbuild_connection_coinbase() 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 a job is one frozen template unit — its header can never name a different height than its coinbase's MN payee.
  2. Reconnect invalidationNodeRPC 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 guardmining_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.
  4. Zero-hash pre-auth job suppressedcached_work() treats an all-zero prev template as an honest set-gap, and the core session (under require_job_snapshot) refuses to serve a job with no atomic snapshot or a zeroed prevhash.
  5. Core double-fetch — the has_header binding closes the send_notify_work template-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.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant