igneum/docs/design/execution-layer.md
igneum-labs 49c1925aef Execution layer design: transaction model, EVM on the DAG, two-dimensional gas, proving interface
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 17:43:42 +00:00

418 lines
51 KiB
Markdown

# Igneum execution layer: design
Design version 0.1, 3 October 2026. Status: Designed. Nothing in this document is implemented or measured unless the sentence says so.
Owner: execution engineer. Reviewers: consensus engineer (sections 1 and 3), cryptographer (sections 5 and 6), miner-community lead (section 4).
This document takes decisions. Each section states the decision, the reason, and the rejected alternative in one line. Numbers follow the labels of `docs/spec/00-overview.md` section 0.2: Measured, Implemented, Designed, Target, or approximate. Line numbers into `vendor/rusty-kaspa` are from commit `01b532e8` (v2.1.0, 22 Sep 2026), the base in `docs/fork-map.md`. Claims about other chains cite a source or say approximate.
What this document rests on, fixed by the design document and CLAUDE.md and not reopened here: blocks carry transactions only and make no claim about state; every node executes natively; every block is proven by miners in shards and aggregated 20 to 60 s behind the tip at launch; base fee burned in full on both gas dimensions; priority fee split 65/15/15/5 with the developer share per call frame; proving pool paid per block as a fixed amount divided by consensus proving cost; SP1 behind a swappable interface; no stake, no other chain in consensus; transactions public.
## Decisions in one table
| # | Decision | Section |
|---|---|---|
| D1 | A block body is a list of Ethereum typed transactions (types 0, 1, 2). Kaspa's UTXO transaction type is removed, not wrapped. No coinbase transaction; rewards are a state transition applied by rule | 1.1 |
| D2 | The canonical sequence is the selected chain. Segment(C) for chain block C = transactions of C's mergeset without the selected parent, in GHOSTDAG ascending blue-work order (reds included), then C's own transactions. This is Kaspa's `consensus_ordered_mergeset` with the segment boundary moved by one block; the global order is identical | 1.2 |
| D3 | Duplicate inclusion: first copy in the sequence executes and pays; later copies are skipped by the nonce rule, cost the including miner block space and a fixed intrinsic proving charge, and are left out of the proof statement's executed set | 1.3 |
| D4 | Nonces are Ethereum nonces. Within one block a sender's transactions are nonce-sorted and nonce-contiguous (body rule). Across parallel blocks the sequence decides | 1.4 |
| D5 | Two failure classes. State-free faults (encoding, signature, chain id, intrinsic gas above limit, body rules) make the block invalid. State-dependent faults (nonce, balance, fee cap below base fee, budget exceeded) skip the transaction with no receipt and no fee | 1.5 |
| D6 | revm executes. State is an Ethereum Merkle Patricia trie with keccak, reth's layout. The state root after each segment is an output of execution and of the proof, not a header field | 2 |
| D7 | The header drops `utxo_commitment` and `accepted_id_merkle_root`. It keeps `hash_merkle_root` over the body and adds `proofs_root` over the proof records the body carries | 2.2 |
| D8 | block.number = selected-chain height of C (contiguous). block.timestamp = max(parent segment timestamp, C.timestamp div 1000), non-decreasing. blockhash(n) = hash of chain block n within 256. PREVRANDAO = keccak(epoch seed, number). coinbase = miner address of the block that first included the executing copy. Cancun opcodes, no blob transactions | 3 |
| D9 | Gas has two dimensions: execution gas on Ethereum's schedule, unchanged; proving gas (pgas) on a per-opcode and per-precompile table calibrated in reference prover kilocycles. One transaction gas limit; the wei budget `gas_limit x max_fee_per_gas` caps both dimensions. Base fee per dimension, EIP-1559 adjusted, burned | 4 |
| D10 | Developer attribution per call frame by execution gas, to the registration of the account whose code runs; registration by the deployer at deployment through a system registry; created contracts inherit the creator's registration unless re-registered in the same transaction; unregistered frames' 15% is burned | 4.5 |
| D11 | Shards are cut from the native execution trace by pgas, never splitting a transaction; continuations for one transaction above the shard budget. Sortition assigns each shard to 8 eligible provers for a 10 s exclusive window, then open. No claim bond in-chain; the bond is for the external job market only | 5 |
| D12 | A full node rejects a block whose proof record disagrees with its own native execution (the native-execution veto). Proof system versioned in consensus, three-month overlap, 90% signalling, latest proof wrapped once at the switch | 5.5 |
| D13 | Native proving precompile at a fixed system address: request now, result delivered by a later proof record that runs a callback; paid by the requester with the external-job split | 6 |
| D14 | Light clients at launch verify the execution proof chain from a certificate they are given. Phase two adds the consensus proof | 7 |
| D15 | Chain id 4461 mainnet, 4462 testnet, 4463 devnet. Standard `eth_*` surface; chain blocks are the RPC blocks; `igneum_*` exposes the DAG, skipped transactions and the executed, proven, locked status | 8 |
## 1. Transaction model
### 1.1 Block body
Decision. A block body is `Vec<Bytes>`: RLP-encoded Ethereum typed transactions (EIP-2718), types 0 (legacy), 1 (EIP-2930) and 2 (EIP-1559), each signed with the chain id. Type 3 (EIP-4844 blob) transactions are rejected at body validation. Type 4 (EIP-7702) is deferred to phase two. Kaspa's `Transaction` struct with inputs, outputs, subnetwork id and payload is removed from the body entirely; nothing is wrapped inside a UTXO payload. The body also carries a `proofs` section (section 5.4) and a 20-byte `miner` address replaces the coinbase payload's script public key.
There is no coinbase transaction. Kaspa builds and checks one per chain block (`consensus/src/pipeline/virtual_processor/utxo_validation.rs:272 to 287`, `verify_coinbase_transaction`) whose outputs pay the mergeset's blue miners. Igneum applies the same payouts as a state transition inside the segment, before the segment's transactions, computed from consensus data alone: subsidy to the `miner` address of every blue block merged by C, and fee shares as section 4.4 defines. The proving pool for segment N is credited when a proof record for segment N is included, to the provers that record names (section 5.4). A block therefore commits to nothing it would have to execute first.
Reason. Wallets, Hardhat, Foundry and Blockscout all speak Ethereum transaction encoding; any wrapper breaks `eth_sendRawTransaction` and `eth_getTransactionByHash`. A coinbase transaction would be a state claim in the block (its outputs depend on fees, which depend on execution), which the design forbids.
Alternative. Keep Kaspa's transaction type with the EVM call in `payload` and `subnetwork_id` as a lane, the Kasplex and Igra pattern. Rejected because it doubles every signature and adds a UTXO layer nobody uses.
### 1.2 Canonical sequence
The real primitive. rusty-kaspa orders a block's mergeset as the selected parent first, then every other mergeset block (blues and reds together) in ascending blue work, ties broken by hash (`consensus/src/model/stores/ghostdag.rs:116 to 136`, `ascending_mergeset_without_selected_parent`; `:175 to 180`, `consensus_ordered_mergeset`; `consensus/src/processes/ghostdag/ordering.rs:38 to 42`, `Ord for SortableBlock`). `calculate_utxo_state` (`utxo_validation.rs:103 to 169`) replays the selected parent's transactions then the merged blocks' transactions in that order against a UTXO view that accumulates as it goes, and a transaction that fails in that view is dropped by `filter_map(.. .ok())` (`:288 to 303`), not treated as a block fault. Reds are replayed too; they are only unpaid.
Decision. Let the selected chain be C_0 (genesis), C_1, ... Define for every chain block C:
```
M(C) = consensus_ordered_mergeset(C) minus the selected parent
(ascending blue work, hash tie-break, blues and reds)
segment(C) = concat(txs(B) for B in M(C)) ++ txs(C)
sequence = segment(C_0) ++ segment(C_1) ++ ...
```
Every block's transactions appear in exactly one segment: a chain block ends its own segment, a non-chain block is merged by exactly one chain block. This is the same global order as Kaspa's: Kaspa's segment for C is `txs(P) ++ txs(M(C))` with P the selected parent, so the concatenation across the chain is identical, with the boundary moved by one block.
Reason for moving the boundary. Kaspa puts the selected parent first because the header of C commits to the UTXO state after the mergeset and before C's own transactions. Igneum headers commit to no state, so a segment can end with C's own transactions, and then an RPC block N holds exactly the transactions of chain block N and the blocks it merged. One rule for users: your transaction executes in the segment of the chain block that merged the block it was first included in, or of that block itself if it became a chain block.
Alternative. Kaspa's boundary unchanged. Rejected only for RPC clarity; the global order is the same either way.
Reorgs. The selected chain can change below the tip until the merge-depth bound of 3,600 s (fork map e1) and in practice within seconds. A node keeps per-segment execution results keyed by chain block hash and re-executes from the first changed chain block. The virtual (`processor.rs`, the tip-of-DAG virtual block) has a segment `M(virtual)` with no own transactions; a node executes it eagerly so a user sees state about one second after inclusion.
### 1.3 Duplicate inclusion
Two miners pull the same transaction from the mempool into parallel blocks. Both blocks are merged; the transaction hash appears twice in the sequence.
Decision. The first copy in the sequence executes and pays. Every later copy is skipped by the nonce rule of section 1.5 (the nonce has moved on), has no receipt, pays no fee, and the proof statement's executed set does not contain it. The including miner pays in block space: the duplicate's bytes count against the block's byte limit and the block's execution gas limit (sum of gas limits, section 4.3), and a fixed intrinsic pgas charge per included transaction (section 4.2) counts against the block's proving budget whether or not the transaction executes. So a block full of duplicates earns nothing and still costs its miner the whole block.
Why "skipped by nonce" is enough. Two copies of one transaction have one sender and one nonce. The second copy fails the nonce check with one account read, so the deduplication is proven by the same cheap check that proves every skip, and no separate "seen hashes" set has to be kept in state. A different transaction from the same sender with the same nonce (a replacement) is handled identically: whichever copy comes first in the sequence wins.
Within one block Kaspa rejects duplicate transaction ids and double spends as body faults (`consensus/src/pipeline/body_processor/body_validation_in_isolation.rs:126 to 133`, `:152 to 160`). Igneum keeps both as body rules: duplicate hash in one block, or two transactions of one sender with one nonce in one block, make the block invalid (section 1.4).
### 1.4 Nonce handling across parallel blocks
Decision. Ethereum nonce semantics, unchanged: a transaction executes only when its nonce equals the sender's account nonce at its position in the sequence. Body rule, checkable without state: within one block, a sender's transactions are sorted by nonce and contiguous. Mempool policy, not consensus: a node admits at most 16 queued transactions per sender and a nonce gap of at most 16 (Designed, parameter open).
Worked example. Sender S has nonce 5. Miner A's block carries S:5, S:6. Miner B's parallel block carries S:5, S:7. Order A then B: S:5 executes, S:6 executes, B's S:5 is skipped (nonce is 7), B's S:7 executes. Order B then A: S:5 executes, S:7 is skipped (nonce is 6), A's S:5 skipped, S:6 executes; S:7 waits in the mempool and executes in a later block. Every node reaches the same result because the order is consensus data.
Consequence for users. A transaction can be skipped in one block and execute in a later one without being re-broadcast, as long as a miner includes it again; the node's mempool re-queues a skipped transaction once (then drops it). `eth_getTransactionReceipt` returns null until the executing copy lands, as on Ethereum for a pending transaction.
Alternative. Per-block nonces or sequence-independent nonces (Sui-style objects). Rejected: every wallet assumes Ethereum nonces.
### 1.5 Invalid transactions are skipped by rule
Kaspa's rule for merged blocks is that a transaction invalid in context is dropped and the block stays valid (`utxo_validation.rs:288 to 303`). For the chain block's own transactions Kaspa is stricter: all must be valid or the block fails chain qualification (`:244 to 254`, `InvalidTransactionsInUtxoContext`). Igneum cannot keep that stricter rule, because a block producer does not know the state its block will execute against (its segment position depends on blocks it has not seen).
Decision. Two classes.
| Class | Checked | Examples | Effect |
|---|---|---|---|
| State-free fault | At body validation, before GHOSTDAG, no state needed | Malformed RLP, bad signature, wrong chain id, type 3 or 4 transaction, intrinsic gas above the transaction's gas limit, sum of gas limits above the block's execution limit, body bytes above the limit, duplicate hash in block, same sender and nonce twice in block, nonces not sorted per sender | Block invalid, never merged |
| State-dependent fault | At execution, at the transaction's position in the sequence | Nonce mismatch (including duplicates), balance below `gas_limit x max_fee_per_gas + value`, `max_fee_per_gas` below the execution base fee at that position, `max_priority_fee` above `max_fee`, sender is a contract (EIP-3607), the block's remaining execution or proving budget cannot hold the transaction's limit | Transaction skipped: not executed, no receipt, no fee, no nonce change. Later transactions in the block are still tried |
A transaction that executes and reverts is a normal failed transaction: receipt with status 0, gas paid, nonce incremented.
The skip check costs a bounded, fixed amount of proving work (one account read, one comparison per rule), which section 4.2 charges to the block as the intrinsic pgas per transaction. That is the whole defence against a block stuffed with unexecutable transactions: the miner buys block space and proving budget and gets nothing back.
Alternative. Make a chain block's own transactions strict as Kaspa does. Rejected above: the producer cannot see its segment position.
## 2. State and execution
### 2.1 Executor
Decision. revm (bluealloy/revm) is the EVM, driven by an Igneum block executor that feeds it the sequence of section 1.2 with the environment of section 3 and the two-dimensional gas of section 4. State is an Ethereum Merkle Patricia trie with keccak-256 keys and RLP accounts, in reth's database layout, so `eth_getProof` and every Ethereum tool that reads state work unchanged. The executor is a library with no network dependency, used by the node, the shard planner, the prover's witness generator and the differential test.
Reason. revm is what reth, rsp and SP1 Reth run; using it both natively and inside the zkVM makes the proof statement "the same code, run twice" and shrinks the surface where native and proven execution can disagree. The MPT is the layout rsp proves today (approximate; verify against `vendor/rsp` once cloned).
Alternative. A prover-friendly trie (binary, Poseidon or Blake3 keyed) for cheaper state proofs. Deferred to phase two with a measurement: trie-proof pgas share of a representative segment. If it is above 30% the trie changes behind the same executor interface.
### 2.2 State root as an output, and what the header commits to
Decision. Execution of segment(C) produces: the executed set (which transactions ran), receipts, the state root `root(C)` after the segment, per-shard intermediate roots (section 5.1), and per-block gas and pgas totals. None of this goes in C's header. `root(C)` is the public output of the proof of segment(C) and appears on chain only inside a proof record (section 5.4) carried by a later block.
The header keeps Kaspa's `hash_merkle_root` over the body's transactions and `miner` address, and replaces `accepted_id_merkle_root` and `utxo_commitment` (header.rs:154 to 172) with one field, `proofs_root`, the Merkle root of the proof records in the body. The vote key is added as the fork map's point d. Kaspa's `accepted_id_merkle_root` in v2.1.0 carries the KIP-21 sequencing commitment (`utxo_validation.rs:462 to 490`); Igneum does not keep it, because its purpose (letting an L2 read the accepted order from the header) is served by proof records.
Reason. A header field that depends on execution is a claim the producer can get wrong, which was hostile-review finding 1; dropping both fields makes the header a pure function of the DAG and the body.
Alternative. Keep `utxo_commitment` as the state root, Kaspa-style, with the block invalid when wrong. Rejected by the design document: an unprovable block must not exist.
### 2.3 Executed, proven, locked
What each word means to a user, a wallet or an exchange.
| Status | When | Who vouches | Can it be undone | Who should act on it |
|---|---|---|---|---|
| Executed | A full node has placed the transaction in a segment of its selected chain and run it, about 1 s after inclusion | That node's own execution | Yes, by a selected-chain reorg, bounded by merge depth 3,600 s and in practice seconds | Wallets showing a balance, games, anything that can tolerate a rare rollback |
| Proven | A proof record for the segment is included in a block, 20 to 60 s behind the tip at launch (Target) | Mathematics, for the execution; the DAG, for the order | The execution cannot be wrong; the segment can still be reorged out with its proof | Light clients and bridges reading state, together with a lock |
| Locked | A certified checkpoint (finality rule v2) covers the chain block, about 90 to 120 s after the transaction (Designed) | Two thirds of active 30-day miner weight | Not without two thirds of active weight equivocating | Exchanges crediting deposits, bridge withdrawals, anything irreversible |
A bridge withdrawal waits for proven and locked. `igneum_getTransactionStatus` returns the three flags (section 8.2).
## 3. EVM semantics on a DAG
The environment revm sees is built from the chain block C whose segment is executing. Ported contracts get this table; it is the "documented differences" the FUD ledger entry P5 promises.
| Opcode or field | Igneum value | Ethereum value | Note for ported contracts |
|---|---|---|---|
| `block.number` (NUMBER) | Selected-chain height of C: genesis 0, each chain block +1, contiguous | Block height | About one per second but not fixed; use timestamps for time. Blue score and DAA score are readable from the system contract `IgneumInfo` |
| `block.timestamp` (TIMESTAMP) | `ts(C) = max(ts(parent chain block), C.timestamp_ms div 1000)` | Header timestamp, strictly increasing | Non-decreasing, equal values happen at one block a second. Kaspa only requires a header timestamp above the past median time of a 27-sample window and below now plus 132 s (`constants.rs:23 to 30`; `post_pow_validation.rs:20 to 24`; `pre_ghostdag_validation.rs:38 to 42`), so without the max rule time could step backwards |
| `blockhash(n)` (BLOCKHASH) | Hash of chain block n for `number - 256 <= n < number`, else 0 | Same | Hashes of merged non-chain blocks are not reachable from the EVM; `IgneumInfo` exposes the mergeset of a chain block |
| `block.prevrandao` (PREVRANDAO, and DIFFICULTY) | `keccak256(epoch_seed ‖ number)`, where `epoch_seed` is the 10-minute class-group VDF output for the epoch that contains C (spec section 4) | RANDAO mix | Unbiasable by the producer but predictable for the whole epoch (about 1 h) by anyone who has computed the VDF. Fine for a game's shuffle revealed later, wrong for a per-block lottery. Use the proving precompile with a committed input for anything adversarial |
| `block.coinbase` (COINBASE) | `miner` address of the block that first included the executing copy | Fee recipient | Also the address that receives the 65% share, so the correspondence Ethereum contracts assume holds. For a transaction in a red block it is still the red block's miner, who is paid the share (section 4.4) |
| `block.gaslimit` (GASLIMIT) | Per-block execution gas limit, a consensus constant | Header gas limit | A segment can hold up to the mergeset limit of blocks (180 at 1 BPS, `bps.rs:75 to 86`), so a segment's total can exceed `gaslimit` |
| `block.basefee` (BASEFEE) | Execution base fee at this segment (section 4.3) | Same | The proving base fee is readable from `IgneumInfo` |
| `block.chainid` (CHAINID) | 4461 | 1 | |
| BLOBHASH, BLOBBASEFEE | Zero and 1 respectively; no blob transactions | EIP-4844 | As on L2s without blobs |
| Opcode set | Cancun (PUSH0, TSTORE, TLOAD, MCOPY, SELFDESTRUCT per EIP-6780) | Cancun or later | EIP-7702 and Prague BLS precompiles deferred to phase two after pgas calibration |
| Precompiles | 0x01 to 0x09 (ecrecover, sha256, ripemd160, identity, modexp, ecadd, ecmul, ecpairing, blake2f). 0x0a point evaluation absent (returns failure) | Cancun has 0x0a | Heavy precompiles cost more here because pgas is charged (section 4.2) |
Precedents. Conflux eSpace defines NUMBER as the Tree-Graph epoch number (the pivot chain index) and documents that it is not a clock, widens BLOCKHASH to 65,535 blocks, and returns a Core Space address for COINBASE (doc.confluxnetwork.org/docs/espace/build/evm-compatibility). Conflux also defers execution five epochs so the order settles before state is computed (doc.confluxnetwork.org/docs/general/build/node-development/consensus-design). Igneum executes at once and re-executes on reorg instead, because headers commit to nothing. Kasplex and Igra are based rollups on Kaspa that read the accepted-transaction order from L1 and execute it in an L2 node (docs-kasplex.gitbook.io/l2-network; igralabs.com, chain id 38833 on chainid.network); their block-number and timestamp definitions are not published in the pages read, approximate.
Alternative for `block.number`. Blue score of C. Rejected: it jumps by the mergeset's blue count, so `eth_getBlockByNumber` would have gaps and Blockscout would show missing blocks.
## 4. Gas
### 4.1 Two dimensions
Decision. Every transaction is metered twice.
| Dimension | Unit | Schedule | Limit | Base fee |
|---|---|---|---|---|
| Execution gas | gas | Ethereum's, unchanged (revm's Cancun table) | Transaction `gas_limit`; block `B_e` | `f_e`, EIP-1559 adjusted toward a target of `B_e / 2` per block |
| Proving gas | pgas, 1 pgas = 1,000 reference-prover cycles, rounded up (Designed unit) | Igneum table, section 4.2 | Implicit per transaction through the wei budget; block `B_p` | `f_p`, EIP-1559 adjusted toward `B_p / 2`, smoothed over the difficulty window as the design document requires |
Charge for a transaction: `gas_used x (f_e + tip) + pgas_used x f_p` wei, where `tip = min(max_priority_fee, max_fee - f_e)`. The wei budget `gas_limit x max_fee_per_gas` must cover the charge; if during execution the running charge would exceed the budget, the transaction halts as out of gas, reverts, and pays what it consumed up to the budget. `gas_limit` alone still bounds execution gas. Both base fees are burned in full; the tip splits as section 4.4.
Why one gas limit and a wei budget. Ethereum transactions carry one gas limit and wallets sign nothing else. zkSync folds its second dimension (pubdata) into one gas number through `gasPerPubdata = fairPubdataPrice / baseFee` (docs.zksync.io, fee model), and Polygon zkEVM caps its counters per batch rather than pricing them (0xPolygon/zkevm-rom, `docs/opcode-cost-zk-counters.md`). Igneum prices both and lets the signed budget cap both, so `eth_estimateGas` returns a gas limit that, at the quoted `eth_gasPrice`, covers the two charges, and `eth_gasPrice` quotes `f_e + f_p x (pgas_est / gas_est) + tip`. Unmodified wallets keep working. A transaction heavy in pairing or modexp will see a higher quoted price than on Ethereum, which the ledger entry P5 already states.
Alternative. A second limit field in a new transaction type. Rejected until wallets support it; it can be added later as a type with the same charging rule.
### 4.2 The pgas table
Decision. pgas is charged per opcode and per precompile from a consensus table, calibrated by running each opcode and precompile in isolation in the reference prover (SP1, section 5.6) and taking cycles per invocation and per byte, rounded up to the next 1,000 cycles. The table is a consensus constant with a version, re-measured at each proof system change. Entries that exist from day one, with their shape:
| Item | Charge shape | Precedent (approximate, from the sources named) |
|---|---|---|
| Intrinsic, per included transaction | Fixed, charged to the block even when skipped | Covers signature recovery and the skip checks; secp256k1 recovery is an SP1 precompile that cuts cycles 5 to 10x against pure RISC-V (blog.succinct.xyz, SP1 is live) |
| Plain opcodes (arithmetic, stack, control) | Fixed per opcode, mostly 1 pgas | Polygon zkEVM: ADD 1 binary counter, MUL 1 arith counter |
| KECCAK256 | Fixed plus per 136-byte block | Polygon zkEVM SHA3: 192 arith, 193 binary, 2 mem_align, 2 keccak_f, 10 poseidon per call |
| SLOAD, SSTORE, BALANCE, EXTCODE*, CALL to a cold account | Fixed per trie access, the largest per-op entry | Polygon zkEVM: 11 poseidon per SLOAD or SSTORE (their trie is Poseidon; an MPT keccak path is costlier, to be measured) |
| MLOAD, MSTORE, memory expansion, CALLDATACOPY, CODECOPY, LOGn | Per 32-byte word | Polygon zkEVM charges copy and log opcodes by bytes |
| EXP | Per byte of exponent | Polygon zkEVM: up to 512 arith plus 1,025 binary |
| Precompiles ecrecover, sha256, ripemd160, blake2f | Fixed plus per block of input | SP1 precompiles for keccak, sha256, secp256k1 |
| modexp | Per limb-multiplication, from input sizes | No zkVM precompile; pure RISC-V big-number arithmetic, expected to be the most expensive entry per gas |
| ecadd, ecmul, ecpairing | Fixed per operation, per pair for pairing | SP1 bn254 precompiles: Groth16 verification fell from 174 M to 9.4 M cycles (blog.succinct.xyz, SP1 benchmarks 8/6/24) |
Target, not measured: the table is complete enough that for the ethereum/tests state tests the ratio `pgas / gas` falls inside a band of 0.1 to 10 for 95% of tests. The band is what makes the single-gas-limit fold of section 4.1 usable. Phase 2 measures it.
### 4.3 Per-block budgets
Decision. `B_e` and `B_p` are consensus constants per block, and the bound is enforced at execution: a transaction whose `gas_limit` or whose running pgas would push its block's totals above `B_e` or `B_p` is skipped (section 1.5). Body validation already rejects a block whose sum of gas limits exceeds `B_e`, so execution gas is bounded without state; pgas is not bounded without state, so the execution-time skip is the binding rule for it. A segment of k blocks can carry up to `k x B_p`, bounded by the mergeset limit, and the proving pool for the segment is divided by the segment's consensus pgas (section 4.4), so a burst of blocks earns the provers no more per block.
`B_p` at launch is set from measured prover throughput, the formula the ledger entry P2 asks for:
```
B_p = (cards_proving x shards_per_card_per_minute x pgas_per_shard) / (60 x blocks_per_second x 2)
```
with the factor 2 the EIP-1559 target-to-limit ratio. `pgas_per_shard` is the shard size of section 5.1 (Target: one shard in about 20 s on a 12 GB, 3060-class card, the phase 2 gate). `cards_proving` is read from the devnet, then the testnet. The litepaper publishes the formula and the inputs.
Backlog rule. If the oldest unproven segment is more than 600 chain blocks behind the tip (Designed, parameter open), `B_p` for new blocks halves each further 600 blocks until the backlog clears, so unproven state stays bounded. Execution and locks never wait on proofs (ledger P9).
### 4.4 Fee flows
| Flow | Share | Recipient | When credited |
|---|---|---|---|
| Execution base fee `f_e x gas_used` | 100% | Burned | At execution |
| Proving base fee `f_p x pgas_used` | 100% | Burned | At execution |
| Tip `tip x gas_used` | 65% | The `miner` of the block that first included the executing copy, red or blue | At execution |
| Tip | 15% | Developer registrations, per call frame (section 4.5); unregistered frames' share burned | At execution |
| Tip | 15% | Burned | At execution |
| Tip | 5% | Development fund contract | At execution |
| Subsidy | 80% of emission | Blue blocks' `miner` addresses, red blocks unpaid (Kaspa's rule) | In the segment that merges the block |
| Proving pool | 20% of emission for the proven segment, divided among its shards by consensus pgas, with 10% of the pool to the aggregator (Designed, parameter open) | Provers named in the proof record | In the segment that includes the proof record |
The red-block miner keeps the 65% tip share because the sequence executed its transaction; red-ness is a GHOSTDAG verdict on the block's position, not on its contents. Alternative: tip share to the merging chain block's miner. Rejected: it rewards chain blocks for merging red blocks, which they do not choose.
### 4.5 Developer attribution
Decision. During execution the executor keeps a per-frame counter of execution gas consumed by the frame's own opcodes (not its sub-calls). At the end of the transaction the 15% developer share is divided among frames in proportion to that gas, and each frame's part goes to the registration of the account whose code ran in that frame: for CALL and STATICCALL the callee, for DELEGATECALL and CALLCODE the code address (the implementation behind a proxy). Precompile frames and EOA transfers have no registration; their share is burned.
Registration lives in a system contract `DeveloperRegistry` at a fixed address, populated by rule on CREATE and CREATE2: the new contract's registration is copied from its creator's registration if the creator has one (a factory passes its payee to everything it deploys), and the creating transaction may overwrite it by calling `DeveloperRegistry.register(newContract, payee)` before it ends. A deployer EOA sets its own default payee once with `register(self, payee)`; a contract can change its own registration by calling `register(address(this), payee)` from its own code. Nobody else can.
Self-dealing (ledger E3). The base fees are burned, so a developer who spams its own contract loses 100% of two base fees and 85% of the tip to recover 15%. A developer who also mines the including block recovers 80% of the tip and still loses 20% of the tip plus both base fees. No positive-expectation loop exists. The share does inflate "gas earned by app" on a leaderboard at that cost, so the explorer must not rank apps on raw developer share; section 8.4 asks Blockscout's ranking to exclude transactions whose sender, coinbase and payee coincide.
Alternative. Attribute by the proxy address rather than the code address. Rejected: it lets a proxy claim a library author's work; the code address is what actually ran.
## 5. Proving interface, forward design for v2
Phase 2 measures; this section fixes the interface the measurement is built against so the devnet can run it.
### 5.1 Shard plan from native execution
The native executor emits, per segment, a trace of transaction boundaries with cumulative gas and pgas, and the state root at every transaction boundary (incremental MPT root maintenance, as reth does per block). The shard planner cuts the segment at transaction boundaries so each shard's pgas is at most `S_p`, the shard budget (Target: about 20 s on a 12 GB card, the phase 2 gate; the earlier "under 5 s chunk" figure in the design document's build-plan diagram is superseded by `site/journey.json`). A single transaction above `S_p` is one shard proven with continuations: the zkVM checkpoints its own execution and the pieces are aggregated like shards (SP1 proves long programs this way, approximate). The plan is deterministic from the trace, so every node computes the same shard list and shard ids `(segment hash, shard index)`.
Intermediate roots. Shard i's statement is: from pre-root `r_i` and the committed transaction list of the shard, running the executor yields post-root `r_{i+1}` and receipts root `h_i`, with the executed set recorded. Pre- and post-roots come from native execution and are inputs to the prover, outputs of the proof. The witness for shard i (the touched accounts, storage slots and MPT nodes, plus block environment) is generated by any full node from its own execution and served over the proving gossip; it is not consensus data.
### 5.2 Shard assignment
Decision. Sortition, no in-chain bond. For each shard, `VRF(epoch_seed, shard id)` ranks eligible provers (vote keys with at least 100 blue blocks in the 30-day window, the finality-rule population) and the top 8 hold an exclusive 10 s window from the moment the segment is executed. The first valid shard proof included in a block earns that shard's part of the pool; during the window only the 8 are paid, after it anyone is. Proofs are gossiped and any block producer includes them (section 5.4). Nothing waits on an assigned prover, so there is no griefing to bond against (ledger P9), and a datacentre cannot sweep every shard by latency (ledger P8). Parameters 8 and 10 s are Designed, to be set on the devnet from the shard-time distribution.
The bond stays for the external job market, where a customer does wait: a job is claimed with a bond in IGN, slashed on a late or bad proof, under the job contract of section 6.
Alternative. First-come claims with a small bond, the design document's original line. Rejected for P8 and P9 above.
### 5.3 Aggregation
Shard proofs for a segment fold into one segment proof by recursion (SP1's compress stage, approximate), the aggregator being anyone, paid the aggregator share. The segment proof for N also verifies the segment proof for N-1, so one proof attests the whole chain of state from genesis and a client needs only the latest (the design document's "proofs chain from genesis"). For the on-chain bridge verifier and for light clients the segment proof is wrapped once into a curve-based proof (Groth16 or Plonk over bn254); the cost and latency of that wrapper on consumer hardware is the open measurement of ledger entry P3 and belongs in the phase 2 benchmark next to the shard time.
### 5.4 Proof records in blocks
A block body's `proofs` section carries zero or more proof records:
```
ProofRecord {
version: u16, // proof system version, section 5.6
segment: Hash, // chain block hash whose segment is proven
pre_root: B256,
post_root: B256,
receipts: B256,
provers: Vec<(VoteKey, shard_index)>, // who is paid
aggregator: VoteKey,
proof: Bytes, // compressed recursive proof
}
```
Full nodes verify `proof` natively (a compressed SP1 proof verifies in well under a second on a CPU, approximate, to be measured) and run the native-execution veto of section 5.5. `proofs_root` in the header commits to the list. A proof record for a segment that already has one is ignored, not a fault. Records are what credits the proving pool (section 4.4).
### 5.5 The native-execution veto
Decision. A block is invalid if any proof record it carries has `post_root` or `receipts` different from the node's own native execution of that segment, or fails cryptographic verification, or names a segment not on the node's selected chain. The record is a claim about an earlier segment that every full node can check by running it, so this is not a state claim the producer must execute to make; it is a claim the producer can check before including. A forged proof from a soundness bug (ledger P7) is therefore rejected by every full node, and the damage is limited to light clients that saw it before a full node did. The emergency path for a live soundness bug is human: a release that bumps the proof system version with a shortened overlap, activated by miner signalling, and the litepaper says so.
### 5.6 The versioned prover trait and the swap procedure
```rust
pub trait ProofSystem: Send + Sync {
const VERSION: u16;
type ShardProof; type SegmentProof; type WrappedProof;
fn program_id(&self) -> B256; // hash of the executor guest
fn prove_shard(&self, w: &ShardWitness) -> Result<Self::ShardProof>;
fn aggregate(&self, prev: Option<&Self::SegmentProof>, shards: &[Self::ShardProof]) -> Result<Self::SegmentProof>;
fn wrap(&self, p: &Self::SegmentProof) -> Result<Self::WrappedProof>;
fn verify_segment(&self, p: &Self::SegmentProof, claim: &SegmentClaim) -> bool;
fn verify_wrapped(&self, p: &Self::WrappedProof, claim: &SegmentClaim) -> bool;
fn pgas_table(&self) -> &PgasTable; // calibrated for this version
}
```
Version 1 implements the trait with SP1 (Hypercube class, hash-based, so consumer cards prove without elliptic-curve MSM). Alternative implementations are RISC Zero or OpenVM; the trait exists so that choice is a release, not a redesign.
Swap procedure, fixed by the design document and spelled out here:
| Step | Rule |
|---|---|
| 1 | A release ships version N+1 behind the trait with its own pgas table and a test-vector set: 1,000 segments proven under both versions with identical claims |
| 2 | Signalling: blocks carry a readiness bit; when 90% of blocks in a window signal, activation height is set at 3 months ahead |
| 3 | Overlap: from activation, proof records of version N and N+1 are both valid for 3 months; `B_p` is computed from the stricter of the two tables |
| 4 | At the end of the overlap the latest segment proof under N is wrapped once under N+1 (a proof that verifies the N proof) so the chain of proofs continues and light clients keep only the N+1 verifier |
| 5 | Version N verification code is removed in the following release |
### 5.7 Devnet v1 scope
Devnet v1 runs with a stub `ProofSystem` whose `prove_shard` returns a signed claim and whose `verify_segment` checks the signature, so the whole pipeline (trace, shard plan, sortition, records, veto, pool credit) runs on the devnet before SP1 proving is fast enough. The stub is never a mainnet version. Phase 2's SP1 implementation replaces it behind the same trait.
## 6. Native proving precompile
Decision. A system contract `Prover` at address `0x0000000000000000000000000000000000000200` (chosen outside the Ethereum 0x01 to 0x11 and RIP-7212 0x100 ranges; confirm against the RIP registry before devnet) with this interface:
```solidity
interface IProver {
// Submit a job. program is the hash of a registered guest program (any zkVM program
// for the current proof system version), input its public input, maxPgas the proving
// budget the requester will pay for, callback the contract to call with the result.
function request(bytes32 program, bytes calldata input, uint64 maxPgas, address callback)
external payable returns (bytes32 jobId);
function result(bytes32 jobId) external view returns (uint8 status, bytes memory output);
function registerProgram(bytes calldata elf) external returns (bytes32 program);
}
interface IProverCallback {
function onProof(bytes32 jobId, bool ok, bytes calldata output) external;
}
```
Who pays. The requester's `msg.value` is escrowed as the job fee and must be at least `maxPgas x f_p x 1.5` (the 1.5 is the premium that makes a job worth a miner's switch from hashing, Designed, parameter open); the request itself is a normal transaction paying both gas dimensions. On delivery the fee splits by the external-job rule: 10% burned, 5% to the development fund, the rest to the prover; unused fee above the actual pgas is refunded to the requester. A job nobody proves within 3,600 chain blocks expires and refunds in full.
How results return. A prover runs the program, and its job proof is gossiped like a shard proof. A block producer includes it as a `JobRecord {jobId, output, proof}` in the body's `proofs` section. When the segment carrying the record executes, the executor verifies the job proof with the current `ProofSystem` (full nodes cannot re-run arbitrary programs natively, so for jobs the proof is the only check, unlike segments), stores `(status, output)` in `Prover`, and calls `callback.onProof` with a gas stipend of 200,000 execution gas paid from the escrow (Designed). The segment's own proof recursively verifies the job proof, so a light client needs nothing extra. Jobs are assigned by the same sortition as shards.
Alternative. A synchronous precompile that proves inline. Rejected: a proof takes seconds to minutes and cannot sit inside a 1 s block.
## 7. Light clients
At launch (ledger P4, conceded): a light client is given a recent checkpoint certificate out of band (as Ethereum light clients are given a sync-committee checkpoint), checks the BLS certificate against the vote-key set and weights it is also given, then verifies the latest wrapped segment proof whose segment is at or below the certified checkpoint, and reads state through `eth_getProof` against that `post_root`. One wrapped proof verification per update; the wrapper cost is the P3 measurement.
Phase two: a consensus proof, a zkVM program over the 30-day header window and the vote certificates that outputs "this checkpoint is certified under finality rule v2", recursively folded into the segment proof, so the certificate becomes self-verifying and the bridge on Ethereum needs no relayer trust. Costed in the build plan as phase two; nothing in sections 1 to 6 changes for it, because the segment claim already commits to the chain block hash.
## 8. Migration and tooling
### 8.1 Chain id
Decision. 4461 mainnet, 4462 testnet, 4463 devnet. All three are absent from chainid.network's registry on 3 October 2026 (checked with `chains_mini.json`, 2,783 entries). Register the three on ethereum-lists/chains before the public testnet so wallets name the network. Alternative: a vanity id derived from the name; none checked was free and meaningful, and the id has no technical weight.
### 8.2 JSON-RPC surface
Standard namespace, unchanged semantics where the table says so; RPC "blocks" are chain blocks, their transaction list is the executed set of the segment, in sequence order, and skipped transactions are absent from the block but reachable through `igneum_*`.
| Method group | Status | Note |
|---|---|---|
| `eth_chainId`, `eth_blockNumber`, `eth_getBlockByNumber`, `eth_getBlockByHash`, `eth_getTransactionByHash`, `eth_getTransactionReceipt`, `eth_getLogs`, `eth_getBalance`, `eth_getCode`, `eth_getStorageAt`, `eth_getTransactionCount`, `eth_call`, `eth_estimateGas`, `eth_gasPrice`, `eth_maxPriorityFeePerGas`, `eth_feeHistory`, `eth_sendRawTransaction`, `eth_getProof`, `eth_syncing`, `net_version`, `web3_clientVersion` | Supported at devnet v1 | `eth_estimateGas` and `eth_gasPrice` fold pgas (section 4.1). `eth_feeHistory` reports `f_e`; `f_p` in an extra field |
| `eth_subscribe` (newHeads, logs, newPendingTransactions) | Supported | newHeads fires per chain block |
| `debug_traceTransaction`, `debug_traceBlockByNumber`, `trace_block` | Supported, revm inspectors | Needed by Blockscout and Foundry's debugger |
| `eth_getBlockReceipts`, `eth_getUncleBy*` | Supported; uncles always empty | Merged blocks are not uncles; see `igneum_*` |
| `igneum_getDagBlock(hash)` | New | Header, parents, blue or red, merging chain block, transactions with executed or skipped and the skip reason |
| `igneum_getSegment(number)` | New | Mergeset in order, executed set, shard plan, proof record if any |
| `igneum_getTransactionStatus(hash)` | New | `{included_in: [hashes], executing_copy, executed, proven, locked, skip_reason}` |
| `igneum_getProvingFee`, `igneum_getBudgets` | New | `f_p`, `B_e`, `B_p`, backlog depth |
### 8.3 Hardhat and Foundry
Point `--rpc-url` at a node with chain id 4463, deploy the same bytecode, verify in the explorer. Three things to tell a developer in the docs:
1. `eth_estimateGas` includes proving cost; a transaction heavy in `modexp` or `ecpairing` estimates higher than on Ethereum.
2. `block.number` and `block.timestamp` follow section 3; forge tests that assume one block per `vm.roll` still pass because the devnet node mines one chain block per second, but `vm.warp` to a past time is rejected (timestamps are non-decreasing).
3. Register a developer payee: `DeveloperRegistry.register(address(this), payee)` from the deployer, or inherit from a registered factory.
A Hardhat and a Foundry template with these three set up ship with the devnet.
### 8.4 Blockscout
Blockscout indexes through standard RPC and needs: `eth_getBlockByNumber` with contiguous numbers (section 3 gives this), `eth_getBlockReceipts`, `eth_getLogs`, `debug_traceTransaction` with the `callTracer` for internal transactions, and a verified-contract flow (Sourcify or its own, unchanged). Igneum-specific work, in the Blockscout fork: a DAG panel per block (its mergeset from `igneum_getSegment`), a status chip (executed, proven, locked), a "skipped" tab for transactions with a skip reason, the two base fees on the transaction page, and the app leaderboard excluding self-dealing transactions (section 4.5). Nothing in Blockscout's core schema changes.
### 8.5 Differential test plan against reth
| Test | Method | Pass rule |
|---|---|---|
| EVM equivalence | Run ethereum/tests GeneralStateTests (Cancun) through the Igneum executor with pgas charging disabled and the environment of section 3 pinned to Ethereum values | Post-state roots identical to revm's statetest runner |
| Linear-chain equivalence | Generate random transaction sets; build an Igneum DAG with one parent per block (a chain); execute with Igneum; execute the same sequence as Ethereum blocks in reth's block executor with gas limits matched | Identical receipts, logs and state roots per block |
| Order equivalence | Random DAGs from a simnet with 10 parents max; compute the sequence of section 1.2 two ways: Igneum's executor and an independent linearizer written from `ghostdag.rs:116 to 136` that then feeds reth | Identical state roots after every segment |
| Skip rule | Inject duplicates, nonce gaps, under-funded senders and over-budget transactions into parallel blocks | Executed set and state roots identical across 3 independent node implementations of the skip rule (two Rust, one Python oracle) |
| pgas determinism | Replay 10,000 mainnet-shaped transactions on 3 machines | Identical pgas per transaction |
| Native versus proven | For 1,000 segments, prove with the SP1 implementation and compare the claim to native | Zero disagreements; any disagreement is a release blocker |
## 9. Risks, open questions, acceptance criteria
### 9.1 Risks and open questions, with the experiment that settles each
| # | Risk or question | Experiment | Phase |
|---|---|---|---|
| R1 | The pgas table is wrong by an order of magnitude for some opcode, so a cheap transaction stalls provers (hostile review finding 3) | Calibrate every opcode and precompile in SP1 with 3 input sizes; replay ethereum/tests and 10,000 mainnet-shaped transactions; publish the `pgas / gas` distribution; require the 0.1 to 10 band for 95% | 2 |
| R2 | Shard time on a 12 GB card misses 20 s | The phase 2 gate: shard of `S_p` pgas on a 3060-class card, 5 runs, median under 20 s; if missed, halve `S_p` and `B_p` and re-measure, the chain carries less gas rather than redesigning | 2 |
| R3 | MPT witnesses dominate pgas | Measure trie-proof share per shard on the replay; above 30% triggers the binary-trie evaluation of section 2.1 | 2 |
| R4 | Wrapper cost for light clients unmeasured (ledger P3) | Wrap a segment proof to Groth16 on a 12 GB card and on a 24 GB card; record time, memory, proof size, verification time on a phone | 2 |
| R5 | Native and zkVM execution disagree (a completeness bug turns into a permanent backlog) | The native-versus-proven differential of section 8.5 on every release; the backlog rule of section 4.3 bounds the damage | 3 |
| R6 | Reorg re-execution cost at 1 BPS makes nodes lag | Devnet with regional latency: record selected-chain reorg depth distribution and re-execution time per reorg; require p99 re-execution under 500 ms | 3 |
| R7 | Sortition parameters (8 provers, 10 s) leave shards unproven or let one operator dominate | Devnet with 20 nodes and 3 prover speeds: measure shard latency and the share of shards won by the fastest prover; require under 25% to the fastest | 3 |
| R8 | The two base fees oscillate against each other or against the lottery (hostile review finding 4) | Simulate `f_e`, `f_p` and the hash-or-prove switch with the devnet's fee traces; tune the smoothing window | 3 |
| R9 | `block.timestamp` monotonic rule drifts far from wall-clock under a burst of back-dated blocks | Simnet with adversarial timestamps inside Kaspa's 132 s tolerance; measure drift; cap at the tolerance if needed | 3 |
| R10 | Blockscout's assumptions about uncles, reorgs or block numbers break on chain blocks | Index a 24-hour devnet run in a Blockscout fork; zero indexer errors | 3 |
| R11 | The wei-budget cap confuses wallets that compute `gas_limit x gasPrice` themselves | Test MetaMask, Rabby and Frame against the devnet; any mismatch becomes an RPC change, not a protocol change | 3 |
| R12 | Job proofs are verified only cryptographically by full nodes (section 6), so a soundness bug reaches state through a job | Jobs are gated behind the proof system version; a job output cannot mint IGN or touch system contracts; review with the cryptographer before devnet v2 | 4 |
### 9.2 Devnet v1 acceptance criteria
Devnet v1 is phase 3 (Feb to Mar 2027, 20 nodes) with the stub `ProofSystem` of section 5.7 unless the phase 2 SP1 implementation is ready. The gate from `site/journey.json` is "1 block a second held with proofs under 60 s behind the tip"; these criteria make it checkable for the execution layer.
| # | Criterion | Measure |
|---|---|---|
| A1 | 20 nodes hold 1 block per second for 24 hours with the EVM executor on, no node more than 3 segments behind the virtual | Node logs |
| A2 | All nodes agree on `post_root` for every segment | Hash comparison over the run |
| A3 | Differential tests of section 8.5 rows 1 to 4 pass on the release commit | CI |
| A4 | A Hardhat and a Foundry deployment of Uniswap v2 core, an ERC-20 and an ERC-721 succeed and trade for 1 hour under load from 3 senders with parallel inclusion; no transaction permanently lost; skipped copies visible through `igneum_getTransactionStatus` | Script and explorer |
| A5 | Proof records (stub or SP1) included within 60 s of every segment for 95% of segments; pool credited to the recorded provers | `igneum_getSegment` |
| A6 | A proof record with a wrong `post_root` injected by a test node makes the block invalid on every other node | Test harness |
| A7 | MetaMask sends a transfer and a contract call against the devnet using only `eth_*` with the folded gas quote | Manual, recorded |
| A8 | Blockscout fork indexes the run with zero errors and shows the DAG panel, status chip and skipped tab | Indexer logs |
| A9 | Developer share lands at the registered payee for a factory-deployed contract; self-dealing transactions are excluded from the leaderboard | Script |
Sources read for this document: `vendor/rusty-kaspa` at `01b532e8` (files cited inline), `docs/fork-map.md`, `docs/fud-ledger.md` entries F13, P1 to P10, E3, E4, C4, C5 and overclaims 25 to 27 and 35 to 38, `docs/spec/00-overview.md`, the Igneum design document (sections "Architecture", "Security model", "Risks and their solutions", "Finality rule, version 2", "What the hostile review changed", "Build plan"), `site/journey.json`, Conflux eSpace EVM compatibility and consensus design pages, Kasplex L2 docs, 0xPolygon/zkevm-rom `docs/opcode-cost-zk-counters.md`, ZKsync fee-model docs, Succinct's SP1 launch and 8/6/24 benchmark posts, chainid.network. Figures from the web sources are approximate unless the page states the number.