Spec 09 pool protocol, spec 10 light client, phone-app design

Pool protocol after Stratum V2 job declaration: member-checked or member-built templates, JSON over TLS, shares at target64 << s on the 32-lane unit, one vote key per operator held by the member, votes relayed and carried by the pool with a chain-only drop test. Light client: trust table, checkpoint-mode bytes per day, pinned seed list, the read-only node API, the homepage card's steps. Phone app: wallet, miner monitor, node card, store rules, build plan on journey.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-03 21:43:51 +00:00
parent 2d19f43c0a
commit 06b8aa85e5
4 changed files with 492 additions and 0 deletions

157
docs/design/phone-app.md Normal file
View file

@ -0,0 +1,157 @@
# Igneum phone app: the light-client wallet for iOS and Android
Design version 0.1, 3 October 2026. Status: Designed. Nothing is built. The app is the light client of `docs/spec/10-light-client.md` with a wallet, a miner monitor and the live network view on top. It mines nothing, holds no vote key, and asks for nothing from anyone but the user. The permanent line of spec 8.5 item 3 appears on every screen that mentions a seed, a key or support, verbatim:
**Nobody from Igneum will ever ask for your seed.**
## Decisions in one table
| Id | Decision | Section |
|---|---|---|
| A1 | No mining on the device, no remote-mining control that starts or stops rigs from the app in version 1; the app reads | 1 |
| A2 | The chain is verified by the section 10 engine in checkpoint mode; a balance is shown with its status (executed, proven, locked) and the engine's own status (voter set from nodes, or verified) | 2 |
| A3 | Keys live in the platform's hardware-backed store; the seed is a 24-word BIP-39 phrase shown once, confirmed by re-entry, never uploaded, never typed anywhere in the app except the restore screen; hardware wallets over Bluetooth in a later version | 3 |
| A4 | Miner monitoring is read-only over the pool protocol's `stats` message (spec 9.5) or the user's own node's RPC, authorised by a node card the desktop app shows as a QR code | 4, 5 |
| A5 | The live network view shows the same data as `site/live.html`, taken from the app's own light client where the engine can verify it and from the observer feed where it cannot, each labelled | 6 |
| A6 | One Rust engine (light client, wallet signing, pool-protocol client) shared by the phone app, the browser card and the desktop app, through a foreign-function layer | 7 |
| A7 | Store release only from the public testnet (journey phase 5), on testnet coins; mainnet in the same app at phase 6 | 9 |
## 1. What the stores allow, as far as this document knows
Store rules from memory, approximate, to be re-read at submission:
| Store | Rule, approximate | What it means for this app |
|---|---|---|
| Apple App Store Review Guidelines, 2.4.2 | Apps may not run unrelated background processes such as cryptocurrency mining and should not drain the battery or strain the device | No hashing on the phone, ever; the light client's BLS and proof checks are brief and foreground |
| Apple, 3.1.5 (b), cryptocurrencies | Wallet apps may store and transfer virtual currency when the developer is enrolled as an organisation; apps may not mine on the device; apps may facilitate mining performed off device; exchanges only from the exchange itself; no token offerings | The developer account is an organisation (section 10); the app is a wallet and a monitor; it sells nothing and lists no exchange |
| Google Play, financial services policy | Apps that mine cryptocurrency on devices are not allowed; apps that remotely manage mining are allowed | Same posture; remote management is still left out of version 1 (A1) so that the app's only write path is a signed transaction the user sends |
| Both stores | Financial apps need clear disclosure, a privacy policy, and no misleading claims | The verification status is shown as the engine computes it (spec 10.8 wording), never "verified" without a check having run |
The last row is the honest-sentence rule applied to a screen: the app shows "voter set from nodes" until phase two, exactly as the homepage card does.
## 2. Verify the chain
The engine runs spec 10 in checkpoint mode: trusted checkpoint and seed list compiled into the build, headers and certificates per checkpoint, the latest wrapped proof, `eth_getProof` for every balance. Per day about 3.4 MB at 1,000 voters (spec 10.5, Designed, approximate), on Wi-Fi or cellular at the user's choice, with "proof on demand" as the low-data setting (2.3 MB a day).
The Verify screen shows: the latest certified checkpoint and its age; the signed weight as a fraction of total; the latest proven segment and its distance behind the checkpoint; the proof system version; bytes fetched today; the voter-set status ("from 3 of 5 nodes" or, from phase two, "verified"); the nodes in use and their identity keys; and the last failure, if any, in the words of spec 10.8 item 7.
Full-header mode (spec 10.4 item 6, the lottery verified on the phone) is not in version 1; it depends on O-10.4.
## 3. The wallet
| Item | Decision | Label |
|---|---|---|
| Key storage | iOS Secure Enclave through the Keychain with device-only, biometric-gated access; Android Keystore with StrongBox when the device has it, else TEE-backed. The signing key for an EVM transaction is secp256k1, which the enclaves do not sign natively, so the private key is wrapped by an enclave key and unwrapped into memory only for the duration of one signature (approximate: the common pattern on both platforms) | Designed |
| Seed | BIP-39, 24 words, generated on device; shown once, confirmed by re-entering words in random positions (spec 8.5 item 1's rule for the desktop app, applied here); optional second confirmation after 24 hours | Designed |
| Derivation | BIP-44 path `m/44'/coin'/0'/0/n` with Igneum's SLIP-44 coin type; the coin type is unregistered (Open, P1 below) and the app uses Ethereum's 60 until it is, so a seed restored into any Ethereum wallet shows the same addresses | Designed, Open |
| Address | 20-byte EVM address, chain id 4461 / 4462 / 4463 (spec 7.4), EIP-155 signatures, transaction types 0, 1, 2 (design document, execution layer, 1.1) | Designed |
| Gas | One gas limit and one price, as the node quotes them with proving gas folded in (spec 7.1, "quoted gas price"); the app shows the quote and never lets a user set a limit below the estimate | Designed |
| Status line | Every transaction shows executed, proven, locked as `igneum_getTransactionStatus` reports them, with the app's own verification state beside it: "locked" is shown only when the engine has verified the certificate that covers the block | Designed |
| Backup | None by the project. The app offers no cloud backup of the seed and refuses the platform's automatic keychain sync for the wallet entries; the user writes the words down | Designed (spec 8.5 item 3) |
| Hardware wallet | Version 2: Ledger over Bluetooth with the Ethereum app's signing flow (approximate), the phone as the light client and the device as the signer; the desktop app's "hardware wallet as the destination for earnings" (spec 8.5 item 2) then has a phone counterpart | Designed, later |
| Restore | The only screen that accepts a seed. It shows the permanent line above the input, refuses paste from the clipboard by default (the user can enable it), and clears the clipboard after a paste | Designed |
| The line | "Nobody from Igneum will ever ask for your seed." on the seed screen, the restore screen, the settings screen, the support screen and the store listing | Decided (spec 8.5 item 3) |
What the wallet does not do: no in-app purchase of IGN, no swap, no fiat on-ramp, no price feed in version 1 (a price needs a listing, and listings are not sought before launch, ledger X8), no staking because there is no stake (spec 5.6), no vote key (spec 8.5 item 4: the vote key is a mining key and lives with the miner).
## 4. Watch your own miners
Read-only. The user adds a source and the app polls it:
| Source | Protocol | What the app reads | Trust |
|---|---|---|---|
| A pool the user mines on | The pool protocol over TLS (spec 9.3), a `hello`, `authorize` with a read-only token the pool issued to the user (not the vote key: the phone never holds it), then `stats` and `votes_carried` | Hashrate per worker, shares and their weight (spec 9.8), estimated and paid earnings in the pool's scheme, the pool's `vote_mode` and `share_scheme`, the carriage ratio of spec 9.7 item 6 for the user's key | The pool's word for pool-side numbers; the chain's word for blocks found under the user's key, which the engine can verify from headers it fetches |
| The user's own node | The node's RPC with the read-only token from the node card (section 5) | `getFinalityWeights` for the user's key: blocks in the window, weight, participation, stripped-until; the miner's status lines; the node card's health fields (state, blocks, peers, blocks per second) | The user's own machine |
| The chain alone | The light client | Blocks whose header names the user's key hash, from headers the engine fetches; coinbase payouts to the user's address from `eth_getProof` on the balance | Verified |
The Miners screen shows one card per rig as the Windows app's dashboard does (one card per GPU: hash rate in big digits, blocks found; `proto-cuda/windows-app/README.txt`), one card for the key (weight, participation, dust warning under 100 blocks in the window, spec 3.1 W3), and one line per pool with its mode and carriage ratio. A pool in custodial mode (spec 9.6 item 5) is shown with a warning, and a carriage ratio below the O-9.6 threshold with another.
The app changes nothing on the rig. Start, stop, pool switching and overclocks stay on the desktop app and the mining OS. This keeps the app's only side effect a signed transaction (A1) and keeps the store review simple.
## 5. The node card
The desktop app's dashboard already draws a NODE card (state, blocks, headers, blue score, peers, blocks per second, difficulty direction, uptime; `proto-cuda/windows-app/README.txt` item 2). This design gives the card a "Show on phone" action that renders a QR code, and the phone app a scanner that reads it. The desktop side is Designed and not in the launcher today.
Card payload, one JSON object in the QR, under 1 KB:
| Field | Meaning |
|---|---|
| `v` | Card format version, 1 |
| `chain_id` | 4461 / 4462 / 4463 |
| `name` | The machine's name as the launcher shows it |
| `rpc` | The node's RPC address for the phone: `wss://host:port` or `tcp://ip:port`; the launcher fills in the LAN address and the user may edit it to a public one |
| `node_key` | The node's identity key (spec 10.6 item 1), so the phone pins the node and ignores a certificate for the name |
| `token` | A read-only token the node mints for this card; it unlocks the miner's status lines and nothing else, and chain reads need no token (spec 10.7) |
| `vote_key_hash` | The operator's key hash, so the Miners screen knows which blocks are the user's |
| `pools` | Optional: the pools the rig mines on, as addresses, so the app can offer to add them |
| `expires` | DAA score after which the token is dead; the launcher re-issues on request |
The card carries no secret key, no seed and no payout key. Scanning it on a phone that belongs to someone else gives that phone read access to one node's miner statistics until `expires`, which is why the launcher shows the card only on a click and the node lets the user revoke a token from the dashboard.
## 6. The live network
The same panels as `site/live.html` (status, network, blocks per second over 60 s, blue score, difficulty, hash-rate estimate, miners active in 10 minutes, peers, the block DAG, miners in the last 10 minutes, events, blocks per minute over the last hour) and the finality panel (checkpoints and the newest lock), with one rule: a number the engine can verify comes from the engine, a number it cannot comes from the observer feed (`site/api/live.mjs`, `GET /api/live`), and each panel says which. The engine can verify: the latest certified checkpoint, its signed weight, the chain's blocks per second over the headers it holds, and any block it fetched. It cannot verify: the observer's hash-rate estimate, miner counts, the events list and anything about mempools, because those are one observer's reading of its own node. The DAG picture is drawn from headers the engine holds; the homepage's simulated preview is never shown in the app.
## 7. The engine and the API it needs
One Rust crate, `igneum-light`, holds the light client (spec 10), the wallet's transaction building and signing, the pool-protocol client (spec 9, member role without the signer: it never holds a vote key), and the node-card parser. It is exposed to Swift and Kotlin through a foreign-function layer (UniFFI is the candidate, approximate), compiled to WebAssembly for the homepage card (spec 10.8), and linked by the desktop app for its own wallet screens, so that one code path produces every verification result the project shows anywhere.
What the app needs from a node is the read-only table of spec 10.7. The app adds nothing to it: wallet reads are `eth_getProof`, `eth_getBalance`, `eth_call`, `eth_estimateGas`, `eth_gasPrice`, `eth_getTransactionCount` and `eth_sendRawTransaction` (design document, execution layer, 8.2), status is `igneum_getTransactionStatus`, and the miner monitor is `getFinalityWeights` plus the launcher's status lines behind the node-card token. Everything the app sends to a node is a signed transaction; every other message is a read.
What the app needs from a pool is `hello`, `authorize` with a read token, `stats`, `votes_carried` and `bye` (spec 9.5). A pool that does not serve read tokens is shown as "no monitor", and the chain-alone row of section 4 still works.
## 8. What the app verifies about itself
Spec 8 binds the desktop client: reproducible builds, the release key in genesis, no silent updates. A store build cannot meet all of it:
| Rule of spec 8 | On the phone | Label |
|---|---|---|
| Reproducible build with the hash in the repository | Android: the APK can be reproducible and the repository carries its hash. iOS: Apple re-signs and may re-encrypt the binary it distributes, so byte reproducibility against the store build is not available (approximate); the repository carries the hash of the submitted archive instead | Designed, Open (P2) |
| Signed by the release key | The store signs the binary; the release key signs the archive hash the repository publishes, and the app shows its build hash on the About screen next to the published one for the user to compare | Designed |
| No silent updates | Store auto-updates are the platform's, not the app's; the app cannot refuse them. The app refuses nothing and changes nothing by itself: no remote configuration, no feature flags fetched from a server, no code loaded at run time, so a store update is the only way its behaviour changes and that update is visible in the store's history | Designed |
| The client cannot change consensus | The app signals nothing and mines nothing; nothing to bind | |
| Official sources | The two stores plus the APK on the project's domain with its hash beside the button (spec 8.4 item 2 names the domain and the repository as the only sources; the stores are added as official sources for this app only, and the spec gains that sentence at its next MINOR step, P3) | Designed |
## 9. Screens
1. **Welcome.** What the app does, the permanent line, two buttons: "Verify the chain" (no wallet needed) and "Create or restore a wallet".
2. **Home.** Balance with its status word (executed, proven, locked), the verification badge from the engine, last transactions, a line for miners if any are configured.
3. **Send.** Address, amount, the quoted price, the status words explained in one line each, biometric confirmation.
4. **Receive.** Address and QR.
5. **Activity.** Transactions with the three status flags and the checkpoint that locked each.
6. **Verify.** Everything in section 2.
7. **Network.** Everything in section 6.
8. **Miners.** Everything in section 4; add a source by scanning a node card or entering a pool.
9. **Scan.** The node-card scanner.
10. **Settings.** Network (testnet or mainnet), data mode, nodes (the seed list, the user's own), the hardware wallet (version 2), About with the build hash.
11. **Seed.** Shown once at creation; confirmation by re-entry.
12. **Restore.** The one screen that accepts words.
13. **Support.** The official sources, the hash, the line, and no chat: support never asks for anything.
## 10. Build plan, tied to `site/journey.json`
| Journey phase | What ships | Gate for the app |
|---|---|---|
| Phase 3, devnet (started 3 Oct 2026, 20 nodes by Mar 2027) | `igneum-light` reads the devnet: checkpoint headers, certificates, voter set from nodes; no proofs yet (the devnet has no execution layer); the Network and Verify screens on an internal build; the node-card QR added to the Windows launcher's NODE card | Certificates verified on a phone against the devnet; O-10.1's byte counts recorded; O-10.3's BLS timings recorded |
| Phase 4, finality and job market (Apr to Jul 2027) | The Miners screen over the reference pool of spec 9 (O-9.1); the wallet on devnet coins behind an internal flag; the carriage ratio of spec 9.7 item 6 computed on the phone | The pool protocol's O-9.5 and O-9.6 measurements; the eclipse re-run of O-10.5 with the phone as the client |
| Phase 5, public testnet (Aug to Oct 2027) | Store submission on both platforms with testnet coins only; the homepage card switched from PREVIEW to the testnet build of the same engine (spec 10.8); the wrapped proof verified once R4 (ledger P3) has run | Both stores approve a build that mines nothing; the card's steps 1 to 7 run in a browser; 1,000-miner gate data from the Miners screen is not used, because X5's independence definition is a separate measurement |
| Phase 6, mainnet fair launch (Nov 2027) | Mainnet selectable in the same app; the trusted checkpoint for mainnet shipped in the first release after the first certificate (spec 3.8's first month means no certificate before day 30, so the mainnet client runs on the finality-depth bound until then and says so) | The app shows "finality not active" for the first month, as the exchange guidance of spec 3.9 requires |
| Phase two of the proof system (after mainnet, design document section 7) | The consensus proof replaces the voter set from nodes; the "from nodes" status disappears from every screen and the card | O-10.8 |
No durations for the engineering are given here beyond the journey's phase dates; the work is tied to those gates, not to a calendar of its own.
## 11. Open items
| Id | Item | What closes it |
|---|---|---|
| P1 | The SLIP-44 coin type is unregistered; the app uses 60 meanwhile, which makes Igneum addresses collide with Ethereum addresses from the same seed (a convenience for users and a footgun for anyone who sends IGN to an Ethereum-only wallet) | Register a coin type before the public testnet and decide whether to keep 60 for compatibility; owner: execution engineer |
| P2 | iOS store builds are not byte-reproducible against the repository | Decide what the About screen compares (the archive hash, signed by the release key) and write the sentence into spec 8 at its next MINOR step |
| P3 | Spec 8.4 item 2 names the domain and the repository as the only official sources; the stores are a third for this app | Spec 8 MINOR step |
| P4 | The read-only token format for pools and node cards, and its revocation | Define with the reference pool of O-9.1; the node card's `expires` field is the first version |
| P5 | Remote management of rigs from the phone (start, stop, switch pool) was left out of version 1 for store simplicity and to keep the app's only write a signed transaction | Revisit at phase 5 after both stores have approved version 1 |
| P6 | Hardware wallets over Bluetooth | Version 2, after a device vendor's Igneum chain id support, which needs the chain id registered on ethereum-lists/chains (spec 7.1) |
| P7 | The developer entity shown on the store listing | Section 12 |
## 12. The decision for the project lead
Both stores show the developer's legal name on the listing, and Apple's wallet rule (3.1.5 (b), approximate) wants an organisation account, not an individual. CLAUDE.md's standing rule is that the organisation owner is never publicly visible. A store listing cannot honour that rule: some entity's name will be on the page. The decision is which entity publishes the app (an existing company, a new one for Igneum, or a foundation that does not yet exist), which also decides the privacy policy's signatory and the release-key steward's relation to it (spec 8.2 item 2, O-8.1). Nothing in sections 1 to 11 depends on the choice, and nothing can be submitted to a store without it.

View file

@ -0,0 +1,189 @@
# Igneum protocol specification, section 9: the pool protocol
Spec version 0.1, 3 October 2026. Status of this section: Designed. Nothing here is implemented. The only miner-to-worker protocol that exists is the devnet worker protocol (`igneum-miner --worker`, `vendor/igneum-node/igneum/miner/src/main.rs`; `--serve` in `proto-cuda/host.cu`, `proto-opencl/host.c`, `proto-metal/main.swift`), which runs between two processes on one machine and is cited here where this section reuses it. The review of 3 October 2026 found that no pool protocol existed and that "shares on a 64-bit lane hash with vardiff are unspecified" (`docs/review/round-3-2026-10-03.md`, farm operator, attack 3, R3.15). This section is the answer.
Lineage, credited. The shape is Stratum V2's (Braiins and the Stratum V2 working group, stratumprotocol.org), from memory and approximate: a binary, encrypted protocol with a Mining Protocol for shares, a Job Declaration Protocol under which a miner builds its own block template from its own node and declares it to the pool, which may accept it and must then pay shares found on it, and a Template Distribution Protocol between the miner and its node. Igneum keeps the three ideas (encrypted transport, miner-built templates under the pool's payout, pool pays by shares) and changes what the header carries: on Igneum the header names a vote key (section 2.4) and the vote key decides finality (section 3), so this section also fixes whose key a pooled block names and who signs the checkpoint votes. The ledger entries F10 and G6 (pools hold the votes; Stratum V2 job declaration is optional) are the two criticisms this section is written against.
Two words that collide. In this section "pool" means a mining pool: an operator that distributes work to many hashers and pays them by shares. The "proving pool" of sections 2.5 and 5.3 is the 20% emission share paid to provers and is called "the proving share" here whenever it is meant.
## 9.1 Roles and what each one holds
| Role | Runs | Holds | Never holds |
|---|---|---|---|
| Member | The operator's miner program (the signer), on a machine the operator controls, driving one or more GPU workers over the worker protocol | The operator's vote key (section 3.1 W1, section 8.5 item 4), the operator's payout preference, a verifier: either a full node or the light client of section 10 | The pool's keys |
| Worker | A GPU process fed `job` lines by the member (the `--serve` protocol) | Nothing: no key, no template, no network connection | Any key |
| Pool | A server that builds templates, issues jobs, verifies shares, pays members, relays votes and carries them in its templates | Its own payout address, its own full node, its members' public keys and share ledger | Any member's secret key |
| Member's verifier | A full node or a section 10 light client | The chain state the member checks templates and checkpoints against | |
The signer is one process per vote key (9.6). Hashing rigs that carry no key are workers, and a member MAY drive any number of workers on any number of machines from one signer.
## 9.2 What the pool cannot do, and how a member would notice
This section exists so that each row of the following table is true for a conforming member against any pool, conforming or not.
| The pool tries to | Why it fails against a conforming member | How it shows |
|---|---|---|
| Leave out or reorder transactions the member wanted in its block | The member receives the full template (9.4, mode A) or builds its own (mode C) and checks it against its verifier before hashing it; a declared template is paid like any other | A pool that refuses declared templates answers with a code (9.5, `template_refused`) and the member counts refusals; the explorer shows which templates each pool's blocks came from by the `template_mode` tag in the coinbase extra data |
| Name its own vote key in the header | The pre-PoW hash commits to `vote_key_hash` (section 2.2, fork point a5), the member checks the field against its own key before hashing and refuses a job that names another | A pool whose blocks all name one key is a custodial pool (9.6 item 5); any observer counts keys per payout address from headers and bodies alone |
| Drop a member's checkpoint votes | Votes are signed by the member and reach the chain by three roads (9.7): the pool's template, the member's own template and any node's `submitFinalityVote`. The member checks that the template it is about to hash carries its outstanding votes | Participation per key is public (`getFinalityWeights`, section 3.10) and computed from votes in blocks (Q2); 9.7 item 6 gives the chain-only test |
| Make a member sign a wrong checkpoint | The member signs only a checkpoint hash its own verifier reports; a member without a verifier does not vote (9.7 item 2) | A member that signs two hashes at one index produces equivocation evidence against itself (section 3.6), which is why the rule is "no verifier, no vote" |
| Withhold a block the member found | The member holds the full block and submits it to its own node as well as to the pool (9.5, `solution`) | The block is on the chain whatever the pool did |
| Underpay shares | Share weight is fixed by this section (9.8) and the member keeps its own ledger of accepted shares | Not a protocol matter beyond the ledger; the member changes pool |
What the pool can still do: choose the transactions in its own templates for members who take them as offered (mode A members who do not check, or mode B members), decline declared templates with a stated code, and concentrate hashrate. Vote weight, however, follows the members' keys (9.6), so pool concentration no longer equals vote concentration as section 3.7 item 3 states; that sentence describes a custodial pool and is narrowed by this section once a conforming pool exists (O-9.1).
## 9.3 Transport and encoding
Decision: newline-delimited JSON over TLS 1.3, one JSON object per line, UTF-8, binary fields as lowercase hex. Not the node's gRPC.
Why. The pool protocol crosses operators and languages: pools, mining operating systems and rental markets each have their own code base, and the lingua franca of every one of them is a line of JSON on a socket (Stratum V1 made it so). The node's gRPC surface is the node's own interface, it is edited at every fork point (`docs/fork-divergence.md`, the `rpc/grpc` rows) and a pool written against it would break at each. Line-oriented text also matches the worker protocol the member already speaks downward, so one miner program has one shape of message in both directions. TLS is required because Stratum V1 in plaintext is hijackable on the wire (the reason Stratum V2 encrypts, approximate), and a hijacked connection could feed a member a checkpoint hash to sign.
| 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 |
| 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 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 | Designed |
## 9.4 Templates
Three modes. The pool announces which it supports in `welcome`; every conforming pool MUST support A and C, and MAY support B.
| Mode | What the member receives | What the member checks | For whom |
|---|---|---|---|
| A, full template (default) | The complete block: header fields except nonce, the coinbase (payout, the member's key reveal when outstanding, the finality section with the member's votes), every transaction as hex | Everything in 9.4.1 | A member with a verifier and ordinary bandwidth |
| B, commitment | The header fields, the coinbase, the Merkle path from the coinbase to `hash_merkle_root`, and the transaction count; not the transactions | Header, coinbase and path; it cannot check transaction choice and the protocol marks its blocks `template_mode: "B"` | A member on a metered link that has chosen to trust the pool's transaction choice |
| C, declared | Nothing: the member builds the template from its own node with the pool's payout address and declaration constraints, and declares it; the pool replies with an id and issues jobs on it | The pool checks validity and payout; the member checks nothing further because it built it | A member that wants its own transaction choice, which is the Stratum V2 job declaration idea |
### 9.4.1 What a member checks before hashing a template (modes A and B)
A conforming member MUST verify, against its verifier, every item below, and MUST refuse the job (`job_refused` with the code) when one fails:
1. `vote_key_hash` equals the hash of the member's own key.
2. The coinbase `miner` address is the pool's address announced in `welcome` (so the pool, not a third party, is paid) and, when the member's key has not yet been revealed on chain, the coinbase extra data carries the member's `KeyReveal`.
3. The finality section carries every vote the member sent in this session that is not yet carried by a block in the template's past (the pool reports carriage in `votes_carried`, and the member's verifier can confirm it).
4. `daa_score`, `bits`, `pruning_point`, the parents and `seed_source` agree with what the member's verifier reports for a block built now: parents are known tips or within the merge-depth bound, `bits` is the target the verifier computes for those parents (section 2.3), the epoch seed in `seeds` is the one the verifier holds for that DAA score (section 1.12).
5. Mode A only: `hash_merkle_root` recomputes from the transactions and the coinbase, no transaction is a state-free fault (design document, execution layer 1.5), and, when the member's verifier is a full node, the node accepts the block as a template (the node's own template validation; the RPC is the forward reference of O-9.4).
6. Mode B only: the Merkle path from the coinbase proves membership under `hash_merkle_root`.
Cost. Items 1 to 4 are field comparisons. Item 5 is one Merkle root over the block's transactions and one node call, both well under the 1-s template lifetime at any block size the gas budgets of the design document allow (Designed, not measured; the measurement is O-9.5). The lottery hash is not evaluated at job time at all: the member's CPU evaluates one 32-lane unit per `found` nonce only (9.8), at 0.441 ms per unit on one M5 Max core (Measured, `docs/bench-log.md`, igneum-pow entry; section 1.11).
### 9.4.2 Declared templates (mode C)
1. The member sends `declare_template` with the full block (as mode A would carry it) built from its own node, with the pool's payout address as `miner`, its own `vote_key_hash`, and the pool's `declaration` constraints from `welcome` honoured: at most `max_extra_bytes` of member extra data after the pool's tag, and the pool's `tag` (an opaque string the pool uses to attribute the block) placed first in the coinbase extra data.
2. The pool MUST reply within `declare_timeout_ms` (Designed 500 ms) with `template_ack` (an id) or `template_refused` with one of the codes of 9.5. A pool MUST NOT refuse a declared template on the content of its transactions; it MAY refuse for invalidity against its own node, a wrong payout, a missing tag, oversize, or a rate above `max_declarations_per_s`.
3. The pool issues jobs on a declared template as on its own; shares on it are paid identically (9.8).
4. A pool MAY cap the share of its hashrate on declared templates only by refusing new declarations with code `capacity`, never by paying them less.
A member that is refused on code `other` more than `refusal_tolerance` times in a session SHOULD disconnect and say why in its log; the pool's refusal rate is reported by the member in `stats` so that a mining operating system can show it.
## 9.5 Messages
Direction P = pool, M = member. Every message is one object with `"t"` naming the type.
| 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` or `pps` with its parameters), `min_shift`, `max_shift` | 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) | |
| `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` | 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 |
| `declare_template` | M to P | `header`, `coinbase`, `transactions` | Mode C |
| `template_ack` | P to M | `template_id` | |
| `template_refused` | P to M | `code` in {`invalid`, `payout`, `tag`, `oversize`, `rate`, `capacity`, `other`}, `detail` | |
| `job` | P to M | `job_id`, `template_id`, `prehash` (64 hex), `target64` (16 hex), `share_target64` (16 hex), `nonce_start` (hex, a multiple of 32), `nonce_count` (hex, a multiple of 32), `clean` (bool) | Work. `clean` true means abandon earlier jobs. The member forwards it to its workers as a worker-protocol `job` line with `share_target64` in the target field |
| `job_refused` | M to P | `job_id`, `code` in {`vote_key`, `payout`, `reveal`, `votes`, `header`, `merkle`, `invalid`, `seeds`}, `detail` | The member will not hash this job and says why |
| `share` | M to P | `job_id`, `nonce` (16 hex), `hash` (16 hex) | A lane hash at or below `share_target64` |
| `share_result` | P to M | `job_id`, `nonce`, `accepted` (bool), `code` in {`ok`, `stale`, `duplicate`, `above_target`, `wrong_hash`, `unknown_job`}, `weight` (the share's weight, 9.8) | |
| `solution` | M to P | `job_id`, `nonce`, `hash`, `block_hash` | A lane hash at or below `target64`. The member has already submitted the block to its own node, or does so at once if it has none |
| `checkpoint` | P to M | `index`, `hash`, `blue_score`, `daa_score`, `state` | The pool's view, for information. A member MUST NOT sign from this message alone (9.7) |
| `vote` | M to P | `index`, `hash`, `vote` (280 bytes hex, section 3.10 C2) | The member's signed vote, for relay and carriage |
| `vote_ack` | P to M | `index`, `relayed` (bool) | |
| `votes_carried` | P to M | list of {`index`, `block_hash`} | Blocks the pool's node has seen that carry the member's votes |
| `stats` | both | member: `hashrate`, `workers`, `refusals`, `shares`; pool: `members`, `hashrate`, `blocks_24h`, `declared_share` | For dashboards and the phone app (`docs/design/phone-app.md`), read-only |
| `ping`, `pong` | both | `id` | Liveness |
| `bye` | both | `reason` | Close |
Sizes. A `job` line is under 300 bytes; a `share` under 120; a `vote` about 600 (hex of 280). A mode A `template` is the block, which at 1 BPS and the design document's budgets is a few KB to a few hundred KB (Designed, unmeasured; O-9.5); a mode B `template` is about 1 KB plus the coinbase.
## 9.6 Identity: one vote key per operator
Rule of 3 October 2026 (ledger F17; section 3.1 W6): keys are free, weight is the only Sybil-resistant quantity, and a rule that counts keys is a rule a splitter wins. This section applies it to pools.
1. A member's vote key is the operator's key, the one its client created under section 8.5 item 4. A pool MUST NOT create, derive or hold vote keys for its members, and a conforming client defaults to one key per operator. The devnet launcher's `MINERS=8` identities per vendor is a devnet convenience and is withdrawn by this rule for any public network (ledger F17, "the client's one-key default" item, closed here as a rule and Open as code, O-9.2).
2. The header of every block a member finds names the member's key, in every mode. The pool's blocks therefore carry as many keys as it has members, and each member's 30-day weight (W2) is the blocks that member found, under any pool or alone, because weight follows the key and not the payout address. Moving between pools moves nothing.
3. Many rigs, one key: the key lives in one signer process (9.1) and the rigs are workers. An operator who runs two signers under one key risks equivocation (3.6): BLS signatures are deterministic, so two signers that see the same checkpoint hash produce the same vote, but two signers whose verifiers disagree during a partition sign two hashes at one index and strip the key's weight for 30 days. Two signers under one key is therefore a MUST NOT, and the client enforces it by refusing to start a second signer on the same key file.
4. Succession (W5) is the operator's: a pool never signs a succession message, because it never holds the key.
5. A custodial pool (`vote_mode: "pool"`): a pool MAY name its own key in every header it issues, and a member MAY accept that. The member's client MUST show the mode before the first share and MUST default to refusing it. A custodial pool's blocks all name one key, so its share of vote weight is visible to anyone who counts keys per payout address, and the explorer labels it. Section 3.7 item 3 describes this pool and no other.
6. Dust (W3) is per key: a member who finds fewer than 100 blocks in 30 days has no vote whichever pool it joins, which at one block a second is about 0.004% of hashrate (section 3.7 item 6). Pooling changes a member's variance of income, not its weight.
## 9.7 Votes: signed by the member, relayed by the pool
1. The member's signer reads checkpoints from its own verifier (a full node's `getFinalityCheckpoints`, or the light client of section 10), signs every checkpoint of the presence window it has not signed (section 3.3 Q2, as the devnet miner does, `Voter::tick` in `main.rs`), and sends each vote to the pool as a `vote` message. Nothing in the vote depends on the pool: the message signed is `(chain_id, index, hash(C_i))` (C2), and the pool only sees the signature.
2. A member whose only source of checkpoints is the pool's `checkpoint` message MUST NOT vote. A silent key leaves the active denominator after the presence window and loses nothing but its say (Q2); a key that signs what it was told loses 30 days of weight if it was told wrong (3.6). The client enforces this: voting is on only when a verifier is configured, and the light client of section 10 counts as one.
3. The pool MUST relay every member vote to the network as its own node received it (the p2p vote message of section 3.10, C2) within `relay_ms` (Designed 1,000 ms, under the certificate grace of Q4, Open O-3.4) and MUST carry it in its next template's finality section unless a block in that template's past already carries it, under the per-block vote bound of O-3.3.
4. Aggregation in templates. Votes for one `(index, checkpoint hash)` pair MAY be aggregated inside a block into one signature with a bitmap (Q2). A pool with many members SHOULD aggregate its members' votes per index before carriage: at 10,000 voters and 2,880 checkpoints a day, unaggregated carriage needs 333 votes per block at 280 bytes each (93 KB per block), while an aggregate per index per block is one signature and a bitmap (Designed; the bitmap's size and canonical list are O-3.12). Aggregation needs no secret key, so the pool can do it, and the member's participation is credited either way (Q2 counts a vote carried "as a vote or inside a certificate", and an in-block aggregate is a vote carrier; the spec 03 wording should say "or inside an aggregate", O-9.3).
5. Three roads. A member's vote reaches the chain through the pool's template (item 3), through the member's own declared templates (mode C, its own finality section), and through `submitFinalityVote` on any node it can reach, including seed nodes; the signer SHOULD use at least two of the three. A vote carried twice costs block space and nothing else (section 3.10: "votes not already in its past").
6. Detecting a pool that drops votes, from the chain alone. For member key k with pool payout address A, over a presence window: let `V_k` be the indices at which k voted (k's own log, or the votes seen on any road), `B_A` the blocks whose `miner` is A, and `C_A(k)` the indices among `V_k` carried by some block in `B_A` within 60 blocks of k's vote. A conforming pool's `|C_A(k)| / |V_k|` is near 1 minus the fraction already carried by other blocks (item 3 lets it skip votes already in the past); a pool that drops votes shows a ratio near 0 while the same votes appear in other producers' blocks or in k's own. The ratio is computable by anyone from headers and bodies (k's key hash is in the header of k's blocks and k's key is revealed in the body; A is in every coinbase), so a pool's carriage ratio is a public statistic the explorer and the phone app show per pool. The threshold below which the client warns is Open (O-9.6).
## 9.8 Shares
The hash is 64 bits and is defined over an aligned group of 32 nonces (section 1.9), so a share is defined the same way.
1. A share for job `j` is a nonce `n` in the job's range with `hash(n) <= share_target64(j)`, where `hash(n)` is lane `n AND 31` of the group `n AND ~31` under the job's `prehash`, init words `I = seed_words_from_bytes("igneum-block/" || prehash || nonce_hi_le32)` (section 1.6, header binding, O-1.9) and the job's seed pair.
2. `share_target64 = min(2^64 - 1, target64 << s)` for the shift `s` the pool last sent in `set_target`, `min_shift <= s <= max_shift`. The pool MUST choose `s` so that `target64 << s` does not saturate; a saturated target makes every hash a share and the pool's verification cost (item 5) unbounded. `target64` is the block target of the template's `bits` under the mapping of section 1.10 (`target64 = target256 >> 192`, candidate, Open O-2.4).
3. Weight. A share at shift `s` counts `2^-s` of a block: its expected cost is `2^64 / share_target64` hashes against `2^64 / target64` for a block, and the ratio is `2^-s` exactly, so share weights are exact binary fractions and a pool's ledger needs no floating point. A `solution` (a share at or below `target64`) counts as a share at its job's shift plus whatever the scheme pays for the block.
4. Vardiff. The pool SHOULD set `s` per member so that the member sends about one share per `share_interval_s` (Designed 10 s): at the worker protocol's 64-bit hash and a card at 229 Mhash/s (the RTX 5090 bench figure quoted in `docs/review/round-3-2026-10-03.md`, R3.15), ten seconds is 2.3 x 10^9 hashes, which fixes `s` once `target64` is known. The pool adjusts `s` by one per `set_target` and never more than once per 30 s (Designed), so the share rate is a smooth estimate of a member's hashrate for the dashboards.
5. Verification. The pool MUST verify every share by evaluating the 32-lane group on a CPU exactly as a node verifies a block (section 1.11): 0.441 ms per group steady on one M5 Max performance core, 0.87 ms worst cold (Measured, `docs/bench-log.md`, igneum-pow entry). One such core verifies about 2,270 shares a second, so at one share per 10 s per member one core covers about 22,000 members (derived from the measurement; a server core is the measurement of O-9.5). A pool MAY sample shares at high shifts; it MUST NOT credit a share it did not verify at a shift below `sample_shift` (Designed 8, so every share worth more than 1/256 of a block is checked).
6. The member re-checks every `found` nonce on its own CPU before sending it as a share, as the devnet miner already does for blocks (`mine_worker`, "cpu re-check ok"); a worker whose `found` lines fail the re-check is reported as `WORKER MISMATCH` and the share is not sent.
7. Rejections. `stale`: the job's template is superseded and the share is older than `stale_grace_ms` (Designed 2,000 ms, two block times, so a share found a moment after a new template still pays); `duplicate`: the nonce was already credited for the job; `above_target`: the pool's own evaluation exceeds `share_target64`; `wrong_hash`: the member's `hash` field disagrees with the pool's evaluation (a worker fault, reported to the operator).
8. Payment schemes are the pool's business. The protocol fixes share weight and the `share_scheme` disclosure; it does not fix PPLNS window lengths or PPS fees. A pool that pays the proving share: a member's blocks pay 80% of emission to the pool's address and 20% to the provers of the block (section 2.5), so a pool receives only the 80%; whether a pool also proves (its node holds the members' keys' sortition eligibility, section 7.2, because eligibility is by key and the key is the member's) is answered by 7.2 item 2: the assignee is the member's key, so a pool cannot prove in its members' name without their shard proofs carrying their keys (R3.13, `provers` naming in the proof statement). Pools that offer proving to members do so under the external job market's terms, outside this section.
## 9.9 Seeds and the epoch boundary
The program changes every epoch and the dataset every day (section 1.12), and an ahead-of-time worker needs the next pair before the boundary (the `prepare` command of the worker protocol, measured across boundaries in `docs/bench-log.md`, hot-swap entry).
1. The pool MUST send `seeds` on connection and whenever the current or next pair changes, with `next_at_daa` the DAA score of the boundary.
2. The member MUST check `epoch_seed` and `next_epoch_seed` against its verifier (the VDF output for the epoch, section 4.3; on the devnet, the hash of the last selected-chain block below `3,600 e - 600`) and refuse jobs on a seed its verifier does not confirm (`job_refused`, code `seeds`). A pool that could choose the seed could choose the program, which is the grinding section 4 exists to prevent.
3. A member whose verifier is the light client of section 10 verifies the VDF proof (516 bytes, 4.5 ms, Measured, section 4.4) that the pool forwards in `seeds`, against the `seed_source` its checkpoint chain confirms.
## 9.10 Parameters in this section
| Parameter | Value | Label |
|---|---|---|
| Transport | TLS 1.3, newline-delimited JSON | Designed |
| Ports | 4461 / 4462 / 4463 | Designed |
| Required modes | A (full template) and C (declared); B optional | Designed |
| `declare_timeout_ms` | 500 | Designed, Open O-9.5 |
| `relay_ms` | 1,000 | Designed, tied to Q4's grace (O-3.4) |
| `share_interval_s` | 10 | Designed, Open O-9.5 |
| Share target | `target64 << s`, never saturated | Designed |
| Share weight | `2^-s` of a block | Designed |
| `sample_shift` | 8 | Designed |
| `stale_grace_ms` | 2,000 | Designed |
| Vardiff step | one shift per 30 s at most | Designed |
| Pool-side verification per share | 0.441 ms per 32-lane group, one M5 Max core | Measured (section 1.11); server core Open O-9.5 |
| Vote key per member | the operator's own; one signer per key; pool holds none | Designed (ledger F17) |
| Custodial mode | allowed, disclosed, refused by default, visible on chain | Designed |
| Member without a verifier | hashes, does not vote | Designed |
| Vote carriage by the pool | every member vote in the next template, aggregated per index when many | Designed |
| Member template checks | 9.4.1 items 1 to 6 | Designed |
## 9.11 Open items
| Id | Item | What closes it | Gate |
|---|---|---|---|
| O-9.1 | Section 3.7 item 3 ("pools hold their hashers' votes") describes a custodial pool once this section is implemented; the litepaper's pool sentence (ledger F10, G6, overclaims 44 and 45) should say that a conforming pool's members keep their keys and that custodial pools are visible on chain | A reference pool and a reference member run on the phase 4 devnet; the `getFinalityWeights` listing shows one key per member under the pool's payout address; then the sentence is rewritten and 3.7 narrowed | 4 |
| O-9.2 | The devnet launcher runs `MINERS=8` identities per vendor (ledger F17); the one-key default is a rule here and not code | The launcher and the official client start one signer per operator and any number of workers; the signer refuses a second instance on the same key file | 4 |
| O-9.3 | Q2 counts a vote carried "as a vote or inside a certificate"; an in-block aggregate of votes (9.7 item 4) is a third carrier and the wording should name it; the bitmap and canonical list are O-3.12 | Spec 03 wording at the next MINOR step; the per-block vote bound re-run of O-3.3 with pool-aggregated carriage in the model | 3 |
| O-9.4 | Mode A item 5 needs a node RPC that validates a block as a template without submitting it; none is named | Add the RPC to the fork (candidate: `validateBlockTemplate`), with the cost per call measured at the design document's gas budgets | 2 |
| O-9.5 | Every timing here is Designed: template size and bandwidth per member at 1 BPS, the member's check cost (9.4.1), `declare_timeout_ms`, `share_interval_s`, the pool's verification throughput on a server core | A reference pool with 100 members on the phase 4 devnet: record template bytes per second per member, check time per template on a 2019-class laptop core, declared-template acceptance latency, shares verified per second per core on a server CPU; set the four parameters from the distributions | 4 |
| O-9.6 | The carriage ratio of 9.7 item 6 has no threshold, and its 60-block window is a guess | On the same devnet, one pool conforming and one dropping every vote: record both ratios per key over a day; set the warning threshold at the point that separates them with no false warnings on the conforming pool | 4 |
| O-9.7 | The TLS exporter binding in `authorize` is named and not specified (which exporter label, which bytes are signed) | Write the exact bytes at the first implementation; test that a replayed `authorize` on a second connection is refused | 4 |
| O-9.8 | A member with only the light client of section 10 as its verifier checks headers against a checkpoint chain, not against a full node's tip, so item 4 of 9.4.1 (parents are known tips) is weaker for it: it can confirm the parents descend from the last certified checkpoint and no more | Decide at gate 4 whether a light-client member may vote (9.7 item 2 says yes) after the eclipse test of O-3.7 is re-run with light-client members in the model | 3, with 4 |
| O-9.9 | HiveOS and the rental markets need this protocol to list the algorithm (ledger entry on rental, "cannot list an algorithm whose kernel changes hourly without a stratum for it") | The reference member runs under HiveOS against the reference pool through 24 epoch changes with in-worker compilation (R3.15's acceptance test: outage under 2 s per change) | 4 |

View file

@ -0,0 +1,144 @@
# Igneum protocol specification, section 10: the light client
Spec version 0.1, 3 October 2026. Status of this section: Designed. Nothing here is implemented. The design is `docs/design/execution-layer.md` section 7 (D14: at launch a light client verifies the execution proof chain from a certificate it is given; phase two adds the consensus proof) and ledger entries P3 and P4, both conceded: a phone verifies "in milliseconds" only with a curve-based wrapper nobody has measured, and a trustless light client needs a consensus proof that is phase two. This section says what a phone or a browser verifies at launch, what it is given, what it fetches, and what it trusts, so that the homepage's card and the litepaper's sentence can be made true as written.
Terms. A light client holds no state and executes nothing. It verifies three things about the chain: that a checkpoint is certified (section 3), that a state root is the output of the proven execution up to that checkpoint (section 7 and the design document, sections 2 and 5), and that an account or a storage slot has a given value under that root (`eth_getProof`). "Launch" is the phase one client; "phase two" is the client with the consensus proof.
## 10.1 Trust assumptions, stated plainly
| What the client trusts | At launch | Phase two | How the trust is bounded |
|---|---|---|---|
| Its own binary and the seed list inside it | Yes | Yes | The release process of section 8: reproducible build, hash beside the download, release key in genesis. A store build of the phone app is re-signed by the store (`docs/design/phone-app.md`, section 8) |
| The first certificate and voter set it starts from (the trusted checkpoint) | Yes, given out of band: shipped in the release and refreshed from `N` of `M` seed nodes that agree | Replaced by the consensus proof: any certificate is checked from genesis | The same assumption Ethereum light clients make for a sync-committee checkpoint (design document, section 7), approximate |
| The voter list and weights at each new checkpoint | Yes, unless the client runs in full-header mode (10.4) and recomputes them | No | Weight is a function of 30 days of headers (W2); a checkpoint-mode client cannot recompute it and asks nodes, with `N` of `M` agreement (10.6). The header commitment of O-10.2 would remove this row without the consensus proof |
| The BLS certificate's signers | Two thirds of active and 56.7% of total 30-day weight did not equivocate (section 3.7 item 1) | Same | The chain's own finality assumption; nothing a client can do about it |
| The proof system | The wrapped verifier key shipped in the client is the one the chain's proof system version uses (design document, section 5.6) | Same | A soundness bug is a light-client problem by design (ledger P7, section 5.7 emergency path); a full node executes natively and rejects a forged proof, a light client does not, and the client says so in its status |
| The nodes it fetches from | Only for liveness and for the voter set row above; headers, certificates and proofs are verified, not trusted | Only for liveness | A lying node can withhold or delay; it cannot forge a certificate, a proof or a header's PoW |
| The lottery hash of each header | Not verified in checkpoint mode; verified in full-header mode on the phone (10.4) | Covered by the consensus proof | A certified checkpoint is signed by weight, and weight is blocks, so a client that trusts certificates trusts the PoW behind them |
What "no node, no trust, no middleman" would require: all of phase two plus full-header mode, or phase two alone for a client that is content to trust certificates for PoW. The homepage sentence is rewritten in 10.8.
## 10.2 What the client holds
| Datum | Size | Source | Label |
|---|---|---|---|
| Checkpoint headers, one per certified index, or every header in full-header mode | About 400 bytes per header (10.5) | Nodes | Designed, approximate |
| The latest certificate per index it follows | 268 bytes plus one bit per voter (10.5) | Nodes, p2p message 70 relayed by a node | Implemented sizes (section 3.10, `docs/fork-divergence.md`) |
| The voter list and weights at the latest checkpoint | 60 bytes per voter (48-byte key, 8-byte weight, 4-byte participation count) | Nodes (`getFinalityWeights`), or recomputed | Designed |
| The latest wrapped segment proof and its public inputs | About 400 bytes (10.5) | Nodes (`igneum_getSegment`, the design document's RPC) | Designed, approximate; the measurement is R4 of the design document (ledger P3) |
| The VDF proof per epoch, full-header mode only | 516 bytes per epoch, 24 a day | Nodes, by `seed_source` | Measured (section 4.4) |
| The 256 MiB cache for the day, full-header mode only | 256 MiB in RAM, rebuilt daily in 0.2 s on a CPU | Computed from the day key (section 1.8) | Measured (section 1.12) |
| The seed node list | Under 4 KB | The release | Designed |
## 10.3 Sync at launch, step by step
1. Start from the trusted checkpoint shipped in the release: index `i0`, `hash(C_i0)`, the certificate, the voter list and weights at `C_i0`, and the latest wrapped proof at or below `C_i0`.
2. Open connections to at least `N` seed nodes (10.6). Ask each for the highest certified index it holds and for its trusted-checkpoint view of `i0`; if fewer than `N` agree with the shipped data, stop and tell the user, because the binary or the seed list is wrong.
3. For each index `i` from `i0 + 1` to the highest certified, in order: fetch the header of `C_i` and its certificate (10.7), verify the certificate (10.4 item 2), and accept `C_i` as certified. A client that was offline for a long time MAY skip to the latest certified index after one verification per presence window (240 indices) rather than per index, because a certificate is valid on its own against the voter set the client holds; the voter set it holds ages by that much, which is the trade of 10.4 item 3.
4. Fetch the latest proof record whose segment is at or below the latest certified checkpoint; verify the wrapped proof (10.4 item 4). Its `post_root` is now the state root the client reads from.
5. Read balances and storage with `eth_getProof` against `post_root` and verify the Merkle-Patricia proofs locally; nothing a node returns about state is accepted without one.
6. Subscribe to new certificates (10.7) and repeat items 3 to 5 as they arrive, about every 30 s.
The client shows, at every moment: the latest certified index and its age, the latest proven segment and its distance behind the checkpoint, and which of the two the balance on screen rests on (design document, section 2.3: a balance is "proven and locked" only when the proof's segment is at or below the certified checkpoint).
## 10.4 What is verified per update
1. **Header.** `hash(C_i)` recomputes from the header's fields (the chain's BLAKE2b-based hash, section 0.6, over the fields of section 2.4). In checkpoint mode nothing links `C_i` to `C_i-1` except the rule that a certified checkpoint's selected chain passes through every earlier certified checkpoint (C3), which the client cannot check without the path; it relies on the certificate's signers having checked it, which is the finality assumption of 10.1. In full-header mode the client holds every header, checks each parent list against headers it holds, computes the selected chain and confirms the path.
2. **Certificate.** The bitmap is over the canonical voter list of `C_i` (keys above dust and not stripped, sorted by key hash; section 3.10 C3). The client sums the public keys of the set bits (G1 additions), verifies the aggregate signature over `"igneum-vote-v1/" || chain_id || 0 || index || hash(C_i)` under the vote tag (C2), sums the signers' weight and checks both tests of Q3: at least two thirds of active weight (weight times participation) and at least 17/30 of total weight. Cost: one aggregation of up to V keys and one pairing check, which on a phone's CPU is milliseconds with a native BLS12-381 library and unmeasured in a browser (Open, O-10.3; a figure from memory would be approximate and is not given).
3. **Voter set.** The client needs the voter list, weights and participation at `C_i`. Checkpoint mode fetches them from nodes and accepts the set when `N` of `M` nodes return the same list root (a hash over the sorted list); full-header mode recomputes W2 and Q2 from headers and the votes in bodies, which needs bodies' finality sections as well as headers (the vote carriage of Q2), and that is why full-header mode costs what 10.5 says. A set that was fetched is marked "voter set: from nodes" in the status; a recomputed one is marked "voter set: verified".
4. **Proof.** The wrapped segment proof is verified with the shipped verifier key against the public inputs of the proof record (`segment`, `pre_root`, `post_root`, `receipts`, `version`; design document, section 5.4). The segment proof for N verifies the proof for N minus 1 recursively (design document, section 5.3), so the client keeps only the latest and never verifies a chain of them. The client checks that `segment` is a chain block at or below the latest certified checkpoint: in checkpoint mode by asking nodes for the chain height of the segment's block and accepting `N` of `M` agreement, in full-header mode from its own selected chain.
5. **State.** `eth_getProof` results are verified against `post_root` as on Ethereum.
6. **Seeds, full-header mode only.** The epoch seed named by `seed_source` is verified with the VDF proof (516 bytes, 4.47 ms per verify, Measured, section 4.4); the header's lottery hash is then evaluated with the CPU verifier (section 1.11): 0.441 ms per header on one M5 Max core, 38 s of one core per day of headers (derived); the phone and browser figures are O-10.4.
Phase two replaces items 2 and 3 with one check: a consensus proof whose statement is "checkpoint `C_i` is certified under finality rule version 2 over the 30-day window ending at `C_i`" (design document, section 7), folded into the segment proof, so one wrapped verification covers finality and execution. The voter set is then never fetched and the "from nodes" status disappears.
## 10.5 Bytes per day
Sizes used. Header: the fixed fields of the forked `Header` (`consensus/core/src/header.rs`: version 2, three 32-byte roots, timestamp 8, bits 4, nonce 8, DAA score 8, blue work up to 24, blue score 8, pruning point 32, `vote_key_hash` 32; Implemented) plus `seed_source` and `proof_ref` (32 each, Designed, section 2.4) come to 286 bytes; the parents by level are variable and at 1 BPS with GHOSTDAG k 18 add a few 32-byte hashes, so 400 bytes per header is the working figure (Designed, approximate; the devnet measurement is O-10.1). Certificate: index 8, checkpoint 32, voter count 4, bitmap one bit per voter, aggregate signature 96, aggregator key hash 32, sortition proof 96 (section 3.10 C3; G2 signatures are 96 bytes, `docs/fork-divergence.md`): 268 bytes plus the bitmap. Wrapped proof: a Groth16 proof over bn254 is three group elements, about 128 bytes compressed and 256 uncompressed, and a Plonk proof is under 1 KB (approximate, from memory; the design document names both and R4 measures); with the public inputs of 10.4 item 4 (130 bytes) and the prover list, 400 bytes per proof record is the working figure (Designed, approximate). Checkpoints: one per 30 blocks, 2,880 a day at 1 BPS. VDF proofs: 516 bytes, 24 a day (Measured).
| Mode | What is fetched per day | 1,000 voters | 10,000 voters | Label |
|---|---|---|---|---|
| Checkpoint mode (launch default) | 2,880 x (header 400 + certificate + proof 400) | 3.44 MB | 6.68 MB | Designed, approximate |
| Checkpoint mode, proof on demand | 2,880 x (header + certificate), plus one proof per app open | 2.28 MB plus 400 bytes per open | 5.52 MB plus 400 bytes per open | Designed, approximate |
| Full-header mode (verifies the selected chain and the lottery) | 86,400 headers + 2,880 certificates + 2,880 proofs + 24 VDF proofs, plus the bodies' finality sections for Q2 (not counted; O-10.1) | 36.9 MB plus finality sections | 40.1 MB plus finality sections | Designed, approximate |
| Per-block proof mode (a proof record for every chain block) | Full-header mode plus 86,400 proofs | 70.3 MB | 73.5 MB | Designed, approximate; not a mode the client offers, listed because the design proves every block and a client could fetch every proof |
| Phase two, consensus proof per checkpoint | 2,880 x (header 400 + folded proof 400) | 2.30 MB | 2.30 MB | Designed, approximate |
| Phase two, on demand | One header and one folded proof per open | 800 bytes per open | 800 bytes per open | Designed, approximate |
| Initial voter set, checkpoint mode | 60 bytes per voter, once per sync | 60 KB | 600 KB | Designed |
A phone on checkpoint mode uses about 100 MB a month (derived). A browser tab open for a minute on phase two uses about 1 KB. The certificate's bitmap grows linearly with voters and the per-key votes carried in bodies grow with voters times checkpoints, which is the scaling question of O-3.12 and section 9.7 item 4.
## 10.6 Node discovery: a seed list in the client, no DNS
1. The release carries a seed list: at least 8 entries of `(address, port, node identity key)`, the identity key being the node's p2p identity (rusty-kaspa's per-node id at the forked commit, forward reference) so the client pins the node and not a name. Addresses are IP literals; the client never resolves a name to find a node and works with DNS blocked or poisoned.
2. On every start the client asks each reachable seed for its peer list (a read-only RPC, 10.7) and keeps a local set of up to 64 nodes, preferring nodes that answered correctly last time; the seed list is the fallback, never the only set. The user MAY add their own node (the node card of `docs/design/phone-app.md`), which is then preferred for everything and still cross-checked against `N` of `M` others for the voter set.
3. `N` of `M`: the client fetches the voter-set root and the segment height (10.4 items 3 and 4) from `M` distinct nodes and accepts when `N` agree. Designed values `M = 5`, `N = 3`; Open (O-10.5) with the eclipse test. Headers, certificates and proofs need no agreement because they are verified.
4. In a browser the transport is WebSocket over TLS (`wss`, rusty-kaspa's wRPC at the forked commit), and a browser will only open `wss` to a host whose certificate a public CA issued, which in practice means a DNS name. So the browser client depends on the seed nodes' DNS names and CA certificates for transport, and on nothing else: the data it receives is verified as above. This is stated on the card (10.8) and is the reason the phone app, which can pin a node key on a raw address, is the client that meets item 1 in full.
5. A node that returns a header, certificate or proof that fails verification is dropped from the local set for the session and the event is shown.
## 10.7 What the client needs from a node
Read-only, served by every full node over the RPC of the fork, and over `wss` for browsers. Existing methods are those of section 3.10 and the design document's section 8.2; the rest are new and Designed.
| Method | Exists | Returns | Used in |
|---|---|---|---|
| `getFinalityCheckpoints(last N)` | Yes (section 3.10) | Index, hash, blue score, DAA, state, weights, aggregator | 10.3 item 3 |
| `getFinalityWeights` | Yes | Per key: hash, pubkey, blocks, voter, participation, stripped-until | 10.4 item 3 (checkpoint mode) |
| `igneum_getCertificate(index)` | New | The certificate bytes of section 3.10 C3 | 10.4 item 2 |
| `igneum_getHeader(hash)` and `igneum_getHeaders(from_hash, count)` | New (Kaspa's `getBlock` with `includeTransactions: false` is close) | Headers, wire-encoded | 10.3, full-header mode |
| `igneum_getVoterSetRoot(index)` | New | Hash of the canonical voter list with weights and participation at `C_index` | 10.4 item 3 |
| `igneum_getSegment(number)` | Design document 8.2 | Mergeset, executed set, shard plan, proof record | 10.4 item 4 |
| `igneum_getLatestProof(at_or_below_hash)` | New | The newest proof record whose segment is at or below the named chain block, with the chain height of that segment | 10.3 item 4 |
| `igneum_getVdfProof(seed_source)` | New (section 4.4 says proofs are "retrievable by `seed_source` from any peer") | `(T, y, pi)`, 516 bytes | Full-header mode |
| `eth_getProof`, `eth_getBalance`, `eth_call` | Design document 8.2 | Ethereum semantics against a named root | 10.3 item 5 |
| `igneum_getTransactionStatus(hash)` | Design document 8.2 | executed, proven, locked | The wallet's status line |
| `igneum_getPeers` | New (Kaspa's `getPeerAddresses` is close) | Addresses and identity keys | 10.6 item 2 |
| `eth_subscribe(newHeads)`, `FinalityLock` notification | Yes (section 3.10 notifications) | Push of new chain blocks and locks | 10.3 item 6 |
Every method above is read-only and needs no authentication; a node MAY rate-limit by address. The node card of the phone app carries a token only to let an operator read their own miner statistics from their own node (phone-app design, section 5), not to read the chain.
## 10.8 The homepage card, made true
The card on `site/index.html` reads today: "This tab · light client. Your browser will verify Igneum. One proof checked here, in milliseconds. No node, no trust, no middleman. Live at testnet." with cells BLOCK PROOF, CHECKPOINT, PROOF SYSTEM, VERIFIED TODAY. The ledger's overclaims 9, 25 and 38 already ask for the "no trust" and "milliseconds" claims to be re-scoped. What the card will do, at the public testnet (journey phase 5), step by step:
1. The page loads a WebAssembly build of the light-client engine (the same engine as the phone app, `docs/design/phone-app.md`, section 7) with the testnet's trusted checkpoint and seed list compiled in, and shows the checkpoint's index and date in the CHECKPOINT cell with the words "starting point, shipped with this page".
2. It opens `wss` to three seed nodes by name (10.6 item 4), asks each for the latest certified index, and shows "asking 3 nodes".
3. It fetches the latest certificate and the voter set, verifies the certificate against the voter set (10.4 item 2) and shows the signed weight as a fraction of total in the CHECKPOINT cell ("locked by 71% of 30-day weight, voter set from 3 nodes"). The words "from nodes" stay until phase two.
4. It fetches the latest proof record at or below that checkpoint and verifies the wrapped proof in the tab; the BLOCK PROOF cell shows the segment's height, the proof's size in bytes and the measured verification time in that browser, in milliseconds or whatever it was. The PROOF SYSTEM cell shows the version from the proof record.
5. VERIFIED TODAY counts proofs this tab verified since it opened, never a number from the server.
6. The sentence under the heading becomes: "This tab checks the latest locked checkpoint and the latest block proof itself. It fetches from three nodes by name and takes the voter list from them until the consensus proof lands (phase two)." The eyebrow stays "light client" and the PREVIEW pill is replaced by the testnet's name.
7. If any step fails, the card says which, in the same cell, and shows nothing it did not verify.
The sentence "Your browser will verify Igneum" is true under that card. "No node, no trust, no middleman" is not true before phase two and is removed now, not at testnet (ledger overclaims list, items 9 and 38).
## 10.9 Parameters in this section
| Parameter | Value | Label |
|---|---|---|
| Default mode | Checkpoint mode | Designed |
| Header size | 286 bytes fixed fields plus parents, 400 working figure | Implemented (fields), Designed (total), Open O-10.1 |
| Certificate size | 268 bytes plus one bit per voter | Implemented (section 3.10) |
| Wrapped proof record | 400 bytes working figure | Designed, approximate, Open (R4, ledger P3) |
| Checkpoints per day | 2,880 | Designed (C1 at 1 BPS) |
| Bytes per day, checkpoint mode, 1,000 voters | 3.44 MB | Designed, derived |
| Bytes per day, phase two | 2.30 MB, or 800 bytes per open | Designed, derived |
| VDF proof | 516 bytes, 4.47 ms verify | Measured (section 4.4) |
| Lottery verify per header | 0.441 ms on one M5 Max core | Measured (section 1.11) |
| Seed list | at least 8 pinned nodes, IP literals, in the release | Designed |
| `M`, `N` | 5, 3 | Designed, Open O-10.5 |
| Local peer set | up to 64 | Designed |
| Browser transport | `wss` to named hosts | Designed; the DNS dependency is stated |
## 10.10 Open items
| Id | Item | What closes it | Gate |
|---|---|---|---|
| O-10.1 | Header bytes on the wire at Igneum parameters, and the bytes of the bodies' finality sections a full-header client needs for Q2, are estimates | Record both over a day of the phase 3 devnet at 1 BPS; replace the 400-byte figure and fill the full-header row | 2 |
| O-10.2 | A checkpoint-mode client takes the voter set from nodes. A header field committing to the canonical voter list with weights and participation at that block (computable by every node from the block's past, like `pruning_point`) would let the client verify the set from the checkpoint header and a Merkle proof, without the consensus proof | Decision at gate 3 (consensus engineer and cryptographer): the cost is one more 32-byte header field and the incremental weight window section 3.10 already needs for mainnet; the alternative is to wait for phase two | 3 |
| O-10.3 | BLS aggregate verification time on a phone CPU and in WebAssembly in a browser, at 1,000 and 10,000 voters | Measure `fast_aggregate_verify` with the forked `blst` build on an iPhone and an Android phone, and a WebAssembly BLS12-381 library in Chrome and Safari; record milliseconds per certificate | 3 |
| O-10.4 | Full-header mode on a phone: 256 MiB cache in RAM, 0.2-s daily fill, 38 s of CPU per day of headers, all from the M5 Max figures; nothing on a phone or in a browser | Run the CPU verifier on a mid-range phone and in WebAssembly against one day of devnet headers; record RAM, time and battery; decide whether the mode ships on phones, in browsers, or on neither | 3 |
| O-10.5 | `M = 5, N = 3` for the voter-set agreement is a guess | Re-run the devnet eclipse test of O-3.7 with a light client among the eclipsed; find the smallest `N` at which the eclipsed client reports "voter set disputed" rather than a wrong set | 3 |
| O-10.6 | The trusted checkpoint's refresh: how old a shipped checkpoint may be before the client refuses to start from it, and how the release carries a new one | Decision with section 8's release process: candidate, no older than one presence window of 240 indices plus one release cycle; the client refuses an older one and asks for an update | 3 |
| O-10.7 | The wrapped proof's size and verification time on a phone and in a browser (ledger P3, design document R4) | The phase 2 benchmark: wrap a segment proof to Groth16 and Plonk on a 12 GB and a 24 GB card, record proof bytes and verification time on a phone and in WebAssembly | phase 2 |
| O-10.8 | The consensus proof's statement, its cost and its latency behind the checkpoint are unwritten (design document, section 7, phase two) | The phase two design: a zkVM program over the window's headers and certificates with the W2, Q2 and Q3 rules; cost on consumer hardware; the latency added to "locked" | phase 2 |
| O-10.9 | The node identity key used for pinning is a forward reference to the fork's p2p identity | Name the key and its encoding in `docs/fork-divergence.md` at the first seed-list release | 2 |

View file

@ -13,6 +13,8 @@
| 6 | `06-open-items.md` | Open | 60 items, each with the experiment or decision that closes it and its gate; O-2.8 closed and O-3.3, O-3.7, O-5.1, O-5.2 and O-5.6 narrowed on 3 October 2026 |
| 7 | `07-execution.md` | Designed (normative extract of `docs/design/execution-layer.md`; nothing implemented or measured) | EVM semantics on the DAG (number, timestamp and its monotonicity rule, blockhash, PREVRANDAO from the epoch VDF, coinbase, gas limit, chain id, the quoted gas price), shard assignment by sortition (8 provers, 10-s window, then open, no bond), bridges (none official, no bridged stablecoins at genesis, proof bridge with the consensus proof in phase two) |
| 8 | `08-client-security.md` | Decided (3 October 2026; nothing implemented) | Reproducible builds with hashes in the repository, the release key published in genesis and held in hardware, the client refuses unsigned updates and never updates silently, the client cannot change consensus (90% signalling), notarised builds, official download sources with the hash beside the button, seed shown and confirmed before mining, hardware wallet option, the permanent line "Nobody from Igneum will ever ask for your seed." |
| 9 | `09-pool-protocol.md` | Designed (nothing implemented) | Pool and member after Stratum V2's job declaration: full, commitment or member-declared templates checked against the member's own verifier, JSON over TLS, shares as partial solutions of the header-bound lottery hash (`target64 << s`, weight `2^-s`, verified per 32-lane group at the measured CPU cost), one vote key per operator held by the member and never the pool, votes signed by the member and only relayed and carried by the pool, the chain-only test for a pool that drops votes, custodial pools visible on chain, open items O-9.1 to O-9.9 |
| 10 | `10-light-client.md` | Designed (nothing implemented) | What a phone or browser verifies without a node: trust assumptions stated per row, checkpoint headers plus BLS certificates with the voter bitmap and the wrapped segment proof at launch, the consensus proof in phase two, bytes per day per mode (3.44 MB in checkpoint mode at 1,000 voters, approximate), a pinned seed list with no DNS dependency on the phone and the stated `wss` dependency in a browser, the read-only node API, the homepage card's steps made true, open items O-10.1 to O-10.9 |
Prototype values (carried by the implementation today, part of the test vectors, confirmed or replaced at gate 1): seed derivation; 64 instructions x 8 iterations; 8 registers; the op weights including the 25% load weight; the output fold rotations; the 256 MiB cache, 64 lines per segment and ChaCha12; 8 item rounds and the mixer shape; the 1 GiB pack dataset against the 2 GiB genesis size; the 3,600 DAA s epoch; the era length, draw bounds and reserve list; the header binding of the init words. The full table with what fixes each is section 1.16.