vardiff oscillates at the shipped defaults: six shares a window is not a rate - #80
Open
Wired4ncer wants to merge 1 commit into
Open
vardiff oscillates at the shipped defaults: six shares a window is not a rate#80Wired4ncer wants to merge 1 commit into
Wired4ncer wants to merge 1 commit into
Conversation
At the shipped defaults -- vardiff_target_spm = 12 over a 30 s window -- an ON-TARGET connection produces six shares. Poisson noise on six samples is +/-41% (1/sqrt(6)), so ratio = observed/target leaves the [0.5, 2.0] deadband routinely even when the difficulty is already correct, and the controller oscillates instead of converging. The existing 4x step cap bounds each swing; it does not stop it. Observed in production, six consecutive retargets of one worker inside four minutes, on a port fed by a proxied fleet: 500000 -> 1141329 (27.4 spm observed, 12.0 target) 1141329 -> 569753 ( 6.0 spm) 569753 -> 500000 ( 3.6 spm) 500000 -> 1064387 (25.5 spm) 1064387 -> 513667 ( 5.8 spm) 513667 -> 500000 ( 4.5 spm) A 14% difficulty rise cannot cut a share rate 7.6x, so observed_spm was never tracking difficulty -- it tracked which connection the proxy fed that window. vardiff_min_samples (20) makes a window that elapsed but holds too few shares stay OPEN and keep accumulating rather than retarget on noise. vardiff_max_window_mult (8) bounds that extension, so a connection whose difficulty is genuinely far too high still ratchets down. Setting vardiff_min_samples to 0 restores the previous behaviour exactly. vardiff_idle_step (2.0) replaces the flat 4x for any window that never met the sample floor. That is what keeps a quiet connection off vardiff_min: one rig behind a proxy arrives as many connections, each going quiet between bursts, and at 4x a pair of near-empty windows cuts difficulty 16x -- which the miner's own firmware then reports back as "difficulty too low". This is separate from VD_FLOOR_MIN_SAMPLES, which already gates the miner-local-floor detector. That constant guards a different reading of the same window; the rate loop had no sample gate at all. Tests: three. test_vardiff_waits_for_min_samples asserts the NEGATIVE -- shares 1..4 must not retarget -- which is the shape that passes for the wrong reason, so the fixture also asserts those four were ACCEPTED (they reached the retarget path) and share 5 proves a retarget was reachable all along: same connection, same window, same ratio, only the sample count changed. The min_samples = 0 test is the negative control for it. The idle-step test pins the value, so the step cannot move without coming through it. Mutation-verified: removing the sample floor fails 2 checks, never applying the idle step fails 1, and ignoring the extension bound fails 1.
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.
Problem
The rate loop retargets on however many shares the window happened to hold, with no minimum.
At the shipped defaults —
vardiff_target_spm = 12,vardiff_window_sec = 30— an on-target connection produces six shares a window. Poisson noise on six samples is ±41% (1/√6), soratio = observed / targetleaves the[0.5, 2.0]deadband on noise alone, and the controller oscillates around the correct difficulty instead of settling on it. The 4x step cap bounds each swing; it does not stop it.Six consecutive retargets of one worker inside four minutes, from production, on a port fed by a proxied fleet:
A 14% difficulty rise cannot cut a share rate 7.6x.
observed_spmwas never tracking difficulty — it tracked which connection the customer's proxy fed that window.This is not the same thing as
VD_FLOOR_MIN_SAMPLES. That constant already gates the miner-local-floor detector, which is a different reading of the same window. The rate loop itself has no sample gate at all.Fix
Three settings, all with defaults that leave a correctly-sized miner untouched:
vardiff_min_samples(20) — a window that has elapsed but holds fewer shares stays open and keeps accumulating instead of retargeting on noise. Leaving the window open — returning before the reset — is the whole mechanism.vardiff_max_window_mult(8) — bounds that extension, so a connection whose difficulty genuinely is far too high submits almost nothing and still ratchets down.vardiff_idle_step(2.0) — the largest step a window that never met the floor may make. A window that met it keeps the historical 4x.That last one is what keeps a quiet connection off
vardiff_min. One rig behind a proxy arrives as many connections, each going quiet between bursts; at 4x a pair of near-empty windows cuts difficulty 16x and pins the worker to the floor — which the miner's own firmware then reports back as "difficulty too low".vardiff_min_samples = 0restores the previous behaviour exactly.Tests
Three, and the suite goes 478 → 496.
test_vardiff_waits_for_min_samplesasserts the negative — shares 1–4 must not retarget — which is the shape that passes for the wrong reason. Two guards: the fixture asserts those four shares were accepted, so they really did reach the retarget path; and share 5 then proves a retarget was reachable all along, on the same connection, the same elapsed window and the same ratio, with only the sample count changed. Themin_samples = 0test is its negative control. The idle-step test pins the resulting difficulty, so the step cannot move without coming through it.Mutation-verified:
make testclean.