Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 55 additions & 18 deletions NONCE_AND_SHARES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<bc1q…>[.rig_label]"
P->>P: validate bech32/base58 → cache payout_address<br/>arm vardiff window
P-->>M: result: true
Expand Down
20 changes: 19 additions & 1 deletion docs/simplepool.html
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ <h3>Where the extranonce lives</h3>
<div class="f"><b>height push</b><span>BIP34</span></div>
<div class="f"><b>coinbase_tag</b><span>e.g. <code>/simplepool/</code></span></div>
<div class="f pool"><b>extranonce1</b><span>4 B · pool assigns</span></div>
<div class="f miner"><b>extranonce2</b><span>4 B · miner sweeps</span></div>
<div class="f miner"><b>extranonce2</b><span>8 B · miner sweeps</span></div>
</div>
<p class="legend">
<span><i style="background:var(--solo-bg);border:1px solid var(--solo)"></i>assigned once per connection</span>
Expand All @@ -697,6 +697,24 @@ <h3>Where the extranonce lives</h3>
distinct coinbase txid, therefore a distinct merkle root, therefore a fresh
2<sup>32</sup> nonce space to sweep.
</p>
<p>
<code>extranonce2</code> 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 <code>extranonce2</code>. 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.
</p>
<p>
The width is not advisory. <code>cb1</code> ends with the scriptSig length
varint, fixed at render time from the two extranonce sizes, so an
<code>extranonce2</code> 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
(<code>wrong extranonce2 size</code>) rather than credit a share for work
that could never become a block.
</p>

<h3>Version rolling</h3>
<p>
Expand Down
13 changes: 13 additions & 0 deletions src/coinbase.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 34 additions & 8 deletions src/stratum.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
Expand Down Expand Up @@ -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. */
Expand All @@ -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);

Expand Down
28 changes: 27 additions & 1 deletion src/stratum.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading