Skip to content

Widen extranonce2 to 8 bytes and enforce the width on submit - #40

Open
rsantacroce wants to merge 1 commit into
mainfrom
2026-08-21-wider-extranonce2
Open

Widen extranonce2 to 8 bytes and enforce the width on submit#40
rsantacroce wants to merge 1 commit into
mainfrom
2026-08-21-wider-extranonce2

Conversation

@rsantacroce

Copy link
Copy Markdown
Collaborator

extranonce2 was 4 bytes, hardcoded in two places: the literal handed back as the third element of the mining.subscribe result, and the /*en2*/ 4 passed into stratum_job_new. Both now come from STRATUM_EXTRANONCE2_SIZE in stratum.h, and the value is 8.

Why

Not search space. At 4 bytes a single connection already gets 2^80 headers per job once nonce and version rolling are counted — a 1 EH/s farm would need about two weeks to exhaust one job, and jobs rotate every template. Capacity was never the constraint.

The reason is subdivision. A stratum proxy in front of the pool splits the extranonce2 it is given into a downstream-miner id (high bytes) plus the downstream miner's own extranonce2 (low bytes). At 4 bytes a proxy spending 3 on addressing leaves its miners a single byte — 256 values — and some firmware refuses to run that narrow. At 8 it can spend 3 and still hand down the conventional 4, so downstream miners see an ordinary pool.

8 rather than 7 because it is the same cost, satisfies any "at least 7" requirement, and 4/8 is the split proxies and firmware already expect. It is one constant if that turns out to be wrong.

The hole this exposed

handle_submit took whatever extranonce2 length the miner sent and spliced it into a cb1 whose scriptSig length varint had been fixed at render time from en1_size + en2_size. A mismatched width produces a coinbase whose declared scriptSig length disagrees with the bytes following it — an invalid transaction whose header still hashes like a valid one. The share gets credited; the problem only surfaces when a found block is rejected by the network.

Before this change that took a misbehaving miner. After it, any miner that ignores mining.subscribe and assumes the classic 4 hits it. So submit now rejects on length (wrong extranonce2 size), with an on_reject call so it lands in the rejects ledger and a log line naming the mismatch, rather than silently accepting work that could never become a block.

Also

The 100-byte scriptSig cap check is added to coinbase_build_split. The coinbasetxn path has always had it; the from-scratch path computed script_sig_len and never checked it. To be precise about what this is: not reachable through config today — a 5-byte height push plus the 76-byte maximum tag plus 12 extranonce bytes is 93. It is the guard that keeps a future widening from emitting a coinbase that only fails at the network.

Docs updated: NONCE_AND_SHARES.md, README.md, docs/simplepool.html. While in NONCE_AND_SHARES.md I also replaced the stale seq ^ now_ms() extranonce1 snippet — it still documented the collision behavior that was removed, in the same section that explains the extranonce layout.

Testing

Clean build under -Wall -Wextra -Werror -Wpedantic. Full suite green: test_stratum 75 passed, test_share 91, test_bitcoind 56, test_coinbase / test_store / test_broadcast / test_thunder all passing.

Six new tests: the advertised size, a too-narrow extranonce2 rejected, a too-wide one rejected, the exact width still accepted, the scriptSig varint agreeing with the assembled coinbase at production width, a wrong-width splice failing to parse as a valid coinbase, and the cap check firing.

Test fixtures now build jobs from the constants rather than a hardcoded 4, so a future width change that misses a call site fails in the tests instead of on a found block.

Not verified: the regtest walkthrough against the enforcer — no local bitcoind/bip300301_enforcer. The coinbasetxn path is the live one, since templates come from the enforcer, and it is covered by unit tests but not end to end. Worth running before this merges. scripts/regtest/cpuminer.js needs no changes — it already sizes its sweep off en2_size from the subscribe reply, so it exercises the new width as-is.

Deploying this

Miners renegotiate the width on mining.subscribe, so a restart is enough for well-behaved clients. Anything that hardcodes 4 will start getting wrong extranonce2 size rejects instead of silently mining invalid work — visible in the rejects ledger and the logs, which is the point, but worth watching the reject rate right after the first restart.

extranonce2 was 4 bytes, hardcoded in two places: the literal handed
back as the third element of the mining.subscribe result, and the
/*en2*/ 4 passed into stratum_job_new. Both now come from
STRATUM_EXTRANONCE2_SIZE in stratum.h, and the value is 8.

The reason is not search space. At 4 bytes a single connection already
gets 2^80 headers per job once nonce and version rolling are counted,
which no hashrate exhausts and which jobs rotate out from under anyway.
The reason is subdivision: a stratum proxy in front of the pool splits
the extranonce2 it is given into a downstream-miner id plus the
downstream miner's own extranonce2. At 4 bytes a proxy spending 3 on
addressing leaves its miners one byte, and some firmware will not run
that narrow. At 8 it can spend 3 and still hand down the conventional 4.

Widening it exposed a hole that was already there. handle_submit took
whatever extranonce2 length the miner sent and spliced it into a cb1
whose scriptSig length varint had been fixed at render time from
en1_size + en2_size. A mismatched width produces a coinbase whose
declared scriptSig length disagrees with the bytes following it — an
invalid transaction whose header still hashes like a valid one, so the
share is credited and only a found block reveals the problem, by being
rejected. Before this change that took a misbehaving miner; after it,
any miner that ignores mining.subscribe and assumes the classic 4 hits
it. So submit now rejects on length ("wrong extranonce2 size") instead
of silently accepting work that could never become a block.

Also add the 100-byte scriptSig cap check to coinbase_build_split. The
coinbasetxn path has always had it; the from-scratch path computed
script_sig_len and never checked it. Not reachable through config today
(a 5-byte height push plus a 76-byte tag plus 12 extranonce bytes is
93), but it is the guard that keeps a future widening from emitting a
coinbase that only fails at the network.

Tests cover the advertised size, rejection of a too-narrow and a
too-wide extranonce2, acceptance at the exact width, agreement between
the scriptSig varint and the assembled coinbase at the production
width, that a wrong-width splice does not parse as a valid coinbase,
and the cap check. The regtest walkthrough against the enforcer was not
run — no local bitcoind/enforcer.
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