Add group-solo mining mode#119
Merged
warioishere merged 4 commits intoApr 22, 2026
Merged
Conversation
Lets friends mine together on the pool with their rewards split among
the group's members (PROP-style: window resets on each found block).
Group membership is looked up per-address on any stratum port, so
miners don't need to reconfigure — the admin just adds their BTC
address to the group.
## Membership & auth
- `GroupService` manages groups and members, with admin-token auth
(pool-generated at creation, SHA-256 hashed, shown to creator exactly
once). Members never need to sign anything; the creator acts on their
behalf via the token.
- One address = max one group (unique constraint).
- Creator can kick members / transfer role / dissolve; non-creators can
self-leave without auth.
- Groups activate at ≥ 2 members; below that they're rejected from
recording shares.
- Creator leaving auto-transfers the creator role to the oldest
remaining member (admin token is rotated); group dissolves if alone.
## Payout engine
- `GroupSoloService` maintains per-group Redis keys
(`groupsolo:{id}:shares|counter|total`). On share accept, the miner's
address is looked up and the share lands in the matching group's
bucket.
- `getPayoutDistribution` builds a fee + members distribution
(same dust/weight-budget/pending rules as the PPLNS engine) and saves
a snapshot. `onBlockFound` uses that snapshot for bookkeeping so
on-chain payouts and DB history match exactly, then clears the group's
Redis keys (PROP reset).
## Stratum integration
- Group membership is detected at authorize (SV1) / channel open (SV2)
and stored per-session as `groupSoloGroupId`. When set, group-solo
takes precedence over the port's `payoutMode` for coinbase build,
share record, and block-found paths.
- No new port required — any port the miner uses routes them into
group-solo if their address is in an active group.
## API (`/api/pplns/groups`)
- `POST /`, `GET /`, `GET /:id`, `GET /:id/distribution`,
`GET /:id/history` — public.
- `POST /:id/members`, `DELETE /:id/members/:address`,
`POST /:id/transfer`, `DELETE /:id` — require `X-Admin-Token`.
- `DELETE /:id/members/:address/self` — unauth, non-creators only.
## Tests
- `group.service.spec.ts` — 16 unit tests (CRUD, token verify/rotate,
activation threshold, auto-transfer, cache invalidation).
- `group-solo.service.spec.ts` — 7 unit tests (share routing, PROP
distribution, round reset, sub-dust pending).
- `group-solo-regtest.spec.ts` — end-to-end regtest integration:
records shares, builds a real coinbase from the service's
distribution, submits the block to Bitcoin Core, verifies round
reset. Self-bootstraps to height ≥ 17 if needed.
Covers the PROP + pending interaction: a sub-dust miner gets credited to pendingSats in round 1, then in round 2 their new share plus the accumulated pending crosses dust and they appear in the coinbase, with pending cleared and the amount moved to totalPaidSats.
Discovered by running two Bitaxes against a regtest group: when a
miner submits shares AFTER the winning job's snapshot was built but
BEFORE the block is found, onBlockFound was crediting them to pending
in addition to paying the snapshot distribution via the coinbase. Net
effect: more than 100% of block reward being distributed (snapshot
claims the full miner cut, late shares get extra pending on top).
Fix distinguishes two "not in coinbase" classes:
- sub-dust / weight-trimmed: was in the Redis window at
snapshot-build time → credit to pending so it accumulates for
future coinbase inclusion (same as PPLNS)
- late arriver: share landed in Redis after the snapshot was built
→ under PROP rules this work is lost for the current block
(coinbase is immutable; crediting would double-count)
The snapshot now records `consideredAddresses` at build time so
onBlockFound can tell the two cases apart. Also adds:
- explicit UUID assignment on group create (PG column has no
gen_random_uuid() default, TypeORM's @PrimaryGeneratedColumn('uuid')
wasn't generating one in practice — caused a not-null violation
when creating groups via the REST API)
New unit test: "late-arriving shares (post-snapshot) are logged but
NOT credited to pending — prevents double-counting" — locks in the
PROP behavior and asserts that total coinbase payout never exceeds
the block reward.
GET /api/pplns/groups/:id now includes:
- totalHashrate: sum over all members
- members[].hashrate: per-member live hashrate
Plus a new dedicated endpoint for lightweight polling:
GET /api/pplns/groups/:id/hashrate
→ { groupId, totalHashrate, members: [{ address, hashrate }] }
Uses ClientService.getByAddress (same source /api/client/:address
uses for totalHashrate), not a Redis time-series lookup — so group
hashrate matches what the user sees on their per-address dashboard.
This was referenced Apr 19, 2026
warioishere
added a commit
that referenced
this pull request
Apr 22, 2026
All of the following features were originally stacked as separate PRs on top of PPLNS: #119 feature/group-solo-mining group-solo engine + API #120 feature/mining-mode-endpoint GET /pplns/mode/:address #121 feature/block-template-mode-aware mode-aware block-template #122 feature/group-chart-endpoint chart + all security hardening + regtests Landing them as separate PRs would have exposed master to intermediate vulnerable states (selfLeave DoS, silent-add token leak, pending-out- of-coinbase math bug, etc.) until #122 closed the stack. All security and regtest work lived exclusively on #122, so merging the lower PRs first would ship a group-solo surface without its hardening. Rolling everything into #118 means the whole feature lands atomically on master with its full test + security story. PR #115 (JDP integration) stays separate, still upstream-blocked. # Conflicts: # src/controllers/pplns/pplns.controller.spec.ts # src/controllers/pplns/pplns.controller.ts
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A new mining mode where friends can mine together on the pool and split rewards among the group's members. PROP-style payout (window resets on each found block). Built on the existing PPLNS engine and coinbase-construction mechanics.
Address-driven, not port-driven. Any miner whose address is in an active group is automatically routed into group-solo payout on whatever port they connect to. No new stratum port, no miner config changes.
How it works
Components
Data model (new): `pplns_group`, `pplns_group_member`, `pplns_group_block_history`, `pplns_group_balance`.
Services:
Stratum integration — V1 + V2 clients detect group membership at authorize / channel-open and switch the session into group-solo payout. Group-solo takes precedence over the port's configured `payoutMode` (so group members always get group treatment).
REST API
Tests (25 new)
Live validation
Tested with two real Bitaxe devices against a regtest node over 104 blocks. All blocks accepted by Bitcoin Core 29.0. Found and fixed one real bug in the process (late-arriving share double-counting — now covered by a unit test).
Test plan