[Draft] feat(vardiff): decline-safe vardiff champion + simulation framework + proof#2154
[Draft] feat(vardiff): decline-safe vardiff champion + simulation framework + proof#2154gimballock wants to merge 11 commits into
Conversation
11b2560 to
88d8d1d
Compare
|
The code is cheap and only meant to demonstrate the feasibility, but the concept ack revolves around these points imo:
|
| | share/min | rate | p10 | p50 | p90 | p99 | | ||
| | --- | --- | --- | --- | --- | --- | | ||
| | 6 | 83.3% | 10m | 12m | 21m | 25m | | ||
| | 12 | 95.4% | 10m | 10m | 20m | 25m | | ||
| | 30 | 99.5% | 10m | 10m | 15m | 25m | | ||
| | 60 | 100.0% | 10m | 10m | 10m | 20m | | ||
| | 120 | 100.0% | 10m | 10m | 10m | 15m | |
There was a problem hiding this comment.
The first row here shows results of the convergence time test for the the default case (6 spm).
The convergence times are between 10 and 25 minutes, with total failures to converge (w/in quiet_window_secs of simulated time) occurring 17% of the time!
the next few rows describe the results for faster share rates. the most extreme times (25m reduces to 15m) and the total failure cases generally disappear around 30 spm.
| ## Settled accuracy (stable load, post-convergence) | ||
|
|
||
| `|final_hashrate / true_hashrate - 1|` at trial end. Smaller is better. | ||
|
|
||
| | share/min | p10 | p50 | p90 | p99 | | ||
| | --- | --- | --- | --- | --- | | ||
| | 6 | 0.0% | 4.9% | 23.6% | 70.3% | | ||
| | 12 | 0.0% | 0.0% | 12.3% | 26.9% | | ||
| | 30 | 0.0% | 0.0% | 0.8% | 15.6% | | ||
| | 60 | 0.0% | 0.0% | 0.0% | 3.1% | | ||
| | 120 | 0.0% | 0.0% | 0.0% | 0.0% | | ||
|
|
||
| ## Steady-state jitter (fires per minute) | ||
|
|
||
| Post-convergence rate of vardiff fires. Smaller is better — ideal is zero under stable load. | ||
|
|
||
| | share/min | p50 | p90 | p99 | mean | | ||
| | --- | --- | --- | --- | --- | | ||
| | 6 | 0.000 | 0.200 | 0.385 | 0.059 | | ||
| | 12 | 0.000 | 0.077 | 0.217 | 0.019 | | ||
| | 30 | 0.000 | 0.000 | 0.067 | 0.002 | | ||
| | 60 | 0.000 | 0.000 | 0.000 | 0.000 | | ||
| | 120 | 0.000 | 0.000 | 0.000 | 0.000 | |
There was a problem hiding this comment.
These two metrics (proximity to true hashrate and post-converged adjustments) show a similar trend,
Lots of undesired behavior in the extreme cases (top 10%, top 1%) of 6 shares/min case that is alleviated at higher share rates.
| ## Reaction time to a 50% drop (step at 15 min) | ||
|
|
||
| | share/min | reacted | p10 | p50 | p90 | p99 | | ||
| | --- | --- | --- | --- | --- | --- | | ||
| | 6 | 69.7% | 1m | 3m | 5m | 5m | | ||
| | 12 | 54.8% | 1m | 3m | 5m | 5m | | ||
| | 30 | 32.6% | 2m | 4m | 5m | 5m | | ||
| | 60 | 16.3% | 3m | 5m | 5m | 5m | | ||
| | 120 | 8.6% | 4m | 5m | 5m | 5m | | ||
|
|
||
| ## Reaction sensitivity (P[fire within 5 min of step change]) | ||
|
|
||
| | Δ% | 6 | 12 | 30 | 60 | 120 | | ||
| | --- | --- | --- | --- | --- | --- | | ||
| | -50% | 0.70 | 0.55 | 0.33 | 0.16 | 0.09 | | ||
| | -25% | 0.44 | 0.23 | 0.08 | 0.00 | 0.00 | | ||
| | -10% | 0.39 | 0.15 | 0.02 | 0.00 | 0.00 | | ||
| | -5% | 0.40 | 0.15 | 0.02 | 0.00 | 0.00 | | ||
| | +5% | 0.39 | 0.13 | 0.02 | 0.00 | 0.00 | | ||
| | +10% | 0.42 | 0.17 | 0.03 | 0.00 | 0.00 | | ||
| | +25% | 0.48 | 0.23 | 0.07 | 0.01 | 0.00 | | ||
| | +50% | 0.64 | 0.47 | 0.32 | 0.22 | 0.29 | |
There was a problem hiding this comment.
These tables show how long it takes for vardiff to respond to an unexpected change in hashrate. Where the changes are to either increase or decrease by proportional amounts anywhere from 5% to 50%.
The first table specifically looks at a 50% draw down showing that a full 30% of the time vardiff fails to adjust after 5 min. The next few rows show that the situation worsens at higher share rates, at 120 spm 91% of the trials failed to adjust after 5m.
The second table shows that this effect is basically the same for hashrate changes in the opposite direction and also that changes of lesser magnitude respond much more quickly.
| //! - **Convergence rate**: `current >= baseline - 0.01` | ||
| //! - **Convergence p90**: `current <= baseline * 1.10` | ||
| //! - **Settled accuracy p50 / p90**: `current <= baseline * 1.15` | ||
| //! - **Jitter p50**: `current <= baseline + 0.02` (absolute; baseline can be near zero) | ||
| //! - **Jitter p95**: `current <= baseline * 1.25` | ||
| //! - **Reaction rate**: `current >= baseline - 0.02` | ||
| //! - **Reaction p50**: `current <= baseline * 1.20` | ||
| //! - **Sensitivity at large |Δ| (|Δ| >= 50%)**: `current >= baseline - 0.02` | ||
| //! - **Sensitivity at small |Δ| (|Δ| <= 5%)**: `current <= baseline + 0.05` |
There was a problem hiding this comment.
Convergence rate: Must be no more than 1% slower than the baseline convergence time
Convergence p90: The slowest 10% convergence times must be within 10% of the baseline's convergence time
Settled accuracy: must be within 15% of baseline's accuracy for the slowest 50% / 10%
Jitter p50/p95: must be within 2% and 25% of baseline
...etc.
You see the pattern, there are lots of magic thresholds in this portion of the code that are arbitrarily chosen at this point and fair game for analysis.
5cbed7c to
85d6f8b
Compare
|
after some optimization I got
|
I'm so excited to see people other people nerding out on vardiff with me! Thank you! A couple things I noticed in your results, the 2m convergence time is impressive but your response to a 50% hashrate drop only succeeds in readjusting 4.4% of the time. I'm not sure how best to balance those two metrics but probably not one at the expense of the other. |
2d10f57 to
414afbb
Compare
211bc98 to
2a88fde
Compare
|
Some learning's I had @gimballock
|
63a19d0 to
a18c3a3
Compare
|
Thanks for these insights @adammwest — especially the point about fitness decomposition and normalization. A lot of what you're describing matches the evolution I've gone through on this PR, so let me give a timeline of Phase 1: Basic metrics + simulation harness Initially I focused on three metrics I thought were important: convergence time, jitter, and accuracy. These were evaluated via a time-compressed simulation that replays a synthetic share stream through the vardiff Phase 2: Decomposed pipeline model I wanted to make algorithm search more systematic, so I decomposed "a vardiff algorithm" into four independent, replaceable components: estimator, statistic, boundary, and decision rule. The idea was to mix-and-match This model worked well for the classic algorithm, the parametric variant, and the EWMA approach. But when I tried to embed a Bayesian model, it broke down — the components aren't truly independent. There's a sequential The resulting three-stage pipeline (Estimator → Boundary → UpdateRule) is what's in this PR. It successfully hosts the classic algorithm, EWMA, AdaCUSUM, and could host a Bayesian approach. Phase 3: Aggregate fitness metric To your point about "how you combine all metrics into a final value" — we now have a configurable aggregate metric that allows weighting across the underlying measurements. This addresses exactly the gaming concern you Your suggestion to separate fitness into improvement vs. regression categories per scenario group (stable, coldstart, reaction) is a good one. Currently the regression test does compare per-cell, so a coldstart regression Phase 4: Realistic operating conditions After discussions with hardware engineers, I retuned the test scenarios to realistic share rates (2–30 spm instead of the earlier 6–120 range). The engineers confirmed that responding to partial hardware failures and Current direction I've backed off from prioritizing convergence speed after seeing overcorrection in practice. The current focus is on:
On your point about normalization: agreed, and the per-metric tolerance budgets in the regression test (absolute slack + optional multiplicative slack) are our current mechanism for this. Open to suggestions on better |
|
Hi, this is quite a detailed analysis and great decomposition of relevant parts. Have you had a look at how ckpool implements this? I think he has quite naturally arrived at a very optimal state, balancing the different metrics. This is the repo, you'll have to grep through to find the vardiff implementation: https://github.com/ckolivas/ckpool I've re-implemented his approach in Rust as well and made it a bit more configurable here: https://github.com/parasitepool/para/blob/master/src/vardiff.rs Would be interesting to see how that algorithm performs in your benchmarks. |
Thanks for the info @paratoxicdev , I will add this to my investigation. I know that is a sv1 native pool but i will see what bits of his research crossover to sv2 context and see if it's competitive! |
|
Here is a breakdown of the calibration comparisons I made between the real hashrate tests and the simulation results confirming the predictions with the understanding that; with this algorithm responsiveness scales with the age of the connection. Suggesting that this detail be included in the simulation so metrics are more directly comparable: |
006363a to
a58132b
Compare
c66784a to
f3ba47b
Compare
…erified deltas, body rewrite, vnprc reply (NOT pushed) PR stratum-mining#2188 (the clean upstream production extraction, distinct from stratum-mining#2154) was opened before the decline-safety arc and ships the SUPERSEDED contender, not the champion. Verified against source: stratum-mining#2188 = EWMA120/AsymCUSUM-at-10/0.2→0.4 (full_remedy- adjacent); champion (champion_composed, composed.rs:261) = EWMA360/AdaptiveSignPersist (seam 6)/AccelRetarget(0.2,0.6,0.05). All three stages differ. Also verified the vnprc review thread: his June-11 objection cites the EXACT job_id_to_target lines that became the corpus's §6(ii) no-lost-work retraction — he was right, independently, three weeks early. Two stale justifications now sit in the thread: vnprc killed stratum-mining#1 (in-flight shares); the arc killed stratum-mining#2 (the fitness sweep, my June-12 defense) by replacing fitness-selection with the decline-safety constraint. Prepared (held for owner timing): the champion extraction spec + cross-check, the PR-body rewrite (360 not 120, AdaptiveSignPersist not CUSUM-at-10, selection = decline-gate not fitness, remove the in-flight-shares framing), and a draft vnprc reply that closes the loop with the current (stronger) justification. The force-push to the public PR + the posted reply are the loud event — explicitly NOT done here; they go together on the owner's go. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clean extraction of the best-performing vardiff algorithm from the simulation framework in stratum-mining#2154, with all test scaffolding, traits, and alternative algorithm implementations removed. The previous VardiffState used a fixed time-dependent threshold ladder and full retarget. This produced: - 6.6% median settled error (p99: 30% at low SPM) - 5–9 minute cold-start convergence (p90) - 33% detection rate for 10% hashrate declines (thermal throttle, failing ASICs) - 28% target overshoot during cold-start ramp (p99 at SPM 6) The new algorithm (EWMA + adaptive boundary + accelerating partial retarget): - Settled accuracy: <3% median error across all SPM - Cold-start overshoot bounded to <10% (was 28%) - Jitter: 0.03 fires/min at low SPM (was 0.06) — half the unnecessary retargets - Small-change detection: 85% reaction to -10% steps at SPM 6 (was 33%) - Transient disconnects recover in 1–2 fires rather than requiring a full cold-start ramp (20%/fire partial retarget vs old algo's 50–67% slash) - Asymmetric cost: loosening fires 3x faster than tightening, because loosening is free but tightening rejects in-flight shares Breaking: adds private fields to VardiffState (previously all-pub). Requires channels_sv2 major version bump. Public constructor API (new, new_with_min) and Vardiff trait interface are unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…itness-pick contender) This PR's flat-struct extraction shipped the mid-arc fitness-selected contender (EWMA 120 / AsymmetricCUSUM-at-10 / retarget 0.2→0.4). The arc's final selection criterion is decline-safety (a hard no-spiral constraint), not scalar fitness — and the gate-passing champion differs on every stage. Replace the params with the champion, verified parameter-for-parameter against champion_composed (the exact constructor the decline-safety gate guard builds and validates in stratum-mining#2154): - Estimator: EWMA 120s → 360s (the τ-safety-valley floor; shorter fails the gate). - Boundary: seam 10 → 6; high boundary AsymmetricCUSUM → sign-persistence CUSUM (adds the consecutive-tick discount); tighten_multiplier 3.0 → 8.0. The stronger asymmetry is the selection-criterion change made concrete: a hard decline-safety constraint demands more tightening-reluctance than a stability-weighted fitness score did. - Update: accelerating retarget eta_max 0.4 → 0.6, acceleration 0.2 → 0.05. Doc comment rewritten: the asymmetry is dangerous-direction (decline-safety) protection, NOT "tightening rejects in-flight shares" (which it doesn't — the pool validates each share against the per-job target snapshotted at job creation; this was a reviewer's correct catch, now retracted in the corpus). Tests updated to the champion's contract (they encoded the contender's looser single-tick behavior): the champion requires SUSTAINED evidence to tighten (8× multiplier + slow EWMA), so asymmetry is now asserted as a sustained-evidence property — deep loosening fires within minutes, symmetric tightening is the deliberately-reluctant direction. Stale comments (3× threshold, eta 0.4, seam 10) corrected; the low-spm PoissonCI test moved to spm=4 (genuinely below seam 6) so it exercises the branch it names. 14 vardiff tests pass; fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the all-pub threshold-ladder VardiffState with a flat-struct adaptive EWMA controller — the decline-safety-selected champion, the clean production extraction of the algorithm derived in stratum-mining#2154. Single-struct replacement: no trait scaffolding, no alternative implementations. Three inline stages (behaviorally identical to the champion_composed reference that passes the decline-safety gate in stratum-mining#2154): - Estimator: EWMA, tau=360s (the τ-safety-valley floor; shorter windows fail the slow-decline gate at sparse rates). - Boundary: adaptive at a seam of 6 spm — PoissonCI below (sparse-data conservatism), sign-persistence CUSUM at/above. The boundary protects the dangerous (tightening) direction two ways: an 8× asymmetric multiplier and a sign-persistence discount that relaxes only after consecutive same-direction ticks. This is decline-safety / dangerous-direction protection — NOT a "tightening rejects in-flight shares" cost (it doesn't: the pool validates each share against the per-job target snapshotted at job creation, which the vardiff path never mutates). - Update: accelerating partial retarget, eta 0.2 → 0.6. Selected by a decline-safety minimax (a hard no-spiral constraint), not a scalar fitness score — which is why the asymmetry is strong (8×): a safety constraint demands more tightening-reluctance than a stability-weighted fitness metric. Breaking: adds private fields to VardiffState (requires channels_sv2 major bump); shares_since_last_update now means "shares since last evaluation tick." Public constructors (new, new_with_min) and the Vardiff trait are unchanged. Tests: implementation-specific suite in test/classic.rs asserting the champion's contract (sustained-evidence tightening reluctance, the asymmetry, the boundary seam, partial-retarget damping), plus a teeth-bearing reset-cleanliness invariant for the sign-persistence state. 15 vardiff tests; fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25b2fed to
e7817b0
Compare
… pipeline) The shipped variable-difficulty controller for channels-sv2, built as a three-stage decomposition (Estimator / Boundary / UpdateRule) composed via Composed<E,B,U>. The champion is EwmaEstimator(360) / AdaptiveSignPersist(spm6) / AcceleratingPartialRetarget — selected by a decline-safety minimax (the death-spiral gate), not by scalar tracking fitness. Includes the estimator family (EWMA, Bayesian, ckpool decay, Holt), the boundary family (PoissonCI, SignPersistenceCusum, adaptive), the update rules (partial/accelerating retarget), and the contrast controllers (classic SMA, pow2-PID). Controller-layer tests live in src/vardiff/test/ and exercise the pipeline independently of the simulation framework. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
558c037 to
cc9834f
Compare
…ards Deterministic in-process framework for characterizing any Vardiff implementation. Decomposes a controller into the three orthogonal stages and characterizes each in isolation, so a metric change is attributable to the stage that changed. Provides the scenario DSL, the Grid sweep harness, the Metric trait, ~75 analysis/figure bins, and pinned baselines (champion/classic) with CI regression guards — including the load-bearing decline-safety champion gate and the classic↔champion behavioral equivalence test. Cargo.lock + rust-toolchain pinned for reproducibility. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92491a4 to
bfca487
Compare
…records, essays, figures The full proof and reader-facing layer behind the controller and framework: - information-floor.md — the paper: the closed information-floor theory, the death-spiral mechanism, the decline-safety selection criterion, and the metric it implies, each result labelled theory / simulation-only / hardware. - THEORY.md — the derivation-and-falsification notebook (reasoning-in-progress). - docs/records/ — the supporting record (architecture, findings, the per-controller investigations, test specs, and status registers), with README.md as the index. - docs/essays/ — the two-part plain-language article (why / how), self-contained (vendored fonts + fallback stacks), byline included. - docs/figures/ — the generated SVG figures the paper and essays embed. - docs/claims/ — the claim-warrant validator and its fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bfca487 to
d9d8619
Compare
…ning#2221 class) Records the bounded result of a parallel source-audit of channels_sv2's public input surface for the stratum-mining#2221 bug class (functions accepting non-finite/out-of-range numeric input that produce garbage/dangerous control values). Confirmed independent findings: A (vardiff new_with_min NaN-disables-clamp) + B (try_vardiff divisor finiteness), both fixed on branch fix/vardiff-reject-non-finite-hashrate; C (server set_nominal_hashrate unvalidated store) as a defense-in-depth note. Six garbage-target caller sites are the blast radius of stratum-mining#2221 (closed on its merge), not new findings. Cleared the false suspects (hash_rate_from_target, client setters, share-accounting sums) — the cleared set outnumbers the confirmed. Sequencing decided: let stratum-mining#2221 merge first, prepare A+B ready-but-unopened, then let maintainers' convention choose the tracking construct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plain-language 'why'/'how' essays (tier 1) assume no statistics; the technical report information-floor.md (tier 3) is full rigor. This primer is the bridge between them — a ground-up, interactive walkthrough of the estimation theory (the precision floor, the collapse) with no prior stats assumed, ending by pointing the reader on to the report. Placed at the docs root beside information-floor.md (NOT in essays/, which would mis-signal its depth). Standalone artifact: it does not yet wire into the reading path (no essay->primer pointer added, to avoid editing the review-branch essays), and references the report in prose rather than as a hyperlink — both deferred until a whole-document read confirms it's ready to advertise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the primer as the middle tier of the reading path: both essays now
point to information-floor-primer.html as the on-ramp ('the reasoning behind
the numbers, no prior statistics assumed') alongside the existing report
link, and the primer's closing now links onward to information-floor.md.
Full chain essays -> primer -> report, all relative paths verified to resolve.
Also folds in the earlier essay edit: the square-root formula (and its two
callbacks) removed from better-vardiff.html in favor of the plain
'diminishing returns' intuition — the formal law stays in the report tier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Step 10's submitting-decline vs. silence explanation, and extend the plot3 interactive with a signed hashrate-trend control: a transient decline drifts the cloud toward over-difficulty (champion tracks, classic strands), and a severe decline crosses into silence where the share-only classic freezes while the champion's idle path recovers. Steady calibration (champion +2.7%, classic +22%, slow-decline.rs) preserved via a hysteresis latch, stable across the run length. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a subsection to §07 (the safety hand-off) contrasting the three ways a vardiff can keep easing once shares thin out: share-driven (no idle path, discontinuity at the disconnect line), explicit timer (a bolted-on second mechanism with a seam), and gain-based (the decaying estimator IS the idle path, smooth through disappearance). Framed as a simpler-vs-costlier spectrum, not a competence ranking. External archetypes named and scoped to what's source-verified; the gain-based smooth-through-disappearance claim verified against the shipped estimator (silence is the zero-share case of the one blend rule, no separate silence branch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
||
| <h3 style="margin-top:34px;">Three ways to keep easing when the shares thin out</h3> | ||
| <p>That hand-off is worth pulling out on its own, because it is a real fork in the design and this build sits at a particular point on it. When a miner falls quiet, <em>something</em> has to keep easing the difficulty without a fresh share to act on — an <strong>idle path</strong>. There are three ways to build one, and they are not so much better-and-worse as simpler-and-costlier, each with its own consequence at the moment a miner actually disappears.</p> | ||
| <p><strong>Share-driven — no idle path at all.</strong> The simplest choice: recompute difficulty only when a share arrives. It is clean and it tracks a healthy miner fine — but it has no move to make when the shares stop, because the thing that triggers a move is the thing that has stopped. A miner sliding into silence is never eased; it drifts until its inter-share gap crosses the pool’s liveness timeout and it is <em>dropped</em> — hardware still running. The discontinuity is real and it lands at the disconnect line: fine, fine, fine, gone.</p> |
There was a problem hiding this comment.
@paratoxicdev I think i resolved the share-based vs timer-based debate here and there is a clear winner, very unexpected and deep finding, what do you think?
There was a problem hiding this comment.
@paratoxicdev — following up on the ckpool-family thread with one more finding, since it lands right on your architecture and I think it's the good kind of news (an easy fix, not a redesign).
First, the motivation, because it's why I keep pulling on this thread rather than just benchmarking: a vardiff controller's job is to hold a connection at target as its hashrate changes — in either direction. The thing I've been checking every controller against is one question: is it safe for a miner whose hashrate is falling? Not "does it track well," but "if this miner degrades, does the controller follow it down, or does it strand the miner at a difficulty it can no longer meet?" That's a correctness property, and it turns out to be the axis controllers actually differ on (they're all pinned to the same information floor on the tracking side).
On the ckpool family specifically — and I mean this as a compliment to how well it's built, because the gap is narrow: it eases beautifully on a miner that's slowing down but still submitting. Your HYSTERESIS_LOW = 0.5 does exactly the right thing — rate drops below half target, difficulty comes down. No complaints there; that's the common case and it handles it.
The gap is the silent miner, and it's structural to share-gating rather than a tuning choice. Because the retarget path only runs on a share arrival, a miner that stops submitting entirely — curtailment, a thermal stall, a board that drops off — produces no share, so nothing calls the handler, so it's never eased. It just ages out at the pool's liveness timeout and gets dropped. The controller can't rescue a miner that's gone quiet, because "gone quiet" is exactly the condition that stops the controller from running. It's a death-spiral-as-information-loop, and it sits right at the disconnect boundary.
Here's why I say it's one of the easier cases to fix: the fix already exists inside your own family. MiningCore carries essentially the identical config shape (targetTime / variancePercent / retargetTime), but it has a second method NOMP and ckpool don't — IdleUpdate — that eases difficulty on a wall-clock timer without waiting for a new share: once a quiet interval passes, it grows the implied gap and lowers difficulty. That's the missing piece. A share-gated controller plus a timer-driven idle-ease path covers both cases — the submitting-but-declining miner (which you already handle) and the gone-silent miner (which the idle path catches). It's an additive branch, not a rewrite of the estimator or boundary you've already tuned well.
I'd characterize the family, for the writeup, as share-driven — quick and natural on live share streams, with the one blind spot being the miner that stops producing them. And I'd point at MiningCore's IdleUpdate as the reference for closing it, since it's the same lineage and stays in the idiom you're already in.
Where I'm headed with all of this, and why I'd love ckpool-family input rather than just to report at you: I'm not trying to crown one controller — I'm trying to get decline-safety understood as a requirement and get a correct implementation into the SV2 standard, so pools get safe behavior by default instead of each rediscovering the same gap. The advice is the same one I'm giving everyone, my own old pools included (they had the wrong-way shape too). The payoff I actually care about is the use cases it unlocks: pools that handle declining and wobbling hashrate gracefully can serve loads others can't — curtailment, solar and other intermittent power, anything where the hashrate legitimately breathes up and down. Getting this right across public pools is what makes those workloads viable on the hardware side.
Your algo is proven on real hashrate at every scale, which is exactly why it's worth closing this one gap — it's most of the way there already. Happy to run your parasitepool/para version through the decline scenarios (silent-miner included) if you want the concrete numbers, and would genuinely value your read on whether the idle-path framing matches what you see in production.
There was a problem hiding this comment.
Hey @gimballock, that's good to know.
So only a silent miner needs to be handled? All other cases perform competitively within your framework?
As you mentioned the ckpool code (and also my Rust implementation) handles silent miner through dropping it after around 10 minutes of no shares submitted. I'm not sure I understand why sending a lower difficulty to a silent miner would revive it. As part of the easing out silent miners ckpool and my code already send pings and mining.notify to entice it to come back. One scenario I guess I can think of it the hashrate drop 99% and the miner is incapable of creating a single share that meets that target within the 10 minute timeframe. I would have to do some math to figure out how likely it is to not hit a share from a 99% drop within 10 minutes if the normal target share rate is about 0.3 per second.
I'd be very interested to see you run the para pool command through your scenarios. Let me know if you need help running it. It supports all networks (signet, regtest, etc.) and also has flags to tweak the vardiff algo to the machines your testing it with (higher or lower start diff for example), it even supports fractional difficulty for signet.
There was a problem hiding this comment.
So is this: sv2/channels-sv2/sim/docs/essays/building-a-vardiff.html the only file I should have a look at or some other stuff in this branch?
There was a problem hiding this comment.
Sorry for the delay, I was working up a better starting point for this work b/c the PR here is a bit difficult to navigate. Feel free to read the essays or the report files for the nitty gritty details, or you can find me on the sv2 discord to chat but now I can say that this is the best intro for this work:
https://delvingbitcoin.org/t/research-a-clockless-vardiff-strands-a-slowing-miner/2718
… in the README The README predated the survey of deployed open-source vardiff controllers and its decline-safety decision tree. Add a name-clean pointer in the Docs section describing the survey by purpose (does it ease without a fresh share; is the ease trigger reachable on a real decline; is safety readable off the construction or resting on where a learned policy converged) and directing readers to the records index. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sponsiveness probe Two additions to the decline-safety instrumentation, both over the SHIPPED controllers (champion_composed / classic_composed / pow2_pid): - decline_safety.rs: `settled_belief_ratio_after_silence` + the `abrupt_silence_sorts_ease_from_strand` CI guard (`#[ignore]`, runs in the --ignored job). Asserts the taxonomy after a curtailment to silence: the timer-driven SRI controllers ease toward the miner via the idle-tick path (belief ratio -> ~0) while the adverse-gain quantized PID stays pinned (~1), with a discrimination-margin check against the pow2 control. Complements the existing survivable-decline margin guard (bounds the silent case, not the ramp). - bin/gradual-responsiveness.rs: sweeps decline rate 3..600 %/hr and reads the decline-phase lag (mean over-difficulty during the ramp). Result: gradual is the EASY regime, not the sluggish one — lag is worst when fast, monotone, both SRI arms. Refutes "safe-abrupt but sluggish-gradual"; the dangerous regime is the abrupt cut. Mechanism: slower decline = longer dwell per level = more shares to measure, which beats the fixed-window lag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Characterizes recovery from over-difficulty — the loud side, uncharacterized by the decline probes. Sweeps ease-depth × ramp-speed, measuring peak realized-spm and transient duration, with an optional max-share-rate cap. Finding: the recovery overshoot decomposes cleanly — - STRUCTURAL peak ~1/(1-ease_depth), tuning-independent (champion and a jump-to-estimate contrast arm have identical peaks); bounded by a share cap. - TUNABLE duration (a jump-to-estimate arm cuts it ~35%). The jump-to-estimate arm (FullRetargetNoClamp, the cost-blind oracle) is a MEASUREMENT CONTRAST, not a production candidate: it FAILS the decline-safety gate (+6.90% worst-settled-e vs champion +2.67%, 5% gate) — the loud-side duration win costs quiet-side safety. Documented in the bin's doc-comment. Backs the essay's ramp-up caveat: overshoot is structural-in-peak (cap-bounded, not a decline-safety property) and tunable-in-duration (loud-side, orthogonal to the idle path). Decline-safety stays architectural. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



Adds a deterministic in-process simulation framework that characterizes any
Vardiffimplementation, the decline-safe champion controller it selected,and the proof that argues both. Read the three commits in order — controller,
framework, docs — each is a coherent review unit on its own.
What this is
A vardiff controller's only observable is its own tracking error, and that one
fact bounds what any controller can do. The framework measures it, the proof
derives the bound, and the champion is the existence proof that the safe corner
of the achievable frontier is occupiable. The headline is structural, not a
horse race: across the operating band every reasonable controller is pinned to
the same information floor (~12% best-to-worst spread), so the residual axis is
safety on a decline, not agility — and that is the one axis controllers
genuinely differ on.
This is the research/proof branch. The clean production extraction that ships
the champion is #2188 (a single flat-struct replacement of
VardiffState,no scaffolding); this branch is the framework that selected it and the proof that
argues it.
The three commits, in reading order
feat(vardiff): decline-safe champion controller— the thing thatships. The three-stage
Composed<Estimator, Boundary, UpdateRule>pipeline;the champion is
EwmaEstimator(360) / AdaptiveSignPersist(spm6) / AcceleratingPartialRetarget, selected by minimax over share rate under adecline-safety constraint (the kills are the selection criterion — the
alternatives that died on a sustained decline are why this one ships).
feat(vardiff/sim): in-process simulation framework + CI regression guards— the apparatus that selected it. Deterministic per-tick Poisson sim, the
metric, the grid, the decline-safety gate, and three CI-guarded regression
tests that re-derive the selection criteria on every run. Carries the pinned
reproduction environment (
Cargo.lock+ toolchain) and the frozenchampion/classic baselines the guards assert against.
docs(vardiff): the proof corpus— the proof and how it's argued.information-floor.md(the closed theory, each result labelled theory /simulation-only / hardware),
THEORY.md(the derivation-and-falsificationnotebook), the two plain-language essays (why → how), the investigations
(incl. the source-verified finding that a deployed pow2-in-loop deadband
construction is structurally unable to ease on a decline), and the
claim-warrant validator. The proof corpus also includes a source-verified
survey of deployed open-source vardiff controllers and their
decline-safety failure modes (
docs/records/VARDIFF_SURVEY.md) — NOMP,ckpool, MiningCore — finding the failure axis is a structural property
(idle-path + trigger-reachability), not algorithm sophistication.
Reproducibility (stated at the scope it is true)
simulation-result claim in
information-floor.mdmaps, via the READMEreproduction index, to a deterministic binary that regenerates the number
(not a frozen value that could drift from the code). The selection criteria
are CI-guarded.
per-job target snapshot in
src/server/extended.rs(verified; the protocolvalidates each share against the target snapshotted at job creation, which the
vardiff path never mutates) — a protocol fact, reproduced by reading the
pinned source, not by a sim.
Cargo.lockhash + pinnedtoolchain), so a recorded seed reproduces deterministically.
registered and re-runnable; tagging the remaining exploration binaries
(which back no proof claim beyond what the index already covers) is a typed
standing debt the validator surfaces, not part of this PR's reproducibility
guarantee.
Known-open items (disclosed, not surprises)
The claim validator surfaces two standing
REVISITflags by design:pow2-in-loop deadband construction is currently a date + line numbers; should
strengthen to a commit SHA (line numbers drift).
public source and names no operator; naming is a deliberate, separate
decision, not made here.
Scope honesty (from the proof): the decline response is hardware-confirmed in
direction on a sustained 50% drop (eased the safe way, no rejection spike); the
settled over-difficulty figure and the slow-moderate-decline gate remain
simulation results; the cost-model weights are a calibrated judgment.