126 KiB
Counter ASIC 3.0: the node side (program class v4 as a second height switch)
6 October 2026, worker "ca3-v4-node". Branches: ca3-v4-node (main repository: igneum-pow, the workers, the fast-time gate) and ca3-v4-node (the fork, from the 0.3.13 tip bb43e9a8). The 2.0 twin of docs/plans/counter-asic-2-node.md: every piece of its section 1 has its v4 twin below; v2 and v3 are byte for byte what they were (the pinned packs, igneum-pow/tests/packs.rs). The class: mx8+sh256x27 (docs/analysis/latency-shadow-2026-10-06.md), gated here as THE class v4 candidate on the project lead's word of 16:50 UTC. Nothing is published; the Mac's app is untouched (its miner stays paused); the live devnet is untouched. Gate rows: docs/plans/counter-asic-3-gate/node-gates.md.
1. What changed, where
| Piece | What |
|---|---|
igneum-pow generator.rs |
ProgramClass::V4, V4_CLASS = LoadClass { shadow: Some(ShadowClass { instrs: 256, reps: 27 }), ..V3_CLASS } (name mx8+sh256x27), GENERATOR_VERSION_V4 = 4, generate_era_generator (the era draw of class v3 over V4_CLASS, generator 4 stamped) used by generate_from_seed_bytes_program_class for (V4, Some(era)); ProgramClass::{load_class, generator_version, from_generator, name, parse} take v4; ProgramClass::has_era (v3 and v4); a v4 program's id is program_id(4, seed, attempt) (Program::program_id), its class Program::program_class = V4 from generator 4. Tests: program_classes (v4 round trip), new program_class_v4_is_class_v3_with_the_shadow_block (the base instructions, attempt and era draw equal class v3's of the same seed and era; shadow 256 x 27, 55,296 per hash; the id differs; the hash differs; v2's id bcc1248b10cc90f2 unchanged) |
igneum-pow lib.rs |
exports GENERATOR_VERSION_V4, V4_CLASS |
igneum-pow emit.rs |
program.h: IGNEUM_GENERATOR 4, IGNEUM_PROGRAM_CLASS "v4", IGNEUM_ERA_SEED_HEX (the v3 comment lines are byte for byte the 0.3.11 ones, so the pinned v3 packs do not change); the shadow lines (IGNEUM_SHADOW_INSTRS, IGNEUM_SHADOW_REPS, the for (sh ...) block in every kernel body) come from the class as before; program.json "program_class": "v4" |
igneum-pow packcheck.rs |
verify_pack_texts_chain accepts generator 2, 3 or 4 (PackFault::WrongClass otherwise, "2, 3 or 4" in the message); the era rule applies to every class with has_era (v3 and v4); test program_class_and_era_are_checked extended (a v4 pack with its era and shadow lines passes when v4 is wanted, is refused when v2 or v3 is wanted; a v3 or v2 pack is refused when v4 is wanted; the v4 era mismatch and the missing-era cases; the v3 header unchanged) |
igneum-pow main.rs |
`--program-class v2 |
proto-cuda/nvrtc/packfile.h |
pf_load accepts generator 2, 3 or 4 and names the class from it ("v4"); pf_pack_class_ok applies the era rule to every class that is not v2; pf_class_token unchanged (reads class=v4). emu/packfile-test.c: 7 new checks (a generator 4 pack loads as v4 with its era; v3 refuses it; it refuses another era; v4 refuses a v3 pack; the class=v4 token; a v3 class line on a generator 4 pack is refused); emu/packfile-test.sh 0 failures on the Mac |
proto-cuda/nvrtc/worker.cpp, proto-opencl/host.c |
no text change: both read the class and era through packfile.h (pairIsClass, takeClassTokens), so a pair's identity carries v4 and its era, a v4 job against a resident v3 pair of the same seeds answers need and program class mismatch, and the prepared v4 pair wins; host.c compiles clean (cc -fsyntax-only) against the new header |
proto-metal/main.swift |
isPackClass (v3 or v4) and packClassOf(generator): servePackProgram and servePackDataset accept generator 2, 3 or 4 and refuse the rest in plain words; a class=v4 line is compiled only from the prepared pack (its program_bound.metal carries the shadow block) and its day built from the pack's memhard.metal, keyed by (day, class, era); an inline class=v4 job with no prepared pair answers need and the class-mismatch error (2.0's G4b path); a class that is not v2, v3 or v4 is refused |
fork consensus/core/src/igneum.rs |
ProgramClass::V4 (generator 4, "v4"), has_era, dataset_class (V4 -> V3: the v4 day cache is the v3 day cache), program_class_for_epoch_at(epoch, N4, N5, L) with two switches (v4 from the first epoch at or above N5, else v3 from N4, else v2; N5 at or below N4 is v4 from N5), program_class_first_epoch_at, program_class_v4_first_epoch_at, the process-wide install_program_class_v4_activation / program_class_v4_activation_daa / program_class_v4_first_epoch; PowEpochInfo.program_class_v4_activation_daa (serde default never); the switch test sweeps both heights |
fork consensus/core/src/config/params.rs |
program_class_v4_activation_daa in Params and OverrideParams (never on devnet, simnet, mainnet; 0 on the testnet), override_params, From<Params>, the digest (unconditionally, right after program_class_v3_activation_daa), install_program_class_v4_activation, program_class_v4_first_epoch; tests: override_params_carry_the_program_class_v4_activation, the digest test's 15th edit, the pinned devnet digest re-pinned (section 2), fast_time_60x_file_is_the_devnet_at_60x (every field) |
fork kaspad/src/daemon.rs |
Program class v4 from the override file: active from epoch E (DAA score N5 rounded up to the epoch boundary at L*E, epochs of L DAA) beside the v3 line; install_program_class_v4_activation next to the v3 install |
fork consensus/pow/src/igneum.rs |
EpochSeeds::day_key keys the day cache on (day, class.dataset_class()), so a v4 epoch after a v3 one builds no new cache; era_bytes for every class with an era; pow_class_of maps V4; build_day builds the dataset class's day. Test program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache (one cache for v3 and v4 seeds of one day; the v4 program is generator 4, V4_CLASS with the era inside, the base instructions and attempt class v3's; the engine's pow equals the standalone epoch's; another era keeps the id and changes the program) |
fork consensus/src/consensus/mod.rs |
get_pow_epoch_info reports program_class_v4_activation_daa; the class of this and the next epoch come from program_class_for_epoch (both switches) |
fork consensus/src/pipeline/header_processor/pre_ghostdag_validation.rs, processes/pruning_proof/igneum_pow.rs |
unchanged: program_class_at(daa) answers v4 under the installed switches; the era seed is read for every class after v2 |
fork rpc/core/src/model/message.rs, rpc/grpc/core/proto/rpc.proto (field 19), rpc/grpc/core/src/convert/message.rs |
RpcPowEpochInfo.program_class_v4_activation_daa (serde default never); proto optional uint64 programClassV4ActivationDaa = 19 so a 0.3.13 node, which sends nothing, reads as never and not as 0 (v4 from genesis); program_class and next_program_class already carry a generator number, so 4 needs no field |
fork igneum/miner/src/main.rs |
the v4 activation installed from the template beside the v3 one (node program class v4 activation: ...); class_tokens: a v3 or v4 epoch's job and prepare lines end with `class=<v3 |
app/igneum-app/src/engine.rs |
confirmed, no change: miner_args pushes --prepare-packs for every worker (line 1478, since 0.3.11; the unit test every_worker_gets_the_prepare_directory_in_the_platform_form), so the Mac's Metal worker takes a v4 pack the way it takes a v3 one |
infra/fast-time/override-60x.json |
program_class_v4_activation_daa at never right after the v3 field. Two faults of the base file fixed on the way: the proving v1 block was in the file TWICE (two merges each appended it; the node refuses a duplicate field, so neither class harness could start a node from the file as committed), and the four 0.3.12 and 0.3.13 fields (proving_v1_fresh_rule_daa, exec_restart_number, exec_restart_hash, exec_restart_trust_daa) were missing (the fork's every-field test skips on the PCs, where the file is not beside the node). New CI check tools/ci/override-json-check.sh (every override file parses with no duplicate key; self-tested on a duplicate), registered in .github/workflows/ci.yml |
infra/fast-time/class-v4.mjs, infra/fast-time/README.md |
the G4 gate (section 3): class-v3.mjs's shape with two switches (--v3-activation 60, `--activation 150 |
2. The epoch-boundary rule with two switches
One epoch has one program, so both switches key on the EPOCH: epoch e is v4 when L * e >= N5, else v3 when L * e >= N4, else v2 (program_class_for_epoch_at); each height rounds UP to the next boundary and never splits an epoch. A block's class is a function of its DAA score alone (program_class_at).
| N4 | N5 | L | epochs | Source |
|---|---|---|---|---|
| 60 | 150 | 60 | e0 v2; e1, e2 v3; e3 (DAA 180) v4 | the gate run, section 3; the unit test |
| 60 | 181 | 60 | e3 v3; e4 (DAA 240) v4 | the unit test |
| 154,800 | 180,001 | 3,600 | e50 v3; e51 (183,600) v4 | the unit test (the devnet shape: v3 live from epoch 43, v4 at tip + 14,400 rounded up) |
| 150 | 120 | 60 | e2 v4 (N5 below N4: v4 from N5) | the unit test |
| any | never | any | the 2.0 answers, unchanged | the unit test sweeps N4 0..399 with N5 never |
| any | 0 | any | v4 from genesis (the testnet) | the unit test |
The digest: program_class_v4_activation_daa enters consensus_digest unconditionally right after the v3 field, so the digest flips the moment a binary carrying the field runs, as the v3 field did on 0.3.11. Measured on the fork (the pinned test consensus_digest_keeps_the_0_3_5_value_until_the_fee_switch_is_set): the devnet digest with no override file moves from c562d70e1428c9789823cc40067623b4767f7c555ce7ff4ea11c1498f013ef6c (0.3.11 to 0.3.13) to 3c50502174b031b9afe8aad3758aa55eb32e3ee20ffa6f9760b2e958547c0690. Consequence: a node on this binary is refused at the handshake by every 0.3.13 node and refuses them, which is the rollout's order step 1 (every node carries the object before any node reaches the height); the cut that carries it is a one-sweep binary rollout like 0.3.11's, and the Mac's app, PC 2 and the seeds all move in that sweep, nothing earlier.
The day cache: the v4 dataset is the v3 dataset (the same mixer x8 construction, growth rule and era layout; the shadow block touches no load and no item), so the node's engine keys its day caches on (day, dataset_class) and a v4 epoch reuses the v3 cache across the N5 boundary: no 1 GiB rebuild on the node at the switch (the kaspa-pow test: cached_days 1, cache_builds 1 for v3 and v4 seeds of one day). The Metal worker keeps separate (day, class, era) entries, so it builds the v4 day once at the boundary from the pack's own memhard.metal (the same bytes; about 41 ms on the GPU by the 2.0 figure).
3. The gates
See docs/plans/counter-asic-3-gate/node-gates.md for the rows in the rollout's section 7 shape and the summary files. The results, in short:
| Gate | State | The line that decides it |
|---|---|---|
| G4 | GREEN | run 1: 3 of 3 switch lines, 181 / 128 blocks across DAA 180, ids agree on all three miners, rejected 0/0/0, one sink 308/308/308; run 2 (--activation never): SUMMARY FAIL: NO v4 epoch seen, exit 1; re-run on 7c22d0d with the id assertion: PASS, every v4 epoch's id (3 of 3 miners) equals the CLI's class v4 id and differs from the same-seed v3 id (e3 30544487d1289d6e against 1ae1c9f0eda0cef5); its failed case (--id-check-against v4) fails on exactly that check |
| G4b | GREEN | three prepared ... class v4 lines (430, 139, 175 ms), every swap with no pause, 123 blocks accepted on v4, re-check mismatched 0, need 0, no mismatch, no exit 42 or 44 |
| G5 | GREEN | the two Windows workers e563126e... and 7fce1249... with the resource block, the Mac worker 30c70754..., the job's igneumd.exe 140be25e..., every sha256 listed |
| G6 | GREEN | PC 2 job build-20261006-155958, every stage ok, 392 s; kaspa-pow 15 with the v4 engine test, no flake |
3a. The program id carries the class (the coordinator's blocker, 7c22d0d)
The hash lane found the seven gate packs under proto-cuda/packs-ca3-v4 carrying the v3 control's program id 73bcbfe8ccf988f1. The cause was not the chain seam (the fork's kaspa-pow test asserts a v3 and a v4 program of one seed and era have different ids, generator 4 in the preimage) but the CLI's --era path: stamp_era in igneum-pow/src/main.rs stamped generator 3 on any class, and program_id(3, seed, attempt) is class-independent inside a generator (spec 01 section 1.4.6, the 2.0 rule: every era of one epoch seed shares one id), so a mx8+sh256x27 pack exported under an era read as a class v3 pack with the v3 id. A worker given such a pack would mine the v4 program under the v3 id and class token.
| Piece | What |
|---|---|
igneum-pow generator.rs |
era_generator_of(base) (4 when the base minus its era is V4_CLASS, else 3), used by generate_era; ProgramClass::of_load_class (V2, V3_CLASS, V4_CLASS, else none); the v4 test asserts the --era path and the chain path make the same v4 program, generator 4, and a measurement class (mx8+sh256x13) stays generator 3 |
igneum-pow main.rs |
stamp_era and show stamp from the class; show honours --program-class and --era-hex (the harness's same-seed v3 and v4 ids come from it) |
igneum-pow packcheck.rs, proto-cuda/nvrtc/packfile.h, proto-metal/main.swift |
the shadow block marks class v4: a generator 3 pack with IGNEUM_SHADOW_INSTRS is refused ("a class v4 program is generator 4 (export the pack as class v4)"), a generator 4 pack without it is refused; a generator 2 pack with a shadow (the ladder of packs-ca3-shadow, class-bearing ids) stays loadable; tests in packcheck.rs and emu/packfile-test.c (4 new checks, 0 failures) |
proto-cuda/packs-ca3-v4/* (7) |
re-exported with the same seeds, day and eras: generator 4, class "v4", id c120d7963abdcd96 (one id per epoch seed across its eras); every other line of every file byte-identical (kernels, memhard, program*.metal, every vector value); measured on the re-exported packs with proto-metal/packbench --batches 60 --batch-log2 24 --group 256 under with-lock.sh run (16:30 to 16:37Z, load average 90 from other agents' builds, a fingerprint is load-independent): v4-devnet-epoch0 f410c731b6bc2d31, v4-era-0 b115c410e08be6ca, v4-era-1 edc2b18fc67e9d1c, v4-era-2 604ed87109570559, v4-era-3 9541e2a41dde2ee6, v4-era-4 a9ffa2b67bd2e366, v4-era-5 1f34e9c465945249, every one equal to the hash lane's table (hash-gates.md); the control mx8-devnet-epoch0 untouched |
infra/fast-time/class-v4.mjs |
two new checks: every v4 epoch's id (the three miners agreeing) equals the CLI's class v4 id of that seed and era and differs from the CLI's class v3 id of the same seed and era; --id-check-against v4 is the assertion's own failed case |
| the fork | no change: the chain path stamped 4 from the first commit; the miner's job line names the class, the workers key a pair on (seeds, class, era), and the id is a pack label, not an identity, which is why the pack rule above is the guard |
4. Tests
| Where | What | State |
|---|---|---|
igneum-pow cargo test --release |
60 unit (the v4 class test, the v4 pack checks) + 7 derive + 4 mixer + 19 packs (the pinned v2 and v3 packs byte-identical) + 7 scratch | pass (Mac, 6 Oct 2026, 15:44Z) |
proto-cuda/nvrtc/emu/packfile-test.sh |
13 + 7 checks: the generator rule with 4, the v4 pack with its era, the token matcher | 0 failures (Mac) |
proto-opencl/host.c |
cc -fsyntax-only against the new packfile.h |
clean (Mac) |
proto-metal/main.swift |
swiftc -O (the G4b worker) |
built (Mac) |
fork cargo test --release -p kaspa-consensus-core |
the two-switch rounding sweep, the v4 params, the digest's 15th edit, the pinned digest, the fast-time every-field test | pass (Mac, 109 + 7, 2 ignored; PC 2 job build-20261006-155958: 109 + 7) |
fork cargo test --release -p kaspa-pow --features igneum-pow |
the v4 engine test beside the v3 one | pass (Mac, 15; PC 2 job: 15, the feature on through unification with igneum-miner) |
PC 2 build-job.mjs suites |
kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows + igneum-app |
G6 row |
5. Unverified, and what is owed
- The AMD vendor (G1 on the RX 9070 XT) and G2 (1,000 random hashes per card re-hashed on the CPU) are the hash side's gates, not this worker's; the OpenCL worker's generator-4 rule was checked by the C test and a syntax check, not by a live worker on a v4 pack (as 2.0's rollout read before its G1).
- The Metal worker's rate in G4b (18.6 MH/s wall) was taken under a load average of 5 to 12 with CPU miners beside it: not a measurement (the measured v4 rate is the shadow analysis's 26.5 to 26.9 MH/s on packbench under the measure lock).
- The era walk for era >= 1 and the pruning-proof era witness: unchanged from 2.0 section 7 (the v4 switch adds nothing there).
next_pairat an epoch boundary that is also an era boundary: the 2.0 note stands for v4 as for v3.- Wire:
RpcPowEpochInfogained one Borsh field (wRPC, versioned byGetBlockTemplateResponse) and one optional proto field; a 0.3.13 miner against this node reads the v4 height as never (the field absent) and would key epochs on the v3 switch alone, so every miner moves in the same binary sweep as the node (the rollout order of 2.0). - The build job has no
featureskey for test units (G6 row): the feature was on through unification; adding the key needs the PC's installed app to honour it, a 0.3.14 app item. - The 48 GiB APFS clone of the 0.3.13 target dir (
vendor/igneum-node-ca3v4/target-ca3v4, untracked) can be deleted after the cut.
6. PROPOSED: miner-signalled class activation (precondition 2 of the cut)
Status: PROPOSED spec text for spec 01 (a new section 1.12.2 beside the epoch rule) and spec 02; implemented behind the override on the fork branch ca3-v4-node (consensus/src/processes/class_signal.rs, the pure rule in consensus/core/src/igneum.rs), gated by the fast-time runs of section 6.4. Not in docs/spec until the project lead adopts it. The 2.0 fixed height stays, as the floor.
6.1 The rule
- The carrier: the block header's
versionfield, 2 bytes little-endian. The low byte is the block version as before (2 on every Igneum network;block_version_of). The high byte is the producer's OBJECT VERSION, the highest program class the node that built the template runs: 4 for this binary (CLASS_SIGNAL_V4), 0 on every block made before it (signalled_version(2, 4) = 0x0402;class_signal_of). Why the header and not the coinbase extra data: the tally is read in header validation and by a node that holds headers only (a pruning-proof sync, a headers-first IBD), the header is what GHOSTDAG orders and what the finality rule already reads for its weight (the vote key hash is a header field,finality.rs), and the bytes are already there: no new field, no new hash preimage, no proto change for the header. A node before this binary rejects any header whose version is not exactly 2 (check_header_version), which is why the signalling binary ships in the same one-sweep rollout as the digest flip it already needs (section 2); from this binary on, only the low byte is checked, so a later object (5, 6) can be signalled to it without another header rule. - The weight: blue blocks, the finality rule's convention. For epoch
ethe anchor is its seed blockS_e, the last selected-chain block whose DAA score is belowL e - lead(the block the epoch seed is already taken from,HeaderProcessor::epoch_seed; so an epoch's seed and its class are decided at the same block of the same chain). The window is theWDAA below and includingS_e: the selected chain is walked down fromS_e, every chain block's mergeset blues counted once (thecompute_weightswalk offinality.rs, bounded by the merge depth), a blue block counted when its DAA score lies in(daa(S_e) - W, daa(S_e)], and signalling when its object byte is at least 4. Share = signalling / total, in basis points. - The decision, per epoch, monotone. Epoch
eis class v4 when (a)L e >= N6, the floor (the fixed height, rounded up to the epoch boundary exactly as the v3 switch is:program_class_for_epoch_at), or (b) epoche - 1was v4, or (c) the window ending atS_eis FULL (daa(S_e) >= W) and its share is at least 9,500 bps. A signal moves the class one step: the rule answers v4 only where the floor rule answers v3 (below the v3 switch the answer is v2 whatever is signalled). The decision is memoised per seed block, so a fork of the chain has its own entries and one tally is paid per epoch per process. The epoch-boundary rounding of the v2 to v3 switch is unchanged: a class never changes inside an epoch, every block's class is its epoch's. - The window
W: 86,400 DAA (one day of blocks at 1 block/s),program_class_v4_signal_window_daain the override file, in the digest once set (the 0.3.15 rule), right after the floor; 0 = signalling off, the floor alone (2.0's rule, byte for byte). REVISED 0.3.16 (Horizon consensus-security): the threshold must hold in EACH ofCLASS_SIGNAL_WINDOWS= 7 CONSECUTIVE windows ending at the epoch's seed block (a week of days), because one day of 95 percent can be bought for a day (about 19 N of hash for 24 hours) and would force a flip onto a fleet not yet on the object, and seven consecutive days cannot be bought unnoticed; the floor is then set a week past the publish (DAA + 604,800 rounded up), not 14,400. The tally is one walk of7 WDAA bucketed per window; the decision rests on the weakest of the seven (programClassV4SignalWeakestBps), the console shows the newest (programClassV4SignalBps). Why a day: the share must mean "the fleet that mines, not the fleet that happens to be up this hour", and a day covers every box's daily pattern (the rented boxes come and go by the hour); it is long enough that 95 percent cannot be reached by a burst and short enough that the flip lands within a day of the last upgrade; the finality rule's 30-day window answers a different question (who may vote) and would hold the class for a month after the fleet was ready. On the fast-time profileWis 120 (two epochs of 60). - The threshold: 9,500 bps (95 percent), a constant (
CLASS_SIGNAL_THRESHOLD_BPS), not a file field, so no file can lower it; 95 percent of the blue blocks of a day is 95 percent of the hash rate of that day, the coordinator's figure; a box that cannot mine v4 (an old worker, section 2's wire note) is at most 5 percent of the hash rate at the flip, and the floor catches the rest. - The floor
N6:program_class_v4_activation_daa, set at the publish as DAA + 14,400 rounded up to the epoch boundary (the 2.0 rule for N4), checkedN6 - DAA >= 10,800. A stalled signal (a fleet that never reaches 95 percent) cannot hold the class forever: atN6v4 holds regardless.neveris allowed in the file and means no floor (the signal alone decides; not for the devnet publish). - What a node reports.
PowEpochInfoand the template'spowEpochcarryprogramClassV4SignalWindowDaa,programClassV4SignalBps(the share of the window ending at the SINK, the live tally the next decision is heading for),programClassSignal(this node's byte) andprogramClassV4SignalEpoch(the epoch v4 was decided by signal on this chain, when it has been); gRPC fields 20 to 23. The daemon printsProgram class v4 signal window from the override file: W DAA ending at each epoch's seed block, threshold 9500 bps of blue blocks; the fixed height is the floorbeside the floor line, andProgram class v4 by miner signal: epoch E (share X bps over W DAA ending at seed block S, threshold 9500 bps, N of M blue blocks)once per flip. Since 500ddd66 (the 0.3.18 pin) the live tally at the sink is memoised for 10 s per sink, and since fd7de1b4 (0.3.19) it is refreshed off the request path, so a template'sprogramClassV4SignalBpsis at most 10 s old (about 600 DAA on a 60x harness); the console reads it and nothing else does. The gate's share is the decision's own line ("Program class v4 by miner signal: epoch N (share S bps ...)"), a fresh walk at the epoch's seed block inside consensus, exact whatever the memo holds; the known-failed case counts its on-chain share from the blocks' version bytes. - The miner. Nothing: the node builds the template header (the miner varies the nonce), so the signal is the node's binary; the miner takes the class of the epoch and the next from the template as before (
next_program_classis the next epoch's decision once its seed block is known, else this epoch's class; a boundary that decides otherwise costs one refused pair and one prepare, the 2.0 era-boundary shape).IGNEUM_CLASS_SIGNAL=<n>on devnet and simnet only lowers a node's byte (the fast-time gate's non-signalling node); it is not read on mainnet or the testnet.
6.2 The devnet object changes shape
Two fields join the live object: program_class_v4_activation_daa is now the FLOOR (its name and its rounding unchanged: the 2.0 fixed-height form), and program_class_v4_signal_window_daa is the window (86,400 on the devnet; 0 turns signalling off). Both enter the digest (unconditionally, right after the v3 field), so the digest moves on the binary rollout once more; the pinned devnet digest of the fork's test is re-pinned to the value of this binary (section 6.4). The publish object and the rehearsal object are in docs/plans/counter-asic-3-rehearsal.md section 2. Defaults: devnet and mainnet 86,400, testnet and simnet 0 (the testnet is v4 from genesis by its floor of 0; the simnet keeps 2.0's rule).
6.3 What it does not cover (owed)
| Item | Why | What is done about it |
|---|---|---|
| A class-signal witness in the pruning-proof format | a node that synced from a proof holds no headers below its pruning point, so an epoch whose window reaches below it cannot be tallied; the node then takes the floor rule for that epoch and logs it (class_signal.rs), which can disagree with a full-history node for the epochs between a signal flip and the floor |
the same class as the era witness of 2.0 section 7 (MissingEraSeed); the floor bounds the exposure to at most N6 - flip; the witness (the per-epoch decision beside the epoch seed in the proof) is the next node item |
| The first tally after a restart | one walk of W chain blocks' mergesets per epoch per process, memoised; a day of blocks is about 86,400 header reads, under a second on the Mac's store |
measured in the fast-time runs below at W = 120 only; the devnet figure is owed from the rehearsal |
| A byte above 4 | accepted and counted as a v4 signal (a later object contains v4); a v5 rule would count bytes at or above 5 | nothing now |
6.4 The fast-time gate (three cases and the failed case)
infra/fast-time/class-v4-signal.mjs: three nodes on override-60x.json (v3 from DAA 60, window 120, the floor at --floor), each node's byte set by IGNEUM_CLASS_SIGNAL (--signal a,b,c), one real CPU miner each, the id assertion of G4 on every v4 epoch.
| Case | Run | SUMMARY | Summary file |
|---|---|---|---|
Two of three signal (--signal 4,4,3 --expect no-flip --epochs 7) |
17:26:4x to 17:34:57Z (load average about 9) | PASS: no v4 epoch over epochs 0 to 7; the share at the sink read 6,166 to 7,583 bps epoch by epoch (two CPU miners of three, the third's blocks byte 3); on the chain 286 blocks byte 4, 138 byte 3, genesis byte 0 (6,729 bps); no node printed the signal line; 0 rejected; one sink d845fdd016a25425 at 424/424/424; the floor line on 3 of 3 (never) |
class-v4-signal-no-flip.json |
All three signal (--signal 4,4,4 --expect flip) |
17:34:5x to 17:39:33Z | PASS: the share 10,000 bps from epoch 1; the class flipped at epoch 3 (DAA 180), the first epoch whose seed block has a full 120-DAA window below it, before the floor (never); 3 of 3 nodes printed Program class v4 by miner signal: epoch 3 (share 10000 bps over 120 DAA ending at seed block 13629115abf4b06bd9b07dfee66c7aab817cef268a34c02f8da6884d52f26017, threshold 9500 bps, 120 of 120 blue blocks) with the same epoch and share; 181 / 124 blocks across DAA 180; 0 rejected; one sink 322e4a57824ab7fd at 304/304/304; the id assertion on epochs 3, 4, 5: the three miners' v4 id equals the CLI's class v4 id and differs from the same-seed v3 id (e3 e48e6be7c6699824 against 6847355c88e8215f) |
class-v4-signal-flip.json |
Nobody signals, the floor at 300 (--signal 3,3,3 --floor 300 --expect floor) |
17:39:3x to 17:46:28Z | PASS: the share 0 bps throughout (424 blocks byte 3); no signal line on any node; the class flipped at epoch 5 (DAA 300), the floor, and not before; the floor line active from epoch 5 on 3 of 3; 301 / 124 blocks; 0 rejected; one sink aa8755bf4c1cc9e2 at 424/424/424; the id assertion on epochs 5 to 7 (e5 99f8a29e69d425fd against 48b8b87ff3a7e9ae) |
class-v4-signal-floor.json |
The known-failed case (--signal 4,4,3 --expect flip --epochs 7) |
17:20:34 to 17:27:04Z | FAIL as it must: no flip at 6,250 to 7,288 bps, and the harness reports FAILED CHECK on eight checks (template_switched_to_v4, switched_at_the_first_full_window_epoch, switched_before_the_floor, signal_line_on_every_node_same_epoch, signal_share_at_or_above_threshold, blocks_on_both_sides, the two id checks), exit 1 |
class-v4-signal-failed-case.json |
The first pass of the three good cases (17:01 to 17:20Z) had the same chain behaviour and failed on one harness check of its own (chain_carries_the_bytes demanded every byte on the chain be a node's and genesis carries byte 0); the check was corrected to allow the one genesis block and the three cases re-run above. Every number here is a count, a line or an id; the Mac's load average (about 9, other agents' builds) moves the wall times only.
6.5 Seven windows (0.3.16, fork 0760b844): the fast-time gate re-run
The rule changed on the 0.3.16 branch (Horizon consensus-security): 95 percent in EACH of CLASS_SIGNAL_WINDOWS = 7 consecutive windows ending at the epoch's seed block, decided on the weakest of the seven; one day of 95 percent can be bought for a day, seven cannot unnoticed. With the harness's 30-DAA window (two per 60-DAA epoch) the seven windows are 210 DAA and the first epoch whose seed block (at 60 e - 10) has 210 DAA below it is epoch 4 (seed at DAA 230), so a flip lands at DAA 240. A floor is REQUIRED for a signalling case since the 0.3.15 canary rule (a node stamps and reads the signal only with both v4 fields set); the first 0.3.16 pass of this harness (20:56Z) ran with the floor at never and every chain byte read 0. The four cases below ran with --floor 100000 (27 hours of fast-time chain away) or the floor under test, binaries from the fork at 0760b844 (target-ca3v4), 6 October 2026, load average 7 to 10 (other agents' builds on the box; counts and ids only here).
| Case | Run (UTC) | SUMMARY | Summary file |
|---|---|---|---|
Two of three signal (--signal 4,4,3 --floor 100000 --expect no-flip --epochs 7) |
20:57:5x to 21:04:45 | PASS: no v4 epoch over epochs 0 to 7; the weakest-window share at the sink 5,666 to 6,333 bps from epoch 4 (0 before: fewer than seven windows exist); on the chain 296 blocks byte 4, 129 byte 3, genesis byte 0 (6,948 bps); no signal line on any node; 0 rejected; one sink 5d0d7f21 at 425/425/425 |
counter-asic-3-gate/class-v4-signal7-no-flip.json |
All three signal (--signal 4,4,4 --floor 100000 --expect flip) |
21:04:5x to 21:10:29 | PASS: the share 10,000 bps in every window; the template flipped at epoch 4 (DAA 241 seen, boundary 240), the first epoch with seven full windows, before the floor; 3 of 3 nodes printed Program class v4 by miner signal: epoch 4 (share 10000 bps over 30 DAA ending at seed block a5adea6f..., threshold 9500 bps in each of 7 consecutive windows, weakest 10000 bps, shares oldest first [10000 x 7], 210 of 210 blue blocks); 241 / 124 blocks either side; 0 rejected; one sink 786d8071 at 364/364/364; the id assertion on epochs 4, 5, 6 (miners' id = CLI v4 id, differs from CLI v3 id) |
class-v4-signal7-flip.json |
Nobody signals, the floor at 360 (--signal 3,3,3 --floor 360 --expect floor) |
21:10:3x to 21:18:15 | PASS: share 0 bps in every window (485 blocks byte 3); no signal line; the class flipped at epoch 6 (DAA 362 seen, the floor's boundary 360) and not before; active from epoch 6 on 3 of 3; 361 / 125 blocks; 0 rejected; one sink 0db7f95e at 485/485/485; the id assertion on epochs 6, 7, 8 |
class-v4-signal7-floor.json |
The known-failed case (--signal 4,4,3 --floor 100000 --expect flip --epochs 7) |
21:18:17 to 21:25:10 | FAIL as it must: no flip at a weakest-window share of 5,333 to 6,333 bps (two CPU miners of three signalling; 288 blocks byte 4, 135 byte 3 on the chain, 6,792 bps); the harness reports FAILED CHECK on nine checks (template_switched_to_v4, switched_at_the_first_full_window_epoch, switched_before_the_floor, signal_line_on_every_node_same_epoch, signal_share_at_or_above_threshold, signal_line_names_seven_windows, blocks_on_both_sides, v4_ids_equal_the_cli_v4_id, v4_ids_differ_from_the_same_seed_v3_id) and exits 1; one sink 1c1eae6d at 423/423/423 |
class-v4-signal7-failed-case.json |
Fork suites on the signalling change (0562a7f2): kaspa-consensus-core 110 + 7 (the signal-rule test, the window in the params, digest and fast-time file tests; the pinned devnet digest re-pinned from 3c505021... to 7f2e49beabc253f327c5ac6bb457a674ea7f527af2971c95d3bdf65ef8bcf977), kaspa-consensus lib 98 (the template test now reads the low byte and the signal byte), kaspa-pow with the feature 15; PC 2: section G6 of node-gates.md.
6.6 The amended class v4 (AP-F8-1, 7 October 2026): object 5, the split protection and the earliest flip
the project lead's word (15:2x UK, through the coordinator): option A, the load-source rule in class v4's chain draw, on 0.3.20 (the feature node, mirror branch release-0.3.20-node). The hash lane's stamp (ca3-v4-amend a0aaca92): generator stays 4 and every generator-4 program id carries "sub/" || 1u16 little-endian inside igneum_pow::generator::program_id (PROGRAM_SUBVERSION_V4 = 1), so a binary from before the rule and one after it never share an id for one seed; packs carry IGNEUM_PROGRAM_SUBVERSION 1 and packcheck refuses a generator-4 pack without it. The node side on release-0.3.20-node: kaspa-pow pins the devnet epoch-0 vectors (the amended id 1a4230699a6b9c60 must equal, the 6 October stream's c120d7963abdcd96 must differ, the v3 control 73bcbfe8ccf988f1 unchanged); the daemon's window line names the object and the sub-version.
The mechanism: a fresh object byte, 5. CLASS_SIGNAL_V4 is 5: the amended binary stamps 5 and the tally counts a block when its byte is at least 5. Object 4 was stamped only by the unpublished dc141409 canary (c18-1), so no published block carries it; a byte-4 block never counts towards the amended flip. A node of the 6 October stream that sees byte 5 counts it as v4 (its rule is "at least 4"), flips to ITS stream at the same epoch and forks alone: every block it makes fails the amended node's id check and every amended block fails its own, so it is alone either way, and it is ours to upgrade in the sweep. Object 6 is class v5's. The two holds already in the rule stand and are what keep the window from opening early: a node stamps and tallies only when both v4 fields are in its file (the 0.3.15 canary rule), and the fields are published only after the one-sweep rollout of section 2, so the first signalling block comes after every node is on 0.3.20. The worker: the release is one bundle per machine (node and worker), and a worker that lags refuses the amended pack at packcheck and mines nothing after the flip; it costs that box, never the chain. Holding the window to the rollout length is not needed on top: seven full windows of a day each are the hold, 604,800 DAA, and the sweep takes about 40 minutes.
The arithmetic (approximate, measured 7 October 09:40Z). DAA 270,659 at the hub's tip; 1.095 DAA/s over the last 25 hours (1.165 over the last 4); epoch 3,600 DAA with a lead of 600; window 86,400 DAA, seven windows 604,800 DAA. From a publish of the two fields at T, after the sweep (about 40 minutes: 32 minutes for the 14 standing boxes one at a time, the hands and the seed 3 minutes, the Mac and PCs by update-now), the seven windows must lie wholly after the sweep's end D_s, so the flip epoch is the first e with 3600 e - 600 >= D_s + 604,800: the flip lands between D_s + 605,400 and D_s + 609,000 DAA, 6 days 9.6 to 10.5 hours later at 1.095 DAA/s (6 days 0 hours at 1.165). Earliest flip: T + 6 days 10 hours to T + 6 days 11 hours UK clock at the 25-hour rate; for a publish at 12:00 UK on 7 October, 13 October between 22:20 and 23:10 UK (about 13:00 UK at the 4-hour rate). The floor as it stands, 831,600, is 560,941 DAA away, about 13 October 09:00 UK at 1.095 DAA/s: it would fire BEFORE any seven-window signal from a publish today can complete, so it moves, per the 0.3.16 rule, to the publish DAA plus 604,800 rounded up to the epoch boundary: 882,000 for a 12:00 UK publish (DAA about 275,900 then), which fires about 30 minutes before the earliest signal flip. Either way the class flips about 6 days 10 hours after the publish, and never before every node has had the sweep plus a week.
The live file already carries both fields (found 7 October 2026, 13:0x UK, from hub-1's /root/fleet/override.json). The live devnet object since 6 October 22:49:45Z (the 0.3.16/0.3.17 publish; digest eada4bda...) carries program_class_v4_activation_daa 831,600 and program_class_v4_signal_window_daa 86,400, sixteen fields in all, so the 0.3.17 fleet has stamped object byte 4 since the 7 October 00:2x UK rollout and tallies byte 4 or above, and the floor is live: at 1.095 DAA/s from DAA 270,659 at 09:40Z, 831,600 lands about 13 October 09:00 UK, before any seven-window signal completes (the first full byte-4 window from about 7 October 00:30 UK, seven ending about 14 October 00:30 UK). A 0.3.17 node left on that file flips to the OLD v4 stream at epoch 231 whatever anyone signals; a 0.3.20 node flips to the amended stream at the same epoch; the two never share an id, so each straggler forks alone there. Therefore the 0.3.20 publish SHIPS a new file: the floor at the publish DAA + 604,800 rounded up to the epoch boundary (about 882,000 for a publish today), the window unchanged, the digest moving, and the one-sweep rollout replaces every 0.3.17 node before 13 October 09:00 UK; the shipper computes the floor at the publish and reads the digest on the 8097d600 binary. The earliest-flip arithmetic above stands (T + 6 days 10 to 11 hours by signal; the moved floor about 30 minutes earlier); the hard date is the one above: any node that misses the sweep is alone on 13 October 09:00 UK.
Testing, limited by the project lead's word: the digest test (the thirteen-field file b18ed271 unchanged, the sixteen-field object re-read on the amended binary; the sub-version is not a params field, so no digest moves), the mixed-version Devnet 2 gate on node-compat.mjs (an amended 0.3.20 node mining beside a 5899f603 node for ten minutes on the live file without the v4 fields, the old node accepting every block), and the fresh-join canary the 0.3.20 cut runs anyway; G4 to G6 recorded as owed. The fast-time signal harness (class-v4-signal.mjs) now stamps 5 for a signaller and 4 for the non-signaller, so its no-flip case also shows a byte-4 node not counting.
6.7 The 0.3.20 node line's gates (7 October 2026, UTC; the box runs CEST and earlier stamps in this section's messages read an hour fast)
The line on the mirror, branch release-0.3.20-node, pairing igneum-pow 8c728ca3 (object bytes settled by main: 0.3.20 ships class v4 sub-version 1 at byte 5, class v5 takes 6, class v4 sub-version 2 takes 7 after 0.3.20): 8097d600 (object 5, isSynced, the weight-table cache, the lazy snapshot, the submit path, the 100 ms template wait, the finality reports with signers), 6b94c823 (test-only: the stale PC 1 test), 6a3432a3 (the key methods, the claims, the settled claim floor, the listener watchdog), 09124180 (N12: a held exec port is a retry), b7cc37e7 (N13: a kept 0.3.17 datadir's virtual-state row read through a v1 mirror and rewritten), c4459193 (the watchdog's poll returns on shutdown), 55768f88 (N14: a bare node's program ids from the embedded verifying keys). Main's word (7 October 2026, 13:4xZ): THE PIN IS c4459193; 55768f88 (the program ids from the embedded keys, N14) is 0.3.21's first node commit and release-0.3.21-node starts at it; no earlier build is a pin for the one-box roll: every build since 10db4b61 dies at start on a kept datadir until b7cc37e7, and b7cc37e7 stops up to 10 s late.
| Gate | Binary | Run (UTC) | Line |
|---|---|---|---|
| Digest (thirteen-field base, sixteen-field refusal) | b7cc37e7, sha256 bc28331abf21f4d5 | 12:14:39 to 12:16:18 | PASS: thirteen fields a89be8a7 on both binaries, peers as the gate wants them; sixteen fields db9a85f9, refused, no peer; the live file's own digest eada4bda unmoved |
| Kept-datadir start (the fleet, p12-vast's copy of pool-1's 0.3.17 datadir) | b7cc37e7 | 12:17:12 and 12:18:53 | PASS: the first start reads the v1 row through the mirror and rewrites it ("1 mergeset rewards"), the finality blob converts (1,747 locks), the node up on its ports, no panic; the second start reads first-try, no rewrite line; 6a3432a3 died on the same copy (the known-failed shape) |
| Mixed-version, ten minutes beside the 5899f603 pair | b7cc37e7 | 12:16:39 to 12:27:21 | FAIL, the binary: clean to the restart step (one digest b0afb2ee on all five, 268 new and 392 old accepted, 0 rejected, counts equal at 324 and 502 through both joins), then the restarted new node died on its own datadir, "While lock file .../meta/LOCK: Resource temporarily unavailable" (conn_builder.rs:167): the listener watchdog from 6a3432a3 slept its whole 10 s poll before checking shutdown, so every node on the line since took up to 10 s longer to stop and the restart met the lock; three more failed checks follow from that one death. Fixed in c4459193 (the poll in 250 ms steps returning on shutdown; a_shutdown_returns_within_a_second_whatever_the_poll) |
| Digest | c4459193 (the candidate pin), sha256 45be9b02d1b002f5, the string read back | 12:33:27 to 12:35:05 | PASS: thirteen fields a89be8a7 on both binaries with the peers as the gate wants them; sixteen fields db9a85f9 refused, no peer; the live file's digest eada4bda unmoved |
| Mixed-version, ten minutes beside the 5899f603 pair | c4459193 | 12:35:26 to 12:45:39 | PASS: one digest b0afb2ee on all five, 215 new and 314 old blocks accepted, 0 rejected, plain header version 2, counts equal at 312, 441 and 529 through both clean joins and the restart step (the new node restarted on its own datadir and resynced), no panic in any log, every check green |
| Kept-datadir start | c4459193 (carried from b7cc37e7: the store code is identical; the fleet's canary re-reads it on this binary) | the b7cc37e7 lines above | |
| Shutdown within a second | c4459193 | 12:29 (build-2) | a_shutdown_returns_within_a_second_whatever_the_poll green in the exec suite's 31, beside the two other watchdog tests |
| The 12 GB settled-claim line (the fleet, p12-vast) | c4459193 | 13:23 to 13:27 | the claim 31 blocks behind the settled number, 8 of 8 shards proved in 136 s at 8.5 GB on a 3060, the submit refused: the node was started bare and built a zero-id statement (N14); not the binary's change but fixed on it |
| Exec suite with the bare-node ids test | 55768f88 (c4459193's child, sha256 279b1b690e854fc9, the string read back) | 13:29 | 32 passed, a_bare_node_resolves_its_program_ids_from_the_embedded_keys (known failed first) |
| Digest, mixed-version, the fleet's set | 55768f88 (0.3.21's first candidate under rule 4a) | from 13:37 | (recorded in the 0.3.21 node plan as they land) |
| Digest | 6b94c823 (the fallback, sha256 b1b7d47b) | 11:56:45 to 11:57:52 | compat and refusal digests right; the one failed check read n3's peers at 1 with its refusal line present (the refused connection's reconnect in flight; the read is now the minimum of five, 36d3efdc) |
| Mixed-version | 6b94c823 | 11:57:53 to 12:08:05 | one digest b0afb2ee on all five, 146 new and 246 old blocks accepted, 0 rejected, counts equal through both joins and the restart; the one failed check: six "Address already in use" panics in the two OLD nodes' server threads, the gate started the second the digest gate's nodes got SIGTERM on the same ports (a 20 s gap now) |
| Mixed-version on the live sixteen-field file (void: the binary at the path was replaced mid-run) | 8097d600 | 11:42:55 to 11:53:07 | the interop fact: one digest on all five, the 5899f603 hub accepted 235 byte-5 blocks from the new node with 0 rejected, old headers 1026 (byte 4), new 1282 (byte 5), counts equal on all five at 472 before the restart step |
| Digest | 6a3432a3 | 11:53:17 to 11:54:23 | the digests right; three of four nodes exited inside a minute on the shared exec port (N12), the mixed-version run stopped as void |
Rules this day added: a kept-datadir restart gate on a standing box's datadir copy beside the wiped canary, every node cut; no persisted struct gains a field under #[serde(default)] without a versioned read (bincode does not default); never rebuild a binary at a path a running gate uses; 20 s between the digest and the mixed-version gates; the harnesses set no exec ports, so a bind failure must never be a death.
6.8 Class v4 sub-version 2 (0.3.21): object byte 7, the re-pin recipe
Main's assignment (7 October 2026, 12:3xZ): 0.3.20 ships class v4 sub-version 1 at object byte 5; class v5 is 6 (class-v5 16afd0a0, class-v5-node 699db5a2, the flip case passed on 6,6,6); class v4 sub-version 2 (the hash lane's dataflow-freshness rule plus the saturated-source count, on ca3-v4-amend after 8c728ca3) takes 7. Sub-version 2 is 0.3.21's and its re-pin is not in the 0.3.20 path. The re-pin on the node side, once the hash lane's commit string and the new devnet epoch-0 id land: archive that commit's igneum-pow beside the fork as the paths-override copy and rsync it to the boxes; CLASS_SIGNAL_V4 5 → 7 with its tests (0x0702, 1794; a byte-5 block does not count towards the sub-version-2 flip); the kaspa-pow vector test's pins moved (the sub-version-2 id must-equal, with PROGRAM_SUBVERSION_V4 2; sub-version 1's 1a4230699a6b9c60 joining c120d7963abdcd96 as must-differ); the daemon's window line naming object 7 and the two earlier streams; the fast-time signal harness stamping 7 for a signaller and 5 for the non-signaller; then the kaspa-pow and consensus-core suites on build-2. The script that applies the fork side in one run sits in the lane's scratchpad (repin-sub2.py: the commit and the id as arguments); about 20 minutes of edits and 2 minutes of suites. The same split-protection arithmetic as 6.6 applies: a byte-5 node that sees byte 7 counts it as v4 (its rule is at least 5), flips to ITS stream (sub-version 1) at the same epoch and forks alone, every block refused by the sub-version-2 id check; the sweep replaces every 0.3.20 node before the sub-version-2 floor, and the floor sits a week past the 0.3.21 publish as 6.6 sets it.
6.9 The 0.3.21 node line (opened 7 October 2026, 13:4xZ)
Main's word: the 0.3.20 pin is c4459193; release-0.3.21-node opens at 55768f88 (N14, the program ids from the embedded verifying keys). The order as it stands at 15:4xZ, each with its own gate line, one rebase round or 0.3.22: 55768f88; the test-only c631c64b first (the two consensus integration test targets, igneum_installed_signals.rs and igneum_order_tests.rs, asserted the signalled header 1026 where it carries 1282 since 8097d600; they are separate test binaries outside the morning's suite set and read red on every commit since, the 0.3.20 pin included, recorded as known red there with the code right; from now every suite set runs the consensus crate whole, cargo test -p kaspa-consensus without --lib); the late-join commit 52e96c94 (70e4601e rebased onto 55768f88; N9's second half); miner-reliability-20 f067f7c1; pool-finish-node b0444f51; horizon-node 437f0438 (6eb21fc9 rebased onto c4459193 by the horizon lane; the fork gate, its field at never); peer-directory-node 2e32d5f6 (db28d331 rebased; its activation at never; one both-add of DST_ADDRESS beside pool-finish's DST_BINDING in consensus/core/src/finality.rs, kept at the merge); the genesis-forward lane's f95178a1 (the difficulty manager reading params.pow_epoch_blocks, the lib-suite flake's fix); then N15 (the chain-path continuity rule and the start-time self-check, branch numbering-fix). The class v4 sub-version 2 re-pin is dropped (sub-version 2 fails the attack-pass lane's 64-seed hot-set gate); a sub-version 3 re-pins on top only if it reads green on both attack-pass gates by 19:00Z, else it is 0.3.22's; 0.3.21 stages on byte 5 as 0.3.20 does. The staging starts at the shipper's sweep-end word; the suites run on build-2 between merges and the digest is read after every one (eada4bda on the live file, the moved value on the moved file, since every switch is at never); every candidate binary takes every gate from its build (rules 4a and 4b).
| Gate | Candidate | Run (UTC) | Line |
|---|---|---|---|
| Digest | 55768f88, sha256 279b1b690e854fc9, the string read back | 13:35:41 to 13:37:19 | PASS: thirteen fields a89be8a7 on both binaries, the peers as the gate wants them; sixteen fields db9a85f9 refused, no peer; the live file's digest eada4bda |
| Mixed-version, ten minutes beside the 5899f603 pair | 55768f88 | 13:37:40 to 13:47:52 | PASS: one digest b0afb2ee on all five, 223 new and 381 old blocks accepted, 0 rejected, plain header version 2, counts equal at 319, 486 and 604 through both clean joins and the restart step, no panic in any log |
The staging (15:41Z on, at the shipper's sweep-end word), each step merged on release-0.3.21-node with the exec suite on build-2 and a push after it: c631c64b → 7ee808d6 (32), 52e96c94 → 78f0e771 (33), f067f7c1 → be60803a (33), b0444f51 → 4362119b (34), 437f0438 → 632437a0 (34), 2e32d5f6 → f2caa145 (34; the DST_ADDRESS both-add kept), f95178a1 cherry-picked → 31407f27 (the consensus lib 120 passed, 3 ignored; a merge of that commit pulls the whole genesis-forward branch, whose other commits conflict in four files, so the one-file commit is picked alone), d8bceca5 → 96161037 (36). The 60x file test read red on the tip for the two new switches (pool split, peer directory) until the file carried them as null (the rule: after every merge that touches OverrideParams, the file's field diff is re-run). The whole set on 96161037, build-2 (the checks on build-1), 15:49 to 15:57Z: consensus-core 126 passed, 2 ignored; the consensus crate whole, the lib 120 passed with 3 ignored and both integration targets 1 passed each; igneum-miner 25 passed against the 8c728ca3 packs (the pool tests read the packs of the paired igneum-pow's stream four dirs above the crate: the parent's 6 October stream failed the class v4 re-check, the rule is in the box placement list); kaspa-pow 17; igneum-exec 36; the kaspad, flows and rpc-service checks green. Binary built on build-1 at 15:58Z at the 0321 worktree path, igneumd sha256 f99340b3cf4ce32e, the string read back; the digest eada4bda on the previous live object and 4bbbe816 on the published floor file (floor 900,000, window 86,400), as the pin reads them.
| Gate | Candidate | Run (UTC) | Line |
|---|---|---|---|
| Digest | 96161037, sha256 f99340b3cf4ce32e, the string read back | 15:59:54 to 16:01:32 | PASS: thirteen fields a89be8a7 on both binaries, the peers as the gate wants them; sixteen fields db9a85f9 refused, no peer |
| Mixed-version, ten minutes beside the 5899f603 pair | 96161037 | 16:01:53 to 16:12:05 | PASS: one digest b0afb2ee on all five, 252 new and 352 old blocks accepted, 0 rejected, plain header version 2, counts equal at 316, 493 and 604 through both clean joins and the restart step, no panic in any log |
| The fleet's set (the kept start with the ids gate, the wipe canary, the cases rerun, the 12 GB settled-claim line, N15's restarts of p1-5090 and p2-3090-3) | 96161037 | from 16:00 | (the fleet lane's lines) |
6.10 The 0.3.22 node line: Devnet 3 (opened 7 October 2026, 16:3xZ on main's clock change)
the project lead's clock through main: Devnet 3 goes now, genesis by 18:30 BST. release-0.3.22-node on 96161037: bd710a36 the class v4 sub-version 3 re-pin (object byte 7; igneum-pow 017e7037, the audit-freeze-2026-10-07 tag, archived beside the fork as igneum-pow-amend; epoch-0 id a785001687d8688a must-equal, c120d796 / 1a423069 / a7886616 must-differ), aded4620 the era VDF merge (the era lane's f2ecf452, rebased on 96161037), fa7f854f the igneum-devnet-3 object, 21d8f454 test-only (object 7 = 1794 in the two signalling integration tests, sub-version 3's retried draw attempt, the eight era fields in the miner's pool test). The candidate is 21d8f454.
The object: --devnet --devnet-suffix=3, network igneum-devnet-3, default P2P 26631 (a suffixed devnet takes ten above the previous; build-1's seed listens there), gRPC/JSON/EVM on the devnet defaults, data dir igneum-devnet-3, EVM chain id 4463 (the devnet's). Genesis 2026-10-07T00:00:00Z, bits 0x1d100000, payload "igneum-devnet-3 | 2026-10-07 | every upgrade on from block zero | coins here have no value | resets are announced", hash a6fa348e2a0fc6a5fa0ae3cb080160f5e2be865af71bac0068ca2fb8d1fcfd7b (computed on build-2 by devnet3_genesis_hashes, which names both constants in one run). Active from DAA 0: difficulty v2, proving v0 and v1 (8 blocks a segment, 600 DAA, 1,000 bps, the fresh rule), finality v3, program class v3 and v4 (byte 7 stamped from genesis under a one-day signal window), the latency ladder (rung 0, stepped by signal), calibrated v1 fees, the era VDF (era 15,552,000 DAA, lead 7,200, class group, T 108,000,000; the first cut about 180 days out; O-4.10 owed before it). At never, main's ruling: difficulty v3, the finality DAA-seconds rule, the fork gate, the peer directory, the signing bonus, the finality leave rule, the pool split, the consensus proof verify, the exec restart; each goes live by activation height after its own gate, no further reset. NetworkId::params_compiled (mainnet, testnet, devnet suffixes 3 to 99) refuses --override-params-file; the digest is read with no file: ab9af79f40cd9b5fbed2577406d6b32e103bd86a7dd26df2aabceb7b673ced23.
| Gate | Candidate | Run (UTC) | Line |
|---|---|---|---|
| Build-1 release build (gate priority) | 21d8f454 | 16:43:15 to 16:45:19 | GREEN: igneumd sha256 b96b424c7b284501...fcca21d carries 21d8f45; igneum-miner c29f33bb...; artefact /srv/artefacts/0322-21d8f454/ |
| igneum-exec suite (build-2) | 21d8f454 | 16:45:31 | GREEN 36 of 36 |
| kaspa-consensus-core (build-2) | 21d8f454 | 16:44:40 and 16:48:25 | GREEN 151 of 151 (the parent's fast-time 60x file carries the era fields, d4162d9c) |
| kaspa-pow, igneum-pow feature (build-2) | 21d8f454 | 16:47:10 | GREEN 17 of 17, pairing id a785001687d8688a |
| kaspa-consensus whole incl. the two integration targets (build-2) | 21d8f454 | 16:47:54 | GREEN 120 + 1 + 1 at object 7 |
| igneum-miner (build-2) | 21d8f454 | 16:48:48 | GREEN 25 of 25 against the sub-version 3 packs (copied from the amend worktree into proto-cuda/packs-ca3-v4 beside the fork on both boxes; the sub-version 1 set kept as packs-ca3-v4-sub1) |
| Canary set from the artefact (build-1) | 21d8f454 | 16:45:53 to 16:47:11 | GREEN: empty-datadir start lines (object 7 / 1794, era VDF from DAA 0, ladder rung 0, fees v1, proving ids from the embedded keys), digest ab9af79f with no file, shutdown 571 ms after SIGTERM, override file refused exit 1 (the shared devnet still takes it, eada4bda), two empty nodes handshake with equal digests, a shared-devnet node rejected "Network mismatch - local: igneum-devnet-3, remote: igneum-devnet" |
| Fresh-genesis agreement on two boxes, the late joiner | 21d8f454 | fleet | (the fleet lane's lines) |
Two findings on the 21d8f454 artefact (16:5xZ), both the fleet's: (a) the engine read-back rule counted "igneum-pow/src/" paths and read zero on the fork-worktree build, which links igneum-pow from the untracked archive named igneum-pow-amend (six "igneum-pow-amend/src/" paths); the rule's pattern becomes igneum-pow*/src/ and the build was re-admitted as byte-equivalent to the build-server lane's hands pair (d208b867, built in the app tree, "igneum-wt-bs0322/igneum-pow/src/" paths). (b) The real fault: the genesis timestamp was 2026-10-08T00:00:00Z (a day's slip: 0x1a0ff0f7c00 is 3 October, counted as 4), so every block any miner found read "the block timestamp is too far into the future" on every node and dn3-g1 accepted nothing in five minutes on either pair. Fixed in 69d1b56e (17:01:30Z): timestamp 1,791,331,200,000 ms = 2026-10-07T00:00:00Z, genesis hash 4020cb4382e3fe4b281c817c02582e147d8f851f566ae9172b28912b8e68b925 (the merkle root unchanged), digest 83eb50cdf2eda4cbd698151653f0bc6c31cb384ba1b607086da05701c31122b2, and the test every_compiled_genesis_lies_in_the_past_of_the_clock (known-failed first: a genesis a day ahead of the build's clock). The candidate is 69d1b56e.
| Gate | Candidate | Run (UTC) | Line |
|---|---|---|---|
| Build-1 release build (gate priority) | 69d1b56e | 17:01:30 to 17:03:03 | GREEN: igneumd sha256 0751598f3d653f8d...f6ede2 carries 69d1b56; igneum-miner c29f33bb... unchanged; /srv/artefacts/0322-69d1b56e/node-lane/ |
| kaspa-consensus whole (build-2) | 69d1b56e | 17:02:17 | GREEN 120 + 1 + 1 |
| igneum-exec (build-2) | 69d1b56e | 17:03:09 | GREEN 36 |
| kaspa-consensus-core (build-2) | 69d1b56e | 17:03:40 | GREEN 152 (the genesis clock test among them) |
| kaspa-pow (build-2) | 69d1b56e | 17:04:31 | GREEN 17 |
| Canary set from the artefact (build-1) | 69d1b56e | 17:03:17 to 17:04:35 | GREEN: start lines, digest 83eb50cd with no file, shutdown 677 ms, override file refused (the shared devnet still takes it), two-node handshake with equal digests, network mismatch for a shared-devnet dialler |
| igneum-miner (build-2) | 69d1b56e | 17:05:22 | GREEN 25 of 25 against the sub-version 3 packs |
| A mined block accepted (the fleet, dn3-g1, the node-lane pair igneumd 0751598f + miner c29f33bb, igneum-worker-cuda) | 69d1b56e | 17:04:55 to 17:06:44 | GREEN: digest 83eb50cd, "genesis 4020cb43... executed: chain id 4463", first "PoW accepted c313ddac... by igneum-lottery-v2-bound (daa 0, epoch seed 4020cb43...)" at 17:06:19.920Z, 3 accepted and 0 rejected by 17:06:44Z. Expected before the first block: the executor's "waiting for consensus to sync (the sink is 61,000 s old)", the 10-minute sink-age rule on a 17-hour-old genesis |
Not in 0.3.22: the N15 kept-datadir class (ledger N15, third paragraph), 0.3.23: fork branch n15-kept = dfae08e5 on 21d8f454 (the scan from the tip down, once per state generation, the pin's hash, the set-aside; exec suite 37, kaspad check green).
7. 0.3.17 node items after the 3.0 lane (Horizon queue, 6 October 2026, evening)
Numbering (the shipper, 6 October 2026, 22:4xZ): this tree ships as 0.3.17. 0.3.16 became the corrective cut of 0.3.15's tree the same night (the Mac and PC 1 had taken early 0.3.15 builds), node f1ea7a38 unchanged. The branch keeps its name ca3-v4-0316 and the commit messages their "0.3.16"; every "0.3.16" in the fork's code comments means this tree.
Branch ca3-v4-0316 in the fork (from f8f0f1df, the dependency pass). Every item behind a switch is never on every network until the override file sets it, and every new switch joins the consensus digest only once set (the 0.3.15 rule), so the pinned devnet digest is unchanged. Suites ran on igneum-build-1 through tools/build-remote.sh; nothing here has run on the devnet.
| Item | Fork commit | What changed | Tests |
|---|---|---|---|
| Peer-driven unwrap class | 8e2f5cbe | 28 sites where a peer's request reached an unwrap on a store read: SyncManagerError::StoreRead and BlockBelowRetention, antipast_hashes_between and find_highest_common_chain_block return results, request_headers, negotiate and the IBD flow answer ProtocolError |
a_sync_request_below_retention_is_an_error_not_a_panic, unknown_hashes_in_peer_driven_sync_requests_are_errors_not_panics; the fuzz gate for the night battery is owed (section 7.3) |
| Seven-window class signal | 0760b844 | section 6.5 | the gate's four cases |
| Finality pause cause (Q1, Q97) | b2e21447 | CheckpointsReport and the gRPC model (serializer 3, proto 11 to 13, RpcHeldBy) and igneum_getProvingStatus carry finalityReason in words, finalityProvisional, heldBy {tableIndex, stayersShareBps, expiresDaa}, pausedSinceMs; one log line per pause start and per cause change, finality resumed once |
a_pause_carries_its_cause_and_its_start; finality 12 of 12 |
igneum_getRecentBlocks(seconds <= 600) |
eec34ac3 | the Ember app's live view: chain blocks newest first with their merged blocks after them, timestamp_source header or chain_block |
recent_blocks_span_rows_and_sources; igneum-exec 21 of 21 |
| Difficulty rule v3 | 325f8b98 | section 7.1 | clock_rule_v3_is_in_seconds_at_every_rate, digest edits 17; consensus-core 112, difficulty 15; the harness, section 7.2 |
| The miner's stall guard and the node's idle-peer drop (ledger N4) | e6e1fbe2 (miner), 96619b7d (node) | section 7.7 | stall_guard_fires_on_a_still_tip_only, a_peer_is_dropped_only_when_it_relays_nothing_while_our_sink_stands_still; igneum-miner 19, kaspa-p2p-flows 34, kaspa-p2p-lib 19; the gate, section 7.7 |
| Relay pipelining (ledger N3, 0.3.18 branch ca3-v4-0318) | bb397f99 | section 7.2 (the pipelined rows) | the_relay_pipeline_bound_reads_its_setting; p2p-flows 35, p2p-lib 19; the harness at the document's own case |
| The sync-request fuzz gate (0.3.18) | 6cf33d36 | the night battery's two-daemon test, testing/integration |
sync_request_fuzz_never_crashes_the_node (forty rounds, ten request shapes), sync_request_fuzz_check_fails_on_a_dead_node; the crate's test build made whole again (five faults since the finality fields) |
| The stale-block nuisance guard (ledger N6, 0.3.18) | 319da644 | section 7.8 | kaspad check, p2p-flows 35, the consensus header tests; the harness, section 7.8 |
| C1 in DAA seconds | 8dbb7a23 | section 7.4 | checkpoints_read_daa_seconds_from_the_activation, digest edits 18; finality 13 of 13 |
| Weight-gated deep fork choice | 61b22057 | section 7.5 | without_the_fork_gate_a_minor_keys_deep_fork_wins (the known-failed case), the_fork_gate_refuses_a_deep_fork_under_a_third_and_passes_the_rest; digest edits 20; finality 15 of 15; the harness, section 7.5 |
| Vote-or-burn and the finality signing bonus | 10db4b61, 812c3ac2 (the silence reading as a function of the block's past) | section 7.6 | a_silent_block_pays_the_burn_or_the_bonus_only_when_its_switch_is_on (coinbase, the known-failed case first), vote_or_burn_and_the_signing_bonus_pay_a_silent_key_less_only_when_switched_on (end to end); digest edits 24; the harness, section 7.6 |
7.1 Difficulty rule v3 (Horizon lane 5 proposal 2), behind difficulty_v3_activation_daa
The record (docs/analysis/horizon/network.md 3.1, 5.4): run A at 10 blocks/s fell from 26 to 3.5 blocks/s with blue at 1.4 and 77 percent red. The rule read the wrong quantity twice. A chain step's work was the blue-work increment (the mergeset blues only), so a wide DAG under-read by (k + 1) / m and eased. Its solvetime was capped at 20 T (2 s at 10 bps), so a relay-delayed chain over-read by spacing / cap and hardened onto the delay. controller.py: the whole-DAG estimator holds 10.00 blocks/s in every cell the fork's rule missed.
| Rule v3, for headers with DAA score at or above the switch | Where |
|---|---|
The step of chain block b = sum of calc_work(bits) over b's mergeset, blues and reds, each at its own bits; the mergesets of consecutive chain blocks partition the DAG, so every block is counted once over the walk |
DifficultyManager::mergeset_step_v3 |
The sanitised clock's cap is 20 s and its lag bound 60 s whatever the target time (CAP_SECONDS_V3, CLOCK_LAG_SECONDS_V3); the header processor switches on header.daa_score |
igneum::difficulty::sanitised_clock_v3, header_processor/processor.rs |
| A step that merges m blocks may span m x 20 s before clamping (an empty mergeset reads as one) | clock_step_v3 |
FUTURE_TOLERANCE_MS and BACK_TOLERANCE_MS were already 10 s in milliseconds; unchanged |
|
Rules v1 and v2 byte-identical below the switch; Params, OverrideParams, the digest (once set), the four network consts, DualWindowManager and SampledDifficultyManager::new carry the height |
params.rs, services.rs, window.rs |
7.2 The controller harness (infra/fast-time/difficulty-v3.mjs)
Three nodes in a star; the two spokes reach the hub through a TCP proxy that holds every byte for d / 3 per leg in each direction (a block crosses a link in three legs, inv, request, block, so the document's propagation delay d is spread over them; the first 5 s of a connection pass undelayed because the p2p version exchange times out at 4 s); override-60x.json re-rated to 10 bps from docs/override-params.md's example (k 124, parents 16, mergeset 248, sample rates 10 and 2) with the depths x10 and the PoW epoch at 600 DAA so an epoch stays 60 s; one CPU miner per node. Pass = the hub's DAG rate within 10 percent of the target over the measured window AND the sink's expected hashes per block (2^256 / (target + 1) from its bits) within 10 percent of the three miners' summed hash= x T, both read from 600 s to 900 s. The red share, the chain step spacing and the three block counts are reported, not judged.
Numbers the document leaves unstated, named here rather than inferred: the hash in "hash-implied" is the CPU miners' own status figure summed; the leg count of the proxy delay is Kaspa's three; sim/difficulty/sim.py --dag-delay cannot replay run A under a whole-mergeset rule because sim.py's replay has one selected chain and no DAG, and the lane's controller.py already shows 10.00 in every cell for the whole-DAG estimator, so the real-node harness is the gate here.
| Case | Run (UTC) | SUMMARY | Summary file |
|---|---|---|---|
| 10 bps, d 3 s (the document's first case), rule v3 | 21:26 to 21:38Z | FAIL on the difficulty check, and not the controller's doing: at 1 s per relay leg the hub took one spoke block in three (Accepted 13 blocks, 1 via relay and 12 via submit block), every block a chain block (mergeset 1.00, red 0), each node on its own near-chain; DAG rate at the hub 9.93 blocks/s (0.7 percent under), difficulty / hash-implied 0.339 because two thirds of the network's hash merged nowhere. The relay ceiling: network.md 5.11, ledger N3, the fourth block-rate gate |
difficulty-v3-10bps-d3000ms-v3.json |
| 10 bps, d 600 ms, rule v3 | 21:42 to 21:57Z | PASS: DAG rate 9.930 blocks/s (0.7 percent under target), difficulty / hash-implied 1.045 (4.5 percent over) over 61 samples from 600 to 900 s; one shared DAG (3,006 merged over 1,132 chain blocks, red 0, chain step median 193 ms, p90 643 ms); blocks 9,409 / 9,409 / 9,408 on the three nodes | difficulty-v3-10bps-d600ms-v3.json |
| 10 bps, d 600 ms, the live rule v2 (the comparison) | 21:58 to 22:13Z | The live rule reads 22 percent UNDER: DAG rate 9.710 (2.9 percent under), difficulty / hash-implied 0.778 over 61 samples on the same DAG shape (2,948 merged over 1,143 chain blocks, red 0, chain step median 190 ms, p90 642 ms); blocks 9,030 / 9,025 / 9,028. The harness reports FAIL on the difficulty check as it must: the blue-work step misses the merged blocks' work (model item 4's under-read) | difficulty-v3-10bps-d600ms-v2.json |
| 1 bps, d 30 s (the document's second case), rule v3, the pipelined relay (bb397f99) | 01:47 to 02:27Z | FAIL on the difficulty check, and not a relay-batch question: the hub took 3 blocks via relay and 6 via submit in 40 minutes, every node its own chain (916 merged over 916 chain blocks), difficulty / hash-implied 0.334; the hub's log carries no IBD, no timeout and no disconnect, only its own prewarm lines; the proxies counted 4 connections (each spoke reconnected once). At a 30-s propagation delay and 1 bps the blocks in flight (30) exceed the bound k = 18 was derived for (a 5-s delay), so the orphan and IBD machinery, not the batch, decides what a node takes; the cause of the near-silence is owed its own reading (section 7.3) and the controller verdict stands on the 600-ms and 3-s cases | difficulty-v3-1bps-d30000ms-v3.json |
| 10 bps, d 3 s, rule v3, the 0.3.18 pipelined relay (fork bb397f99, binaries built together, igneumd 32594abe..., igneum-miner ef51249c...) | 00:49 to 01:05Z | PASS, the ceiling gone at the document's own case: DAG rate 10.010 blocks/s (0.1 percent over target), difficulty / hash-implied 1.077 (7.7 percent over) over 61 samples; the hub took 7,746 blocks via relay against 4,015 via submit (1 in 13 on 0.3.17); one shared DAG (3,033 merged over 1,003 chain blocks, red share 5.3 percent, chain step median 207 ms, p90 687 ms); blocks 9,914 / 9,909 / 9,912 | difficulty-v3-10bps-d3000ms-v3.json |
the same with --pipeline 1 (the 0.3.17 one-at-a-time flow, the known-failed shape) |
01:05 to 01:21Z | FAIL as it must: the hub took 445 via relay against 801 via submit, every block its own chain block (3,014 merged over 3,012), difficulty / hash-implied 0.351 (0.339 on the 0.3.17 binary last night); DAG rate 9.877 at the hub alone | difficulty-v3-10bps-d3000ms-v3-pipe1.json |
| 1 bps, d 10 s, rule v3, pipelined, 420 s measured from 300 (hub at debug) | 02:57 to 03:04Z | the DAG is shared: 254 blocks via relay against 81 via submit, red share 35.8 percent of 134 merged over 40 chain blocks (mergeset 3.4), difficulty / hash-implied 0.989; DAG rate 1.100, on the 10 percent line (the harness reads FAIL on the rate check by rounding) | dv3-10000-debug.json (scratch) |
| 1 bps, d 20 s, the same | 03:04 to 03:11Z | degraded: 338 via relay, red share 36.0 percent of 125 merged over 80 chain blocks (mergeset 1.6), DAG rate 0.808 (19 percent under), difficulty / hash-implied 0.599 | dv3-20000-debug.json (scratch) |
| 1 bps, d 30 s, the same, 420 s | 02:49 to 02:56Z | nothing merges: 54 via relay but 123 merged over 123 chain blocks (mergeset 1.0, red 0), difficulty / hash-implied 0.337; the 40-minute run of 01:47Z relayed 3 blocks in all | dv3-30s-debug.json (scratch) |
| 1 bps, d 30 s, 2,400 s measured from 1,500, hub and spoke at debug, logs kept | 03:14 to 03:54Z | FAIL on the difficulty check as before, and now with the mechanism in the log: the hub skipped 3,396 relayed blocks as lower blue work than virtual's merge depth root (a spoke 1,862), rising from 331 in the first 10 minutes to 960 per 10 minutes by the end (the first skip 90 s in, blue work 131,070 against the root's 703,556, a fivefold gap); the hub took 117 via relay and 28 via submit over the measured span, 919 merged over 919 chain blocks (mergeset 1.0, red 0), difficulty / hash-implied 0.333, DAG rate 1.013; blocks 2,428 / 2,327 / 2,393 |
difficulty-v3-1bps-d30000ms-v3-debug-long.json, logs difficulty-v3-1bps-d30000ms-{hub,spoke}-debug.log |
7.3 Owed from this section
| Owed | Why not here |
|---|---|
| The sync-request fuzz gate for the night battery | a two-daemon harness that speaks the p2p protocol does not exist yet; the manager-level randomised test (sync_requests_with_random_inputs_never_panic) covers the store-read class, not the wire |
| The stale-block nuisance-peer harness case | the shipper's case; needs the two-daemon harness above |
| A DAG replay of run A under rule v3 | needs a DAG simulator the document does not specify |
| The 30-s regime's decay over 40 minutes (3 blocks via relay in the long run against 54 in 7 minutes) | answered 03:54Z (the 7.2 row of 03:14Z): the spokes' chains fall below the hub's merge depth root and the hub skips their relayed blocks before processing (3,396 skips, climbing through the run); nothing decays in the relay, the DAGs split and stay split |
The 30-s reading (7 October 2026, 02:49 to 03:11Z): a protocol regime, not a harness artefact. The same harness at 10, 20 and 30 s of propagation gives a staircase: at 10 s the three nodes share one DAG (mergeset 3.4, red 36 percent, the controller within 1.1 percent); at 20 s the DAG under-merges (mergeset 1.6, the rate 19 percent under, the difficulty 40 percent under); at 30 s nothing merges (mergeset 1.0, every node its own chain). At 1 bps with k = 18 the blocks in flight (2 d λ) pass k between 10 and 20 s, which is k's own derivation (docs/analysis/horizon/network.md model item 1: k from P(Poisson(2 D λ) > k) < 0.01 at D = 5 s). A peer whose delay exceeds that bound cannot follow the chain as a merging peer by design, and the document's second controller case (30 s at 1 bps) cannot be judged by any controller because no block of the far peers enters the hub's chain. Fix shape for 0.3.19, named and not built: (a) an IBD trigger on a stale sink, so a node whose relayed blocks stop merging for a span (its sink's blue work falling behind the heaviest peer's by more than the merge depth) syncs the heavier chain instead of mining alone (the 30-s hub's log shows no IBD line at all); (b) k and the merge depth sized to the measured inter-region delay bound at the block rate (the live devnet's hop is under 0.2 s p50, 0.67 s p99, far inside the bound; a 10 bps step tightens the bound to under 1 s). Per tier today: nothing, no live peer is near 10 s; a home miner on a satellite or a congested link above about 10 s would mine alone and earn nothing, which the app's "synced" (the tip age) does not show, so the stall guard should also read the peers' sink against its own (owed to the app lane as a design note). The 40-minute debug run (03:14 to 03:54Z) names the mechanism from the hub's own log: a relayed block whose blue work sits below the hub's merge depth root is skipped before it is processed (Relay block ... has lower blue work than virtual's merge depth root ..., hence we are skipping it), 3,396 times on the hub, 331 in the first 10 minutes and 960 per 10 minutes by the end; the first skip came 90 s in with the spoke's blue work at 131,070 against the root's 703,556. So the 30-s split is not a slow decay but a fork that opens in the first two minutes and widens: each node's own chain outruns what the 30-s link can deliver, the spokes' blocks arrive under the root and are refused, and their work (two-thirds of the hash) never reaches the hub's difficulty window, which is the 0.333. Fix shapes for 0.3.19 unchanged: a stale-sink IBD trigger does nothing here (every node's sink moves on its own chain); the merge depth root is the bound that matters, and sizing it (with k) to the delay bound is the design question, not the relay.
7.4 C1 in DAA seconds (Horizon lane 5 proposal 3), behind finality_daa_rule_activation_daa
The anchor B_A is the lowest selected-chain block with DAA score at or above the activation; I0 = blue_score(selected parent of B_A) / interval + 1 is the first index whose blue-rule checkpoint is B_A or above. From I0, checkpoint i is the lowest selected-chain block with DAA score >= activation + (i - I0) x interval x bps, determined once the sink's DAA score is depth x bps past that target. Indices below I0 keep the blue-score rule, so the index sequence stays contiguous and the targets monotone (the blue rule's block for I0 - 1 is at or below B_A's selected parent). The anchor is a node-local memo re-derived under a reorg below B_A; is_checkpoint_block_of reads the rule from the chain of the block it is asked about, so an off-chain certificate is judged on its own chain. The presence window, the off-chain retry interval and the fold clock read the cadence and depth in DAA at the point they apply (interval x bps and depth x bps under the rule). The numbers: interval 30 and depth 60 (devnet 20) are read as seconds, which at 1 bps is the same count of DAA as before; at 10 bps a checkpoint comes every 30 s of chain time and the votes per day stay what they are at 1 bps (network.md 5.6, 5.7). Unstated in the document and named here: d's value under the rule (I kept the existing depth figure read in seconds), and the "one carriage per vote" half of proposal 3, which is not in this change.
7.5 Weight-gated deep fork choice (51-percent.md rank 2), behind fork_gate_activation_daa
The record (51-percent.md section 1): a 51 percent withholder's deep private chain wins the ordering race up to the lock, and during a pause or in the first month there is no lock, so a 12-hour double spend costs twelve hours of rent. Rented hash has no weight for ten days.
| The rule, for a sink at or above the switch | Where |
|---|---|
A candidate whose fork from the sink's selected chain lies more than fork_gate_window_daa x bps DAA (600 s) behind the sink is a sink candidate only if the vote keys of every block in its chain since the fork (chain blocks and their mergesets, blues and reds) hold at least one third of voters_at(fork) |
FinalityManager::deep_fork_refusal, asked by sink_search_algorithm for every popped candidate |
| Lets through: the rule off or not yet active at the sink, a candidate on the sink's chain, a fork inside the window, a table of total 0 (the first window), builders at or above a third | |
| A refused tip's parents still enter the heap (the honest tip may be among them); the search's invariant holds | processor.rs |
| Node-local memo: refused blocks with their fork and share (a refused block's decision is history: the table at the fork and its own builders cannot change), the newest refused tip with its builders so a private chain growing block by block costs one mergeset per block | ForkGateMemo |
| Fork choice only, never block validity: a node that never saw the honest chain follows what it sees; rank 8's client-shipped certified checkpoint covers the cold start |
Harness infra/fast-time/fork-gate.mjs: node A (three CPU threads) and node B (one) mine together for 240 s (A about 75 percent of the blue blocks), the link (a pass-through proxy B dials) is cut, B's miner restarts with four threads and mines a private chain for 180 s (the window is 60 DAA), the link is restored and A's sink is watched for 150 s. --gate on --expect hold: A's sink stays A-built and A's log carries Fork choice: block ... not a sink candidate; --gate off --expect reorg: the known-failed case, A's sink becomes B-built; --gate off --expect hold must FAIL.
| Case | Run (UTC) | SUMMARY | Summary file |
|---|---|---|---|
First run, --gate on --expect hold, A mining through the split (binaries at 10db4b61) |
22:03 to 22:13Z | FAIL on the refusal-line check, and the harness itself was at fault: B ended one block heavier (449 against 448), A's sink search never popped B's tip, the gate was never asked; A held by GHOSTDAG alone | fork-gate-gate-on-expect-hold.json (overwritten by the runs below) |
Second run, --gate on --expect hold, A idle through the split (binaries at 10db4b61) |
22:16 to 22:26Z | FAIL, and this one found the rule's gap: B 2,333 bps of the blue blocks at the fork, B heavier by 168 blocks (451 against 283), A's sink became B-built 30 s after the heal with 0 refusal lines. The fork point was A's own tip (A idle), so the depth measured from the sink read 0 and the gate let the chain through. Fixed on the fork, 3301cf32: the depth is the larger of sink minus fork and candidate minus fork, "forked more than D back" read along the tip's own chain | (same file, overwritten) |
--gate off --expect reorg (the rule's known-failed case, binaries at 10db4b61) |
22:26 to 22:35Z | PASS: B held 3,250 bps of the blue blocks at the fork, B heavier by 171 blocks (459 against 288), A's sink became B-built 25 s after the heal with 0 refusal lines: a minority key's deep fork wins under the rule as written (51-percent.md section 1) | fork-gate-gate-off-expect-reorg.json |
--gate off --expect hold (the harness's own failed shape, first attempt) |
22:35 to 22:45Z | FAIL, but not the shape the case is for: both nodes mined 0 blocks for the whole run (B null bps, blue 0 against 0), a harness fault in that run (the port range was shared with a second chain that started at 22:33Z and died on AddrInUse); read against the re-run below |
(overwritten below) |
Third run, --gate on --expect hold, A idle, the once-logging build (9a44fcb8) |
23:27 to 23:37Z | FAIL, and the second gap: A's sink moved onto B's 158-block chain at 457 s with 0 refusal lines. The rule let a candidate on the sink's chain through as "an extension", and with A idle the fork point is the sink itself, so the whole private chain read as an extension; the 23:17Z pass (below the line, 38,367 refusal lines, A had mined a few blocks first) was a race. Fixed on the fork, aa0182aa: no shortcut, the depth along the candidate's chain decides (an honest extension is one block deep) | (overwritten) |
--gate on --expect hold, binaries at aa0182aa (igneumd 679de9a9..., igneum-miner ef51249c...) |
23:45 to 23:55Z | PASS: B held 2,750 bps of the blue blocks at the fork; fork depth 183 DAA against the 60-DAA window; B heavier by 183 blocks (457 against 274) at the heal; A learned 177 of B's blocks and its sink stayed A-built for the 150 s; 268 refusal lines on A, one per refused block | fork-gate-gate-on-expect-hold.json |
--gate off --expect reorg (the rule's known-failed case), aa0182aa |
23:55 to 00:05Z | PASS: B 2,083 bps, 162 DAA deep, heavier by 162; A's sink became B-built 25 s after the heal, 0 refusal lines: a minority key's deep fork wins under the rule as written | fork-gate-gate-off-expect-reorg.json |
--gate off --expect hold (the harness's own failed shape), aa0182aa |
00:05 to 00:14Z | FAIL as it must: B 2,645 bps, 193 DAA deep, A's sink became B-built at 477 s, FAILED CHECK a_held_its_chain, refusal_line_on_a |
fork-gate-gate-off-expect-hold.json |
Three harness faults on the way, each fixed in the harness: a port range shared with another chain (AddrInUse), an RPC call one tick before the socket opened ("rpc not connected", nodes left holding the ports), and a reorg test by the sink's key that read a block B had mined into the shared chain before the cut as a reorg (now: the sink left the pre-cut chain for B's). One build fault, ledger N5: a node built with cargo build -p kaspad alone validated with the stub engine and refused every block for two chains.
7.6 Vote-or-burn and the finality signing bonus (51-percent.md rank 3 and its sibling), each behind its own switch
the project lead, 6 October 2026: "weigh it up, implement only if 95%+ of the mining community would respect it". Both are built behind their own switches, never until set, so the weigh-up can pick one and the project lead decides activation. The silence test is the same for both: the block's vote key is a voter of the weight table at the latest checkpoint in the block's past (weight at or above dust) and signed no vote in the presence window of the block's own past (participation_count 0 over the carried votes; a key younger than the window counts as present, so a new key is never touched). The test reads the carried votes of the block's past, the records the weight rule already reads.
| Rule | Switch and share | What a silent blue block's coinbase pays | Where |
|---|---|---|---|
| Vote-or-burn | vote_or_burn_activation_daa, vote_burn_bps 2,000 |
its producer share of the SUBSIDY less 20 percent, the difference never minted; fees untouched; the pool's fifth untouched; a red block's reward (paid to its merger) untouched | CoinbaseManager::producer_and_pool |
| Signing bonus | signing_bonus_activation_daa, signing_bonus_bps 1,000 |
its producer share less 10 percent, that 10 percent paid to the proving pool instead: nothing destroyed, framed as a reward a signing key earns in full | the same |
| Both | compound: 30 percent off, 10 of it to the pool |
The model figures (51-percent.md section 5, "silence then costs 7,757 IGN an hour at 34 percent"): a full block pays 3,168,808,781 sompi, the producer 2,535,047,025; the burn is 507,009,405 per block, so at 34 percent of an hour's 3,600 blocks (1,224) silence costs 620,579,511,720 sompi = 6,205.8 IGN an hour. The document's 7,757 is 20 percent of the WHOLE subsidy (633,761,756 x 1,224); the rule as asked ("20 percent of its producer share") gives 6,206. Named, not resolved here: which base the weigh-up wants. Also named: the silence reading depends on the carried-vote records a node holds; a node validating a block more than two weight windows after the block's time (an archival sync from genesis) reads them after their pruning horizon, so either switch stays never until a replay test (sync a fresh node across a switched-on span and compare its coinbase verdicts) exists.
The replay gate: FAILED on both switches at 22:13 to 22:31Z, then PASSED on both at 00:16 to 00:30Z once the silence reading became a function of the block's own past (fork 812c3ac2). The first reading took "the latest checkpoint this node has determined in the block's past" from the node's finality state; a node syncing in IBD determines on another schedule than one that saw the chain live, so the two expected different coinbases. Now the index is the one the block's own blue score (or DAA score) determines, the checkpoint block is walked on the block's own chain, the table is weights_at on it, and participation counts the carried votes whose carriers lie in the block's past. The one node-local input left is the vote records' carrier lists; the gate below says they agree over a 300-s span synced from genesis. The long-span case (a 660-s span past two 120-DAA weight windows, the records behind the pruning horizon) passed at 00:42Z on the fast-time profile; on the devnet's 7,200-DAA window the same case is a four-hour run and is owed before a switch is set there. The original failure, kept for the record: A third node C started from nothing after a 300-s span with the burn on and synced from genesis: it held 81 blocks against A's 340 after 240 s and its log read Consensus detected UTXO-invalid blocks which are disqualified from the virtual selected chain (possibly due to inheritance): 240 disqualified vs. 81 valid chain blocks. A node that was not present reads silent_builder differently from one that was (its finality state at validation time is not the same: which checkpoint it has determined for a block's past, which vote records it holds, which carriers), so it expects a different coinbase and marks the chain UTXO-invalid from the first disagreement. That is the hazard section 7.6 named ("the silence reading depends on the carried-vote records a node holds") made concrete, and it is a consensus split, not a payout error. Both switches stay never. The fix class before either is ever set: a silence reading that is a pure function of the block's past as every node sees it (the carried votes in the blocks below the block, counted against a checkpoint definition that needs no node-local determination timing), with this replay gate passing on burn and bonus. The bonus case's replay is expected to fail the same way (same reading); its line follows below.
Harness infra/fast-time/vote-or-burn.mjs: node A's miner votes, node B's runs --no-vote, one thread each, 300 s; the last 120 chain blocks of A whose mergeset is their selected parent alone give (producer, pool) pairs sorted by the parent's key; the medians' ratio B / A is the producer reading, B's pool over A's producer the pool reading. --rule none: 1.00 and 0.25; --rule burn: 0.80 and 0.25; --rule bonus: 0.90 and 0.35; --rule none --expect burn must FAIL.
| Case | Run (UTC) | SUMMARY | Summary file |
|---|---|---|---|
--rule none --expect none |
22:02 to 22:07Z | PASS: 64 pairs paying A, 56 paying B over DAA 202 to 321; producer medians A 253,735,321, B 253,732,680, ratio 1.0000; B's pool 0.2500 of A's producer; A's pool 63,433,830 | vote-or-burn-rule-none-expect-none.json |
--rule none --expect burn (the known-failed case) |
22:07 to 22:12Z | FAIL as it must: ratio 1.0000 against the wanted 0.8 (producer_ratio_matches) |
vote-or-burn-rule-none-expect-burn.json |
--rule burn --expect burn --replay |
22:13 to 22:22Z | the payout PASSES (ratio 0.8000, B's pool 0.2500 of A's producer, 56 and 64 pairs) and the REPLAY FAILS: the late node C held 81 blocks against A's 340 after 240 s, 240 disqualified vs. 81 valid chain blocks (UTXO-invalid from the first silent coinbase it read differently) |
vote-or-burn-rule-burn-expect-burn-replay.json |
--rule bonus --expect bonus --replay |
22:22 to 22:31Z | the payout PASSES (ratio 0.9000, B's pool 0.3500 of A's producer, 54 and 66 pairs; A's pool 0.25) and the REPLAY FAILS the same way: C at 81 blocks against A's 355, 3 rule error lines | vote-or-burn-rule-bonus-expect-bonus-replay.json |
--rule burn --replay --replay-bps 2500 (the replay gate's own failed shape: the late node on another digest) |
22:31 to 22:40Z | FAIL as it must: C held 0 blocks (its handshake refused, 6 digest refusal lines, 0 rule errors); the payout side unchanged (ratio 0.8000) | vote-or-burn-rule-burn-expect-burn-replay-bps2500.json |
--rule burn --expect burn --replay, binaries at 812c3ac2 (igneumd 19670ddd..., igneum-miner ef51249c...) |
00:16 to 00:24Z | PASS: the late node C synced every block in 5 s (313 against 313, one sink fb054883), 0 rule error lines; the payout unchanged (ratio 0.8000, B's pool 0.2500 of A's producer, 65 and 54 pairs) | vote-or-burn-rule-burn-expect-burn-replay.json |
--rule bonus --expect bonus --replay, 812c3ac2 |
00:24 to 00:30Z | PASS: C synced in 5 s (331 against 331, one sink 20e976eb), 0 rule error lines; the payout unchanged (ratio 0.9000, B's pool 0.3500 of A's producer, 48 and 71 pairs) | vote-or-burn-rule-bonus-expect-bonus-replay.json |
--rule bonus --expect bonus --replay --secs 660 (a span past two 120-DAA weight windows), 812c3ac2 |
00:31 to 00:42Z | PASS: C synced the 697-block span from genesis in 5 s (697 against 697, one sink fc4d942d), 0 rule error lines; the payout unchanged (ratio 0.9000, the pool 0.3500, 58 and 61 pairs over DAA 566 to 686) | vote-or-burn-rule-bonus-expect-bonus-replay-long.json |
--rule bonus --expect bonus --replay --secs 14700 --finality devnet (the devnet's own finality object: weight window 7,200 DAA, presence 20, interval 30, depth 20; a span past two devnet windows), 812c3ac2 binaries |
01:00 to 05:19Z | the rule checks PASS: 65 pairs paying A and 55 paying B over DAA 14,419 to 14,538, producer ratio 0.9000 (want 0.9), B's pool 0.3500 of A's producer (want 0.35), 0 rule error lines on C; the harness's own sync check read FAIL and was wrong: A pruned at the moment C joined (the chain had passed the 13,838 pruning depth; "pruning points in history: 13" at 04:49:28Z), so A's count at C's start (the whole chain) is not reachable by a C that prunes too; by the logs C took A's 13,843 blocks and A's sink in 20 s (IBD done 04:49:25Z, C started 04:49:05Z). Clause corrected in the harness (C against A's current count) | vote-or-burn-rule-bonus-expect-bonus-replay-devnet-window.json (the json carries the harness's FAIL; the reading above is the gate's) |
Closed by the project lead's word (7 October 2026, morning): the signing bonus at the testnet genesis (signing_bonus_activation_daa 0, signing_bonus_bps 1,000, the unsigned tenth to the pool) and NO burn. Vote-or-burn is removed from the 0.3.19 line in 420f9305 on ca3-v4-0318 (the two Params fields, their digest arm, the four network constants, the coinbase manager's burn arm, both tests; with_signing_bonus replaces with_silence_rules; the shared silence reading silent_builder is unchanged; the override's two keys are accepted and ignored so a 0.3.16 to 0.3.18 file parses; no network's digest moves). The harness refuses its burn rule from the same day (92c6f0f5); the rows above stand as the record of the weigh-up and the replay gate. Ledger N7 (the testnet lane, 7 October 2026, 09:0x UK): the execution layer never read a block's silence, so with the bonus on the EVM ledger credited a silent block 80/20 against the coinbase's 72/28; fixed in c7ea1e21 (igneum::silent_split, the one arithmetic both ledgers apply; SegmentBlock::silent from consensus's silent_builder; the bonus bps on ChainBlockEnv), no guest change; the harness's bonus case checks the bridge identity per pair row (e726a0fe), the bridge rows:
| Run (UK) | Binary | Case | bridge line | SUMMARY |
|---|---|---|---|---|
| 09:10 to 09:16 | 27d1b520 (before c7ea1e21) | --rule bonus --expect bonus, the first pairing by address |
0 of 63 pair rows agree exactly; the first disagreement an A row 880 sompi apart (one second of launch ramp: the chain block's own entry, the N8 reading) | FAIL on the exact identity, as it must |
| 09:18 to 09:23 | 27d1b520 | the same, the EVM bonus ratio as the failing check | EVM medians null (the mergeset-position pairing found no rows) | FAIL as it must (evm_bonus_ratio_matches) |
| 09:31 to 09:37 | e5e6c2bf (d840537b: N7 in, N8's switch at never) | --rule bonus --expect bonus |
EVM credit medians A 253,736,201 B 228,375,257 sompi, ratio 0.9000 (want 0.9): a silent key's credit is the bonus share on the EVM side too; exact identity 0 of 66 rows (N8's slope, the switch off) | PASS (N7 closed) |
| 09:37 to 09:42 | e5e6c2bf | --subsidy-per-block (N8's switch at 0) |
ratio 0.9000; exact identity 0 of 59, the entry compared was the chain block's own (the pairing took the last entry for the address; the segment lists the parent first) | FAIL on bridge_identity_exact, the harness's fault; re-run with the pairing corrected recorded below |
| 09:46 to 09:52 | e5e6c2bf | the same, the smaller of the two entries | still 880 sompi apart: the live read of a chain block's record (09:5x UK) shows the segment holds the chain block itself, not its parent; the execution layer credits a chain block in its own record and the UTXO coinbase pays it in the child's, both at its own DAA, so the harness paired a parent's payment with the child's credit. N8 is real for non-chain blues only (wide mergesets, the 10-bps profile, owed); the pairing corrected to the parent's own record and the exact identity a failing check in both rule states, the two runs recorded below | FAIL on bridge_identity_exact, the harness's fault |
| 09:58 to 10:04 | e5e6c2bf | --rule bonus --expect bonus, the pairing by the parent's own record |
EVM credit medians A 253,744,124 B 228,361,789 sompi, ratio 0.9000 (want 0.9); exact identity 120 of 120 rows | PASS: N7 closed on the harness (the silent key's EVM credit is the bonus share; the bridge identity exact on the selected chain) |
| 10:04 to 10:09 | e5e6c2bf | the same with --subsidy-per-block (N8's switch at 0) |
ratio 0.9000; exact identity 118 of 118 rows (one wide mergeset skipped) | PASS: the selected chain agrees in both rule states, as the live read said; N8's own case (a side blue priced at the chain block's DAA without the switch) needs wide mergesets, owed on the 10-bps profile |
The testnet lane compiles the switches into TESTNET_PARAMS on testnet-genesis-2-node (the testnet refuses an override file): signing bonus 0 and 1,000; finality_leave_activation_daa 0 with finality.leave_delay 3,600; finality_v3_activation_daa 0; finality_daa_rule_activation_daa 0; difficulty_v3_activation_daa 0; latency_ladder_activation_daa 0 with latency_ladder_window_daa 86,400 and the six rungs (27, 35, 53 admissible; 88, 173 and 267 not); proving_consensus_verify_daa 0 with the manifest's program ids. Rung 3 re-measured on the project lead's word (08:46 UK, igneum-build-1 under the measure hold, both build slots held, load 6.1, core 40 at 3.66 GHz, the ladder worktree's igneum-pow 59ae70cf): reps 88 cold alone 9.04 ms, cold with the SMT sibling loaded 10.85 ms (averages of 50: 5.95 and 10.11), over the 10 ms gate by 0.85, so it stays inadmissible; the control rung 2 (reps 53) read 9.25 cold loaded, admissible as before. Raw lines in counter-asic-3-gate/ladder-bench/20261007T074547Z. |
7.7 The miner's stall guard and the node's idle-peer drop (ledger N4)
The record is ledger N4: a desktop node fell off the network twice (a digest-mismatch refusal, then a same-digest peer that relayed nothing), its tip froze for 53 minutes, and its miner hashed and voted on the frozen template with synced=true in every status line while the node accepted 1,750 blocks into a dead branch.
| Half | Commit | What it does |
|---|---|---|
| Miner | e6e1fbe2 | StallGuard: the age of the template TIP (a stalled node answers every fetch with the same tip); past --stall-secs (default 300, 0 never) the miner prints STALLED '<label>': no new template for <N> s (tip <8 hex>, daa <D>); the node has fallen off the network or stopped moving; hashing and voting paused, exiting with code 45 so the launcher restarts (and after a second stall, restarts the node) on both streams and exits 45. tip_age_s= ends both STATUS lines (seconds since the last new tip) and the watch line (now minus the sink header's timestamp, -1 when unreadable). The app lane (miner-ui-4) reads exit 45 or the STALLED ' tail, restarts the miner on the first and the node on the second, and gates "synced" on tip_age_s at or under 120 s and peers at or above 1. |
| Node | 96619b7d | the relay flow stamps every block a peer delivers (Router::note_block_delivered); the ping loop drops a peer that delivered nothing for PEER_STALL (600 s) over a span in which our own sink did not move (SendPingsFlow::stall_verdict, pure on its inputs), so the connection manager dials another; a peer idle while our sink moves is healthy. |
Gate infra/fast-time/miner-stall.mjs (6 October 2026, 22:23 to 22:29Z, binaries from the fork at e6e1fbe2):
| Case | SUMMARY | Summary file |
|---|---|---|
--case stall (a lone node at a GPU-only genesis difficulty, 2^40 hashes per block against one CPU thread, so the tip never moves; --stall-secs 30) |
PASS: STALLED at N = 30 s, exit 45 at 31.5 s; tip_age_s rose to 29 in the STATUS lines before it; node blocks 0 |
miner-stall-stall-expect-stall.json |
--case moving (two nodes mining each other's blocks on the CPU profile, --stall-secs 30, 90 s) |
PASS: no STALLED line, the miner still running at 90 s, tip_age_s never above 1; 118 blocks | miner-stall-moving-expect-quiet.json |
--case off --expect stall (today's miner: --stall-secs 0 on the frozen tip) |
FAIL as it must: no STALLED line, the miner ran the whole 120 s, tip_age_s reached 109 | miner-stall-off-expect-stall.json |
The node half's gate, infra/fast-time/peer-drop.mjs (7 October 2026, 01:03 to 01:47Z, binaries from the 0.3.18 branch at 319da644 built together): node A with one --addpeer peer B that mines nothing and relays nothing.
| Case | SUMMARY | Summary file |
|---|---|---|
--case still (A without a miner: its sink stands at genesis) |
PASS: the drop line at 720 s (P2P, peer 127.0.0.1:30401: no block from this peer for 720 s while our sink stood at 234e082d... for 600 s (ledger N4); dropping it to dial another peer), A's peer count 1 to 0, the peer re-dialled at 750 s (2 outgoing connections over the run); one sink throughout |
peer-drop-still-expect-drop.json |
--case moving (A with a CPU miner: its sink moves) |
PASS: 0 drop lines, the peer kept for the 780 s, 155 distinct sinks | peer-drop-moving-expect-quiet.json |
--case moving --expect drop (the harness's own failed shape) |
FAIL as it must | peer-drop-moving-expect-drop.json |
The 720 s is the rule's span (600 s) plus one ping interval (120 s): the verdict is read after each pong.
An idle network (the devnet-window replay, 7 October 2026, 05:12 to 05:14Z): once the harness miner stopped, A and C dropped each other at 720 s as idle peers and reconnected at once, and will every 12 minutes on a network where nothing mines at all. Harmless on the live devnet (the tip moves) and noisy on an idle testnet before go. A 0.3.19 refinement: hold the drop while our own sink is older than the span, since a peer cannot show a block that nobody mined.
The 0.3.17 canary (7 October 2026, 04:0x to 04:53 UK) found the node half's blind spot: a fresh node syncing 155,700 headers over a headers-proof IBD got no BLOCK for 12 minutes (the headers stage delivers headers, a proof and trusted data, none of which the relay flow stamps), so the drop closed every peer at 12 minutes and the 17-minute join died; and the class-signal "window below epoch ... is incomplete" line printed about 1,900 times a minute through the same IBD (the floor decision is not memoised, so every header asked again). Fixed on both trees:
| Commit (release branch ca3-v4-0317-fix, on 12153428) | Twin on ca3-v4-0318 | What it does |
|---|---|---|
| 4f4bb9c9 | a3fe9f67 | Router::route_to_flow stamps every sync payload as a delivery (KaspadMessagePayloadType::is_sync_delivery: blocks, bodies, headers, pruning points, the proof and its chunks, trusted data, the UTXO-set and SMT chunks, the execution snapshot, locators; invs, requests, pings, transactions and gossip are not); stall_verdict takes a syncing flag under which it never fires and restarts its clock; both class-signal missing-history lines once per epoch per process at warn, debug after (class_signal::first_time) |
| ddda7d52 | b21659e4 | the flag is is_ibd_running() alone: the first cut also exempted a sink at genesis, which would have blinded peer-drop.mjs --case still (a node at genesis with no IBD must still drop a peer that shows it nothing) |
Known-failed first on each: the canary's inputs without the flag fire, with it never; a full span counts after the sync; the delivery list on both sides; the memo. Suites on igneum-build-1: kaspa-p2p-lib 20, kaspa-p2p-flows 35 (release) and 37 (0318), kaspa-consensus class_signal 1, all green on both trees. The decisive read is the fleet's headers-proof fresh join on the 0.3.18 rebuild (the shipper's canary); infra/fast-time/headers-proof-join.mjs (7.9) is the Mac's version of that case on the 0318 binaries.
7.7.1 The fresh-join cost (the 0.3.17 canary's second finding, 7 October 2026)
The hotfix canary's fresh join fell to 3.7 blocks a second on one core after the block batch while the node replayed the chain's finality (5,005 checkpoints, 2,529 locks), so a fresh join of the devnet takes hours and grows with the chain until the pruning depth bounds it. The finality manager now accounts its time per path (69647bd6: bodies, virtual changes, weight walks with blocks walked, BLS checks, persists; spent_report, one log line at the IBD end) and a join bench in the consensus suite replays a chain of N checkpoints at the devnet's finality numbers into a fresh node (30 signing keys, every block carrying the builder's section; cargo test --config 'env.IGNEUM_JOIN_BENCH="500"' -p kaspa-consensus --lib join_bench).
| Bench (igneum-build-1, warm stores) | Joiner rate | Finality share | Signatures | Virtual path | Weight walk | Persists |
|---|---|---|---|---|---|---|
| 20 checkpoints, 7,821 blocks (04:50Z) | 90.6 blocks/s | 57.4 s of 86.3 s (67 percent) | 34.9 s over 15,559 checks (2.2 ms each) | 22.4 s over 7,821 changes | 0.9 s over 355 tables (1.4 M blocks walked) | 1.0 s over 260 |
| 500 checkpoints, 22,221 blocks (04:56 to 05:05Z) | 101.9 blocks/s | 130.0 s of 218.0 s (60 percent) | 97.3 s over 44,839 checks (2.2 ms each, 90 per checkpoint) | 31.3 s over 22,221 changes (1.4 ms each) | 4.6 s over 835 tables (6.5 M blocks walked) | 4.3 s over 740 |
| the same chain on 8220c944 (the votes checked across cores before the lock; 05:09 to 05:16Z) | 213.9 blocks/s | 30.3 s of 103.9 s (29 percent) | 8.7 s over 44,839 checks (the same checks, across the box's cores) | 20.2 s over 22,221 changes | 4.4 s over 835 tables | 4.0 s over 740 |
The reading: flat per checkpoint (260 ms of finality at 500 as at 20), three quarters of it BLS checks (a vote signature and a sortition proof per voter per checkpoint, the aggregate per certificate, each 2.2 ms on one core), the rest the per-virtual-change re-evaluation of the open window. The canary's 8 s per checkpoint is 30 times this, so the real node carries something the warm bench does not (cold header reads under the weight walk, or the unlocked-checkpoint scans of a chain where half the indices never locked); the real split comes from the IBD-end line on a real join (the shipper's next 0318 canary, or a box joiner). First fix, 8220c944: the BLS checks of a block's votes run across cores before the state lock (exact: the signature result is a pure function of the vote and the chain id; the sortition result is used only when it was read against the checkpoint the node holds at ingestion; the two-path test holds a forged signature and a swapped sortition proof refused on both paths and ten honest votes landing with the same records). The 500-checkpoint join went from 218 s to 104 s, the finality share from 60 to 29 percent, the signatures from 97 s to 9 s; the virtual path (20 s, 0.9 ms per change) is now two thirds of what is left. The next step waits on the real IBD-end line (aa49613f) from the shipper's 0318 canary: a cold weight walk or the unlocked-index scans would show there, not here.
7.7.1a PC 1's relaunch: the template under the finality catch-up, and synced (7 October 2026, 4f7c56a0)
PC 1 relaunched 0.3.17 on a kept datadir: the node read synced=true from its first second, the miners subscribed, and every getBlockTemplate timed out at 5 s for 90 s while the finality catch-up ran (checkpoints 8053 to 8055 locking), so the app's watchdog faulted every card. The reading: the finality hook runs after the virtual commit, so the template was blocked at the finality state mutex itself (the template's section is read under it) by one catch-up call that holds it longer than the client's timeout.
| Rule | What it does |
|---|---|
| The template answers | template_section waits at most 250 ms for the state and otherwise serves the section from a snapshot of the candidates (the newest 8 certificates, 32 evidence records, the open window's votes, the newest 32 leaves, each with its carriers) refreshed at the end of every hook: at most one block stale, and a stale item is in the past or a duplicate, both harmless |
| Synced reads behind | FinalityManager::behind(sink): the catch-up holds the state, or an index the sink determines is not determined yet; ConsensusApi::finality_behind, and getBlockTemplate's and getInfo's isSynced ANDed with its negation |
Known failed first: the test holds the state as the catch-up does, asserts the template answers inside 2 s with the section the free state gave, and that behind reads true while held and false after. The catch-up's own length (why one call holds the state for tens of seconds on the real chain) is 7.7.1's item.
7.7.2 The 0.3.18 node tree (7 October 2026, 06:30 to 07:09 UK)
Renumbered 09:3x UK: the feature tree is 0.3.19 (mirror branch release-0.3.19-node, e69e8a39 at the rename, then dc141409), 0.3.18 is an app-only cut on the 0.3.17 tree, decimals 0.3.20. The shipper's feature-tree node is ca3-v4-0318 merged into release-0.3.18-node, final at e69e8a39 (07:53 UK: the third merge, carrying c6a62e00 and 500ddd66, igneum-exec 26, the consensus crate 111 plus 1 plus 1, p2p-flows 37, the three checks clean); before it ae17ad00 (07:14 UK: 1cf43254 plus the second merge carrying 4f7c56a0 and the branch's copy of the test move; cargo test -p kaspa-consensus 111 plus 1 plus 1, p2p-flows 37, the kaspad, rpc-service and testing-integration checks clean). The first merge: ca3-v4-0318 (8220c944) into release-0.3.18-node (12153428: the ladder 1591ee1d, exec-sync 40fd8d8c, finality d8d1de1a, tail-emission 996b592f, kaspad's igneum-pow default, cc78a21f, the canary fix 90aaf38e), pushed as release-0.3.18-node = 1cf43254. Six conflicts: five taken as the release side (the header-version reading with the ladder's header_signals_active gate in igneum.rs, pre_ghostdag_validation.rs and ibd/flow.rs, where 0318 carried the same canary fix without the ladder; the shipper's two test fixes), the sixth the relay flow, where the pipelined handle_block now carries the release's 0.3.16 IgneumProofMissing retry arm ahead of the N6 arm. On igneum-build-1 at 1cf43254: cargo check -p kaspad and -p kaspa-testing-integration --tests clean, kaspa-consensus lib 110 twice, kaspa-consensus-core 123, kaspa-p2p-flows 37, kaspa-p2p-lib 20.
The merge surfaced a test-order race both parents carry: the class-signal and ladder tables are process-wide statics, two tests install and restore them (block_template_uses_current_block_version, cheap_checks_run_before_the_pow_engine), and any block-building test of the same binary inside that window reads WrongBlockVersion(1026 or 32770, 2); 1 of 112 then 4 of 112 under a box load of 178, 2 of 112 on the quiet box, and the 0.3.17 suite's "two load flakes that pass alone" were the same reading. Fix in 1cf43254 (and on ca3-v4-0318): the two installing tests are their own test targets (consensus/tests/igneum_installed_signals.rs, consensus/tests/igneum_order_tests.rs), one process each, and INSTALL_TEST_LOCK orders installers that share a binary; cargo test -p kaspa-consensus --lib no longer covers them, the crate without --lib does.
A red nobody's suite line compiles: processes::pruning_proof::igneum_m20_tests::witnesses_are_checked_in_epoch_order_under_their_own_seeds sits behind #[cfg(feature = "igneum-pow")] and fails deterministically (MissingEpochSeed(1, _, 109) not matched) alone on an untouched e69e8a39 with the feature on (the testnet lane, 08:52 UK, box load under 10; the decimals lane saw it on eec34ac3), so every "111 passed" above excludes it by construction; the M20 pruning-proof witness work owns it. Two environment facts for anyone building the release tree under this worktree: kaspa-pow links only against the igneum-pow at the ladder commit (0a1faa1c, branches ladder and release-0.3.18 of the igneum repo), which this worktree's parent predates (an untracked cargo paths override to a copy beside the fork does it); and igneum-exec includes the proving ELF manifest and keys from proving/igneum-prove/elf/ five directories up, which the box mirror of the parent had to be given by hand.
7.7.3 The exec RPC crash on an empty state (the join gate's second finding, 7 October 2026, c6a62e00)
The join gate's first run on the fixed binaries (window 150, 07:31 UK) killed the joining node at 66 percent of its headers stage: a tokio worker panicked at igneum/exec/src/rpc.rs:689 ("index out of bounds: the len is 0 but the index is 0") and the node's panic hook exited the process. The site was eth_getBlockByNumber reading state.records[n] after resolve_block mapped "latest" to 0 on an exec state with no record (the follower was still "waiting for consensus to sync"); the client was the Igneum Miner app on the same Mac polling 127.0.0.1:26790, which the joining node had bound. Per tier: a home miner whose node restarts fresh or behind dies on the app's first poll before the follower has executed a block, and again on every restart until the follower gets ahead of the poll; a pool or seed with nothing on its eth_ port is untouched; the code is exec-sync's, so 0.3.17 carries it. Fix c6a62e00: record_at (a Result, never an index) at every single-record site, records_range at the three range sites, resolve_block refusing an empty state, the executing-copy and carried-records reads through get; known failed first, an_empty_exec_state_answers_every_block_read_without_a_panic. The coordinator's reading of the method map (the ten eth_ reads and igneum_getProofRecords, getAssignedShards, getSegment, getShardPlan, getTransactionStatus index the records) is covered by the same accessors and by a second test through the real dispatcher: all 46 methods on an empty state and on a one-record state with 19 parameter shapes each, a reply or an error and never a panic. Main's ruling: the dead harness node's client was not the shipped Miner app (it sends no block-tagged eth_ method), so no 0.3.17.1; 0.3.18 carries the fix.
7.7.4 GetBlockTemplate on a loaded node (the pool window, 7 October 2026, 500ddd66)
The pool window measured get_block_template at 1 to 1.6 s on pool-1 and its solo miner (template_ms=1632) while the mining manager's cache served a per-member template in 0.6 ms, so the second is in the RPC path; at 1 bps a template 1.6 s old loses about a quarter of the blocks found on it. 500ddd66: every stage timed (ibd state, finality section, mining manager, pow epoch, evm, finality behind), one debug line per request and an info line once per 10 s past 500 ms, so a loaded node names its sink; and the two walks get_pow_epoch_info ran on every request are memoised, both exact: the epoch-seed walk from the sink to the seed score (up to an epoch of chain blocks; the memo stands while the seed is still on the sink's selected chain and the chain block above it is at or past the score) and the live class-signal tally at the sink (seven windows; console and gate only; served for the same sink or for 10 s). The Mac reading (08:04 to 08:10 UK, the 10-bps harness at d 600 ms on 27d1b520, hub and spokes at debug):
| Node | Requests in 300 s | Total p50 / p90 / p99 / max | Stage means (ms) |
|---|---|---|---|
| hub | 4,253 | 0 / 0 / 0 / 10 ms | ibd 0.00, finality section 0.00, mining manager 0.01, pow epoch 0.00, evm 0.00, behind 0.00 |
| spoke 1 | 4,259 | 0 / 0 / 0 / 6 ms | the same; the slowest five are all the mining manager |
| spoke 2 | 4,306 | 0 / 0 / 0 / 9 ms | the same |
So on a young chain the RPC path is under a millisecond and the pool's second is chain-length work. The one per-request walk that scales with the chain is the live class-signal tally in get_pow_epoch_info: seven windows of 86,400 DAA on the devnet, which on pool-1's 250,000-DAA chain walks the whole chain and its mergesets on every template (250,000 header reads at a few microseconds each is the 1 to 1.6 s; the epoch-seed walk is at most one epoch and is memoised too). The reading is from the code and the arithmetic until the pool's stage line names it: 500ddd66's memo already serves the tally for 10 s per sink (in the 0.3.18 pin e69e8a39), and fd7de1b4 (the 0.3.19 line, 08:11 UK) refreshes it off the request path (the held tally served always, a refresh thread when stale, only a process's first request walks). The before is the pool's 1,632 ms; the after is the pool's stage line on the next canary. The 5,000-checkpoint join bench (7.7.1's asked size, before and after) was stopped at 07:00 UK because its rayon pool put the box at load 178 under the shipper's suites; it resumes when the 0.3.18 build chain has the box to itself.
7.7.6 The fresh-join cost, measured on the dc141409 canary (c18-1, 7 October 2026, 08:51 to 10:29Z)
The 0.3.20 tree's first fresh join of the live devnet (RTX 3070 pod, wiped datadir): started 08:51:50Z, synced 10:29:19Z at 141,357 blocks, 97.5 minutes wall; one headers-proof IBD and five relay catch-ups of a second each; no proof asks, no idle drops, the app's poller 4,217 reads with 0 errors. The IBD-end line (8.7 above): "finality time: bodies 16,592,238 ms over 141,699 blocks, virtual 1,061,858 ms over 36,183 changes, weight tables 968,243 ms over 359,709 tables (3,832,285,840 blocks walked), signatures 711,416 ms over 439,307, persists 40,537 ms over 3,324".
| Item | Canary (c18-1) | Bench, after row (box, 16 cores, 2,000 checkpoints) | Reading |
|---|---|---|---|
| Weight tables | 359,709 tables, 68 per checkpoint, 10,650 blocks walked each, 968 s | 2,335 tables, 1.2 per checkpoint, 18 s | the cache held 64 tables and cleared itself past that; one IBD batch of 2,000 bodies spans about 66 checkpoints, so every batch threw the set away and walked the window again |
| Signatures | 711 s over 439,307 | 29 s over 136,339 | 1.6 ms a signature against 0.2 ms: the pod's CPU, parallel pre-verification on fewer cores |
| Virtual | 1,062 s over 36,183 changes | 206 s over 67,221 | 29 ms a change against 3 ms |
| Bodies | 16,592 s over 141,699 (117 ms a block, summed over the body workers, 2.8x the wall) | 179 s over 67,221 (2.7 ms) | thread time, not CPU: the body workers queue on the finality state lock while one of them walks a table or verifies a certificate |
Fix on release-0.3.20-node: WEIGHT_TABLE_CACHE = 1,024 tables with oldest-first eviction, never a clear (FinalityManager::weights_at); gated by a unit test that fills the cache past the bound and reads the newest table back without a walk (the known-failed case is the old rule, which leaves one table after each clear). Expected on the next fresh join: the tables fall to about one per checkpoint (5,400 tables, about 15 s on the pod), the bodies thread time with them since the lock is held for the walk; the wall is then the bodies' own cost plus the signatures, about 60 to 70 minutes on the pod, approximate until the 0.3.20 canary's line.
7.7.7 The join bench, release rows (box, 7 October 2026, 11:03 to 12:35 UK)
cargo test --release --config 'env.IGNEUM_JOIN_BENCH="2000"' -p kaspa-consensus --lib join_bench: 2,000 checkpoints, 67,221 blocks, the joiner fed one block at a time. The measured part is the join; the bench's own chain building (about 10 to 20 minutes) and the compile (1 to 2 minutes) are not.
| Row | Tree | Conditions | Joined | Per checkpoint | Finality thread time |
|---|---|---|---|---|---|
| before | aa49613f (0.3.17) | nice 15, 16 cores, box load 175 to 190 | 425.3 s | 213 ms | bodies 266 s, virtual 62 s, tables 16 s, signatures 261 s |
| after | 420f9305 (0.3.20 feature, snapshot per change) | the same | 538.8 s | 269 ms | bodies 179 s, virtual 206 s, tables 18 s, signatures 29 s |
| before | aa49613f | nice 10, 32 cores, box load 20 to 30 (the d1285cef rule) |
439.6 s | 220 ms | bodies 269 s, virtual 64 s, tables 16 s, signatures 264 s |
| after, lazy snapshot | the 0.3.20 node line (this section's fixes) | the same | 183.1 s | 92 ms | bodies 37 s, virtual 51 s, tables 14 s, signatures 32 s |
Reading: the feature tree's parallel signature pre-verification cut the signatures 8x, but the template snapshot rebuilt on every virtual change (2.1 ms, serialized on the virtual processor) cost more wall than it saved, so the first after row joined 27 percent slower than before. With the snapshot refreshed only while templates are wanted (template_wanted, a minute after the last ask), the 0.3.20 line joins 2.4x faster than 0.3.17 under the same conditions (220 to 92 ms per checkpoint; the second before row and the lazy row ran back to back on the same bound). For a miner: a fresh 0.3.20 node replays a day of devnet checkpoints (2,400) in about 3.7 minutes of join on 32 box cores, against 8.8 on 0.3.17; the live join on a pod is set by the bodies and the IBD, see 7.7.6.
7.7.5 The proof oracle at never (ledger N9; the 0.3.19 canary's stall, 7 October 2026)
The e69e8a39 canary on c18-1 passed its headers proof and stalled in the block stage at block 2,728: the daemon installed the proof oracle whatever proving_consensus_verify_daa said, the IBD flow then demanded the proof bytes of every record-carrying block from the syncer, and the serve flow answers from a peer's in-memory pool (records drop at 600 chain blocks, proof bytes live as long as the process), so a block from months ago was served by nobody; 56 asks of pool-1 and the hub, 110 failed IBDs, every peer 0.3.17, which installs no oracle and so never asked. Fix dc141409 on release-0.3.19-node: ProofOracle::active_from carries the switch, and the body rule, the IBD fetch and the relay retry apply from it only (known-failed test on the pure gate). The retention store landed as aea0ca5c on release-0.3.19-node (10:04 UK): the proof archive under the exec db dir's proofs/, one file per proof by the carrying block's DAA, the index rebuilt at start, read after the pool by the oracle's held check and the serve flow, dropped below the pruning point in the chain-facts refresh; the rule applies above the pruning point only (the pipeline skips trusted bodies). Known failed first in its unit test (served by a second instance after the pool holds nothing; gone once the pruning point passes the carrier). The harness hook is e5f993d4 (--prover, --join-after, per-node exec ports on the join harness); the late-join case runs on the testnet lane's fast-time network with provers, the pre-archive binary its known-failed side.
7.8 The stale-block nuisance guard (ledger N6)
The record (the shipper, 6 October 2026, 20:04 to 21:03Z): after the roll-back, four nodes whose datadirs held blocks mined under the older rule re-advertised their sink to the hub on every reconnect (the ready step sends an inv of the sink); the hub answered wrong block version: got 1026 but expected 2, disconnecting, the sender re-dialled in 30 s and advertised it again, and the hub's connection count climbed from 310 to 551 in an hour with no block mined. It stopped when the fleet wiped the datadirs.
| Half | Commit | What it does |
|---|---|---|
| Sender | 319da644 | before the ready-step inv the node asks its own consensus whether the sink still passes its current header rules (HeaderProcessor::header_passes_current_rules: the in-isolation checks and the proof of work); a sink that does not is not advertised, with the line Not advertising sink ...: it fails this node's own current header rules (a datadir from another rule, ledger N6); mine a fresh block or wipe the datadir |
| Receiver | 319da644, a22cb261 | a relayed block refused by a rule is remembered for an hour (the relay flow's rule-error arm, and the IBD flow when an IBD a relay block started ends on a rule); a peer whose inv names a remembered block is banned ten minutes through the connection manager (FlowContext::ban_nuisance_peer) and the flow ends |
| The knob | f2b25fdf | IGNEUM_NUISANCE_GUARD: unset or 1 both halves, 0 neither (the known-failed shape), receiver or sender one half (devnet tool) |
Gate infra/fast-time/nuisance-peer.mjs: node B mines 60 s alone under an override with proof of work OFF and the miner's stub engine (kHeavyHash nonces only a skip-PoW node accepts), stops, and restarts on the SHARED override (proof of work on) with its datadir, so its DAG and its sink hold blocks its own rule now rejects while its digest matches node A's; B dials A and the two are watched for 300 s. The first run (01:16Z) mined with the real engine and B's blocks carried valid proof of work, so A refused nothing; skip_proof_of_work is in the digest, so two connected nodes cannot differ on it, which is why the shape is rebuilt across a restart.
| Case | SUMMARY | Summary file |
|---|---|---|
--guard on --expect flat (binaries at 319da644) |
PASS: B held 156 poisoned blocks across the restart; over 300 s A took 1 inbound connection, refused 0 relayed blocks; B printed the Not advertising sink line once and saw 0 rejects |
nuisance-peer-guard-on-expect-flat.json |
--guard off --expect climb (the known-failed shape) |
PASS: B held 154; A took 3 inbound connections, refused the same block twice (invalid proof-of-work in the IBD it started), banned 0; B saw 6 rejects. The climb's cadence on fast time is the addpeer re-dial's backoff (about two minutes), not the fleet's 30 s |
nuisance-peer-guard-off-expect-climb.json |
--guard off --expect flat (the harness's own failed shape) |
FAIL as it must | nuisance-peer-guard-off-expect-flat.json |
--guard receiver --expect flat (B keeps advertising; the ban's case) at 319da644 |
FAIL, the gap the case was for: A refused twice and banned nothing, because A had no blocks, took B's sink as an orphan, started IBD with B and refused the chain's headers IN THE IBD FLOW, so the relay flow's rule-error arm never ran and nothing was remembered; fixed at a22cb261 (the IBD flow remembers the relay block of a rule-ended IBD) | (overwritten by the re-run) |
--guard receiver --expect flat at a22cb261 (igneumd 2c53d508..., igneum-miner ef51249c...) |
02:39 to 02:44Z | PASS: B held 150 poisoned blocks; A refused once (IbdFlow flow error: block has invalid proof-of-work, disconnecting), banned B on its next advertisement of the same sink (Peer 127.0.0.1:64766 advertised block 469922fd... again after this node refused it by a rule within the last 3600 s (ledger N6); 127.0.0.1 banned for 600 s), 2 inbound connections over 300 s against 3 with the guard off; B saw 5 rejects |
7.9 The headers-proof join gate (the fuzz gate's extension; the 0.3.17 canary's own path)
infra/fast-time/headers-proof-join.mjs: node A mines past the pruning depth at the fast-time profile (finality 120, pruning 4,600) with three CPU threads; a fresh node B joins by headers proof; PASS = B's IBD completes and B holds A's sink with the 1026 header version. --window sets the sampled difficulty window in samples (150 = the minimum, 661 = the devnet's). Ports 30590s; the join harness's node B binds its own exec port only when the Mac's apps leave 26790 free.
| Run (UK) | Binaries | SUMMARY | Summary file |
|---|---|---|---|
| 04:45 to 05:55, window 661 | 80338689 (6e4ace3f + the timers) | FAIL, the known-failed shape found on the first run: B's window walk through the sampled trusted blocks ran out at a gap before genesis, "DAA window data has only 180 entries" ended the IBD, the N6 guard then banned the syncer on every retry (section 7.7.2's young-window reading; the devnet and the testnet never meet it, the fast-time profile does) | headers-proof-join-expect-join-w661.json, B's log beside it |
| 07:01 to 07:41, window 150 | 80338689 | FAIL: B died at 66 percent of its headers stage on the exec RPC panic (7.7.3, fixed in c6a62e00) | headers-proof-join-expect-join-w150.json, B's log beside it |
| 07:52 to 09:22, window 661 | 27d1b520 (e3798a17 + c6a62e00 + the timers) | PASS: A 4,622 blocks at DAA 5,341, the pruning point moved at 4,780 s; B took the headers-proof path, IBD completed in 15.0 s with 4,622 headers, sink version 1026, zero errors; B's IBD-end line: finality time bodies 8 ms over 4,622 blocks, virtual 16 ms over 54 changes, weight tables 5 ms over 178 tables (31,922 blocks walked), signatures 0 (no voters on that chain), persists 1 ms | headers-proof-join-expect-join-w661.json (overwritten by the run), B's log headers-proof-join-expect-join-w661-fixed-b-node.log |
Owed: the same case with the finality numbers of a voting chain (votes and certificates in the bodies, so the IBD-end line carries signatures), which is the join-cost bench's shape on a real network, and the fleet's own fresh join at e69e8a39 plus the oracle gate (7.7.5).