Skip to content

Releases: warioishere/blitzpool-server-rust

v2.4.0

Choose a tag to compare

@warioishere warioishere released this 04 Oct 06:44

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,api and payout,stats,notify) with Postgres and Valkey from one docker-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 .env file is optional.
  • DEPLOYMENT.md walks 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.toml is now a complete, working Solo pool; blitzpool.full.example.toml documents 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 (or docker compose run --rm --no-deps front --sv2-keygen) prints a new [sv2] authority_privkey_hex together 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 own fee_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/fees no longer fails without Group-Solo. groupFeePercent is null while Group-Solo is off, and the new blockpartyFeePercent reports 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_hex claimed 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_address at [blockparty], or unknown field group_fees. Nothing changes silently. Convert the config before deploying the new version:

    Before After
    [group_fees] address [group_solo] fee_address and [blockparty] fee_address
    [group_fees] percent [group_solo] fee_percent and [blockparty] fee_percent
    [group_fees] coinbase_weight_budget [group_solo] coinbase_weight_budget
    Group-Solo / Blockparty fee taken from [pplns] when [group_fees] was absent set fee_address and fee_percent in [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_percent
    Group-Solo always on add [group_solo] to keep it running

    Example: a config with [group_fees] address = "bc1q…", percent = 1.5, coinbase_weight_budget = 25000 and [pplns] min_payout_sats = 5000 becomes

    [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 (or docker compose run --rm --no-deps <service> --config /app/blitzpool.toml --check-config).

  • No new migrations.

  • UI consumers of /api/pplns/fees: show blockpartyFeePercent for Blockparty instead of groupFeePercent, and treat null as "mode off".

v2.3.9

Choose a tag to compare

@warioishere warioishere released this 02 Oct 20:59

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 with not-editable, as it already was for active parties. 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-size reject 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/info reports the front's start time as the pool uptime. 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 ring directly; jsonwebtoken is 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 ring TLS 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 audit on 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_secs and group_by_address_secs from [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:*:total are no longer read or written and can be deleted.

v2.3.8

Choose a tag to compare

@warioishere warioishere released this 01 Oct 18:22

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.Success is refused with missing-txs instead 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.Success with the wrong number of transactions is answered missing-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-scores response 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 SetupConnection with an unsupported version range is now answered protocol-version-mismatch (previously unsupported-version), the same code the mining port uses.

v2.3.7

Choose a tag to compare

@warioishere warioishere released this 30 Sep 06:28

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/:id and member-view) no longer exposes member addresses. It sends memberId, addressLabel and isSelf like the Group-Solo roster; the full address only with the admin token. adminAddress stays 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 Stale instead of JobNotFound. Both protocol adapters folded them into job-not-found before the stats saw them, so the stale columns and the Stale key 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_clients brought 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_baseline creates the base tables on an empty database and changes nothing on an existing one; db/schema.sql is 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/:address answers yes or no without building the group detail, and GET .../: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) and 0017 (adds maxDifficulty to pool_share_statistics_entity and client_statistics_entity). 0000_baseline is 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

Choose a tag to compare

@warioishere warioishere released this 24 Sep 18:06

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 bestDiffNotificationsEnabled still decides.

API

  • nextResetAt comes from the reset cron's own schedule computation.
  • /api/pplns/fees: maxMinerOutputs is the number of miners the coinbase cut actually publishes (it overstated that by two to three). With [pplns.coinbase_autoscale] enabled, coinbaseWeightBudget reports 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.1 is stored as cgminer/sv2), and the downstream report uses it too.

Stratum

  • [stratum] job_retention_ms now applies to SV2 as well; SV2 used a fixed ten minutes. The key is optional with a default of 600000.
  • 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/Santiago on 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 FOUND log line printed the template id under the name height.

Removed

  • The per-session share warmup (warmup_shares). It was configured and shown in the UI but never enforced; the PPLNS min_difficulty is the gate. /api/pplns/fees no longer returns warmupShares.
  • The external share submission endpoints POST /api/share and GET /api/share/top-difficulties. Nothing ever submitted to them; the external_shares_entity table stays.
  • Prometheus metrics that were never emitted. /metrics serves the stream-consumer lag, the parked-block depths, vardiff adjustments and accepted_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
  • A 2.3.5 binary does not start with the cleaned config: driver and warmup_shares were 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-ui change for the PPLNS port-gate callout; without it the callout disappears because warmupShares is gone. That UI change also works against 2.3.5.
  • Local test setup: the test Redis needs --databases 544.

v2.3.5

Choose a tag to compare

@warioishere warioishere released this 10 Sep 12:13

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 than abandoned_balance_days are 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/:address and 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_days now 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_days after 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 payout before core. Until core runs this release no front publishes sessions and the sweep behaves as before. The core swap reconnects every miner, which also recreates the rows of sessions retired earlier.
  • No migration, no config key added or removed.

v2.3.4

Choose a tag to compare

@warioishere warioishere released this 06 Sep 10:26

API responses are compressed on the wire

  • Every response now goes out gzip/br/deflate-encoded for clients that ask for it. The periodic /stats chart 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=14d joins 1d, 3d, 7d and 1m at the same native 10-minute resolution — 2016 points where a 30-day request returns 4320.
  • It exists because the /stats page 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: 1m is unchanged, and every endpoint that already took range accepts the new preset. /api/info/chart/mode/:mode keeps its own narrower set of 1d, 3d and 7d.

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_statistics and find_client_difficulty_statistics had no caller and are gone. They were the only readers of id on 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 core image.

v2.3.3

Choose a tag to compare

@warioishere warioishere released this 02 Sep 06:45

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-session client:live:* Redis hashes with a TTL on the dead-session-sweep clock, and client_entity keeps only the birth row.
  • The two bulk writers behind that load are gone, so CLIENT_ENTITY_BULK_WRITE_LOCK no 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_entity write at all. This removes one of two independent causes, not both.

The public high-score list survives a miner's own reset

  • /api/info → highScores read the same column /bestdiff_reset zeroes, so a miner clearing their own best also erased their leaderboard entry — permanently, because the flush's GREATEST only ever re-offers the current window's maximum.
  • The value is now split. bestDifficulty stays the miner's own, resettable number; allTimeBestDifficulty is 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-all empties 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_reset bot 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_entity columns, and the previous image names currentDifficulty in its row-birth INSERT — an old core container can no longer create a session once the migration has run. Deploy everything at once (docker compose --profile mainnet up -d without 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=N about six to seven minutes after the deploy is expected — rows that existed before the deploy never received a client:live:* key.
  • Migration 0014 seeds allTimeBestDifficulty from whatever bestDifficulty holds 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

Choose a tag to compare

@warioishere warioishere released this 23 Aug 07:45

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-tip builds 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 AllocateMiningJobToken defines 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_entity row. 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:
    SELECT address, worker, to_hex(prefix) FROM pplns_custom_extranonce WHERE prefix < 33554432;
    An empty result means the migration applies cleanly.
  • No configuration keys were removed in this release.

v2.3.1

Choose a tag to compare

@warioishere warioishere released this 12 Aug 22:13

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.