Releases: warioishere/blitzpool-server-rust
Release list
v2.4.0
Running your own pool? Start with DEPLOYMENT.md and the new
simple-setup/directory.Upgrading an existing pool? This release changes the config layout. Read Upgrade notes at the bottom before you update: an old config no longer loads.
A simple setup for running your own pool
simple-setup/runs the pool as two processes (front,apiandpayout,stats,notify) with Postgres and Valkey from onedocker-compose.yml. A Bitcoin Core 31 node is included as an optional profile (mainnet, testnet4 or regtest); your own node works too, through its IPC socket directory. Storage lives in Docker volumes, so there is nothing to prepare, and a.envfile is optional.DEPLOYMENT.mdwalks through it end to end, including a regtest run with CPU miners to see the whole path work in a few minutes, switching on PPLNS, Group-Solo and Blockparty, updates and backups.blitzpool.example.tomlis now a complete, working Solo pool;blitzpool.full.example.tomldocuments every section and key.full-setup/is unchanged and remains the four-process layout for redeploying the API, payouts and notifications one at a time.
Stratum V2 keys
blitzpool --sv2-keygen(ordocker compose run --rm --no-deps front --sv2-keygen) prints a new[sv2] authority_privkey_hextogether with the public key SV2 miners and JD clients pin. It decodes the key the same way the pool does at start.- The front logs its authority public key on every start (
stratum-v2: authority public key). Until now the pool printed it nowhere.
Configuration: each payout mode owns its section
[solo],[pplns],[group_solo]and[blockparty]each carry their ownfee_address,fee_percent, coinbase weight budget and minimum payout. Nothing is inherited from another section any more; on Solo the fee stays optional.- Group-Solo is switched on by
[group_solo], like PPLNS and Blockparty by theirs. Without it the pool runs no Group-Solo engine and template stream, and the API refuses to create a group. A pool that is Solo only no longer needs a fee address. - With
[group_solo]absent the pool refuses to start while active groups exist, so no member is moved to Solo without the operator noticing. - The block reconcile check recognises every mode's fee address as the pool's, so a Group-Solo or Blockparty block paying its own fee address is checked like any other.
API
/api/pplns/feesno longer fails without Group-Solo.groupFeePercentisnullwhile Group-Solo is off, and the newblockpartyFeePercentreports the Blockparty fee, which can now differ from the Group-Solo fee.
Fixes
- The startup hint for a Group-Solo config error pointed at the Solo fee keys; it now names the
[group_solo]keys. - The documentation of
[sv2] authority_privkey_hexclaimed a random key is generated when it is missing. The front requires it; the documentation says so.
Dependencies
- hashbrown 0.17, getrandom 0.4, base64 0.23.
- async-channel stays on 1.x: its channel types cross the boundary to the Bitcoin Core IPC layer from sv2-apps and must match its version. Dependabot no longer proposes its majors.
Upgrade notes
-
The config layout changed, and an old config fails to load. The error names one place that no longer fits, not necessarily the first in the file: for example
missing field fee_addressat[blockparty], orunknown field group_fees. Nothing changes silently. Convert the config before deploying the new version:Before After [group_fees] address[group_solo] fee_addressand[blockparty] fee_address[group_fees] percent[group_solo] fee_percentand[blockparty] fee_percent[group_fees] coinbase_weight_budget[group_solo] coinbase_weight_budgetGroup-Solo / Blockparty fee taken from [pplns]when[group_fees]was absentset fee_addressandfee_percentin[group_solo]and[blockparty]Group-Solo minimum payout taken from [pplns] min_payout_sats[group_solo] min_payout_sats(default 5000)[solo] dev_fee_address,dev_fee_percent[solo] fee_address,fee_percentGroup-Solo always on add [group_solo]to keep it runningExample: a config with
[group_fees] address = "bc1q…",percent = 1.5,coinbase_weight_budget = 25000and[pplns] min_payout_sats = 5000becomes[group_solo] fee_address = "bc1q…" fee_percent = 1.5 coinbase_weight_budget = 25000 min_payout_sats = 5000 [blockparty] fee_address = "bc1q…" fee_percent = 1.5 # existing min_payout_sats / coinbase_weight_budget stay
-
Switch the config and the image together. Every process reads the config only at start: running processes keep working, but a process of the old version that restarts with the new config fails, and the new version does not start with the old one. Convert the config, then recreate all pool processes on the new image.
-
Check the converted file before the swap with
blitzpool --config <file> --check-config(ordocker compose run --rm --no-deps <service> --config /app/blitzpool.toml --check-config). -
No new migrations.
-
UI consumers of
/api/pplns/fees: showblockpartyFeePercentfor Blockparty instead ofgroupFeePercent, and treatnullas "mode off".
v2.3.9
Payouts: a found block books what its own coinbase paid, in every mode
- Group-Solo books a found block from its decoded coinbase alone. Builds no longer write a weight snapshot, so every build is bookable; a block is booked only when the pool built its coinbase, otherwise the round stays untouched.
- Blockparty blocks now wait for their confirmations and settle through the confirmation watcher like PPLNS and Group-Solo, so an orphaned Blockparty block books nothing.
- A Blockparty party is frozen once it is
ready: its coinbase already pays the roster, so changing splits, removing a member or creating a join link is refused withnot-editable, as it already was foractiveparties. The split booked for a block is therefore the one the block paid. - When a Blockparty split cannot be built, the admin is served no job. Previously the admin fell back to a solo coinbase that paid the whole block to the admin and nothing to the members.
- PPLNS and Group-Solo builds share one result type and one conversion into coinbase outputs, and the PPLNS settlement snapshot now lives in the PPLNS engine, its only reader.
Payouts: a new share source no longer pays one miner the whole block for 30 s
- The coinbase builders cache each build for 30 s, and on the front nothing invalidates that cache when a share lands, because shares are recorded by the payout process. A build made while the share source was empty therefore kept paying the asking miner the whole block for up to 30 s after other miners' shares had arrived: Group-Solo cached the bootstrap distribution itself, PPLNS cached the empty window it was built from. An empty share source is no longer cached in either mode, so the first share reaches the next job.
Block reconcile: no false alarm for blocks awaiting confirmation
- A block parked for its confirmations has no payout rows yet, so a reconcile pass shortly after the payout process restarted reported it as "registered but no payout was ever booked". Parked blocks are now held back and checked again on the next pass, so a booking that later fails is still reported. A block the pool never registered is still always reported.
Stratum V2
- A rejected share is weighted with its channel's current difficulty. It was weighted with the difficulty from the last channel open, which vardiff never updated, so SV2 reject charts and the Group-Solo reject lane carried a stale value. The stale per-session difficulty is gone.
- A
bad-extranonce-sizereject is refused before anything is hashed, so it no longer counts as evidence of a too-hard target for vardiff, matching the SV1 pre-hash rejects. - The Standard and Extended submit handlers, the job broadcast for grouped and ungrouped channels and the PPLNS stream handling of the SV1 and SV2 servers each share one implementation instead of two copies.
Stratum V1
- Shutdown waits at most 5 s (
shutdown_drain_timeout) per stream translator, as SV2 already did. A translator that never drained held the shutdown forever.
Job Declaration Protocol
- A job-allocation token stays valid through its expiry millisecond, as the token store documents; the mining-job bridge treated that millisecond as expired.
- Declared transactions are kept as one position-ordered list, and the bridge stores only the fields it reads of a declared job instead of a full copy.
API
/api/inforeports the front's start time as the pooluptime. The front writes it to Redis (pool:core:started_at) on boot, so restarting the API, payout or notify process no longer resets the uptime. Until the front has been restarted once, the API reports its own start as before.- Response-cache entries expire through the cache itself instead of one timer task per insert, so an entry that was invalidated and recomputed is no longer cut short by the old entry's timer.
- A group mutation also drops the group's cached max-difficulty and window-timeline responses.
Notifications
- FCM and Web Push (VAPID) tokens are signed with
ringdirectly;jsonwebtokenis removed. Payloads are unchanged. - Every push path (fan-out, device status, network-difficulty cron) delivers through one FCM and one UnifiedPush sender, and the three device notices share one routing path and one Telegram sender. The network-difficulty cron now logs a failed prune or stamp instead of dropping the error.
Dependencies and security
- axum 0.8, tower-http 0.7, tower_governor 0.8, governor 0.10, reqwest 0.13, redis 1.x, thiserror 2, plus the compatible lockfile updates. This clears RUSTSEC-2026-0258 (h2), RUSTSEC-2026-0285 (rustls), the faster-hex and event-listener soundness advisories and a yanked chacha20.
- Every HTTP client is built through one constructor that installs the
ringTLS provider, so the tree carries no second crypto library. Outgoing HTTPS verifies against the system certificate store. - The Valkey client is built without TLS, which the pool never used, removing the unmaintained
rustls-pemfile(RUSTSEC-2025-0134). - CI runs
cargo auditon push, on pull requests and weekly; Dependabot proposes updates monthly.
Removed
- The daily purge of
rpc_block_entity: nothing writes that table, so the cron deleted nothing after its first run. The table stays. - The Group-Solo per-finder snapshot and round total, which only a fallback no booking reached and tests read.
- The group and Blockparty routing caches on the payout and notify processes, which never read them; they are built only where the front's routing and the API's group endpoints use them.
- Code without callers: the leftovers of directed invitations, unused DB queries and row types, unconstructed error variants, the GeoIP configuration nothing set, the hook generics on the API state and services.
Upgrade notes
- Remove
client_diff_scores_secsandgroup_by_address_secsfrom[api.cache]before deploying: nothing reads them, and an unknown key fails the config load. Removing them first is safe with v2.3.8. - Roll the payout process before the front. The new front no longer sends a Group-Solo snapshot in the block-found event, and an old payout process would leave such a block unbooked. A new payout process with an old front is fine. Blocks already parked in the old format still settle.
- No new migrations.
- The Redis keys
groupsolo:*:totalare no longer read or written and can be deleted.
v2.3.8
Group-Solo: round resets across a daylight-saving change
- Weekly and monthly round resets computed the next reset by adding 24 hours per day. On a 25-hour day (the autumn clock change) that stays on the same date, so the computation never finished and stalled the payout process with it. Dates now advance by calendar day; daily resets on that day no longer land in the past, and custom resets no longer fire an hour early.
Job Declaration Protocol
- A declared coinbase is checked before it reaches the validation engine: only a single-input segwit coinbase with a scriptSig of at most 100 bytes is passed on, anything else is refused with
invalid-coinbase-tx. - A declaration whose node still lacks transactions after
ProvideMissingTransactions.Successis refused withmissing-txsinstead of being accepted without a full node check. - Each declaration starts with fresh validation-engine state, so a tip change between two declarations is no longer reported as
stale-chain-tip. - Up to three declarations may wait for their missing transactions at once, each answered under its own
request_id. - A declaration that arrives before the pool knows a chain tip is refused with
stale-chain-tip, so every accepted declaration is bound to a tip. - A
ProvideMissingTransactions.Successwith the wrong number of transactions is answeredmissing-txs. - Two token allocations right after connecting are both answered; the sustained rate stays one per second.
Stratum
- SV1, SV2, HTTP and TLS are told apart on their opening bytes instead of the first byte. An SV2 connection opens with a pseudo-random key whose first byte matched another protocol's rule for 7 of 256 values, so about one SV2 connection attempt in 37 went to the wrong server.
- A connection whose peer stops acknowledging data is dropped after 150 s instead of after the kernel's retransmission limit.
Fixes
- The difficulty scoreboard showed no 30-day (or 7-day) entries for up to two hours (or 30 minutes) after a new period began: the
diff-scoresresponse was cached past the hour boundary. It now expires at the next full hour.
Upgrade notes
- No new migrations and no config changes.
- A JDP
SetupConnectionwith an unsupported version range is now answeredprotocol-version-mismatch(previouslyunsupported-version), the same code the mining port uses.
v2.3.7
Security: the group admin view could be read with any token
- The open-invite and join-request reads keyed their response cache on whether a token header was present and checked the token only on a cache miss. For 30 s after the admin's own call, any token got the cached admin body. The token is now checked before the cache, the same way the group detail already did it.
Blockparty
- Dissolving a party now frees its members' addresses. The member rows and the join link are deleted in the same transaction as the status change, like Group-Solo. Before, every former member and the admin stayed locked out of later parties, and the custom-extranonce Solo check kept refusing them. Migration 0016 cleans up parties dissolved earlier.
- The party roster (
GET /api/blockparty/:idand member-view) no longer exposes member addresses. It sendsmemberId,addressLabelandisSelflike the Group-Solo roster; the full address only with the admin token.adminAddressstays public, it is the party's mining target.
Best-share charts
- New: the highest single share difficulty per 10-minute slot, pool-wide (
GET /api/info/max-difficulty), per address (GET /api/client/:address/max-difficulty) and per Group-Solo group (GET /api/pplns/groups/:id/max-difficulty), for 1, 3 or 7 days in the slot shape of/accepted. The value is recorded by the existing batched stats flush, without new rows or writes; the group endpoint reads all members in one query. - New:
GET /api/client/:address/best-difficulty/today?since=<ms>returns the best share since the caller's local midnight.
Share statistics
- Stale shares are counted as
Staleinstead ofJobNotFound. Both protocol adapters folded them into job-not-found before the stats saw them, so the stale columns and theStalekey of the reject APIs stayed at zero. - A disconnected session is no longer revived as mining. The live touch was stamped when the satellite consumed a share instead of when the pool accepted it, so a session's last shares could land after its disconnect and
kill_dead_clientsbrought the session back for several minutes. - Accepted and rejected shares are published to the Redis stream in batches, one pipeline per drain instead of one round trip per share. Measured locally the drain goes from about 14,000 to about 160,000 shares per second, which keeps large rentals far away from the point where the publish buffer drops shares.
Group-Solo block preview
- An address without shares in the group's window no longer gets a preview job with itself as finder. The preview names the member with the largest window share instead, and reports it as
previewFinder.
Operations
- The pool builds its schema from an empty database. Migration
0000_baselinecreates the base tables on an empty database and changes nothing on an existing one;db/schema.sqlis gone. - A binary one release older boots against newer migrations. The processes are swapped one at a time, and a core on the previous image used to refuse to start once the api had applied a new migration.
- A refused SV2 channel open and a refused SV1 authorize are logged with the session, the identity and the reason. Before, they only went back to the miner.
GET /api/pplns/groups/membership/:addressanswers yes or no without building the group detail, andGET .../:id/admin-check(Group-Solo and Blockparty) checks an admin token without reading anything else.
Upgrade notes
- Two new migrations:
0016(Blockparty, deletes the member rows and join links of already dissolved parties) and0017(addsmaxDifficultytopool_share_statistics_entityandclient_statistics_entity).0000_baselineis only recorded on an existing database. - A 2.3.6 binary does not start against a database that has migration 0016 or later. Move every process to 2.3.7; from 2.3.7 on, a process one release behind keeps starting.
- No config changes. The Redis and stream formats are unchanged, so the processes can be swapped one after another.
- Stale rejects recorded before the upgrade stay in
JobNotFound; they cannot be split afterwards. The best-share charts fill up from the deploy on.
v2.3.6
Block booking: one settlement path, nothing silently dropped
- The confirmation watcher and the immediate apply settle a found block through one shared function instead of two copies that had already drifted apart.
- A parked block that nothing can book (a Group-Solo block with an unusable group id, or a parked block without a parsed coinbase) moves to the unbookable store instead of being deleted. Its frozen distribution is the only record of what the coinbase paid.
- SV1 and SV2 blocks are booked through the strict whole-transaction coinbase decode that JDP already used. A coinbase with trailing bytes books nothing instead of being read as a prefix.
- The block-found gate compares the share hash against the exact compact target for SV1 and SV2, not a floating-point difficulty.
- A solution the TDP worker never took is no longer reported as a found block. It is logged as a lost block instead of creating a found-block row, a push and a booking that can never confirm.
- JDP: every proven JDC-found block is booked. The allocate blob is built by one encoder for the designated payout output.
Group-Solo
- Window mode buckets rejects and trims them with the window.
- A kick removes the member from the group's own payout layout.
- A member with only rejected shares keeps a row in the group distribution.
- The window trim drops an entry that would go negative, the same rule the PPLNS trim already had.
Notifications
- Best-difficulty messages now reach addresses subscribed on Telegram or ntfy alone. The cron used to scan mobile push subscriptions only, so those users never got one whatever their setting said. Each subscription's own
bestDiffNotificationsEnabledstill decides.
API
nextResetAtcomes from the reset cron's own schedule computation./api/pplns/fees:maxMinerOutputsis the number of miners the coinbase cut actually publishes (it overstated that by two to three). With[pplns.coinbase_autoscale]enabled,coinbaseWeightBudgetreports the live autoscaled budget instead of the configured floor. The group fee comes from the Group-Solo engine's resolved configuration.- Timestamps on the push endpoints use the same ISO format as every other endpoint (
…Z). - Every hashrate field is rounded; per-worker and per-session hashrate were the exceptions.
- SV2 user agents are normalised by the same rule as SV1 (
cgminer/4.11.1is stored ascgminer/sv2), and the downstream report uses it too.
Stratum
[stratum] job_retention_msnow applies to SV2 as well; SV2 used a fixed ten minutes. The key is optional with a default of600000.- sv2-apps v0.8.0 (stratum-core 0.6.0).
- One extranonce allocator for all ports; the worker partitions are unchanged. SV2 now logs partition exhaustion and a failed group job build.
Fixes
- The Group-Solo reset cron panicked on a day without a local midnight (for example
America/Santiagoon its spring-forward day); after that the group never reset again. - The block reconcile check read the genesis block on a short chain and logged a warning on every fresh start.
- The SV1
BLOCK FOUNDlog line printed the template id under the nameheight.
Removed
- The per-session share warmup (
warmup_shares). It was configured and shown in the UI but never enforced; the PPLNSmin_difficultyis the gate./api/pplns/feesno longer returnswarmupShares. - The external share submission endpoints
POST /api/shareandGET /api/share/top-difficulties. Nothing ever submitted to them; theexternal_shares_entitytable stays. - Prometheus metrics that were never emitted.
/metricsserves the stream-consumer lag, the parked-block depths, vardiff adjustments andaccepted_share_undecodable_dropped_total. - Config keys nothing read (see Upgrade notes).
Internal
- Many duplicated implementations are merged into one: the coinbase prefix serializer and witness assembler, the accepted-share stream consumer, the SV1/SV2 block submit path, the share and session sinks, the slot bucketing of the chart endpoints, the address normalizer and more. Behaviour and wire formats are unchanged; the API JSON of the chart endpoints is pinned by characterization tests.
Upgrade notes
- Delete these keys from your config before starting 2.3.6. Every config struct rejects unknown keys, so a leftover key is a boot failure, not a warning:
- top level:
api_secure - the whole
[bitcoin_zmq]and[aggregation]sections [database]:driver,max_query_time_ms,run_migrations[redis]:ttl_secs[sv2]:ed25519_authority_seed_hex,cert_signed_part[debug]:noise_debug[pplns]:warmup_shares[notifications.telegram]and[notifications.ntfy]:diff_notifications
- top level:
- A 2.3.5 binary does not start with the cleaned config:
driverandwarmup_shareswere required there. - Check
[stratum] job_retention_ms: it now also governs SV2. - No new database migrations. The Redis and stream formats are unchanged, so the processes can be swapped one after another.
- The UI needs the matching
blitzpool-uichange for the PPLNS port-gate callout; without it the callout disappears becausewarmupSharesis gone. That UI change also works against 2.3.5. - Local test setup: the test Redis needs
--databases 544.
v2.3.5
PPLNS: inactive miners age out of the window
- The window trims when it exceeds
window_factor × network_difficulty. This pool sits at roughly 0.15 % of that threshold, so the trim never fired, the window grew without bound, and a miner who stopped mining kept its weight for good. Buckets older thanabandoned_balance_daysare now dropped as well; the bucket index is scored by the share's own accept time, written once when a bucket opens. - An existing window is converted at the first boot: every bucket is stamped at that instant and nothing is evicted retroactively.
- The dust sweep pairs an abandoned credit against any open debit, not only against an abandoned one. The counterparty is by construction someone who was mining when the credit was withheld, so requiring silence from both sides meant the sweep never closed a single pair. A cancelled row now stays at zero instead of being deleted, because the same row carries the address's lifetime on-chain payout.
Worker sessions: the front decides who is connected
- A miner that paused with its socket open — standby overnight, a slow rig — lost its
client:live:*key after five minutes, was retired by the dead-session sweep as dead and hard-deleted two hours later while still connected. It vanished from/api/client/:addressand its hashrate from the total until it reconnected. Measured on prod before the fix: 46 of 635 connected devices had no visible row (an upper bound). - The front now publishes which device holds each session (
session:live:<front>) next to the device counts it already published. The sweep treats a held session as alive and falls back to the key verdict only where no front publishes sessions. A dead peer still leaves within about 140 s through the TCP keepalive the front sets on every miner socket. - The revive lookback grows from 15 minutes to the row's two-hour retention.
Upgrade notes
abandoned_balance_daysnow bounds the payout window as well as the ledger: a miner silent that long stops earning a share of the next block. Check the value before deploying; the example config describes both effects.- Exactly
abandoned_balance_daysafter the first boot on this release, every pre-existing bucket becomes eligible at once and the window drops to the work done since that boot, drained at 64 buckets per share append. - Roll out
payoutbeforecore. Untilcoreruns this release no front publishes sessions and the sweep behaves as before. Thecoreswap reconnects every miner, which also recreates the rows of sessions retired earlier. - No migration, no config key added or removed.
v2.3.4
API responses are compressed on the wire
- Every response now goes out gzip/br/deflate-encoded for clients that ask for it. The periodic
/statschart and scoreboard pulls are JSON that compresses roughly five to ten times, and uncompressed they arrived as an egress burst large enough to bufferbloat a 100 Mbit uplink and delay the stratum notify queued behind it. - A no-op for clients that send no
Accept-Encoding, so nothing about the response bodies themselves changed.
Chart ranges: a 14-day preset
?range=14djoins1d,3d,7dand1mat the same native 10-minute resolution — 2016 points where a 30-day request returns 4320.- It exists because the
/statspage never reads further back than 14 days and had to ask for the 30-day payload to get there, so roughly half of every response was transferred, parsed and discarded. - Additive:
1mis unchanged, and every endpoint that already tookrangeaccepts the new preset./api/info/chart/mode/:modekeeps its own narrower set of1d,3dand7d.
Database: page headroom so the busiest statistics table can update in place
- Of
client_statistics_entity's 106.7 M updates only 16.7 % reused their page; the rest wrote a new entry into all five of its indexes, which is how 274 MB of table accumulated 794 MB of index. The four sibling tables measure 78-99 % and are deliberately left alone. - No indexed column changes on the upsert path, so the cause is page space: a row about twenty counter columns wide, updated roughly ten times per 10-minute slot, at fillfactor 100. Migration 0015 sets fillfactor 80 on that one table.
find_client_statisticsandfind_client_difficulty_statisticshad no caller and are gone. They were the only readers ofidon those two tables, whose primary-key indexes are otherwise maintained on every insert for nothing.
Upgrade notes
- Migration 0015 runs at boot. It does not rewrite the table and takes no exclusive lock. Only newly written pages carry the reserve, so the effect arrives gradually as the 14-day retention turns the table over.
- It does not reclaim the existing index bloat. That needs a
REINDEX CONCURRENTLY, which cannot run inside a migration's transaction and stays a separate operator step. - The cost is table size, about 20 % more for the same rows. Reversible with
ALTER TABLE client_statistics_entity RESET (fillfactor);— neither direction touches data. - If 2.3.3 was never deployed, its upgrade notes still apply on top of these. Migrations 0013 and 0014 then run at the same boot, and 0013 is not backward-compatible with an older
coreimage.
v2.3.3
Live session state moved out of Postgres
- The four per-session live fields (
hashRate,currentDifficulty,channelCount,bestDifficulty) were rewritten ~3×/minute per active session — roughly 2 000 row writes a minute at current pool size. They now live in per-sessionclient:live:*Redis hashes with a TTL on the dead-session-sweep clock, andclient_entitykeeps only the birth row. - The two bulk writers behind that load are gone, so
CLIENT_ENTITY_BULK_WRITE_LOCKno longer serialises two long statements over the same ~700 rows. It still guards row births and stays for the next multi-row writer. - Scope, measured rather than assumed: of 42 slow-statement burst minutes on the previous release, 23 contain only statistics inserts and no
client_entitywrite at all. This removes one of two independent causes, not both.
The public high-score list survives a miner's own reset
/api/info→highScoresread the same column/bestdiff_resetzeroes, so a miner clearing their own best also erased their leaderboard entry — permanently, because the flush'sGREATESTonly ever re-offers the current window's maximum.- The value is now split.
bestDifficultystays the miner's own, resettable number;allTimeBestDifficultyis the pool's record and is never lowered by any reset or delete path. Both are folded by the same upsert, so there is no second writer to drift. - A reset now also clears the per-session bests in Redis. It never did, so a miner who reset kept seeing the old value on every worker row until the next share.
delete-allempties the address-settings row instead of deleting it, so the record survives. What stays behind carries no address — only a difficulty, a firmware string and a timestamp.
Fixes
- The
/bestdiff_resetbot command and the reset endpoint left different state behind: the endpoint deleted the notification baseline row, the bot command did not. Both now go through one function.
Upgrade notes
- Migrations 0013 and 0014 run at boot. 0013 drops four
client_entitycolumns, and the previous image namescurrentDifficultyin its row-birth INSERT — an oldcorecontainer can no longer create a session once the migration has run. Deploy everything at once (docker compose --profile mainnet up -dwithout service names), not service by service. - Rolling back is not "put the old image back": the dropped columns have to be added again first, or the old code writes into columns that no longer exist.
- One large
crons.kill_dead_clients: reconciled sessions swept=Nabout six to seven minutes after the deploy is expected — rows that existed before the deploy never received aclient:live:*key. - Migration 0014 seeds
allTimeBestDifficultyfrom whateverbestDifficultyholds at that moment. An address whose best was reset shortly before the deploy carries the reset value into the leaderboard, not its historic record.
v2.3.2
Job Declaration Protocol
- An allocate token now authorises exactly one declaration, and is spent before the pool asks its node to validate the job. A second declaration on the same token is answered
invalid-mining-job-token. - A refused declaration spends its token just like an accepted one. This is a deliberate change: a client that re-declares after
stale-chain-tipbuilds against a fresh template and takes the next token from its queue, so it never hands the old one back. - An allocate the pool cannot answer now says so and closes the connection, instead of leaving the client waiting for a response
AllocateMiningJobTokendefines no error frame for. - A payout list that is absent — the pool serving this miner no job at all — is reported as absent, not as a payout split needing more than one output. The old wording sent the operator looking for a split that does not exist instead of at the distribution build that failed.
- A declared job is validated once per frame instead of twice, and the per-frame declare context travels as a single type.
Version rolling
- An extended job allows version rolling unconditionally.
- SV1 rejects version bits outside the mask the miner negotiated (BIP-310).
Custom extranonce
- A Solo address can pin its own extranonce prefix per worker, authorised by a signed challenge and a stored bearer token.
- The reserved-prefix rule now lives on the table as a CHECK constraint rather than only in the API handler. The handler was its single enforcement point while the table itself is hand-writable.
Statistics
- Stale shares get their own column pair, and both new reject reasons reach the API — the per-worker breakdown adds up to the rejected total again.
One concept, one implementation
- The block-found and declaration paths travel in named types instead of runs of same-typed positional arguments. Exchanging two of them compiled and wrote a session id into the address column.
- The template revenue, the node validation, the declare-error construction, the wire-codec primitives and the pool's epoch-millisecond clock each have one implementation again.
- SV2 spec citations name their heading instead of a section number, so a renumbering upstream cannot quietly invalidate them.
Fixes
- A JDP-declared block lost its
blocks_entityrow. The session id written there did not fit the column, and the insert is best-effort, so the row was dropped without a trace. Payout accounting was never affected.
Removed
- Two empty skeleton modules in the SV2 crate that the crate had grown away from — errors and configuration live next to what they describe.
Upgrade notes
- Migration 0012 adds the custom-extranonce tables and a CHECK constraint pinning a customer-set prefix above the extranonce allocator's own worker partitions (
prefix >= 33554432, i.e.0x02000000). ⚠️ The migration fails if an existing row violates that constraint, and the failure is deliberate: such a row is exactly the collision the constraint exists to prevent, and wants looking at rather than migrating around. Check before deploying:An empty result means the migration applies cleanly.SELECT address, worker, to_hex(prefix) FROM pplns_custom_extranonce WHERE prefix < 33554432;
- No configuration keys were removed in this release.
v2.3.1
Job Declaration Protocol
Base-protocol clients are served. A job-declaring client that does not
speak the payout extension now gets custom jobs under §6.4.3: the coinbase is
held to the pool's designated output, the job to its own token's miner address
and to the pool's chain tip.
A custom job is bound to what authorised it. The token and the pool's tip
both have to match, and a job on a channel the pool has served no extended job
yet is refused retryably — nothing else could pin its block-candidate
threshold, and handing that to the client turns every ordinary share into a
"block found".
A payout distribution is published only once the mode is known. It used to
fall back to a guess, which meant whoever declared before their miner connected
got a plan built for the wrong mode. Each published entry now carries the
accounting it was built for, so a plan cannot be mined on a stream it does not
belong to.
A session already being served re-asks its mode. A group join takes effect
mid-connection without a reconnect, and the client is handed a new plan instead
of being left on the one built for the mode it no longer has.
Notifications
ntfy inbound topics are split across several URLs. A single subscription
URL stopped working past roughly a dozen addresses, which silently killed the
user→pool direction. Outgoing push was never affected.
Development
The bitcoin-core regtest suite is one driver per concept rather than one per
payout mode, and runs on macOS.
Upgrading
No schema migration and no configuration change in this release.