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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ somebody money if they go unread.

## Unreleased

### Jobs built from an unchanged template are now distinct work

The pool re-issues a job every 30 s even when the template has not moved, and
until now that job's coinbase, merkle root and every header were byte-identical
to the previous one's. Rigs that restart their search on each job re-found
hashes the pool had already credited, which showed up as a storm of "duplicate
share" rejections. Each job now carries its own 4-byte salt in the coinbase
scriptSig (one 5-byte push after the tag; derived from the job id). The push
counts against the 100-byte scriptSig cap and against `coinbase_max_bytes`, so
a coinbase sitting exactly at either limit can lose its last payout or be
refused where it was not before. Nothing to configure.

### Operators: the dashboard now listens on loopback by default

`dashboard/server.js` binds `DASHBOARD_BIND`, default `127.0.0.1`, where it
Expand Down
2 changes: 1 addition & 1 deletion NONCE_AND_SHARES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ simplepool uses the standard stratum-v1 split:
```
coinbase scriptSig layout (assembled at share-check time):

[ height_push ] [ tag ] [ extranonce1 (4 B) ][ extranonce2 (8 B) ]
[ height_push ] [ tag ] [ job salt (5 B) ] [ extranonce1 (4 B) ][ extranonce2 (8 B) ]
└── pool assigns ──┘└── miner picks ──┘
```

Expand Down
10 changes: 7 additions & 3 deletions docs/simplepool.html
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,7 @@ <h3>Where the extranonce lives</h3>
<div class="bytes">
<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"><b>job salt</b><span>5 B · per job</span></div>
<div class="f pool"><b>extranonce1</b><span>4 B · pool assigns</span></div>
<div class="f miner"><b>extranonce2</b><span>8 B · miner sweeps</span></div>
</div>
Expand Down Expand Up @@ -2515,7 +2516,7 @@ <h4>Coinbase &amp; fee</h4>
<tbody>
<tr><td><code>operator_address</code></td><td>—</td><td><strong>required</strong>; must decode, else exit 2</td><td>Receives the <code>fee_bps</code> output. A network mismatch with the node (mainnet vs test) only warns.</td></tr>
<tr><td><code>fee_bps</code></td><td><code>100</code></td><td>0–1000, else refuse to start</td><td>Fee in basis points; 100 = 1%. 0 removes the fee output. A fee below 546 sats is dropped rather than made dust.</td></tr>
<tr><td><code>coinbase_tag</code></td><td><code>/simplepool/</code></td><td>≤63 chars; scriptSig must stay ≤100 bytes</td><td>Text in the coinbase scriptSig after the BIP34 height.</td></tr>
<tr><td><code>coinbase_tag</code></td><td><code>/simplepool/</code></td><td>≤63 chars; the whole scriptSig (height, any template scriptSig, tag, the 5-byte job salt, 12 bytes of extranonce) must stay ≤100 bytes</td><td>Text in the coinbase scriptSig after the BIP34 height.</td></tr>
</tbody>
</table>
</div>
Expand Down Expand Up @@ -2997,13 +2998,16 @@ <h3>Input</h3>
<div class="f"><b>prevout</b><span>32 × 00 · ffffffff</span></div>
<div class="f"><b>BIP34 height</b><span>minimal push</span></div>
<div class="f"><b>tag</b><span><code>coinbase_tag</code>, ≤ 75 B</span></div>
<div class="f"><b>job salt</b><span>04 + 4 B LE</span></div>
<div class="f pool"><b>extranonce1</b><span>4 B</span></div>
<div class="f miner"><b>extranonce2</b><span>8 B</span></div>
<div class="f"><b>sequence</b><span>ffffffff</span></div>
</div>
<p>
<code>coinb1</code> ends just before extranonce1; <code>coinb2</code> starts
at the sequence. The whole scriptSig must be 2–100 bytes. When the template
<code>coinb1</code> ends just before extranonce1 (after the job salt push, a
per-job value so jobs rebuilt from an unchanged template are distinct work);
<code>coinb2</code> starts at the sequence. The whole scriptSig, salt
included, must be 2–100 bytes. When the template
supplies its own coinbase (<code>coinbasetxn</code>, as the enforcer does),
its scriptSig, version and locktime are kept and the tag and extranonces are
appended to it.
Expand Down
118 changes: 72 additions & 46 deletions src/coinbase.c
Original file line number Diff line number Diff line change
Expand Up @@ -507,15 +507,66 @@ void coinbase_parts_free(coinbase_parts_t *p) {
free(p->cb2); p->cb2 = NULL; p->cb2_len = 0;
}

/* Largest tag push (1 + 75) plus the salt push, rounded up. */
#define CB_TAG_PUSH_MAX 88

/* ---- coinbase tag + per-job salt push ----
*
* The operator's tag as one data push, then -- when job_salt is non-zero -- a
* second push carrying the salt: 0x04 followed by 4 bytes, little-endian.
* The salt exists so that two jobs built from the SAME template are still
* different work. A pool re-issues a job whenever it re-polls, and when the
* template has not moved the coinbase, the merkle root and therefore every
* header a rig can build are byte-identical to the previous job's. Rigs that
* restart their search on each job then re-find hashes the pool has already
* credited, and the pool answers with a storm of duplicate-share rejections.
* A different salt per job changes the merkle root, so a repeated search
* cannot repeat a header. The tag push itself is untouched.
*
* Bytes added: COINBASE_JOB_SALT_PUSH_BYTES (5). Every builder counts them
* against the 100-byte scriptSig cap, and every byte-budget model of the
* coinbase (the window builders' probes) counts them as well. Salt 0 means
* "no push" and keeps the previous layout byte for byte. */
static size_t cb_tag_push(const char *coinbase_tag, uint32_t job_salt,
uint8_t out[CB_TAG_PUSH_MAX]) {
size_t n = 0;
if (coinbase_tag && *coinbase_tag) {
size_t tlen = strlen(coinbase_tag);
if (tlen > 75) tlen = 75;
out[n++] = (uint8_t)tlen;
memcpy(out + n, coinbase_tag, tlen);
n += tlen;
}
if (job_salt) {
out[n++] = 4;
out[n++] = (uint8_t)(job_salt);
out[n++] = (uint8_t)(job_salt >> 8);
out[n++] = (uint8_t)(job_salt >> 16);
out[n++] = (uint8_t)(job_salt >> 24);
}
return n;
}

/* Size cb_tag_push() would emit, for the probes that only need a length. */
static size_t cb_tag_push_len(const char *coinbase_tag, uint32_t job_salt) {
size_t n = 0;
if (coinbase_tag && *coinbase_tag) {
size_t t = strlen(coinbase_tag);
n += (t > 75 ? 75 : t) + 1;
}
if (job_salt) n += COINBASE_JOB_SALT_PUSH_BYTES;
return n;
}

int coinbase_build(uint32_t height, int64_t value_sats,
const char *payout_address,
const char *witness_commitment_hex,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size, size_t extranonce2_size,
coinbase_parts_t *out, char *errbuf, size_t errlen) {
return coinbase_build_split(height, value_sats,
payout_address, NULL, 0,
witness_commitment_hex, coinbase_tag,
witness_commitment_hex, coinbase_tag, job_salt,
extranonce1_size, extranonce2_size,
out, NULL, NULL, errbuf, errlen);
}
Expand All @@ -525,7 +576,7 @@ int coinbase_build_split(uint32_t height, int64_t value_sats,
const char *operator_address,
int fee_bps,
const char *witness_commitment_hex,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size, size_t extranonce2_size,
coinbase_parts_t *out,
int64_t *out_miner_sats, int64_t *out_fee_sats,
Expand Down Expand Up @@ -590,22 +641,16 @@ int coinbase_build_split(uint32_t height, int64_t value_sats,
size_t height_push_len = bip34_height_push(height, height_push);

/* Tag push. */
uint8_t tag_push[80];
size_t tag_push_len = 0;
if (coinbase_tag && *coinbase_tag) {
size_t tlen = strlen(coinbase_tag);
if (tlen > 75) tlen = 75;
tag_push[0] = (uint8_t)tlen;
memcpy(tag_push + 1, coinbase_tag, tlen);
tag_push_len = tlen + 1;
}
uint8_t tag_push[CB_TAG_PUSH_MAX];
size_t tag_push_len = cb_tag_push(coinbase_tag, job_salt, tag_push);

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 the 5-byte job salt push when job_salt is non-zero
* 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. */
Expand Down Expand Up @@ -960,7 +1005,7 @@ int coinbase_build_window(uint32_t height, int64_t value_sats,
const coinbase_payee_t *payees, size_t n_payees,
const char *operator_address, int fee_bps,
const char *witness_commitment_hex,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size, size_t extranonce2_size,
size_t max_coinbase_bytes,
int64_t payout_floor_sats,
Expand All @@ -977,11 +1022,7 @@ int coinbase_build_window(uint32_t height, int64_t value_sats,
size_t wc_probe_len = 0;
if (witness_commitment_hex && *witness_commitment_hex)
wc_probe_len = strlen(witness_commitment_hex) / 2;
size_t tag_len_probe = 0;
if (coinbase_tag && *coinbase_tag) {
size_t t = strlen(coinbase_tag);
tag_len_probe = (t > 75 ? 75 : t) + 1;
}
size_t tag_len_probe = cb_tag_push_len(coinbase_tag, job_salt);
uint8_t hp_probe[8];
size_t ss_probe = bip34_height_push(height, hp_probe) + tag_len_probe
+ extranonce1_size + extranonce2_size;
Expand Down Expand Up @@ -1041,15 +1082,8 @@ int coinbase_build_window(uint32_t height, int64_t value_sats,

uint8_t height_push[8];
size_t height_push_len = bip34_height_push(height, height_push);
uint8_t tag_push[80];
size_t tag_push_len = 0;
if (coinbase_tag && *coinbase_tag) {
size_t tlen = strlen(coinbase_tag);
if (tlen > 75) tlen = 75;
tag_push[0] = (uint8_t)tlen;
memcpy(tag_push + 1, coinbase_tag, tlen);
tag_push_len = tlen + 1;
}
uint8_t tag_push[CB_TAG_PUSH_MAX];
size_t tag_push_len = cb_tag_push(coinbase_tag, job_salt, tag_push);
size_t en_total = extranonce1_size + extranonce2_size;
size_t script_sig_len = height_push_len + tag_push_len + en_total;
if (script_sig_len < 2 || script_sig_len > 100) {
Expand Down Expand Up @@ -1090,7 +1124,7 @@ int coinbase_build_window(uint32_t height, int64_t value_sats,

static int build_from_template_impl(const char *coinbase_tx_hex,
cb_repl_fn repl_fn, void *repl_ctx,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size,
size_t extranonce2_size,
coinbase_parts_t *out,
Expand Down Expand Up @@ -1205,11 +1239,7 @@ static int build_from_template_impl(const char *coinbase_tx_hex,
* commitment OP_RETURNs the enforcer put in the template: they are why
* the same 16 payouts can fit under one budget and not another, and why
* a cap counted in outputs cannot express the limit at all. */
size_t tag_probe = 0;
if (coinbase_tag && *coinbase_tag) {
size_t t = strlen(coinbase_tag);
tag_probe = (t > 75 ? 75 : t) + 1;
}
size_t tag_probe = cb_tag_push_len(coinbase_tag, job_salt);
size_t ss_probe = (size_t)ss_len + tag_probe
+ extranonce1_size + extranonce2_size;
size_t fixed_bytes = 4 + 1 + 36 + (ss_probe < 253 ? 1 : 3) + ss_probe
Expand Down Expand Up @@ -1240,14 +1270,8 @@ static int build_from_template_impl(const char *coinbase_tx_hex,
}

/* Optional coinbase tag, appended into the scriptSig. */
uint8_t tag_push[80]; size_t tag_push_len = 0;
if (coinbase_tag && *coinbase_tag) {
size_t tlen = strlen(coinbase_tag);
if (tlen > 75) tlen = 75;
tag_push[0] = (uint8_t)tlen;
memcpy(tag_push + 1, coinbase_tag, tlen);
tag_push_len = tlen + 1;
}
uint8_t tag_push[CB_TAG_PUSH_MAX];
size_t tag_push_len = cb_tag_push(coinbase_tag, job_salt, tag_push);

/* New scriptSig = server scriptSig (BIP34 height + any server data) +
* tag + extranonce placeholder. Coinbase scriptSig is capped at 100. */
Expand Down Expand Up @@ -1380,7 +1404,7 @@ int coinbase_build_from_template(const char *coinbase_tx_hex,
const char *miner_address,
const char *operator_address,
int fee_bps,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size,
size_t extranonce2_size,
coinbase_parts_t *out,
Expand All @@ -1399,7 +1423,8 @@ int coinbase_build_from_template(const char *coinbase_tx_hex,
ctx.out_miner_sats = out_miner_sats;
ctx.out_fee_sats = out_fee_sats;
return build_from_template_impl(coinbase_tx_hex, repl_single, &ctx,
coinbase_tag, extranonce1_size,
coinbase_tag, job_salt,
extranonce1_size,
extranonce2_size, out, out_has_witness,
errbuf, errlen);
}
Expand All @@ -1409,7 +1434,7 @@ int coinbase_build_window_from_template(const char *coinbase_tx_hex,
size_t n_payees,
const char *operator_address,
int fee_bps,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size,
size_t extranonce2_size,
size_t max_coinbase_bytes,
Expand All @@ -1433,7 +1458,8 @@ int coinbase_build_window_from_template(const char *coinbase_tx_hex,
ctx.payout_floor_sats = payout_floor_sats;
ctx.res = res;
return build_from_template_impl(coinbase_tx_hex, repl_window, &ctx,
coinbase_tag, extranonce1_size,
coinbase_tag, job_salt,
extranonce1_size,
extranonce2_size, out, out_has_witness,
errbuf, errlen);
}
Expand Down Expand Up @@ -1531,7 +1557,7 @@ int coinbase_template_reward(const char *coinbase_tx_hex, int64_t *out_sats) {
/* The replacement machinery hands the callback the reward it computed;
* we keep the number and put the output back unchanged. */
if (build_from_template_impl(coinbase_tx_hex, cb_reward_probe, &reward,
NULL, 4, 4, &throwaway, NULL,
NULL, 0, 4, 4, &throwaway, NULL,
err, sizeof err) < 0) {
return -1;
}
Expand Down
24 changes: 19 additions & 5 deletions src/coinbase.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ typedef struct {
size_t cb2_len;
} coinbase_parts_t;

/* Per-job salt (every coinbase_build* function takes one, right after the
* tag). When non-zero the scriptSig gets ONE extra push after the tag:
*
* 0x04 | salt[0] | salt[1] | salt[2] | salt[3] (little-endian)
*
* which is COINBASE_JOB_SALT_PUSH_BYTES (5) bytes. It makes two jobs built
* from the same template different work: without it the coinbase, merkle root
* and every header are identical between re-polls of an unchanged template,
* and rigs that restart their search per job re-find hashes already credited.
* The 5 bytes count against the 100-byte scriptSig cap and against every
* byte-budget model of the coinbase. Salt 0 emits no push and the layout is
* byte-identical to a build without one; probes and tests pass 0. */
#define COINBASE_JOB_SALT_PUSH_BYTES 5

/* Build coinbase1/coinbase2 halves around the extranonce placeholder,
* single-payout — the entire value_sats goes to payout_address.
*
Expand All @@ -22,7 +36,7 @@ typedef struct {
int coinbase_build(uint32_t height, int64_t value_sats,
const char *payout_address,
const char *witness_commitment_hex,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size, size_t extranonce2_size,
coinbase_parts_t *out, char *errbuf, size_t errlen);

Expand All @@ -42,7 +56,7 @@ int coinbase_build_split(uint32_t height, int64_t value_sats,
const char *operator_address,
int fee_bps,
const char *witness_commitment_hex,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size, size_t extranonce2_size,
coinbase_parts_t *out,
int64_t *out_miner_sats, int64_t *out_fee_sats,
Expand Down Expand Up @@ -170,7 +184,7 @@ int coinbase_build_window(uint32_t height, int64_t value_sats,
const coinbase_payee_t *payees, size_t n_payees,
const char *operator_address, int fee_bps,
const char *witness_commitment_hex,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size, size_t extranonce2_size,
size_t max_coinbase_bytes,
int64_t payout_floor_sats,
Expand Down Expand Up @@ -203,7 +217,7 @@ int coinbase_build_from_template(const char *coinbase_tx_hex,
const char *miner_address,
const char *operator_address,
int fee_bps,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size,
size_t extranonce2_size,
coinbase_parts_t *out,
Expand Down Expand Up @@ -281,7 +295,7 @@ int coinbase_build_window_from_template(const char *coinbase_tx_hex,
size_t n_payees,
const char *operator_address,
int fee_bps,
const char *coinbase_tag,
const char *coinbase_tag, uint32_t job_salt,
size_t extranonce1_size,
size_t extranonce2_size,
size_t max_coinbase_bytes,
Expand Down
Loading
Loading