farm tier: give serialising clients a real extranonce at subscribe - #783
Merged
Conversation
A serialising SV1 client — proxies and rented-hashrate marketplaces — waits for the `mining.subscribe` RESPONSE before it will authorise. That response has to carry the real, channel-allocated extranonce, and only the pool can mint one, so the channel must open before `mining.authorize` arrives and its identity cannot hold the miner's address. The address then has to travel per share in the Worker-Specific Hashrate Tracking TLV. This is the pool half of that, and it ships first: a translator that opens channels provisionally is only safe against a pool that already understands them. Rolled the other way round, `build_webhook_user_identity` would splice a worker onto the sentinel and credit the address portion — `sri` — to nobody, which is how #447 misattributed ~395 shares in July. - `PROVISIONAL_CHANNEL_IDENTITY = "sri/donate/provisional"`. It has to be a shape `PayoutMode::try_from` already parses or the channel open is rejected outright, and three segments so it cannot collide with a miner authorising as plain `sri/donate`. Pinned by a test. - `build_webhook_user_identity` returns `Option<String>` and decides from the CHANNEL identity, never by inspecting the TLV. A worker name may legitimately contain a dot (`addr.farm1.rig1` yields `farm1.rig1`, #481), so "the TLV looks dotted" does not imply it holds an address — keying on that would silently reassign `farm1` as a payout target. There is a test for exactly that shape. - A provisional channel whose TLV is absent, empty, or a bare worker resolves to `None` and the share is not credited. Splicing would have produced `sri/donate/provisional.rig1` — an address of `sri` — which looks entirely normal in the logs while paying nobody. `attributable` now folds in that resolution. - The block path stays deliberately ungated: a block is always reported, falling back to the raw channel identity so the record still points somewhere traceable. Behaviour for every channel opened on `mining.authorize` is unchanged — the address still comes from the channel and the worker from the TLV. Claude-Session: https://claude.ai/code/session_01ResUgwsjEwGZ99vZn4hu4R
…s a real extranonce
A serialising SV1 client waits for the `mining.subscribe` RESPONSE before it will
authorise. The channel opens on authorize, so those clients deadlock, and the 1.5s
fallback answers subscribe with an 8-byte all-zero PLACEHOLDER extranonce while the
real prefix is 12 bytes.
That placeholder is not merely approximate, it is unusable. The coinbase declares a
91-byte scriptSig ending in `OP_PUSHBYTES_20`, reserving exactly 20 bytes:
real: en1 12 + en2 8 = 20 ✅
placeholder: en1 8 + en2 8 = 16 ❌ 4 bytes short
so the client builds a coinbase shorter than its own length byte — malformed
transaction, wrong merkle root, every share invalid. `mining.set_extranonce` cannot
rescue it: that is an OPTIONAL extension, and these clients never send
`mining.extranonce.subscribe` (0 occurrences in 24h of fleet logs, 0 on the wire).
Behind `open_channel_on_subscribe`, the channel opens at subscribe under
`PROVISIONAL_CHANNEL_IDENTITY`, so the subscribe response carries the real prefix
first time, for pipelining and serialising miners alike.
The address then cannot live in the channel identity, so it travels per share in the
worker TLV, which now carries the full `<address>.<worker>` in that mode. The pool
half (562225d) uses it verbatim and fails closed if it is missing.
- `tlv_compatible_username` REFUSES rather than truncates. A cut identity is not a
shorter identity: it mangles the payout address, or collapses two workers sharing a
prefix onto one `miner_id`. Its private 32-byte cap was a third copy of a limit owned
by `extensions_sv2::MAX_USER_IDENTITY_LENGTH`; two copies of that constant drifting
is what sent shares out with no TLV at all and misattributed ~395 of them under #447.
It now defers to the single owner.
- `PROVISIONAL_CHANNEL_IDENTITY` lives in `extensions_sv2` alongside the extension it
belongs to, imported by both binaries. Duplicating it is the same failure mode as the
length constant, so it gets one owner from the start.
- `channel_open_requested` guards the open. `channel_id` cannot: it is set only when
`OpenExtendedMiningChannelSuccess` returns, so a pipelining miner's authorize lands
mid-flight and would burn a second upstream channel.
⛔ ROLLOUT GATE, default OFF, so this binary is safe to roll at any time. `pool_sv2`
must understand provisional channels on EVERY node before it is switched on anywhere;
rolled the other way round the pool credits `sri` — nobody. Roll `pool_sv2` fleet-wide,
then flip the flag. Remove the flag once the fleet is on it.
The blockers that shelved #447 are both resolved on `main`: the duplicated
`MAX_USER_IDENTITY_LENGTH` (#422) and `set_difficulty` never reaching the wire (#455,
fixed by #575).
Tests: 62 translator, 35 pool, clippy clean. Not yet exercised end-to-end against a
live pool — the handshake needs a canary with the flag on.
Claude-Session: https://claude.ai/code/session_01ResUgwsjEwGZ99vZn4hu4R
…tranonce prefixes Opening the channel on every `mining.subscribe` was wrong in a way the pool already had a comment about. The pool's extended allocator is `server_id || counter` with a two-byte server id, leaving a 16-BIT counter — 65,535 prefixes for the lifetime of the process, never handed back (#746). Measured on vm1: `sri-pool` up 28.5h, counter at `0x1be3` = 7,139, so ~250/h and roughly ten days of uptime before every channel open starts failing. Two problems with opening unconditionally: - Subscribe-only probes (~15% of connections) would each burn a prefix despite never mining, shortening that window for no benefit. - #746 deliberately moved allocation AFTER validation so an UNAUTHENTICATED client cannot burn the space. A subscribe arrives before any authorize, so opening on it immediately hands that capability straight back — a drive-by prober could exhaust the pool for every honest miner. That is a security regression, not a capacity one. So the open is debounced by `SUBSCRIBE_OPEN_DEBOUNCE` (300ms), sized from measured client behaviour rather than guessed: a pipelining miner's authorize follows its subscribe by ~12ms and a marketplace capability probe disconnects ~10-30ms in. Neither reaches the timer, so the pipelining path is byte-identical to today and probes cost nothing. Only a client still holding the connection open — the serialising shape this exists to serve — opens early, and it waits the channel-open round trip (~50-300ms) instead of the old 1500ms, for a real extranonce instead of an unusable one. Tests: 62 translator, 35 pool, clippy clean. Claude-Session: https://claude.ai/code/session_01ResUgwsjEwGZ99vZn4hu4R
defenwycke
added a commit
that referenced
this pull request
Aug 28, 2026
Caught on the ghost-vm5 canary within four minutes of turning
`open_channel_on_subscribe` on: the real miner was credited as
bc1q7zvdh3…y492.bc1q7zvdh3…y492.bitaxe3
— the address spliced onto itself, a phantom `miner_id` that looks entirely
legitimate. 16 shares over 3m44s, all on vm5, flag now reverted.
WHAT I GOT WRONG
The TLV carried the full `<address>.<worker>` whenever the config flag was on. That
was right for the design as first written, where every channel opened on subscribe.
It stopped being right when the open was debounced (#783): a pipelining miner now
still opens on `mining.authorize` with its OWN `<address>.<worker>` as the channel
identity, so the pool spliced a full identity onto a channel that already carried the
address.
Whether a channel is provisional is a property of the CONNECTION, not of the config.
`channel_opened_provisionally` records it where it is decided — the debounce path that
actually claims the open — and the TLV is keyed on that. A pipeliner authorises ~12ms
in, long before the 300ms timer, so it reads false and sends the worker segment alone,
exactly as before this feature existed.
The payout address survived: `parse_user_identity` splits on the FIRST dot, so the
address resolved correctly throughout and no money moved. The worker did not, which
would have split one miner's stats across two ids.
DEFENCE IN DEPTH
`build_webhook_user_identity` now refuses to splice an address onto itself. The
invariant is that a full TLV only ever accompanies a provisional channel, but a
translator that breaks it should not be able to mint phantoms — which matters during a
mixed-version window, where the pool cannot assume anything about the translator in
front of it. A worker is never legitimately named after the address it sits behind, and
a merely-similar prefix (`bc1qAAAB` behind `bc1qAAA`) is still spliced; both pinned by
test.
⚠ This is the second time a rule that reads correctly in isolation was wrong because it
was keyed on the wrong scope. Prefer "what did this connection actually do" over "what
is this node configured to do" wherever the two can disagree.
Tests: 36 pool, 62 translator, clippy clean.
Claude-Session: https://claude.ai/code/session_01ResUgwsjEwGZ99vZn4hu4R
defenwycke
added a commit
that referenced
this pull request
Aug 28, 2026
…#785) Caught on the ghost-vm5 canary within four minutes of turning `open_channel_on_subscribe` on: the real miner was credited as bc1q7zvdh3…y492.bc1q7zvdh3…y492.bitaxe3 — the address spliced onto itself, a phantom `miner_id` that looks entirely legitimate. 16 shares over 3m44s, all on vm5, flag now reverted. WHAT I GOT WRONG The TLV carried the full `<address>.<worker>` whenever the config flag was on. That was right for the design as first written, where every channel opened on subscribe. It stopped being right when the open was debounced (#783): a pipelining miner now still opens on `mining.authorize` with its OWN `<address>.<worker>` as the channel identity, so the pool spliced a full identity onto a channel that already carried the address. Whether a channel is provisional is a property of the CONNECTION, not of the config. `channel_opened_provisionally` records it where it is decided — the debounce path that actually claims the open — and the TLV is keyed on that. A pipeliner authorises ~12ms in, long before the 300ms timer, so it reads false and sends the worker segment alone, exactly as before this feature existed. The payout address survived: `parse_user_identity` splits on the FIRST dot, so the address resolved correctly throughout and no money moved. The worker did not, which would have split one miner's stats across two ids. DEFENCE IN DEPTH `build_webhook_user_identity` now refuses to splice an address onto itself. The invariant is that a full TLV only ever accompanies a provisional channel, but a translator that breaks it should not be able to mint phantoms — which matters during a mixed-version window, where the pool cannot assume anything about the translator in front of it. A worker is never legitimately named after the address it sits behind, and a merely-similar prefix (`bc1qAAAB` behind `bc1qAAA`) is still spliced; both pinned by test. ⚠ This is the second time a rule that reads correctly in isolation was wrong because it was keyed on the wrong scope. Prefer "what did this connection actually do" over "what is this node configured to do" wherever the two can disagree. Tests: 36 pool, 62 translator, clippy clean. Claude-Session: https://claude.ai/code/session_01ResUgwsjEwGZ99vZn4hu4R
defenwycke
added a commit
that referenced
this pull request
Aug 29, 2026
…se could catch `fd74982f4` changed behaviour that nothing asserted. The suite already had a probe for the re-key — `[411-rekey]` — but it drives the *serializer* shape (subscribe, wait, authorize), where the subscribe response lands before authorise arrives and the post-hoc path never runs. It therefore reports MET on a node where the defect is live, which is how this survived the #783/#784/#785 round. The new case drives the shape the tcpdump actually captured: no `mining.configure` at all, then `mining.subscribe` and `mining.authorize` pipelined into ONE TCP segment. Measured against the live fleet on vm1 today, before the fix: [unneg-extranonce] FAIL — set_extranonce received: YES — never negotiated [411-rekey] MET (informational) So the gate can fail, and it fails on exactly the case the old probe passes. It asserts absence over a 4 s window; the offending notification fires ~1.2 ms in, so the window is evidence rather than haste. ⚠ Still open: this proves the protocol violation is gone, NOT that it was why Braiins disconnects. A synthetic client held the socket for 8 s across the defect without closing, so the disconnect cause remains unproven.
defenwycke
added a commit
that referenced
this pull request
Aug 29, 2026
…ked for it (#786) * fix(translator): only send `mining.set_extranonce` to clients that asked for it `mining.set_extranonce` is an opt-in notification — a client accepts it only if it requested `subscribe-extranonce` in `mining.configure`. The farm-tier path sent it unconditionally, and Braiins' hashrate proxy closes the connection on receipt. Observed on vm1, 2026-08-28, via tcpdump. Braiins subscribes and authorises in one TCP segment, receives a structurally valid job, then sends a clean FIN ~13 ms later: IN {"id":1,"method":"mining.subscribe","params":[]} {"id":2,"method":"mining.authorize","params":["bc1q…loadtest","x"]} OUT {"id":2,…,"result":true} OUT {"method":"mining.set_extranonce",…} <- never negotiated OUT {"id":1,…,"result":[…,"01000d48…",8]} <- real extranonce anyway IN [F.] That connection sends no `mining.configure` at all, so it never opted in. 12,929 farm-port connections in 24 h on one node, median session 0.107 s. The guard could not have worked. `already_subscribed` was inferred from the extranonce still being the `DownstreamData::new` default of 8 zero bytes, which is equally true of "subscribe answered with a placeholder" and "subscribe not answered yet". In the defer-open path the subscribe response is still pending and carries the real extranonce, so the notification is redundant as well as unnegotiated. `Configure::extensions` is private and only the version-rolling pair had readers, so the translator had no way to consult the opt-in; add `subscribes_extranonce()` to the vendored sv1 crate and record it on `DownstreamData` at `mining.configure`. ASICs send `mining.subscribe` first and are unaffected, which is why the live miners never surfaced this. ⚠ NOT COMPILED — the laptop is out of disk until the WSL vhdx is compacted, and building would wedge Windows. Build and run against a strict SV1 client before deploying; this is not yet proven to be why Braiins disconnects. Claude-Session: https://claude.ai/code/session_01Qdso8qy4cQ2o49L1XyQ75d * test(sv1): gate the unnegotiated `mining.set_extranonce`, which no case could catch `fd74982f4` changed behaviour that nothing asserted. The suite already had a probe for the re-key — `[411-rekey]` — but it drives the *serializer* shape (subscribe, wait, authorize), where the subscribe response lands before authorise arrives and the post-hoc path never runs. It therefore reports MET on a node where the defect is live, which is how this survived the #783/#784/#785 round. The new case drives the shape the tcpdump actually captured: no `mining.configure` at all, then `mining.subscribe` and `mining.authorize` pipelined into ONE TCP segment. Measured against the live fleet on vm1 today, before the fix: [unneg-extranonce] FAIL — set_extranonce received: YES — never negotiated [411-rekey] MET (informational) So the gate can fail, and it fails on exactly the case the old probe passes. It asserts absence over a 4 s window; the offending notification fires ~1.2 ms in, so the window is evidence rather than haste. ⚠ Still open: this proves the protocol violation is gone, NOT that it was why Braiins disconnects. A synthetic client held the socket for 8 s across the defect without closing, so the disconnect cause remains unproven. * test(sv1): add the positive control — opt-in clients must STILL get `set_extranonce` `[unneg-extranonce]` proves the notification is gone for clients that never asked. On its own that is half a truth table, and the dangerous half is missing: a change that suppressed `mining.set_extranonce` for EVERYONE would leave it green while breaking every client that legitimately re-keys. Nothing in the suite asserted the notification is still delivered when it is welcome. `[neg-extranonce]` is the same shape in every respect but one — this client lists `subscribe-extranonce` in `mining.configure`. The opt-in is the only variable: opt-in absent -> must NOT arrive ([unneg-extranonce]) opt-in present -> MUST arrive ([neg-extranonce]) Measured against the live UNFIXED fleet, both ports, before this change: :4444 [unneg-extranonce] FAIL [neg-extranonce] PASS :3333 [unneg-extranonce] FAIL [neg-extranonce] PASS So the control passes on today's binary, which is the point — it must hold before AND after, and only the negative case is allowed to flip. ⚠ The server does not ack `subscribe-extranonce` in its `mining.configure` reply, though it sends the notification regardless. The fix keys off the client's REQUEST rather than the server's ack, so the control is unaffected; the ack is reported but deliberately not gating, to avoid coupling this case to a separate defect. ⚠ `:3333` is not exempt from any of this. Both ports are served by ONE `Sv1Server` and differ only in the difficulty floor, and the defect measures identically on both — ASICs escape it through handshake ORDER (subscribe first), not through the port.
defenwycke
added a commit
that referenced
this pull request
Aug 29, 2026
Three changes since the deployed binaries, all merged to main: - #750 — Stage 6 Release B: delete the BFT payout vote and the GHOST-03 ledger sweep - #782 — share fee drift with the miners instead of the treasury (ships DORMANT) - #786 — only send `mining.set_extranonce` to clients that asked for it ## What actually changes on the wire **#786 is the only behaviour change this roll delivers.** Braiins' proxy closes the connection on receipt of an unnegotiated `mining.set_extranonce`; proven against the real `braiinssystems/farm-proxy:26.02` image, which names its own reason (`protocol error: invalid-message-type`, `subs=0`) and whose FIN lands 19.1 ms after that notification and BEFORE the subscribe response. Both SV1 ports are affected — `:3333` and `:4444` are one `Sv1Server` differing only in difficulty floor — so this is not farm-tier-only. **#750 is behaviourally inert**, because `PAYOUT_FROM_SHARD_HEIGHT = 964_100` already stopped the vote at runtime on 2026-08-26. It deletes machinery that is already unreachable. ⛔ It is NOT reversible by a gate: rollback is the `.bak` binary. **#782 is inert by construction.** `FEE_DRIFT_MINER_SHARE_HEIGHT = u64::MAX`, so miner pay is UNCHANGED by this roll — the treasury keeps absorbing every sat above the ~804,768 floor. Arming it is a separate, deliberate act once the fleet is uniform and clear of this roll's settling window. ## Binaries Measured against what the fleet actually RUNS (`57d05b17f`), not against the `v1.11.28` tag — #783/#784/#785 shipped after that tag, so the tag understates what is already deployed: ghost-pool 15 files (#750, #782) translator_sv2 4 files (#786) pool_sv2 0 files — not rebuilt, not redeployed `pool_sv2` does not reach the `sv1` crate (`cargo tree -p pool_sv2` finds no `sv1`), so the config -> `pool_sv2` -> `ghost-pool` ordering constraint from #742 does not apply to this roll. ⚠ Consequence accepted deliberately: `pool_sv2` will keep reporting 1.11.28 while the other two report 1.11.29, so `check-fleet-uniformity.sh` shows expected skew, not drift. ## Lockfiles Bumped in THIS commit, not a follow-up. v1.11.28 changed only `Cargo.toml` and needed `228f5a2c5` to repair both lockfiles afterwards; a stale `fuzz/Cargo.lock` fails CI as a red "Format" job whose actual failing step is `Fuzz targets build --locked`. Verified with `grep -F` (the `.` in a version string is a wildcard): 0 stale refs, 35 + 16 new.
defenwycke
added a commit
that referenced
this pull request
Sep 6, 2026
…850) Answers "why did nothing catch this?" — something almost did. `check-stratum-config-agreement.sh` exists, runs in CI, and is written for exactly this class of bug. It compared a hardcoded list of keys, and `open_channel_on_subscribe` was added to both config files long after that list was written. That is the second time the same allowlist has missed something. The script's own comments record the first: #480, where `supported_extensions` / `required_extensions` diverged silently because they were not listed, so which attribution path a node took depended on which file provisioned it. ## The mechanism, not another entry The filter was in the PARSER (`elif key in SHARED`), so unlisted keys never entered the maps and comparing the maps compared only what someone had remembered to list. It now collects every key and compares every key present in both files. `SHARED` becomes a REQUIRED-PRESENT assertion, and `NOT_COMPARED` is an explicit exception list — the opposite of an allowlist. An unknown key is compared and must be justified to skip, rather than ignored until noticed. Fail-closed. Three exceptions, each with a reason: `authority_pubkey` and `user_identity` are per-node (the installer substitutes shell variables), and bare `port` is ambiguous across TOML sections — the parser is section-blind outside `[farm_tier]`, so it compares 8333 (P2P) against 34256 (SV2). The farm port is compared separately and section-aware. 10 keys compared before, 16 now. ## The divergence it found ⚠ Both in-repo copies said `open_channel_on_subscribe = false` while all eight nodes run `true`. They agreed with EACH OTHER, which is why a two-file check could not have caught it even with the key listed — the authoritative third copy is the fleet. `install-node.sh` provisions from that file, so node nine would have been given the pre-#783 behaviour: a serialising client (a proxy, or a rented-hashrate marketplace like Braiins) answered with an 8-byte placeholder extranonce against a 12-byte real prefix, building a 4-byte-short coinbase, with EVERY share rejected. Both copies are now `true`, which the config's own note says is due — "remove this flag once the fleet is on it", and the fleet has been since #783/#784/#785. ## Verified by mutation, because I got this wrong once already My first attempt changed only the comparison loop and left the parser filter in place. It reported "10/10 shared keys agree" against a deliberately flipped value — a check that cannot fail, which is the exact failure being fixed. Caught by flipping the field and expecting red. The version here fails with `open_channel_on_subscribe: install-node.sh = 'true' but translator-config.toml = 'false'` and exits 1. Claude-Session: https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym
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.
Two Braiins Hashpower contracts were left running overnight on 2026-08-27 and both paused.
The pool delivered zero shares:
sharesandshares_archiveare both 0 for%loadtest%,and have never held a Braiins share on vm1.
What was wrong
A serialising SV1 client — proxies and rented-hashrate marketplaces — waits for the
mining.subscribeRESPONSE before it will authorise. The channel opens onmining.authorize, so those clients deadlock, and a 1.5 s fallback answers subscribe with an8-byte all-zero placeholder extranonce while the real prefix is 12 bytes.
That placeholder is not approximate, it is unusable. The coinbase declares a 91-byte scriptSig
ending in
OP_PUSHBYTES_20, reserving exactly 20 bytes:so the client builds a coinbase shorter than its own length byte — malformed transaction, wrong
merkle root, every share invalid.
mining.set_extranoncecannot rescue it: that is anOPTIONAL extension and Braiins never sends
mining.extranonce.subscribe(0 occurrences in 24 hof fleet logs, 0 on the wire).
Reproduced on a live node with this repo's own tool, which already carries the acceptance test
for it as an informational probe:
What this does
Behind
open_channel_on_subscribe, the channel opens at subscribe underPROVISIONAL_CHANNEL_IDENTITY, so the subscribe response carries the real prefix first time.The address then cannot live in the channel identity, so it travels per share in the worker
TLV, which carries the full
<address>.<worker>in that mode; the pool uses it verbatim andfails closed if it is missing rather than guessing.
may legitimately contain a dot (
addr.farm1.rig1yieldsfarm1.rig1, pool:build_webhook_user_identitysplits on the first dot,extract_worker_nameon the last — multi-dot workers collapse #481), so keying onthat would silently reassign
farm1as a payout address. There is a test for that shape.tlv_compatible_usernamerefuses rather than truncates. A cut identity is not a shorteridentity: it mangles the payout address, or collapses two workers sharing a prefix onto one
miner_id. Its private 32-byte cap was a third copy of a limit owned byextensions_sv2::MAX_USER_IDENTITY_LENGTH; two copies of that constant drifting is what sentshares out with no TLV at all and misattributed ~395 of them under translator: open the channel on subscribe, and carry the full identity in the TLV #447. It now defers to the
single owner, and
PROVISIONAL_CHANNEL_IDENTITYgets one owner from the start for the samereason.
channel_open_requestedguards the open.channel_idcannot: it is set only whenOpenExtendedMiningChannelSuccessreturns, so a pipelining miner's authorize lands mid-flightand would burn a second upstream channel.
The open is debounced, and that is not a detail
The pool's extended allocator is
server_id || counterwith a two-byte server id, leaving a16-bit counter — 65,535 prefixes per process, never handed back (#746). Measured on vm1:
sri-poolup 28.5 h, counter at0x1be3= 7,139, so ~250/h and roughly ten days of uptimebefore every channel open starts failing, with the pool still green and
OpenMiningChannelErrorthe only tell.Opening on every subscribe would have been worse than a capacity problem. #746 deliberately
allocates the prefix after validation so an unauthenticated client cannot burn the
space — and a subscribe arrives before any authorize, so opening on it hands that capability
straight back to any drive-by prober.
SUBSCRIBE_OPEN_DEBOUNCEis 300 ms, sized from measured client behaviour: a pipelining minerauthorises ~12 ms after subscribe and a marketplace capability probe disconnects ~10-30 ms in.
Neither reaches the timer, so the pipelining path is byte-identical to today and probes cost
nothing. Only a client still holding the connection open opens early.
Why #447 is no longer blocked
#447 did this and was reverted (#456) pending #455. Both blockers are resolved on
main:MAX_USER_IDENTITY_LENGTHthat made a full identity fail to encode — fix: share attribution credited the operator address instead of the miner #422;set_difficultynever reaching the wire — translator: set_difficulty is never written straight to the wire — every change waits for the next mining.notify #455, fixed by fix(translator): send set_difficulty straight out once the miner has a job #575.Rollout
⛔
pool_sv2leads, on every node, before the flag is turned on anywhere. Rolled the otherway round the pool splices a worker onto the sentinel and credits the address portion —
sri—to nobody. That is the #447 failure mode exactly.
The flag ships off, so both binaries are safe to roll at any time; flipping it is a separate,
reversible step. Remove the flag once the fleet is on it.
Canary: ghost-vm6, both binaries, 2026-08-28
Deployed via
scripts/deploy-node.sh --canary(pool_sv2 first), which ran its own smoke andstarted the soak clock. Backups
pool_sv2.bak.20260828-122244,translator_sv2.bak.20260828-122429.Flag OFF — all 10 gating cases pass and
[411-rekey]still NOT MET, byte-for-byte thepre-deploy baseline. No regression for anything that works today.
Flag ON — all 10 still pass, and the acceptance probe flips:
The placeholder path is gone rather than merely avoided, measured across the restart:
Farm port
:4444, serialising client, fixed node against an unchanged one:extranonce1Channel identities since the flip show the debounce doing its job — real Braiins traffic
pipelines and keeps its own identity, only genuine serialising clients go provisional:
0 errors, 0 panics, 0 attribution failures; all units active.
What the canary does NOT prove
vm6 carries no miners, so no share was credited end-to-end.
attribution_probe.pycannotreach production difficulty (#464), so
verify_attribution.shagainst real traffic is thecheck that counts before this goes near a node with miners.
I also have not proven this is specifically why the two contracts paused. What is measured:
Braiins never opts into
set_extranonce, ~24% of connections fleet-wide were being answeredwith an unmineable placeholder, and zero Braiins shares have ever been credited. The causal
chain is strong; the last link is inferred.
Not in this PR
The 16-bit prefix ceiling is real and pre-existing. Widening the server-id handling or
reclaiming prefixes on
CloseChannelwould remove the ten-day limit. Flagged, not fixed.