Finality: the total-weight floor is two thirds (O-3.15 decided, 4 October 2026)
A lock needs two thirds of all 30-day weight signing; finality pauses whenever less than two thirds is connected and signing, the chain runs on proof of work meanwhile and the node reports it. Spec 3.3, 3.3.1, 3.7, 3.9, 3.10 Q3 row, 3.11 rewritten with the new arithmetic (safety one third in every view, liveness two thirds connected, the per-view window bound stated as 3.7 item 9); O-3.15 decided, O-3.16 closed, O-3.18 and O-3.19 narrowed. Simulator: --floor, the +local partition mode, scenario L; A to L re-run at the 2/3 floor over five seeds with the 0.85 deltas in results_v2.md. Litepaper finality sentences and the 'does not claim' item. Ledger F2, F9, F16, F18 restated, F21 added (the window bound and the post-heal finality fork from the devnet). Bench-log: node build and tests, simulator deltas, three-node six-voter runs of 6A, 6B and 6A with a long heal on ports 29200 and up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b72b79bfa5
commit
271a0a2e76
7 changed files with 452 additions and 93 deletions
|
|
@ -542,3 +542,35 @@ Packs regenerated (`proto-cuda/packs/`): `igneum-genesis` and `igneum-hourly` (c
|
|||
| 2-node test network on the real engine (ports 29000 to 29012, `/tmp/igneum-gen2`, `igneum-devnet-900`, `IGNEUM_DEVNET_GENESIS_BITS=0x1f010000`, `IGNEUM_POW_EPOCH_BLOCKS=100`, `IGNEUM_POW_EPOCH_LEAD=20`, one 3-thread CPU miner per node for 300 s) | 338 blocks accepted on both nodes, 0 rejected, 0 invalid, sink identical at 10 of 10 samples; four epochs crossed (DAA 0, 100, 200, 300; epoch seeds `234e08...`, `d3f427...`, `de316c...`, `971384...`, all attempt 0, ids `8f8806638d59850f`, `c015349db63beb2c`, `d7d52120407a0b69`, `512527bb7a528476`), program and cache ready in 192 to 284 ms on the miners, 4 cache builds per node; m1 158 and m2 180 blocks at 0.046 MH/s each; one WARN per node (eth JSON-RPC port 26790 held by the live devnet node, harmless) |
|
||||
|
||||
Not done: no NVIDIA or AMD hardware has run a version 2 pack (the RTX 5090's 192 of 192 and the gfx1036 run of 3 October were version 1; the kernel text is unchanged); the edge, stats, determinism and memcheck sections of TESTS.md were not re-run (they do not depend on the generator); the live devnet (v3, version 1 programs) was not touched, so the cut-over is where version 2 goes live; `proto-metal/main.swift` carries the version 2 port uncommitted next to the hot-swap working-tree changes (not in this agent's file list), and the Mac app's Metal worker must be rebuilt from it before the cut-over or Mac GPU shares will fail the CPU re-check; the v4 binaries above were built from the worktree as found, which also holds another agent's uncommitted finality floor change (2/3 of total, O-3.15); the GPU `prepare` hot-swap path was not exercised here (CPU miners only). The ten non-load weights and the 6-sigma bias threshold remain prototype values (spec 1.16).
|
||||
|
||||
## 2026-10-04 finality floor 2/3: the total-weight floor raised from 17/30 to two thirds, simulator A to L re-run, attack scenarios 6A and 6B on a three-node, six-voter network (cryptographer)
|
||||
|
||||
Decision of 4 October 2026 (the project lead, O-3.15): a lock needs two thirds of all 30-day weight, and finality pauses whenever less than two thirds of that weight is connected and signing; the chain continues on proof of work meanwhile and the node reports it. Spec 3.3, 3.3.1, 3.7, 3.9, 3.11 rewritten; litepaper Finality and "What Igneum does not claim" updated; ledger F2, F9, F16, F18 restated and F21 added (the window bound of attack scenario 6A).
|
||||
|
||||
Node. Branch `devnet-v4` of `vendor/igneum-node` (worktree `vendor/igneum-node-v4`, from `dc749905`), commit `6457ca95`, two files: `consensus/core/src/finality.rs` (`FLOOR_NUM / FLOOR_DEN` 2/3, was 17/30; the Q3 arithmetic as `FinalityParams::{quorum_met, floor_met, locks}`, both comparisons inclusive) and `consensus/src/processes/finality.rs` (`lock_test` calls it). Build `CARGO_TARGET_DIR=target-integration nice -n 10 cargo build --release -j 6 -p kaspad --features kaspad/igneum-pow`, 3 min 17 s on a machine at load 3 to 13 (another agent's igneum-pow rebuild and the live devnet running). Tests `cargo test --release -j 6 -p kaspa-consensus-core -p kaspa-consensus -- finality`: 7 of 7 in consensus-core including the new `floor_is_two_thirds_of_total_and_inclusive` (4 of 6 locks, 3 of 6 does not, 2 of 3 locks, 67 of 100 locks, 66 does not, 57 does not; the total test implies the active test at every participation; a 3/3 side never locks whatever the other side's participation decays to), 2 of 2 in consensus (`no_certificate_while_the_window_is_filling` unchanged). The live devnet (26610, 26611, 26640, 26641, 28640, the seed relay on 26680 and `observer.mjs`) was never touched.
|
||||
|
||||
Simulator. `sim/finality_v2.py`: `--floor f` (a lock needs f x 2/3 of total; default 1.0 since this date, `--floor 0.85` reproduces the 3 October tables), the `+local` partition mode (a side's weight table counts only the blocks it has seen since the split, as a real node's window does; the 3 October tables kept weights global), scenario L (silent weight at 25 to 45%, churn, the poisoned eclipse, 12-day partitions with local weights), H widened to 30% and 33% attackers, I given the 34% case. A to G at `--quick` for seeds 7, 11, 13, 17, 19 (about 1 min a seed), H to L at full length for the same seeds (H 25 s, I 13 s, J 16 s, K 400 s, L 240 s), all at nice 10. Full tables and the 0.85 against 2/3 deltas in `sim/results_v2.md`, "Floor 2/3".
|
||||
|
||||
| Simulator measure | Floor 0.85 (3 October) | Floor 2/3 (4 October) |
|
||||
|---|---|---|
|
||||
| Smallest equivocator that splits a 50/50 honest partition (H, 150 and 360 min, retarget) | 14% in some seeds, 20% in every seed from minute 12 | 34% (sides 67.0%): 2 to 54 conflicts, first at minute 2 to 77; 33% (66.5%) and below: 0 conflicts, no lock on either side, every seed |
|
||||
| 40/40 honest plus a 20% equivocator reaching both, sides 60/60 (I) | 256 to 276 conflicts in 150 min | 0 conflicts, no lock |
|
||||
| Silent weight that keeps mining: where the pause begins (J, L1) | between 40% (13-min first lock) and 45% (never) | between 32% and 34%: 30% locks every checkpoint, 32% locks 69 to 100%, 33% locks 0 to 11%, 34% and above lock nothing for as long as they stay silent; first lock 0 min after the silent set returns |
|
||||
| Churn, first lock (L2, D) | 35%: 2 min; 50%: 4.1 days | 35%: 1.7 days (analytic 1.4); 50%: 10.1 to 10.3 days (analytic 10.0) |
|
||||
| Poisoned eclipse, 34% attacker plus a 20% pool, 1, 2, 4 h (L3) | 0 conflicts | 0 conflicts, 0 locks on the eclipsed side, every seed |
|
||||
| Bought keys worth 40% that withhold their votes, 30 days (K) | 305 to 1,085 stalls of 86,400 | 63,307 to 68,716 stalls: the pause lasts until the bought weight decays below one third, day 19 to 20 |
|
||||
| Long honest partition, each side counting only what it has seen (L4, 12 days) | 50/50: both sides lock alone from day 4.1; 60/40: the 60 side at once, the 40 side from day 8.5 | 50/50: day 10.1 to 10.3 (predicted 10.0); 60/40: the 60 side from day 5.1 (predicted 5.0), the 40 side never in 12 days; 55/45: day 7.9 and 12.0 |
|
||||
|
||||
Test network. Three `igneumd` (the floor-2/3 build) on `igneum-devnet-921` (6A), `-922` (6B) and `-923` (6A, long heal): n1 listens (gRPC 29210, p2p 29211, wRPC 29212), n2 dials n1 (29220 to 29222), n0 dials n1 through a TCP proxy on 29290 (29200 to 29202); cutting the proxy isolates n0 from {n1, n2}. Data under `/tmp/igneum-floor`; harness `/tmp/igneum-floor/harness/floor-run.mjs` over the env-driven `lib/` of the fin-fixes re-run (the repo harness `tools/finality-attacks` was not edited; its s6 still reads 56.7%). Override: `skip_proof_of_work`, interval 30, depth 20, window 1,800 DAA, dust 5, presence 20, 8 aggregators, ban 1,800, `min_daa` 1,800, fallback 15. Six vmine voters from the fin-attacks `igneum-miner` at share 1/6, 6 bps (each 611 to 639 blocks accepted over the run, 0 rejected from the miner's side), 390-s warmup (the window full at DAA 1,800, locks from about 300 s), 150-s split, 60-s heal window (300 s in the third run). Predicted bound for a 3/3 side on a full sliding window: share(T) = 1/2 + R T / (2 W), so two thirds at T* = W / (3 R) = 200 s at W = 1,800 and R = 3 blocks/s; the old floor's 17/30 at 2 W / (15 R) = 80 s.
|
||||
|
||||
| Scenario | Criterion (spec Q3, 3.3.1) | Measured | Verdict |
|
||||
|---|---|---|---|
|
||||
| 6A, 3/3 (a0 a1 a2 on n0; b0 b1 on n1, b2 on n2), 150 s | zero new locks on any node during the split (each side under two thirds of its own table); no conflicting certificates; locks resume after the heal | cut at DAA 2,250 with 75 locks on all three nodes, window 1,800 of 1,800, side A at 48.3% and side B at 51.7% of every node's table; new locks during the split 0 / 0 / 0; shares at the end of the split 60.8% (A) and 62.7% (B), both still under the floor and both past the old 56.7% floor (B crossed it at about 76 s, A at about 106 s, so the 3 October rule would have locked on both sides inside this split); conflicting certificates 0 / 0 / 0; `finality_reason` stayed `active` throughout (a lock within the last 20 indices); after the heal n1 and n2 resumed (75 to 97 within 60 s), n0 did not redial the proxy within 60 s (the connection manager retries a `--connect` peer at 30 x 2^attempts seconds, so after four failed attempts during the split the next redial was minutes away); the third run below extends the heal window | PASS on the floor (0 locks either side, 0 conflicts); the 60-s heal window was too short for n0's redial, see 6A long heal |
|
||||
| 6B, 4/2 (p0 p1 on n1, p2 p3 on n2; q0 q1 on n0), 150 s | the 4 side locks on both its nodes, the 2 side does not; no conflicting certificates; identical locked hashes across nodes | cut at DAA 2,399 with 79 / 80 / 79 locks; the 4 side held 1,222 of 1,800 = 67.9% of every table (Poisson noise put it 1.2 points over the floor at the cut); first new lock on the 4 side 2 s (n2, index 80) and 8 s (n1, index 81) after the cut, signed 1,222, active 1,800, total 1,800: 67.9% of total and of active, 4 votes seen, the two silent keys still at participation 1 inside the presence window so the two tests bound at the same fraction; 20 and 21 new locks on the 4 side during the split, 0 on the 2 side (32.1% rising to 42.1% of its own table by the end); 0 conflicting certificates; 0 locked indices disagreeing across nodes; n1 and n2 at 109 after the heal | PASS |
|
||||
| 6A again, long heal (`igneum-devnet-923`), 150-s split, 300-s heal window | as 6A, with a heal window longer than the redial backoff | cut at DAA 2,279 with 75 / 76 / 75 locks, sides at 49.9% and 50.1%; new locks during the 150-s split 0 / 0 / 1, the one being n2 catching up to the common pre-split checkpoint 76 at the instant of the cut (signed by both sides, 67.7% of total), so 0 side-alone locks either side; shares at the end of the split 61.1% and 61.8%. The gate reopened at 150 s but n0's redial came at 209 s (the 30 x 2^attempts backoff), so the sides kept mining apart. Side B locked alone first at 205 s after the cut: checkpoint 96 (blue score 2,880, 601 own blocks after the cut) by its 3 keys at 1,206 of 1,800 = 67.0% of total, one lock over the floor, against the predicted bound W / (3 R) = 200 s. Side A (n0) locked alone from checkpoint 98 at 215 s, 6 s after its redial, by its 3 keys at 1,017 of a table of 1,362 = 74.7%: once side B's 600 post-split blocks arrived they were merged red, so in n0's view they count for nothing (W2 counts blue blocks) and its own share rose at once. From there each side's F1 pinned it to its own certified chain: 26 conflicting certificates logged on n0 (indices 98 to 123), 2 on n1 and 3 on n2 (indices 118 to 120, the first of n0's to reach them), 23 locked indices disagreeing across the three nodes at the end of the 300-s heal window, 39 / 42 / 42 locks in all, no equivocation and no strip (each key voted once per index, for its own side's checkpoint). The network healed and finality did not: a finality fork with no attacker, exactly the state 3.11.4 leaves to operators (F5) | PASS on the floor for 150 s (0 side-alone locks); the window bound crossed at 205 s against 200 predicted; the heal does not undo it (spec 3.7 item 9, ledger F21) |
|
||||
|
||||
The long-heal run is the measurement the 3 October harness could not make: the old floor fell at 84 s of a young window (S6A); the two-thirds floor on a full 1,800-DAA window held for 150 s and fell at 205 s, 3.25x later as the arithmetic says (2F / (13R) against F / (2R) on a young window, 2W / (15R) against W / (3R) on a full one), and it fell on both sides within 10 s of each other because a 50/50 split crosses the bound at the same moment from both ends. On mainnet the same bound is 10 days of a 30-day window at 50/50 (spec 3.3.1, `sim/results_v2.md` L4). What the DAG adds to the simulation: after the heal the losing side's blocks are red in the winner's view, so the crossing is sudden rather than gradual, and once either side has certified a checkpoint of its own F1 never lets it back, so the fork is permanent until an operator sets a trusted certificate (F5, not implemented).
|
||||
|
||||
The inclusive comparison at exactly two thirds is pinned by the integer test `3 x signed >= 2 x total` and the unit test (4 of 6, 2 of 3 lock; the 3 October S6B run had already measured the active test passing at exactly 2/3 with 4 of 6 equal voters); the devnet's 4 side sat at 67.9% rather than 66.67% because block counts are Poisson, and locked at the first checkpoint after the cut.
|
||||
|
||||
Notes. (1) The v4 node logged "PoW rejected ... by igneum-lottery-v1-bound" about five times a second per node (2,952 lines on n0 over 6A) although the override carries `skip_proof_of_work` and the six miners saw every submission accepted; the window held exactly 1,800 blocks of weight on every node and each side's DAA advanced at 3 per second as planned, so the lines did not move the measurement, but what the v4 pipeline is re-checking there is a question for the consensus engineer before the v4 cut-over (30 of a sample of 200 rejected hashes were later accepted on the same node). (2) The repo harness `tools/finality-attacks/run.mjs` s6 criterion text and the s5 "below the 56.7% floor" pass test are now stale and should read two thirds. (3) Not done: the two-hour presence window and a 30-day window at mainnet length; the first-month gate under the new floor is unchanged (`min_daa` = window). (4) The harness's 60-s heal window (6A, 6B) is shorter than the connection manager's redial backoff for a `--connect` peer after a cut, so a healed proxy does not mean a reconnected n0 inside it; the long-heal run used 300 s and n0 redialled at 59 s after the gate reopened. (5) Stop everything: every node, miner and proxy of the three runs was stopped by the harness at the end of each run; ports 29200 to 29299 were free afterwards (`lsof` 0 listeners).
|
||||
|
|
|
|||
|
|
@ -1473,3 +1473,13 @@ Evidence: `docs/bench-log.md`, 4 October 2026 "difficulty rule: timestamp attack
|
|||
|
||||
- **F17** (keys are free, the draw is per key). Status: Fixed in the node (4 October 2026). `is_aggregator` draws the 8 aggregators by weight, `output x total < 8 x weight x 2^64`, so a splitter holds the tickets its weight buys and no more; spec 3.10 S1 row; unit test `sortition_is_by_weight_not_key_count` (200 dust keys plus 6 real ones); attack harness scenario 2 re-run: honest keys drew 1.61 seats per crowded checkpoint against 1.55 expected by weight, where master drew 0.32 against 0.33 per key (`docs/bench-log.md`, "finality fixes F17 and F1"). Still open from this entry: the client's one-key default, S2 (O-3.5), the bitmap size (O-3.12). Was: Rule fixed (spec 7.2 and W6), node per key.
|
||||
- **F1** (finality is attackable for the first month). Status: Fixed in the node on spec 3.8's recommended rule (4 October 2026); the litepaper statement and the launch-month simulation (O-3.1) remain. `min_daa = weight_window` (2,592,000 DAA s on mainnet, 7,200 on devnet): no checkpoint certifies and no certificate is accepted while the window behind it is younger than its full length, and the node reports "finality not active, window filling, N of M"; spec 3.10 C5 row; unit test `no_certificate_while_the_window_is_filling`; attack harness scenario 5 re-run: master locked checkpoint 1 at DAA 29 by the burster's key alone (1 of 1 voters, 22 of 22 weight), fin-fixes locked nothing under the window and first at checkpoint 61 (DAA 1,829) with 5 of 6 voters (`docs/bench-log.md`, same entry). Was: Conceded, not yet stated in the litepaper; experiment and rule change scheduled.
|
||||
|
||||
## Status updates, 4 October 2026 (the floor raised to two thirds, O-3.15; branch devnet-v4)
|
||||
|
||||
the project lead's decision of 4 October 2026: the total-weight floor of Q3 is 2/3, not 17/30. A lock needs two thirds of all 30-day weight signing (the active test is implied), and finality pauses whenever less than two thirds of the weight is connected and signing; the chain continues on proof of work meanwhile and the node reports it. Spec 3.3, 3.3.1, 3.7, 3.9, 3.11; `sim/results_v2.md`, "Floor 2/3"; `docs/bench-log.md`, "finality floor 2/3"; litepaper Finality and "What Igneum does not claim".
|
||||
|
||||
- **F2** (the two-hour presence window is an eclipse vector). Status: Closed by rule, floor raised (4 October 2026). A lock needs two thirds of total weight whatever the presence window says, so an eclipsed faction can lock only with two thirds of the network inside the eclipse, which is the twenty-day public event of spec 3.1. Re-measured at the 2/3 floor: the poisoned eclipse (34% attacker plus a 20% pool, the eclipsed side at 54%) gives 0 conflicting locks and 0 locks on the eclipsed side at 1, 2 and 4 h in every seed (`sim/results_v2.md` L3 and F2's floor1.00 rows). Was: Closed by rule (the 56.7% floor).
|
||||
- **F16** (a lock can become uncertified after a heal). Status: rule unchanged (spec 3.11.4: a verified certificate is never withdrawn, O-3.17 implements the conflict report). What the floor changes is how the state F16 describes arises: two certificates at one index now need equivocators holding at least one third of total weight in every scenario (two certificates need 4/3 of weight in signatures), not 13.3% across a partition that outlasts the presence decay (`sim/results_v2.md` H at 2/3: 0 conflicts and no lock on either side through a 33% equivocator, conflicts from minute 0 at 34%). Ten days of 100% hashrate in public, or the long-partition case of F21.
|
||||
- **F18** ("a silent minority cannot freeze finality" is false under the floor). Status: Fixed again (4 October 2026). The litepaper now says a lock needs two thirds of all 30-day weight and that finality pauses whenever less than two thirds is connected and signing. The pause threshold moved from about 42% of weight silent to one third: in the model, whose keys are in outage 2.2% of the time, 30% silent locks every checkpoint, 32% locks 88%, 33% locks 11% and 34% locks none for as long as it stays silent (`sim/results_v2.md` L1). Was: Fixed (56.7% sentence).
|
||||
- **F9** (half the hashrate leaves and finality stalls for ten days). Status: Conceded, stated (4 October 2026). Under the 2/3 floor the critic's number is back: 50% churn pauses finality for 10 days and 35% churn for 1.4 days, until the departed weight ages out of the window (`sim/results_v2.md` D's total column, which the 2/3 floor equals arithmetically, and L2). The chain runs on proof of work meanwhile, the node reports the pause, and the litepaper says so. This is the price of the one-third safety bound and was taken knowingly. Was: Answered by design (the active denominator recovered in two hours).
|
||||
- **F21** (new, from attack scenario 6A). "Your floor is a fraction of a table each side computes for itself. Cut the network in half and leave it cut: after a while each half's window is full of its own blocks, each half holds two thirds of its own table, and both lock without any attacker at all. Your simulation never saw it because it kept the weights global." Status: Conceded, stated (4 October 2026): spec 3.3.1, 3.7 item 9, 3.9 guidance; `sim/results_v2.md` L4 (view-local weights). Correct. A side with pre-split share s holds s + (1 - s) t / 30 of its own table on day t and two thirds of it from day 30 (2/3 - s) / (1 - s): day 10 at 50/50 (day 4 under the old floor), day 5 for the 60 side of 60/40 (at once under the old floor). On the devnet the old floor fell at 84 s of a young 1,439-DAA window (`docs/bench-log.md`, "finality v2 attack harness", S6A); at 2/3 the same cut on a full 1,800-DAA window held for the whole 150-s split and fell at 205 s against a predicted W / (3R) = 200 s (`docs/bench-log.md`, "finality floor 2/3", 6A and 6A long heal). What the devnet adds to the simulation: after the heal the other side's blocks are merged red, so each side's own share jumps rather than drifts, both sides of a 50/50 split certify their own checkpoints within 10 s of each other, and F1 then pins each node to its own certified chain: 26 conflicting certificates and 23 disagreeing locked indices across three nodes, no equivocation, a finality fork that the network heal did not undo and that only an operator's trusted certificate (F5, not implemented) can resolve. No rule removes it, because a view cannot count blocks it has never seen; the floor at two thirds moved the day from 4 to 10, and the exchange guidance treats a node partitioned for more than a day as proof of work until it has rejoined. A rule option for gate 3, not adopted: evaluate the floor against the table of the last locked checkpoint while no newer lock exists, which trades F9's 10-day recovery for a manual override.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Spec version 0.1, 3 October 2026. Status of this section: Designed. Simulated at the checkpoint level with latency, partitions and eclipses but without a DAG (`sim/finality_v2.py`, `sim/results_v2.md`, `docs/bench-log.md` entry "sim/finality_v2.py"). Not implemented. Not externally reviewed. Gate 3 is the external review that tries to break it.
|
||||
|
||||
The rule is exactly the design document's "Finality rule, version 2, after the second hostile review" with the quorum floor added on 3 October 2026 after the second simulation. CLAUDE.md's FINALITY RULE V2 paragraph is the short form. Every quantity is in blue score, DAA seconds or past-median time, never wall-clock (section 0.6). Past-median time, `mt(B)`, is the median timestamp of a block's sampled past (Kaspa's `consensus/src/processes/past_median_time.rs`, `PAST_MEDIAN_TIME_SAMPLE_INTERVAL` 10, kept): consensus data computed from the block's past, which a burst of blocks cannot run fast the way it runs DAA score (ledger M14 and F14, round 3).
|
||||
The rule is exactly the design document's "Finality rule, version 2, after the second hostile review" with the quorum floor added on 3 October 2026 after the second simulation and raised to two thirds of total weight on 4 October 2026 (the project lead's decision, O-3.15). The rule in two sentences: a checkpoint locks when two thirds of all 30-day mining weight has signed it. Finality pauses whenever less than two thirds of that weight is connected and signing, the chain continues on proof of work meanwhile, and the node reports it. CLAUDE.md's FINALITY RULE V2 paragraph is the short form (its 56.7% figure predates the decision). Every quantity is in blue score, DAA seconds or past-median time, never wall-clock (section 0.6). Past-median time, `mt(B)`, is the median timestamp of a block's sampled past (Kaspa's `consensus/src/processes/past_median_time.rs`, `PAST_MEDIAN_TIME_SAMPLE_INTERVAL` 10, kept): consensus data computed from the block's past, which a burst of blocks cannot run fast the way it runs DAA score (ledger M14 and F14, round 3).
|
||||
|
||||
Two statements frame everything below. Finality is miner-only and self-contained: no stake, no bond, no other chain, no committee that is not the set of recent miners. Equivocation costs history, not coins, because there are no coins to slash.
|
||||
|
||||
|
|
@ -39,34 +39,38 @@ All in public, on the hashrate charts. 51% never reaches 2/3 while honest miners
|
|||
|
||||
- **Q1.** Presence window P = 7,200 s of past-median time: index j is in the window of index i when `0 < mt(C_i) - mt(C_j) <= 7,200`. About 240 indices at the target rate. Denominated in median time, not indices or DAA seconds, so a burst of blocks cannot shrink the window to minutes (ledger M14 and F14, round 3; the simulation ran with a fixed 240 indices).
|
||||
- **Q2.** Participation of key k at index i, "block reading" (rule of 3 October 2026, ledger F3): the number of indices j in the presence window of i (Q1) for which a valid vote by k at index j appears in the past of C_i, divided by the number of indices in that window, capped at 1. A vote "appears in the past of C_i" when it is carried, as a vote or inside a certificate, by any block in the past of C_i, blue or red. A key whose first block is younger than the presence window counts 1. Votes are block payload: every block MUST carry every valid vote its producer has received for i_b or an index in its presence window (i_b the highest checkpoint index determined in the block's past) that is not already carried by a block in its past, up to the per-block vote bound of O-3.3; votes for one `(index, checkpoint hash)` pair MAY be aggregated inside the block into one BLS signature with a bitmap. A block that omits a vote it has received is not invalid (no node can prove what another received); the rule binds honest producers and the argument of 3.3.2 says why that is enough. This reading is objective, since every node computes it from the same past of C_i, and self-healing, since a missed index rolls out of the window after two hours of median time. The "cert reading" of the simulation (only votes inside certificates count) is a subset of it and was the reading the simulation ran with; the node-local "seen" reading is rejected as not objective and the "frozen" reading as total with extra steps (`sim/results_v2.md`, "Recommended parameters").
|
||||
- **Q3.** Active weight at i is the sum over voters of weight x participation. Total weight at i is the sum of weight over all keys above dust. A certificate for index i locks when the unscaled weight of its signers is at least **2/3 of active weight** AND at least **56.7% of total weight** (the floor 0.85 x 2/3 = 17/30). Both tests use weights and participation computed at C_i, so any node can verify a certificate from C_i's past.
|
||||
- **Q3.** Active weight at i is the sum over voters of weight x participation. Total weight at i is the sum of weight over all keys above dust. A certificate for index i locks when the unscaled weight of its signers is at least **2/3 of active weight** AND at least **2/3 of total weight** (the floor; 17/30 from 3 October 2026 until the project lead raised it to two thirds on 4 October 2026, O-3.15). Both comparisons are inclusive: exactly two thirds locks. Both tests use weights and participation computed at C_i, so any node can verify a certificate from C_i's past. Since active weight never exceeds total weight, the second test implies the first: in plain words, a checkpoint locks when two thirds of all 30-day weight has signed it, and finality pauses whenever less than two thirds of that weight is connected and signing, with the chain continuing on proof of work meanwhile and the node reporting the pause (3.9). The active test and the participation of Q2 stay in the rule as the liveness-side report: they are what the node shows operators about who is present, and they are the test that would bind again if a later review lowered the floor.
|
||||
- **Q4.** Certificate grace: an aggregator closes a certificate at the later of quorum time and `t0 + grace`, where grace MUST be at least 3x the worst honest one-way network delay (15 s in the simulation at a 2-s delay; at a 5-s delay the slowest region already lost 1.7 points of participation to a 15-s grace, `sim/results_v2.md` A). The value is Open (O-3.4) and does not affect validity, only which votes a certificate carries.
|
||||
|
||||
### 3.3.1 Why the floor, from the simulation
|
||||
### 3.3.1 Why the floor, and why two thirds, from the simulation
|
||||
|
||||
Both denominators alone fail (`sim/results_v2.md`, seed 7, seed 11 agrees on B and E):
|
||||
The active denominator alone fails (`sim/results_v2.md`, seed 7, seed 11 agrees on B and E): a side that cannot see the other side's votes sees stalls, stalls shrink its denominator by 1/240 per index, and once the denominator has fallen to 1.5x the side's own weight the side certifies alone, after `240 x (1 - 1.5 s) / s` slots for a side of share s (confirmed within 5% across the sweep): 60 min for a 50% side, 120 for 40%, 180 for 33%. A 50/50 honest partition produced 90 conflicting locks in 150 min, a 60/40 one 624 in 360 min, a 33/33/34 one 1,198, and a 34% attacker poisoning one eclipsed 20% pool finalised a private fork in 49 min (F2). The floor is the second test of Q3 and makes it bind before that point. Its level was 0.85 x 2/3 = 17/30 from 3 October 2026 (chosen because no honest side of any tested split held 56.7% and 40% silent still recovered in 13 minutes) and is 2/3 since 4 October 2026 (the project lead, O-3.15), after 3.11.2 showed that every point of liveness the lower floor bought past one third cost two points of partition safety, and after attack scenario 6A showed the lower floor falling on the devnet.
|
||||
|
||||
| Scenario | Active alone (cert reading, P = 240) | Total alone | Active + floor 0.85 |
|
||||
The two floors, measured (`sim/results_v2.md`: the 3 October tables at 0.85, the "Floor 2/3" section at 2/3, five seeds; every cell at 2/3 equals the total-denominator column of the 3 October tables, as the arithmetic requires):
|
||||
|
||||
| Scenario | Active alone (cert reading, P = 240) | Floor 0.85 (lock needs 56.7% of total) | Floor 2/3 (lock needs 66.7% of total, Q3) |
|
||||
|---|---|---|---|
|
||||
| E: 50/50 honest partition, no attacker, 150 min | 90 conflicting locks, first at 60 min (240 with instant DAA retarget, first at 30 min) | 0 | 0 |
|
||||
| E: 60/40 honest partition, 360 min, with retarget | 624 conflicts | 0 | 0; majority side locks from minute 13 |
|
||||
| E: 33/33/34, 360 min, with retarget | 1,198 conflicts | 0 | 0 |
|
||||
| F2: 34% attacker poisons one eclipsed 20% pool, 1 / 2 / 4 h | 10 / 65 / 174 conflicts, first at 49 min | 0 | 0 (floor 0.80 gave 10 / 65 / 174, because the eclipsed side's 54% clears 53.3%) |
|
||||
| C: 34% of weight silent, keeps mining | 0 min to first lock, 0 stalls | never (8,666 stalls in 3 days, and for the whole window) | 0 min |
|
||||
| C: 40% silent | 13 min, 26 stalls | never | 13 min, 138 intermittent stalls in 3 days |
|
||||
| C: 45% silent | 20 min | never | never, for as long as they stay silent |
|
||||
| D: 35% churn (stops mining and signing) | 2 min | 41 h | 2 min, 98 intermittent stalls in 3 days |
|
||||
| D: 50% churn | 31 min | 10.1 days | 4.1 days (floor 0.80: 2.2 days) |
|
||||
| H: 50/50 honest partition with an equivocator holding a of total, 150 and 360 min, retarget | breaks at every a | 0 conflicts up to 13%; 14% (sides 57.0%) conflicts in some seeds from minute 48; 20% (60%) 256 to 276 conflicts from minute 12; 34% from minute 0 | 0 conflicts and no lock on either side up to 33% (sides 66.5%); 34% (sides 67.0%) 2 to 54 conflicts, first at minute 2 to 77, the sides at the knife edge against the 2.2% outage shortfall |
|
||||
| I: 40/40 honest with a 20% equivocator reaching both (sides 60/60) | breaks | 256 to 276 conflicts, first at minute 12 to 16 | 0 conflicts, no lock on either side |
|
||||
| I: 40/40/20 honest, with or without a 10% or 20% equivocator | breaks | 0 conflicts, no side locks | 0 conflicts, no side locks |
|
||||
| E: 60/40 honest, 360 min | 624 conflicts | 0; the 60 side locks from minute 13 | 0; neither side locks |
|
||||
| F2 and L3: 34% attacker poisons one eclipsed 20% pool, 1 / 2 / 4 h | 10 / 65 / 174 conflicts, first at 49 min | 0 (floor 0.80 gave 10 / 65 / 174, because the eclipsed side's 54% clears 53.3%) | 0 conflicts, 0 locks on the eclipsed side, every seed |
|
||||
| C, J, L1: weight silent, keeps mining | 0 min at 34%, 13 min at 40%, 20 min at 45% | 0 min at 34%; 13 min and 138 intermittent stalls at 40%; never at 45% | every checkpoint at 30% (one seed 40 stalls in 6 h); 69 to 100% of checkpoints at 32%; 0 to 11% at 33%; never at 34% and above, for as long as they stay silent |
|
||||
| D, L2: churn (stops mining and signing) | 2 min at 35%, 31 min at 50% | 2 min at 35% (98 intermittent stalls); 4.1 days at 50% | 1.7 days at 35% (analytic 1.4), 10.1 to 10.3 days at 50% (analytic 10.0) |
|
||||
| K: bought keys worth 40% withhold their votes, 30 days | not run | 305 to 1,085 stalled checkpoints of 86,400 | 63,307 to 68,716 stalled: the pause lasts until the bought weight has decayed below one third, day 19 to 20 |
|
||||
| L4: long honest partition, each side counting only the blocks it has seen, 12 days | not run | 50/50: both sides lock alone from day 4.1; 60/40: the 60 side at once, the 40 side from day 8.5; 55/45: day 1.2 and 6.5 | 50/50: both from day 10.1 to 10.3; 60/40: the 60 side from day 5.1, the 40 side never in 12 days; 55/45: day 7.9 and 12.0 |
|
||||
|
||||
The mechanism active alone fails by: a side that cannot see the other side's votes sees stalls, stalls shrink its denominator by 1/240 per index, and once the denominator has fallen to 1.5x the side's own weight the side certifies alone, after `240 x (1 - 1.5 s) / s` slots for a side of share s (confirmed within 5% across the sweep): 60 min for a 50% side, 120 for 40%, 180 for 33%. The floor makes the second test of Q3 bind before that point: no honest side of any tested split holds 56.7% of total.
|
||||
What two thirds buys: the safety bound is one third of total weight in every scenario, including partitions and eclipses of any tested length, because two certificates at one index need two thirds of total each, 4/3 in all, so at least a third signed both (3.11.2). The 13.3% bound of the lower floor, the 14% knife edge and the 60/60 case are gone.
|
||||
|
||||
What the floor costs: liveness ends between 40% and 45% of weight silent (total alone: 34%; active alone: above 55%), 50% churn stalls 4.1 days, and at 40% silent or 35% churn the margin is one pool outage thin. The safety bound stays at 1/3 of weight for every event tested; the liveness bound moves from 1/3 (total) to about 42% silent.
|
||||
What two thirds costs: finality pauses whenever less than two thirds of 30-day weight is connected and signing. In the model, whose honest keys are in outage 2.2% of the time, the pause begins between 32% and 34% of weight silent, a silent set that keeps mining holds it for as long as it stays silent, and a departed set holds it for `30 (1 - 1/(3x))` days (1.4 days at 35%, 10 days at 50%, the figures ledger F9 quotes). The chain continues on proof of work meanwhile, every certified checkpoint stays binding, the node reports the pause, and the first lock comes 0 minutes after the missing weight returns (J, L1). That is the rule in two sentences, and the litepaper states it.
|
||||
|
||||
What no floor removes: the weight table is per view. A side of an honest partition fills its own window with its own blocks, holds `s + (1 - s) t / 30` of its own table on day t for a pre-split share s, and reaches two thirds of it on day `30 (2/3 - s) / (1 - s)`: 10 days at 50/50 (4 under the old floor), 5 days for the 60 side of 60/40 (0 under the old floor). L4 measures this within the outage shortfall; the devnet found the young-window form of it first (attack scenario 6A, `docs/bench-log.md`: the old floor fell at 84 s of a 1,439-DAA window, the bound being `2F / (13R)` for a window weight F and a side rate R, which at two thirds becomes `F / (2R)`, 3.25x longer; re-measured on a full 1,800-DAA window at the 2/3 floor, "finality floor 2/3": the bound is `W / (3R)` = 200 s, the floor held for the 150-s split and fell at 205 s, and the fork it left was not undone by the heal). 3.7 item 9 and the exchange guidance of 3.9 carry it.
|
||||
|
||||
The simulation ran with the cert reading of participation. The block reading of Q2 credits a superset of the same votes (every vote in a certificate is in a block, and votes carried outside certificates are added), so a key's participation under the block reading is never lower than under the cert reading. For a key that is silent (C) or gone (D) nothing changes, because it emits no votes. For a key whose votes arrive late (F1 below) the block reading keeps it in the denominator for longer, which raises the weight a lock needs. That direction is safe; its cost in lock latency and in the C and D stall figures is the re-run named in O-3.3, with the per-block vote bound as the parameter.
|
||||
|
||||
### 3.3.2 The eclipse case (ledger F2): closed by the floor
|
||||
|
||||
Decision of 3 October 2026. The presence window of Q1 is an eclipse vector on its own: a faction that keeps the big pools' votes from the rest of the network for two hours, without stopping their blocks, becomes the whole of active weight and locks alone. The answer is not a longer window or a silent-key rule; it is the second test of Q3, already in the rule. A lock needs at least 56.7% of total weight whatever the presence window says, so an eclipsed or isolated faction can never lock unless it holds a majority of all 30-day weight, and a faction that holds that much is a public 51% event of 3.1, not an eclipse.
|
||||
Decision of 3 October 2026, floor raised 4 October 2026. The presence window of Q1 is an eclipse vector on its own: a faction that keeps the big pools' votes from the rest of the network for two hours, without stopping their blocks, becomes the whole of active weight and locks alone. The answer is not a longer window or a silent-key rule; it is the second test of Q3, already in the rule. A lock needs two thirds of total weight whatever the presence window says, so an eclipsed or isolated faction can never lock unless it holds two thirds of all 30-day weight, and a faction that holds that much is the twenty-day public event of 3.1, not an eclipse.
|
||||
|
||||
The numbers (`sim/results_v2.md`, section F, seed 7, 1,000 keys, one pool holding 20% of total weight always online in its own region):
|
||||
|
||||
|
|
@ -75,10 +79,11 @@ The numbers (`sim/results_v2.md`, section F, seed 7, 1,000 keys, one pool holdin
|
|||
| F1: pool receives every block and vote D hours late and votes correctly, late | any | 0 / 0 / 0 | never | 0.500 at 1 h, 0.000 at 2 and 4 h | 120 to 125 min |
|
||||
| F2: a 34% attacker feeds the pool a private fork, signs both forks; eclipsed side holds 54% of weight, honest side 46% | active alone (cert reading) | 10 / 65 / 174 | 49 min | 0.787 / 0.562 / 0.108 | 115 / 85 / 20 min |
|
||||
| F2, same | active + floor 0.80 (lock needs 53.3% of total) | 10 / 65 / 174 | 49 min | same as active alone | same |
|
||||
| F2, same | **active + floor 0.85 (lock needs 56.7% of total, rule Q3)** | **0 / 0 / 0** | **never** | 0.738 / 0.471 / 0.000 | 125 min |
|
||||
| F2, same | active + floor 0.85 (lock needs 56.7% of total, the rule of 3 October) | 0 / 0 / 0 | never | 0.738 / 0.471 / 0.000 | 125 min |
|
||||
| F2, same | **active + floor 2/3 (lock needs 66.7% of total, rule Q3 since 4 October; L3, five seeds)** | **0 / 0 / 0** | **never** | 0.725 to 0.738 / 0.442 to 0.471 / 0.000 | 120 to 125 min |
|
||||
| F2, same | total alone | 0 / 0 / 0 | never | 0.738 / 0.471 / 0.000 | 125 min |
|
||||
|
||||
Why 0.85 and not 0.80: the eclipsed side in F2 holds 54% of total weight, which clears a 53.3% floor and fails a 56.7% one. Under the active denominator alone the eclipsed view certifies the attacker's fork 49 minutes in, once its denominator has decayed below 54% / (2/3) = 81%; the 0.85 floor binds before that point at every eclipse length tested, and the honest side saw 0 stalls during and after the heal in every row. A pure delay (F1) never produced a conflicting lock under any denominator because 80% of weight kept signing; its only cost falls on the delayed pool, which leaves the denominator and returns to participation 1 about two hours after its view catches up.
|
||||
Why the floor and not the window: the eclipsed side in F2 holds 54% of total weight, which clears a 53.3% floor, fails a 56.7% one and fails two thirds by a wide margin. Under the active denominator alone the eclipsed view certifies the attacker's fork 49 minutes in, once its denominator has decayed below 54% / (2/3) = 81%; any floor above 54% binds before that point at every eclipse length tested, and the honest side saw 0 stalls during and after the heal in every row. At two thirds the same attacker would need a 33% pool beside its own 34% to bring the eclipsed side to the floor, which is two thirds of the network inside one eclipse, the 51% event of 3.1 and not an eclipse. Re-measured at the 2/3 floor in `sim/results_v2.md` L3 (five seeds, 1, 2 and 4 h): 0 conflicting locks, 0 locks on the eclipsed side. A pure delay (F1) never produced a conflicting lock under any denominator because 80% of weight kept signing; its only cost falls on the delayed pool, which leaves the denominator and returns to participation 1 about two hours after its view catches up.
|
||||
|
||||
What this does not prove. The model grants the attacker the eclipse for free and lets it mine its fork at its full rate for the pool alone while still signing the honest chain (`sim/results_v2.md`, "cannot tell us"). The attacker in F2 holds 34%, above the 1/3 safety bound of 3.7, which is the point: with the floor, even a faction above the bound cannot turn an eclipse into a conflicting lock. The devnet single-node eclipse of O-3.7 confirms the rule against a real network; it does not reopen the choice of rule.
|
||||
|
||||
|
|
@ -116,14 +121,15 @@ Two votes by one key for different checkpoint blocks at one index are equivocati
|
|||
|
||||
## 3.7 Residual risks, stated
|
||||
|
||||
1. Safety holds with under one third of window weight under hostile keys. Reaching a third takes ten days of 100% hashrate or twenty days of 51%, in public. Beyond a third, two locks can coexist under a partition (E: a 34% equivocator across a 50/50 split breaks every variant, 33% + 34% = 67% per side) and equivocation costs history, not coins.
|
||||
2. Liveness pauses after a sudden loss of more than about 42% of weight from signing (3.3.1). During a pause the chain is proof-of-work only in practice, so the node ships a flag that tells exchanges to credit nothing until the next lock (3.9).
|
||||
1. Safety holds with under one third of window weight under hostile keys, in every scenario tested, including partitions of any length over which the weight tables agree: two certificates at one index need two thirds of total weight each, 4/3 in all, so at least one third of the weight signed both and that weight is equivocating (3.11.2). The bound does not depend on the presence window, on synchrony or on how long a partition lasts. Reaching a third takes ten days of 100% hashrate or twenty days of 51%, in public. At a third and beyond, two locks can coexist under a partition (H: a 34% equivocator across a 50/50 split gives each side 67% and both lock at minute 0; 33% gives 66.5% and neither locks) and equivocation costs history, not coins.
|
||||
2. Liveness pauses whenever less than two thirds of 30-day weight is connected and signing (Q3). With the model's 2.2% of honest weight in outage at any moment, the pause begins at about 32% silent in the simulation and is total from 34% (`sim/results_v2.md` L1); a set that keeps mining can hold the pause for as long as it stays silent, a set that stops mining ages out over 30 (1 - 1/(3x)) days for a share x (1.4 days at 35%, 10 days at 50%, L2 and D). During a pause the chain continues on proof of work: blocks, GHOSTDAG ordering and execution go on, every certified checkpoint stays binding, and the node reports `finality_active` false with the reason `paused` so that exchanges credit nothing until the next lock (3.9). The pause is the price of the one-third safety bound of item 1; the project lead took it on 4 October 2026 (O-3.15).
|
||||
3. Pools hold their hashers' votes. Vote concentration equals pool concentration, as on Bitcoin, and is public. Stratum v2 job declaration changes transaction choice, not the vote key (ledger F10, G6).
|
||||
4. A 51% owner who drives half the honest miners away and holds for 30 days owns finality thereafter. Same as Bitcoin, with a month's warning.
|
||||
5. The delay function (section 4) is a new dependency. The evaluator ships in every node.
|
||||
6. The dust threshold excludes solo miners under 100 blocks a month from voting, not from rewards.
|
||||
7. New honest miners are under-weighted for the whole window: after an overnight doubling the new cohort holds t/60 of weight on day t and the old cohort can lock without a single new signature for 19 days (`sim/results_v2.md` G). A doubling and a 1x renter are the same event to the rule, by design.
|
||||
8. The simulation has no DAG: a partition side's checkpoint block is "the block at blue score 30 i in that view" and conflict counts are index collisions, not reorg depths. Real GHOSTDAG merge under the 3,600-s bound, DAA lag (of the order of an hour, approximate), VRF aggregator noise, uptime (the 0.978 resting participation is a guess), regional silent sets and the cost of keys are all outside the model.
|
||||
9. The weight table is per view. A side of an honest partition sees only its own blocks after the split, so its share of its own 30-day table rises as `s + (1 - s) t / 30` on day t for a pre-split share s, and it holds two thirds of its own table from day `30 (2/3 - s) / (1 - s)`: day 10 at 50/50, day 5 for the 60 side of 60/40, day 7.8 for the 55 side of 55/45 (3.3.1, measured in `sim/results_v2.md` L4 and found first on the devnet by attack scenario 6A, where the old floor fell at 84 s of a young window). From that day the side locks alone and two sides can hold conflicting locks at the heal with no attacker. The heal does not undo it: the other side's blocks arrive and are merged red, which only raises each side's share of its own table (W2 counts blue blocks), each side certifies its own checkpoints, and F1 pins every node to the chain its certificates name, so the network heals and finality stays forked until an operator sets a trusted certificate (F5; 3.11.4). Measured on the devnet: a 50/50 split on a full 1,800-DAA window at 3 blocks/s held for 150 s and both sides locked alone at 205 and 215 s against a predicted W / (3R) = 200 s, leaving 23 locked indices in disagreement across three nodes with no equivocation (`docs/bench-log.md`, "finality floor 2/3", 6A long heal). Item 1's bound is about the weight each view counts; a partition that lasts a third of the window changes what the views count. The exchange guidance of 3.9 therefore treats a pause combined with peer loss as proof of work, and 3.11.4 says what the node does with the pair.
|
||||
|
||||
## 3.8 The first month
|
||||
|
||||
|
|
@ -146,10 +152,10 @@ Designed. The node exposes `finality_active` (true when a certificate has formed
|
|||
|---|---|
|
||||
| `finality_active` and the deposit's block is in the past of `last_certified` | Credit. Expected wait about 90 to 120 s after inclusion |
|
||||
| `finality_active`, deposit not yet covered | Wait for the next certificate; do not count blocks |
|
||||
| `finality_active` false (first month, or a pause under 3.7 item 2) | Treat the chain as proof of work. The reorg bound to rely on is the finality depth of section 2.1, 43,200 DAA s: the node follows any heavier selected chain forked above it (ledger F15, round 3). Merge depth, 3,600 DAA s, limits which old blocks can be merged and is not a reorg bound. Credit nothing until the deposit's block is at least 12 hours of past-median time below the selected tip; count median time, not DAA score, because DAA seconds run fast during a retarget lag (ledger M14). For amounts that matter apply the operator's own hashrate judgement, as for any young proof-of-work chain. Listings are not sought before launch in any case (ledger X8) |
|
||||
| `finality_active` false (first month, or a pause under 3.7 item 2: less than two thirds of 30-day weight connected and signing, reason `paused`) | Treat the chain as proof of work. The reorg bound to rely on is the finality depth of section 2.1, 43,200 DAA s: the node follows any heavier selected chain forked above it (ledger F15, round 3). Merge depth, 3,600 DAA s, limits which old blocks can be merged and is not a reorg bound. Credit nothing until the deposit's block is at least 12 hours of past-median time below the selected tip; count median time, not DAA score, because DAA seconds run fast during a retarget lag (ledger M14). For amounts that matter apply the operator's own hashrate judgement, as for any young proof-of-work chain. Listings are not sought before launch in any case (ledger X8) |
|
||||
| Two certificates at one index observed (C4 evidence) | Suspend credits until the node's view resolves under 3.5 |
|
||||
|
||||
A certified checkpoint overrides the heaviest chain, so fresh hashrate cannot reorganise past `last_certified`; two thirds of 30-day weight can, and 3.1 says what that costs.
|
||||
A certified checkpoint overrides the heaviest chain, so fresh hashrate cannot reorganise past `last_certified`; two thirds of 30-day weight can, and 3.1 says what that costs. A pause is not an attack: it means less than two thirds of the weight is signing, which the node also shows as the participation of each key (Q2) and which an operator sees coming as keys drop out of the presence window. A pause together with a loss of peers is a partition; under 3.7 item 9 the side the node is on may come to lock alone after days, so a node that has been partitioned for more than a day should be treated as proof of work until it has rejoined and `finality_active` is true again.
|
||||
|
||||
## 3.10 Implementation notes (devnet v2, 3 October 2026)
|
||||
|
||||
|
|
@ -166,7 +172,7 @@ Status of this section: Implemented in `vendor/igneum-node` (reading guide in `d
|
|||
| C4 | A second certificate at an index for another block is kept and logged (`conflicting_certificates`) | Not published as evidence. The rule is now fixed by 3.11 item 4 (the node keeps the certificate it verified first, never re-evaluates it, and reports the conflict); the node does not yet clear `finality_active` or expose `finality_conflict` when the pair appears |
|
||||
| C5, 3.8 | `min_daa` = `weight_window` (2,592,000 DAA s on mainnet, 7,200 on devnet; a unit test pins the equality). `evaluate` never locks, and `ingest_certificate` refuses a certificate from any source, while the checkpoint's DAA score is below `min_daa`; the node logs "finality not active, window filling, N of M" at every determination until the sink's DAA score reaches `min_daa` and reports the same through `getFinalityCheckpoints` (`finality_reason`, `window_filled_daa`, `window_full_daa`). Unit test `processes::finality::tests::no_certificate_while_the_window_is_filling`: one key holding 100% of the weight signs every checkpoint of a 150-block chain at a 60-DAA window; nothing certifies below DAA 60, a hand-built certificate at an early index is refused, every checkpoint from DAA 60 locks (fin-fixes, 4 October 2026) | Implemented on 3.8's recommendation ahead of the launch-month simulation (O-3.1), which is still not run; gate 3 can lower the gate but not remove it without reopening ledger F1. The sink's DAA score the report compares is the one the virtual processor last handed the manager, so a restarted node reports the window as filling until its first virtual resolution |
|
||||
| Q1, Q2 | Presence window 20 indices on devnet (240 mainnet). Block reading: participation counts the indices in `[i - P, i - 1]` at which a vote by the key is carried by any block, blue or red, in the past of C_i; a key whose first block in the window is younger than P x 30 DAA seconds counts the full window; every template carries up to 48 votes not already in its past, certificates and evidence first | The per-block vote bound (48) is the devnet value of O-3.3. Participation is credited for any vote by the key at the index, whatever block it names; 3.11.1 requires the vote to name the checkpoint on the crediting chain, else a key can stay in the active denominator by voting for blocks of its own and never add to a certificate (O-3.19) |
|
||||
| Q3 | Integer tests: `3 x signed x P >= 2 x active_num` (active_num = sum of weight x participation count) and `30 x signed >= 17 x total`, both at C_i; bans known at evaluation time are applied to the voter list | |
|
||||
| Q3 | Integer tests: `3 x signed x P >= 2 x active_num` (active_num = sum of weight x participation count) and `3 x signed >= 2 x total` (was `30 x signed >= 17 x total` until 4 October 2026; `FinalityParams::FLOOR_NUM / FLOOR_DEN` = 2/3 on branch `devnet-v4`, with `quorum_met`, `floor_met` and `locks` as pure functions), both inclusive, both at C_i; bans known at evaluation time are applied to the voter list. Unit test `floor_is_two_thirds_of_total_and_inclusive`: 4 of 6 locks, 3 of 6 does not, 67 of 100 locks, 66 does not, the total test implies the active test for every participation. Measured on the three-node, six-voter network of `docs/bench-log.md`, "finality floor 2/3" (4 October 2026): no lock on either side of a 3/3 split, the 4 side of a 4/2 split locks at exactly two thirds | |
|
||||
| Q4 | No grace. A node that serves an eligible aggregator (S1) aggregates and gossips a certificate the moment the votes it has seen meet Q3, naming that aggregator; any other node waits until the sink is `checkpoint_depth + aggregator_fallback` DAA seconds past the checkpoint block (fallback 15 on both networks) and then aggregates with a zero aggregator (anyone MAY aggregate, the liveness fallback; fin-fixes, 4 October 2026) | The grace timer (O-3.4) is not implemented: the fallback bounds how long a checkpoint waits for its drawn aggregators, it does not hold a certificate open for late votes, so a certificate still often carries fewer signers than the votes that exist (the lock still meets Q3) |
|
||||
| S1 | VRF output = SHA-256 of the voter's BLS signature over `"igneum-sortition-v1/" \|\| chain_id \|\| 0 \|\| index \|\| hash` under the sortition tag (unique per key and message, so the signature is the proof); eligible when `output x total_weight < 8 x weight x 2^64`, drawn by weight (W6, ledger F17, fin-fixes 4 October 2026): the expected number of aggregators is 8 by weight whatever the key count, a key with no weight never draws, a key holding 1/8 of total weight or more always does (so with 8 or fewer equal voters everyone is eligible). Unit test `sortition_is_by_weight_not_key_count`: 200 dust keys draw nothing, 6 real keys draw `sum min(1, 8 w / T)`, 16 equal keys draw 8.00, a key split into 10 or 200 parts draws what it drew whole | Was `output x voters < 8 x 2^64` (per key) until 4 October 2026; measured on the attack harness (`docs/bench-log.md`, "finality v2 attack harness" S2, then "finality fixes F17 and F1"). A key above 1/8 of total weight that splits itself gains seats (its single ticket was capped at 1); seats carry no reward and no power, since anyone MAY aggregate and Q3 is tested by weight |
|
||||
| S2 | Not implemented (sub-user sortition above 8,192 voters) | |
|
||||
|
|
@ -197,24 +203,13 @@ Status of this section: Designed, 3 October 2026. Every bound below is derived f
|
|||
|
||||
**S.** As long as the adversary holds less than `X` of total weight, the honest nodes never hold two certificates at one index naming different blocks, and never hold a certificate whose checkpoint block is off the chain of a lower certified checkpoint. S does not depend on synchrony: it holds before and after GST.
|
||||
|
||||
The arithmetic. A certificate verified at `C_i` in a view `v` has signer weight at least `q_v T` where `q_v = max(2/3 x A_v / T, 17/30)` (Q3). Honest keys sign one block per index, so for two certificates at index i on different blocks, with signer weights `s_1` and `s_2`, the weight that signed both is at least `s_1 + s_2 - T`, and that weight is adversary weight (equivocators). So a conflicting pair needs `a >= 2 q_min - 1` where `q_min` is the smaller binding fraction of the two views:
|
||||
The arithmetic. A certificate verified at `C_i` in a view `v` has signer weight at least `q_v T` where `q_v = max(2/3 x A_v / T, 2/3) = 2/3` (Q3 with the floor at two thirds: since `A_v <= T` the floor always binds, so the binding fraction is two thirds in every view, whatever its presence window holds). Honest keys sign one block per index, so for two certificates at index i on different blocks, with signer weights `s_1` and `s_2`, the weight that signed both is at least `s_1 + s_2 - T >= 4T/3 - T = T/3`, and that weight is adversary weight (equivocators). So a conflicting pair needs `a >= 1/3`, and `X = 1/3`.
|
||||
|
||||
| Active weight in the weaker view, `A_v / T` | Binding fraction `q_v` | Equivocating weight a pair needs, `X = 2 q_v - 1` |
|
||||
|---|---|---|
|
||||
| 1.00 (every honest voter present) | 2/3 | 1/3 = 33.3% |
|
||||
| 0.95 | 0.633 | 26.7% |
|
||||
| 0.90 | 0.600 | 20.0% |
|
||||
| 0.85 or less (the floor binds) | 17/30 = 0.567 | 4/30 = 13.3% |
|
||||
What the floor removed. Under the 0.85 floor of 3 October 2026 the binding fraction fell with the view's active weight, `q_v = max(2/3 x A_v / T, 17/30)`, so a view that had lost honest votes to a partition, an eclipse (3.3.2) or silence needed less than two thirds of total to lock, and `X = 2 q_v - 1` fell from 1/3 in a connected network to 4/30 = 13.3% once a partition had outlasted the presence decay (41 minutes of median time under the block reading of Q2, 18 under the cert reading the simulator runs; measured in H: a 14% equivocator conflicted from minute 48, a 20% one from minute 12). With the floor at two thirds the active weight of a view no longer enters the lock threshold, so the bound has no time term: `X = 1/3` of total weight against a connected network, against a partition of any length over which the weight tables agree, against an eclipse, and with the adversary reaching every side. Measured in H at the 2/3 floor (five seeds, 150 and 360 min, each side retargeting at once): 0 conflicting locks and no lock on either side of a 50/50 split with equivocators of 10%, 13%, 14%, 20%, 30% and 33% (each side holds 55% to 66.5% of total, under the floor); at 34% each side holds 67% and both lock from minute 0. The one-third line is 3.1's headline: ten days of 100% hashrate, twenty days of 51%, in public.
|
||||
|
||||
An honest view has `A_v < T` only when honest votes are missing from its presence window: a partition, an eclipse (3.3.2), or silence. Honest weight `h_out` unreachable from the view for `tau <= P` has participation `1 - tau / P` there, so `A_v / T = 1 - h_out tau / P` (less the uptime shortfall, about 2% in the model). For a partition into two honest parts `h_1` and `h_2` with the adversary reaching both, side j can lock alone once `h_j + a >= s_min(tau)` where `s_min(tau) = max(17/30, 2 (1 - tau/P) / (3 - 2 tau/P))` under the block reading of Q2; the two expressions meet at `tau = 9P/26 = 41.5 min`. Under the cert reading the simulator ran (every key decays during a stall) it is `s_min = max(17/30, 2/3 (1 - tau/P))`, meeting at `tau = 0.15 P = 18 min`, and the time for a side holding `s` to lock alone is `P (1 - 1.5 s)`: 12 min at 60%, 18 min at 56.7%, 0 at 2/3 and above. Both sides over `s_min` is `a >= 2 s_min(tau) - 1`.
|
||||
The trade the floor sets was linear, and it was decided. With the floor at `f x 2/3` the partition bound is `4f/3 - 1` and the liveness bound of 3.11.3 is `1 - 2f/3` of weight silent: 13.3% and 43.3% at f = 0.85, 33.3% and 33.3% at f = 1. the project lead chose f = 1 on 4 October 2026 (O-3.15): one third on both sides, a pause whenever less than two thirds of the weight is signing, no scenario in which a faction under a third can split finality.
|
||||
|
||||
So, in words, with the floor at 0.85:
|
||||
|
||||
- `X = 1/3` of total weight while every honest voter's vote reaches every honest node within 41 minutes of median time (18 under the simulated reading). This is 3.1's headline: ten days of 100% hashrate, twenty days of 51%, in public.
|
||||
- `X = 4/30 = 13.3%` of total weight against a partition of the honest network into two parts, each holding at least `17/30 - a` of total weight, unreachable from each other for 41 minutes or longer (18 under the simulated reading), with the adversary reaching both. The eclipse of 3.3.2 is the case where one part is the victim set: F2's 34% attacker with a 20% pool sits at 54% and fails; the same attacker with a 23% pool would not. 4/30 of weight is four days of 100% hashrate or eight days of 51%.
|
||||
- Between those, `X(tau) = 2 s_min(tau) - 1` falls from 1/3 to 4/30 as the partition lengthens.
|
||||
|
||||
The floor sets this trade linearly: with the floor at `f x 2/3` the partition bound is `4f/3 - 1` and the liveness bound of 3.11.3 is `1 - 2f/3` of weight silent. At 0.85 that is 13.3% and 43.3%; at 1 (the total denominator) both are 1/3; every point of liveness past one third costs two points of partition safety (O-3.15). The choice of 0.85 is the project's; this section only states what it buys and what it costs.
|
||||
What remains is the weight table itself (3.7 item 9). The bound above is about the total weight `T` as each view computes it from its own past. In a partition each side's window fills with its own blocks only, so a side with pre-split share `s` holds `s + (1 - s) t / 30` of its own table on day `t` and reaches two thirds of it on day `30 (2/3 - s) / (1 - s)`: 10 days at 50/50, 5 days for the 60 side of 60/40 (measured within the uptime shortfall in `sim/results_v2.md` L4; 4 days and 0 days under the old floor). From that day the side locks alone with `a = 0`, and two such sides hold conflicting certificates at the heal. That is the twenty-day event of 3.1 seen from inside a partition, with the clock started at the split: the floor at two thirds moved it from day 4 to day 10 and cannot remove it, because a view cannot count blocks it has never seen.
|
||||
|
||||
The second clause of S (chain of a lower certified checkpoint) follows from the first with F1 and C3. Once an honest node holds a certificate for `C_i` it never selects or signs a chain that misses `C_i`, and after GST every honest node holds every certificate within one block interval plus `Delta` (C3 carriage and gossip). A certificate at `j > i` off `C_i`'s chain therefore needs `q T` of weight that lacks `C_i`'s certificate, which before GST is a side of a partition, and the first clause already bounds any lock that side forms; after GST only the adversary lacks it, and `a < q`. The residual is honest votes for `C_i` in flight across a partition boundary in the `Delta` before the split, which strengthen `C_i`'s certificate and nothing else.
|
||||
|
||||
|
|
@ -222,22 +217,20 @@ At and above `X`. Two valid certificates can exist at one index, each held by ho
|
|||
|
||||
### 3.11.3 Liveness
|
||||
|
||||
**L.** After GST, if honest voters holding at least `Y = 17/30` of total weight are mutually connected within `Delta` and signing, a new checkpoint certifies within `T` of the moment that condition holds, whatever the adversary does within the model, where
|
||||
**L.** After GST, if honest voters holding at least `Y = 2/3` of total weight are mutually connected within `Delta` and signing, a new checkpoint certifies within `T` of the moment that condition holds, whatever the adversary does within the model, where
|
||||
|
||||
`T = D(Y) + I + d + G + Delta`, with `D(Y) = P (1 - Y / (2 (1 - Y)))` for `17/30 <= Y < 2/3` and `D(Y) = 0` for `Y >= 2/3`.
|
||||
`T = I + d + G + Delta` = 107 s at `I = 30 s`, `d = 60 s`, `G = 15 s`, `Delta = 2 s`.
|
||||
|
||||
The arithmetic. The floor needs `Y >= 17/30` of total, which no behaviour of the rest can raise or lower (silence leaves `T` unchanged; equivocation lowers it). The active test needs `Y >= 2/3 x A / T`. Keys outside the connected honest set either vote for the honest checkpoint, in which case their weight is in the certificate and the test passes trivially, or do not, in which case 3.11.1's reading of Q2 gives them participation `1 - t/P` after `t` of silence, so `A / T = Y + (1 - Y)(1 - t/P)` and the test passes at `t >= D(Y)`. `I + d` is the wait for the next checkpoint to be determined (one interval plus the determination depth), `G + Delta` the vote round trip and the grace of Q4. Under the cert reading of the simulation `D(Y) = P (1 - 1.5 Y)`, which is shorter; the block reading is the specified one (O-3.18 measures it).
|
||||
The arithmetic. The floor needs `Y >= 2/3` of total, which no behaviour of the rest can raise or lower (silence leaves `T` unchanged; equivocation lowers it). The active test needs `Y >= 2/3 x A / T`, which `Y >= 2/3` satisfies at once because `A <= T`; the presence decay of Q2 no longer enters the bound (under the 0.85 floor it added a term `D(Y)` of up to 41 minutes for `17/30 <= Y < 2/3`; O-3.18 is narrowed accordingly). `I + d` is the wait for the next checkpoint to be determined (one interval plus the determination depth), `G + Delta` the vote round trip and the grace of Q4. Below `Y = 2/3` there is no bound: finality pauses.
|
||||
|
||||
| Honest connected weight `Y` | `D(Y)`, block reading | `D(Y)`, cert reading (simulated) | `T` at `I = 30 s`, `d = 60 s`, `G = 15 s`, `Delta = 2 s` |
|
||||
|---|---|---|---|
|
||||
| 2/3 or more | 0 | 0 | 107 s |
|
||||
| 60% | 30 min | 12 min | 32 min |
|
||||
| 17/30 = 56.7% | 41.5 min | 18 min | 43 min |
|
||||
| under 17/30 | finality pauses | finality pauses | no bound |
|
||||
| Honest connected and signing weight `Y` | `T` |
|
||||
|---|---|
|
||||
| 2/3 or more | 107 s (simulated lock latency after the checkpoint block: median 2.5 s, p99 4.6 s, A; the test network: median 1.0 s) |
|
||||
| under 2/3 | finality pauses: no certificate until `Y` reaches 2/3 again, by silent keys returning or absent keys' blocks ageing out of the window |
|
||||
|
||||
Why the adversary cannot block L within the model: withholding votes changes nothing above; equivocating strips its weight and lowers `T`; dropping votes from its own blocks delays a vote's entry into the DAG by one honest block, since Q2 needs one block in `C_i`'s past to carry it and honest producers carry every vote they receive; aggregating dishonestly costs nothing because anyone MAY aggregate (S1) and every honest node aggregates the votes it holds; buying keys moves weight between holders and leaves `Y`'s arithmetic as it is.
|
||||
|
||||
**When finality pauses.** If the honest voters that are connected and signing hold less than 17/30 of total weight, which with the model's 97.8% resting participation happens once about 42% of weight is silent, no certificate can form until silent keys return or their blocks age out of the window (up to 30 days; a key that keeps mining never ages out). The chain does not stop: blocks, GHOSTDAG ordering (F2 among the tips through the last certified checkpoints) and execution continue on proof of work, every certified checkpoint stays binding, and the node reports `finality_active` false with the reason `paused` (3.9: the exchange guidance for that state is the finality depth in median time). The honest name for this state is "finality temporarily unavailable", and it is the state the test network showed for 12 checkpoints with one voter at 39.6% of total weight (3.11.7). The litepaper sentence that says a silent minority cannot freeze finality is false under the floor and is R3.18's fix.
|
||||
**When finality pauses.** Finality pauses whenever less than two thirds of the weight is connected and signing. In the model, whose honest keys are in outage 2.2% of the time, that is reached once about 32% of weight is silent (L1: 30% silent locks every checkpoint, 32% locks 88% of them, 33% locks 11% with a first gap of 49 minutes, 34% locks none for as long as it stays silent), and a key that keeps mining never ages out, so a 34% silent set holds the pause for as long as it stays silent (J and L1: 0 conflicts, the first lock 0 minutes after it returns). A set that stops mining ages out: with a share `x` gone and the survivors inheriting the block supply, the live share of total is `1 - x (30 - t) / 30` on day `t` and reaches two thirds on day `30 (1 - 1/(3x))`: 1.4 days at 35%, 10 days at 50% (L2 and D's total column). The chain does not stop: blocks, GHOSTDAG ordering (F2 among the tips through the last certified checkpoints) and execution continue on proof of work, every certified checkpoint stays binding, and the node reports `finality_active` false with the reason `paused` (3.9: the exchange guidance for that state is the finality depth in median time). The honest name for this state is "finality temporarily unavailable", and it is the state the test network showed for 12 checkpoints with one voter at 39.6% of total weight (3.11.7) and the state the floor test network showed on both sides of a 3/3 split (bench-log, "finality floor 2/3"). The litepaper says so in its Finality section and in "What Igneum does not claim" (R3.18).
|
||||
|
||||
### 3.11.4 Recovery and what "irreversible" means
|
||||
|
||||
|
|
@ -272,18 +265,18 @@ Each guarantee, the scenario that tests it, and the measured result. Bench-log c
|
|||
| Weight equals blocks and tracks hashrate (3.11.1) | results A, 60 days; test network checkpoint 4 | corr(hash, weight) 1.00000, weight:hash 0.82 to 1.12 (A); weights 34 + 31 + 30 + 24 blocks for four miners, total 119 (bench-log "Key reveal") |
|
||||
| S, connected network, `a < 1/3` | results A (no attacker, 60 days); test network, four miners, 53 minutes | 0 conflicting locks in 172,883 checkpoints (A); 93 checkpoints, every one locked on all three nodes, 0 conflicting certificates, identical hashes and weights at every RPC sample (bench-log "Checkpoints and locks") |
|
||||
| S under an honest partition, `a = 0` | results E, floor rows, 50/50, 60/40, 67/33, 80/20, 33/33/34 for 360 min with retarget; results I, 40/40/20 | 0 conflicts in every split (E); 40/40/20 honest three-way split: 0 conflicts and 0 locks on any side for 150 and 360 min (finality paused on all three), first lock 0 min after the heal, five seeds (I) |
|
||||
| S under a partition with an equivocator below 4/30 | results H, 50/50 honest, attacker 10% and 13%, 150 and 360 min, five seeds | 0 conflicting locks and no lock on either side at 10% and 13% for 150 and 360 min in every seed (H); 13% is the knife edge, each side holding 56.5% against the 56.7% floor |
|
||||
| S fails at and above 4/30 under a partition (the bound) | results H, attacker 14%, 20%, 34%; results I, 40/40 plus a 20% equivocator | 14% (each side 57.0%): conflicts in 2 of 5 seeds at 150 min and 4 of 5 at 360 min, first at 48 to 284 min, the model's 2% uptime shortfall deciding; 20% (60.0%): 256 to 276 conflicts in 150 min and 658 to 692 in 360, first at 12 to 16 min against 12 predicted; 34% (67.0%): first conflict at 0 to 1 min (H); 40/40 plus a 20% equivocator reaching both: 256 to 276 conflicts in 150 min, first at 12 to 16 min; the same 20% against a 40/40/20 honest split (sides 52/52/36 of total) gives 0 (I) |
|
||||
| S under an eclipse | results F2, 34% attacker plus a 20% pool (54% side), 1, 2, 4 h | 0 conflicting locks at every length (F2) |
|
||||
| L at `Y >= 2/3`: `T = 107 s` | results A lock latency; test network steady state | median 2.5 s, p99 4.6 s after the checkpoint block at `Delta = 2 s` (A); determination to lock median 0.80 s, p90 1.08 s, max 1.55 s (bench-log "Checkpoints and locks") |
|
||||
| L at `17/30 <= Y < 2/3` | results C, 40% silent; results J, 34% and 40% silent for 1, 6, 24 h; test network phase A, one of three miners stopped | 13 min to the first lock, 138 intermittent stalls in 3 days (C, cert reading); 34% silent: first lock 0 to 2 min, 98 to 100% of checkpoints locked, longest gap 1 to 15 min over 24 h; 40% silent: first lock 11 to 13 min, 78 to 99% locked, longest gap 11 to 47 min (J, cert reading); locks continued with 33% silent (bench-log "Partition test", phase A) |
|
||||
| Pause below the floor, chain continues | results C, 45% silent; results J, 45% silent for 1, 6, 24 h; test network phase B, one voter alone | never locks while silent (C); 45% silent: no lock for the whole 1, 6 and 24 h, longest gap 60, 360 and 1,440 min, 0 conflicts (J); 0 locks in 12 checkpoints with 39.6% of total and 72.3% of active, the floor alone holding, `finality_active` reporting the pause (bench-log "Partition test", phase B) |
|
||||
| S under a partition with an equivocator below 1/3 | results H at the 2/3 floor, 50/50 honest, attacker 10%, 13%, 14%, 20%, 30% and 33%, 150 and 360 min, five seeds; results I, 40/40 plus a 20% equivocator reaching both (sides 60/60) | 0 conflicting locks and no lock on either side at every attacker share up to 33% (each side 55.0% to 66.5% of total) for 150 and 360 min in every seed (H); 0 conflicts and no lock in the 60/60 case that gave 256 to 276 conflicts under the 0.85 floor (I); the 3/3 split of the three-node devnet: no lock on either side for the whole 150-s split (bench-log "finality floor 2/3", 6A) |
|
||||
| S fails at and above 1/3 under a partition (the bound) | results H, attacker 34%; results I, 40/40 plus a 34% equivocator reaching both | 34% (each side 67.0%): 2 to 54 conflicts in 150 to 360 min, first at minute 2 to 77 against 0 predicted, the model's 2.2% outage shortfall holding the sides at the knife edge (H, I) |
|
||||
| S under an eclipse | results F2 and L3, 34% attacker plus a 20% pool (54% side), 1, 2, 4 h, five seeds | 0 conflicting locks and 0 locks on the eclipsed side at every length in every seed (L3) |
|
||||
| S under a long honest partition, each side counting only what it has seen (3.7 item 9) | results L4, 50/50, 60/40 and 55/45 for 12 days, three seeds, floor 2/3 against 0.85; devnet 6A on a young window | 50/50: both sides lock alone from day 10.1 to 10.3 (predicted 10.0; 4.1 under the old floor); 60/40: the 60 side from day 5.1 (predicted 5.0; at once under the old floor), the 40 side never in 12 days (predicted 13.3); 55/45: day 7.9 and 12.0 (predicted 7.8 and 11.8); every pre-heal lock kept at the heal (L4); the old floor fell at 84 s of a 1,439-DAA devnet window (bench-log "finality v2 attack harness", S6A); at the 2/3 floor a 50/50 split on a full 1,800-DAA window at 3 blocks/s held for 150 s and both sides locked alone at 205 and 215 s (predicted 200), after which 26 conflicting certificates and 23 disagreeing locked indices stood across three healed nodes with no equivocation (bench-log "finality floor 2/3", 6A long heal) |
|
||||
| L at `Y >= 2/3`: `T = 107 s` | results A lock latency; test network steady state; devnet 6B, the 4 side of a 4/2 split at exactly two thirds | median 2.5 s, p99 4.6 s after the checkpoint block at `Delta = 2 s` (A); determination to lock median 0.80 s, p90 1.08 s, max 1.55 s (bench-log "Checkpoints and locks"); the 4 side locks at exactly 2/3 of total and of active, the comparison inclusive (bench-log "finality floor 2/3", 6B) |
|
||||
| Pause below two thirds, chain continues | results L1, 25% to 45% silent for 1 and 6 h; results J, 34%, 40%, 45% for 1, 6, 24 h; test network phase B, one voter alone; devnet 6A, both sides of a 3/3 split | 25% and 30% silent lock every checkpoint (one seed at 30% stalls 40 of 720 in 6 h); 32% locks 54 to 100% of checkpoints (1 h) and 69 to 95% (6 h); 33% locks 0 to 11%, the first lock after 24 to 266 min when there is one; 34% and above lock nothing for the whole 1, 6 and 24 h, longest gap 60, 360 and 1,440 min, 0 conflicts (L1, J); 0 locks in 12 checkpoints with 39.6% of total and 72.3% of active, `finality_active` reporting the pause (bench-log "Partition test", phase B); no lock on either side of the 3/3 split, each holding one half (bench-log "finality floor 2/3", 6A) |
|
||||
| Resume after the pause within `T` | results J, after the silent set resumes; test network phase C | first lock 0 min after the silent set resumes at every weight and length, 0 stalls in the 3 h after (J); checkpoints 73 to 85 locked within 30 s of the restart, 86 to 92 at the steady cadence (bench-log phase C) |
|
||||
| Deterministic heal, no lock reversed | results H and I, every pre-heal certificate present after the heal; results E post-heal stalls; test network heal | every pre-heal certificate present after the heal in all 60 runs of H and 40 of I, 0 post-heal stalls (H, I); 0 post-heal stalls in every E run; no conflicting certificate and no stall on any node through the heal (bench-log phase C) |
|
||||
| Equivocation strips the key, locks continue | test network, miner m4 with `--equivocate` from index 43; results E and F, strip at the heal | stripped on every node at index 43, voter list 3, locks at 3 of 3 votes from index 44 (bench-log "Equivocation"); post-heal stalls 0 with the attacker stripped (E) |
|
||||
| Weight ages out: churn | results D, 35% and 50% stop mining and signing | first lock 2 min at 35%, 4.1 days at 50% (D, floor 0.85) |
|
||||
| Acquired keys decay as the window moves (3.11.5) | results K, keys worth 20% and 40% bought, 30% hashrate, signing and silent, 30 days, five seeds | share follows b (1 - t/30) + 0.3 t/30 within 0.6 points at every sampled day in every seed; keys worth 20% rise to 30% on day 30 and never reach 1/3; keys worth 40% hold the veto from day 1 to day 19 or 20 (formula 20) and end at 30%; withholding its votes, the 40% buyer stalls 305 to 1,085 of 86,400 checkpoints in 30 days (79 to 186 on day 1, 0 to 50 on day 30) and the 20% buyer 0 to 318; 0 conflicts (K) |
|
||||
| Signing stops while mining continues, 1, 6, 24 h | results J | stalls while silent 0 to 31 (34%), 23 to 123 (40%), every checkpoint (45%); first lock after resume 0 min; 0 conflicts (J) |
|
||||
| Weight ages out: churn | results L2 and D, 35% and 50% stop mining and signing, three seeds | first lock 1.7 days at 35% (analytic 1.4) with 1,322 to 1,518 intermittent stalls in the 3 days after; 10.1 to 10.3 days at 50% (analytic 10.0); 0 conflicts (L2, floor 2/3; D's total column agrees at 41 h and 10.1 days) |
|
||||
| Acquired keys decay as the window moves (3.11.5) | results K, keys worth 20% and 40% bought, 30% hashrate, signing and silent, 30 days, five seeds | share follows b (1 - t/30) + 0.3 t/30 within 0.6 points at every sampled day in every seed; keys worth 20% rise to 30% on day 30 and never reach 1/3; keys worth 40% hold the veto from day 1 to day 19 or 20 (formula 20) and end at 30%; withholding its votes, the 40% buyer stalls 63,307 to 68,716 of 86,400 checkpoints in 30 days (the pause lasts until it has decayed below one third) and the 20% buyer 304 to 1,045; 0 conflicts (K, floor 2/3) |
|
||||
| Signing stops while mining continues, 1, 6, 24 h | results J and L1 | 34% and above: every checkpoint stalled for the whole silence; 33%: 665 to 727 of 720 in 6 h; 32%: 35 to 221; 30%: 0 to 40; first lock after resume 0 min at every weight; 0 conflicts (J, L1) |
|
||||
| Seeds during a pause (3.11.6) | devnet epoch boundary through a forced pause | not yet run (O-4.3 implementation) |
|
||||
| Two certificates at one index: no lock withdrawn (3.11.4) | devnet with a forced double certificate | not yet run (O-3.17) |
|
||||
| `T` under the block reading (3.11.3) | O-3.3 re-run | not yet run (O-3.18) |
|
||||
|
|
|
|||
|
|
@ -142,11 +142,13 @@ Section 3.11 states the finality guarantees with their assumptions and derives t
|
|||
|
||||
| Id | Item | What closes it | Gate |
|
||||
|---|---|---|---|
|
||||
| O-3.15 | The floor sets a linear trade (3.11 item 2): with the floor at f x 2/3 of total, two conflicting certificates need equivocators holding 4f/3 - 1 of total weight across a partition that outlasts the presence decay, and liveness survives up to 1 - 2f/3 of weight silent. At f = 0.85 (Q3): 13.3% and 43.3%. At 0.90: 20% and 40%. At 0.95: 26.7% and 36.7%. At 1 (the total denominator): 33.3% and 33.3%. Every point of liveness past one third costs two points of partition safety | Decision on f, owner the project lead with the cryptographer; the litepaper then states both numbers. `sim/results_v2.md` H gives the measured conflict times at 0.85 for 10%, 13%, 14%, 20% and 34% attackers; re-run H at the chosen f | 3 |
|
||||
| O-3.16 | 3.3.1 ("the safety bound stays at 1/3 of weight for every event tested") and 3.7 item 1 ("safety holds with under one third") state the connected-network bound only. 3.11 item 2 gives 1/3 while every honest voter's votes reach every honest node within 41 minutes of median time (18 minutes under the simulated cert reading), falling to 4/30 beyond that. The runs behind 3.3.1 probed 0% and 34% attackers and a 54% eclipsed side, never the gap between | Rewrite 3.7 item 1 and the last paragraph of 3.3.1 to cite 3.11 item 2; same sentence in the litepaper (with R3.18) | 3 (text) |
|
||||
| O-3.15 (decided) | **Decided 4 October 2026 by the project lead: f = 1, the floor is 2/3 of total weight (Q3).** The trade it settled: with the floor at f x 2/3 of total, two conflicting certificates need equivocators holding 4f/3 - 1 of total weight across a partition that outlasts the presence decay, and liveness survives up to 1 - 2f/3 of weight silent; at f = 0.85 that was 13.3% and 43.3%, at f = 1 both are one third. Since active weight never exceeds total, the active test of Q3 is implied and a lock is two thirds of all 30-day weight signing; finality pauses whenever less than two thirds is connected and signing, the chain continues on proof of work meanwhile, and the node reports it. Measured after the decision: `sim/results_v2.md`, "Floor 2/3" (H: 0 conflicts through a 33% equivocator, conflicts at minute 0 from 34%; L1: the pause begins between 32% and 34% silent in the model, 33% silent locks 11% of checkpoints; L2: 35% churn stalls about 1.4 days and 50% churn 10 days, the total-denominator figures of D; L4: a side of a long honest partition holds two thirds of its own window from day 30 (2/3 - s) / (1 - s), 10 days at 50/50 against 4 under the old floor); the three-node, six-voter devnet of `docs/bench-log.md`, "finality floor 2/3" (scenario 6A: no lock on either side of a 3/3 split; 6B: the 4 side locks at exactly 2/3, inclusive) | Closed. The litepaper states both numbers (two thirds to lock, pause below two thirds signing) | 3 |
|
||||
| O-3.16 (text closed) | 3.3.1 ("the safety bound stays at 1/3 of weight for every event tested") and 3.7 item 1 ("safety holds with under one third") stated the connected-network bound only; under the 0.85 floor 3.11 item 2 gave 1/3 only while every honest voter's votes reached every honest node within 41 minutes, falling to 4/30 beyond that | Closed 4 October 2026 by O-3.15: at f = 1 the bound is one third in every view whatever the presence window says (two certificates need 4/3 of weight in signatures), and 3.3.1, 3.7, 3.11.2 and the litepaper say so. What remains is the window bound of 3.3.1 (a side that mines alone for a third of the window holds two thirds of its own table), stated there and in 3.7 item 9, measured in L4 and on the devnet (6A) | 3 (text) |
|
||||
| O-3.17 | Two valid certificates at one index: 3.5's proposal (strike the equivocators, re-evaluate, treat the index as uncertified if neither or both lock) makes a verified lock revocable (R3.17, ledger F16). 3.11 item 4 replaces it: a verified certificate is never withdrawn, the node reports `finality_conflict`, clears `finality_active`, keeps following the certificate it verified first, and the split is resolved by operators through F5, as Kaspa resolves a finality conflict by notification and not by rule (`vendor/rusty-kaspa/consensus/notify/src/notification.rs`, `FinalityConflict`) | Replace the paragraph in 3.5 with 3.11 item 4; implement `finality_conflict` in the node; devnet test that forces a double certificate and checks that no node ever reports a lock it later withdraws. Closes the rule half of O-3.6; the devnet half stays | 3 |
|
||||
| O-3.18 | The liveness bound T of 3.11 item 3 is derived under the block reading of Q2 (absent keys decay, present keys hold 1), which recovers more slowly than the cert reading the simulator runs (predicted 35 minutes against the measured 13 at 40% silent). The measured J, C and D figures are therefore lower bounds on T | The O-3.3 re-run under the block reading reports the recovery time at 40% silent and 35% churn and confirms or corrects T | 3 |
|
||||
| O-3.18 (narrowed) | The liveness bound T of 3.11 item 3 was derived under the block reading of Q2 (absent keys decay, present keys hold 1), which recovers more slowly than the cert reading the simulator runs. With the floor at 2/3 (O-3.15, 4 October 2026) the presence decay no longer enters T: a lock needs two thirds of total whatever participation says, so T = I + d + G + Delta (107 s) whenever two thirds is connected and signing, and below that finality pauses for as long as the shortfall lasts (silence) or until the missing weight ages out (churn, 30 (1 - 1/(3x)) days for a set holding x). What is left is the exit from a pause under the block reading: the first lock after the silent set returns is 0 minutes under the cert reading (`sim/results_v2.md` J and L1) | The O-3.3 re-run under the block reading reports the first lock after a 40% silent set returns and confirms or corrects the 0-minute figure | 3 |
|
||||
| O-4.3 (decision) | Decided 3 October 2026 by 3.11 item 6: the seed checkpoint is the selected-chain block at the checkpoint blue score the lead rule names, certified or not, so a finality pause never stops the hourly program. Remaining: implement `seed_source` from the checkpoint block (the devnet keys the program on the header's `daa_score`, R3.26) and run an epoch boundary through a forced pause on the devnet | Implementation and the devnet pause test | 3 |
|
||||
| O-3.19 | Q2 credits participation for "a valid vote by k at index j" and the node credits any vote at the index whatever block it names (3.10). A key can then vote for a block of its own at every index, keep participation 1 and its weight in the active denominator, and never add to a certificate; honest voters would need 2/3 of total for every lock and the liveness bound of 3.11 item 3 would fall back to one third. 3.11.1 reads Q2 as crediting only a vote that names C_j on the selected chain of C_i, so a key either helps the certificate or decays out | Write the reading into Q2; implement it in the node's participation count; re-run C with an adversary voting for private blocks | 3 |
|
||||
| O-3.19 (narrowed) | Q2 credits participation for "a valid vote by k at index j" and the node credits any vote at the index whatever block it names (3.10). A key can then vote for a block of its own at every index, keep participation 1 and its weight in the active denominator, and never add to a certificate. Under the 0.85 floor that pushed the honest lock threshold from 17/30 of total up to 2/3 of total; under the 2/3 floor (O-3.15, 4 October 2026) honest voters need 2/3 of total for every lock anyway, so the attack changes no lock and no bound. What it still distorts is the report: a key voting for private blocks reads as present. 3.11.1 reads Q2 as crediting only a vote that names C_j on the selected chain of C_i | Write the reading into Q2 and the node's participation count as a reporting rule; no re-run of C is needed for the lock threshold | 3 (reporting) |
|
||||
|
||||
Count after this addition: section 3 has 19 items (O-3.15 to O-3.19 added; O-3.6 narrowed to the devnet test), section 4 keeps 9 with O-4.3 decided and awaiting implementation; total 66.
|
||||
|
||||
Update of 4 October 2026 (floor 2/3): O-3.15 decided and O-3.16 closed as text (both kept in the table with their resolution), O-3.18 and O-3.19 narrowed as stated in their rows. Section 3 keeps 17 open items.
|
||||
|
|
|
|||
|
|
@ -89,8 +89,9 @@ class P:
|
|||
self.presence = 240 # checkpoints in the participation window
|
||||
self.dust = 100 # blocks
|
||||
self.quorum = TWO_THIRDS
|
||||
self.floor = 0.0 # hybrid: active denominator never below floor x total weight (0 = off)
|
||||
self.floor = 0.0 # hybrid: active denominator never below floor x total weight (0 = off; 1.0 = the rule of 4 Oct 2026, a lock needs 2/3 of total)
|
||||
self.daa = "none" # "none": a partition side mines at its hashrate share; "full": each side retargets to 30 blocks/slot at once
|
||||
self.local = False # True: a partition side's weight table counts only the blocks it has seen (its own after the split), as a real node's window does
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def label(self):
|
||||
|
|
@ -101,6 +102,8 @@ class P:
|
|||
s += "+floor%.2f" % self.floor
|
||||
if self.daa != "none":
|
||||
s += "+daa"
|
||||
if self.local:
|
||||
s += "+local"
|
||||
return s
|
||||
|
||||
|
||||
|
|
@ -119,6 +122,7 @@ class View:
|
|||
self.stalls = [] # (idx, slot)
|
||||
self.key_mask = None # keys whose region is on this side
|
||||
self.mine_mask = None
|
||||
self.excl = np.zeros(n) # blocks mined off this side since the split, unseen by it (only used with P.local)
|
||||
|
||||
def clone_ring_from(self, other):
|
||||
self.ring[:] = other.ring
|
||||
|
|
@ -280,6 +284,10 @@ class Sim:
|
|||
self.buckets[hp] = 0.0
|
||||
self.buckets[hp] += blocks
|
||||
self.weight += blocks
|
||||
if p.local and len(self.views) > 1:
|
||||
# a side's window holds only the blocks it has seen: the other sides' post-split blocks are unseen
|
||||
for v in self.views:
|
||||
v.excl += blocks * ~v.mine_mask
|
||||
gidx = max(v.next_idx for v in self.views)
|
||||
newly = (blocks > 0) & (self.first_idx == -1)
|
||||
if newly.any():
|
||||
|
|
@ -315,22 +323,24 @@ class Sim:
|
|||
src = v.regions[0]
|
||||
jit1 = np.exp(self.rng.normal(0.0, p.jitter, self.R))
|
||||
jit2 = np.exp(self.rng.normal(0.0, p.jitter, (self.R, self.R)))
|
||||
elig = (self.weight >= p.dust) & ~self.stripped
|
||||
# the weight table this side computes: global, or (P.local) less the blocks it has not seen since the split
|
||||
wt = np.maximum(self.weight - v.excl, 0.0) if p.local else self.weight
|
||||
elig = (wt >= p.dust) & ~self.stripped
|
||||
voters = elig & self.online & self.signs & (v.key_mask | self.equiv)
|
||||
if p.denom == "active":
|
||||
denom = float((self.weight * self.participation(v, idx))[elig].sum())
|
||||
denom = float((wt * self.participation(v, idx))[elig].sum())
|
||||
else:
|
||||
denom = float(self.weight[elig].sum())
|
||||
total = float(self.weight[elig].sum())
|
||||
denom = float(wt[elig].sum())
|
||||
total = float(wt[elig].sum())
|
||||
if p.denom == "active" and p.floor > 0:
|
||||
denom = max(denom, p.floor * total)
|
||||
need = p.quorum * denom
|
||||
vi = np.flatnonzero(voters)
|
||||
signed_w = float(self.weight[vi].sum())
|
||||
signed_w = float(wt[vi].sum())
|
||||
ratio = signed_w / denom if denom > 0 else 0.0
|
||||
best = None
|
||||
if denom > 0 and vi.size > 0:
|
||||
w = self.weight[vi]
|
||||
w = wt[vi]
|
||||
reg = self.region[vi]
|
||||
hop1 = np.where(self.equiv[vi], p.intra, self.D[src, reg] * jit1[reg])
|
||||
issue = t0 + hop1 + self.extra_delay[vi]
|
||||
|
|
@ -656,7 +666,7 @@ def scenario_c(args):
|
|||
configs = [("active", "cert", args.hours_c, 0.0, 240), ("active", "seen", args.hours_c, 0.0, 240),
|
||||
("active", "frozen", args.hours_c, 0.0, 240), ("active", "cert", args.hours_c_total, 0.0, 2880),
|
||||
("active", "cert", args.hours_c_total, 0.8, 240), ("active", "cert", args.hours_c_total, 0.85, 240),
|
||||
("total", "cert", args.hours_c_total, 0.0, 240)]
|
||||
("active", "cert", args.hours_c_total, 1.0, 240), ("total", "cert", args.hours_c_total, 0.0, 240)]
|
||||
labels = []
|
||||
for denom, pmode, hours, floor, presence in configs:
|
||||
lab = P(denom=denom, pmode=pmode, floor=floor, presence=presence).label()
|
||||
|
|
@ -717,7 +727,7 @@ def scenario_d(args):
|
|||
rows = []
|
||||
dconfigs = [P(denom="active", delay=args.delay), P(denom="active", presence=2880, delay=args.delay),
|
||||
P(denom="active", floor=0.8, delay=args.delay), P(denom="active", floor=0.85, delay=args.delay),
|
||||
P(denom="total", delay=args.delay)]
|
||||
P(denom="active", floor=1.0, delay=args.delay), P(denom="total", delay=args.delay)]
|
||||
for frac in fracs:
|
||||
for p in dconfigs:
|
||||
days = args.days_d35 if frac < 0.4 else args.days_d50
|
||||
|
|
@ -826,7 +836,8 @@ def scenario_e(args):
|
|||
configs2 = configs + [P(denom="active", pmode="cert", daa="full", delay=args.delay),
|
||||
P(denom="active", pmode="seen", daa="full", delay=args.delay),
|
||||
P(denom="active", pmode="cert", floor=0.8, daa="full", delay=args.delay),
|
||||
P(denom="active", pmode="cert", floor=0.85, daa="full", delay=args.delay)]
|
||||
P(denom="active", pmode="cert", floor=0.85, daa="full", delay=args.delay),
|
||||
P(denom="active", pmode="cert", floor=1.0, daa="full", delay=args.delay)]
|
||||
labels2 = [p.label() for p in configs2]
|
||||
rows = []
|
||||
for name, fr in (("50/50", [0.5, 0.5]), ("60/40", [0.6, 0.4]), ("67/33", [0.67, 0.33]), ("80/20", [0.8, 0.2]),
|
||||
|
|
@ -840,7 +851,7 @@ def scenario_e(args):
|
|||
out.append("Supplementary, 0% attacker, 150 and 360 min. Cell = conflicting locks; minutes to each side's first lock. "
|
||||
"'+daa' = each side retargets to 1 block/s at once (worst case for the presence clock); "
|
||||
"'+floor0.80' = active denominator never below 80% of total weight (a lock needs at least 53.3% of total), "
|
||||
"'+floor0.85' needs 56.7%.")
|
||||
"'+floor0.85' needs 56.7%, '+floor1.00' needs 66.7% (the rule of 4 October 2026; arithmetically the total column).")
|
||||
out.append("")
|
||||
out.append(md_table(["honest split", "partition min"] + labels2, rows))
|
||||
out.append("")
|
||||
|
|
@ -932,7 +943,8 @@ def scenario_f(args):
|
|||
out.append("")
|
||||
rows = []
|
||||
configs2 = configs + [P(denom="active", pmode="cert", floor=0.8, delay=args.delay),
|
||||
P(denom="active", pmode="cert", floor=0.85, delay=args.delay)]
|
||||
P(denom="active", pmode="cert", floor=0.85, delay=args.delay),
|
||||
P(denom="active", pmode="cert", floor=1.0, delay=args.delay)]
|
||||
for dur in (1, 2, 4):
|
||||
for p in configs2:
|
||||
r = run_eclipse(args.seed, dur, True, p)
|
||||
|
|
@ -998,20 +1010,34 @@ def scenario_g(args):
|
|||
|
||||
|
||||
# ---------------------------------------------------------------- additions, 3 October 2026, for section 3.11 Guarantees
|
||||
# Scenarios H to K run the rule exactly as Q3 specifies it (active denominator, cert reading, floor 0.85, so a lock needs
|
||||
# 2/3 of active and 17/30 of total) over several seeds. Nothing above this line changed.
|
||||
# Scenarios H to K run the rule exactly as Q3 specifies it (active denominator, cert reading, the floor at FLOOR_F x 2/3 of
|
||||
# total) over several seeds. Floor factor FLOOR_F: 0.85 until 4 October 2026 (a lock needed 2/3 of active and 17/30 of
|
||||
# total); 1.0 since (decision of 4 October 2026, O-3.15: a lock needs 2/3 of total, which implies the active test).
|
||||
# `--floor 0.85` reproduces the 3 October tables. The floor-1.0 rule is arithmetically the "total" denominator of A to G.
|
||||
|
||||
NEW_SEEDS = (7, 11, 13, 17, 19)
|
||||
P_FLOOR = 17.0 / 30.0
|
||||
FLOOR_F = 1.0
|
||||
P_FLOOR = FLOOR_F * TWO_THIRDS
|
||||
|
||||
|
||||
def set_floor(f):
|
||||
"""Set the floor factor for rule_p and the predictions of H to L (called from main with --floor)."""
|
||||
global FLOOR_F, P_FLOOR
|
||||
FLOOR_F = float(f)
|
||||
P_FLOOR = FLOOR_F * TWO_THIRDS
|
||||
|
||||
|
||||
def rule_p(delay, **kw):
|
||||
"""Q3 as specified: active/cert with the 0.85 floor."""
|
||||
base = dict(denom="active", pmode="cert", floor=0.85, delay=delay)
|
||||
"""Q3 as specified: active/cert with the floor at FLOOR_F x 2/3 of total."""
|
||||
base = dict(denom="active", pmode="cert", floor=FLOOR_F, delay=delay)
|
||||
base.update(kw)
|
||||
return P(**base)
|
||||
|
||||
|
||||
def rule_name():
|
||||
return "active/cert + floor %.2f (a lock needs %s of total)" % (FLOOR_F, pct(P_FLOOR))
|
||||
|
||||
|
||||
def seeds_of(args):
|
||||
s = getattr(args, "seeds", "") or ""
|
||||
out = tuple(int(x) for x in s.split(",") if x.strip())
|
||||
|
|
@ -1096,18 +1122,18 @@ def scenario_h(args):
|
|||
seeds = seeds_of(args)
|
||||
q = getattr(args, "quick", False)
|
||||
durs = (60,) if q else (150, 360)
|
||||
atts = (0.0, 0.10, 0.13, 0.14, 0.20, 0.34)
|
||||
out = ["### H. Partition of a 50/50 honest network with an equivocating attacker, rule as specified (active/cert + floor 0.85), "
|
||||
"each side retargets at once (+daa, the median-time clock of Q1), seeds %s" % ",".join(str(s) for s in seeds), ""]
|
||||
atts = (0.0, 0.10, 0.13, 0.14, 0.20, 0.30, 0.33, 0.34)
|
||||
out = ["### H. Partition of a 50/50 honest network with an equivocating attacker, rule as specified (%s), "
|
||||
"each side retargets at once (+daa, the median-time clock of Q1), seeds %s" % (rule_name(), ",".join(str(s) for s in seeds)), ""]
|
||||
out.append("Attacker = one key holding the stated share of TOTAL weight, mining on the first side, voting on both. Each side holds "
|
||||
"(1 - a)/2 + a of total. Prediction (section 3.11 item 2): a side holding s of total locks alone once s >= 17/30 and its "
|
||||
"(1 - a)/2 + a of total. Prediction (section 3.11 item 2): a side holding s of total locks alone once s >= the floor (%s) and its "
|
||||
"view's active weight has decayed to 1.5 s, which under the cert reading is P x (1 - 1.5 s) slots after the split "
|
||||
"(P = 240, so 2 h x (1 - 1.5 s)); two sides over 17/30 need a >= 4/30 = 13.3%.")
|
||||
"(P = 240, so 2 h x (1 - 1.5 s), 0 at s >= 2/3); two sides over the floor need a >= %s." % (pct(P_FLOOR), pct(2 * P_FLOOR - 1)))
|
||||
out.append("")
|
||||
rows = []
|
||||
for a in atts:
|
||||
s = (1.0 - a) / 2.0 + a
|
||||
pred = "no (side holds %s < 56.7%%)" % pct(s) if s < P_FLOOR else "%.0f min" % (120.0 * max(0.0, 1.0 - 1.5 * s))
|
||||
pred = "no (side holds %s < %s)" % (pct(s), pct(P_FLOOR)) if s < P_FLOOR else "%.0f min" % (120.0 * max(0.0, 1.0 - 1.5 * s))
|
||||
for dur in durs:
|
||||
rs = [run_partition2(sd, [0.5, 0.5], a, dur, rule_p(args.delay, daa="full")) for sd in seeds]
|
||||
rows.append([pct(a, 0), pct(s), dur, pred, span(r["conflicts"] for r in rs), span_min(r["first_conflict_min"] for r in rs),
|
||||
|
|
@ -1123,11 +1149,12 @@ def scenario_i(args):
|
|||
seeds = seeds_of(args)
|
||||
q = getattr(args, "quick", False)
|
||||
durs = (60,) if q else (150, 360)
|
||||
out = ["### I. The 40/40/20 split, rule as specified (active/cert + floor 0.85), +daa, seeds %s" % ",".join(str(s) for s in seeds), ""]
|
||||
out = ["### I. The 40/40/20 split, rule as specified (%s), +daa, seeds %s" % (rule_name(), ",".join(str(s) for s in seeds)), ""]
|
||||
cases = [("honest 40/40/20, no attacker", [0.4, 0.4, 0.2], 0.0),
|
||||
("honest 40/40/20 plus a 10% equivocator (sides 36+10 / 36+10 / 18+10)", [0.4, 0.4, 0.2], 0.10),
|
||||
("honest 40/40/20 plus a 20% equivocator (sides 32+20 / 32+20 / 16+20)", [0.4, 0.4, 0.2], 0.20),
|
||||
("honest 40/40 plus a 20% equivocator reaching both (sides 40+20 / 40+20)", [0.5, 0.5], 0.20)]
|
||||
("honest 40/40 plus a 20% equivocator reaching both (sides 40+20 / 40+20)", [0.5, 0.5], 0.20),
|
||||
("honest 40/40 plus a 34% equivocator reaching both (sides 33+34 / 33+34)", [0.5, 0.5], 0.34)]
|
||||
rows = []
|
||||
for name, fr, a in cases:
|
||||
for dur in durs:
|
||||
|
|
@ -1284,8 +1311,107 @@ def scenario_k(args):
|
|||
return "\n".join(out)
|
||||
|
||||
|
||||
def run_churn(seed, frac, days, p):
|
||||
"""Scenario D as a function: a set holding `frac` of weight stops mining and signing at hour 3; survivors inherit the block supply."""
|
||||
sim, rng, _ = build_honest(p, seed)
|
||||
sim.warm_start()
|
||||
sim.init_views(warm=True)
|
||||
sim.run(3 * SLOTS_PER_HOUR)
|
||||
gone, got = pick_weight_subset(rng, sim.weight, frac)
|
||||
sim.signs[gone] = False
|
||||
sim.online[gone] = False
|
||||
sim.flaky[gone] = False
|
||||
sim.hash[gone] = 0.0
|
||||
t_event = sim.slot
|
||||
sim.run(int(days * SLOTS_PER_DAY))
|
||||
recs = sim.recs()
|
||||
fl = first_lock_after(recs, t_event)
|
||||
st = stalls_between(recs, t_event, sim.slot)
|
||||
later = stalls_between(recs, fl, sim.slot) if fl is not None else 0
|
||||
return dict(got=got, first_lock_days=None if fl is None else (fl - t_event) / float(SLOTS_PER_DAY), stalls=st, later=later,
|
||||
live_share=float(sim.share(np.flatnonzero(~gone))), conflicts=len(sim.conflicts))
|
||||
|
||||
|
||||
def scenario_l(args):
|
||||
"""The floor at 2/3 of total (4 October 2026): the cases the decision turns on, over seeds. Rule as rule_p (FLOOR_F)."""
|
||||
seeds = seeds_of(args)
|
||||
few = seeds[:3]
|
||||
q = getattr(args, "quick", False)
|
||||
out = ["### L. The floor at %s of total (floor factor %.2f): silent weight, churn, the eclipse, and long partitions with view-local "
|
||||
"weight; seeds %s (churn and long partitions: %s)" % (pct(P_FLOOR), FLOOR_F, ",".join(str(s) for s in seeds), ",".join(str(s) for s in few)), ""]
|
||||
# (a) silent weight, fine resolution around one third
|
||||
hours = (1,) if q else (1, 6)
|
||||
fracs = (0.25, 0.30, 0.32, 0.33, 0.34, 0.40, 0.45)
|
||||
out.append("L1. Signing stops while mining continues (as J), finer around one third. The floor needs the signing weight at or above %s of total; "
|
||||
"the model's resting participation is 0.978, so the online signing share is about 0.978 x (1 - silent)." % pct(P_FLOOR))
|
||||
out.append("")
|
||||
rows = []
|
||||
for frac in fracs:
|
||||
for h in hours:
|
||||
rs = [run_silent_resume(sd, frac, h, rule_p(args.delay)) for sd in seeds]
|
||||
rows.append([pct(frac, 0), pct(0.978 * (1.0 - frac)), h, span_min(r["first_lock"] for r in rs), span(r["stalls"] for r in rs),
|
||||
span(int(round(100 * r["locked_share"])) for r in rs) + "%",
|
||||
span((r["gap"] for r in rs), "%.0f"), span_min(r["resume"] for r in rs), span(r["post_stalls"] for r in rs),
|
||||
span(r["conflicts"] for r in rs)])
|
||||
out.append(md_table(["silent weight", "online signing share, about", "silent hours", "first lock after the stop, min", "stalled checkpoints while silent",
|
||||
"checkpoints locked while silent", "longest gap without a lock, min", "first lock after resume, min",
|
||||
"stalls in 3 h after resume", "conflicting locks"], rows))
|
||||
out.append("")
|
||||
# (b) churn
|
||||
d35 = 1 if q else 3
|
||||
d50 = 1 if q else 12
|
||||
out.append("L2. Churn (as D): a set stops mining and signing; survivors inherit the block supply (perfect retarget). Analytic first lock at day "
|
||||
"30 (1 - (1 - %s) / x) for a set holding x: %s." % (pct(P_FLOOR), ", ".join("%s: %s" % (pct(x, 0), "never" if 1 - (1 - P_FLOOR) / x <= 0 else "day %.1f" % (30 * (1 - (1 - P_FLOOR) / x))) for x in (0.35, 0.50))))
|
||||
out.append("")
|
||||
rows = []
|
||||
for frac, days in ((0.35, d35), (0.50, d50)):
|
||||
rs = [run_churn(sd, frac, days, rule_p(args.delay)) for sd in few]
|
||||
rows.append([pct(frac, 0), days, span_min(None if r["first_lock_days"] is None else r["first_lock_days"] * 1440.0 for r in rs) if days <= 1
|
||||
else ("never in %d days" % days if all(r["first_lock_days"] is None for r in rs) else span((r["first_lock_days"] for r in rs if r["first_lock_days"] is not None), "%.1f") + " days"),
|
||||
span(r["stalls"] for r in rs), span(r["later"] for r in rs), span((100 * r["live_share"] for r in rs), "%.1f") + "%", span(r["conflicts"] for r in rs)])
|
||||
out.append(md_table(["churn weight", "days run", "first lock after the event", "stalled checkpoints", "stalled after the first lock",
|
||||
"live share of total weight at the end", "conflicting locks"], rows))
|
||||
out.append("")
|
||||
# (c) the poisoned eclipse of F2
|
||||
out.append("L3. The poisoned eclipse (as F2): a 34% attacker feeds a 20% pool a private fork and signs both; the eclipsed side holds 54% of total.")
|
||||
out.append("")
|
||||
rows = []
|
||||
for dur in ((1,) if q else (1, 2, 4)):
|
||||
rs = [run_eclipse(sd, dur, True, rule_p(args.delay)) for sd in seeds]
|
||||
rows.append([dur, span(r["conflicts"] for r in rs), span_min(r["first_conflict_min"] for r in rs), span(r["ecl_locks"] for r in rs),
|
||||
span(r["honest_stalls"] for r in rs), span(r["post_stalls"] for r in rs), span(("%.3f" % r["min_part"] for r in rs), "%s")])
|
||||
out.append(md_table(["eclipse h", "conflicting locks", "first conflict, min", "locks on the eclipsed side", "honest-side stalls during",
|
||||
"stalls after heal", "pool participation, minimum"], rows))
|
||||
out.append("")
|
||||
# (d) long honest partitions with view-local weight tables: the window bound of attack scenario 6A
|
||||
days = 1 if q else 12
|
||||
out.append("L4. Long honest partitions with view-local weight ('+local'): after the split a side's window holds only the blocks it has seen, so its own "
|
||||
"share of its own table rises as s + (1 - s) T / 30 on day T (each side retargets, +daa). Prediction: a side with pre-split share s locks "
|
||||
"alone from day 30 (floor - s) / (1 - s), 0 if s is already at the floor; %d days, no attacker, seeds %s. The 3 October tables kept weights "
|
||||
"global (results_v2.md, 'Weights in a partition'), which hid this bound; attack scenario 6A found it on the devnet." % (days, ",".join(str(s) for s in few)))
|
||||
out.append("")
|
||||
rows = []
|
||||
for name, fr in (("50/50", [0.5, 0.5]), ("60/40", [0.6, 0.4]), ("55/45", [0.55, 0.45])):
|
||||
for f in sorted({FLOOR_F, 0.85}, reverse=True):
|
||||
pf = f * TWO_THIRDS
|
||||
preds = []
|
||||
for s in fr:
|
||||
preds.append("0" if s >= pf else ("%.1f" % (30.0 * (pf - s) / (1.0 - s)) if 30.0 * (pf - s) / (1.0 - s) <= days else "> %d" % days))
|
||||
p = P(denom="active", pmode="cert", floor=f, daa="full", local=True, delay=args.delay)
|
||||
rs = [run_partition2(sd, fr, 0.0, days * 1440, p, pre_min=60, post_min=180) for sd in few]
|
||||
fl = [[None if r["side_first_lock"][i] is None else r["side_first_lock"][i] / 1440.0 for r in rs] for i in range(len(fr))]
|
||||
rows.append([name, "%.2f (%s)" % (f, pct(pf)), days, " / ".join(preds),
|
||||
" / ".join(("never" if all(x is None for x in col) else span((x for x in col if x is not None), "%.1f") + ("" if all(x is not None for x in col) else " (never in %d of %d)" % (sum(x is None for x in col), len(col)))) for col in fl),
|
||||
span(r["conflicts"] for r in rs),
|
||||
("never" if all(r["first_conflict_min"] is None for r in rs) else span((r["first_conflict_min"] / 1440.0 for r in rs if r["first_conflict_min"] is not None), "%.1f") + " days"),
|
||||
"yes" if all(r["kept"] for r in rs) else "NO", span(r["post_stalls"] for r in rs)])
|
||||
out.append(md_table(["honest split", "floor factor (lock needs, of the side's own table)", "partition days", "predicted first lock per side, day",
|
||||
"first lock per side, day", "conflicting locks", "first conflict", "every pre-heal lock kept", "stalls in 3 h after heal"], rows))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
SCENARIOS = {"A": scenario_a, "B": scenario_b, "C": scenario_c, "D": scenario_d, "E": scenario_e, "F": scenario_f, "G": scenario_g,
|
||||
"H": scenario_h, "I": scenario_i, "J": scenario_j, "K": scenario_k}
|
||||
"H": scenario_h, "I": scenario_i, "J": scenario_j, "K": scenario_k, "L": scenario_l}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
|
@ -1295,8 +1421,11 @@ def main(argv=None):
|
|||
ap.add_argument("--delay", type=float, default=2.0, help="one-way inter-region delay in seconds for the main runs")
|
||||
ap.add_argument("--grace", type=float, default=15.0)
|
||||
ap.add_argument("--quick", action="store_true", help="shortened runs for development")
|
||||
ap.add_argument("--seeds", default="", help="comma-separated seeds for scenarios H to K (default 7,11,13,17,19)")
|
||||
ap.add_argument("--seeds", default="", help="comma-separated seeds for scenarios H to L (default 7,11,13,17,19)")
|
||||
ap.add_argument("--floor", type=float, default=FLOOR_F,
|
||||
help="floor factor f for the rule of H to L and the floor columns of C to F: a lock needs f x 2/3 of total (1.0 = the rule of 4 Oct 2026; 0.85 = the 3 Oct tables)")
|
||||
args = ap.parse_args(argv)
|
||||
set_floor(args.floor)
|
||||
q = args.quick
|
||||
args.days_a = 3 if q else 60
|
||||
args.days_delay = 1 if q else 3
|
||||
|
|
@ -1310,9 +1439,10 @@ def main(argv=None):
|
|||
print()
|
||||
p = P()
|
||||
print("seed %d, slot %.0f s, %d blocks per checkpoint, window %d h, presence %d checkpoints, dust %d, quorum 2/3, "
|
||||
"floor factor %.2f (a lock needs %s of total under the rule as specified), "
|
||||
"inter-region delay %.1f s (intra %.1f s, jitter sigma %.2f), grace %.0f s, uptime %.2f (mean outage %d slots), "
|
||||
"uptime %.3f for keys at or above %s of hashrate, honest keys %d Pareto %.1f, geography %s%s" % (
|
||||
args.seed, SLOT_S, BLOCKS_PER_CP, WINDOW_HOURS, p.presence, p.dust, args.delay, p.intra, p.jitter,
|
||||
args.seed, SLOT_S, BLOCKS_PER_CP, WINDOW_HOURS, p.presence, p.dust, FLOOR_F, pct(P_FLOOR), args.delay, p.intra, p.jitter,
|
||||
args.grace, p.uptime, p.outage, p.uptime_big, pct(p.big_share, 0), N_HONEST, PARETO_SHAPE, "/".join(pct(g, 0) for g in GEOGRAPHY),
|
||||
", QUICK" if q else ""))
|
||||
print()
|
||||
|
|
|
|||
|
|
@ -416,3 +416,195 @@ J measures the pause and the resume. 34% silent costs 0 to 31 stalled checkpoint
|
|||
K confirms section 3.11.5: a bought key is worth the blocks it holds and nothing more. The share of an attacker who buys keys worth b and mines at 30% of the network follows b (1 - t/30) + 0.3 t/30 within 0.6 points at every sampled day in every seed (the deviation is the sellers' fresh keys sitting under dust for their first days). Keys worth 20% climb to 30% on day 30 and never reach a third; keys worth 40% hold the veto from the day of purchase until day 19 or 20 (formula: 20) and are worth 30% on day 30, the same as fresh hashrate. Withholding its votes, the 40% buyer stalls 305 to 1,085 of 86,400 checkpoints over the 30 days, most of them on day 1 (79 to 186) while its bought weight is above 40% of the active denominator, and the 20% buyer 0 to 318; neither produced a conflicting lock. Not modelled: a seller who keeps a copy of a sold key and equivocates with it, which strips the buyer (3.11.5).
|
||||
|
||||
What these runs still cannot tell us is unchanged from the list above: no DAG, perfect retarget, cert reading, idealised aggregation, uptime a guess, random silent sets, keys free.
|
||||
|
||||
## Floor 2/3, 4 October 2026: the rule re-run with the floor at two thirds of total weight
|
||||
|
||||
Decision of 4 October 2026 (the project lead, O-3.15): the floor of Q3 is 2/3 of total weight, not 17/30. Since active weight never exceeds total, the active test is implied and a lock is two thirds of all 30-day weight signing; finality pauses whenever less than two thirds of the weight is connected and signing. In the simulator the rule is `floor=1.0` (`rule_p`, default since this date; `--floor 0.85` reproduces the 3 October tables), which is arithmetically the `total` denominator of A to G: the floor1.00 and total columns agree in every cell below. Two additions to the simulator: `--floor`, and the `+local` mode in which a partition side's weight table counts only the blocks it has seen (its own after the split), as a real node's window does; every earlier table kept weights global, which hid the window bound that attack scenario 6A found on the devnet (`docs/bench-log.md`, "finality v2 attack harness"). Machine load 3 to 13 (other agents' builds and test networks), `nice 10`: H 25 s, I 13 s, J 16 s, K 400 s, L about 10 min; A to G in `--quick` mode about 1 min per seed.
|
||||
|
||||
### A to G at the 2/3 floor, seeds 7, 11, 13, 17, 19, `--quick` (3-h silent runs, 1-day churn runs)
|
||||
|
||||
Every cell of the new `active/cert+floor1.00` column equals the `total` column in every seed. Against the 0.85 floor of 3 October:
|
||||
|
||||
| Scenario | Floor 0.85 (3 October, seed 7; the 3 October tables) | Floor 2/3 (five seeds) |
|
||||
|---|---|---|
|
||||
| C, 34% silent keeps mining | 0 to 2 min to the first lock, 0 to 3 stalled | never while silent: 356 to 366 stalled in 3 h in every seed |
|
||||
| C, 40% silent | 11 to 13 min, 23 to 52 stalled | never while silent |
|
||||
| C, 45%, 50%, 55% silent | 45%: never; 50%, 55%: never | never |
|
||||
| D, 35% churn | 2 min, 98 intermittent stalls in 3 days | no lock in the 1-day quick run (2,864 to 2,898 stalled); analytic day 1.4, measured in L2 below |
|
||||
| D, 50% churn | 4.1 days | no lock in 1 day; analytic day 10 (the total column of D measured 10.1 days), L2 below |
|
||||
| E, 50/50 honest, 150 and 360 min, +daa | 0 conflicts, no side locks | 0 conflicts, no side locks, every seed |
|
||||
| E, 60/40 | 0 conflicts; the 60 side locks from minute 13 | 0 conflicts; neither side locks (60% is under the floor), every seed |
|
||||
| E, 67/33 | 0 conflicts; the 67 side locks from minute 0 to 8 | 0 conflicts; the 67 side locks after 40 to 212 min in 3 of 5 seeds and never in 2 (67% sits 0.3 points over the floor against the 2.2% outage shortfall; the 33 side never) |
|
||||
| E, 80/20 | 0 conflicts; the 80 side at minute 0 | 0 conflicts; the 80 side at minute 0 |
|
||||
| E, 33/33/34 | 0 conflicts, no side locks | 0 conflicts, no side locks |
|
||||
| F2, poisoned eclipse (34% attacker, 20% pool, eclipsed side 54%), 1, 2, 4 h | 0 conflicts, 0 locks on the eclipsed side | 0 conflicts, 0 locks on the eclipsed side, every seed; pool participation minimum 0.725 to 0.738 at 1 h, 0 at 4 h, back to 1 within 120 to 125 min of the heal |
|
||||
| A, B, G | unchanged (weight, dust, the renter formula and the doubling do not involve the floor) | unchanged |
|
||||
|
||||
The pattern. Everything a floor under two thirds bought in liveness is gone (a silent third pauses finality; churn waits for the window), and everything it cost in safety is gone with it (no honest split under two thirds locks, however long the presence window has decayed). The remaining sections measure the bounds the new rule states.
|
||||
|
||||
### H to L at the 2/3 floor, seeds 7, 11, 13, 17, 19, full lengths (the simulator's own output; H, I, J, K as before, L new)
|
||||
|
||||
### H. Partition of a 50/50 honest network with an equivocating attacker, rule as specified (active/cert + floor 1.00 (a lock needs 66.7% of total)), each side retargets at once (+daa, the median-time clock of Q1), seeds 7,11,13,17,19
|
||||
|
||||
Attacker = one key holding the stated share of TOTAL weight, mining on the first side, voting on both. Each side holds (1 - a)/2 + a of total. Prediction (section 3.11 item 2): a side holding s of total locks alone once s >= the floor (66.7%) and its view's active weight has decayed to 1.5 s, which under the cert reading is P x (1 - 1.5 s) slots after the split (P = 240, so 2 h x (1 - 1.5 s), 0 at s >= 2/3); two sides over the floor need a >= 33.3%.
|
||||
|
||||
| attacker (of total) | each side holds | partition min | predicted first conflict | conflicting locks | first conflict, min | first lock per side, min | every pre-heal lock kept at the heal | stalls in 3 h after heal |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 0% | 50.0% | 150 | no (side holds 50.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 0% | 50.0% | 360 | no (side holds 50.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 10% | 55.0% | 150 | no (side holds 55.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 10% | 55.0% | 360 | no (side holds 55.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 13% | 56.5% | 150 | no (side holds 56.5% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 13% | 56.5% | 360 | no (side holds 56.5% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 14% | 57.0% | 150 | no (side holds 57.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 14% | 57.0% | 360 | no (side holds 57.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 20% | 60.0% | 150 | no (side holds 60.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 20% | 60.0% | 360 | no (side holds 60.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 30% | 65.0% | 150 | no (side holds 65.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 30% | 65.0% | 360 | no (side holds 65.0% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 33% | 66.5% | 150 | no (side holds 66.5% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 33% | 66.5% | 360 | no (side holds 66.5% < 66.7%) | 0 | never | never / never | yes | 0 |
|
||||
| 34% | 67.0% | 150 | 0 min | 2 to 51 | 2 to 77 | 0 to 60 / 0 to 76 | yes | 0 |
|
||||
| 34% | 67.0% | 360 | 0 min | 5 to 54 | 2 to 77 | 0 to 60 / 0 to 76 | yes | 0 |
|
||||
|
||||
### I. The 40/40/20 split, rule as specified (active/cert + floor 1.00 (a lock needs 66.7% of total)), +daa, seeds 7,11,13,17,19
|
||||
|
||||
| case | partition min | conflicting locks | first conflict, min | locks per side during | every pre-heal lock kept | first lock after heal, min | stalls in 3 h after heal |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| honest 40/40/20, no attacker | 150 | 0 | never | 0 / 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40/20, no attacker | 360 | 0 | never | 0 / 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40/20 plus a 10% equivocator (sides 36+10 / 36+10 / 18+10) | 150 | 0 | never | 0 / 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40/20 plus a 10% equivocator (sides 36+10 / 36+10 / 18+10) | 360 | 0 | never | 0 / 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40/20 plus a 20% equivocator (sides 32+20 / 32+20 / 16+20) | 150 | 0 | never | 0 / 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40/20 plus a 20% equivocator (sides 32+20 / 32+20 / 16+20) | 360 | 0 | never | 0 / 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40 plus a 20% equivocator reaching both (sides 40+20 / 40+20) | 150 | 0 | never | 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40 plus a 20% equivocator reaching both (sides 40+20 / 40+20) | 360 | 0 | never | 0 / 0 | yes | 0 to 0 | 0 |
|
||||
| honest 40/40 plus a 34% equivocator reaching both (sides 33+34 / 33+34) | 150 | 2 to 51 | 2 to 77 | 11 to 61 / 40 to 159 | yes | 0 | 0 |
|
||||
| honest 40/40 plus a 34% equivocator reaching both (sides 33+34 / 33+34) | 360 | 5 to 54 | 2 to 77 | 22 to 87 / 212 to 354 | yes | 0 to 0 | 0 |
|
||||
|
||||
### J. Signing stops while mining continues for 1, 6 and 24 hours, then resumes; rule as specified, seeds 7,11,13,17,19
|
||||
|
||||
A random set holding x of weight stops signing at hour 3 and resumes after the stated time. Its weight never ages out because it keeps mining. 'Longest gap' is the longest interval without a lock while they are silent: the time the node reports finality unavailable.
|
||||
|
||||
| silent weight | silent hours | first lock after the stop, min | stalled checkpoints while silent | checkpoints locked while silent | longest gap without a lock, min | first lock after resume, min | stalls in 3 h after resume | conflicting locks |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 34% | 1 | never | 118 to 123 | 0% | 60 | 0 | 0 | 0 |
|
||||
| 34% | 6 | never | 714 to 727 | 0% | 360 | 0 to 0 | 0 | 0 |
|
||||
| 34% | 24 | never | 2871 to 2890 | 0% | 1440 | 0 | 0 | 0 |
|
||||
| 40% | 1 | never | 118 to 123 | 0% | 60 | 0 | 0 | 0 |
|
||||
| 40% | 6 | never | 714 to 727 | 0% | 360 | 0 to 0 | 0 | 0 |
|
||||
| 40% | 24 | never | 2871 to 2890 | 0% | 1440 | 0 | 0 | 0 |
|
||||
| 45% | 1 | never | 118 to 123 | 0% | 60 | 0 | 0 | 0 |
|
||||
| 45% | 6 | never | 714 to 727 | 0% | 360 | 0 to 0 | 0 | 0 |
|
||||
| 45% | 24 | never | 2871 to 2890 | 0% | 1440 | 0 | 0 | 0 |
|
||||
|
||||
### K. Acquired keys: an attacker buys keys holding 20% or 40% of window weight and mines at 30% of network hashrate from day 0; rule as specified, 30 days, seeds 7,11,13,17,19
|
||||
|
||||
The sellers keep their rigs and mine on under fresh keys (so honest hashrate is unchanged and the fresh keys start under dust). Formula (section 3.11 item 5): share(t) = b (1 - t/30) + 0.30 t/30, the bought blocks age out of the window as the attacker's own blocks enter it. 'Fresh hash only' is b = 0: share(t) = 0.30 t/30.
|
||||
|
||||
Attacker weight share, simulated over the seeds / formula (attacker signs every checkpoint):
|
||||
|
||||
| day after purchase | bought 0% | bought 20% | bought 40% |
|
||||
|---|---|---|---|
|
||||
| +1 | 1.0 to 1.0% / 1.0% | 20.3 to 20.4% / 20.3% | 39.8 to 39.8% / 39.7% |
|
||||
| +5 | 5.0 to 5.0% / 5.0% | 21.7 to 21.7% / 21.7% | 38.5 to 38.7% / 38.3% |
|
||||
| +10 | 10.0 to 10.0% / 10.0% | 23.3 to 23.4% / 23.3% | 36.7 to 36.9% / 36.7% |
|
||||
| +15 | 15.0 to 15.0% / 15.0% | 25.0 to 25.0% / 25.0% | 35.0 to 35.1% / 35.0% |
|
||||
| +20 | 20.0 to 20.0% / 20.0% | 26.6 to 26.7% / 26.7% | 33.2 to 33.4% / 33.3% |
|
||||
| +25 | 24.9 to 25.0% / 25.0% | 28.0 to 28.2% / 28.3% | 31.1 to 31.4% / 31.7% |
|
||||
| +30 | 30.0 to 30.0% / 30.0% | 30.0 to 30.0% / 30.0% | 30.0 to 30.0% / 30.0% |
|
||||
|
||||
| bought weight | attacker | bought, as picked | peak share | share at day 30 | holds at least 1/3 (can veto) | stalled checkpoints in 30 days | conflicting locks |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 0% | signs | 0.0% | 30.0 to 30.0% | 30.0 to 30.0% | never | 0 | 0 |
|
||||
| 20% | signs | 20.0 to 20.0% | 30.0 to 30.0% | 30.0 to 30.0% | never | 0 | 0 |
|
||||
| 40% | signs | 40.0 to 40.0% | 39.8 to 39.8% | 30.0 to 30.0% | from day 1 until day 19 to 20 | 0 | 0 |
|
||||
| 20% | silent | 20.0 to 20.0% | 30.0 to 30.0% | 30.0 to 30.0% | never | 304 to 1045 | 0 |
|
||||
| 40% | silent | 40.0 to 40.0% | 39.8 to 39.8% | 30.0 to 30.0% | from day 1 until day 19 to 20 | 63307 to 68716 | 0 |
|
||||
|
||||
Stalled checkpoints per day while the attacker withholds its votes (2,880 checkpoints a day):
|
||||
|
||||
| bought weight | day | stalled that day |
|
||||
|---|---|---|
|
||||
| 20% | +1 | 0 to 54 |
|
||||
| 20% | +5 | 0 to 61 |
|
||||
| 20% | +10 | 0 to 22 |
|
||||
| 20% | +15 | 0 to 103 |
|
||||
| 20% | +20 | 0 to 40 |
|
||||
| 20% | +25 | 0 to 76 |
|
||||
| 20% | +30 | 17 to 154 |
|
||||
| 40% | +1 | 2880 to 2898 |
|
||||
| 40% | +5 | 2853 to 2895 |
|
||||
| 40% | +10 | 2870 to 2886 |
|
||||
| 40% | +15 | 2863 to 2901 |
|
||||
| 40% | +20 | 2857 to 2885 |
|
||||
| 40% | +25 | 137 to 675 |
|
||||
| 40% | +30 | 39 to 188 |
|
||||
|
||||
### L. The floor at 66.7% of total (floor factor 1.00): silent weight, churn, the eclipse, and long partitions with view-local weight; seeds 7,11,13,17,19 (churn and long partitions: 7,11,13)
|
||||
|
||||
L1. Signing stops while mining continues (as J), finer around one third. The floor needs the signing weight at or above 66.7% of total; the model's resting participation is 0.978, so the online signing share is about 0.978 x (1 - silent).
|
||||
|
||||
| silent weight | online signing share, about | silent hours | first lock after the stop, min | stalled checkpoints while silent | checkpoints locked while silent | longest gap without a lock, min | first lock after resume, min | stalls in 3 h after resume | conflicting locks |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 25% | 73.4% | 1 | 0 | 0 | 100% | 1 | 0 | 0 | 0 |
|
||||
| 25% | 73.4% | 6 | 0 | 0 | 100% | 1 | 0 to 0 | 0 | 0 |
|
||||
| 30% | 68.5% | 1 | 0 | 0 | 100% | 1 | 0 | 0 | 0 |
|
||||
| 30% | 68.5% | 6 | 0 | 0 to 40 | 94 to 100% | 1 to 8 | 0 to 0 | 0 | 0 |
|
||||
| 32% | 66.5% | 1 | 0 | 0 to 56 | 54 to 100% | 1 to 10 | 0 | 0 | 0 |
|
||||
| 32% | 66.5% | 6 | 0 | 35 to 221 | 69 to 95% | 8 to 32 | 0 to 0 | 0 | 0 |
|
||||
| 33% | 65.5% | 1 | 24 to 49 (never in 3 of 5) | 108 to 123 | 0 to 11% | 34 to 60 | 0 | 0 | 0 |
|
||||
| 33% | 65.5% | 6 | 24 to 266 (never in 1 of 5) | 665 to 727 | 0 to 8% | 90 to 360 | 0 to 0 | 0 | 0 |
|
||||
| 34% | 64.5% | 1 | never | 118 to 123 | 0% | 60 | 0 | 0 | 0 |
|
||||
| 34% | 64.5% | 6 | never | 714 to 727 | 0% | 360 | 0 to 0 | 0 | 0 |
|
||||
| 40% | 58.7% | 1 | never | 118 to 123 | 0% | 60 | 0 | 0 | 0 |
|
||||
| 40% | 58.7% | 6 | never | 714 to 727 | 0% | 360 | 0 to 0 | 0 | 0 |
|
||||
| 45% | 53.8% | 1 | never | 118 to 123 | 0% | 60 | 0 | 0 | 0 |
|
||||
| 45% | 53.8% | 6 | never | 714 to 727 | 0% | 360 | 0 to 0 | 0 | 0 |
|
||||
|
||||
L2. Churn (as D): a set stops mining and signing; survivors inherit the block supply (perfect retarget). Analytic first lock at day 30 (1 - (1 - 66.7%) / x) for a set holding x: 35%: day 1.4, 50%: day 10.0.
|
||||
|
||||
| churn weight | days run | first lock after the event | stalled checkpoints | stalled after the first lock | live share of total weight at the end | conflicting locks |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 35% | 3 | 1.7 to 1.7 days | 6136 to 6446 | 1322 to 1518 | 68.5 to 68.5% | 0 |
|
||||
| 50% | 12 | 10.1 to 10.3 days | 29735 to 31126 | 693 to 1318 | 70.0 to 70.0% | 0 |
|
||||
|
||||
L3. The poisoned eclipse (as F2): a 34% attacker feeds a 20% pool a private fork and signs both; the eclipsed side holds 54% of total.
|
||||
|
||||
| eclipse h | conflicting locks | first conflict, min | locks on the eclipsed side | honest-side stalls during | stalls after heal | pool participation, minimum |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 0 | never | 0 | 0 | 0 | 0.725 to 0.738 |
|
||||
| 2 | 0 | never | 0 | 0 | 0 | 0.442 to 0.471 |
|
||||
| 4 | 0 | never | 0 | 0 | 0 | 0.000 |
|
||||
|
||||
L4. Long honest partitions with view-local weight ('+local'): after the split a side's window holds only the blocks it has seen, so its own share of its own table rises as s + (1 - s) T / 30 on day T (each side retargets, +daa). Prediction: a side with pre-split share s locks alone from day 30 (floor - s) / (1 - s), 0 if s is already at the floor; 12 days, no attacker, seeds 7,11,13. The 3 October tables kept weights global (results_v2.md, 'Weights in a partition'), which hid this bound; attack scenario 6A found it on the devnet.
|
||||
|
||||
| honest split | floor factor (lock needs, of the side's own table) | partition days | predicted first lock per side, day | first lock per side, day | conflicting locks | first conflict | every pre-heal lock kept | stalls in 3 h after heal |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 50/50 | 1.00 (66.7%) | 12 | 10.0 / 10.0 | 10.2 to 10.2 / 10.1 to 10.3 | 2890 to 3550 | 10.2 to 10.4 days | yes | 0 |
|
||||
| 50/50 | 0.85 (56.7%) | 12 | 4.0 / 4.0 | 4.1 to 4.2 / 4.1 to 4.2 | 20644 to 20827 | 4.2 to 4.4 days | yes | 0 |
|
||||
| 60/40 | 1.00 (66.7%) | 12 | 5.0 / > 12 | 5.1 to 5.3 / never | 0 | never | yes | 0 |
|
||||
| 60/40 | 0.85 (56.7%) | 12 | 0 / 8.3 | 0.0 to 0.0 / 8.5 to 8.6 | 8465 to 8869 | 8.5 to 8.6 days | yes | 0 |
|
||||
| 55/45 | 1.00 (66.7%) | 12 | 7.8 / 11.8 | 7.9 to 8.0 / 12.0 to 12.0 (never in 1 of 3) | 0 to 4 | 12.0 days | yes | 0 |
|
||||
| 55/45 | 0.85 (56.7%) | 12 | 1.1 / 6.4 | 1.2 to 1.4 / 6.5 to 6.5 | 14313 to 14504 | 6.5 to 6.5 days | yes | 0 |
|
||||
|
||||
### Interpretation of H to L at the 2/3 floor, and the deltas against 0.85
|
||||
|
||||
| Measure | Floor 0.85 (3 October) | Floor 2/3 (4 October) |
|
||||
|---|---|---|
|
||||
| H, equivocator across a 50/50 honest split, smallest share that conflicts | 14% in some seeds (sides 57.0%), 20% in every seed from minute 12 to 16 | 34% (sides 67.0%): 2 to 54 conflicts in 150 to 360 min, the first at minute 2 to 77; 33% (sides 66.5%) and everything below: 0 conflicts and no lock on either side in every seed. The predicted "0 min" at 34% is the arithmetic without outages; with 2.2% of honest weight in outage a 67.0% side sits at the knife edge and locks intermittently, which is why the conflict counts are tens, not hundreds |
|
||||
| I, 40/40 plus a 20% equivocator reaching both (sides 60/60) | 256 to 276 conflicts in 150 min, first at minute 12 to 16 | 0 conflicts, no lock on either side |
|
||||
| I, 40/40 plus a 34% equivocator reaching both (sides 67/67) | not run (34% was known to break 0.85 at once) | 2 to 54 conflicts, as H's 34% row |
|
||||
| I, honest 40/40/20 with and without a 10% or 20% equivocator | 0 conflicts, no locks | 0 conflicts, no locks |
|
||||
| J and L1, silent set that keeps mining: pause threshold | between 40% (13-min first lock, 23 to 123 stalls) and 45% (never) | between 32% and 34%: 30% silent locks every checkpoint for 6 h in 4 of 5 seeds (0 to 40 stalls in one), 32% locks 69 to 100%, 33% locks 0 to 11% with a first lock after 24 to 266 min when there is one, 34% and above lock nothing for as long as they stay silent |
|
||||
| J, first lock after the silent set returns | 0 min | 0 min, every weight, every length; 0 conflicts |
|
||||
| L2 and D, churn | 35%: 2 min, 98 intermittent stalls in 3 days; 50%: 4.1 days | 35%: 1.7 days (analytic 1.4 plus the outage shortfall), then 1,322 to 1,518 intermittent stalls while the margin is thin; 50%: 10.1 to 10.3 days (analytic 10.0), then 693 to 1,318 intermittent stalls |
|
||||
| K, acquired keys worth 40% that withhold their votes | 305 to 1,085 stalls in 30 days | 63,307 to 68,716 stalls of 86,400: a silent 40% bought key pauses finality until it has decayed below one third (day 19 to 20), as the arithmetic says; keys worth 20% that withhold: 304 to 1,045 stalls (the knife edge at 30% own hashrate, days 25 to 30); shares unchanged; 0 conflicts in every K row |
|
||||
| L3 and F2, poisoned eclipse (34% attacker, 20% pool, eclipsed side 54%) | 0 conflicts | 0 conflicts, 0 locks on the eclipsed side, every seed and length |
|
||||
| L4, long honest partition with view-local weight, 50/50 | both sides lock alone from day 4.1 to 4.2 (predicted 4.0); 20,644 to 20,827 conflicts by day 12 | both sides from day 10.1 to 10.3 (predicted 10.0); 2,890 to 3,550 conflicts by day 12 |
|
||||
| L4, 60/40 | the 60 side at once, the 40 side from day 8.5 to 8.6 (predicted 8.3); 8,465 to 8,869 conflicts | the 60 side from day 5.1 to 5.3 (predicted 5.0), the 40 side never in 12 days (predicted 13.3); 0 conflicts |
|
||||
| L4, 55/45 | day 1.2 to 1.4 and 6.5 (predicted 1.1 and 6.4); 14,313 to 14,504 conflicts | day 7.9 to 8.0 and 12.0 (predicted 7.8 and 11.8); 0 to 4 conflicts at the very end of the 12 days |
|
||||
|
||||
What the floor at two thirds buys and costs, in one line each. Safety: no equivocator under one third of total weight produces a conflicting lock in any partition or eclipse of any tested length, because two certificates need 4/3 of weight in signatures; the 13.3% bound of 3 October is gone and the one-third headline of spec 3.1 holds in every view. Liveness: finality pauses whenever less than two thirds of the weight is connected and signing, which in the model is reached at 32 to 34% silent (the 2.2% outage shortfall sits inside the margin) and lasts for as long as a mining silent set stays silent, or 30 (1 - 1/(3x)) days for a departed share x. The window bound: a side of an honest partition holds two thirds of its own table from day 30 (2/3 - s) / (1 - s), 10 days at 50/50 against 4 under the old floor; no floor removes it, the exchange guidance of spec 3.9 covers it, and the devnet measured the young-window form of it (`docs/bench-log.md`, "finality v2 attack harness" S6A and "finality floor 2/3").
|
||||
|
||||
What these runs still cannot tell us: as before, plus that the '+local' mode ages the unseen blocks with the global buckets (exact for partitions under 30 days, which is every run here) and that the pre-split half of each table ages at the global rate while the real window is in each side's own DAA time, the same thing under '+daa'.
|
||||
|
|
|
|||
|
|
@ -328,10 +328,10 @@ body.all .pager{display:none}
|
|||
<h3>Speed</h3>
|
||||
<p>Igneum orders blocks with GHOSTDAG, the BlockDAG consensus proven on Kaspa. Blocks arrive in parallel and are ordered rather than orphaned, so the chain runs at one block a second at launch with scheduled steps to four and ten. A transaction is included in about one second, against twelve on Ethereum, and locked in about two minutes against roughly thirteen. Emission per block is a schedule keyed to difficulty-adjusted time, and every block in the window is paid at that rate, red or blue, so coins track blocks within the accuracy of the difficulty controller.</p>
|
||||
<h3>Finality</h3>
|
||||
<p>Every 30 seconds of chain a checkpoint forms, deep enough past the tip that the DAG will not reorder it. Every miner with at least 100 blocks in the last 30 days signs it, and the checkpoint locks when signatures representing two thirds of the active mining weight arrive. Once a checkpoint is locked it overrides the heaviest chain, so no amount of fresh hashrate can reorganise past it. In the chain's first month the weights behind those locks are thin, because nobody has a long history yet, and the chain leans on proof of work and its 12-hour finality depth the way every new proof-of-work chain does.</p>
|
||||
<p>Every 30 seconds of chain a checkpoint forms, deep enough past the tip that the DAG will not reorder it. Every miner with at least 100 blocks in the last 30 days signs it, and the checkpoint locks when signatures representing two thirds of all the mining weight of those 30 days arrive. Once a checkpoint is locked it overrides the heaviest chain, so no amount of fresh hashrate can reorganise past it. In the chain's first month the weights behind those locks are thin, because nobody has a long history yet, and the chain leans on proof of work and its 12-hour finality depth the way every new proof-of-work chain does.</p>
|
||||
<div class="pull">The word sustained is the whole defence. Block rewards go to whoever mines, new or old. The right to lock history is earned.</div>
|
||||
<p>A miner's vote weight is simply the blocks it has mined over the trailing 30 days, measured by work, so splitting into many keys buys nothing and joining a pool costs nothing. Hashrate that arrived today holds almost none of it. Even an attacker who brought the whole network's hashrate would need ten days of mining in public to hold a third of the weight, and twenty days to hold two thirds. At 51% of the network they never reach two thirds at all while the honest miners keep mining. Rental is priced by the hour. The only route left is to drive honest miners off the chain and hold two thirds for a month on the public hashrate charts, which is the same limit Bitcoin lives with, with a month's warning attached.</p>
|
||||
<p>Two further rules close the gaps. A key that stops signing drops out of the active count within two hours. A lock still needs 56.7% of all 30-day weight, so if more than about 42% of weight goes quiet finality pauses until it returns or ages out, up to 30 days, and the chain runs on proof of work meanwhile. The node reports the pause. Beneath the latest lock the depth to rely on is the finality depth: a node never switches to a chain forked more than 12 hours of median time back, and a certified checkpoint shortens that to its own age. Kaspa's one-hour merge depth is a limit on which old blocks a new block may merge, not a reorganisation bound. Signing two different checkpoints at the same height is equivocation, provable by anyone, and it strips the key of its vote for 30 days.</p>
|
||||
<p>Two further rules close the gaps. A lock needs two thirds of all 30-day weight, so finality pauses whenever less than two thirds of that weight is connected and signing, until it returns or ages out of the window, up to 30 days, and the chain runs on proof of work meanwhile. The node reports the pause. A key that stops signing is reported as absent within two hours, which is how operators see a pause coming. Beneath the latest lock the depth to rely on is the finality depth: a node never switches to a chain forked more than 12 hours of median time back, and a certified checkpoint shortens that to its own age. Kaspa's one-hour merge depth is a limit on which old blocks a new block may merge, not a reorganisation bound. Signing two different checkpoints at the same height is equivocation, provable by anyone, and it strips the key of its vote for 30 days.</p>
|
||||
<h3>What is not here</h3>
|
||||
<p>No stake. No coin-holder class votes on anything. No anchoring into Bitcoin or any other chain. Nothing in Igneum's consensus depends on anything outside Igneum.</p>
|
||||
</section>
|
||||
|
|
@ -507,7 +507,7 @@ body.all .pager{display:none}
|
|||
<li><strong>A chip is impossible.</strong> No. A chip is pointless, because the target moves before it ships. The honest efficiency ceiling for a fixed chip on a memory-bound program is under 2x, approximate, and Igneum's generator changes under it every hour.</li>
|
||||
<li><strong>A guaranteed income floor.</strong> No. External proving is a small market today. Igneum's miners have the lowest cost in it, which is an edge and nothing more.</li>
|
||||
<li><strong>Finality that no amount of hardware can break.</strong> No. A miner holding a third of the last 30 days of blocks can split finality during a network partition, and two thirds can lock a bad checkpoint for a double-spend bounded by the 12-hour finality depth. Reaching a third takes at least ten days of the whole network's hashrate, in public. That is harder than attacking Bitcoin, where a majority can reorganise at once, and it is the limit of proof of work without stake or an outside chain. Igneum chose those limits on purpose.</li>
|
||||
<li><strong>Finality that never pauses.</strong> No. A lock needs 56.7% of all 30-day mining weight. If more than about 42% of that weight stops signing, finality pauses until it returns or ages out of the window, up to 30 days. The chain keeps running on proof of work and the node reports the pause.</li>
|
||||
<li><strong>Finality that never pauses.</strong> No. A lock needs two thirds of all 30-day mining weight. Whenever less than two thirds of that weight is connected and signing, finality pauses until it returns or ages out of the window, up to 30 days. The chain keeps running on proof of work and the node reports the pause.</li>
|
||||
<li><strong>A finished protocol.</strong> The sustained-mining finality rule is the newest piece and the one that external review will try hardest to break. The specification, the review and the benchmarks are published as they happen.</li>
|
||||
</ul>
|
||||
<p>Everything in this document is subject to the gates on the roadmap. Nothing in it is an offer to sell anything.</p>
|
||||
|
|
|
|||
Loading…
Reference in a new issue