igneum/docs/spec/02-consensus.md
2026-10-03 22:11:27 +00:00

148 lines
20 KiB
Markdown

# Igneum protocol specification, section 2: the ordering layer
Spec version 0.1, 3 October 2026. Status of this section: Designed. The base (rusty-kaspa at commit `01b532e8b553523216471682649693af92f0fd16`, v2.1.0) is built and has run a 3-node devnet at Kaspa's own parameters (`docs/bench-log.md`, entry "rusty-kaspa base build and 3-node devnet"). No fork point is implemented. `docs/fork-divergence.md` does not exist yet; when it does, it records what was changed against this section.
This section is written as a delta on rusty-kaspa. Everything not named here is Kaspa's rule at the forked commit. File and line references are those of `docs/fork-map.md`, which must be re-checked after any vendor update.
## 2.1 Block rate and GHOSTDAG parameters
| Parameter | Launch value | Kaspa source | Label |
|---|---|---|---|
| Target block rate | 1 block per DAA second | `consensus/core/src/config/bps.rs`, `Bps::<1>` (fork map f1) | Designed |
| GHOSTDAG k | 18 | Kaspa's k table at 1 BPS (fork map f1) | Designed, taken from Kaspa's table (delta 0.01, network delay bound 5 s, `constants.rs:13 to 16`) |
| Max parents | 10 | same | Designed |
| Mergeset size limit | 180 | same | Designed |
| Merge depth | 3,600 DAA s (3,600 blocks at 1 BPS) | `MERGE_DEPTH_DURATION` (fork map e1); `check_bounded_merge_depth`, `post_pow_validation.rs:79` | Designed, unchanged from Kaspa. Bounds which red blocks a block may merge; it is not a reorg bound (ledger F15, round 3) |
| Finality depth | 43,200 DAA s (12 hours at 1 BPS) | `FINALITY_DURATION`; `virtual_processor/processor.rs:1626` | Designed. The reorg bound a user relies on whenever no certified checkpoint is newer: the virtual switches to any heavier selected chain forked above this depth (ledger F15). Live finality is section 3 |
| Pruning depth | 108,000 DAA s | `PRUNING_DURATION` | Designed; MUST stay above the longest checkpoint gap and MUST NOT pass the latest certified checkpoint (section 3, F3) |
| Coinbase maturity | 100 blocks | `bps.rs:119 to 121` | Designed |
| Block-rate steps | 1, then 4, then 10 per second | design document, decisions table | Designed. Each step is a planned fork with its own test campaign (as Kaspa's Crescendo), taken only once proving lag holds under 60 s. Each step re-derives k, max parents and mergeset limit from Kaspa's table; the DAA-second schedules of sections 1, 3, 4 and 5 do not move |
Every Kaspa fork activation (`ForkActivation`) is `always()` on Igneum: there is no history to replay (fork map f2). Own genesis, network prefix, ports and seeders (`network.rs:42 to 60, 238 to 252`).
The reorg bound (ledger F15, round 3). Merge depth limits what an old block can merge; it does not stop the virtual switching to a heavier selected chain. The depth that stops a switch is the finality point, `FINALITY_DURATION`, 43,200 DAA s, and that is the bound users and exchanges rely on whenever no certified checkpoint is newer (section 3.9). It is stated to users in past-median time, 12 hours, because DAA seconds run fast under a retarget lag (ledger M14; `docs/review/round-3-2026-10-03.md`, "Tonight's devnet", where DAA time ran 2.9x the wall clock for 17 minutes). Whether to lower `FINALITY_DURATION` and accept Kaspa's finality-conflict handling at the lower depth is decided at gate 2 with a simnet test: a heavier private chain forked 2, 6 and 13 hours back is released and the node must refuse it at the depth this section names.
## 2.2 Proof of work (fork points a1 to a5)
| Fork point | Kaspa today | Igneum |
|---|---|---|
| a1, a2 | `PowHash` cSHAKE256 then `KHeavyHash` matrix | Both deleted. The lottery hash of section 1 replaces them. `kaspa_pow::State` carries the `igneum_pow::Epoch` for the header's epoch (program by epoch of the header's DAA score, dataset by day of the header's DAA score, section 1.12) |
| a3 | `pow <= target` on a `Uint256` | Section 1.10: `hash64 <= target64`. The mapping from the 256-bit target and the block level for pruning proofs are Open (O-2.4) |
| a4 | Header validated in isolation | Header validation gains a dependency on chain state: the epoch seed (section 4). The seed source is named in the header (section 2.4) so the dependency is checkable from the header plus its past. During IBD and pruning-proof validation (`pruning_proof/validate.rs:192`) the node MUST be able to derive the program for any header from headers and certificates alone; this is the hardest fork point (fork map, risk High) and is Open until the devnet proves it (O-2.5) |
| a5 | Pre-PoW hash over 12 fields | Adds `vote_key_hash`, `seed_source` and `proof_ref` (section 2.4) so all three are committed by the nonce. Every header-hash test vector changes, genesis included |
What the nonce commits to: the pre-PoW header hash `H` enters the register initialisation (section 1.6, Open O-1.9). The VDF requirement that the seed checkpoint commits to the full block hash including the nonce (`proto-vdf/README.md`) is satisfied by Kaspa's `Header.hash` covering the nonce.
## 2.3 Difficulty adjustment (fork points c1, c2)
Status: Implemented on the `difficulty` branch of `vendor/igneum-node` (`SampledDifficultyManager::igneum_difficulty_bits`, pure core `igneum_target`, constants `kaspa_consensus_core::igneum::difficulty`), measured in `sim/difficulty/` and on a 3-node CPU test network (`docs/analysis/difficulty-2026-10-03.md`). Network parameter `difficulty_rule`: `igneum-dual` on every Igneum network, `kaspa-sampled` selectable through the override file (`{"difficulty_rule": "kaspa-sampled"}`) so the two rules can be compared on the same genesis. This closes O-2.1: the window is not shortened and the cost-equalising rule of section 1.4.2 is no longer load-bearing for difficulty (it remains desirable for the verifier bound).
Lineage, credited: the long lane is Kaspa's KIP-4 sampled window (`difficulty.rs`, samples at every 4th block, 661 samples, `estimateNetworkHashesPerSecond`'s blue-work-over-time estimator); the short lane is Zawy's LWMA weighting (linear weights rising to the newest block, solvetimes clamped symmetrically, 20 target times here against Zawy's 6) applied to the same work-over-time estimator instead of LWMA's average target, which is biased while targets ramp (measured: the fast lane stalled at 15x of a 50x step before the change, `docs/analysis/difficulty-2026-10-03.md`).
### The rule
1. Every lane estimates the hash rate as work over time (blue-work increment over clamped solvetime), never as average target times average solvetime.
2. Short lane S: the newest 120 selected-chain blocks of the block's own epoch, linear weights. Reference lane R: for the first 600 blocks of an epoch the epoch window (all its blocks, blended with the parent's implied rate as k : 16); from block 600 of the epoch Kaspa's sampled window restricted to the epoch.
3. S takes over when S and R differ by more than 25%; otherwise R rules. The trigger compares the two lanes, not the short rate to target (the literal form chatters once the short window is back on target while the long window is still polluted: measured 1,910 s against 72 s to settle on the polluted-window case).
4. The output may make the target fall (difficulty rise) by at most 3% per block and rise (difficulty fall) by at most 10% per block, relative to the selected parent's target, then is capped at `MAX_DIFFICULTY_TARGET`.
5. The parent's target is held for the first 8 blocks of every epoch, including epoch 0: the warm-up starts at block 8, not 600.
| Parameter | Value | Constant | Label |
|---|---|---|---|
| Epoch | 3,600 DAA s, the block's own DAA score (section 1.12) | `POW_EPOCH_BLOCKS` | Designed |
| Short window | 120 chain blocks | `SHORT_WINDOW` | Measured: 60 is noisier (steady std 0.069 vs 0.039), 180 slower on hops (356 s vs 276 s) |
| Hold at epoch start | 8 blocks | `EPOCH_HOLD` | Designed |
| Epoch-lane prior weight | 16 blocks | `PRIOR_BLOCKS` | Measured: 8, 16, 32 indistinguishable |
| Long lane from | 600 blocks of the epoch (150 samples) | `LONG_MIN` | Kaspa's minimum window, kept |
| Long window | 661 samples at 4-block intervals (2,644 blocks), restricted to the epoch | Kaspa's constants, kept | Designed |
| Trigger | 25% disagreement between S and R | `TRIGGER_PERCENT` | Measured: 15% engages on noise (steady std 0.077), 35% slower on hops |
| Harden clamp | 3% per block | `HARDEN_PERCENT` | Measured: 2% slows the 50x step-up to 84 s from 62 s, 5% buys nothing on the up step |
| Ease clamp | 10% per block | `EASE_PERCENT` | Measured: 3% leaves the 50x step-down at 1,786 s and a 211 s worst gap, 10% gives 1,164 s and 65 s |
| Solvetime cap | 20 target times, both signs | `CAP_BLOCKS` | Measured: Zawy's 6 T leaves the 50x step-down at 1,164 s, 20 T gives 657 s and a 35 s worst gap; 132 s (the future tolerance) gains nothing more and lets an idle gap over-ease (16x peak against 8.5x on the devnet case) |
| Timestamp rules | 132 s future tolerance in isolation, strictly above the sampled past median in context | Kaspa's, unchanged | Designed |
What the clamps and caps bound. A forged timestamp can move one capped solvetime by at most 20 s in either direction and the next honest block's solvetime cancels it under the symmetric cap (a run of L forged blocks nets L - 1 s of real time), so a minority cannot bias a lane by more than a few percent; the per-block clamps bound how fast any estimator error, honest or not, reaches the target. The 3% harden clamp means a 50x step up is corrected in about 130 blocks (a few seconds of wall time at 50x), the 10% ease clamp means a 50x step down costs about 40 blocks of shrinking gaps (11 minutes measured, against 3.4 hours for Kaspa's rule).
### Measured comparison
Simulator (`sim/difficulty/sim.py`, seed 7, honest timestamps; the full tables with overshoot, gap and noise columns are in the analysis document). "Settled" is the first time the 121-block mean rate stays within 10% of target for 100 blocks.
| Case | Kaspa sampled DAA | Monero 720 | LWMA 60 | LWMA 120 | Igneum |
|---|---|---|---|---|---|
| Hash rate x50, settled s | 1,542 | 94 | 105 | 231 | 62 |
| Hash rate /50, settled s | 12,296 | 6,433 | 578 | 1,074 | 657 |
| Hash rate /50, worst gap s | 179 | 187 | 119 | 65 | 35 |
| Epoch step +-30%, settled s (mean of 6) | 1,583 | 456 | 157 | 153 | 144 |
| Epoch step +-30%, first within 10% s | 1,583 | 456 | 91 | 137 | 87 |
| 3x pool hopping every 15 min, settled s | never | 337 (6 of 12 never) | 167 | 184 | 190 |
| 10x pool hopping every 15 min, settled s | never | 284 (6 of 12 never) | 264 | 326 | 212 |
| Polluted window (devnet case), settled s | 2,748 (peak 7.9x) | 124 (peak 15x) | 66 | 110 | 70 |
| Genesis 10x too hard, settled s | never | 287 | 155 | 188 | 322 |
| Genesis 10x too easy, settled s | 1,555 | 95 | 58 | 88 | 59 |
| Steady std of block rate, constant hash rate | 0.012 | 0.037 | 0.131 | 0.092 | 0.038 |
| Steady std of block rate, 10%/h random walk | 0.049 | 0.049 | 0.132 | 0.093 | 0.062 |
| Devnet record (75x step on arrival of the PC), settled s | never (peak 7.6x, 2,340 blocks above 2x) | 136 | 75 | 157 | 79 |
Test network (3 igneumd nodes, CPU miners, 20 minutes, `docs/analysis/difficulty-2026-10-03.md`): genesis 4x too hard, CPU miners A then A+B+C (x1.51) then A (/1.45) on a loaded machine; measured first-within-10% 132 s, 214 s, 85 s against simulator medians of 263 s, 61 s and 231 s on the same profile; 1,133 blocks, 0 rejected; Kaspa's rule on the same genesis would not have retargeted within the run (600-block dead zone at 0.25 blocks/s). Kaspa's rule live on the same genesis, 10 minutes: 70 blocks, bits unchanged on all 70, 0.08 blocks/s, worst gap 63 s, never within 10% of target.
### What this section does not do
Blocks off the selected chain count only through blue work (red blocks carry work the estimator ignores, as Kaspa's estimator does). The chain walk costs up to 600 compact-header reads per block for the first 600 blocks of an epoch and 120 afterwards; re-measure before the 4 BPS step, where the sampled window should replace the walk for the epoch lane too. Real-time targeting (a target that depends on the header's own timestamp) was considered for step-downs and left out: it changes template building and header validation, and the 10% ease clamp with the 20 T cap already brings the 50x step-down to 11 minutes. The 30-day vote-weight window of section 3 is denominated in past-median time since 3 October 2026 (section 3.1 W2, ledger F14), so a retarget lag cannot age it; it is not a retarget change.
## 2.4 Header (fork point d)
Kaspa's `Header` (version, parents, hash_merkle_root, accepted_id_merkle_root, utxo_commitment, timestamp, bits, nonce, daa_score, blue_score, blue_work, pruning_point) plus:
| Field | Size | Meaning | Label |
|---|---|---|---|
| `vote_key_hash` | 32 bytes | Hash (the chain's BLAKE2b-based `Hash`) of the producer's BLS12-381 G1 compressed public key (48 bytes). The first block that uses a key reveals the key itself in the coinbase payload. Vote weight accrues to this key (section 3, W1) | Designed. The design document says a 32-byte hash; `docs/fork-map.md` row d says a 48-byte key. This specification takes the hash: 32 bytes x 86,400 blocks/day = 2.8 MB/day of header growth against 4.1 MB with the key |
| `seed_source` | 32 bytes | Hash of the checkpoint block from which the header's epoch program seed was derived (section 4.3) | Designed, proposed here, Open (O-4.3) |
| `proof_ref` | 32 bytes | Hash of the highest aggregated block proof the producer knows | Forward reference. The proof format, what "highest" means and the validation rule belong to the chunked proving protocol, which is out of scope for 0.1. Until that protocol is specified the field is all zeros and unvalidated |
Also edited (fork map d): p2p `p2p.proto:76 to 89`, `convert/header.rs:13, 45`; RPC `rpc.proto:25`, `model/header.rs:85, 105, 248`; genesis headers; the headers store (serde, re-sync needed); header mass.
Certificates (section 3, C3) and votes (section 3, Q2) travel in the block body, not the header: every block carries the highest certificate its producer knows and every valid vote it has received for the presence window that is not already in its past, and a block whose selected chain does not pass through every certified checkpoint in its past is invalid (post-PoW validation, fork map e2).
## 2.5 Emission (fork points b1, b2, b3)
Designed (design document, "The token" and "Difficulty, block timing and proving cadence"). No pre-deflationary phase, no month table, no treasury.
| Parameter | Value | Label |
|---|---|---|
| Hard cap | 4,000,000,000 IGN, approached and never reached | Designed |
| Year | 31,557,600 DAA s (365.25 days) | Decided 3 October 2026 (ledger E9, round 3): the specification follows the code, `consensus/core/src/igneum.rs` `SECONDS_PER_YEAR`; the earlier 365-day prototype value is withdrawn. The design document says "1 billion a year" |
| Emission in the first two years | 1,000,000,000 IGN per year | Designed |
| Halving interval | 63,115,200 DAA s (2 years of 365.25 days), for ever | Decided (ledger E9), `HALVING_INTERVAL_SECONDS` |
| Launch ramp | linear from 10% at genesis to 100% at DAA second 2,592,000 (30 days) | Designed |
| Split | 80% block producer, 20% proving pool. A red block inside the DAA window pays its 80% to the miner of the block that merges it and its 20% to the pool | Designed; the red rule Decided 3 October 2026 (ledger E10, round 3), `coinbase.rs:102 to 109` |
| Base unit | Open (O-2.6): the code keeps Kaspa's 8 decimals (`SOMPI_PER_KASPA`, devnet v0), under which the cap is 4 x 10^17 units and fits a u64; 18 decimals (the EVM convention) puts the cap at 4 x 10^27 and needs a wider type. `docs/fork-map.md` b2 wrote `cap_sompi = 4e9 x 1e8`. Decided before the first testnet genesis |
Emission per DAA second at DAA score `t`:
```
E(t) = ramp(t) * floor(10^9 * UNIT / 31,557,600) >> floor(t / 63,115,200)
ramp(t) = min(1, 1/10 + 9/10 * t / 2,592,000) evaluated in integers as a rational with denominator 25,920,000
```
The pre-ramp rate is 31.68808781 IGN per DAA second at 8 decimals (`BASE_SUBSIDY_PER_SECOND_SOMPI` = 3,168,808,781, asserted by the code's own test) and about 31.688 IGN at any unit (Decided, ledger E9). The geometric series sums to 4 x 10^9 IGN; the ramp withholds 0.45 x 2,592,000 / 31,557,600 x 10^9 = about 37 million IGN that are never minted, and integer floors withhold a negligible further amount, so the cap is a strict bound.
Emission is keyed to DAA score, not to timestamps: `E` is a function of DAA score and miner-chosen timestamps cannot mint (hostile review table, "Emission per wall-clock second invites timestamp games"). `E` is paid per block, so coins are blocks times `E`: when the controller lets the block rate run above target, short-run emission runs above schedule by the same factor, as on every proof-of-work chain, and the cap is unaffected because the halving schedule and the ramp are in DAA seconds (Decided 3 October 2026, ledger E10, round 3; the earlier sentence "more blocks never means more coins" is withdrawn, and the devnet of 3 October 2026 minted 4.7x the schedule for eight minutes during a retarget lag, `docs/review/round-3-2026-10-03.md`, "Tonight's devnet"). Payment is through the merging block's coinbase as in Kaspa (`coinbase.rs:97 to 142`): the coinbase of block B pays, for each blue block M in B's mergeset, `E(daa_score(B))` split 80% to M's miner and 20% to the proving pool; for each red block in B's mergeset that is inside the DAA window, the same `E` split 80% to B's own miner and 20% to the pool (`coinbase.rs:102 to 109`, Kaspa's rule, Decided, ledger E10); a red outside the DAA window earns only its coinbase-level fees, of which Igneum has none (fees are in section 5). How the DAA-score increment is apportioned among the blues of one mergeset at higher block rates is Open (O-2.7); at 1 BPS the mergeset is usually one block.
The 20% proving share is paid to the prover set recorded for the proven block, which is known 20 to 60 s after the block (design document, "Proving lag"). The coinbase payload format gains prover outputs (fork map b3, risk High). The rule that the pool is paid as a fixed amount per block divided among shards by consensus proving cost is in section 5.3.
Fees are not in the coinbase; they are in the execution layer (section 5).
## 2.6 Duplicate inclusion for the EVM layer
Designed (hostile review table, "Parallel blocks include the same transaction"). Blocks carry transactions only and make no claim about state. The execution layer orders transactions by the GHOSTDAG ordered sequence (the selected chain's mergeset order, Kaspa's), and:
1. The first copy of a transaction (by transaction hash) in the ordered sequence executes and pays its fees.
2. Every later copy is deduplicated before execution and before proving. It executes nothing and pays nothing.
3. A later copy still occupies block space (mass) in the block that includes it, so the including miner bears the cost.
4. Transactions from one account are subject to the EVM nonce rule over the same ordered sequence: a transaction whose nonce is not the account's next nonce at its position is skipped, not failed, and pays nothing. This is Kaspa's "skip conflicting spends" pattern applied to the EVM.
Block number, timestamp, blockhash, coinbase and prevrandao over the ordered sequence are fixed in section 7.1 (ledger P5, closed 3 October 2026).
## 2.7 What the devnet showed and did not show
Measured (`docs/bench-log.md`, devnet entry): three kaspad nodes at Kaspa's devnet parameters (10 BPS, k 124) held identical block counts, DAA scores and sink hashes at 18 of 19 ten-second samples under a 27 MH/s CPU miner; the DAA raised difficulty from genesis bits at block 6,018 and the block rate fell from 56 to 62 blocks/s toward the 10 BPS target. GHOSTDAG k was not exercised (a single serial miner never produced parallel blocks); a second miner is the next step. Nothing in that run used an Igneum parameter.