igneum/docs/fork-divergence.md
2026-10-04 01:23:42 +00:00

69 KiB

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<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).

| 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 <reason> (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-<hash> igneumd/2.1.0-<hash> 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 <tmp>/rusty-kaspa <tmp>/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<u8> (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<EvmTransaction>> / Vec<EvmTransaction>); 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<Transaction>, Vec<EvmTransaction>); 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 <hex32> (so a miner's EVM address, the low 20 bytes, is a key it holds), --hold-ms <n> (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 <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-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 (<label>-i), the voters ticked by a background task off the mining loop; Seeder keeps master's genesis discovery plus the hot swap's per-block memo and walk_calls. Everything else merged clean (pow_epoch on the template beside the finality fields).
2 fin-fixes da1eb889 none
3 r3-fixes 8684775f none validate_header keeps the r3 order and epoch_seed keeps the lead; both call check_pow_and_calc_block_level last.
4 execution-layer b7fca5a0 Cargo.lock, rpc/service/src/service.rs, igneum/miner/src/main.rs The template carries both pow_epoch and the execution layer's EVM selection. Miner: --vote-key-hash is gone (the vote key hash is now the hash of a BLS key, so nobody holds an EVM key for its low 20 bytes); the payout address moves into the body as the coinbase extra-data tag `IGNA
5 difficulty 52eacad9 consensus/core/src/config/params.rs, consensus/core/src/igneum.rs, igneum/miner/src/main.rs The hardened commit (10 s timestamp rules, sanitised clock, 2^128 floor) had landed at 00:21 UTC, so no wait. Both master and difficulty had added OverrideParams.genesis_bits: one declaration kept, difficulty_rule beside it. igneum.rs: PowEpochInfo and the pow_epoch_* functions kept beside DifficultyRule and the difficulty module. Miner: ours (master's Seeder is a superset). One deliberate change beyond the text: the window manager's epoch length is pow_epoch_blocks() (3,600 on the devnet, the IGNEUM_POW_EPOCH_BLOCKS override on test networks), not the constant, so the difficulty rule's epoch lane and the program change agree on every network.
6 harness f42af9a2 Cargo.toml, Cargo.lock Both member lists kept (execution crates and harness crates).
7 windows-node 352495f6 none
8 exec-attacks 63ff5d99 none
9 diff-attacks 4806e21c consensus/src/processes/difficulty.rs The branch's should_panic flood reproduction is superseded by the hardened commit's igneum_flood_at_85_blocks_per_second_stops_at_the_minimum_target; the hardened test kept.

Test-only and bug-fix commits after the merges: 04b5a16d (p2p convert tests: BlockBody is a pair, BlockMessage has evm_transactions; the execution branch had not updated them, so cargo test -p kaspa-p2p-lib did not compile), 0e5124c6 (three Kaspa UTXO-body tests marked ignore with the reason: validate_body_in_isolation_test, validate_body_in_context_test, double_search_disqualified_test build bodies with UTXO transactions, which the execution layer rejects as UtxoTransactionsRetired; they failed on that branch before the merge), dc749905 (estimated_header_size in protocol/flows/src/v10/request_headers.rs counted four hash fields; the header has carried voteKeyHash since 815cd00f, so IBD header chunks were under-estimated by one hash field per header and estimated_header_size_covers_protobuf_size failed; a real bug on master, fixed).

Full suites at the end (cargo test --release --no-fail-fast over kaspa-consensus-core, kaspa-consensus, kaspa-pow, kaspa-p2p-flows, kaspa-p2p-lib, kaspa-rpc-core, kaspa-grpc-core, kaspa-rpc-service, kaspa-notify, kaspa-mining, kaspa-addressmanager, kaspa-connectionmanager, kaspa-database, kaspa-addresses, kaspa-txscript, igneum-exec, igneum-evm-types, igneum-miner, igneum-harness-sim, igneum-p2p-probe, kaspad, with igneum-pow on): 628 passed, 0 failed, 24 ignored (the 3 above plus the pre-existing ignored measurements) before the request_headers fix; kaspa-p2p-flows 30 of 30 after it. Per crate: consensus-core 75, consensus 83, pow 11, p2p-flows 30, p2p-lib 18, rpc-core 131, grpc-core 9, notify 20, mining 52, database 22 + 7, txscript 155, addresses 3, addressmanager 1, exec 6, evm-types 3, kaspad 2.

Devnet v4 rules that exist only in this integration

Rule Where Why
EVM payout address in the body: a miner appends `IGNA 40 lowercase hexto its template extra data beside theIGNK key reveal (igneum-miner mine ... --evm-address 0x...); the executor pays the block's rewards there (kaspa_consensus_core::evm::{MINER_ADDRESS_TAG, miner_address_extra_data, miner_address_in, block_miner_evm_address}, igneum_exec::service::miner_address_ofreads the coinbase of every segment block). A block without the tag keeps the devnet v3 truncation (low 20 bytes ofvote_key_hash`), which with BLS keys is an address nobody holds.
Difficulty epoch lane length = pow_epoch_blocks() consensus/src/consensus/services.rs Same value as before on the devnet (3,600); only differs under the test-network override.

Compatibility: devnet v4 is a new chain from the same genesis

A v4 node cannot follow the chain the devnet has mined since 3 October: (1) the body store format is BlockBody(utxo_txs, evm_txs) (execution layer), so the old database does not open; (2) the difficulty rule on igneum-devnet is IgneumDual, so the old headers' bits (Kaspa's sampled DAA) fail the difficulty check; (3) the timestamp rules are 10 s both ways, which old headers were never held to; (4) the coinbase payload cap is 16,384 (v2). The genesis, the network id igneum-devnet, the ports and the address prefix are unchanged, so the cut-over is: every node and the Windows package together, each node with an empty data directory, the old directory moved aside. An old miner (pre-hot-swap igneum-miner.exe) walks the epoch seed without the lead, so its blocks are rejected from the first epoch boundary (DAA 3,600): the PC switches to the new exe at the same time.

Test network, 4 Oct 2026 02:02 to 02:18 BST (full numbers in docs/bench-log.md)

3 igneumd from target-integration/release on igneum-devnet-880 (override file: genesis_bits 0x1f010000, finality window 300 DAA with min_daa 300; env IGNEUM_POW_EPOCH_BLOCKS=300, IGNEUM_POW_EPOCH_LEAD=60, so the swap crossed three boundaries in 16 minutes; stated here because the real devnet values are 7,200, 3,600 and 600), 3 CPU miners --engine igneum-pow at 3 threads, every miner voting, all paying one EVM address. 1,055 blocks accepted on every node, 0 rejected, sink identical on all three at 31 of 31 samples, difficulty 56,268 to 110,533 (dual-lane rule holding about 1.1 blocks/s on 0.23 MH/s). Boundaries at DAA 300, 600 and 900: the first block of the new epoch was accepted 2.23, 0.50 and 1.47 s after the last of the old (median inter-block gap 0.59 s, p90 2.21 s), every node built exactly 4 caches (genesis day plus three seeds), every miner's CPU program and cache for the new seed ready in 191 to 212 ms. Finality: window filling until DAA 300, first lock at checkpoint 11 (blue score 331, 3 of 3 voters, 100%), 24 locks to checkpoint 34, identical on all nodes, lock latency median 1.01 s (max 2.7 s), 34 of 34 votes accepted per miner. EVM: tools/evm-smoke 84 of 85 checks (chain id 4463, the miner funded at the IGNA address, 59 transfers executed across 10 chain blocks, contract deploy, call, receipts, logs, revert, state roots identical on the three nodes); the one failing check wants duplicate sends to land in parallel blocks within six attempts, and this PoW network had one tip at most samples, so no parallel block appeared in that window (the check was written for simnet miners paced by --hold-ms). igneum-exec-diff over the smoke's export: 199 segments, 59 transactions, 8 accounts, 0 mismatches. Harness scenario 5 on the merged node: 63 cases (46 RPC, 17 p2p), node up on every case, 0 cache builds (the five M15 cases that built on d62708a8 built nothing here; the node log shows one build, the honest day), the p2p M15 cases disconnected the peer (strike guard). Scenario 2 adapted to the merged timestamp rules in a copy of the harness (the repo copy still probes 132 s and pmt+1): rejected below max(pmt + 1, parent - 10 s), accepted at the floor, future flip between +10.00 and +10.02 s; stretch drift in the simulator +0.2% and -0.7% (PASS). Not exercised: the GPU prepare path (CPU miners only; the Metal, CUDA and OpenCL workers are unchanged by this integration).

Binaries (built from dc749905)

File Path Size
igneumd (Mac, arm64) vendor/igneum-node/target-integration/release/igneumd 40,463,680 bytes
igneum-miner (Mac) vendor/igneum-node/target-integration/release/igneum-miner 7,916,096 bytes
igneum-exec-diff, igneum-inject, igneum-p2p-probe, igneum-harness-sim (Mac) vendor/igneum-node/target-integration/release/
igneumd.exe (Windows x86_64) vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release/igneumd.exe 50,169,344 bytes (imports libstdc++-6.dll as before; make-package.sh ships the three mingw DLLs)
igneum-miner.exe (Windows) vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release/igneum-miner.exe 10,045,440 bytes
Windows package /tmp/igneum-integration/igneum-node-windows-v4.zip (made with proto-cuda/windows-node/make-package.sh vendor/igneum-node-v4 <zip>; vendor/igneum-node-v4/target is a symlink to target-integration for that script) 32,946,104 bytes

Cut-over (NOT performed; the morning session does it)

The live node is ./target/release/kaspad (pid 72039, started from vendor/igneum-node) under caffeinate -dims (pid 72041); the observer peer is igneum-obsnode (pid 73692) with tools/observer/observer.mjs (pid 74029, cwd the repo root, IGNEUM_RPC ws://127.0.0.1:28640); the Mac seed relay is pid 72139 from target-finality. All from /Users/joshm/Projects/igneum:

# 1. stop node 1 (caffeinate exits with it) and move the v3 database aside (devnet v4 is a new chain, see Compatibility)
kill -INT 72039; while kill -0 72039 2>/dev/null; do sleep 1; done
mv /tmp/igneum-devnet/node1 /tmp/igneum-devnet/node1-v3-2026-10-04

# 2. start igneumd v4 with the same appdir, rpclisten and listen, plus the two peers
cd /Users/joshm/Projects/igneum/vendor/igneum-node
nohup caffeinate -dims target-integration/release/igneumd --devnet --nodnsseed --disable-upnp --enable-unsynced-mining \
  --appdir=/tmp/igneum-devnet/node1 --rpclisten=0.0.0.0:26610 --listen=0.0.0.0:26611 \
  --addpeer=188.245.5.161:26611 --addpeer=192.168.68.67:26611 --nologfiles > /tmp/igneum-devnet/node1-v4.out 2>&1 &
# the execution layer starts with it (eth_ JSON-RPC on 127.0.0.1:26790; --evm-disable turns it off)

# 3. restart the observer peer from the same binary (fresh appdir) and observer.mjs
kill -INT 73692; kill -INT 74029
nohup target-integration/release/igneumd --devnet --nodnsseed --disable-upnp --appdir=/tmp/igneum-devnet/observer-v4 \
  --rpclisten=127.0.0.1:26640 --rpclisten-json=127.0.0.1:28640 --listen=127.0.0.1:26641 --connect=127.0.0.1:26611 \
  --nologfiles > /tmp/igneum-devnet/observer-v4.out 2>&1 &
cd /Users/joshm/Projects/igneum && IGNEUM_RPC=ws://127.0.0.1:28640 nohup node tools/observer/observer.mjs > /tmp/igneum-devnet/observer-mjs-v4.out 2>&1 &
# block numbers restart at 0 on the new chain; LIVE_TABLE_PREFIX=v4_ keeps the v3 rows apart if wanted

# 4. the Mac seed relay (pid 72139) and igneum-seed-1 (188.245.5.161) need the same binary: the relay from
#    target-integration with its current flags (infra/seed-nodes/addpeer-from-mac.sh), the VM through
#    infra/seed-nodes/provision-seed.sh (it builds from a source tarball of the worktree; point it at vendor/igneum-node-v4).
#    Until they run v4 they offer the v3 chain, which a v4 node rejects.

# 5. the Windows package: /tmp/igneum-integration/igneum-node-windows-v4.zip (igneumd.exe, igneum-miner.exe, DLLs, src.zip);
#    the PC's START-NODE.bat flags are unchanged; the miner adds --evm-address <its payout address> if it wants EVM rewards.

Where to read the run: /tmp/igneum-integration/net/logs/ (node and miner logs), /tmp/igneum-integration/net/samples.jsonl (30-s samples), /tmp/igneum-integration/net/smoke.log, /tmp/igneum-integration/harness/results/, /tmp/igneum-integration/cross-build.log.