# Igneum fork divergence from rusty-kaspa 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 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 | File (vendor/igneum-node/) | What changed | Why | Risk | Upstream-merge note | |---|---|---|---|---| | `consensus/core/src/header.rs`, `consensus/core/src/hashing/header.rs` | `Header` gains `vote_key_hash: Hash` (32 bytes) as the last field; `new_finalized` takes it; `hash_override_nonce_time` writes it after `pruning_point` so the header hash and the PoW commit to it | Finality rule v2: vote weight is blue blocks per BLS vote key. The hash of the key rides in every header now so the weight table can be built from headers alone later (fork-map d) | Low by depth, high by spread: every header hash and every genesis hash moved | Any upstream change to `Header` or to the hashing order conflicts here. Re-derive the four genesis hashes after merging (`consensus/core/src/config/genesis.rs` tests print them). | | `consensus/core/src/config/genesis.rs` | All four genesis hashes recomputed; devnet genesis has payload `igneum-devnet`, timestamp 2026-10-03T00:00Z, nonce 0; bits `0x1d100000` (2^28 expected hashes per block, hash `edc4fa84...fb07`) for the GPU devnet (RTX 5090 229 MH/s + M5 Max 45 MH/s + gfx1036 4 MH/s measured, about 1.04 blocks/s); earlier the same day `0x1e400000` (2^18) for three 6-thread CPU miners on the real hash (0.294 MH/s: 825 blocks in 641 s) and `0x1e020000` (2^23) for the kHeavyHash stub | A devnet the miners at hand hold near 1 block per second until the DAA takes over at 600 blocks | Low | Mainnet, testnet and simnet genesis blocks are still Kaspa's content with new hashes. Replace them with Igneum genesis blocks before any public network. Every bits change moves the devnet genesis hash (print it with the ignored test). | | `consensus/core/src/errors/block.rs`, `consensus/src/pipeline/header_processor/pre_ghostdag_validation.rs` | `RuleError::MissingVoteKeyHash`; `check_vote_key_hash_present` rejects an all-zero `vote_key_hash` | Nodes only check presence until the finality layer exists | Low | Keep. The presence check becomes a key-registry check later. | | `protocol/p2p/proto/p2p.proto`, `protocol/p2p/src/convert/{header,block,messages}.rs`, `protocol/flows/src/v10/request_headers.rs` | `BlockHeader` wire message gains `Hash voteKeyHash = 15`; converters copy it both ways | p2p round trip of the field | Low | Field number 15 must stay unique if upstream adds header fields. Protocol version is still Kaspa's 11; bump it when the fork gets its own peers. | | `rpc/grpc/core/proto/rpc.proto`, `rpc/core/src/model/header.rs`, `rpc/core/src/model/optional/header.rs`, `rpc/core/src/model/verbosity.rs`, `rpc/core/src/convert/verbosity.rs`, `rpc/grpc/core/src/convert/{header,optional/header}.rs`, `rpc/service/src/converter/consensus.rs`, `consensus/client/src/header.rs` | `RpcHeader` and `RpcOptionalHeader` carry `vote_key_hash`; gRPC proto fields (`string voteKeyHash = 16` on the header, `optional string voteKeyHash = 15` on the optional header) and converters; wasm client header | RPC round trip, template to miner and block back | Low | Mechanical. Borsh wire for wRPC changed shape: old wRPC clients cannot decode headers. | | `consensus/src/model/stores/headers.rs`, `consensus/src/test_helpers.rs`, `mining/src/testutils/consensus_mock.rs`, `mining/src/template_limits_tests.rs`, `consensus/src/pipeline/virtual_processor/processor.rs`, `consensus/src/pipeline/body_processor/body_validation_in_isolation.rs` | Header store serde includes the field; template builder passes the field through; tests construct it | Storage and mining paths | Low | Database format changed: a node from before this commit must resync from scratch. | | `consensus/core/src/network.rs` | Network name prefix `igneum-` (was `kaspa-`) for every network id (`igneum-mainnet`, `igneum-testnet-10`, `igneum-devnet`, `igneum-simnet`); devnet ports gRPC 26610, wRPC borsh 27610, wRPC json 28610, P2P 26611 (Kaspa devnet: 166xx to 186xx); error text | The prefixed name is the p2p handshake magic (`FlowContext::handshake` rejects a `Version.network` mismatch), so `igneum-devnet` never completes a handshake with `kaspa-devnet`. Distinct ports stop a Kaspa node on the same host from being dialled by mistake | Low | Mainnet, testnet and simnet ports are still Kaspa's. Change them before any public network. | | `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`, `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/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` (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` on the processor; one `info` line per accepted or rejected PoW naming the engine (`PoW accepted 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 ` (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--` identities keep working, with new `vote_key_hash` values (weight restarts under the new hashes). | | `consensus/core/src/finality.rs` (fin-fixes, 4 Oct 2026) | `is_aggregator(output, weight, total_weight, aggregators)`: eligible when `output x total < aggregators x weight x 2^64` (was `output x voters < aggregators x 2^64`, a per-key draw); `FinalityParams.min_daa` = `weight_window` on both networks (mainnet 2,592,000, devnet 7,200; was 3,600 and 0) with `may_certify` and `window_filling`; new field `aggregator_fallback` (15 DAA s on both networks); `CheckpointsReport` gains `finality_reason`, `window_filled_daa`, `window_full_daa`; tests `sortition_is_by_weight_not_key_count` (200 dust keys plus 6 real ones) and `first_month_rule_is_the_full_window` | Ledger F17 (the 8 aggregators were drawn per key, so a 200-key splitter crowded real signers out) and ledger F1 (a 20-s burst locked checkpoints 1 to 10 alone on a young window), both measured by `tools/finality-attacks` on 4 Oct 2026; spec 3.4 S1, 3.8 and 3.10 | Medium: `min_daa` changes when the first certificate can form (a devnet needs 7,200 DAA s of history before any lock; a restarted devnet node reports the window as filling until its first virtual resolution); `FinalityParams` gains a field, so an `override-params-file` that sets `finality` must name `aggregator_fallback` | Pure addition inside the Igneum file. | | `consensus/src/processes/finality.rs` (fin-fixes, 4 Oct 2026) | `ingest_vote` passes the key's weight and the table total to the draw; `ingest_certificate` refuses a certificate whose checkpoint is under `min_daa`; `evaluate` never locks under `min_daa` and aggregates at once only when a local signer is a drawn aggregator, else after `daa(C_i) + checkpoint_depth + aggregator_fallback`; the determination log and `checkpoints_report` carry "finality not active, window filling, N of M"; test module `tests::no_certificate_while_the_window_is_filling` (TestConsensus chain of 150 blocks at a 60-DAA window, one key with all the weight) | The same two ledger entries, applied where the node decides | Medium: the fallback adds up to 15 DAA s to a lock on a node that serves no drawn aggregator (none on a network of 8 or fewer equal voters, where everyone is drawn) | The test module uses `TestConsensus` and `ConsensusApi` only; conflicts with the difficulty branch are none (it does not touch this file). | | `rpc/core/src/model/finality.rs`, `rpc/grpc/core/proto/rpc.proto`, `rpc/grpc/core/src/convert/message.rs`, `rpc/service/src/service.rs` (fin-fixes, 4 Oct 2026) | `GetFinalityCheckpointsResponse` gains `finality_reason`, `window_filled_daa`, `window_full_daa` (proto fields 8 to 10, serializer version 2) | Spec 3.9: the flag carries its reason | Low: a pre-fix gRPC client ignores the new fields (the fin-attacks miner drove the fixed node unchanged) | Additive. | | `igneum/miner/src/main.rs` (fin-fixes, 4 Oct 2026) | The voter prints `FINALITY (active=...)` whenever the node's reason changes | A window-filling first month is visible in the miner log | None to consensus | Line-local; the difficulty branch edits the same file elsewhere. | 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. Merge note for `fin-fixes` (4 Oct 2026, branched from master `2a00ff55`, which already contains the hot-swap branch, so no hot-swap conflict exists): against the difficulty branch (`3ea7a3e3`, files `consensus/core/src/config/params.rs`, `consensus/core/src/igneum.rs`, `consensus/src/consensus/services.rs`, `consensus/src/processes/{difficulty,window}.rs`, `igneum/miner/src/main.rs`, the integration tests) the only shared file is `igneum/miner/src/main.rs`, where fin-fixes adds one field to `Voter` and one `println!` in `tick`, both line-local to the voter, so the merge is textual at worst. Fin-fixes does not touch `params.rs`; the difficulty branch's `Params` edits do not touch `finality`. Merge fin-fixes first, then difficulty, and re-run `cargo test -p kaspa-consensus-core -p kaspa-consensus -- finality` after each. ## The rename (3 Oct 2026): what a miner, a user or an operating system sees Trigger: macOS asked Josh 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. | Surface | Before | After | Where | |---|---|---|---| | Node binary, process name, macOS and firewall prompts | `kaspad` | `igneumd` | `kaspad/Cargo.toml`: package still `kaspad`, `autobins = false`, `[[bin]] name = "igneumd"`; `cargo build --release -p kaspad --features igneum-pow` writes `target/release/igneumd` | | p2p user agent (the `Version` handshake message) | `/kaspad:2.1.0/` | `/igneumd:2.1.0/` | `core/src/kaspad_env.rs` `name()`; used by `protocol/p2p/src/convert/model/version.rs` and `protocol/flows/src/flow_context.rs`. The handshake also checks `network` (`igneum-devnet`) and protocol version (11, unchanged), so an old `kaspad`-named Igneum node and `igneumd` still peer | | CLI name, help, `--version` | `kaspad`, "Kaspa full node daemon (rusty-kaspa) v2.1.0" | `igneumd`, "Igneum full node daemon (igneumd) v2.1.0" | `kaspad/src/args.rs`, `kaspad/Cargo.toml` description | | Network flags | `--devnet` "Use the Igneum development network", `--testnet` "Use the test network" | `--devnet` (alias `--igneum-devnet`), `--testnet` and `--simnet` say Igneum | `kaspad/src/args.rs` | | Environment variables | `KASPAD_APPDIR`, `KASPAD_RPCLISTEN`, ... (40) | `IGNEUMD_APPDIR`, `IGNEUMD_RPCLISTEN`, ... (same suffixes) | `kaspad/src/args.rs` | | Default data directory | `~/.rusty-kaspa` (macOS, Linux), `%LOCALAPPDATA%\rusty-kaspa` (Windows) | `~/.igneum`, `%LOCALAPPDATA%\igneum` | `kaspad/src/daemon.rs` `get_app_dir`. Upstream never used Application Support on macOS; the Go-era help text in the trailing comment of `args.rs` that mentions it is dead text | | Log file names | `rusty-kaspa.log`, `rusty-kaspa_err.log` | `igneumd.log`, `igneumd_err.log` | `core/src/log/consts.rs` | | Startup banner | `kaspad v2.1.0-` | `igneumd/2.1.0-` | `kaspad/src/daemon.rs` | | Shutdown, FD-limit, DB-version and confirmation messages | "Kaspad has stopped", "The kaspad node requires", "Kaspad DB version", "pass --yes to the Kaspad command line" | igneumd in each | `kaspad/src/main.rs`, `kaspad/src/daemon.rs` | | Heap profile file (feature `heap`) | `kaspad-heap.json` | `igneumd-heap.json` | `kaspad/src/main.rs` | | UPnP port-mapping description shown by routers | `rusty-kaspa` | `igneum` | `components/addressmanager/src/lib.rs` | | Temp directory for database tests and tools | `/rusty-kaspa` | `/igneum` | `database/src/utils.rs` | | p2p and gRPC error text | "received kaspad p2p message", "Kaspad gRPC message" | igneumd | `protocol/p2p/src/core/router.rs`, `rpc/grpc/server/src/connection.rs` | | Address prefixes | `kaspa`, `kaspatest`, `kaspasim`, `igneumdev` | `igneum`, `igneumtest`, `igneumsim`, `igneumdev` | `crypto/addresses/src/lib.rs`; the stratum bridge accepts and defaults to the Igneum prefixes | | DNS seeders | nine Kaspa mainnet, three Kaspa testnet hostnames | none | `consensus/core/src/config/params.rs` | | Default build set | `cargo build` at the root built `kaspa-cli`, `kaspa-wallet`, `kaspa-wrpc-proxy` and the wasm bundles too | `default-members` leaves those out; they stay members, so `-p kaspa-cli` and `--workspace` still build them | `Cargo.toml` | | `igneum-miner` | no user-visible string named kaspad (checked) | unchanged | `igneum/miner/` | Kept on purpose, not visible outside the tree, so upstream merges stay clean: | Stays Kaspa-named | Why | |---|---| | Crate names (`kaspa-consensus`, `kaspa-p2p-lib`, `kaspad` the package, `kaspad_lib`, ...) and every `use kaspa_*` path | Renaming 60 crates touches every file in the tree; each upstream commit would then conflict on its first line | | Protobuf packages and messages (`protowire`, `KaspadMessage`, `KaspadRequest`, `KaspadResponse`, `kaspad_message::Payload`) | The gRPC and p2p wire names are matched by generated code on both sides; changing them is a wire-format fork for no user-visible gain (clients see the port and the methods, not the package name) | | The module name `kaspad_env` in `kaspa-core` | Internal; its `name()` now returns `igneumd` | | `SOMPI_PER_KASPA` and the 8-decimal unit constants | Open decision (8 or 18 decimals) in the table above; rename with the unit decision | | RPC `server_version` (`getInfo`) | Still the bare `2.1.0`, as clients and the observer parse it; the node name is in the p2p user agent and the banner | | p2p protocol version 11 | Not a name. Bump when the fork takes its own peers | | Mainnet, testnet and simnet ports and genesis content | Not names. Ports and Igneum genesis blocks for the public networks are a separate pass (see the rows above) | | Wallet, cli, wasm crates and the fork's README | Not in the default build and not shipped; their `kaspa:` test strings and docs are untouched | Devnet compatibility: the devnet genesis, consensus rules, ports, network id `igneum-devnet` and address prefix `igneumdev` are unchanged, so an `igneumd` built from this commit syncs the chain mined by the earlier `kaspad`-named build and the Windows miner keeps submitting to it (it derives `igneumdev` addresses; a change of the devnet prefix would have broken its templates at cut-over). Verified 3 Oct 2026 20:33 to 20:36 BST: a fourth node from `target-rename/release/igneumd` (built with `CARGO_TARGET_DIR=target-rename cargo build --release -p kaspad -p igneum-miner --features igneum-pow`, 2 min 31 s) on gRPC 26630, P2P 26631, appdir `/tmp/igneum-rename-test`, peered to node 1 at 127.0.0.1:26611, logged `igneumd/2.1.0-745d41ef` as its first line, completed IBD in under a second (140 headers, 140 blocks, sink `7f4cba28...3aa11`, the same sink and DAA score node 1 reported) and node 1 kept running. A fifth node peered to the fourth showed in `getConnectedPeerInfo` as `/igneumd:2.1.0/igneumd:2.1.0/` while node 1 showed as `/kaspad:2.1.0/kaspad:2.1.0/` (the doubled agent is upstream behaviour: `Version::default` and `add_user_agent` both write it). Note for test nodes: `--connect` sets the inbound limit to 0 and skips the P2P listener; use `--addpeer` with `--listen` when another node must dial in. Cut-over for node 1: stop the old `kaspad` process, start `igneumd --devnet` with the same `--appdir`, `--rpclisten` and `--listen`; the database format did not change. ## Execution layer (devnet v3, branch `execution-layer`, 3 Oct 2026) Implements `docs/design/execution-layer.md` D1 to D10 and section 8.2 on a 3-node simnet with CPU stub miners. Worktree `vendor/igneum-node-exec` on branch `execution-layer`, forked from `d62708a8` (the rename commit). Not merged into `master` yet; the merge plan with the finality branch is in the design document's implementation notes. Status words follow the design document: Implemented means it runs on the test network, prototype means a placeholder the design leaves to measurement. | File (vendor/igneum-node-exec/) | What changed | Why | Risk | Upstream-merge note | |---|---|---|---|---| | `consensus/core/src/evm.rs` (new), `consensus/core/src/lib.rs`, `consensus/core/Cargo.toml` (sha3) | `EvmTransaction = Vec` (raw EIP-2718 bytes), `evm_tx_hash` (keccak256), `evm_chain_id` (4461 / 4462 / 4463, simnet shares the devnet id), `miner_evm_address` (low 20 bytes of `vote_key_hash`), `BLOCK_EXECUTION_GAS_LIMIT` 30 M, `MAX_EVM_BODY_BYTES` 1 MiB, the `EvmTemplateSource` trait | Design D1, D8, 8.1; the devnet rule for the `miner` address until the body carries a 20-byte field | Low | Pure addition. | | `consensus/core/src/block.rs` | `Block` and `MutableBlock` gain `evm_transactions` (`Arc>` / `Vec`); `Block::new` keeps its signature (empty EVM list), `with_evm_transactions`, `from_arcs_with_evm`; `MutableBlock::set_evm_transactions` recomputes the merkle root and re-finalizes the header | Design D1: EVM transactions in the body | Medium by spread: every `Block {..}` literal in the tree had to name the field (six sites) | Upstream additions of `Block` literals will fail to compile until the field is added; the compiler finds them. | | `consensus/core/src/merkle.rs` | `calc_block_hash_merkle_root(utxo_txs, evm_txs)`: leaves are Kaspa's transaction hashes followed by keccak256 of each raw EVM transaction | Design D7: `hash_merkle_root` is reused as the body commitment and now covers the EVM transactions. With no EVM transactions the root equals Kaspa's, so no genesis hash moved | Low | Pure addition; `calc_hash_merkle_root` is untouched. | | `consensus/src/model/stores/block_transactions.rs` | Stored body is `BlockBody(utxo_txs, evm_txs)`; `get_evm`, `insert_batch` and `insert` take both | One store, one write per body | Low | Database format changed: a node from before this branch must resync. `insert` has one more argument; upstream call sites conflict trivially. | | `consensus/src/pipeline/body_processor/processor.rs`, `body_validation_in_isolation.rs`, `consensus/core/src/errors/block.rs`, `consensus/Cargo.toml` | Body validation checks the new merkle root, rejects any UTXO transaction besides the coinbase (`RuleError::UtxoTransactionsRetired`), and runs the state-free EVM body rules through `igneum_evm_types::validate_body` (`RuleError::BadEvmBody`: decoding, signature, chain id, types 0 to 2 only, intrinsic gas within the limit, no duplicate hash, per sender nonces sorted and contiguous, sum of gas limits within `B_e`, body bytes within the limit); `commit_body` writes the EVM part | Design D1 (UTXO transaction type retired from bodies), D4, D5 state-free class | High: changes what every node accepts as a body | Keep. The coinbase stays a UTXO transaction until the UTXO layer is stripped (see "what is missing"). | | `consensus/src/consensus/mod.rs`, `consensus/core/src/api/mod.rs`, `components/consensusmanager/src/session.rs` | `get_block` and `get_block_even_if_header_only` fill the field; new `get_block_evm_transactions(hash)` on `ConsensusApi` and `async_get_block_evm_transactions` on the session | Readers for the executor and the p2p body server | Low | Mechanical. | | `protocol/p2p/proto/p2p.proto`, `protocol/p2p/src/convert/block.rs`, `protocol/flows/src/ibd/flow.rs`, `protocol/flows/src/v10/request_block_bodies.rs` | `BlockMessage.evmTransactions = 3` and `BlockBodyMessage.evmTransactions = 2` (`repeated bytes`); `BlockBody` on the wire is `(Vec, Vec)`; IBD body sync and the body server carry both | p2p round trip of the body | Low | Field numbers 3 and 2 must stay unique if upstream adds fields to these messages. Protocol version still 11. | | `rpc/grpc/core/proto/rpc.proto`, `rpc/core/src/model/block.rs`, `rpc/core/src/model/optional/block.rs`, `rpc/core/src/convert/block.rs`, `rpc/grpc/core/src/convert/block.rs`, `rpc/service/src/converter/consensus.rs`, `rpc/core/src/model/tests.rs` | `RpcBlock`, `RpcRawBlock` and `RpcOptionalBlock` carry `evm_transactions` (gRPC `repeated string evmTransactions = 4` as hex; JSON hex list; Borsh serializer version 2 with a fallback for version 1) | Template to miner and block back carry the EVM part | Low | wRPC Borsh shape changed again (version field bumped, old shape still decodes). | | `rpc/service/src/service.rs`, `rpc/service/Cargo.toml` | `RpcCoreService` holds an `EvmTemplateSource`; `get_block_template` attaches the execution layer's selection to the mining manager's template with `set_evm_transactions` | Design D1: templates carry EVM transactions. The mining manager and its cache stay UTXO-only; the EVM part is selected per request | Low | A few lines in `get_block_template_call`; conflicts with the finality branch's RPC edits are line-local. | | `consensus/core/src/config/params.rs` | `SIMNET_PARAMS.blockrate = BlockrateParams::new::<1>().increase_max_block_parents(64)`, `pre_crescendo_target_time_per_block` from `OneBps` | Simnet = the devnet DAG shape (1 BPS, k 18, mergeset 180, merge depth 3,600) without proof of work, so a CPU test network of the execution layer runs the devnet rules | Low | Simnet genesis content and hash unchanged (bits and payload untouched). | | `kaspad/src/args.rs`, `kaspad/src/daemon.rs`, `kaspad/Cargo.toml` | `--evm-rpclisten=IP[:PORT]` (default port 26790 on devnet and simnet), `--evm-disable`; the daemon builds `igneum_exec::ExecService`, hands its template source to the RPC service and registers it as an async service | Wiring | Low | Mechanical. | | `igneum/evm-types/` (new crate `igneum-evm-types`) | Decoding through alloy (`TxEnvelope::decode_2718`), sender recovery, Cancun intrinsic gas, the per-transaction and per-body state-free rules; depends on alloy only so consensus and the executor share one decoder | Design 1.5 state-free class in one place | None to consensus beyond its use above | New crate. Pins `alloy-consensus 2.5`, `alloy-primitives 1.7`, `alloy-eips 2.5`. | | `igneum/exec/` (new crate `igneum-exec`) | The execution layer: `service.rs` chain follower (polls `get_virtual_chain_from_block`, builds segment(C) from `get_block_acceptance_data` which is written in `consensus_ordered_mergeset` order, unwinds reorgs from a 64-deep snapshot ring), `executor.rs` (revm 43 over the segment, rewards by rule, two-dimensional gas, fee flows, skip rule), `pgas.rs` (the prototype pgas table and the per-frame attribution inspector, CREATE rule for the registry), `state.rs` (in-memory revm `CacheDB`, MPT state root through alloy-trie as an output), `pool.rs` (EVM mempool), `rpc.rs` (`eth_*` and `igneum_*` JSON-RPC on axum), `registry.rs` and `contracts/DeveloperRegistry.sol` (system contract at `0x...0210`, runtime bytecode embedded), `bin/diff.rs` (`igneum-exec-diff`, the plain-revm differential harness) | Design sections 1 to 4 and 8.2 | None to consensus (the node runs without it under `--evm-disable`) | New crate. Pins `revm 43`, `alloy-trie 0.9`, `axum 0.8`. The in-memory state and the full-recompute state root are devnet scope (see "what is missing"). | | `igneum/miner/src/main.rs` | `--vote-key-hash ` (so a miner's EVM address, the low 20 bytes, is a key it holds), `--hold-ms ` (paces the stub miner on a network without proof of work), `--network simnet` (simnet address prefix for label addresses) | Test-network tooling | None | Internal tool. | ## Decisions recorded as open | 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 | 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 is a quarter of the lead past `boundary - LEAD` (the selected chain still flips between sibling tips right at the seed score; the quarter leaves three quarters of the lead for the prepare), 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 | Hot swap (3 Oct 2026): the serve protocol has `prepare []`; 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 ...`, switches instantly on the first job with the prepared pair and drops the old pair after it. Metal compiles from the seed; OpenCL builds `/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-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) | Period | Years | Per second (units) | Per second (coins) | Per block at 1 BPS | |---|---|---|---|---| | 0 | 0 to 2 | 3,168,808,781 | 31.68808781 | same | | 0, day 0 of the ramp (10%) | | 316,880,878 | 3.16880878 | same | | 0, day 15 of the ramp (55%) | | 1,742,844,829 | 17.42844829 | same | | 1 | 2 to 4 | 1,584,404,390 | 15.84404390 | same | | 2 | 4 to 6 | 792,202,195 | 7.92202195 | same | | 31 | 62 to 64 | 1 | 0.00000001 | same | | 32 and after | 64 on | 0 | 0 | 0 | Split of 3,168,808,781: producer 2,535,047,025 (80%, plus the rounding remainder), proving pool 633,761,756 (20%). Total over the schedule: under the 4,000,000,000-coin cap by less than 100 coins (test `total_emission_stays_under_the_cap`). ## Integration 4 Oct 2026: branch `devnet-v4` (release engineer) One branch, `devnet-v4` in `vendor/igneum-node` (worktree `vendor/igneum-node-v4`, head `dc749905`), merges every branch of 3 and 4 October in the order below. Every merge was built (`cargo build --release -p kaspad -p igneum-miner -p igneum-exec --features kaspad/igneum-pow`, `CARGO_TARGET_DIR=vendor/igneum-node/target-integration`, nice 19, 6 jobs) and its branch tests passed before the next merge. Commit messages carry the conflict notes; this section is the summary and the cut-over guide. Nothing of the live devnet (node 1 on 26610/26611, the observer peer on 26640/26641/28640, the seed relay on 26680) was touched; the test network ran on ports 28800 to 28923 under `/tmp/igneum-integration`. | # | Merged | Commit | Conflicts | Resolution | |---|---|---|---|---| | 0 | master | `2a00ff55` | | Finality v2 and the BLS miner. The hot swap was NOT in master: `git grep` finds no `pow_epoch`, `POW_EPOCH_LEAD` or `RpcPowEpochInfo` anywhere in `2a00ff55`, so the fin-fixes merge note above ("master already contains the hot-swap branch") was wrong. The hot swap lived only as the uncommitted working tree of `vendor/igneum-node-hotswap` (13 files, 832 insertions on top of the rename commit). It was captured as commit `a4224689` on branch `hotswap-wip` (a new worktree; the hotswap worktree was not touched). | | 1 | hotswap-wip | `a4224689` | `igneum/miner/src/main.rs` (14 hunks) | The BLS `Identity` and the `Voter` are kept and threaded through the hot swap's pipelined worker: one BLS identity and one voter per `--identities` label (`