Skip to content

dash(stratum): firmware-safe power-of-two vardiff quantization (v0.2.3.6) [stacks on #770]#773

Merged
frstrtr merged 3 commits into
masterfrom
dash/vardiff-poweroftwo-quantize
Jul 20, 2026
Merged

dash(stratum): firmware-safe power-of-two vardiff quantization (v0.2.3.6) [stacks on #770]#773
frstrtr merged 3 commits into
masterfrom
dash/vardiff-poweroftwo-quantize

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 20, 2026

Copy link
Copy Markdown
Owner

DASH firmware-safe power-of-two vardiff quantization (v0.2.3.6)

Resolves the retention-critical ~25% (23–30% observed) share-reject rate on the live DASH mining hotel.

Stacks on #770 (dash/tip-notify-fallback-arm / v0.2.3.5 tip-poll). Base is master; review only the two commits on top of #770. Merge #770 first.

Root cause

#766's hashrate-vardiff advertises arbitrary real difficulties. ASIC firmware rounds the advertised pool difficulty DOWN to a power-of-two grid, mines that easier target, then submits shares between the rig's rounded target and the pool's exact required difficulty. Every such share is silently rejected at the P5 gate (share_difficulty < required_difficulty, src/core/stratum_server.cpp). ~28% expected — matches the field. Rejects are cosmetic for block-finding (the block-check passes by orders of magnitude) but trigger miner-firmware failover to backup pools, bleeding the tenant's hashrate.

F1 — retention fix

In HashrateTracker::set_difficulty_from_hashrate, right after the min/max clamp, snap the advertised difficulty to exp2(floor(log2(d))) — the largest power of two not exceeding the estimator's ideal D — then re-floor at min_difficulty_. Advertised == applied == required, closing the firmware-grid reject window. Rounding DOWN keeps accepted-share cadence at/above target. issued_difficulty (frozen at job creation from get_current_difficulty()) and the advertised set_difficulty carry the same quantized value by construction. Bounds / target_time / deadband unchanged. DASH-scoped by construction (only the use_hashrate_vardiff path).

set_difficulty_hint / suggest-difficulty is intentionally left untouched: it is the miner-requested fixed-diff path, shared across all coins, and quantizing it would take the change out of DASH scope.

F2 — observability

Rate-limited (≤ 1 / 5 s / session) INFO diagnostics at the P5 low-diff reject (logs hash_diff / required / issued / vardiff) and the P1 malformed-params reject. INFO + throttled so it cannot flood.

KAT

Extends the already-allowlisted test_dash_work_source target (in build.yml --target on both Linux legs) with HashrateVardiffQuantize.*: drives the tracker through its public API and asserts the advertised difficulty is an exact power of two and never exceeds the estimator's ideal D (rounds down). Plus a pure-math floor-on-log2-grid check.

Consensus-neutrality (pre-v36 vardiff rule)

Per-connection vardiff/stratum path only. git diff origin/master | grep -iE 'get_share_bits|share_bits|share_target|DGW|oracle|payout|pplns' over this change's code lines is EMPTY — the only matches in the full range are comments (a pre-existing context line and the KAT's own doc comment). PASS.

Build / test

  • conan (cached) + cmake reconfigure + cmake --build build_ci --target c2pool-dash test_dash_work_source → exit 0.
  • KAT: 6/6 pass (HashrateVardiffQuantize.AdvertisesPowerOfTwoRoundedDown, ...QuantizationFormulaIsFloorLog2, plus the 4 pre-existing DashWorkSource routing tests).
  • Release tarball c2pool-dash-0.2.3.6-linux-x86_64.tar.gz, bundle self-reports 0.2.3.6.

Not deployed; systemd drop-in not staged — integrator handles staging/deploy.

@frstrtr
frstrtr marked this pull request as ready for review July 20, 2026 14:55
frstrtr added 3 commits July 20, 2026 19:01
…ssued-diff hashrate credit

Two consensus-neutral stratum fixes for the mining-hotel DASH node. Both touch
only the per-connection vardiff/pseudoshare path and the template-refresh/notify
path; get_share_bits/pool share_bits, the DGW retarget, the sharechain share
target, and block-acceptance are untouched.

FIX 1 -- fallback-arm event-driven tip refresh. On the dashd-fallback arm there
is no tip-change signal like the embedded arm's header-chain tip-changed
callback; the template cache only re-sources on a 30 s staleness TTL. DASH
blocks arrive ~every 150 s, so up to ~30 s of stale-tip mining can occur per
block (accepted pseudoshares that can no longer win the current block). Add a
lightweight run-path poller (gated on the fallback arm: coin-P2P null + armed
rpc + live stratum acceptor) that reads dashd getbestblockhash every 3 s; on a
tip change it drops the cached template + bumps work-generation + notify_all()
-- the SAME refresh pair the embedded arm fires. Reuses the existing NodeRPC
client (new trivial getbestblockhash method); RPC failures are swallowed so a
daemon hiccup never crashes the run-loop.

FIX 2 -- credit the hashrate EWMA + RateMonitor at the difficulty the job was
issued/mined at (job.issued_difficulty), not the live vardiff, so a vardiff
retarget between job-issue and submit does not skew the estimate. Falls back to
the live vardiff when issued_difficulty is unset. Estimate/stats only; the
acceptance gate is unchanged.

Extends test_dash_stratum_work_source with a KAT pinning the fallback-arm
tip-change refresh contract (template swaps to the new tip + work generation
bumps -- the clean_jobs/notify signal). The target is already in both build.yml
--target allowlists, so CI discovery is unchanged.
…re-safe)

ASIC firmwares round the advertised pool difficulty DOWN to a power-of-two
grid, mine that easier target, then submit shares between the rig's rounded
target and the pool's exact required difficulty — every one silently rejected
at the P5 vardiff gate (share_difficulty < required_difficulty). ~28% of the
DASH hotel's shares, matching the 23-30% observed reject rate. Rejects are
cosmetic for block-finding (block-check passes by orders of magnitude) but
drive miner-firmware failover to backup pools, bleeding tenant hashrate.

F1 (retention): in HashrateTracker::set_difficulty_from_hashrate, after the
min/max clamp, snap the advertised difficulty to exp2(floor(log2(d))) — the
largest power of two not exceeding the estimator's ideal D — then re-floor at
min. Advertised == applied == required, so the firmware grid no longer opens a
reject window. Rounding DOWN keeps accepted-share cadence at/above target. Only
the DASH use_hashrate_vardiff path is affected; bounds/target/deadband
unchanged. issued_difficulty (frozen at job creation from
get_current_difficulty()) and the advertised set_difficulty now carry the same
quantized value by construction.

F2 (observability): rate-limited (<= 1 / 5s / session) INFO diagnostics at the
P5 low-diff reject (logs hash_diff / required / issued / vardiff) and the P1
malformed-params reject, so any residual grid gap is visible in the field
without flooding.

KAT: extends test_dash_work_source with a power-of-two-quantization check that
drives the tracker through its public API and asserts the advertised difficulty
is an exact power of two and never exceeds the estimator's ideal D (rounds
down). Consensus-neutral: per-connection vardiff/stratum path only.
The power-of-two hashrate-vardiff quantization re-floored the advertised diff
at min_difficulty_ (max(min_difficulty_, d)). DASH's min_difficulty_ (0.0005)
is not on the power-of-two grid, so a floor-pinned or warm-up advertise fell
back to the raw 0.0005 and re-opened the ~2.3% firmware reject gap at the floor.

Quantize the floor as well so the result is a power of two by construction:
advertise the largest grid step <= the configured floor (0.00048828125), at
most one step below it, preserving the round-DOWN cadence invariant. Scoped to
the hashrate-vardiff path (set_difficulty_from_hashrate, DASH-only); the shared
StratumConfig min_difficulty default is unchanged.

Adds a floor-pinned KAT: a slow miner whose ideal D falls below the floor must
still be advertised an exact power of two (fails pre-fix).
@frstrtr
frstrtr force-pushed the dash/vardiff-poweroftwo-quantize branch from a0e4d47 to aafb66c Compare July 20, 2026 15:32
@frstrtr
frstrtr merged commit 14718d5 into master Jul 20, 2026
26 of 27 checks passed
frstrtr pushed a commit that referenced this pull request Jul 21, 2026
…reward-safe --embedded-mainnet (default OFF)

Rebased onto master (#781 io-decouple + #773/#777/#775/#782). Integrates the
embedded DASH template arm into the decoupled tip-poll/rpc_pool architecture WHILE
preserving both: #781's refresh_executor/resource_template_now split AND every
v0.2.4 embedded guard. Default behaviour is UNCHANGED and reward-safe — with
--embedded-mainnet OFF (the default) the template stays arm=dashd-fallback, the
path the hotel runs, byte-for-byte.

Wires the vendored SML/QuorumManager/CCbTx machinery into the live embedded
template so the daemonless arm assembles a real DIP-0004 type-5 CCbTx, proven
byte-identical to a real dashd getblocktemplate from the raw mnlistdiff wire
(merkleRootMNList + merkleRootQuorums recomputed from wire; bestCL; creditPool
accrual). The mainnet gate lifts behind the explicit --embedded-mainnet opt-in.

Consensus guards (all fail-closed to the reward-safe dashd fallback):
- merkleRootQuorums: hash ALL active commitments (no rotated per-index dedup) —
  byte-identical to dashd (109-quorum from-wire KAT).
- H-1: a malformed quorum tail wipes + full-resyncs (never papered over by a
  later incremental; base-continuity tightened).
- DKG mining-phase heights (is_dkg_commitment_window over every enabled llmqType)
  and superblock heights: refuse embedded.
- bestCL freshness: committed ChainLock must be within one block of the tip.
- creditPool: INDEPENDENT seed-height gate (the seed cbTx's own nHeight must equal
  the tip) — catches a seed one block behind that value/hash self-checks miss;
  pre-emit value re-check + serve-time cache re-validation for the build-vs-serve
  skew; GBT-xcheck backstop cross-checks the embedded creditPool vs dashd.
- C-3: the embedded block excludes all DIP special txs (1-4,6,8,9) — their
  own-block state effects are not modelled in the committed CbTx (Phase-2).
- pre-emit HARD GATE re-validates the built CbTx (both roots + all height-class +
  bestCL + creditPool checks) before serve; discards to fallback on any failure.

Reward-safety posture: --embedded-mainnet is default OFF (unconfigured mainnet
nodes are byte-unchanged dashd-fallback). The embedded-ON daemonless seed-lag
(arm falls back rather than serving stale at some heights) is deferred as a
separate goal; ON is reward-safe (worst case = fallback, never a bad block).

Tests (dash embedded suite): node_coin_state 15, coin_state_maintainer 11,
stratum_work_source 38, embedded_gbt 10, mnlistdiff_root_parity 4, byte_parity 4,
quorum_root 11, mempool 19, node_reception_wire 21, header_chain 8. c2pool-dash
builds+links against the #781 io-decoupled main_dash.
frstrtr pushed a commit that referenced this pull request Jul 21, 2026
…reward-safe --embedded-mainnet (default OFF)

Rebased onto master (#781 io-decouple + #773/#777/#775/#782). Integrates the
embedded DASH template arm into the decoupled tip-poll/rpc_pool architecture WHILE
preserving both: #781's refresh_executor/resource_template_now split AND every
v0.2.4 embedded guard. Default behaviour is UNCHANGED and reward-safe — with
--embedded-mainnet OFF (the default) the template stays arm=dashd-fallback, the
path the hotel runs, byte-for-byte.

Wires the vendored SML/QuorumManager/CCbTx machinery into the live embedded
template so the daemonless arm assembles a real DIP-0004 type-5 CCbTx, proven
byte-identical to a real dashd getblocktemplate from the raw mnlistdiff wire
(merkleRootMNList + merkleRootQuorums recomputed from wire; bestCL; creditPool
accrual). The mainnet gate lifts behind the explicit --embedded-mainnet opt-in.

Consensus guards (all fail-closed to the reward-safe dashd fallback):
- merkleRootQuorums: hash ALL active commitments (no rotated per-index dedup) —
  byte-identical to dashd (109-quorum from-wire KAT).
- H-1: a malformed quorum tail wipes + full-resyncs (never papered over by a
  later incremental; base-continuity tightened).
- DKG mining-phase heights (is_dkg_commitment_window over every enabled llmqType)
  and superblock heights: refuse embedded.
- bestCL freshness: committed ChainLock must be within one block of the tip.
- creditPool: INDEPENDENT seed-height gate (the seed cbTx's own nHeight must equal
  the tip) — catches a seed one block behind that value/hash self-checks miss;
  pre-emit value re-check + serve-time cache re-validation for the build-vs-serve
  skew; GBT-xcheck backstop cross-checks the embedded creditPool vs dashd.
- C-3: the embedded block excludes all DIP special txs (1-4,6,8,9) — their
  own-block state effects are not modelled in the committed CbTx (Phase-2).
- pre-emit HARD GATE re-validates the built CbTx (both roots + all height-class +
  bestCL + creditPool checks) before serve; discards to fallback on any failure.

Reward-safety posture: --embedded-mainnet is default OFF (unconfigured mainnet
nodes are byte-unchanged dashd-fallback). The embedded-ON daemonless seed-lag
(arm falls back rather than serving stale at some heights) is deferred as a
separate goal; ON is reward-safe (worst case = fallback, never a bad block).

Tests (dash embedded suite): node_coin_state 15, coin_state_maintainer 11,
stratum_work_source 38, embedded_gbt 10, mnlistdiff_root_parity 4, byte_parity 4,
quorum_root 11, mempool 19, node_reception_wire 21, header_chain 8. c2pool-dash
builds+links against the #781 io-decoupled main_dash.

CI-fold: build.yml "Build tests" allowlists test_dash_embedded_gbt but not
the two byte-parity executables, so CTest registered them (gtest_add_tests AUTO)
while CI never built them -> ctest "Not Run" exit 8 (Linux x86_64 red), and the
from-wire merkleRoot + real-dashd CbTx round-trip proofs ran in NO CI job.
Folded test_dash_embedded_cbtx_byte_parity.cpp + test_dash_mnlistdiff_root_parity.cpp
INTO the test_dash_embedded_gbt executable (distinct TEST suites; DASH_FIXTURE_DIR
moved to that target) and removed the standalone add_executable/gtest_add_tests
registrations. test_dash_embedded_gbt 10 -> 18 cases (byte-parity 4 + from-wire 4),
all green; no unbuilt-executable CTest registrations remain.
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