diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cfad6b..2a60bdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/NONCE_AND_SHARES.md b/NONCE_AND_SHARES.md
index 1309ca8..98e51b1 100644
--- a/NONCE_AND_SHARES.md
+++ b/NONCE_AND_SHARES.md
@@ -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 ──┘
```
diff --git a/docs/simplepool.html b/docs/simplepool.html
index f17a332..5606c82 100644
--- a/docs/simplepool.html
+++ b/docs/simplepool.html
@@ -920,6 +920,7 @@
Where the extranonce lives
height pushBIP34
coinbase_tage.g. /simplepool/
+
job salt5 B · per job
extranonce14 B · pool assigns
extranonce28 B · miner sweeps
@@ -2515,7 +2516,7 @@ Coinbase & fee
operator_address | — | required; must decode, else exit 2 | Receives the fee_bps output. A network mismatch with the node (mainnet vs test) only warns. |
fee_bps | 100 | 0–1000, else refuse to start | Fee in basis points; 100 = 1%. 0 removes the fee output. A fee below 546 sats is dropped rather than made dust. |
- coinbase_tag | /simplepool/ | ≤63 chars; scriptSig must stay ≤100 bytes | Text in the coinbase scriptSig after the BIP34 height. |
+ coinbase_tag | /simplepool/ | ≤63 chars; the whole scriptSig (height, any template scriptSig, tag, the 5-byte job salt, 12 bytes of extranonce) must stay ≤100 bytes | Text in the coinbase scriptSig after the BIP34 height. |
@@ -2997,13 +2998,16 @@ Input
prevout32 × 00 · ffffffff
BIP34 heightminimal push
tagcoinbase_tag, ≤ 75 B
+ job salt04 + 4 B LE
extranonce14 B
extranonce28 B
sequenceffffffff
- coinb1 ends just before extranonce1; coinb2 starts
- at the sequence. The whole scriptSig must be 2–100 bytes. When the template
+ coinb1 ends just before extranonce1 (after the job salt push, a
+ per-job value so jobs rebuilt from an unchanged template are distinct work);
+ coinb2 starts at the sequence. The whole scriptSig, salt
+ included, must be 2–100 bytes. When the template
supplies its own coinbase (coinbasetxn, as the enforcer does),
its scriptSig, version and locktime are kept and the tag and extranonces are
appended to it.
diff --git a/src/coinbase.c b/src/coinbase.c
index 3ee1456..bee5a8c 100644
--- a/src/coinbase.c
+++ b/src/coinbase.c
@@ -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);
}
@@ -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,
@@ -590,15 +641,8 @@ 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;
@@ -606,6 +650,7 @@ int coinbase_build_split(uint32_t height, int64_t value_sats,
/* 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. */
@@ -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,
@@ -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;
@@ -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) {
@@ -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,
@@ -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
@@ -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. */
@@ -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,
@@ -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);
}
@@ -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,
@@ -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);
}
@@ -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;
}
diff --git a/src/coinbase.h b/src/coinbase.h
index d2e6e25..f3b46d7 100644
--- a/src/coinbase.h
+++ b/src/coinbase.h
@@ -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.
*
@@ -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);
@@ -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,
@@ -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,
@@ -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,
@@ -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,
diff --git a/src/stratum.c b/src/stratum.c
index 08538f3..639cc08 100644
--- a/src/stratum.c
+++ b/src/stratum.c
@@ -159,6 +159,11 @@ struct stratum_job {
uint32_t ntime;
uint8_t network_target_be[32];
uint32_t height;
+ /* Per-job coinbase salt (see cb_tag_push in coinbase.c): derived from
+ * job_id at construction, never 0, so two jobs from one template
+ * practically never share a coinbase (32 bits: a collision is possible,
+ * just negligible over a template's lifetime). */
+ uint32_t cb_salt;
char **tx_hex_list; /* owned */
size_t tx_count;
@@ -191,6 +196,9 @@ struct stratum_job {
_Atomic int refs;
};
+/* Defined with the dedupe helpers below; the job constructor needs it first. */
+static uint64_t fnv1a(const char *s);
+
stratum_job_t *stratum_job_new(
const char *job_id,
int32_t version,
@@ -235,6 +243,12 @@ stratum_job_t *stratum_job_new(
j->ntime = ntime;
if (network_target_be) memcpy(j->network_target_be, network_target_be, 32);
j->height = height;
+ /* job_id is unique per job (main.c derives it from the build time), so
+ * hashing it makes the salt unique too. Truncate to 32 bits and force
+ * non-zero: 0 means "no salt push" to the builders and would silently
+ * bring back identical work. */
+ j->cb_salt = (uint32_t)fnv1a(j->job_id);
+ if (j->cb_salt == 0) j->cb_salt = 1;
if (tx_count && tx_hex_list) {
j->tx_hex_list = calloc(tx_count, sizeof(char *));
if (!j->tx_hex_list) goto fail;
@@ -872,7 +886,7 @@ static int render_finder_coinbase(stratum_server_t *s, stratum_conn_t *c,
c->payout_address,
s->cfg.operator_address,
s->cfg.fee_bps,
- s->cfg.coinbase_tag,
+ s->cfg.coinbase_tag, job->cb_salt,
job->en1_size, job->en2_size,
parts, NULL, NULL, NULL,
err, errlen);
@@ -880,7 +894,7 @@ static int render_finder_coinbase(stratum_server_t *s, stratum_conn_t *c,
return coinbase_build_split(job->height, job->value_sats,
c->payout_address,
s->cfg.operator_address, s->cfg.fee_bps,
- job->wc_hex, s->cfg.coinbase_tag,
+ job->wc_hex, s->cfg.coinbase_tag, job->cb_salt,
job->en1_size, job->en2_size,
parts, NULL, NULL, err, errlen);
}
@@ -911,7 +925,7 @@ static int conn_render_coinbase(stratum_server_t *s, stratum_conn_t *c,
rc = coinbase_build_window_from_template(
job->coinbasetxn_hex, job->payees, job->n_payees,
s->cfg.operator_address, s->cfg.fee_bps,
- s->cfg.coinbase_tag, job->en1_size, job->en2_size,
+ s->cfg.coinbase_tag, job->cb_salt, job->en1_size, job->en2_size,
conn_coinbase_budget(s, c), s->cfg.payout_floor_sats,
&parts, NULL, NULL,
err, sizeof err);
@@ -919,7 +933,7 @@ static int conn_render_coinbase(stratum_server_t *s, stratum_conn_t *c,
rc = coinbase_build_window(
job->height, job->value_sats, job->payees, job->n_payees,
s->cfg.operator_address, s->cfg.fee_bps, job->wc_hex,
- s->cfg.coinbase_tag, job->en1_size, job->en2_size,
+ s->cfg.coinbase_tag, job->cb_salt, job->en1_size, job->en2_size,
conn_coinbase_budget(s, c), s->cfg.payout_floor_sats,
&parts, NULL, err, sizeof err);
}
@@ -933,14 +947,14 @@ static int conn_render_coinbase(stratum_server_t *s, stratum_conn_t *c,
rc = coinbase_build_from_template(job->coinbasetxn_hex,
s->cfg.pool_btc_address,
s->cfg.operator_address, s->cfg.fee_bps,
- s->cfg.coinbase_tag,
+ s->cfg.coinbase_tag, job->cb_salt,
job->en1_size, job->en2_size,
&parts, NULL, NULL, NULL, err, sizeof err);
} else {
rc = coinbase_build_split(job->height, job->value_sats,
s->cfg.pool_btc_address,
s->cfg.operator_address, s->cfg.fee_bps,
- job->wc_hex, s->cfg.coinbase_tag,
+ job->wc_hex, s->cfg.coinbase_tag, job->cb_salt,
job->en1_size, job->en2_size,
&parts, NULL, NULL, err, sizeof err);
}
@@ -2418,14 +2432,14 @@ static int submit_with_job(stratum_server_t *s, stratum_conn_t *c, cJSON *id,
wrc = coinbase_build_window_from_template(
job->coinbasetxn_hex, job->payees, job->n_payees,
s->cfg.operator_address, s->cfg.fee_bps,
- s->cfg.coinbase_tag, job->en1_size, job->en2_size,
+ s->cfg.coinbase_tag, job->cb_salt, job->en1_size, job->en2_size,
conn_coinbase_budget(s, c), s->cfg.payout_floor_sats,
&throwaway, NULL, &res, werr, sizeof werr);
} else {
wrc = coinbase_build_window(
job->height, job->value_sats, job->payees, job->n_payees,
s->cfg.operator_address, s->cfg.fee_bps, job->wc_hex,
- s->cfg.coinbase_tag, job->en1_size, job->en2_size,
+ s->cfg.coinbase_tag, job->cb_salt, job->en1_size, job->en2_size,
conn_coinbase_budget(s, c), s->cfg.payout_floor_sats,
&throwaway, &res, werr, sizeof werr);
}
@@ -2701,6 +2715,9 @@ void stratum_conn_set_coinbase_budget_for_test(stratum_conn_t *c, int bytes) {
if (c) c->pol_max_coinbase_bytes = bytes;
}
+void stratum_job_set_cb_salt_for_test(stratum_job_t *j, uint32_t salt) { j->cb_salt = salt; }
+uint32_t stratum_job_cb_salt_for_test(const stratum_job_t *j) { return j->cb_salt; }
+
stratum_conn_t *stratum_conn_new_for_test(stratum_server_t *s) {
stratum_conn_t *c = calloc(1, sizeof(*c));
if (!c) return NULL;
diff --git a/src/stratum.h b/src/stratum.h
index 2dfa884..99ecd40 100644
--- a/src/stratum.h
+++ b/src/stratum.h
@@ -32,7 +32,8 @@ typedef struct stratum_job stratum_job_t;
* 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. */
+ * (BIP34 height push + coinbase_tag + job salt push + en1 + en2) within 100
+ * bytes; the salt push is 5 bytes when present (see coinbase.h). */
#define STRATUM_EXTRANONCE1_SIZE 4
#define STRATUM_EXTRANONCE2_SIZE 8
@@ -380,6 +381,11 @@ void stratum_server_free(stratum_server_t *s);
* construct one of these directly. */
typedef struct stratum_conn stratum_conn_t;
+/* Override a job's coinbase salt. Only for tests that need two job ids to be
+ * the SAME work (the hash-dedupe ring); production salts come from job_id. */
+void stratum_job_set_cb_salt_for_test(stratum_job_t *j, uint32_t salt);
+uint32_t stratum_job_cb_salt_for_test(const stratum_job_t *j);
+
/* Allocate a connection state attached to a server. Used by tests; the
* real listener uses an internal allocator. */
stratum_conn_t *stratum_conn_new_for_test(stratum_server_t *s);
diff --git a/tests/test_coinbase.c b/tests/test_coinbase.c
index fe60671..49815bb 100644
--- a/tests/test_coinbase.c
+++ b/tests/test_coinbase.c
@@ -1,5 +1,6 @@
#include "coinbase.h"
#include "stratum.h"
+#include "sha256.h"
#include
#include
@@ -92,7 +93,7 @@ static void test_build_coinbase_structural(void) {
int rc = coinbase_build(800000, 625000000,
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
- wc_hex, "/drivepool/", 4, 4,
+ wc_hex, "/drivepool/", 0, 4, 4,
&parts, err, sizeof err);
if (rc != 0) {
fprintf(stderr, "coinbase_build err: %s\n", err);
@@ -191,7 +192,7 @@ static void test_build_coinbase_split_fee_math(void) {
800000, 5000000000LL,
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
- 100, NULL, "/simplepool/", 4, 4,
+ 100, NULL, "/simplepool/", 0, 4, 4,
&parts, &miner_sats, &fee_sats, err, sizeof err);
assert(rc == 0);
assert(fee_sats == 50000000LL);
@@ -203,7 +204,7 @@ static void test_build_coinbase_split_fee_math(void) {
800000, 5000000000LL,
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
- 0, NULL, "/simplepool/", 4, 4,
+ 0, NULL, "/simplepool/", 0, 4, 4,
&parts, &miner_sats, &fee_sats, err, sizeof err);
assert(rc == 0);
assert(fee_sats == 0);
@@ -216,7 +217,7 @@ static void test_build_coinbase_split_fee_math(void) {
800000, 30000LL,
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
- 100, NULL, "/simplepool/", 4, 4,
+ 100, NULL, "/simplepool/", 0, 4, 4,
&parts, &miner_sats, &fee_sats, err, sizeof err);
assert(rc == 0);
assert(fee_sats == 0);
@@ -236,7 +237,7 @@ static void test_bip34_small_height_uses_opn(void) {
/* height = 5 should produce scriptSig starting with OP_5 = 0x55. */
int rc = coinbase_build(5, 5000000000LL,
"bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
- NULL, "/simplepool/", 4, 4,
+ NULL, "/simplepool/", 0, 4, 4,
&parts, err, sizeof err);
assert(rc == 0);
@@ -294,7 +295,7 @@ static void test_build_from_template(void) {
int64_t miner_sats = 0, fee_sats = 0;
int rc = coinbase_build_from_template(
- ENF_COINBASE_HEX, ENF_ADDR, NULL, 0, "/x/", 4, 4,
+ ENF_COINBASE_HEX, ENF_ADDR, NULL, 0, "/x/", 0, 4, 4,
&parts, &has_witness, &miner_sats, &fee_sats, err, sizeof err);
if (rc != 0) fprintf(stderr, "build_from_template err: %s\n", err);
assert(rc == 0);
@@ -388,7 +389,7 @@ static void test_build_from_template_fee_split(void) {
int64_t miner_sats = 0, fee_sats = 0;
int rc = coinbase_build_from_template(
- ENF_COINBASE_HEX, ENF_ADDR, ENF_ADDR, 100, "/x/", 4, 4,
+ ENF_COINBASE_HEX, ENF_ADDR, ENF_ADDR, 100, "/x/", 0, 4, 4,
&parts, &has_witness, &miner_sats, &fee_sats, err, sizeof err);
assert(rc == 0);
assert(fee_sats == 50000000LL); /* 1% of 50 BTC */
@@ -446,7 +447,7 @@ static void test_count_outputs(void) {
/* witness_commitment_hex */
"6a24aa21a9ed2222222222222222222222222222"
"222222222222222222222222222222222222",
- /* coinbase_tag */ NULL,
+ /* coinbase_tag */ NULL, 0,
4, 4, &parts, NULL, NULL, err, sizeof err) == 0);
/* cb1 + extranonce1 + extranonce2 + cb2 is the coinbase a miner submits. */
size_t n = parts.cb1_len * 2 + 16 + parts.cb2_len * 2 + 1;
@@ -527,7 +528,7 @@ static void test_scriptsig_length_matches_advertised_extranonce(void) {
assert(coinbase_build_split(800000, 5000000000LL, ENF_ADDR, ENF_ADDR, 100,
"6a24aa21a9ed2222222222222222222222222222"
"222222222222222222222222222222222222",
- "/simplepool/",
+ "/simplepool/", 0,
en1, en2, &parts, NULL, NULL,
err, sizeof err) == 0);
@@ -573,7 +574,7 @@ static void test_wrong_width_extranonce_desyncs_the_parse(void) {
assert(coinbase_build_split(800000, 5000000000LL, ENF_ADDR, ENF_ADDR, 100,
"6a24aa21a9ed2222222222222222222222222222"
"222222222222222222222222222222222222",
- "/simplepool/",
+ "/simplepool/", 0,
STRATUM_EXTRANONCE1_SIZE,
STRATUM_EXTRANONCE2_SIZE, &parts, NULL, NULL,
err, sizeof err) == 0);
@@ -611,7 +612,7 @@ static void test_scriptsig_over_100_is_rejected(void) {
coinbase_parts_t parts = {0};
char err[256] = {0};
int rc = coinbase_build_split(800000, 5000000000LL, ENF_ADDR, NULL, 0,
- NULL, "/simplepool/",
+ NULL, "/simplepool/", 0,
/* en1 */ 4, /* en2 */ 90,
&parts, NULL, NULL, err, sizeof err);
assert(rc < 0);
@@ -877,7 +878,7 @@ static void test_the_result_names_which_payees_were_paid(void) {
{ WC, 49999900LL },
};
assert(coinbase_build_window(800000, 100000000LL, payees, 3,
- NULL, 0, NULL, NULL, 4, 8,
+ NULL, 0, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err) == 0);
assert(res.paid_count == 2);
assert(res.dropped_below_floor == 1);
@@ -907,7 +908,7 @@ static void test_the_result_names_which_payees_were_paid(void) {
{ WC, 59999500LL },
};
assert(coinbase_build_window(800000, 100000000LL, mid, 3,
- NULL, 0, NULL, NULL, 4, 8,
+ NULL, 0, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err) == 0);
assert(res.paid_count == 2);
assert(res.paid_payee[0] == 1);
@@ -920,7 +921,7 @@ static void test_the_result_names_which_payees_were_paid(void) {
/* Nothing dropped: every payee is marked, and only those. */
const coinbase_payee_t all[] = { { WA, 60000000LL }, { WB, 40000000LL } };
assert(coinbase_build_window(800000, 100000000LL, all, 2,
- NULL, 0, NULL, NULL, 4, 8,
+ NULL, 0, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err) == 0);
assert(res.paid_payee[0] == 1 && res.paid_payee[1] == 1);
assert(res.paid_payee[2] == 0);
@@ -946,7 +947,7 @@ static void test_the_template_builder_reports_the_same_paid_set(void) {
{ WC, reward - 100 - b },
};
assert(coinbase_build_window_from_template(ENF_COINBASE_HEX, payees, 3,
- NULL, 0, NULL, 4, 4, 0, 0,
+ NULL, 0, NULL, 0, 4, 4, 0, 0,
&parts, NULL, &res,
err, sizeof err) == 0);
assert(res.paid_count == 2);
@@ -966,7 +967,7 @@ static void test_window_pays_each_miner_its_own_output(void) {
{ WA, 2475000000LL }, { WB, 1485000000LL }, { WC, 990000000LL },
};
int rc = coinbase_build_window(800000, 5000000000LL, payees, 3,
- WOP, 100, NULL, "/simplepool/", 4, 8,
+ WOP, 100, NULL, "/simplepool/", 0, 4, 8,
0, 0, &parts, &res, err, sizeof err);
assert(rc == 0);
assert(res.paid_count == 3);
@@ -988,14 +989,14 @@ static void test_a_split_that_does_not_add_up_is_refused(void) {
coinbase_parts_t parts; char err[256];
const coinbase_payee_t short_[] = { { WA, 1000000LL } };
int rc = coinbase_build_window(800000, 5000000000LL, short_, 1,
- WOP, 100, NULL, NULL, 4, 8,
+ WOP, 100, NULL, NULL, 0, 4, 8,
0, 0, &parts, NULL, err, sizeof err);
assert(rc < 0);
assert(strstr(err, "payees sum to") != NULL);
const coinbase_payee_t over[] = { { WA, 9000000000LL } };
rc = coinbase_build_window(800000, 5000000000LL, over, 1,
- WOP, 100, NULL, NULL, 4, 8,
+ WOP, 100, NULL, NULL, 0, 4, 8,
0, 0, &parts, NULL, err, sizeof err);
assert(rc < 0);
printf("ok: a window split that does not sum to the block is refused\n");
@@ -1020,7 +1021,7 @@ static void test_a_payee_below_the_floor_is_shared_out_not_given_to_the_operator
{ WB, 100LL }, /* far below the 546-sat dust limit */
};
int rc = coinbase_build_window(800000, 100000000LL, payees, 2,
- WOP, 100, NULL, NULL, 4, 8,
+ WOP, 100, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err);
assert(rc == 0);
assert(res.paid_count == 1);
@@ -1067,7 +1068,7 @@ static void test_the_operator_cannot_profit_by_shrinking_the_coinbase(void) {
coinbase_parts_t parts; char err[256];
coinbase_window_result_t res;
assert(coinbase_build_window(800000, 5000000000LL, payees, N, WOP, 100,
- NULL, "/sp/", 4, 8, budgets[b], 546,
+ NULL, "/sp/", 0, 4, 8, budgets[b], 546,
&parts, &res, err, sizeof err) == 0);
/* The fee never moves, whatever the budget does. */
assert(res.fee_sats == 50000000LL);
@@ -1096,7 +1097,7 @@ static void test_the_payout_floor_is_configurable(void) {
/* Default floor (dust): both are paid. */
assert(coinbase_build_window(800000, 100000000LL, payees, 2,
- WOP, 0, NULL, NULL, 4, 8,
+ WOP, 0, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err) == 0);
assert(res.paid_count == 2);
assert(res.redistributed_sats == 0);
@@ -1104,7 +1105,7 @@ static void test_the_payout_floor_is_configurable(void) {
/* Floor above the small claim: it is forfeited, not carried. */
assert(coinbase_build_window(800000, 100000000LL, payees, 2,
- WOP, 0, NULL, NULL, 4, 8,
+ WOP, 0, NULL, NULL, 0, 4, 8,
0, 50000, &parts, &res, err, sizeof err) == 0);
assert(res.paid_count == 1);
assert(res.dropped_below_floor == 1);
@@ -1116,7 +1117,7 @@ static void test_the_payout_floor_is_configurable(void) {
* to have and pretending otherwise would build an unspendable block. */
const coinbase_payee_t dusty[] = { { WA, 99999900LL }, { WB, 100LL } };
assert(coinbase_build_window(800000, 100000000LL, dusty, 2,
- WOP, 0, NULL, NULL, 4, 8,
+ WOP, 0, NULL, NULL, 0, 4, 8,
0, 1, &parts, &res, err, sizeof err) == 0);
assert(res.paid_count == 1);
assert(res.dropped_below_floor == 1);
@@ -1139,7 +1140,7 @@ static void test_the_cap_falls_on_whoever_is_last_in_the_order(void) {
};
int64_t value = 1000000LL + 3000000LL + 6000000LL; /* fee_bps 0: no fee */
int rc = coinbase_build_window(800000, value, payees, 3,
- WOP, 0, NULL, NULL, 4, 8,
+ WOP, 0, NULL, NULL, 0, 4, 8,
/* Byte budget admitting exactly two of the
* three payouts: the envelope, scriptSig
* and reserved operator output come to
@@ -1176,7 +1177,7 @@ static void test_the_caller_can_promote_a_small_claim(void) {
{ WA, 1000000LL }, { WC, 6000000LL }, { WB, 3000000LL },
};
int64_t value = 10000000LL;
- assert(coinbase_build_window(800000, value, payees, 3, WOP, 0, NULL, NULL,
+ assert(coinbase_build_window(800000, value, payees, 3, WOP, 0, NULL, NULL, 0,
4, 8, 180, 0, &parts, &res,
err, sizeof err) == 0);
assert(res.paid_count == 2);
@@ -1203,7 +1204,7 @@ static void test_a_dropped_claim_needs_no_operator_address(void) {
{ WA, 999900LL }, { WB, 100LL }, /* the second is dust */
};
int rc = coinbase_build_window(800000, 1000000LL, payees, 2,
- NULL, 0, NULL, NULL, 4, 8,
+ NULL, 0, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err);
assert(rc == 0);
assert(res.dropped_below_floor == 1);
@@ -1228,7 +1229,7 @@ static void test_no_operator_address_means_no_fee(void) {
coinbase_window_result_t res;
const coinbase_payee_t payees[] = { { WA, 990000LL }, { WB, 10000LL } };
int rc = coinbase_build_window(800000, 1000000LL, payees, 2,
- NULL, 100, NULL, NULL, 4, 8,
+ NULL, 100, NULL, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err);
assert(rc == 0);
assert(res.fee_sats == 0); /* fee_bps=100 but nowhere to pay */
@@ -1247,7 +1248,7 @@ static void test_a_window_of_only_dust_is_refused(void) {
coinbase_parts_t parts; char err[256];
const coinbase_payee_t payees[] = { { WA, 100LL }, { WB, 100LL } };
int rc = coinbase_build_window(800000, 200LL, payees, 2,
- WOP, 0, NULL, NULL, 4, 8,
+ WOP, 0, NULL, NULL, 0, 4, 8,
0, 0, &parts, NULL, err, sizeof err);
assert(rc < 0);
assert(strstr(err, "payout floor") != NULL);
@@ -1257,7 +1258,7 @@ static void test_a_window_of_only_dust_is_refused(void) {
static void test_an_empty_window_is_refused(void) {
coinbase_parts_t parts; char err[256];
int rc = coinbase_build_window(800000, 5000000000LL, NULL, 0,
- WOP, 100, NULL, NULL, 4, 8,
+ WOP, 100, NULL, NULL, 0, 4, 8,
0, 0, &parts, NULL, err, sizeof err);
assert(rc < 0);
assert(strstr(err, "nobody to pay") != NULL);
@@ -1273,7 +1274,7 @@ static void test_the_witness_commitment_is_preserved(void) {
const char *wc = "6a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf9";
const coinbase_payee_t payees[] = { { WA, 5000000000LL } };
int rc = coinbase_build_window(800000, 5000000000LL, payees, 1,
- NULL, 0, wc, NULL, 4, 8,
+ NULL, 0, wc, NULL, 0, 4, 8,
0, 0, &parts, &res, err, sizeof err);
assert(rc == 0);
uint64_t n = 0; int64_t sum = 0;
@@ -1293,9 +1294,9 @@ static void test_the_coinbase_is_deterministic(void) {
};
int64_t value = 5000000LL;
assert(coinbase_build_window(800000, value, payees, 3, NULL, 0, NULL,
- "/sp/", 4, 8, 0, 0, &a, NULL, err, sizeof err) == 0);
+ "/sp/", 0, 4, 8, 0, 0, &a, NULL, err, sizeof err) == 0);
assert(coinbase_build_window(800000, value, payees, 3, NULL, 0, NULL,
- "/sp/", 4, 8, 0, 0, &b, NULL, err, sizeof err) == 0);
+ "/sp/", 0, 4, 8, 0, 0, &b, NULL, err, sizeof err) == 0);
assert(a.cb2_len == b.cb2_len);
assert(memcmp(a.cb2, b.cb2, a.cb2_len) == 0);
coinbase_parts_free(&a);
@@ -1354,7 +1355,7 @@ static void test_window_from_template_preserves_commitments(void) {
* split below is exact without hardcoding the fixture's reward. */
coinbase_parts_t probe; int64_t reward = 0, unused = 0;
assert(coinbase_build_from_template(ENF_COINBASE_HEX, ENF_ADDR, NULL, 0,
- NULL, 4, 4, &probe, NULL, &reward,
+ NULL, 0, 4, 4, &probe, NULL, &reward,
&unused, err, sizeof err) == 0);
coinbase_parts_free(&probe);
assert(reward > 0);
@@ -1363,7 +1364,7 @@ static void test_window_from_template_preserves_commitments(void) {
int64_t a = (reward * 6) / 10;
const coinbase_payee_t payees[] = { { WA, a }, { WB, reward - a } };
int rc = coinbase_build_window_from_template(
- ENF_COINBASE_HEX, payees, 2, NULL, 0, "/x/", 4, 4, 0, 0,
+ ENF_COINBASE_HEX, payees, 2, NULL, 0, "/x/", 0, 4, 4, 0, 0,
&parts, &has_witness, &res, err, sizeof err);
if (rc != 0) fprintf(stderr, "window_from_template err: %s\n", err);
assert(rc == 0);
@@ -1400,7 +1401,7 @@ static void test_the_template_reward_matches_what_the_builder_splits(void) {
char err[256] = {0};
coinbase_parts_t probe; int64_t builder_reward = 0, unused = 0;
assert(coinbase_build_from_template(ENF_COINBASE_HEX, ENF_ADDR, NULL, 0,
- NULL, 4, 4, &probe, NULL,
+ NULL, 0, 4, 4, &probe, NULL,
&builder_reward, &unused,
err, sizeof err) == 0);
coinbase_parts_free(&probe);
@@ -1417,7 +1418,7 @@ static void test_the_template_reward_matches_what_the_builder_splits(void) {
const coinbase_payee_t payees[] = { { WA, reward / 2 },
{ WB, reward - reward / 2 } };
assert(coinbase_build_window_from_template(ENF_COINBASE_HEX, payees, 2,
- NULL, 0, NULL, 4, 4, 0, 0,
+ NULL, 0, NULL, 0, 4, 4, 0, 0,
&parts, NULL, &res,
err, sizeof err) == 0);
assert(res.paid_sats == reward);
@@ -1472,7 +1473,7 @@ static void test_the_slot_estimate_tracks_what_the_builder_admits(void) {
coinbase_parts_t parts;
coinbase_window_result_t r;
if (coinbase_build_window(800000, 5000000000LL, p, (size_t)n,
- WOP, 100, NULL, "/sp/", 4, 8,
+ WOP, 100, NULL, "/sp/", 0, 4, 8,
BUDGETS[b], 546, &parts, &r,
err, sizeof err) != 0) break;
coinbase_parts_free(&parts);
@@ -1511,7 +1512,7 @@ static void test_both_window_builders_split_identically(void) {
coinbase_parts_t probe; int64_t reward = 0, unused = 0;
assert(coinbase_build_from_template(ENF_COINBASE_HEX, ENF_ADDR, NULL, 0,
- NULL, 4, 4, &probe, NULL, &reward,
+ NULL, 0, 4, 4, &probe, NULL, &reward,
&unused, err, sizeof err) == 0);
coinbase_parts_free(&probe);
@@ -1523,9 +1524,9 @@ static void test_both_window_builders_split_identically(void) {
{ WA, payable - 40000 - 100 }, { WB, 40000 }, { WC, 100 },
};
assert(coinbase_build_window(800000, reward, payees, 3, WOP, 100, NULL,
- "/x/", 4, 4, 0, 0, &p1, &r1, err, sizeof err) == 0);
+ "/x/", 0, 4, 4, 0, 0, &p1, &r1, err, sizeof err) == 0);
assert(coinbase_build_window_from_template(ENF_COINBASE_HEX, payees, 3,
- WOP, 100, "/x/", 4, 4, 0, 0,
+ WOP, 100, "/x/", 0, 4, 4, 0, 0,
&p2, NULL, &r2, err, sizeof err) == 0);
assert(r1.paid_count == r2.paid_count);
assert(r1.paid_sats == r2.paid_sats);
@@ -1559,7 +1560,7 @@ static void test_commitments_eat_the_payout_budget(void) {
coinbase_parts_t probe; int64_t reward = 0, unused = 0;
assert(coinbase_build_from_template(ENF_COINBASE_HEX, ENF_ADDR, NULL, 0,
- NULL, 4, 4, &probe, NULL, &reward,
+ NULL, 0, 4, 4, &probe, NULL, &reward,
&unused, err, sizeof err) == 0);
coinbase_parts_free(&probe);
@@ -1578,7 +1579,7 @@ static void test_commitments_eat_the_payout_budget(void) {
* enforcer template admits 5 and a bare coinbase admits 6. */
const size_t BUDGET = 300;
assert(coinbase_build_window_from_template(ENF_COINBASE_HEX, payees, N,
- WOP, 0, NULL, 4, 4, BUDGET, 0,
+ WOP, 0, NULL, 0, 4, 4, BUDGET, 0,
&parts, NULL, &res,
err, sizeof err) == 0);
size_t paid_with_template = res.paid_count;
@@ -1595,7 +1596,7 @@ static void test_commitments_eat_the_payout_budget(void) {
/* The same window and the same budget, built from scratch — no template,
* so no commitment OP_RETURNs spending the budget. More miners fit. */
assert(coinbase_build_window(800000, reward, payees, N, WOP, 0, NULL,
- NULL, 4, 4, BUDGET, 0, &parts, &res,
+ NULL, 0, 4, 4, BUDGET, 0, &parts, &res,
err, sizeof err) == 0);
assert(res.paid_count > paid_with_template);
coinbase_parts_free(&parts);
@@ -1622,7 +1623,7 @@ static void test_the_built_coinbase_respects_its_budget(void) {
for (size_t budget = 200; budget <= 600; budget += 100) {
assert(coinbase_build_window(800000, total, payees, N, WOP, 0, NULL,
- "/sp/", 4, 8, budget, 0, &parts, &res,
+ "/sp/", 0, 4, 8, budget, 0, &parts, &res,
err, sizeof err) == 0);
/* cb1 + extranonce + cb2 is the whole serialized coinbase. */
size_t built = parts.cb1_len + 12 + parts.cb2_len;
@@ -1635,6 +1636,228 @@ static void test_the_built_coinbase_respects_its_budget(void) {
printf("ok: a built coinbase never exceeds its byte budget\n");
}
+/* ---------------------------------------------------------------------------
+ * Per-job coinbase salt.
+ *
+ * Two jobs built from the same template must be different work, or a rig that
+ * restarts its search per job re-finds hashes the pool already credited. The
+ * salt is ONE push after the tag: 0x04 + 4 bytes little-endian, counted
+ * against the 100-byte scriptSig cap and against the window byte budgets.
+ * Salt 0 emits nothing. Every builder is exercised through one dispatcher so
+ * a builder that forgets the salt cannot hide.
+ * ------------------------------------------------------------------------- */
+
+enum { K_PLAIN, K_SPLIT, K_WINDOW, K_TEMPLATE, K_WINDOW_TEMPLATE, K_COUNT };
+static const char *const KIND_NAME[K_COUNT] = {
+ "coinbase_build", "coinbase_build_split", "coinbase_build_window",
+ "coinbase_build_from_template", "coinbase_build_window_from_template" };
+
+/* budget 0 = default. *paid receives the window's paid_count (windows only). */
+static int build_kind(int kind, uint32_t salt, const char *tag,
+ size_t en1, size_t en2, size_t budget,
+ coinbase_parts_t *p, size_t *paid,
+ char *err, size_t errlen) {
+ coinbase_window_result_t res; memset(&res, 0, sizeof res);
+ int rc = -1;
+ int64_t reward = 0;
+ switch (kind) {
+ case K_PLAIN:
+ rc = coinbase_build(800000, 5000000000LL, WA, NULL, tag, salt,
+ en1, en2, p, err, errlen);
+ break;
+ case K_SPLIT:
+ rc = coinbase_build_split(800000, 5000000000LL, WA, WOP, 100, NULL,
+ tag, salt, en1, en2, p, NULL, NULL,
+ err, errlen);
+ break;
+ case K_WINDOW: {
+ enum { N = 12 };
+ coinbase_payee_t py[N];
+ for (int i = 0; i < N; ++i) { py[i].address = (i % 2) ? WA : WB; py[i].sats = 1000000LL; }
+ rc = coinbase_build_window(800000, N * 1000000LL, py, N, NULL, 0, NULL,
+ tag, salt, en1, en2, budget, 0, p, &res,
+ err, errlen);
+ break; }
+ case K_TEMPLATE:
+ rc = coinbase_build_from_template(ENF_COINBASE_HEX, WA, WOP, 100, tag,
+ salt, en1, en2, p, NULL, NULL, NULL,
+ err, errlen);
+ break;
+ case K_WINDOW_TEMPLATE: {
+ assert(coinbase_template_reward(ENF_COINBASE_HEX, &reward) == 0);
+ enum { N = 12 };
+ coinbase_payee_t py[N];
+ int64_t each = reward / N, used = 0;
+ for (int i = 0; i < N; ++i) {
+ py[i].address = (i % 2) ? WA : WB;
+ py[i].sats = (i == N - 1) ? reward - used : each;
+ used += py[i].sats;
+ }
+ rc = coinbase_build_window_from_template(ENF_COINBASE_HEX, py, N, NULL,
+ 0, tag, salt, en1, en2,
+ budget, 0, p, NULL, &res,
+ err, errlen);
+ break; }
+ }
+ if (paid) *paid = res.paid_count;
+ return rc;
+}
+
+/* cb1 = version(4) | in_count(1) | prevout(36) | varint(scriptSig len) |
+ * scriptSig up to the extranonce. The scriptSig is under 253 bytes, so its
+ * length varint is one byte. */
+#define CB1_SS_LEN_OFF 41
+#define CB1_SS_OFF 42
+
+/* The salted cb1 is the unsalted one with the length byte raised by 5 and
+ * the 5-byte push appended where the extranonce begins -- after the tag,
+ * after everything else already in the scriptSig. cb2 is untouched. */
+static void assert_salt_layout(const coinbase_parts_t *u, const coinbase_parts_t *s,
+ uint32_t salt) {
+ assert(s->cb1_len == u->cb1_len + COINBASE_JOB_SALT_PUSH_BYTES);
+ assert(s->cb2_len == u->cb2_len && memcmp(s->cb2, u->cb2, u->cb2_len) == 0);
+ assert(s->cb1[CB1_SS_LEN_OFF] == u->cb1[CB1_SS_LEN_OFF] + 5);
+ assert(memcmp(s->cb1, u->cb1, CB1_SS_LEN_OFF) == 0);
+ assert(memcmp(s->cb1 + CB1_SS_OFF, u->cb1 + CB1_SS_OFF,
+ u->cb1_len - CB1_SS_OFF) == 0);
+ const uint8_t *push = s->cb1 + u->cb1_len;
+ assert(push[0] == 0x04);
+ assert(push[1] == (uint8_t)salt);
+ assert(push[2] == (uint8_t)(salt >> 8));
+ assert(push[3] == (uint8_t)(salt >> 16));
+ assert(push[4] == (uint8_t)(salt >> 24));
+}
+
+static void test_job_salt_push(void) {
+ const uint32_t SALT = 0xA1B2C3D4u;
+ for (int k = 0; k < K_COUNT; ++k) {
+ coinbase_parts_t u = {0}, s = {0}, s2 = {0}; char err[256] = {0};
+ assert(build_kind(k, 0, "/sp/", 4, 8, 0, &u, NULL, err, sizeof err) == 0);
+ assert(build_kind(k, SALT, "/sp/", 4, 8, 0, &s, NULL, err, sizeof err) == 0);
+ assert_salt_layout(&u, &s, SALT);
+ /* A different salt changes those 4 bytes and nothing else. */
+ assert(build_kind(k, SALT + 1, "/sp/", 4, 8, 0, &s2, NULL, err, sizeof err) == 0);
+ assert(s2.cb1_len == s.cb1_len && memcmp(s2.cb1, s.cb1, s.cb1_len) != 0);
+ assert(memcmp(s2.cb1, s.cb1, s.cb1_len - 4) == 0);
+ /* Same with no tag at all: the push is the only thing added. */
+ coinbase_parts_t nu = {0}, ns = {0};
+ assert(build_kind(k, 0, NULL, 4, 8, 0, &nu, NULL, err, sizeof err) == 0);
+ assert(build_kind(k, SALT, NULL, 4, 8, 0, &ns, NULL, err, sizeof err) == 0);
+ assert_salt_layout(&nu, &ns, SALT);
+ coinbase_parts_free(&u); coinbase_parts_free(&s); coinbase_parts_free(&s2);
+ coinbase_parts_free(&nu); coinbase_parts_free(&ns);
+ }
+ printf("ok: job salt is one 5-byte push after the tag, in every builder\n");
+}
+
+/* Salt 0 must be byte-identical to the layout without a salt. Derived here
+ * from the serialization rules, independently of the builder. */
+static void test_job_salt_zero_is_the_classic_layout(void) {
+ coinbase_parts_t p = {0}; char err[256] = {0};
+ assert(coinbase_build(800000, 5000000000LL, WA, NULL, "/sp/", 0, 4, 8,
+ &p, err, sizeof err) == 0);
+ /* height push 03 00 35 0c | tag push 04 "/sp/" | 12 extranonce bytes */
+ static const uint8_t ss[] = { 0x03, 0x00, 0x35, 0x0c,
+ 0x04, '/', 's', 'p', '/' };
+ assert(p.cb1_len == 4 + 1 + 36 + 1 + sizeof ss);
+ assert(p.cb1[CB1_SS_LEN_OFF] == sizeof ss + 12);
+ assert(memcmp(p.cb1 + CB1_SS_OFF, ss, sizeof ss) == 0);
+ coinbase_parts_free(&p);
+ printf("ok: salt 0 emits no push\n");
+}
+
+/* The 100-byte cap counts the push, in every builder: whatever extranonce
+ * width the unsalted build can reach, the salted one reaches exactly 5 less. */
+static void test_job_salt_counts_against_the_scriptsig_cap(void) {
+ for (int k = 0; k < K_COUNT; ++k) {
+ coinbase_parts_t p = {0}; char err[256] = {0};
+ size_t max0 = 0, max1 = 0;
+ for (size_t en2 = 1; en2 <= 100; ++en2) {
+ if (build_kind(k, 0, "/sp/", 4, en2, 0, &p, NULL, err, sizeof err) == 0) {
+ coinbase_parts_free(&p); max0 = en2;
+ }
+ if (build_kind(k, 7, "/sp/", 4, en2, 0, &p, NULL, err, sizeof err) == 0) {
+ coinbase_parts_free(&p); max1 = en2;
+ }
+ }
+ if (max0 != max1 + 5) fprintf(stderr, "%s: max en2 %zu unsalted, %zu salted\n",
+ KIND_NAME[k], max0, max1);
+ assert(max0 > 5 && max0 == max1 + 5);
+ /* Exactly at the cap it builds and the scriptSig is 100 bytes... */
+ assert(build_kind(k, 7, "/sp/", 4, max1, 0, &p, NULL, err, sizeof err) == 0);
+ assert(p.cb1[CB1_SS_LEN_OFF] == 100); /* length byte includes the extranonces */
+ coinbase_parts_free(&p);
+ /* ...and one byte more is refused, where unsalted it is still fine. */
+ assert(build_kind(k, 7, "/sp/", 4, max1 + 1, 0, &p, NULL, err, sizeof err) < 0);
+ assert(strstr(err, "scriptSig length") != NULL);
+ assert(build_kind(k, 0, "/sp/", 4, max1 + 1, 0, &p, NULL, err, sizeof err) == 0);
+ coinbase_parts_free(&p);
+ }
+ printf("ok: the salt push counts against the 100-byte scriptSig cap\n");
+}
+
+/* The window builders' byte budget is the whole transaction, so the push has
+ * to be in it. Find the smallest budget that pays all 12 miners; with a salt
+ * that threshold must move up by exactly the 5 bytes the push adds. */
+static void test_job_salt_counts_against_the_window_byte_budget(void) {
+ for (int k = K_WINDOW; k <= K_WINDOW_TEMPLATE; k += (K_WINDOW_TEMPLATE - K_WINDOW)) {
+ size_t need0 = 0, need1 = 0;
+ for (int salted = 0; salted < 2; ++salted) {
+ size_t need = 0;
+ for (size_t b = 100; b <= 2000 && !need; ++b) {
+ coinbase_parts_t p = {0}; char err[256] = {0}; size_t paid = 0;
+ if (build_kind(k, salted ? 9 : 0, "/sp/", 4, 8, b, &p, &paid,
+ err, sizeof err) == 0) {
+ coinbase_parts_free(&p);
+ if (paid == 12) need = b;
+ }
+ }
+ if (salted) need1 = need; else need0 = need;
+ }
+ if (need1 != need0 + COINBASE_JOB_SALT_PUSH_BYTES)
+ fprintf(stderr, "%s: budget to pay all %zu unsalted, %zu salted\n",
+ KIND_NAME[k], need0, need1);
+ assert(need0 > 0 && need1 == need0 + COINBASE_JOB_SALT_PUSH_BYTES);
+
+ /* At the edge, the salted coinbase really is within the budget. */
+ coinbase_parts_t p = {0}; char err[256] = {0}; size_t paid = 0;
+ assert(build_kind(k, 9, "/sp/", 4, 8, need1, &p, &paid, err, sizeof err) == 0);
+ assert(paid == 12);
+ size_t en = 12;
+ size_t actual = p.cb1_len + en + p.cb2_len;
+ assert(actual <= need1);
+ coinbase_parts_free(&p);
+ /* One byte under it, somebody is cut. */
+ assert(build_kind(k, 9, "/sp/", 4, 8, need1 - 1, &p, &paid, err, sizeof err) == 0);
+ assert(paid < 12);
+ coinbase_parts_free(&p);
+ }
+ printf("ok: the window byte budget counts the salt push (edge: +5 exactly)\n");
+}
+
+/* Same template, different salt: different coinbase, so a different txid --
+ * and with no other transactions that txid IS the merkle root. */
+static void test_job_salt_makes_distinct_work(void) {
+ for (int k = 0; k < K_COUNT; ++k) {
+ uint8_t root[2][32];
+ for (int j = 0; j < 2; ++j) {
+ coinbase_parts_t p = {0}; char err[256] = {0};
+ assert(build_kind(k, 1000 + (uint32_t)j, "/sp/", 4, 8, 0, &p, NULL,
+ err, sizeof err) == 0);
+ size_t n = p.cb1_len + 12 + p.cb2_len;
+ uint8_t *tx = malloc(n); assert(tx);
+ memcpy(tx, p.cb1, p.cb1_len);
+ memset(tx + p.cb1_len, 0x11, 12);
+ memcpy(tx + p.cb1_len + 12, p.cb2, p.cb2_len);
+ uint8_t h1[32];
+ sha256(tx, n, h1); sha256(h1, 32, root[j]);
+ free(tx); coinbase_parts_free(&p);
+ }
+ assert(memcmp(root[0], root[1], 32) != 0);
+ }
+ printf("ok: two salts from one template give different merkle roots\n");
+}
+
int main(void) {
test_p2pkh_address();
test_the_built_coinbase_respects_its_budget();
@@ -1670,6 +1893,11 @@ int main(void) {
test_scriptsig_length_matches_advertised_extranonce();
test_wrong_width_extranonce_desyncs_the_parse();
test_scriptsig_over_100_is_rejected();
+ test_job_salt_push();
+ test_job_salt_zero_is_the_classic_layout();
+ test_job_salt_counts_against_the_scriptsig_cap();
+ test_job_salt_counts_against_the_window_byte_budget();
+ test_job_salt_makes_distinct_work();
test_bip350_supported();
test_bip350_refused_by_policy();
test_bip350_invalid();
diff --git a/tests/test_pplns.c b/tests/test_pplns.c
index 1d71c6e..cd344b2 100644
--- a/tests/test_pplns.c
+++ b/tests/test_pplns.c
@@ -217,7 +217,7 @@ static void test_the_floor_and_dust_boundaries_are_exact(void) {
char berr[256] = {0};
coinbase_payee_t whole[] = { { A, 54500LL } };
CHECK(coinbase_build_window(800000, 54500LL, whole, 1, A, 100, NULL,
- NULL, 4, 8, 0, 546, &parts, NULL,
+ NULL, 0, 4, 8, 0, 546, &parts, NULL,
berr, sizeof berr) == 0);
coinbase_parts_free(&parts);
printf("ok: the floor and dust boundaries are exact, and the builder agrees\n");
@@ -285,7 +285,7 @@ static void test_the_builder_accepts_what_the_splitter_produces(void) {
coinbase_parts_t parts;
char berr[256] = {0};
int rc = coinbase_build_window(800000, REWARDS[i], out, 3,
- A, FEES[j], NULL, "/sp/", 4, 8,
+ A, FEES[j], NULL, "/sp/", 0, 4, 8,
0, 546, &parts, NULL,
berr, sizeof berr);
if (rc != 0) {
@@ -344,7 +344,7 @@ static void test_a_window_nobody_clears_is_predicted_and_refused(void) {
* it would render nothing on every connection. */
coinbase_parts_t parts;
char berr[256] = {0};
- CHECK(coinbase_build_window(800000, 5000, out, N, NULL, 0, NULL, NULL,
+ CHECK(coinbase_build_window(800000, 5000, out, N, NULL, 0, NULL, NULL, 0,
4, 8, 0, 0, &parts, NULL,
berr, sizeof berr) < 0);
CHECK(strstr(berr, "no payee fits") != NULL);
@@ -355,7 +355,7 @@ static void test_a_window_nobody_clears_is_predicted_and_refused(void) {
CHECK(pplns_split_window(5000, 0, 0, claims, N, 100.0 + (N - 1), 0,
out, N, &r, err, sizeof err) == 0);
CHECK(r.below_floor == (size_t)(N - 1));
- CHECK(coinbase_build_window(800000, 5000, out, N, NULL, 0, NULL, NULL,
+ CHECK(coinbase_build_window(800000, 5000, out, N, NULL, 0, NULL, NULL, 0,
4, 8, 0, 0, &parts, NULL,
berr, sizeof berr) == 0);
coinbase_parts_free(&parts);
@@ -499,7 +499,7 @@ static void test_oversizing_the_slot_estimate_starves_the_largest_claims(void) {
coinbase_window_result_t res;
char berr[256] = {0};
CHECK(coinbase_build_window(800000, 5000000000LL, payees, N, NULL, 0,
- NULL, NULL, 4, 8, TIGHT, 546,
+ NULL, NULL, 0, 4, 8, TIGHT, 546,
&parts, &res, berr, sizeof berr) == 0);
/* Claim 0 is the largest in the window. Where did it land, and was
* that position paid? */
diff --git a/tests/test_stratum.c b/tests/test_stratum.c
index 040c627..bbd0ea4 100644
--- a/tests/test_stratum.c
+++ b/tests/test_stratum.c
@@ -1,5 +1,6 @@
#include "../src/stratum.h"
#include "../src/share.h"
+#include "../src/sha256.h"
#include "../src/store.h" /* store_fraction_delta_t, for the payout-queue observer */
#include "../src/cjson/cJSON.h"
@@ -1751,6 +1752,94 @@ static void test_extranonce1_unique_across_connections(void) {
stratum_server_free(s);
}
+/* Pull params[idx] (a string) out of the mining.notify line in buf. */
+static char *notify_param_str(const char *buf, size_t len, int idx) {
+ char *copy = malloc(len + 1), *res = NULL;
+ if (!copy) return NULL;
+ memcpy(copy, buf, len); copy[len] = '\0';
+ for (char *line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
+ if (!strstr(line, "mining.notify")) continue;
+ cJSON *msg = cJSON_Parse(line);
+ if (!msg) continue;
+ cJSON *params = cJSON_GetObjectItem(msg, "params");
+ cJSON *v = cJSON_IsArray(params) ? cJSON_GetArrayItem(params, idx) : NULL;
+ if (cJSON_IsString(v)) res = strdup(v->valuestring);
+ cJSON_Delete(msg);
+ break;
+ }
+ free(copy);
+ return res;
+}
+
+/* coinb1 / coinb2 the server sends for `job`, to a fresh connection. */
+static void job_coinbase_parts(stratum_server_t *s, stratum_job_t *job,
+ char **cb1, char **cb2) {
+ stratum_server_set_job(s, job, 1);
+ stratum_conn_t *c = stratum_conn_new_for_test(s);
+ char *out = NULL; size_t olen = 0;
+ stratum_handle_message(s, c, "{\"id\":1,\"method\":\"mining.subscribe\",\"params\":[]}",
+ &out, &olen); free(out); out = NULL; olen = 0;
+ stratum_handle_message(s, c,
+ "{\"id\":2,\"method\":\"mining.authorize\","
+ "\"params\":[\"" TEST_ADDR "\",\"x\"]}", &out, &olen);
+ *cb1 = out ? notify_param_str(out, olen, 2) : NULL;
+ *cb2 = out ? notify_param_str(out, olen, 3) : NULL;
+ free(out);
+ stratum_conn_free_for_test(c);
+}
+
+/* The defect: the pool re-issues a job on an unchanged template, and the
+ * coinbase -- hence merkle root -- came out byte-identical. Jobs built from
+ * the same template with different ids must now differ in coinbase, and
+ * through it in merkle root; a job whose salt is forced to 0 carries no push,
+ * which is 5 bytes (10 hex chars) less. */
+static void test_jobs_from_one_template_have_distinct_coinbases(void) {
+ stratum_cfg_t cfg = { .bind_port = 0, .max_conns = 1, .initial_diff = 1.0 };
+ snprintf(cfg.bind_addr, sizeof(cfg.bind_addr), "127.0.0.1");
+ snprintf(cfg.coinbase_tag, sizeof cfg.coinbase_tag, "/sp/");
+ stratum_server_t *s = NULL;
+ CHECK(stratum_server_start(&cfg, &s) == 0);
+ if (!s) return;
+ uint8_t net[32]; memset(net, 0xff, 32);
+
+ stratum_job_t *a = make_test_job("18f3a0c1d2e", net);
+ stratum_job_t *b = make_test_job("18f3a0c1d4f", net); /* 33 ms later */
+ stratum_job_t *z = make_test_job("18f3a0c1d50", net);
+ stratum_job_set_cb_salt_for_test(z, 0);
+ CHECK(stratum_job_cb_salt_for_test(a) != 0);
+ CHECK(stratum_job_cb_salt_for_test(b) != 0);
+ CHECK(stratum_job_cb_salt_for_test(a) != stratum_job_cb_salt_for_test(b));
+
+ char *a1, *a2, *b1, *b2, *z1, *z2;
+ job_coinbase_parts(s, a, &a1, &a2);
+ job_coinbase_parts(s, b, &b1, &b2);
+ job_coinbase_parts(s, z, &z1, &z2);
+ CHECK(a1 && a2 && b1 && b2 && z1 && z2);
+ if (a1 && a2 && b1 && b2 && z1 && z2) {
+ CHECK(strcmp(a1, b1) != 0); /* different coinbase */
+ CHECK(strcmp(a2, b2) == 0); /* nothing else moved */
+ CHECK(strlen(a1) == strlen(z1) + 10); /* 5 bytes = 10 hex chars */
+ CHECK(strcmp(a2, z2) == 0);
+
+ /* With no other transactions the coinbase txid is the merkle root. */
+ char *cbs[2] = { a1, b1 }, *cb2s[2] = { a2, b2 };
+ uint8_t root[2][32];
+ for (int j = 0; j < 2; ++j) {
+ size_t l1 = strlen(cbs[j]) / 2, l2 = strlen(cb2s[j]) / 2;
+ size_t n = l1 + STRATUM_EXTRANONCE1_SIZE + STRATUM_EXTRANONCE2_SIZE + l2;
+ uint8_t *tx = calloc(1, n);
+ for (size_t i = 0; i < l1; ++i) { unsigned v; sscanf(cbs[j] + 2*i, "%2x", &v); tx[i] = (uint8_t)v; }
+ for (size_t i = 0; i < l2; ++i) { unsigned v; sscanf(cb2s[j] + 2*i, "%2x", &v);
+ tx[l1 + STRATUM_EXTRANONCE1_SIZE + STRATUM_EXTRANONCE2_SIZE + i] = (uint8_t)v; }
+ uint8_t h[32]; sha256(tx, n, h); sha256(h, 32, root[j]);
+ free(tx);
+ }
+ CHECK(memcmp(root[0], root[1], 32) != 0);
+ }
+ free(a1); free(a2); free(b1); free(b2); free(z1); free(z2);
+ stratum_server_free(s);
+}
+
/* The per-connection ring keys on (job_id|en2|ntime|nonce|version), so the
* same solution resubmitted under a *different* job id slips past it. When
* both jobs carry the same template the header — and therefore the hash — is
@@ -1767,7 +1856,8 @@ static void test_dedupe_same_hash_across_job_ids(void) {
stratum_server_start(&cfg, &s);
uint8_t net[32] = {0};
- stratum_server_set_job(s, make_test_job("J1", net), 1);
+ stratum_job_t *j1 = make_test_job("J1", net);
+ stratum_server_set_job(s, j1, 1);
stratum_conn_t *c = stratum_conn_new_for_test(s);
char *out = NULL; size_t olen = 0;
@@ -1785,8 +1875,13 @@ static void test_dedupe_same_hash_across_job_ids(void) {
CHECK(obs.shares == 1);
free(out); out=NULL; olen=0;
- /* Same template, new id. Identical header -> identical hash. */
- stratum_server_set_job(s, make_test_job("J2", net), 1);
+ /* Same template, new id, and -- pinned for this test -- the same coinbase
+ * salt, so the header really is identical. In production every job gets
+ * its own salt (see the J3 block below), which is why this ring is the
+ * second line of defence and not the first. */
+ stratum_job_t *j2 = make_test_job("J2", net);
+ stratum_job_set_cb_salt_for_test(j2, stratum_job_cb_salt_for_test(j1));
+ stratum_server_set_job(s, j2, 1);
stratum_handle_message(s, c,
"{\"id\":4,\"method\":\"mining.submit\","
"\"params\":[\"w\",\"J2\",\"deadbeefcafebabe\",\"60000000\",\"00000001\"]}",
@@ -1794,6 +1889,24 @@ static void test_dedupe_same_hash_across_job_ids(void) {
CHECK(obs.shares == 1); /* still one */
CHECK(obs.rejects >= 1);
CHECK(strstr(obs.last_reason, "duplicate") != NULL);
+ free(out); out=NULL; olen=0;
+
+ /* Same template AGAIN under a third id, this time with its own salt (the
+ * production path): the coinbase differs, so the same (en2, ntime, nonce)
+ * is a different header and a genuinely new share. A rig that re-searches
+ * identical work no longer re-finds credited hashes, because the work is
+ * no longer identical. */
+ stratum_job_t *j3 = make_test_job("J3", net);
+ CHECK(stratum_job_cb_salt_for_test(j3) != stratum_job_cb_salt_for_test(j1));
+ CHECK(stratum_job_cb_salt_for_test(j3) != 0);
+ stratum_server_set_job(s, j3, 1);
+ int rejects_before = obs.rejects;
+ stratum_handle_message(s, c,
+ "{\"id\":5,\"method\":\"mining.submit\","
+ "\"params\":[\"w\",\"J3\",\"deadbeefcafebabe\",\"60000000\",\"00000001\"]}",
+ &out, &olen);
+ CHECK(obs.shares == 2);
+ CHECK(obs.rejects == rejects_before);
free(out);
stratum_conn_free_for_test(c);
@@ -3201,6 +3314,65 @@ static void test_the_queue_credits_the_miners_the_block_actually_skipped(void) {
printf("ok: the payout queue credits the miner the block actually skipped\n");
}
+/* One found block on a two-miner window under a coinbase byte budget `budget`,
+ * with the job salt either left as stratum_job_new set it or forced to 0.
+ * Returns how many rotations the block staged, or -1 if no block was found
+ * (the coinbase could not be rendered at that budget at all). */
+static int salted_edge_rotations(size_t budget, int force_unsalted) {
+ obs_t obs = {0};
+ stratum_cfg_t cfg = { .bind_port = 0, .max_conns = 2, .initial_diff = 1e12,
+ .coinbase_pays_window = 1,
+ .max_coinbase_bytes = budget,
+ .ctx = &obs, .on_share = on_share,
+ .on_reject = on_reject, .on_block = on_block,
+ .on_window_fractions = on_window_fractions };
+ snprintf(cfg.bind_addr, sizeof cfg.bind_addr, "127.0.0.1");
+ stratum_server_t *s = NULL;
+ stratum_server_start(&cfg, &s);
+ if (!s) return -2;
+ stratum_conn_t *c = stratum_conn_new_for_test(s);
+ handshake(s, c);
+ uint8_t net[32]; memset(net, 0xff, 32);
+ stratum_job_t *job = make_test_job("JQS", net);
+ if (force_unsalted) stratum_job_set_cb_salt_for_test(job, 0);
+ const coinbase_payee_t win[] = {
+ { TEST_ADDR, 3000000000LL }, { TEST_ADDR2, 2000000000LL },
+ };
+ const int64_t ids[] = { 301, 302 };
+ CHECK(stratum_job_set_window(job, win, ids, 2) == 0);
+ stratum_server_set_job(s, job, 1);
+ char *out = NULL; size_t olen = 0;
+ stratum_handle_message(s, c,
+ "{\"id\":9,\"method\":\"mining.submit\","
+ "\"params\":[\"w\",\"JQS\",\"deadbeefcafebabe\",\"60000000\",\"00000001\"]}",
+ &out, &olen);
+ free(out);
+ int r = obs.blocks == 1 ? (int)obs.frac_calls : -1;
+ stratum_conn_free_for_test(c);
+ stratum_server_free(s);
+ return r;
+}
+
+/* The after-block recount must render with the job's salt.
+ *
+ * submit_with_job() rebuilds the window coinbase after a block to learn who
+ * the block actually paid, and stages the rotation from that. It must pass
+ * the same salt the miner hashed: at the exact edge of the byte budget the
+ * salted coinbase cuts a payee that an unsalted one would still pay, and a
+ * recount without the salt would then record "everyone paid" for a block
+ * that skipped somebody. So find a budget where the salted job cuts and the
+ * unsalted one does not, and require the salted block to stage the rotation. */
+static void test_the_after_block_recount_uses_the_job_salt(void) {
+ size_t edge = 0;
+ for (size_t b = 100; b <= 600 && !edge; ++b)
+ if (salted_edge_rotations(b, 1) == 0 && salted_edge_rotations(b, 0) == 1)
+ edge = b;
+ CHECK(edge > 0);
+ /* and the two sides of it really are the edge */
+ CHECK(salted_edge_rotations(edge + COINBASE_JOB_SALT_PUSH_BYTES, 0) == 0);
+ printf("ok: the after-block recount renders with the job salt (edge %zu)\n", edge);
+}
+
/* A window nothing was dropped from stages nothing.
*
* The deltas are "who did this block treat differently from their claim", so a
@@ -3585,6 +3757,7 @@ int main(void) {
test_socket_setup_disabled();
test_extranonce1_unique_across_connections();
test_dedupe_same_hash_across_job_ids();
+ test_jobs_from_one_template_have_distinct_coinbases();
test_share_dedupe_index_tracks_the_ring();
test_rejected_candidate_is_not_a_block();
test_accepted_candidate_reports_accepted();
@@ -3599,6 +3772,7 @@ int main(void) {
test_pplns_coinbase_pays_every_miner_in_the_window();
test_the_queue_credits_the_miners_the_block_actually_skipped();
test_a_block_that_pays_everyone_stages_no_rotation();
+ test_the_after_block_recount_uses_the_job_salt();
test_a_rejected_candidate_stages_no_rotation();
test_pplns_btc_takes_a_bitcoin_username();
test_pplns_thunder_takes_a_thunder_username();