Nothing limits mining.authorize: budget it per connection and per address - #79
Open
Wired4ncer wants to merge 1 commit into
Open
Nothing limits mining.authorize: budget it per connection and per address#79Wired4ncer wants to merge 1 commit into
Wired4ncer wants to merge 1 commit into
Conversation
A failed authorize -- no worker name, a malformed username, an address that
does not decode -- costs a reject observation and a log line, and nothing
bounded how many of those one client could buy before it had authenticated at
all. It is the cheapest write on the pool and it is open to anyone who can
reach the port. A successful authorize is not free either: it validates an
address and seeds the connection's difficulty, and a client holding one valid
address can repeat it as fast as it likes.
Two limits from one config number, auth_max_failures (default 3), with a
window, auth_fail_lockout_sec (default 60):
per connection the third failure is answered, then the socket is closed;
per address an address that has failed three times inside the window is
refused at the TOP of the handler -- before the params are
read, so no decoding, no reject observation, no log line per
attempt -- and the connection is closed. Logged once per
lockout, not once per refused attempt, for the same reason.
A successful authorize clears the address's record, so a miner that fixes a
typo is not made to wait; the window expiring clears it too. Successful calls
are budgeted as well: past sixty in ten seconds on one connection each is
refused, writes no observation, and spends the failure budget. Sixty because
the ceiling exists to stop deliberate spam, which is hundreds a second, while
a proxy multiplexing many workers over one socket authorizes them in a burst.
The per-address table is fixed and bounded -- 1024 slots, linear probing eight
deep, evicting the earliest window in the run -- so a client spraying
addresses can only ever reset someone else's count, never grow the table. It
is a limiter, not a ledger.
A refusal from the PPS gate is deliberately NOT counted: the miner did nothing
wrong and cannot fix it by retrying differently, so charging it would lock out
honest miners reconnecting exactly when accrual is suspended.
This needs the peer address on the connection, which the tree did not keep, so
accept() records it. IPv4-mapped IPv6 is un-mapped: on a dual-stack listener
every IPv4 client arrives mapped, and without that one client is two keys on
two listeners and its log lines stop matching what ss prints.
auth_fail_lockout_sec = 0 with the budget on is refused at load -- every entry
would expire as it was written, so the per-address half would silently do
nothing while the config said it was on.
Tests: four in test_stratum, four in test_config. The zero-disables test is
the negative control; without it "budget enforced" and "feature switched off"
are indistinguishable. The call ceiling is asserted exactly, so the constant
cannot move without coming through the test. Mutation-verified: never closing
the connection fails 2 checks, never firing the per-address lockout fails 3,
and 60 -> 10 fails 3.
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
mining.authorizehas no limit of any kind on it today.A failed authorize — no worker name, a malformed username, an address that does not decode — costs the pool a
on_rejectobservation and a log line. Nothing bounds how many of those one client can buy, and it can buy them before it has authenticated at all. It is the cheapest write on the pool and it is open to anyone who can reach the port.A successful authorize is not free either: it validates an address and seeds the connection's difficulty, and a client holding one valid address can repeat it as fast as it likes.
max_submits_per_seccoversmining.submit. There is no equivalent for the call that comes before it.Fix
Two limits from one config number,
auth_max_failures(default 3), with a window,auth_fail_lockout_sec(default 60):A successful authorize clears the address's record, so a miner that fixes a typo is not made to wait. The window expiring clears it too.
Successful calls are budgeted as well: past 60 in 10 seconds on one connection, each is refused, writes no observation, and spends the failure budget. Sixty rather than something tighter because the ceiling exists to stop deliberate spam, which is hundreds a second, while a proxy that multiplexes many workers over one socket and authorizes them in a burst is a legitimate pattern that has to clear it.
The per-address table is fixed and bounded — 1024 slots, linear probing 8 deep, evicting the earliest window in the run — so a client spraying addresses can only ever reset someone else's count, never grow the table. It is a limiter, not a ledger: losing an entry can never cost anyone a lockout they had not earned.
One thing deliberately not counted: a refusal from the PPS gate. The miner did nothing wrong and cannot fix it by retrying differently, so charging it would lock out honest miners reconnecting exactly when accrual is suspended — which is when they are most likely to retry.
This needs the peer's address on the connection, which the tree does not currently keep, so the accept path now records it. IPv4-mapped IPv6 is un-mapped: on a dual-stack listener every IPv4 client arrives mapped, and without that the same client is two different keys on two different listeners, and its log lines stop matching what
ssornetstatprints.Config
Ships on. Unlike
max_submits_per_secit refuses nothing a correct miner does — it only shortens how long a client may keep failing.auth_max_failures = 0disables both halves.auth_fail_lockout_sec = 0with the budget on is refused at load: every entry would expire as it was written, so the per-address half would silently do nothing while the config said it was on.Tests
Four in
test_stratum, four intest_config, and the suite goes 478 → 509.The zero-disables test is the negative control: without it, "budget enforced" and "feature switched off" are indistinguishable from the assertions of the other two. The call-ceiling test asserts the count exactly, so the constant cannot move without coming through it.
Mutation-verified, because a green test proves nothing until it is shown to fail:
make testis clean and deterministic: 478 -> 509, 509/509 on every run.One thing worth flagging, which is not mine
While running
make asanI hit an intermittent failure in an existing test,test_vardiff_tracks_miner_local_floor(the two floor assertions aroundtests/test_stratum.c:851). It is a timing margin -- a 1-second vardiff window against an 1,100 ms sleep -- and it only appears when the machine is busy.Measured rather than guessed, running this branch and pristine
mainas separate sanitizer binaries:mainSo it fails on
maintoo and is not introduced here, though this branch does seem to tip it over more readily -- most likely just a slower binary with 31 more tests in the same process. CI runsmake test, notmake asan, so it should not surface there. Mentioning it because it cost me an hour to rule out, and it will cost the next person the same.