236 lines
17 KiB
Markdown
236 lines
17 KiB
Markdown
# igneum-pool: the Igneum mining pool (pool-0, and the open pool)
|
|
|
|
A public mining pool for Igneum (`docs/spec/09-pool-protocol.md`, `docs/plans/pool.md`). It talks to one or more
|
|
`igneumd` nodes, builds a template per member with the member's own vote key in the header, hands out jobs over the
|
|
spec 09 protocol (newline JSON over TCP), sets a share target per member so each sends about one share every 10 s,
|
|
verifies every share on the CPU warp verifier the node uses, submits found blocks to every node, pays PPLNS from the
|
|
pool's coinbase address on the EVM side with a configurable fee (default 1%), and serves the stats API that
|
|
WhatToMine and MiningPoolStats read plus a page.
|
|
|
|
Built 5 October 2026 on branch `pool-v0`. Nothing is deployed. Measured on a private fast-time network
|
|
(`docs/plans/pool.md` section 5): three CPU miners of different identities, 0 rejected and 0 stale shares in the
|
|
second run, 289 blocks found and 0 orphaned at 49% of the network, PPLNS paid in dry run and then for real (24
|
|
transfers, 0 failed, the miners' EVM balances rose by what the ledger said), the API at 100 requests per second with
|
|
p99 under 3 ms, share checks 2.1 ms each under load (0.44 ms idle, the crate bench).
|
|
|
|
## Build
|
|
|
|
The crate reads the node fork by path from `../vendor/igneum-node` (the fork checkout, or a symlink to the fork
|
|
worktree that carries the pool-mode miner, branch `pool-v0` of the fork) and `../igneum-pow` from this repository.
|
|
|
|
```
|
|
cd pool
|
|
cargo build --release # target/release/igneum-pool, about 2 min warm on the Mac
|
|
cargo test --release # vardiff, PPLNS, share validation against the warp verifier, the API fixture
|
|
```
|
|
|
|
The miner with pool mode: `cargo build --release -p igneum-miner` in the fork worktree (branch `pool-v0`).
|
|
|
|
## Run
|
|
|
|
```
|
|
igneum-pool --node grpc://127.0.0.1:26610 --evm-rpc http://127.0.0.1:26790 \
|
|
--listen 0.0.0.0:4463 --http 127.0.0.1:4480 --data-dir /var/lib/igneum-pool \
|
|
--network devnet --fee-percent 1 --min-payout 1 --pplns-window 2 --payout-interval-s 60 \
|
|
--name "Igneum pool" --public-url pool.example.net:4463 [--dry-run]
|
|
```
|
|
|
|
| Flag | Default | Meaning |
|
|
|---|---|---|
|
|
| `--node` | `grpc://127.0.0.1:26610` | gRPC of a node; repeat for more. The first builds templates and confirms blocks; every one receives found blocks |
|
|
| `--evm-rpc` | `http://127.0.0.1:26790` (devnet), 26890 testnet, 8545 mainnet | the first node's eth_ JSON-RPC, for payouts |
|
|
| `--listen` | `0.0.0.0:4463` (devnet), 4462 testnet, 4461 mainnet | where members connect (spec 9.3 ports) |
|
|
| `--http` | `127.0.0.1:4480` | the stats API and the page; put a TLS terminator in front |
|
|
| `--data-dir` | `data` | `state.json` (the ledger snapshot) and the payout key |
|
|
| `--payout-key` | `<data-dir>/payout-key.json` | the pool's secp256k1 key, created on first start, mode 0600 |
|
|
| `--fee-percent` | `1` | the pool fee, taken from each confirmed block's reward before the PPLNS split |
|
|
| `--min-payout` | `1.0` IGN | a member's balance is paid once it reaches this |
|
|
| `--pplns-window` | `2.0` blocks | the window, in blocks of expected work (shares weigh `2^-s`) |
|
|
| `--payout-interval-s` | `60` | a payout round every so often, at most 16 transfers per round |
|
|
| `--dry-run` | off | compute and record payouts, send nothing |
|
|
| `--network` | `devnet` | devnet, testnet or mainnet: chain id (4463, 4462, 4461), default ports |
|
|
| `--share-interval-s` | `10` | vardiff target: one share per member per interval |
|
|
| `--min-shift`, `--max-shift` | `0`, `60` | bounds on the share shift; the saturation cap always applies |
|
|
| `--stale-grace-ms` | `2000` | a share on a superseded job inside the grace still pays |
|
|
| `--orphan-after-daa` | `120` | a pending block this far behind the virtual and not blue is an orphan |
|
|
| `--verify-threads` | `2` | share checks running at once on the CPU |
|
|
| `--template-parallel` | `2` | template fetches in flight against the node (one gRPC connection is one request stream; a burst of one fetch per member against a node that builds a template in over a second queued past the client's request timeout on pool-1, 7 October 2026) |
|
|
| `--tls-cert`, `--tls-key` | none | TLS 1.3 on the member port (spec 9.3) from a PEM chain and key |
|
|
| `--tls-self-signed` | off | TLS 1.3 from a self-signed pair made under the data dir on first start; the pool prints the certificate's pin, members pass it as `--pool-pin` |
|
|
| `--allow-plain` | off | plain TCP members on testnet or mainnet (the devnet allows plain by default; the public networks refuse to start in the clear without this) |
|
|
| `--alert-webhook` | none | Q72: an `http://` URL POSTed once per address that sent shares before and none for ten minutes |
|
|
| `--open` | off | the open pool: the share sidechain with no operator (below) |
|
|
| `--p2p-listen`, `--peer` | `0.0.0.0:<member port + 10>`, none | the open pool's share gossip: where to listen, who to connect to |
|
|
| `--chain-share-s`, `--window-shares`, `--open-dev-fee-percent`, `--open-chain`, `--chain-genesis-target64` | `10`, `2160`, `1`, `igneum-open-v1`, the network's genesis block target eight times easier | the share chain's constants; every member of one chain holds the same or its shares are refused |
|
|
|
|
At start the pool prints its payout address. That address is the pool's coinbase address: every template names it in
|
|
the `IGNA` field, so the execution layer credits it 80% of every blue block the pool finds. The 20% proving share goes
|
|
to the proving escrow by the chain's own rule (`consensus/core/src/igneum.rs`, `igneum/exec/src/executor.rs`); the
|
|
pool never touches it and cannot.
|
|
|
|
## TLS on the member port (spec 9.3, 7 October 2026)
|
|
|
|
`--tls-self-signed` makes `tls-cert.pem` and `tls-key.pem` under the data directory on first start and prints the
|
|
certificate's pin (`BLAKE2b("igneum-pool-cert-pin-v1" || DER)`, 64 hex); a member connects with
|
|
`igneum-miner ... --pool host:4463 --pool-pin <pin>`. A public pool with a certificate from a root the members'
|
|
systems trust runs `--tls-cert chain.pem --tls-key key.pem`, and members use `--pool-tls`. Over TLS every `authorize`
|
|
carries the binding: the member's BLS signature over this connection's TLS exporter (label
|
|
`EXPORTER-igneum-pool-binding`, context the chain id), so an `authorize` replayed on another connection is refused
|
|
(`src/tls.rs`, the test `self_signed_tls_pins_exports_and_binds_one_connection`). HiveOS: `pools://host:4463` in the
|
|
Flight Sheet for `--pool-tls`, `POOL_PIN=<pin>` in the extra config for a pinned pool. Testnet and mainnet pools refuse to
|
|
start in the clear unless `--allow-plain` says so.
|
|
|
|
## The open pool (`--open`, 7 October 2026; docs/plans/pool.md section 10; spec 09 section 9.12)
|
|
|
|
The same binary beside a member's own node, with no payout key: the member's miners connect to it on loopback (or a
|
|
rig's LAN), it builds their templates from the member's node with the member's key in the header and the member's own
|
|
address in the coinbase, stamps the share chain's parent (`IGNS`) and the window's split (`IGNP`) into every coinbase,
|
|
and gossips shares with the other members' daemons (`--peer`). A block found on the chain pays the window from its own
|
|
coinbase by the execution layer's split rule (`pool_split_activation_daa` in the node's override file; blocks below it
|
|
pay the finder alone). Nobody holds a balance; the only fee is the software dev fee as one entry of the split (the solo
|
|
miner's 1 percent).
|
|
|
|
```
|
|
igneum-pool --open --node grpc://127.0.0.1:26610 --listen 127.0.0.1:4463 --http 127.0.0.1:4480 --data-dir ~/.igneum-open \
|
|
--p2p-listen 0.0.0.0:4473 --peer other-member.example:4473 --network devnet
|
|
igneum-miner mine grpc://127.0.0.1:26610 1 100000000 me --pool 127.0.0.1:4463 --evm-address 0xYOURADDRESS --worker-name gpu0 --worker ./igneum-worker-cuda
|
|
```
|
|
|
|
Every share the daemon's own members find is one line of `<data-dir>/shares.log`, self-certifying; `igneum-pool
|
|
verify-share '<line>' [--block '<raw block json>']` re-hashes it and, against a block (its raw JSON from
|
|
`/api/open/share/<hash>`), says whether that block's split pays the share's address (`BLOCK PAYS`, exit 0; `BLOCK DROPS`,
|
|
exit 3). `/api/open` is the chain (height, tip, target, the window's split), `/api/open/shares?from=&limit=` the shares,
|
|
`/api/payments` the coinbase credits by block. The gate: `pool/tools/open-gate.mjs` (100 members, 10 daemons, 4 nodes on
|
|
a fast-time network; pool.md section 10.5 has the numbers).
|
|
|
|
## What the operator must secure
|
|
|
|
- **The payout key** (`payout-key.json`). It holds every reward the pool earns until the payout round sends it on.
|
|
Back it up off the box before the first block. Anyone with the file can empty the pool. Keep the data directory
|
|
at mode 0700, the file at 0600 (the pool sets 0600 on creation), and never put it in a repository or a log.
|
|
- The pool's HTTP port behind a TLS terminator (Caddy or nginx) with the page on one hostname and the member port
|
|
(4463) reachable; v0 speaks plain TCP to members, so the terminator does not cover shares yet (TLS for the member
|
|
port is the v0 gap named in `docs/plans/pool.md`).
|
|
- The node's RPC (`--rpclisten`) and EVM RPC (`--evm-rpclisten`) on loopback only. The pool talks to them locally.
|
|
- `state.json`: the ledger. It is rewritten every `--snapshot-interval-s` (15 s): balances, blocks, payments, the
|
|
hashrate samples and the check costs (Q70: a restart keeps the rate tiles and the luck; the loss window is one
|
|
interval), and the hourly history per address (7 days). A lost file loses unpaid balances since the last payout, so
|
|
back it up with the key.
|
|
|
|
## Deploy on one Hetzner box (the seed-node pattern)
|
|
|
|
The seed nodes (`infra/seed-nodes/README.md`) are Hetzner VMs with a fixed IPv4, `igneumd` as a systemd unit, RPC on
|
|
loopback, built by the cross-compile on the Mac (`infra/cross/build-linux.sh`). A pool box is the same plus this
|
|
binary and two more ports.
|
|
|
|
1. Create the VM as a seed would be created: `SEED_NAME=igneum-pool-1 SEED_TYPE=cpx31 ./create-seed.sh` from
|
|
`infra/seed-nodes` (the firewall script opens 22 and 26611; add 4463 and 443). The seeds run on cx23 (2 vCPU, 4 GB,
|
|
USD 6.49 a month net plus the IPv4, approximate from the seed plan); a pool wants a core for the node, a core for
|
|
share checks (one share per 10 s per member, 0.44 ms each on an M5 Max core, so a server core at roughly half that
|
|
rate covers thousands of members, approximate) and 8 GB for the node plus the pool's 256 MiB cache per day seed.
|
|
A cpx31 (4 vCPU, 8 GB) is about USD 14 a month net (approximate, price list not re-read tonight).
|
|
2. Provision the node exactly as `provision-seed.sh` does, with `--utxoindex` added and `--evm-rpclisten=127.0.0.1:26790`
|
|
(the pool needs the EVM RPC for payouts). The node must be the devnet-v4 line or later (it reports `pow_epoch` with
|
|
every template; the pool refuses to run against one that does not).
|
|
3. Cross-compile `igneum-pool` with the same `cargo-zigbuild` target as the node (`x86_64-unknown-linux-gnu.2.36`)
|
|
from `pool/`, copy it to `/opt/igneum/bin/igneum-pool`, and install the unit below. Re-stamp copied sources with
|
|
`touch` before any build on the far side (the stale-build rule in CLAUDE.md); nothing is built on the server.
|
|
4. `caddy` (or nginx) in front of `127.0.0.1:4480` for `https://pool.<domain>/`; the member port 4463 open in the
|
|
Hetzner firewall.
|
|
5. First start with `--dry-run` for a day, read `/api/blocks` and `/api/payments`, then restart without it.
|
|
|
|
```
|
|
# /etc/systemd/system/igneum-pool.service
|
|
[Unit]
|
|
Description=Igneum mining pool
|
|
After=network-online.target igneumd.service
|
|
Requires=igneumd.service
|
|
|
|
[Service]
|
|
User=igneum
|
|
EnvironmentFile=/etc/igneum/pool.env
|
|
ExecStart=/opt/igneum/bin/igneum-pool --node grpc://127.0.0.1:26610 --evm-rpc http://127.0.0.1:26790 \
|
|
--listen 0.0.0.0:4462 --http 127.0.0.1:4480 --data-dir /var/lib/igneum-pool --network testnet \
|
|
--fee-percent 1 --min-payout 1 --name "Igneum pool" --public-url pool.igneum.network:4462
|
|
Restart=always
|
|
RestartSec=5
|
|
UMask=0077
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
Costs, approximate: the VM (USD 6.49 to 14 a month net by type), the IPv4 (Hetzner's primary IPv4 price, a few
|
|
dollars), outbound traffic inside Hetzner's included allowance at a few hundred members (a template is a few KB per
|
|
member per second in mode A; 1,000 members at 10 KB/s each is 10 MB/s, which is where mode B or a template RPC that
|
|
takes a list of keys comes in, O-9.4). Measure before quoting.
|
|
|
|
## Connect a miner
|
|
|
|
```
|
|
igneum-miner mine grpc://127.0.0.1:26610 1 100000000 rig1 --pool pool.example.net:4463 \
|
|
--evm-address 0xYOURADDRESS --worker-name rig1-gpu0 --worker ./igneum-worker-cuda --worker-args "--device 0 --pack packs/devnet"
|
|
igneum-miner mine none 4 100000000 rig1 --pool pool.example.net:4463 --evm-address 0xYOURADDRESS --worker-name rig1-cpu
|
|
```
|
|
|
|
The first argument is the member's own node (its verifier: seed checks, voting with its own key, a second road for
|
|
found blocks) or `none` (hashes, does not vote). `--evm-address` is the payout address, `--worker-name` the label on the
|
|
page. HiveOS: Flight Sheet pool URL `pool://pool.example.net:4463` (`packaging/hive/README.md`). In pool mode the miner
|
|
software takes no dev fee; the pool's fee is in the welcome line the miner prints.
|
|
|
|
## The API
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `/api/stats` | `pool` (hashrate, miners, workers, shares, blocks 24 h and total, last block, fee, min payout, luck, effort, paid, share check cost) and `network` (chain id, difficulty, hashrate, DAA, reward) |
|
|
| `/api/blocks?limit=N` | blocks found: hash, DAA, time, finder, status (pending, confirmed, orphan), reward, effort, the PPLNS split |
|
|
| `/api/miners/<address>` | hashrate (10 min, 1 h, reported), shares, workers (with shift, proving flag, online), balance, paid, payments |
|
|
| `/api/payments?limit=N` | every payout with its transaction hash and receipt status |
|
|
| `/api/pool-stats` | the flat camelCase object pool dashboards poll (hashrate, miners, workers, blocks, lastBlock, fee, minPayout) |
|
|
| `/metrics` | Prometheus text (Q72) |
|
|
| `/health` | 200 when the node is synced and answered a template in the last 30 s, else 503 with the node's state (Q72) |
|
|
| `/api/open`, `/api/open/shares`, `/api/open/share/<hash>` | the open pool's share chain |
|
|
|
|
The page (Q69 to Q73): every number formatted as the site's are (en-GB separators, hash rate to one decimal on a kH to
|
|
PH ladder, IGN to 4 decimals on tiles and 6 in tables), luck in MiningPoolStats' convention (expected over actual),
|
|
the payments of a looked-up address under its workers with a 7-day hourly sparkline, the node's state in words, and the
|
|
network's finality state beside the sentence "payouts follow blue confirmation, not finality".
|
|
|
|
The field lists are constants in `src/api.rs`; `cargo test` checks the fixtures in `tests/fixtures/` against them.
|
|
Captured responses from the measured run are the fixtures.
|
|
|
|
## Files
|
|
|
|
| File | What |
|
|
|---|---|
|
|
| `src/main.rs` | start-up, tasks, the status line, shutdown snapshot |
|
|
| `src/config.rs` | flags and defaults |
|
|
| `src/protocol.rs` | the spec 09 messages, newline JSON |
|
|
| `src/server.rs` | member connections: hello, authorize (proof of possession checked), shares, solutions, stats |
|
|
| `src/node.rs` | templates per member, jobs, block submission, the blue-set walk that confirms or orphans, network numbers, vardiff ticks |
|
|
| `src/verify.rs` | the share check on the warp verifier, the spec's codes |
|
|
| `src/vardiff.rs` | the share shift per member |
|
|
| `src/pplns.rs` | the window, the split, the distribution with the fee |
|
|
| `src/payout.rs` | the payout key, EIP-1559 transfers through eth_ JSON-RPC, receipts, dry run |
|
|
| `src/state.rs` | the ledger and its snapshot |
|
|
| `src/api.rs` | the HTTP server, the routes, the field contracts, `/metrics`, `/health` |
|
|
| `src/tls.rs` | TLS 1.3 on the member port, the self-signed pair, the exporter of the binding |
|
|
| `src/sidechain.rs` | the open pool's share chain: shares, the target, the window's split, fork choice, `verify-share` |
|
|
| `src/open.rs` | the open pool's daemon side: stamping templates, accepting shares, the shares log, the API |
|
|
| `src/p2p.rs` | the share gossip between members |
|
|
| `tools/open-gate.mjs` | the open pool's gate on a fast-time network |
|
|
| `web/index.html` | the page |
|
|
| `tools/measure.mjs` | the private-network measurement |
|
|
|
|
## Building on igneum-build-1 (6 October 2026)
|
|
|
|
Every Linux build and suite runs on the box through `tools/build-remote.sh` from this directory (`IGNEUM_AGENT=pool`). The
|
|
crate reads the fork through the `vendor/igneum-node` symlink; the build library syncs the fork worktree it points at as a
|
|
whole repository, and the symlink is made once on the box by hand: `ln -s <fork worktree name> /srv/builds/<worktree>/vendor/igneum-node`,
|
|
with the line `vendor/igneum-node` in `/srv/builds/<worktree>/.igneum-scratch-spare` so the mirror's clean before every build
|
|
keeps it (7 October 2026: without the line the first box build of the pool crate removed it and cargo found no fork).
|
|
Artefacts land in `pool/target-remote/release/igneum-pool` (x86_64 Linux); the Mac builds no Linux binary.
|
|
|
|
Start-up contract (7 October 2026): the daemon exits 2 when `--listen` or `--http` cannot be bound, exits 3 when the node
|
|
answers no template with `pow_epoch` within `--node-wait-secs` (default 60), and prints `node <url> answers templates: ...`
|
|
before it accepts a member; a `STATUS` line every 30 s names the member count, jobs issued, shares and the node's state.
|