From 4e40e08fd407b98f07a5bc7a63ac091c47913c8f Mon Sep 17 00:00:00 2001 From: satoshi Date: Tue, 1 Sep 2026 23:53:06 +0300 Subject: [PATCH] Patch 9: fix shares being rejected after a pool restart bug --- README.md | 4 +- ...0009-resume-client-diff-on-reconnect.patch | 117 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 ckpool/patches/0009-resume-client-diff-on-reconnect.patch diff --git a/README.md b/README.md index d8f19e9..53ccb2b 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ CKPool is a high-performance stratum server, but it has no web interface and lim ### Patches Applied to CKPool -Eight patches are applied to upstream commit `cfb0f83` (which itself includes the workbase_id fix, extended ESP32/NerdMiner timeouts, configurable `dropidle`, and vardiff improvements): +Nine patches are applied to upstream commit `cfb0f83` (which itself includes the workbase_id fix, extended ESP32/NerdMiner timeouts, configurable `dropidle`, and vardiff improvements): | Patch | Purpose | |-------|---------| @@ -44,6 +44,7 @@ Eight patches are applied to upstream commit `cfb0f83` (which itself includes th | `0006` | Expose raw reject count in pool stats | | `0007` | Replace hardcoded "ckpool" branding in the coinbase scriptSig with "kamado" (same 6 bytes, consensus-safe) | | `0008` | Log full PoW reproduction data (raw 80-byte header, coinbase, merkle branches) when a share sets a new best difficulty | +| `0009` | Send `mining.set_difficulty` before the first `mining.notify`, resume a reconnecting miner's tuned difficulty, and grant its first job a difficulty grace window | ### Middleware API @@ -94,6 +95,7 @@ The Svelte 5 dashboard connects via WebSocket for real-time push updates (no pol - **ckpool kill-on-failure** — When bitcoind becomes unreachable, kamado-api kills ckpool so miners can failover to another pool. ckpool restarts automatically when bitcoind recovers. - **Deferred RPC** — Dashboard RPCs wait until the ckpool notifier completes on tip change to prioritize system resources for block reconstruction and validation. - **Log cursor persistence** — The tailer's file offset is stored in SQLite so block-solve detection resumes correctly after restart. +- **Clean restarts for miners** — Upstream ckpool sends a reconnecting miner its first job *before* telling it the difficulty, and resets every client to the pool's `startdiff`, discarding what vardiff had tuned. Both produce a burst of "Above target" rejects on every pool restart. Patch `0009` sends `mining.set_difficulty` first, seeds the starting difficulty from the worker's retained hashrate, and accepts shares for the already-in-flight job down to the pool minimum until the next `mining.notify`. - **Warm-up resilience** — The ckpool socket client handles transient EOFs during startup gracefully instead of crashing. ## API Reference diff --git a/ckpool/patches/0009-resume-client-diff-on-reconnect.patch b/ckpool/patches/0009-resume-client-diff-on-reconnect.patch new file mode 100644 index 0000000..d133d2d --- /dev/null +++ b/ckpool/patches/0009-resume-client-diff-on-reconnect.patch @@ -0,0 +1,117 @@ +diff --git a/src/stratifier.c b/src/stratifier.c +index a856b3eb..fcb9eb94 100644 +--- a/src/stratifier.c ++++ b/src/stratifier.c +@@ -5477,6 +5477,69 @@ static void update_solo_client(sdata_t *sdata, workbase_t *wb, const int64_t cli + stratum_add_send(sdata, json_msg, client_id, SM_UPDATE); + } + ++/* Kamado (patches/0009): restore a reconnecting miner's difficulty and ++ * grant the work it already holds a difficulty grace window. ++ * ++ * Upstream gives every new connection ckp->startdiff, so a pool restart ++ * discards the difficulty vardiff had tuned each miner to and makes it ++ * re-converge from the pool default. ckpool does persist per-worker ++ * hashrate across restarts (read_userstats, decayed by the downtime), so ++ * the starting difficulty can be derived from it with the same formula ++ * and clamps add_submit's vardiff uses. ++ * ++ * The grace window covers work in flight. A miner may hold queued jobs, ++ * and much firmware only applies a new difficulty on the following job, ++ * so its first shares can be below the difficulty we just assigned. ++ * Pointing diff_change_job_id past the current workbase makes the ++ * existing check in parse_submit validate those against old_diff (the ++ * pool minimum) instead of rejecting them as "high diff". The next ++ * mining.notify closes the window, since by then the client has had the ++ * difficulty for a whole job. ++ * ++ * Needs to be entered with no workbase or instance lock held. */ ++static void resume_client_diff(ckpool_t *ckp, sdata_t *sdata, stratum_instance_t *client) ++{ ++ const worker_instance_t *worker = client->worker_instance; ++ int64_t optimal, mindiff, network_diff = 0; ++ double dsps = 0; ++ ++ ck_rlock(&sdata->workbase_lock); ++ if (sdata->current_workbase) { ++ network_diff = sdata->current_workbase->network_diff; ++ /* Every job sent so far, including the one about to go out, ++ * predates this client acting on its difficulty. */ ++ client->diff_change_job_id = sdata->workbase_id + 1; ++ } ++ ck_runlock(&sdata->workbase_lock); ++ ++ /* Read unlocked, as the vardiff path in add_submit does: these are ++ * decaying averages used as a heuristic, not accounting. */ ++ if (worker) ++ dsps = worker->dsps5 > 0 ? worker->dsps5 : worker->dsps1; ++ if (dsps > 0) { ++ optimal = lround(dsps * 3.33); ++ /* Client suggest diff overrides worker mindiff */ ++ if (client->suggest_diff) ++ mindiff = client->suggest_diff; ++ else ++ mindiff = worker->mindiff; ++ optimal = MAX(optimal, ckp->mindiff); ++ optimal = MAX(optimal, mindiff); ++ if (ckp->maxdiff) ++ optimal = MIN(optimal, ckp->maxdiff); ++ if (network_diff > 0) ++ optimal = MIN(optimal, network_diff); ++ if (optimal > 0 && optimal != client->diff) { ++ LOGNOTICE("Client %s resuming at diff %"PRId64" from retained hashrate instead of startdiff %"PRId64, ++ client->identity, optimal, ckp->startdiff); ++ client->diff = optimal; ++ } ++ } ++ /* Floor for the grace window only. client->diff stays what we ++ * advertise; vardiff overwrites old_diff on its first adjustment. */ ++ client->old_diff = ckp->mindiff > 0 ? ckp->mindiff : 1; ++} ++ + /* Needs to be entered with client holding a ref count. */ + static json_t *parse_authorise(stratum_instance_t *client, const json_t *params_val, + json_t **err_val) +@@ -5558,6 +5621,19 @@ out: + sdata_t *sdata = ckp->sdata; + workbase_t *wb; + ++ /* Kamado (patches/0009): pick up where vardiff left off and ++ * arm the first-job grace window. Called before any lock is ++ * taken here — it acquires the workbase lock itself. */ ++ resume_client_diff(ckp, sdata, client); ++ ++ /* Kamado (patches/0009): mining.set_difficulty must precede ++ * the first mining.notify. Upstream sent the job first, so a ++ * miner started hashing it at whatever difficulty its firmware ++ * defaults to and its early shares came back under the ++ * difficulty we had assigned — a burst of "high diff" rejects ++ * from every miner on every pool restart. */ ++ stratum_send_diff(sdata, client); ++ + /* To avoid grabbing recursive lock */ + ck_wlock(&sdata->workbase_lock); + wb = sdata->current_workbase; +@@ -5573,8 +5649,6 @@ out: + ck_wlock(&sdata->workbase_lock); + wb->readcount--; + ck_wunlock(&sdata->workbase_lock); +- +- stratum_send_diff(sdata, client); + } + return json_boolean(ret); + } +@@ -6567,9 +6641,12 @@ static void update_client(const stratum_instance_t *client, const int64_t client + { + sdata_t *sdata = client->sdata; + ++ /* Kamado (patches/0009): difficulty before work, so a client never ++ * starts a job without knowing the difficulty it will be judged ++ * against. Upstream sent the update first. */ ++ stratum_send_diff(sdata, client); + if (!client->ckp->btcsolo) + stratum_send_update(sdata, client_id, true); +- stratum_send_diff(sdata, client); + } + + static json_params_t