Pool docs for review B F12/F13/INT-04/INT-15: the README's flags and the ledger and intent paragraphs, spec 09 section 9.3 (the frame bound while reading; binding v2, the exact bytes; welcome, authorize and authorized fields), docs/plans/pool.md section 11 (what was, what is, the regressions, the consequences per tier)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
30632328ac
commit
eef1d425e8
3 changed files with 41 additions and 6 deletions
|
|
@ -557,3 +557,19 @@ this process's run, and say which is which.
|
|||
| A pool user without a node | pool-0 as before (hashes, does not vote) | cannot join the open pool: the daemon builds templates from a node; a user without one takes pool-0 |
|
||||
| The pool operator (pool-0) | TLS on the member port, `/metrics`, `/health`, the alert webhook, the persisted ledger | none exists |
|
||||
| The network | pool concentration is not vote concentration, as before; an open pool's blocks carry as many keys as it has members and pay as many addresses as the window holds, so the coinbase grows by 48 bytes per payee (at most 256) | the executor's split is the one new consensus-visible rule, behind a switch that is never on the devnet |
|
||||
|
||||
## 11. 8 October 2026: review B F12, F13, INT-04 and INT-15 closed by construction (branch pool-review-b, landed from pool-review-b-land)
|
||||
|
||||
The second external review (docs/analysis/review-2026-10-08-b) and the full-system review's I08 read the pool crate statically. What was true on 8 October morning and what the crate does now:
|
||||
|
||||
| Finding | Was | Now | The regression that pins it (pool crate, release suite on build-6) |
|
||||
|---|---|---|---|
|
||||
| F12 payouts: broadcast before a durable debit | `send`, then the in-memory debit, then a periodic snapshot; a crash in between paid again from the stale balance with the next nonce; a receipt was `confirmed` the moment it appeared | `state::PaymentIntent`: the signed transaction and its hash on disk with the balance reserved in one fsync'd write BEFORE the broadcast; states prepared, broadcast, mined, finalised, failed, with reorged and replaced as events; every round and every restart reconciles by the intent's hashes (the same bytes again, a fee replacement under the same nonce after `replace_after_s`, a spent nonce re-signed under the same intent); finalised only when `igneum_getTransactionStatus` says locked and `igneum_getFinalityCheckpoints` says finality active with `latestLockKind` final (the node lane's F04 field; recovery or a pause keeps the obligation pending); a receipt that goes away is a reorg back to broadcast | `payout::intent_tests`: a_restart_from_the_pre_broadcast_snapshot_never_pays_twice, a_lost_send_response_is_reconciled_by_the_intent_and_never_paid_again, a_stuck_transaction_is_replaced_under_the_same_intent_not_paid_again, a_reorged_receipt_and_a_finality_pause_leave_the_obligation_pending, a_crash_on_either_side_of_the_broadcast_pays_once, a_failed_transaction_re_credits_once (a mock eth_ and igneum_ JSON-RPC on 127.0.0.1, `mock_rpc.rs`: the real client, signing and 2718 encoding; nothing leaves the process) |
|
||||
| F13 admission: `MAX_LINE` checked after the line, an unbounded writer queue, the nonce in `seen` before its check, a repeated `authorize` adding members and the disconnect removing the last | as the review read it | `frame::read_frame` refuses a frame the moment it crosses the bound (memory: the bound plus one 64 KiB buffer; time: the read budget); `admission::Outgoing` is bounded (`--out-queue`) and a full queue disconnects the slow reader; `admission::Members` is keyed by session (a re-authorise replaces atomically, the disconnect removes that session's member only, `--max-members`); a nonce is in flight while checked and remembered only when accepted, `seen` bounded per job; the vote key and the claimed hash against the share target are prefilters before the verifier; a share budget per member (`--shares-per-s`, `--share-burst`), an in-flight bound (`--in-flight-checks`), connection budgets (`--max-connections`, `--max-connections-per-ip`), an authorise deadline (`--authorise-within-s`). The member's vote key stays committed in the template header (node.rs `issue_job`), unchanged | `frame::tests::an_oversized_unterminated_frame_is_refused_within_the_memory_and_time_budget`, `admission::tests::repeated_authorisation_on_one_session_leaves_one_member_and_the_disconnect_removes_it`, `the_member_bound_holds_and_a_replacement_does_not_count_twice`, `a_slow_reader_cannot_grow_the_outgoing_queue`, `the_share_budget_is_a_bucket`, `connections_are_bounded_in_total_and_per_address`, `pool::share_tests::an_invalid_share_flood_leaves_no_nonce_state`, `a_share_flood_over_the_budget_is_refused_before_the_check` |
|
||||
| INT-04 the ledger: an unreadable `state.json` started the pool empty | "unreadable; starting empty" | every save keeps the previous snapshot and a SHA-256 sidecar; a present ledger that cannot be read or fails its digest restores the previous snapshot or refuses to start (exit 4, the restore line; `--start-empty` only knowingly); a save that fails broadcasts nothing | `state::ledger_tests::an_unreadable_ledger_is_refused_or_restored_never_emptied`, `sent_payments_of_the_earlier_loop_become_intents`, `payout::intent_tests::a_ledger_that_cannot_be_written_broadcasts_nothing` |
|
||||
| INT-15 (I08): a public proof of possession was the authorisation over plain TCP; over TLS the exporter bound the session but not the payout | as read | the challenge-bound binding v2 (spec 9.3): a signature over this session's challenge, the exporter, the payout and the pool address; replay into another session, payout, pool, chain or under another key is refused (`server::authorise_check`); the public networks take v2 only, devnet admits v1 and the unbound authorise until the 2.0.2 miner carries v2 (the node lane, by 23:30 on 8 October) | `server::authorise_tests::a_replayed_proof_of_possession_is_not_an_authorisation`, `the_binding_policy_follows_the_network` |
|
||||
| F03 seam: `EpochSeeds` without `shadow_reps`, `KeyReveal` without `sig_scheme` on master's copy | master's pool predated the pool-2.0 line | the pool-2.0 tree landed on master; `cargo check` green against the 2.0.1 manifest's node sha 7cfa422a on build-6, the suite green against successor-2.0.1 515ba674 | the rule 24 crate gate at every landing |
|
||||
|
||||
Known-failed first: commit 75383929 on pool-review-b carried the tests over the 8 October semantics, and the suite read seven red on build-6 (the three payout tests paid 200 of 100 due or never settled, the frame reader blocked on a newline that never came, the registry kept two members, the queue took 10,000 lines, the nonce set held 100,000 refused nonces); 065148a2 and 4a8dcbc0 turned them green, 51 of 51.
|
||||
|
||||
Consequences per tier. A pool member on any card: a payout cannot be paid twice or lost across a pool crash, a lost node answer, a reorg or a stuck transaction; a payment shows `mined` until the chain's own finality covers it, so the row a member reads as final is final. A pool operator: the ledger cannot start empty over a corrupt file, the previous snapshot and its digest are always there to restore from, and one bad member cannot hold the daemon's memory or its verifier (every bound has a flag and a default). The network: a captured `authorize` cannot be replayed to redirect a member's pay or to vote under its key from another session, and the vote key stays the member's in every template (the pin "vote keys stay with the miner", docs/plans/igneum-2.0.md).
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ Why. The pool protocol crosses operators and languages: pools, mining operating
|
|||
| Rule | Value | Label |
|
||||
|---|---|---|
|
||||
| Transport | TLS 1.3, server-authenticated; the pool's certificate chain or a pinned public key the member obtained with the pool's address | Designed |
|
||||
| Framing | One JSON object per line, `\n` terminated, no object over 4 MiB (a template with its transactions) | Designed |
|
||||
| Framing | One JSON object per line, `\n` terminated, no object over 4 MiB (a template with its transactions). The bound is enforced while the bytes arrive (review B F13, 8 October 2026): a frame is refused with `bye` the moment it crosses 4 MiB, never collected first; the pool also bounds its outgoing queue per connection (a reader that does not drain is disconnected), its connections in total and per address, its members, each member's shares a second and checks in flight, and the time from connect to `authorize` | Implemented (8 October 2026, `pool/src/frame.rs`, `admission.rs`) |
|
||||
| Field order | Irrelevant; unknown fields MUST be ignored | Designed |
|
||||
| Integers | JSON numbers up to 2^53; anything that can exceed it (nonces, targets, hashes, weights in base units) is a hex string | Designed |
|
||||
| Identifiers | `id` on every request, echoed on the reply; notifications carry no `id` | Designed |
|
||||
| Port | 4461 (the mainnet chain id, section 7.4), 4462 testnet, 4463 devnet; a pool MAY use another and say so in its address | Designed |
|
||||
| Member authentication | The `authorize` message carries the member's BLS public key and a proof of possession (the `KeyReveal` of section 3.10, 144 bytes), plus `binding`: a signature over the session's TLS exporter value under the same key, so the key proves it is live and the connection is bound to it | Implemented (7 October 2026, O-9.7 closed below) |
|
||||
| The binding's bytes (O-9.7) | Exporter: TLS 1.3 `export_keying_material` with label `EXPORTER-igneum-pool-binding`, context = the chain id as 8 little-endian bytes, 32 bytes out, on both sides. Message: `igneum-pool-binding-v1/` \|\| chain id (8 bytes LE) \|\| exporter. Signature: BLS12-381 G2 under the tag `IGNEUM_POOL_BINDING_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_` (never the vote or the PoP tag; a binding is not a vote). The pool refuses an `authorize` whose binding does not verify for THIS connection's exporter with `bye`; a replay on another connection carries another exporter and fails. Over plain TCP the field is empty and ignored (devnet only; testnet and mainnet pools refuse plain without `--allow-plain`) | Implemented (`consensus/core/src/finality.rs` binding_message, sign_binding, verify_binding; pool `tls.rs`; miner `pool.rs`) |
|
||||
| The binding's bytes (O-9.7) | Exporter: TLS 1.3 `export_keying_material` with label `EXPORTER-igneum-pool-binding`, context = the chain id as 8 little-endian bytes, 32 bytes out, on both sides. Message: `igneum-pool-binding-v1/` \|\| chain id (8 bytes LE) \|\| exporter. Signature: BLS12-381 G2 under the tag `IGNEUM_POOL_BINDING_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_` (never the vote or the PoP tag; a binding is not a vote). The pool refuses an `authorize` whose binding does not verify for THIS connection's exporter with `bye`; a replay on another connection carries another exporter and fails. Over plain TCP the field is empty and ignored (devnet only; testnet and mainnet pools refuse plain without `--allow-plain`) **Binding v2 (review B INT-15, 8 October 2026):** `welcome` carries `challenge` (32 bytes hex, fresh per connection) and `binding` (`v2`, `v1` or `none`, the pool's policy); the member signs, with `sign_binding` and the same tag, `session_binding_v2` = SHA-256 of `igneum-pool-session-v2/` \|\| chain id (8 bytes LE) \|\| challenge (32) \|\| the TLS exporter (32; all zero over plain TCP) \|\| the payout address (20) \|\| the pool's coinbase address (20, `welcome.pool_address`), so a public proof of possession replayed into another session, another payout, another pool or another chain is refused; the challenge is spent by the authorise it admits and `authorized` carries the next. Testnet and mainnet pools admit v2 only; a devnet pool admits the exporter-only v1 binding over TLS and, over plain TCP, an unbound authorise (both logged) unless `--require-binding-v2` | Implemented (`consensus/core/src/finality.rs binding_message, sign_binding, verify_binding; pool `tls.rs`; miner `pool.rs`) |
|
||||
| Server identity | A chain from a root the member's system trusts (`--pool-tls`), or a pinned certificate (`--pool-pin <hex>`: BLAKE2b of `igneum-pool-cert-pin-v1` \|\| the DER certificate, the pin a self-signed pool prints at start and shows on its page) | Implemented |
|
||||
|
||||
## 9.4 Templates
|
||||
|
|
@ -89,9 +89,9 @@ Direction P = pool, M = member. Every message is one object with `"t"` naming th
|
|||
| Type | Direction | Fields | Meaning |
|
||||
|---|---|---|---|
|
||||
| `hello` | M to P | `versions` (list), `chain_id`, `client`, `modes` supported | Opens the session |
|
||||
| `welcome` | P to M | `version`, `chain_id`, `pool_address`, `modes`, `vote_mode` (`member` or `pool`), `declaration` {`tag`, `max_extra_bytes`, `max_declarations_per_s`, `declare_timeout_ms`}, `share_scheme` (`pplns`, `pps` or `pplns-sidechain` with its parameters, and `software_dev_fee_percent`: the software fee a member pays in this mode, published beside the pool fee), `min_shift`, `max_shift`, `pool_mode` (`operator` or `open`, 9.12), `tls` | The pool's terms; the member shows `vote_mode` and `share_scheme` to its operator |
|
||||
| `authorize` | M to P | `pubkey` (48 bytes hex), `pop` (96 bytes hex), `binding` (signature over the TLS exporter), `label` | Names the member by its vote key |
|
||||
| `authorized` | P to M | `member_id`, `revealed` (true when the key has been revealed on chain, per the pool's node) | |
|
||||
| `welcome` | P to M | `version`, `chain_id`, `pool_address`, `modes`, `vote_mode` (`member` or `pool`), `declaration` {`tag`, `max_extra_bytes`, `max_declarations_per_s`, `declare_timeout_ms`}, `challenge` (32 bytes hex, this session's; 9.3 binding v2), `binding` (`v2`, `v1` or `none`: what the pool admits), `share_scheme` (`pplns`, `pps` or `pplns-sidechain` with its parameters, and `software_dev_fee_percent`: the software fee a member pays in this mode, published beside the pool fee), `min_shift`, `max_shift`, `pool_mode` (`operator` or `open`, 9.12), `tls` | The pool's terms; the member shows `vote_mode` and `share_scheme` to its operator |
|
||||
| `authorize` | M to P | `pubkey` (48 bytes hex), `pop` (96 bytes hex), `binding` (96 bytes hex: binding v2 over this session's challenge, exporter, payout and pool, 9.3; the exporter-only v1 on devnet), `label`, `payout` (the v0 EVM payout address) | Names the member by its vote key; a public PoP alone is not an authorisation (review B INT-15) |
|
||||
| `authorized` | P to M | `member_id`, `revealed` (true when the key has been revealed on chain, per the pool's node), `key_hash`, `challenge` (the next 32-byte challenge of this session, for a re-authorise) | |
|
||||
| `seeds` | P to M | `epoch_seed`, `day_seed`, `seed_source`, `vdf_proof` (516 bytes hex, section 4.4, when the member asks), `next_epoch_seed`, `next_day_seed`, `next_at_daa`, `program_class`, `next_program_class`, `era_seed`, `shadow_reps`, `next_shadow_reps` (the program class, era seed and latency-ladder rung of the current and the next epoch: a member refuses a job whose program it cannot name) | Current and next seed pair so the member can `prepare` its workers ahead (the worker protocol's `prepare`) |
|
||||
| `set_target` | P to M | `shift` s (9.8) | Vardiff |
|
||||
| `template` | P to M | `template_id`, `mode` (A or B), `header` (every field but nonce, hex), `coinbase`, `transactions` (A) or `merkle_path` plus `tx_count` (B), `votes_carried` (indices of the member's votes in this template) | A new template; supersedes the previous one for new jobs |
|
||||
|
|
|
|||
|
|
@ -60,6 +60,13 @@ igneum-pool --node grpc://127.0.0.1:26610 --evm-rpc http://127.0.0.1:26790 \
|
|||
| `--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) |
|
||||
| `--max-members`, `--max-connections`, `--max-connections-per-ip` | `2048`, `4096`, `64` | review B F13 (8 October 2026): the admission bounds; a connection past them is closed at accept with the reason in the log |
|
||||
| `--out-queue` | `256` lines | lines queued to one member before it is a slow reader and is disconnected |
|
||||
| `--shares-per-s`, `--share-burst` | `50`, `200` | a member's share budget (a bucket); a share over it is refused with code `budget` before any check |
|
||||
| `--in-flight-checks` | `8` | share checks one member may have on the verifier at once; over it, code `busy` |
|
||||
| `--authorise-within-s` | `30` | a connection that has not authorised by then is closed |
|
||||
| `--require-binding-v2` | off on devnet, always on testnet and mainnet | review B INT-15: only the challenge-bound `authorize` binding admits (spec 9.3); devnet otherwise admits the exporter-only binding and, over plain TCP, the unbound authorise of the 2.0.1 miner, each logged |
|
||||
| `--start-empty` | off | review B INT-04: start a NEW ledger over a `state.json` that cannot be read or fails its digest (the unreadable file is kept as `state.json.unreadable`); without it the pool refuses to start, exit code 4, with the restore line |
|
||||
| `--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 |
|
||||
|
||||
|
|
@ -115,7 +122,19 @@ a fast-time network; pool.md section 10.5 has the numbers).
|
|||
- `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.
|
||||
back it up with the key. Every save is durable (fsync, a rename) and keeps the previous snapshot as
|
||||
`state.json.prev` with a SHA-256 sidecar for each (`state.json.sha256`, `state.json.prev.sha256`; check a backup
|
||||
with `sha256sum -c`). A present ledger that cannot be read or fails its digest is NEVER an empty ledger (review B
|
||||
INT-04): the previous snapshot is restored when its digest holds, else the pool refuses to start (exit 4) until
|
||||
the operator restores a verified backup or passes `--start-empty` knowingly.
|
||||
- **Payments are intents** (review B F12, 8 October 2026). A payout round signs each transfer, writes the intent
|
||||
(the exact signed transaction and its hash) and the reserved balance to `state.json` durably, and only then
|
||||
broadcasts. The intent moves through `prepared`, `broadcast`, `mined`, `finalised` (the chain's declared finality
|
||||
covers its block: a lock of the final kind while finality is active; a recovery lock or a pause keeps it pending)
|
||||
or `failed` (reverted, or its nonce spent by another transaction: the balance comes back once). A crash on either
|
||||
side of the broadcast, a lost RPC answer, a restart from an older snapshot, a reorg or a stuck transaction are all
|
||||
reconciled by the intent's hashes: the same bytes sent again, or a fee replacement under the same nonce; never a
|
||||
second payment. The API's payment rows carry the intent's state in `status`.
|
||||
|
||||
## Deploy on one Hetzner box (the seed-node pattern)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue