diff --git a/NONCE_AND_SHARES.md b/NONCE_AND_SHARES.md
index 26b8901..c27796b 100644
--- a/NONCE_AND_SHARES.md
+++ b/NONCE_AND_SHARES.md
@@ -44,19 +44,50 @@ simplepool uses the standard stratum-v1 split:
```
coinbase scriptSig layout (assembled at share-check time):
- [ height_push ] [ tag ] [ extranonce1 (4 B) ][ extranonce2 (4 B) ]
+ [ height_push ] [ tag ] [ extranonce1 (4 B) ][ extranonce2 (8 B) ]
└── pool assigns ──┘└── miner picks ──┘
```
- **extranonce1 (4 bytes, `en1`)** — assigned by the pool when the
connection subscribes. Immutable for the life of that TCP session.
-- **extranonce2 (4 bytes, `en2`)** — the miner's private search field.
+- **extranonce2 (8 bytes, `en2`)** — the miner's private search field.
Each `mining.submit` carries an `en2` value; the miner sweeps it
independently.
-Together they give each connection **2⁶⁴ distinct coinbases** to try
-before it has to reconnect for a fresh `en1`. At any real hashrate,
-that's effectively unbounded.
+Both widths come from `STRATUM_EXTRANONCE1_SIZE` /
+`STRATUM_EXTRANONCE2_SIZE` in `src/stratum.h`; the `en2` size is what
+the pool reports as the third element of the `mining.subscribe` result.
+
+`en1` is fixed for the life of the connection, so each connection has
+**2⁶⁴ distinct coinbases** to try before it would need to reconnect for
+a fresh one; across the pool the `(en1, en2)` pair space is 2⁹⁶. At any
+real hashrate, both are effectively unbounded.
+
+#### Why extranonce2 is 8 bytes and not the classic 4
+
+Not for search space. Even at 4 bytes a single connection gets 2⁸⁰
+headers per job (see the version-rolling section below), which a 1 EH/s
+farm would take about two weeks to exhaust — and jobs rotate every
+template. Capacity was never the constraint.
+
+The reason is **subdivision**. A stratum proxy sitting between the pool
+and a farm fans one upstream connection out to many downstream miners
+by splitting the `en2` field it was given: high bytes become a
+downstream-miner id, low bytes are passed down as that miner's own
+`en2`. At 4 bytes upstream, a proxy spending 3 on addressing leaves its
+miners a single byte — 256 values — and some firmware refuses to run
+that narrow. At 8 the proxy can spend 3 and still hand down the
+conventional 4, so every downstream miner sees an ordinary pool.
+
+**Changing these widths is consensus-relevant, not cosmetic.** `cb1`
+ends with the scriptSig length varint, computed once from
+`en1_size + en2_size` when the coinbase is rendered. An `en2` of any
+other width produces a coinbase whose declared scriptSig length
+disagrees with the bytes that follow it — an invalid transaction whose
+header nonetheless hashes fine. `handle_submit` therefore rejects any
+submission whose `en2` is not exactly `job->en2_size`
+(`src/stratum.c`, error `wrong extranonce2 size`) rather than crediting
+a share for work that could never become a block.
For each `en2` the miner picks, it then sweeps the header's 4-byte
`nonce` field (2³² hashes) and, if version-rolling was negotiated,
@@ -65,27 +96,32 @@ also permutes the masked version bits. So each `en2` value gives
### Extranonce1 allocation — how uniqueness is guaranteed
-`src/stratum.c:688-694`:
+In `handle_subscribe` (`src/stratum.c`):
```c
-/* Allocate extranonce1 from server counter ^ time. */
+/* Take extranonce1 straight from the server counter. */
unsigned seq = atomic_fetch_add(&s->extranonce1_seq, 1);
-uint32_t mix = seq ^ (uint32_t)now_ms();
+uint32_t mix = (uint32_t)seq;
c->extranonce1[0] = (uint8_t)(mix >> 24);
c->extranonce1[1] = (uint8_t)(mix >> 16);
c->extranonce1[2] = (uint8_t)(mix >> 8);
c->extranonce1[3] = (uint8_t)mix;
```
-Two properties matter:
-
-1. **Uniqueness across concurrent connects** — `atomic_fetch_add` on
- `extranonce1_seq` guarantees that no two connections can read the
- same `seq` value even if they subscribe in the same nanosecond.
-2. **Freshness after counter wrap** — the 32-bit `seq` will wrap
- after 4.3 billion connections. XORing with `now_ms()` (also 32
- bits) ensures that even if a rig disconnects and reconnects days
- later after the counter has cycled, it gets a different `en1`.
+**Uniqueness across concurrent connects** — `atomic_fetch_add` on
+`extranonce1_seq` guarantees that no two connections can read the same
+`seq` value even if they subscribe in the same nanosecond. The counter
+is seeded from the clock once at startup, so a restart doesn't hand out
+the same values to a fresh set of connections.
+
+The counter is *not* mixed with the clock at use. An earlier version
+did (`seq ^ now_ms()`), and that destroyed the uniqueness guarantee it
+was meant to reinforce: the XOR collides whenever the delta in the
+clock equals the delta in the counter — an even `seq` at an even
+millisecond and the next `seq` one millisecond later land on the same
+value. A miner opening several connections at once hit that routinely,
+and two connections sharing an `en1` render identical coinbases, so
+both find the same hash from the same nonce.
**No two miners on the pool are searching the same
`(header, coinbase, nonce)` triple.** That's the fairness guarantee
@@ -116,7 +152,8 @@ into the header's `version` field.
Current default mask: **`0x1fffe000`** — the 16 bits between position
13 and 28. That expands the effective per-`(en1, en2)` search space by
2¹⁶, so a single `en2` value covers 2³² × 2¹⁶ = 2⁴⁸ ≈ 280 trillion
-headers.
+headers, and one connection covers 2⁶⁴ × 2⁴⁸ = 2¹¹² over the full `en2`
+sweep.
**The pool never re-uses a version-rolled header for share
validation**: on submit, the miner tells us the exact rolled version
diff --git a/README.md b/README.md
index 498606f..90bb41c 100644
--- a/README.md
+++ b/README.md
@@ -148,7 +148,7 @@ sequenceDiagram
Note over M,P: per-connection setup
M->>P: TCP connect :3334
M->>P: mining.subscribe
- P-->>M: extranonce1 (4B, per-conn), en2_size
+ P-->>M: extranonce1 (4B, per-conn), en2_size (8B)
M->>P: mining.authorize "
assigned once per connection
@@ -697,6 +697,24 @@
arm vardiff window
P-->>M: result: true
diff --git a/docs/simplepool.html b/docs/simplepool.html
index e48cf4d..5050de5 100644
--- a/docs/simplepool.html
+++ b/docs/simplepool.html
@@ -682,7 +682,7 @@ Where the extranonce lives
/simplepool/Where the extranonce lives
distinct coinbase txid, therefore a distinct merkle root, therefore a fresh
232 nonce space to sweep.
+ extranonce2 is 8 bytes rather than the classic 4, and the
+ reason is not search space — 4 bytes already outruns any hashrate. It is so
+ that a stratum proxy in front of the pool can subdivide the field, taking
+ the high bytes as a downstream-miner id and passing the low bytes down as
+ that miner's own extranonce2. At 4 bytes a proxy spending 3 on
+ addressing leaves its miners a single byte, which some firmware refuses to
+ run with; at 8 it can spend 3 and still hand down the conventional 4.
+
+ The width is not advisory. cb1 ends with the scriptSig length
+ varint, fixed at render time from the two extranonce sizes, so an
+ extranonce2 of any other width yields a coinbase whose declared
+ length disagrees with its contents — an invalid transaction that still
+ hashes like a valid one. The pool rejects such submissions
+ (wrong extranonce2 size) rather than credit a share for work
+ that could never become a block.
+
diff --git a/src/coinbase.c b/src/coinbase.c
index 087b6d6..85013dc 100644
--- a/src/coinbase.c
+++ b/src/coinbase.c
@@ -522,6 +522,19 @@ int coinbase_build_split(uint32_t height, int64_t value_sats,
size_t en_total = extranonce1_size + extranonce2_size;
size_t script_sig_len = height_push_len + tag_push_len + en_total;
+ /* Consensus caps the coinbase scriptSig at 100 bytes; a block that
+ * exceeds it is rejected outright. The budget is the BIP34 height push
+ * plus the operator's coinbase_tag (up to 76 bytes with its length byte)
+ * plus both extranonces, so a long tag and a wide extranonce can reach it
+ * together. The coinbasetxn path below checks this already -- check here
+ * too rather than emitting a coinbase that only fails at the network. */
+ if (script_sig_len < 2 || script_sig_len > 100) {
+ set_err(errbuf, errlen, "coinbase scriptSig length %zu out of range "
+ "(height %zu + tag %zu + extranonce %zu)",
+ script_sig_len, height_push_len, tag_push_len, en_total);
+ return -1;
+ }
+
/* Build outputs blob: miner payout, [operator fee], [witness commitment]. */
bbuf_t outs;
bbuf_init(&outs);
diff --git a/src/main.c b/src/main.c
index ff83d93..3cbb59e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -233,7 +233,7 @@ static stratum_job_t *build_job_from_template(const proxy_config_t *cfg,
job_id, t->version, prev_le,
t->coinbase_value_sats,
t->default_witness_commitment,
- /*en1*/ 4, /*en2*/ 4,
+ STRATUM_EXTRANONCE1_SIZE, STRATUM_EXTRANONCE2_SIZE,
(const uint8_t (*)[32])branches, branch_count,
t->bits, t->curtime, target_be,
(uint32_t)t->height,
diff --git a/src/stratum.c b/src/stratum.c
index 95c92a4..17d5789 100644
--- a/src/stratum.c
+++ b/src/stratum.c
@@ -200,7 +200,7 @@ struct stratum_conn {
pthread_t thr;
int thr_started;
- uint8_t extranonce1[4];
+ uint8_t extranonce1[STRATUM_EXTRANONCE1_SIZE];
double difficulty;
int subscribed;
int authorized;
@@ -746,8 +746,8 @@ static int handle_subscribe(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
c->extranonce1[3] = (uint8_t)mix;
c->subscribed = 1;
- char ex1_hex[9];
- bytes_to_hex(c->extranonce1, 4, ex1_hex);
+ char ex1_hex[STRATUM_EXTRANONCE1_SIZE * 2 + 1];
+ bytes_to_hex(c->extranonce1, sizeof c->extranonce1, ex1_hex);
cJSON *result = cJSON_CreateArray();
cJSON *subs = cJSON_CreateArray();
@@ -761,7 +761,10 @@ static int handle_subscribe(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
cJSON_AddItemToArray(subs, sn);
cJSON_AddItemToArray(result, subs);
cJSON_AddItemToArray(result, cJSON_CreateString(ex1_hex));
- cJSON_AddItemToArray(result, cJSON_CreateNumber(4));
+ /* The miner sizes its extranonce2 sweep off this number, and the coinbase
+ * we render for it reserves exactly this many bytes. handle_submit
+ * enforces the agreement. */
+ cJSON_AddItemToArray(result, cJSON_CreateNumber(STRATUM_EXTRANONCE2_SIZE));
return emit_response(buf, len, id, result, NULL);
}
@@ -1055,6 +1058,28 @@ static int handle_submit(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
return emit_response(buf, len, id, NULL, err);
}
+ /* The extranonce2 must be exactly the width we advertised on subscribe
+ * and reserved when rendering this job's cb1. cb1 ends with the scriptSig
+ * length varint, which was computed as en1_size + en2_size; splicing in a
+ * different width produces a coinbase whose declared scriptSig length
+ * disagrees with the bytes that follow it. That transaction is invalid,
+ * so any block built on it is rejected by the network -- but its header
+ * still hashes, so without this check the share would look fine and be
+ * credited. Reject instead of silently mining garbage: a miner that
+ * ignored the advertised size needs to hear about it. */
+ if (en2_len != job->en2_size) {
+ free(en2_bytes);
+ if (s->cfg.on_reject) {
+ s->cfg.on_reject(s->cfg.ctx, c->worker_name, now_ms(),
+ "wrong extranonce2 size");
+ }
+ LOG_WARN("submit from %s: extranonce2 is %zu bytes, expected %zu "
+ "(miner ignored the size from mining.subscribe)",
+ c->worker_name, en2_len, job->en2_size);
+ cJSON *err = make_error(20, "wrong extranonce2 size");
+ return emit_response(buf, len, id, NULL, err);
+ }
+
/* Render this connection's coinbase for `job` if not cached. The
* cache is keyed on job_id; submits against an older job retired into
* the recent ring will rebuild on demand. */
@@ -1069,13 +1094,14 @@ static int handle_submit(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
}
/* coinbase = cb1 || ex1 || ex2 || cb2 */
- size_t cb_len = c->cb1_len + 4 + en2_len + c->cb2_len;
+ size_t en1_len = sizeof c->extranonce1;
+ size_t cb_len = c->cb1_len + en1_len + en2_len + c->cb2_len;
uint8_t *cb = malloc(cb_len);
if (!cb) { free(en2_bytes); return -1; }
size_t off = 0;
- memcpy(cb + off, c->cb1, c->cb1_len); off += c->cb1_len;
- memcpy(cb + off, c->extranonce1, 4); off += 4;
- memcpy(cb + off, en2_bytes, en2_len); off += en2_len;
+ memcpy(cb + off, c->cb1, c->cb1_len); off += c->cb1_len;
+ memcpy(cb + off, c->extranonce1, en1_len); off += en1_len;
+ memcpy(cb + off, en2_bytes, en2_len); off += en2_len;
memcpy(cb + off, c->cb2, c->cb2_len); off += c->cb2_len;
free(en2_bytes);
diff --git a/src/stratum.h b/src/stratum.h
index 004d07c..da93526 100644
--- a/src/stratum.h
+++ b/src/stratum.h
@@ -7,13 +7,39 @@
typedef struct stratum_job stratum_job_t;
+/* The extranonce split, advertised on mining.subscribe and baked into every
+ * per-connection coinbase.
+ *
+ * extranonce1 is the pool's per-connection identifier; it must be unique
+ * across live connections or two miners render identical coinbases (see
+ * handle_subscribe). extranonce2 is the miner's private search field: it
+ * owns those bytes and sweeps them freely.
+ *
+ * extranonce2 is 8 bytes rather than the classic 4. Raw search space is not
+ * the reason -- 4 bytes already gives one connection 2^80 headers per job,
+ * which no hashrate exhausts. The reason is subdivision: a stratum proxy in
+ * front of the pool carves extranonce2 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, which
+ * some firmware refuses to run with. At 8 it can spend 3 and still hand down
+ * the conventional 4, so downstream miners see an ordinary pool.
+ *
+ * Widening these is a consensus-relevant change, not a cosmetic one: cb1
+ * carries the scriptSig length varint computed from en1_size + en2_size, so
+ * a submitted extranonce2 of any other length yields a coinbase whose
+ * declared scriptSig length disagrees with its contents. handle_submit
+ * rejects on length for exactly that reason. Keep the coinbase scriptSig
+ * (BIP34 height push + coinbase_tag + en1 + en2) within 100 bytes. */
+#define STRATUM_EXTRANONCE1_SIZE 4
+#define STRATUM_EXTRANONCE2_SIZE 8
+
/* Create a job from template fields. The coinbase is *not* baked into the
* job — each connection renders its own coinbase paying its miner address
* (minus the configured operator fee). The job carries everything else
* the server needs to materialise a per-connection coinbase on demand:
* - value_sats: coinbasevalue from getblocktemplate
* - witness_commitment_hex: optional, may be NULL
- * - en1_size / en2_size: extranonce sizes, both currently 4
+ * - en1_size / en2_size: extranonce sizes; see STRATUM_EXTRANONCE*_SIZE
*
* tx_hex_list may be NULL if tx_count == 0. The job takes ownership of
* its own heap copies; caller's buffers are not retained.
diff --git a/tests/test_coinbase.c b/tests/test_coinbase.c
index 0418633..5623341 100644
--- a/tests/test_coinbase.c
+++ b/tests/test_coinbase.c
@@ -1,4 +1,5 @@
#include "coinbase.h"
+#include "stratum.h"
#include