Skip to content

Add group-solo mining mode#119

Merged
warioishere merged 4 commits into
feature/pplns-pool-supportfrom
feature/group-solo-mining
Apr 22, 2026
Merged

Add group-solo mining mode#119
warioishere merged 4 commits into
feature/pplns-pool-supportfrom
feature/group-solo-mining

Conversation

@warioishere
Copy link
Copy Markdown
Owner

⚠️ Stacked on top of #118 (`feature/pplns-pool-support`). Merge that one first — GitHub will then auto-update this PR's base to `blitzpool-master`.

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

  1. Admin creates a group via `POST /api/pplns/groups`, gets a one-time admin token.
  2. Admin adds member BTC addresses via `POST /api/pplns/groups/:id/members` with the admin token. Members never need to authenticate — trust model is: admin adds addresses, miners just point their hardware at the pool as usual.
  3. Members mine with their own addresses — they're automatically routed into the group's Redis bucket. When the group finds a block, the coinbase contains one output per member (plus the pool fee output), sized proportionally to each member's shares in the current round.
  4. PROP semantics: Round resets on block found. Late-arriving shares (after the winning job's snapshot was built) are logged as audit rows but not credited — the coinbase is immutable and crediting would double-count. Sub-dust / weight-trimmed miners still accumulate to pending and get paid when they cross dust in a future round.

Components

Data model (new): `pplns_group`, `pplns_group_member`, `pplns_group_block_history`, `pplns_group_balance`.

Services:

  • `GroupService` — CRUD, admin-token auth (SHA-256 hashed, shown to creator exactly once on create), address→group cache for fast lookup in the stratum hot path, min-2-member activation, creator auto-transfer on leave.
  • `GroupSoloService` — PROP engine with snapshot-based bookkeeping (coinbase matches on-chain reality byte-for-byte), handles sub-dust vs late-arriver distinction to prevent double-counting.

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

  • `POST /api/pplns/groups` — create group (returns adminToken once)
  • `GET /api/pplns/groups` — list active groups
  • `GET /api/pplns/groups/:id` — details + per-member hashrate + totalHashrate
  • `GET /api/pplns/groups/:id/hashrate` — compact hashrate-only endpoint (for dashboard polling)
  • `GET /api/pplns/groups/:id/distribution` — current round shares per address
  • `GET /api/pplns/groups/:id/history?limit=N` — block-found audit trail
  • `POST /api/pplns/groups/:id/members` — add member (admin token)
  • `DELETE /api/pplns/groups/:id/members/:address` — kick (admin token)
  • `DELETE /api/pplns/groups/:id/members/:address/self` — non-creator self-leave (no auth)
  • `POST /api/pplns/groups/:id/transfer` — rotate creator role + admin token
  • `DELETE /api/pplns/groups/:id` — dissolve

Tests (25 new)

  • `group.service.spec.ts` (16) — CRUD, token verify/rotate, min-size activation, auto-transfer, cache invalidation, address-exclusivity
  • `group-solo.service.spec.ts` (9) — share routing, PROP distribution, round reset, sub-dust pending, multi-round pending accumulation, late-arriver-not-double-credited (locks in the PROP double-counting fix discovered during live testing with two Bitaxes)
  • `group-solo-regtest.spec.ts` (1) — 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 and that non-members are excluded from the coinbase

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

  • Full Jest suite green (504 tests, 58 suites)
  • Build clean, no TypeScript errors
  • Live-tested with 2 Bitaxes on SV1: 104 blocks, all accepted by bitcoind
  • Unit test locks in PROP late-arriver behavior to prevent regression

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.
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
@warioishere warioishere merged commit 60ad37c into feature/pplns-pool-support Apr 22, 2026
@warioishere warioishere mentioned this pull request Apr 22, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant