Skip to content

vardiff oscillates at the shipped defaults: six shares a window is not a rate - #80

Open
Wired4ncer wants to merge 1 commit into
LayerTwo-Labs:mainfrom
Wired4ncer:up/vardiff-sample-floor
Open

vardiff oscillates at the shipped defaults: six shares a window is not a rate#80
Wired4ncer wants to merge 1 commit into
LayerTwo-Labs:mainfrom
Wired4ncer:up/vardiff-sample-floor

Conversation

@Wired4ncer

Copy link
Copy Markdown
Contributor

Problem

The rate loop retargets on however many shares the window happened to hold, with no minimum.

At the shipped defaultsvardiff_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), so ratio = observed / target leaves 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:

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. observed_spm was 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 = 0 restores the previous behaviour exactly.

Tests

Three, and the suite goes 478 → 496.

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. 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. The min_samples = 0 test is its negative control. The idle-step test pins the resulting difficulty, so the step cannot move without coming through it.

Mutation-verified:

Mutation Result
sample floor removed 2 checks fail
idle step never applied 1 check fails
extension bound ignored 1 check fails

make test clean.

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.
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