docs: R3.26 / M15 fork-divergence rows and bench-log attack measurement
fork-divergence.md: a section for branch r3-fixes (the PoW-before-validation reorder, the cache-build cap and the per-peer guard), the merge-overlap note for the finality branch, and the updated "PoW before or after GHOSTDAG" and "Day seed" open decisions. bench-log.md: the before-and-after attack numbers (50 bogus headers build 50 caches in 10.6 s before, 0 and all rejected in 14 ms after), one cache builds in about 0.2 s, and the M16 Mac inline-dataset note. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
21c195e420
commit
aa00452711
2 changed files with 60 additions and 8 deletions
|
|
@ -240,3 +240,15 @@ Machine: Apple M5 Max, load average 60 to 110 (three other agents building), rus
|
|||
Cross-compile: `cargo build --release -j 6 -p kaspad -p igneum-miner --features igneum-pow --target x86_64-pc-windows-gnu` at `nice -n 19`, 8 min 25 s from a clean target directory (recipe in `proto-cuda/windows-node/cross-build.sh`). `librocksdb-sys` (bindgen plus the bundled rocksdb and snappy C++) built with `LIBCLANG_PATH=/opt/homebrew/opt/llvm/lib`, `BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw32 --sysroot=<mingw sysroot> -I<mingw include>"` and `CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++`; zstd, lz4, bzip2, zlib, secp256k1 and mimalloc built with the mingw gcc. `igneumd.exe` 43,172,352 bytes, `igneum-miner.exe` 9,391,104 bytes. The exe imports `libstdc++-6.dll`: `-static-libstdc++` is ignored by the gcc driver rustc links with, and `-C link-arg=-static` (second full build, 8 min) does not cover a library rustc passes as `-Bdynamic`; the package ships `libstdc++-6.dll`, `libgcc_s_seh-1.dll` and `libwinpthread-1.dll` next to the exe (fix for next time: empty `CXXSTDLIB_x86_64_pc_windows_gnu` plus `-Wl,-Bstatic -lstdc++`). Not run on Windows yet.
|
||||
Sync test (native build of the same worktree, 21:44 to 21:46 BST, live node on 26610/26611 untouched): node A on 27000/27001 with START-NODE.bat's flags (`--addpeer=192.168.68.64:26611 --listen=0.0.0.0:27001 --nodnsseed --disable-upnp --nologfiles`) connected and started IBD within 20 ms, had 5,144 headers and 1,386 blocks at 7 s, and from 17 s on matched the live node's block count, DAA score, blue score and sink at every 10-s sample over 60 s (5,157 to 5,175 blocks, sink identical 5 of 6 samples, `synced=true`); every header checked by `igneum-lottery-v1-bound`. Live node `peers` 1 -> 2 -> 1. Node B on 27010/27011 dialled A's listener, synced to the same sink within 25 s and learnt the live node from A (outbound 2): a node with `--addpeer` plus `--listen` accepts inbound connections (`--connect` would set the inbound limit to 0, `kaspad/src/daemon.rs`). SIGINT stopped both cleanly.
|
||||
Package: `~/Desktop/igneum-node-windows.zip` from `proto-cuda/windows-node/make-package.sh` (exe, miner exe, src.zip snapshot of the worktree HEAD plus `igneum-pow/`, scripts, README). Build on the PC (BUILD-NODE.bat) estimated at 15 to 25 minutes on the 9800X3D, approximate, untested.
|
||||
|
||||
## 3 October 2026, R3.26 / M15: PoW checked after the cheap checks, cache-build cap, attack before and after (consensus-engineer)
|
||||
|
||||
Machine: Apple M5 Max (18 logical cores), load average 60 to 110 (three other agents building at the same time), rustc 1.99.0. Worktree `vendor/igneum-node-r3`, branch `r3-fixes` at `5166ee26` on top of the rename commit `d62708a8`. Release builds; the real engine needs `--features igneum-pow`.
|
||||
Fix: `validate_header` now runs version, timestamp-not-in-future, parent, vote-key, parents-exist, GHOSTDAG, pruning, DAA-score, difficulty, blue-score, blue-work and past-median checks before the PoW engine; the engine (the one 256 MiB cache per day seed) is the last check that chooses seeds. The engine is process-wide, holds `KEEP = 4` `(epoch seed, day)` caches, keeps the chain's current and next day resident, runs at most one build per seed pair and at most 2 at once with a queue of 4 (then `PowCacheQueueFull`, retryable, not a peer fault). A per-peer p2p guard counts an off-day cold build or a rejected-before-PoW header as a strike; more than `IGNEUM_POW_STRIKES` (default 2) in an hour disconnects the peer and bans its IP for an hour.
|
||||
Cache build time: one 256 MiB ChaCha12 program-plus-cache build in 222 ms on one core under this load (the engine smoke test on an idle machine earlier the same day measured about 0.2 s; `proto-metal` reported 273.6 ms for the CPU reference fill under the same load). The honest 20-to-60 s proving lag and the 10 ms CPU verify gate are unaffected.
|
||||
Attack, before and after (ignored test `measure_m15_attack_before_and_after`, release, `--features igneum-pow`, `validate_and_insert_block`, the path `submit_block` and block relay call into): an honest 10-block chain builds 1 cache (genesis epoch, genesis day). Then 50 headers with bogus timestamps (50 distinct past days) and bogus DAA scores.
|
||||
- Before (the pre-fix order, replayed by calling the engine with the header's own unvalidated day seed): 50 cold 256 MiB builds, 10,595 ms, and the honest day's entry is evicted (`KEEP` was 3).
|
||||
- After (the new order): 0 builds, all 50 rejected (`TimeTooOld` or `UnexpectedHeaderDaaScore`) in 14 ms total. The live day stays resident.
|
||||
Tests: kaspa-pow `--features igneum-pow` 8 pass (engine smoke, `one_build_per_seed_pair_under_contention`, `live_days_survive_off_day_builds`, `build_queue_is_bounded`, index and live-day helpers, shared-engine, stub); kaspa-consensus header_processor `cheap_checks_run_before_the_pow_engine` pass; kaspa-p2p-flows `pow_guard` 2 pass; the full kaspa-consensus release suite otherwise unchanged.
|
||||
M16 Metal note (R3.5, cheap reconfirmation only): the Mac `--inline-dataset` shortcut at the 256 MiB cache, 256 MiB dataset, under the same heavy load, ran honest 91.7 Mhash/s against inline 5.29 Mhash/s (inline about 17x slower); this is noisier and slower than the idle-machine figures already in `proto-metal/MEMHARD.md` (10x slower at a 256 MiB dataset, 4.8x at 1 GiB), because the inline kernel is compute-bound and the machine was loaded. The 64 MiB on-die-SRAM emulation M16 wants (inline kernel with a 64 MiB cache, `cacheLog2Words = 24` in `proto-metal/main.swift`) is the RTX 5090 run reserved for the project lead's PC, as R3.5 states; it is not done here and the Mac number above does not price a die.
|
||||
Not done: the real-engine daemon RPC run (honest blocks need GPU-mined pow, so the measurement used the equivalent validate path with `skip_proof_of_work`); the 64 MiB-cache inline kernel on the 5090; the chain-derived day seed by DAA score (spec 01 section 1.12, still the timestamp-day devnet rule); the VDF epoch seed and finality.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Fork: `vendor/igneum-node`, a git clone of `vendor/rusty-kaspa` at v2.1.0 commit `01b532e8` (22 Sep 2026). Every Igneum change is a commit on top of that base, one per subject, so `git log 01b532e8..HEAD` in the fork is the full list. This file is the reading guide: what each change touched, why, how risky it is, and what to do when upstream moves. `docs/fork-map.md` is the plan this implements; row ids there (a1 to f2) are cited below.
|
||||
|
||||
Status: devnet v1, mining layer on the real header-bound lottery hash (3 Oct 2026, later the same day as v0): CPU and GPU miners, three workers (Metal, CUDA, OpenCL). The node binary is `igneumd` since the rename pass of 3 Oct 2026 (see "The rename" below). Finality, VDF seeds, the zkEVM and the proving layer are not in the fork yet.
|
||||
Status: devnet v2, sustained-mining finality rule v2 on the real header-bound lottery hash (3 Oct 2026, the same night as v1): BLS12-381 vote keys, weight by blue blocks, 30-blue-score checkpoints, votes and certificates carried in blocks and gossiped on p2p, quorum and floor, locked checkpoints bounding fork choice, equivocation bans (see "Finality v2" below). The node binary is `igneumd` since the rename pass of 3 Oct 2026 (see "The rename" below). VDF seeds, the zkEVM and the proving layer are not in the fork yet.
|
||||
|
||||
## The table
|
||||
|
||||
|
|
@ -18,15 +18,51 @@ Status: devnet v1, mining layer on the real header-bound lottery hash (3 Oct 202
|
|||
| `consensus/core/src/config/params.rs` | `MAINNET_PARAMS.dns_seeders` and `TESTNET_PARAMS.dns_seeders` emptied (upstream: nine Kaspa mainnet seeders, three testnet seeders) | An Igneum node started with `--testnet` or no network flag must never dial Kaspa seeders | Low | Fill with Igneum seeders before any public network. Devnet and simnet were already empty. |
|
||||
| `crypto/addresses/src/lib.rs`, `crypto/txscript/src/standard.rs` (test vectors), `bridge/src/default_client.rs`, `bridge/src/share_handler.rs`, `bridge/src/tests.rs` | Address prefixes `igneum` (mainnet), `igneumtest`, `igneumsim`, `igneumdev` (upstream `kaspa`, `kaspatest`, `kaspasim`, `kaspadev`); devnet first on 3 Oct 2026 v0, the other three in the rename pass later that day | No Igneum address string parses as a Kaspa address on any network. The prefix is only part of the string encoding and its checksum; the script public key behind an address is unchanged, so nothing on chain moved | Low | Any upstream test that spells out a `kaspa:` address fails here; the vectors in `addresses` and `txscript` were regenerated. Wallet, cli and wasm crates (not in the default build) still carry `kaspa:` strings in their own tests. |
|
||||
| `consensus/core/src/config/bps.rs`, `consensus/core/src/config/params.rs` | `OneBps = Bps<1>`; `DEVNET_PARAMS` uses `BlockrateParams::new::<1>()`: 1,000 ms blocks, GHOSTDAG k 18 (`calculate_ghostdag_k(2 x 5 x 1, 0.01)`), 10 max parents, mergeset limit 180, merge depth 3,600 blocks, finality depth 43,200 blocks, pruning depth 108,000 blocks, coinbase maturity 100; `crescendo_activation: always()`; doc table and a test pinning every value | 1 block per second at launch (fork-map f1, f2, e1). Finality and pruning depths are upper bounds only: live finality will be the certified checkpoint | Low; this is Kaspa mainnet's pre-Crescendo path | The `ForkedParam` and `bps_history` plumbing is still present for the other networks. Strip it in one pass when mainnet params are written. |
|
||||
| `consensus/core/src/igneum.rs` (new), `consensus/core/src/lib.rs`, `consensus/core/src/constants.rs` | Emission constants and functions: 1,000,000,000 coins in year one, per-second rate halving every two years (`SUBSIDY_PER_SECOND_BY_PERIOD[i] = 3,168,808,781 >> i`, 33 periods), `block_subsidy(daa_score, bps)`, 30-day linear launch ramp from 10%, `proving_pool_share` 20% and `producer_share` 80%, `proving_pool_script_public_key` (OP_RETURN tagged `igneum-proving-pool-v0`), `POW_EPOCH_BLOCKS = 3,600` | The design's schedule, hard cap 4,000,000,000 (the geometric series sums to it; rounding leaves under 100 coins unminted), no emission treasury (fork-map b1, b2) | Medium: consensus money | Pure addition. Keep as the one source of the schedule. |
|
||||
| `consensus/core/src/igneum.rs` (new), `consensus/core/src/lib.rs`, `consensus/core/src/constants.rs` | Emission constants and functions: 1,000,000,000 coins in year one, per-second rate halving every two years (`SUBSIDY_PER_SECOND_BY_PERIOD[i] = 3,168,808,781 >> i`, 33 periods), `block_subsidy(daa_score, bps)`, 30-day linear launch ramp from 10%, `proving_pool_share` 20% and `producer_share` 80%, `proving_pool_script_public_key` (OP_RETURN tagged `igneum-proving-pool-v0`), `POW_EPOCH_BLOCKS = 3,600`, `POW_EPOCH_LEAD = 600`, `pow_epoch_blocks()`, `pow_epoch_lead()`, `pow_epoch_seed_score(epoch)` (env overrides for test networks), `PowEpochInfo` | The design's schedule, hard cap 4,000,000,000 (the geometric series sums to it; rounding leaves under 100 coins unminted), no emission treasury (fork-map b1, b2) | Medium: consensus money | Pure addition. Keep as the one source of the schedule. |
|
||||
| `consensus/src/processes/coinbase.rs` | Kaspa's pre-deflationary phase, 426-month table and Crescendo rescaling removed; `CoinbaseManager::new(max_spk_len, max_payload_len, bps)`; `calc_block_subsidy` reads the period table; `expected_coinbase_transaction` pays 80% plus fees per rewarded block to its declared script and pools 20% of every subsidy (blues and reds) into one output to the proving pool script, placed after the blue outputs and before any red reward; tests rewritten | 80/20 split in consensus; the 20% is burned on devnet v0 and becomes the prover payout when the proving layer records prover sets (fork-map b3) | High: changes what every node accepts as a valid coinbase | Upstream edits to `coinbase.rs` will conflict. The payload format is unchanged (full subsidy in the payload, split derived from it), so Kaspa's payload parsing merges cleanly. |
|
||||
| `consensus/src/consensus/services.rs`, `consensus/src/consensus/test_consensus.rs`, `consensus/src/pipeline/body_processor/body_validation_in_context.rs`, `consensus/src/processes/parents_builder.rs`, `consensus/src/processes/transaction_validator/tx_validation_in_isolation.rs` | Call sites of the new `CoinbaseManager` constructor; subsidy expectations in tests use `igneum::block_subsidy` | Wiring | Low | Mechanical. |
|
||||
| `consensus/pow/src/igneum.rs` (new), `consensus/pow/src/lib.rs`, `consensus/pow/Cargo.toml`, `Cargo.lock` | `PowEngine` trait (`check_header(header, &EpochSeeds) -> (passed, pow)`), `HeavyHashEngine` stub (default), `IgneumEngine` behind feature `igneum-pow` calling the `igneum-pow` crate in its header-bound form (`Epoch::from_seed_bytes`, `Epoch::pow_bound`): `H` = header hash with the nonce zeroed (timestamp kept), init words `seed_words_from_bytes("igneum-block/" \|\| H \|\| nonce_hi_le32)`, lane nonce = low 32 bits, pow value = lane hash in the top 64 bits with zero low bits; epoch seed bytes = the epoch block hash, day seed bytes = `"igneum-day/" \|\| day_le64`; caches three `(epoch seed, day seed)` entries of program plus 256 MiB cache and exposes them to the miner (`epoch_for`, `header_prehash`, `target64`); `igneum-pow` as an optional path dependency `../../../../igneum-pow`; doc note on the existing `calc_block_level` (pruning proofs still use the stub for block levels) | The hash swap behind a trait (fork-map a1 to a3); the binding closes the "lane hash does not absorb the header" gap of v0 (spec 01 O-1.9) | Medium | Pure addition in the pow crate. `State` (kHeavyHash) is untouched, so upstream pow changes merge. The path dependency must become a workspace or git dependency when the fork gets its own repository. |
|
||||
| `consensus/src/pipeline/header_processor/processor.rs`, `consensus/src/pipeline/header_processor/pre_ghostdag_validation.rs` | PoW check moved from `validate_header_in_isolation` to after GHOSTDAG (`check_pow_and_calc_block_level(header, selected_parent)`); `epoch_seed` walks the selected-parent chain to the last block below the epoch's start DAA score (genesis for epoch 0) with a memo; `pow_engine: Arc<dyn PowEngine>` on the processor; one `info` line per accepted or rejected PoW naming the engine (`PoW accepted <hash> by igneum-lottery-v1-bound ...`) | The epoch seed is chain state, so PoW cannot be checked in isolation any more (fork-map a4). Temporary seed rule until the 10-minute VDF over a certified checkpoint exists | High: a header now reaches GHOSTDAG before its PoW is checked, so an attacker can make a node run GHOSTDAG on headers with bad nonces (bounded by the per-peer header rate; the stub engine ignores the seeds so devnet v0 is not exposed) | Upstream rarely touches this ordering, but any refactor of `process_header` conflicts. Pruning-proof validation (`processes/pruning_proof/validate.rs`) still uses the stub for block levels; thread seeds through it before enabling the real engine on a pruning network. |
|
||||
| `consensus/core/src/api/mod.rs`, `consensus/src/consensus/mod.rs`, `components/consensusmanager/src/session.rs`, `rpc/core/src/model/message.rs`, `rpc/grpc/core/proto/rpc.proto`, `rpc/grpc/core/src/convert/message.rs`, `rpc/service/src/service.rs`, `kaspad/src/args.rs` | `ConsensusApi::get_pow_epoch_info` (and the session wrapper); `GetBlockTemplateResponse.pow_epoch: Option<RpcPowEpochInfo>` (borsh version 2, gRPC `RpcPowEpochInfo powEpoch = 4`); `IGNEUM_DEVNET_GENESIS_BITS` test override in `apply_to_config` | The block template tells the miner the current and next epoch seeds, so GPU workers prepare the next program one lead ahead (hot swap, 3 Oct 2026) | Low | Additive. An old wRPC client reads version 2 as its own fields plus a trailing option; gRPC clients ignore the new field. |
|
||||
| `consensus/src/pipeline/header_processor/processor.rs`, `consensus/src/pipeline/header_processor/pre_ghostdag_validation.rs` | PoW check moved from `validate_header_in_isolation` to after GHOSTDAG (`check_pow_and_calc_block_level(header, selected_parent)`); `epoch_seed` walks the selected-parent chain to the last block below `epoch x EPOCH - LEAD` (genesis for epoch 0) with a memo (the lead since the hot-swap work of 3 Oct 2026; `Consensus::get_pow_epoch_info` walks the same way from the sink for the block template); `pow_engine: Arc<dyn PowEngine>` on the processor; one `info` line per accepted or rejected PoW naming the engine (`PoW accepted <hash> by igneum-lottery-v1-bound ...`) | The epoch seed is chain state, so PoW cannot be checked in isolation any more (fork-map a4). Temporary seed rule until the 10-minute VDF over a certified checkpoint exists | High: a header now reaches GHOSTDAG before its PoW is checked, so an attacker can make a node run GHOSTDAG on headers with bad nonces (bounded by the per-peer header rate; the stub engine ignores the seeds so devnet v0 is not exposed) | Upstream rarely touches this ordering, but any refactor of `process_header` conflicts. Pruning-proof validation (`processes/pruning_proof/validate.rs`) still uses the stub for block levels; thread seeds through it before enabling the real engine on a pruning network. |
|
||||
| `consensus/Cargo.toml`, `kaspad/Cargo.toml` | Feature `igneum-pow` forwarded (`kaspad -> kaspa-consensus -> kaspa-pow`) | `cargo build -p kaspad --features igneum-pow` selects the real engine | Low | Keep. |
|
||||
| `consensus/core/src/config/constants.rs` | Comment block only: the DAA constants kept at 1 BPS (sample every 4 blocks, 661 samples, 2,644-block window, min window 150 samples) and the two timestamp rules kept (132 s future tolerance in isolation, strictly above the sampled past median time of 27 samples in context) | Difficulty step verified rather than changed (fork-map c1, c2); the known gap (per-epoch hash-speed step vs a 44-minute window) is recorded there | None | Comment only. |
|
||||
| `igneum/miner/` (new crate `igneum-miner`), `Cargo.toml` (workspace member), `Cargo.lock` | Devnet miner: `--engine stub` (kHeavyHash, v0) or `--engine igneum-pow` (CPU warps through the node's own `IgneumEngine` cache, 64-bit nonces, random start per thread), `--worker <exe>` (GPU serve protocol: `job`/`found`/`done` lines, CPU re-check of every found nonce, one submit per template, `--exit-on-seed-change` exits 42 for ahead-of-time workers, `--worker-args`, `--status-secs`), `export-pack` (the pack for the node's current epoch and day plus `seeds.txt`), `bad-nonce` (submits an unmined nonce, expects a rejection); the epoch seed is derived as the node does it, walking from the sink over gRPC; `watch` and `inspect` as in v0 | Kaspa ships no miner; the devnet needs one that follows the fork's own PoW crate, and the GPU workers need a driver | None to consensus | Internal tool. Depends on `igneum-pow` by path and on `kaspa-pow` with the `igneum-pow` feature. Cross-compiles for `x86_64-pc-windows-gnu` with mingw-w64 (no rocksdb in its closure). |
|
||||
|
||||
## Finality v2 (3 Oct 2026, night): what landed and where
|
||||
|
||||
The rule is `docs/spec/03-finality.md` (implementation notes in its section 3.10). The crypto and the wire types are in `kaspa-consensus-core`, the processing in `kaspa-consensus`, the RPC in the usual six layers, the p2p relay in `kaspa-p2p-flows`. Nothing here touches the lottery hash or the coinbase amounts.
|
||||
|
||||
| File (vendor/igneum-node/) | What changed | Why | Risk | Upstream-merge note |
|
||||
|---|---|---|---|---|
|
||||
| `Cargo.toml`, `consensus/core/Cargo.toml`, `crypto/hashes/src/hashers.rs` | Workspace dependency `blst = "0.3.17"` (BLS12-381, minimal-pubkey setting: G1 keys 48 bytes, G2 signatures 96 bytes), `sha2` in consensus-core; new BLAKE2b domain `VoteKeyHash => "IgneumVoteKeyHash"` | The vote key, its hash in the header (W1) and the sortition VRF output (S1) | Low | Pure additions. `blst` compiles with mingw for the Windows miner. |
|
||||
| `consensus/core/src/finality.rs` (new), `consensus/core/src/lib.rs` | `FinalityParams` (MAINNET and DEVNET constants, Q3 thresholds 2/3 and 17/30 as integers), `VoteSecretKey::from_label` (ikm = BLAKE2b("igneum-vote-key-ikm-v1" \|\| label), standard BLS KeyGen, info "igneum"), `vote_key_hash(pubkey)`, proof of possession, `Vote` (index, checkpoint, pubkey, signature, sortition proof; 280 bytes), `Certificate` (index, checkpoint, voter count, signer bitmap over the canonical voter list, aggregate signature, aggregator key hash and VRF proof), `Evidence` (two votes), `KeyReveal` (`IGNK` + 288 hex chars of pubkey \|\| pop in the miner's extra data), the finality section codec at the end of the coinbase extra data (`items \|\| len_le32 \|\| "IGNF"`), domain tags `IGNEUM_VOTE_V1_...`, `IGNEUM_POP_V1_...`, `IGNEUM_SORTITION_V1_...`, vote message `"igneum-vote-v1/" \|\| chain_id \|\| 0 \|\| index_le64 \|\| checkpoint`; VRF = SHA-256 of the sortition signature, aggregator when `output x voters < aggregators x 2^64`; report types for the API | Everything two nodes must agree on byte for byte | Medium: consensus data formats | Pure addition. The DSTs and the message layout are frozen by the first public network. |
|
||||
| `consensus/core/src/config/params.rs` | `Params.finality: FinalityParams` (mainnet and testnet MAINNET; devnet and simnet DEVNET: interval 30, depth 20, window 7,200 DAA s, dust 5, presence 20 indices, 8 aggregators, ban 7,200 DAA s, min DAA 0); devnet `max_coinbase_payload_len` 16,384 (`MAX_COINBASE_PAYLOAD_LEN_WITH_FINALITY`); `OverrideParams.genesis_bits` (genesis hash recomputed from the header) and `OverrideParams.finality`; a suffixed devnet (`NetworkId::with_suffix(Devnet, n)`) keeps the devnet rules under its own `net` | The rule's parameters as network parameters; votes and certificates ride in the coinbase extra data, so the 204-byte Kaspa cap had to go on devnet; a private test network needs a low genesis difficulty and its own handshake magic | Medium: a node with the old 204 cap rejects every block that carries a finality section (cut-over note below) | Mainnet keeps 204 until its own value is set; a mainnet block with a certificate needs about 300 bytes plus 281 per vote. |
|
||||
| `database/src/registry.rs`, `consensus/src/processes/finality.rs` (new), `consensus/src/processes/mod.rs` | Prefix `IgneumFinality = 70`; `FinalityManager`: key registry (coinbase reveals with a valid proof of possession whose hash matches the header, and votes, which carry the key); weight table at a checkpoint (walk the selected chain of C through every chain block's `mergeset_blues`, count headers by `vote_key_hash` with DAA score in `(daa(C) - window, daa(C)]`, stop when the chain block's DAA score is a merge depth below the window), canonical voter list (above dust, not stripped, sorted by key hash); checkpoint determination (lowest selected-chain block with blue score >= 30 i, once the sink's blue score reaches 30 i + d; never revisited); votes with carriers (every block that carries the vote), the F3 block reading of participation (indices in `[i - P, i - 1]` with a carrier in the past of C_i, keys younger than P count 1); sortition from the vote's proof; certificate aggregation as soon as the votes seen meet Q3 (anyone MAY aggregate; a local eligible voter is named, else a zero aggregator), certificate verification (bitmap positions, `fast_aggregate_verify`, aggregator proof); lock test `3 x signed x P >= 2 x active_num` and `30 x signed >= 17 x total`; locks, `FinalityLock` notification; equivocation (second vote by a key at an index for another hash: evidence, weight zero until detection DAA + ban, evidence carried in blocks); template section (certificates and evidence not in the template's past, then up to 48 votes not in its past); state persisted as one bincode blob once a second | Spec 03 sections 3.1 to 3.6 at devnet scale | High: new consensus-adjacent state; the weight walk is O(window) per checkpoint (fine at 7,200, not at 2.6 million: mainnet needs an incremental window) | Pure addition. The manager holds its own lock and never takes it twice (the first run deadlocked the virtual processor through `compute_weights`). |
|
||||
| `consensus/src/consensus/services.rs`, `consensus/src/consensus/mod.rs`, `consensus/core/src/api/mod.rs`, `components/consensusmanager/src/session.rs` | `ConsensusServices.finality`; `ConsensusApi` gains `finality_weights_report`, `finality_checkpoints_report`, `finality_submit_vote`, `finality_submit_certificate`, `finality_template_section`, `finality_take_gossip` (default `unimplemented!()`, so mocks are untouched); async forwarders on the session | Reach the manager from RPC and p2p | Low | Mechanical. |
|
||||
| `consensus/src/pipeline/body_processor/processor.rs`, `consensus/src/pipeline/virtual_processor/processor.rs` | After `commit_body`, `finality.on_block_body(block)` reads the coinbase (reveal, votes, certificates, evidence) of every block, blue or red; in `resolve_virtual`, `finality.fork_choice_lock(depth_finality_point, tips)` returns the highest locked checkpoint in the future of the depth-based finality point that some body tip passes through, and it replaces the finality point for tip filtering and the sink search (F1 to F3); after the virtual state is committed, `finality.on_virtual_changed(sink)` determines new checkpoints and re-evaluates the open ones | The hooks of spec 03 | Medium: the virtual processor thread now runs the finality evaluation (under 10 ms at devnet scale); a lock no tip passes through logs a warning and leaves fork choice to depth finality | `resolve_virtual` is the conflict point with upstream; the two hooks are three lines. |
|
||||
| `rpc/core/src/model/finality.rs` (new), `rpc/core/src/model/{mod,message}.rs`, `rpc/core/src/api/{rpc,ops,notifications}.rs`, `rpc/core/src/convert/{notification,scope}.rs`, `rpc/grpc/core/proto/{rpc,messages}.proto`, `rpc/grpc/core/src/{ops.rs,convert/{kaspad,message,notification}.rs,ext/kaspad.rs}`, `rpc/grpc/server/src/request_handler/factory.rs`, `rpc/grpc/client/src/lib.rs`, `rpc/wrpc/{server/src/router,client/src/client}.rs`, `rpc/service/src/service.rs` | Methods `getFinalityWeights` (params, latest checkpoint, total and active weight, voters, per key: hash, pubkey hex, blocks, voter, participation, stripped-until DAA), `getFinalityCheckpoints` (`last` N: index, hash, blue score, DAA, state proposed/certified/locked, signed/active/total weight, fractions, votes seen, voters, aggregators, locked-at DAA, certificate aggregator), `submitFinalityVote` (hex of the vote bytes; accepted, reason, equivocation); notification `FinalityLock` (the lock event: index, hash, blue score, DAA, weights, fractions, voters) with `NotifyFinalityLock`; ops 154 to 156, 19 and 69; gRPC message numbers 1120 to 1128; `getBlockTemplate` appends the node's finality section after the miner's extra data | Miners vote and read checkpoints over RPC; the observer and the site read locks | Low | Every layer follows the `GetSinkBlueScore` pattern; the wasm client is not touched (not in the default build). |
|
||||
| `notify/src/{events,scope}.rs`, `consensus/notify/src/notification.rs` | `EventType::FinalityLock` (`EVENT_COUNT` 10), `FinalityLockScope`, consensus `FinalityLockNotification` | The lock event through the notification system | Low | Mechanical. |
|
||||
| `protocol/p2p/proto/{messages,p2p}.proto`, `protocol/p2p/src/core/{payload_type,hub}.rs`, `protocol/flows/src/{lib,flow_context,service}.rs`, `protocol/flows/src/v12/mod.rs` (new), `protocol/flows/src/v10/{mod,finality}.rs` | `IgneumFinalityMessage { kind (1 vote, 2 certificate), payload bytes }` as payload 70; `PROTOCOL_VERSION` 12 (a peer at 12 or later gets the v11 flows plus `FinalityRelayFlow`; 11 and 10 still peer and never receive finality messages); `Hub::broadcast_where`; `FlowContext::broadcast_finality` (peers with advertised protocol version >= 12 only); a 250 ms pump in `P2pService::start` drains `finality_take_gossip` (votes received over RPC, certificates built or relayed) to those peers; the relay flow verifies through consensus and leaves re-broadcast to the pump | Votes and certificates as their own p2p message (C2, C3) without disconnecting pre-v2 peers (an unknown payload is a protocol error in the router) | Low | Payload number 70 must stay unique; the version gate is the merge-safe way to add a message. |
|
||||
| `kaspad/src/args.rs` | `--devnet-suffix=N` (network id `igneum-devnet-N`, own data directory and handshake magic) | A private test network beside the shared devnet on one machine | None | Pure addition. |
|
||||
| `igneum/miner/src/main.rs` | `Identity` (BLS key from the identity label, `vote_key_hash` = hash of the key, key reveal in every template's extra data); `Voter`: once a second reads `getFinalityCheckpoints`, signs every new unlocked checkpoint (vote plus sortition proof) and submits it, prints `VOTE` and `LOCK` lines and a `VOTER SUMMARY` with lock latency as seen over RPC; `--no-vote`, `--equivocate` (also signs a wrong hash at every index; the node strips the key); the genesis hash is discovered from the node (pruning point walked to the parentless block) so a private test network with its own genesis mines correctly | The miner is the signer (the node never holds a key) | None to consensus | The Windows launcher is unchanged: the positional identity label is the key label, so `nvidia-<PC>-<i>` identities keep working, with new `vote_key_hash` values (weight restarts under the new hashes). |
|
||||
|
||||
Devnet compatibility and cut-over: the devnet genesis, network id and ports are unchanged, so a v2 node syncs the chain mined so far. Two things are one-way: (1) a v2 node accepts coinbase payloads up to 16,384 bytes and a v1 node accepts 204, so once a v2 miner votes, the blocks that carry a finality section are invalid to every v1 node; (2) a v2 node's p2p protocol version is 12 and it only sends finality messages to version 12 peers, so a v1 node peers but never sees votes. Cut over every node (node 1, the observer peer) and the Windows package together.
|
||||
|
||||
## R3 fixes, M15: PoW cost before validation (branch `r3-fixes`, 3 Oct 2026)
|
||||
|
||||
Review round 3 R3.26 (ledger M15): the lottery engine ran before the DAA-score and past-median checks, so a header with any past timestamp or claimed DAA score forced a 256 MiB cache build for an arbitrary day seed and evicted the honest entry (`KEEP` was 3). Branch `r3-fixes` (on top of the rename commit `d62708a8`, not the hot-swap work; the overlap note is below) reorders the checks, caps the builds and bans the peer that forces them. Measured before and after: `docs/bench-log.md`, the M15 entry (50 bogus headers built 50 caches in 10.6 s before, 0 caches and all rejected in 14 ms after).
|
||||
|
||||
| File (vendor/igneum-node/) | What changed | Why | Risk | Upstream-merge note |
|
||||
|---|---|---|---|---|
|
||||
| `consensus/pow/src/igneum.rs`, `consensus/core/src/errors/block.rs` | The engine is process-wide (`engine()`, `install_engine`); `check_header` returns a `PowCheck` with an optional `BuildReport`; `KEEP` raised 3 to 4; the chain's current and next day stay resident (`note_chain_day`, `is_live_day`); at most one build per seed pair, `MAX_INFLIGHT_BUILDS` 2 at once, a queue of `MAX_QUEUED_BUILDS` 4 then `PowEngineError::BuildQueueFull`; a 1,024-entry ring of per-header build reports; new `RuleError::PowCacheQueueFull` | Bound what a stream of headers can make a node build when the real engine is on (M15 rules 1 to 4) | Medium: changes the engine API (`check_header` signature and return) | The `PowEngine` trait signature moved; the stub still builds nothing. Pure additions to `igneum-pow` usage; the crate itself is unchanged. |
|
||||
| `consensus/src/pipeline/header_processor/processor.rs`, `.../pre_ghostdag_validation.rs`, `.../post_pow_validation.rs` | `validate_header` runs isolation, parents, GHOSTDAG, `pre_pow_validation` (pruning, DAA, difficulty), `pre_pow_context_checks` (blue score, blue work, past median), then the engine, then the rest of `post_pow_validation` (mergeset size, merge depth, indirect parents); the day seed uses the validated timestamp; `note_chain_day` fed from the headers-selected tip; one log line per cold build; the processor takes `engine()` | The cheap checks and the checks that choose the seeds run before the engine (M15), so a bad header never reaches a build; identical consensus result for a valid header | High: the header validation order moved | The conflict point with upstream and with any branch that edits `validate_header` (the hot-swap `epoch_seed` lead and `get_pow_epoch_info` live in the same two files on master). Merge by keeping the reordered body and the hot-swap `epoch_seed`; both call `check_pow_and_calc_block_level` last. |
|
||||
| `components/addressmanager/src/lib.rs`, `components/connectionmanager/src/lib.rs` | `ban_for(ip, duration)` on both; `MAX_BANNED_TIME` made public | A one-hour ban rather than Kaspa's full day | Low | Pure additions beside the existing `ban`. |
|
||||
| `protocol/flows/src/flowcontext/pow_guard.rs` (new), `.../flowcontext/mod.rs`, `.../flow_context.rs`, `.../v10/blockrelay/flow.rs`, `.../ibd/flow.rs`, `protocol/flows/Cargo.toml`, `Cargo.lock` | `PowGuard` counts an off-day cold build or a pre-PoW rejection per IP; `FlowContext::charge_pow_strikes` reads the engine's build report and the rule error; the relay and IBD header-join paths call it; more than `IGNEUM_POW_STRIKES` (default 2) in an hour disconnects and bans for an hour; flows depends on `kaspa-pow` | Per-peer accounting (M15 rule 3) | Low: additive; `flow_context.rs` and `Cargo.toml` overlap the finality-v2 additions | New file plus small hooks. `flow_context.rs` conflicts textually with the finality-v2 `FlowContext` additions; both are field and method additions, no shared lines. |
|
||||
|
||||
Tests: `kaspa-pow --features igneum-pow` cap tests (`one_build_per_seed_pair_under_contention`, `live_days_survive_off_day_builds`, `build_queue_is_bounded`), `kaspa-consensus` `cheap_checks_run_before_the_pow_engine`, `kaspa-p2p-flows` `pow_guard`, and the ignored measurement `measure_m15_attack_before_and_after`.
|
||||
|
||||
Merge note for the finality branch (which files overlap): `consensus/core/src/errors/block.rs` (both add a `RuleError` variant), `consensus/src/pipeline/header_processor/processor.rs` (finality adds no code here but the hot-swap branch does; the reorder is the conflict), `protocol/flows/src/flow_context.rs` and `protocol/flows/Cargo.toml` (both add to `FlowContext` and the manifest). No shared lines in any of them; each is additive on both sides.
|
||||
|
||||
## The rename (3 Oct 2026): what a miner, a user or an operating system sees
|
||||
|
||||
Trigger: macOS asked the project lead whether "kaspad" may access the local network. Everything visible from outside the source tree now says Igneum. Internal crate names, module paths, protobuf packages and Rust identifiers keep their upstream names (next table) so `git merge` against rusty-kaspa stays mechanical.
|
||||
|
|
@ -71,13 +107,17 @@ Devnet compatibility: the devnet genesis, consensus rules, ports, network id `ig
|
|||
| Decision | v0 choice | Why it is open |
|
||||
|---|---|---|
|
||||
| 8 or 18 decimals | Kaspa's 8 (`SOMPI_PER_KASPA`), so one coin is 100,000,000 units and the cap is 4e17 units, inside u64 | The zkEVM side expects 18 decimals (wei). 18 decimals put the cap at 4e27, which does not fit u64, so the UTXO amount type, mass rules and every RPC amount would change. Decide with the execution engineer before the EVM bridge; a fixed 1e10 scaling at the bridge is the alternative. |
|
||||
| Epoch seed | Hash of the last selected-chain block of the previous 3,600-block epoch; genesis for epoch 0 | The design uses a 10-minute class-group VDF over a certified checkpoint (bench-log, proto-vdf). The v0 rule is grindable in principle (a miner choosing which block ends an epoch) and needs the VDF and checkpoints to close. |
|
||||
| Day seed for the 256 MiB cache | `"igneum-day/" \|\| day_le64` with `day = header.timestamp / 86,400,000` | Timestamps are miner-chosen inside the two timestamp rules, so a day boundary can be straddled by a few blocks; harmless for a cache seed. Spec 01 O-1.10 proposes the first epoch seed of the day instead, which waits for the VDF schedule. |
|
||||
| Epoch seed | Devnet seed rule with a lead (hot swap, 3 Oct 2026): the seed of epoch `k` is the hash of the last selected-chain block whose DAA score is below `k x EPOCH - LEAD` (`EPOCH` 3,600, `LEAD` 600 DAA score, about 10 minutes at 1 block/s; `kaspa_consensus_core::igneum::{POW_EPOCH_BLOCKS, POW_EPOCH_LEAD, pow_epoch_seed_score}`); genesis for epoch 0. Every block template carries `pow_epoch` (`RpcPowEpochInfo`: epoch length and lead, the template's epoch index and seed, the boundary DAA score, the next epoch's seed once the sink has reached `boundary - LEAD`, the day index and the next one), so a miner sends `prepare` to its GPU worker one lead ahead and the worker swaps programs at the boundary with no pause and no process exit. Before 3 Oct 2026 evening: the last selected-chain block below the epoch's start score (no lead). `IGNEUM_POW_EPOCH_BLOCKS` and `IGNEUM_POW_EPOCH_LEAD` override both for private test networks (the lead is clamped below the epoch length); `IGNEUM_DEVNET_GENESIS_BITS` overrides the devnet genesis difficulty for such a network (the genesis hash moves, so it never peers with the real devnet). | This is the devnet stand-in for the 20-minute VDF lead of spec 04 (section 4.3: `C(e)` is the checkpoint at least 1,200 DAA s before the epoch, the VDF takes 600 s, so the program is knowable 10 minutes early). The design uses a 10-minute class-group VDF over a certified checkpoint (bench-log, proto-vdf); the devnet rule keeps the lead and drops the delay, so it is grindable in principle (a miner choosing which block sits at the seed score) and needs the VDF and checkpoints to close. The hash binding (`igneum-pow/src/bind.rs`) is unchanged. |
|
||||
| Day seed for the 256 MiB cache | `"igneum-day/" \|\| day_le64` with `day = header.timestamp / 86,400,000` | Timestamps are miner-chosen inside the two timestamp rules, so a day boundary can be straddled by a few blocks; harmless for a cache seed. On branch `r3-fixes` the day seed is computed only after the future and past-median checks pass (M15 below), so an arbitrary past day no longer reaches the engine. Spec 01 O-1.10 proposes the first epoch seed of the day instead, which waits for the VDF schedule. |
|
||||
| Lane hash to 256-bit target | Lane hash (64 bits) in the top 64 bits, low 192 bits zero; `pow <= target` is exactly `lane <= target >> 192` | Closed for the binding (3 Oct 2026, `igneum-pow/src/bind.rs`: the init words commit to the nonce-zeroed header hash and the high nonce word). Still open: the block level for pruning proofs reads `calc_level_from_pow` on a value whose low 192 bits are zero (`leading_zeros(lane)` shifted), and pruning-proof validation itself still uses the stub. |
|
||||
| GPU workers and the hourly program | Metal recompiles at runtime; CUDA and OpenCL are built ahead of time per pack and are rebuilt by the launcher at each epoch or day change (miner exit 42) | NVRTC and a runtime OpenCL rebuild inside the worker would remove the rebuild gap (about 30 s for CUDA). |
|
||||
| GPU workers and the hourly program | Hot swap (3 Oct 2026): the serve protocol has `prepare <epoch_seed_hex> <day_seed_hex> [<pack_dir>]`; the worker compiles the next program (and builds the next day's dataset) in the background, keeps at most two programs and two datasets resident, answers `prepared <epoch> <day> <ms> ...`, switches instantly on the first job with the prepared pair and drops the old pair after it. Metal compiles from the seed; OpenCL builds `<pack_dir>/kernel_bound.cl` at runtime; CUDA runs nvcc to two cubins in the background and loads them through the driver API (`cudaGetDriverEntryPoint`). The miner (`igneum-miner --worker`) writes the pack for the prepared seeds under `--prepare-packs` and sends `prepare` as soon as the template reports the next seed; `--exit-on-seed-change` (exit 42, launcher rebuild) is the fallback for a worker whose ready line says `prepare 0`. A worker crash is restarted by the miner after a random 5 to 60 s. | Measured on the Metal worker across a short-epoch test network: see `docs/bench-log.md`, hot-swap entry. CUDA and OpenCL prepare paths are written and compile-checked (OpenCL ran on the M5 Max through Apple's OpenCL); the CUDA path has not run on NVIDIA hardware yet. |
|
||||
| Proving pool payee | OP_RETURN burn tagged `igneum-proving-pool-v0` | Becomes a payout to the prover set of the proven block once the proving layer records prover sets (20 to 60 s behind the tip). |
|
||||
| Finality and pruning depths | Kaspa's 12 h and 30 h at 1 BPS | Upper bounds. Live finality is the 30-s certified checkpoint; pruning depth must stay above the longest checkpoint gap. |
|
||||
| PoW before or after GHOSTDAG | After | Needed for the chain-derived seed; costs GHOSTDAG work on invalid headers. A header-only seed (for example the VDF output carried in the header and verified against the checkpoint) would move it back. |
|
||||
| Finality and pruning depths | Kaspa's 12 h and 30 h at 1 BPS | Upper bounds. Live finality is the 30-blue-score locked checkpoint (v2 landed 3 Oct 2026); pruning depth must stay above the longest checkpoint gap and F3 (pruning point never past the latest lock) is not yet enforced. |
|
||||
| Checkpoint determination depth d | 20 blue score on devnet (spec placeholder 60) | C1 says d scales with the reorg-depth distribution the devnet records; 20 was chosen so a lock lands within about a minute at 1 block/s. |
|
||||
| Who aggregates | Any node, as soon as the votes it has seen meet Q3; a local eligible voter is named in the certificate, else a zero aggregator | Spec S1 says the 8 VRF-selected aggregators publish and anyone MAY; without a grace timer (Q4, O-3.4) the first node to see quorum publishes, which costs nothing in safety and favours liveness on a 4-node network. |
|
||||
| Vote and certificate carriage | Coinbase extra data, trailer-framed (`... IGNF`), devnet cap 16,384 bytes, 48 votes per block | Spec says "block body"; a dedicated transaction type would avoid the coinbase cap and is the mainnet form. |
|
||||
| Key reveal | `IGNK` plus hex in the miner's extra data through `getBlockTemplate` | The gRPC template request carries extra data as a UTF-8 string, so the 144 binary bytes became 288 hex characters. |
|
||||
| PoW before or after GHOSTDAG | After | Needed for the chain-derived seed; costs GHOSTDAG work on invalid headers. The expensive half of that cost, a 256 MiB cache build per bad timestamp or DAA score, is closed on branch `r3-fixes` (M15 below): the cheap checks run before the engine, the engine caps and bounds its builds, and a peer that forces off-day builds or sends pre-PoW-rejected headers is banned. The GHOSTDAG work on a bad nonce remains, bounded by the per-peer header rate and the same strike guard. A header-only seed (for example the VDF output carried in the header and verified against the checkpoint) would move the whole check back. |
|
||||
|
||||
## Per-second subsidy numbers (8 decimals, 1 BPS)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue