Horizon lane 5, network: block rate, propagation, node cost, bandwidth, pruning and the snapshot path, with the run A schedule and scripts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 20:52:33 +00:00
parent 7893d84a0c
commit 3349d8049d
15 changed files with 1225 additions and 11 deletions

View file

@ -2,36 +2,160 @@
Written 6 to 7 October 2026 by the Horizon coordinator (branch `horizon`, worktree `igneum-wt-horizon`) on the project lead's ask of 6 October 2026, 20:4x UK: "deep backward and forward predictive research and modelling for our algo and all of our systems: is there room for improvement, room for more coin utility, algo improvements, security improvements, anything we can do to make a 51% attack impossible, basically creating a level of polish that has not been seen before." Later the same evening: "research anything else that we can research too, predictions, forward thinking, what we can actually do that has not been done or applied, think outside the box", "be revolutionary", and "if we create a new way of hashing or a new way of proof of work to revolutionise the space then that's absolutely fine, I want you to deploy everything to create something that has not ever been done before."
The bar main set, and the bar this document holds every claim to: "impossible" is not available to any proof-of-work chain. The bar is that a majority of hash buys nothing: it cannot reverse what finality locked, cannot forge a proof the nodes re-execute, cannot change a rule without 95 percent signalling, and loses more than it earns. Every claim here is a number with a model or a simulation behind it, priced in rented hash at the measured 6 October rate (`docs/bench-log.md`, "Rental cost of hash, 6 October 2026": USD 0.0117 per MH/s-hour, the live devnet at 1.16 GH/s), with its consequence per tier and what to build.
The bar main set, and the bar this document holds every claim to: "impossible" is not available to any proof-of-work chain. The bar is that a majority of hash buys nothing: it cannot reverse what finality locked, cannot forge a proof the nodes re-execute, cannot change a rule without 95 percent signalling, and loses more than it earns. Every claim here is a number with a model or a simulation behind it, priced in rented hash at the measured 6 October rate (`docs/bench-log.md`, "Rental cost of hash, 6 October 2026": USD 0.0117 per MH/s-hour, so USD 11.7 per GH/s-hour and about USD 281 per GH/s-day; the live devnet at 1.16 GH/s), with its consequence per tier and what to build. Hours are agent hours (the project lead's rule: Claude-side work takes hours, never weeks).
## The lanes
| Lane | File | State |
|---|---|---|
| 1 consensus-security | `docs/analysis/horizon/consensus-security.md`, and the paper `docs/analysis/51-percent.md` | running |
| 2 algorithm | `docs/analysis/horizon/algorithm.md` | running |
| 3 finality-and-weight | `docs/analysis/horizon/finality-and-weight.md` | running |
| 4 economy-and-utility | `docs/analysis/horizon/economy-and-utility.md` | running |
| 5 network | `docs/analysis/horizon/network.md` | queued (waits for the 10 bps rows of `block-rate-devnet2.md`) |
| 6 polish | `docs/analysis/horizon/polish.md` | queued |
| 7 frontier | `docs/analysis/horizon/frontier.md`, model `sim/horizon/frontier/frontier_model.py` | landed 6 Oct 2026, 21:1x UK |
| 8 new-proof-of-work | `docs/analysis/horizon/new-pow.md`, prototypes in `proto-newpow/` | running (designs tonight, measured rows tomorrow afternoon UK) |
| 1 consensus-security | `docs/analysis/horizon/consensus-security.md`, and the paper `docs/analysis/51-percent.md`; models `sim/horizon/consensus-security/` | landed, commit eec2cd7 |
| 2 algorithm | `docs/analysis/horizon/algorithm.md`; model `sim/horizon/algorithm/model.py` | landed, commit 5ff7393 |
| 3 finality-and-weight | `docs/analysis/horizon/finality-and-weight.md`; models `sim/horizon/finality-and-weight/` | landed, commit c3aa502 |
| 4 economy-and-utility | `docs/analysis/horizon/economy-and-utility.md`; models `sim/horizon/economy-and-utility/` | landed, commit 4617a01 |
| 5 network | `docs/analysis/horizon/network.md` | running (takes the 10 bps runs A, A2 and the 1 bps control B) |
| 6 polish | `docs/analysis/horizon/polish.md` | running |
| 7 frontier | `docs/analysis/horizon/frontier.md`; model `sim/horizon/frontier/frontier_model.py` | landed, commit 5ff7393 |
| 8 new-proof-of-work | `docs/analysis/horizon/new-pow.md`; prototypes `proto-newpow/` | designs landed (5ff7393); measured rows by the afternoon of 7 October UK |
## 1. One page for the project lead
(Written last, from the lanes.)
What the night found, in the order it matters.
1. **The one line where a majority earns more than it spends is the proving pool, not the chain.** Consensus checks a carried proof record's signature and its statement, not the proof (spec 07, 7.7 items 3 and 4; ledger P21, decided as v0 through the public testnet). Any block producer can therefore carry its own fake-proof records and be paid its block share of the 20 percent pool: 11,636 IGN an hour at 51 percent of blocks (lane 1, `cost_model.py`). Every other attack line costs more than it earns. Fix: verify the aggregated segment proof in consensus, 8 to 12 hours, no liveness cost. Until it lands, the public text must not say the 20 percent pool is "paid to provers" without the caveat.
2. **Tonight's two-hour finality pause was the rule working, then the frozen table holding it, and the fix is a signed exit.** Twenty keys holding 42.7 percent of the frozen voter table left in three minutes (the class v4 rehearsal job); checkpoint 6843 saw 53.1 percent of total and the 2/3 rule paused as designed; locks had formed without the hub, so topology is refuted (lane 3, from the observer rows). Rule v2 would have re-locked after 35 minutes; rule v3's frozen table holds it for a window: 2 hours on the devnet, 30 days on mainnet for the same event. Of five candidate rules simulated, only the departure announcement (a `leave` item in blocks, the key out of every denominator one hour later) keeps zero conflicting locks in every partition and eclipse AND ends the pause in under an hour (6 hours). The operational rule costs nothing: a standing box never leaves the live chain for an experiment, and any orchestrated departure over 10 percent of weight goes in slices under 10 percent an hour.
3. **A 51 percent attacker on Igneum buys a 90-second reorder window and nothing past a certificate.** A 45 to 51 percent withholder wins the selected-chain race over a 90-s hold 70 to 85 percent of the time (32 to 46 blocks at 1 bps); the lock lands 63 to 93 s after the checkpoint block and bounds what a majority can reorder; sustained withholding lifts weight share to about 56 percent, never two thirds (lane 1, GHOSTDAG simulator mirroring `protocol.rs`, 20 seeds). The veto (one third of weight) costs 20 days at 51 percent of hash: USD 6k at 1 GH/s, USD 5.8 M at 1 TH/s, half earned back as subsidy; once held, a pause is free and a pause-time 12-hour double spend costs USD 146 to 146k. Weight-gated deep fork choice (a tip forked more than 10 minutes back is a candidate only if its builders hold a third of the weight table at the fork) and vote-or-burn (a silent key's blocks burn 20 percent of their producer share) close those two lines, 16 to 24 hours together, no liveness cost. The paper is `docs/analysis/51-percent.md`.
4. **The chip question is settled in kind and open in degree.** The chip that matters is the stored-dataset memory-controller chip: 5.7x per joule against the 5090 at class v3, 2.1x at class v4 with a chip core as efficient as the GPU's (k = 1), 0.9x against the M5 Max (lane 2, `chip-model-v3` method). The reserve and the era draw buy about nothing against a chip (every drawn parameter is firmware; a reserve block is about USD 4 of silicon) and the public text should say what they do buy. The lever is the latency-shadow size N, and lane 2 and lane 7 agree it belongs in the era draw at genesis as a verifier-bounded ladder {100k, 130k, 200k, 330k, 650k, 1.0M}, each step by 90 percent miner signal, never unconditional (an unconditional doubling retires the M5 Max at era 1). First measured verifier proxies tonight: class v4 5.06 ms cold on a box core, 8.23 with the SMT sibling loaded (passes); dr736 10.51 and 15.49 (out); R0 is dr368.
5. **Fees are not a security budget for a decade, and the proving price is a function of network hash.** All utility curves together pay miners and provers USD 450 a day at launch and USD 4,200 in year 5 against USD 54,800 and 13,700 of daily emission (lane 4). The price a prover must charge is the subsidy it forgoes, 1 / network hash: 100 to 300x Boundless's rate at 1.16 GH/s, 0.2 to 0.4x at 100 GH/s beside its miner. The adopted job floor overprices the market above about USD 0.014 per IGN. A ten-day prover refusal strands 547,570 IGN a day of pool credit in an escrow with no rule to return it. Fixes: decouple the job price from `f_p` (16 hours), roll unproven credit forward (8 hours), publish the price as a formula, never a number (3 hours).
6. **The signalling window can be bought for a day.** The P2 one-day 95 percent window costs about 19 N of hash for 24 hours (USD 534k at 100 GH/s) and would force a class flip onto a fleet not yet on the object; a 6 percent holdout buys delay to the floor for nothing. Seven consecutive daily windows with the floor a week past publish fixes it, 3 hours. The three signalling thresholds (60 parameter, 90 upgrade, 95 class with floor) are stated inconsistently across spec 5.7, CLAUDE.md and the litepaper and must become one sentence.
7. **New proof of work.** Scheme A (mining is proving) is ruled out twice over, by lane 8's design pass (2.9 MB of openings per block, a 32 to 40 ms proof verify against the 10 ms gate, sampleability) and by lane 7's prior-art pass (Ball et al. 2017, Ofelimos 2022, Aleo). Schemes B (a tensor-shaped integer shadow that forces a chip to carry a GPU-class datapath) and C (the dataset derived from the execution state, so every hash proves the miner holds the chain) are in prototype on two rented 4090s; measured rows land by the afternoon. (Section 4, lane 8, when it lands.)
8. **The frontier list, honestly cut.** Do now: the finality weight table carried inside the recursive segment proof (a consensus proof at mergeset cost, a browser that trusts no node for the voter set; 60 hours, measure the in-guest BLS and colouring cycles first), reproducible-build attestations with Ember refusing a release under N of M (12 hours), a WASM verifier of the wrapped block proof in the tab (16 hours), Ember as node, wallet and light client for everyone (20 hours). Never: proving others' chains as the main income (all of Ethereum L1's proving is about USD 36 a day at the Sep 2026 tracker cost against USD 13,700 a day of year-1 emission at USD 0.005), the hash partly a proof, proof verify on a hardware wallet's secure element, a general unverifiable compute market, burn-redirect audit bounties (a dev fund with a veto).
Rows from lanes 5 (network: block rate, the controller's red-block feedback seen in run A, node and bandwidth tiers), 6 (polish: the ten things the project lead would notice) and 8 (the two prototypes' measured rows) join this page and the table below when they land.
## 2. The ranked list: top 25 across every lane
Rank is payoff over cost across lanes, with safety first, then liveness, then money, then text. "L1 r3" means lane 1's own rank 3; the lane file holds the full evidence row.
| Rank | Item | Lane | Evidence | Model | Hours | Consequence per tier | What to build | Gate |
|---|---|---|---|---|---|---|---|---|
| 1 | Verify the aggregated segment proof in consensus; a record whose proof fails against the pinned aggregator key is invalid | L1 r1 | spec 07 7.7 items 3 and 4; P21; a producer captures its block share of the 20 percent pool with fake records | capture = pool x (H inside the window + most outside): 11,636 IGN/h at 51 percent; SP1 light-verifier cost per record 1.8 to 2.4 s measured (bench-log) | 8 to 12 | prover: paid only for proofs; holder: the pool is real; node operator: one verify per record on the proof pool thread; miners: nothing | the verify call in the record check of `consensus/core` proving, the v0 per-shard record payout-only and capped at the exclusive window | fast-time: a fake-proof record is rejected by every node and the producer loses the block; a true record pays |
| 2 | The departure announcement: a `leave` item (key, DAA score, signature) in blocks; D = 1 h later the key is in no denominator, sliding or frozen, and its votes are invalid; app Stop and the fleet library send it | L3 r1, L1 r8 | tonight's pause (L3 3.1): 42.7 percent left in 3 min, the frozen table held a window; sim T: first lock 1 h after a 34, 45 or 50 percent departure, 0 conflicts in every partition, eclipse and equivocator row | `finality_horizon.py` seeds 7, 11, 13 | 6 (+4 for F5's trusted certificate) | holder: a 30-day mainnet pause becomes 1 h when leavers are honest; a silent leaver still costs a window; pool and rig: one message on a clean stop; attacker: buying keys to leave them gains nothing (w + L must still reach 2/3) | the item in the coinbase finality section, the denominator rule in `finality.rs`, the send in Ember and `tools/fleet/lib` | harness: 45 percent leaves with leaves, lock within D + 1 checkpoint; 0 conflicts in 50/50, 60/40 and the 34 percent eclipse |
| 3 | Signalling over 7 consecutive daily windows at 95 percent, the floor no nearer than 7 days past the publish, the stale-box list empty before the floor | L1 r6, L4 4.5 | the P2 one-day window is buyable: 19 N of hash for 24 h (USD 534k at 100 GH/s) forces a flip onto an unready fleet; a 6 percent holdout buys delay for nothing | `signalling.py`, `signal_game.py` | 3 | pool and rig: a week more before a class change and a week of visible share; home miner: a week to update; attacker: the bill x7 in public | the window count in the P2 rule, spec 5.7 text, one sentence carrying 60 / 90 / 95 in spec, CLAUDE.md and the litepaper | fast-time: 6 of 7 days at 95 percent does not flip; 7 does; the harness's failed case |
| 4 | Weight-gated deep fork choice: a tip whose fork point is older than D (10 min of past-median time) is a candidate only if the blocks on it since the fork were produced by keys holding at least 1/3 of the weight table at the fork point | L1 r2 | during a pause or the first 20 days a renter with fresh keys double-spends at the 12-h depth for USD 146 at 1 GH/s | `cost_model.py`; deterministic over the block's past | 10 to 16 | holder and exchange: rented hash cannot reorg past 10 min even while nothing locks; honest miners: nothing (their keys hold the weight); new chain: the first 20 days gain a bound they lack today | the candidate filter in GHOSTDAG tip selection over the certified weight table | fast-time: a 51 percent fresh-key fork from 15 min back is refused by every honest node; a 1/3-weight fork is accepted; partition heal unchanged |
| 5 | Vote-or-burn: a block whose producer key has participation under 0.5 over the presence window in its own past burns 20 percent of its producer share | L1 r3 | the pause as a liveness attack has zero marginal cost once the veto is held | 0.2 x A x 0.8 x subsidy: 7,757 IGN/h at 34 percent (USD 155/h at 0.02) | 6 to 8 | attacker: a pause costs; honest home miner: nothing while signing (the official client signs every checkpoint); pool user: the pool's participation; partition-safe because the test is the block's own past | the participation read in coinbase validation, the burn in the subsidy split | fast-time: a 34 percent silent set's blocks pay 80 percent; a 50/50 partition burns nothing on either side |
| 6 | Roll unproven shard credit into the next proven segment's pool instead of the escrow for ever | L4 r2 | a ten-day prover refusal strands 547,570 IGN a day (5.5 M IGN) with no rule; spec 5.3, 7.7 item 3, 7.8 item 7 are silent | `stress.py` refuse scenario, `stranded_share` | 8 | prover: credit is never lost to the market's slow days; holder: no undecided burn; rollup customer: nothing | the roll-forward in the pool credit split, spec 5.3 text | fast-time: 100 unproven then 10 proven segments return the escrow to zero |
| 7 | Decouple the job price from `f_p`: reserve = measured proving electricity per pgas at the published settlement rate (spec 5.10.3), the 1.5 premium becomes a bid | L4 r1 | at the adopted floor a billion-cycle job is 15 IGN = USD 0.075 / 0.30 / 1.50 against Boundless's 0.21 (approximate); above USD 0.014 per IGN the chain overprices by rule | `utility.py` sections 1 to 2 | 16 | rollup customer: a price that can clear; prover: still paid above electricity; holder: more jobs, more burn | the reserve rule in spec 5.4 and 5.11, the bid field on the job | a simulated job book clears within 20 percent of Boundless's median at all three prices |
| 8 | Operational rule, no protocol change: a standing box never leaves the live chain for an experiment; any orchestrated departure over 10 percent of weight is staged in slices under 10 percent an hour; the fleet library refuses to swap a standing box's chain | L3 r2 | the rehearsal took 36.5 percent at once; sim L2 and M4: a gradual departure costs nothing because every lock re-freezes the table | spec 3.7 item 2 | 1 | fleet operator: finality stays on; every tier: no pause from our own experiments | a refusal in `tools/fleet/lib` plus a `--staged` departure | the next rehearsal keeps `finality_active` true |
| 9 | Peer floor and mesh, and no `unwrap` on a peer-driven path with a sync-request fuzz gate (tonight's pruned-node crash: `consensus/src/processes/sync/mod.rs:87`, plus 27 sibling sites listed in L1) | L1 r4 | run A's star (321 tips, 77 percent red); the hub crash at 19:57Z: any peer can crash any pruned node at zero hash cost | a star with its hub down is n islands; the sibling list by file:line | 9 to 11 | node operator: no crash from a peer's request; home miner and rig: the app alarms under 3 outbound peers or 2 checkpoints without a vote; fleet: no star | the fleet lib dials 3 boxes beside the hands; the node alarm and app line; the unwrap sweep; the fuzz in `tools/ci` | the fuzz runs the sync request space against a pruned node with no panic; the alarm fires on a known-cut case and stays quiet on a healthy one |
| 10 | N (the latency-shadow size) as a genesis ladder per era {100k, 130k, 200k, 330k, 650k, 1.0M}, floor and ceiling fixed at genesis (10x verifier headroom on the 2.5x rule; O-1.14 sets the ceiling), one era draw consumed as for `epoch_len`, each step by 90 percent miner signal over 7 days, never unconditional | L2 r5, L7 finding 1 | HBM4 raises the stored-dataset chip's bare edge (4.9x to 12x depending on tFAW, L2 and L7 disagree and both are labelled); N = 100k holds the chip at 2.1x on GDDR7 at k = 1, N = 330k at 1.3x; an unconditional doubling retires the M5 Max at era 1 (-10.5 percent at 200k) | `model.py --section ladder` (L2 5.3a, the reconciled table) | 6 to 8 | 100k to 130k: the M5 Max -3.3 points, nobody else; 130k to 200k: M5 Max -6 more, the 5090 -2.7 at its cap, the 4070 +21 W; 200k to 330k: compute-bound on every capped NVIDIA card; pool users nothing at any step; verifier +0.17 to 0.56 ms per warp | the ladder in the era-draw table of spec 1.13, the step rule on P2's mechanism | per step: every public-benchmark card within 5 percent of its previous-step rate, bit-exact on three vendors, verifier under 10 ms cold on the O-1.14 core |
| 11 | Aggregate-first vote verification and aggregated in-block carriage as the mainnet default (spec 3.4.2 item 2, decided) | L3 r3 | per-vote verification is 1.6 s per checkpoint per node at 1,000 voters and about 13 s at 8,192 (approximate); measured lock delay p50 0.86 to 1.26 s at 4 to 93 voters on node 1 | L3 4.3 table; blst 0.3.17 `fast_aggregate_verify` already in the fork | 8 | node operator on a laptop: a checkpoint costs one pairing, not a thousand; every tier: lock delay stays under 3 s at 8,192 voters | batch votes per (index, hash), verify once, bisect on failure; the in-block aggregate | fast-time with 1,000 and 8,000 synthetic voters: p50 lock delay under 3 s, CPU under 25 percent of a core, 0 conflicts |
| 12 | The finality weight table carried inside the recursive segment proof, updated one mergeset per segment: a consensus proof at mergeset cost | L7 r1 | phase two's hardest item (P4) becomes incremental on code that exists; about one BLS verify per 30 s (one shard's budget, approximate, unmeasured) | `frontier_model.py` | 60 (first 10: measure) | phone wallet and bridge: finality from one proof, no node trusted for the voter set; rollup customer: one object; prover: one more public-value block per segment | the W2 transition in the aggregator guest, the GHOSTDAG colouring check in-guest | first: BLS verify and colouring cycle counts inside the SP1 guest on a 24 GB card within 2x of the estimate; then a proof per checkpoint under 30 s on a proving-only 5090 |
| 13 | Reproducible-build attestations in a registry contract; Ember refuses a release under N of M attestations | L7 r3 | ledger G7, G13: the update key is an operator channel; Bitcoin's guix.sigs is the precedent with the chain as the sigs repo | text and contract | 12 | every tier: an update needs N independent builders, not one key; fleet: the box's reproducible Windows and Linux builds (CLAUDE.md 6 Oct) are the inputs | the contract, the builder tool, the Ember check | a release with 1 of 3 attestations is refused by Ember; 3 of 3 installs; the known-failed case |
| 14 | Close O-1.14 with a real 2019 laptop run; adopt the half-core proxy as the standing stand-in; dr736 out, dr368 in as R0 | L2 r1 | box proxy: dr736 10.51 ms cold and 15.49 half-core; class v4 5.06 and 8.23 | L2 5.5 | 2 | miners 0; pool verifier cores x1.3 at dr368 instead of x2.4; a 2019 node keeps 1.8 ms under the gate on class v4 | Windows cross build on the box, relay to the laptop, bench, log | ms per warp under 10 cold on the real core for class v4 and dr368 |
| 15 | The vote signs the execution root too (chain id, index, block hash, post_root); a certificate pins the state; snapshots check against the last certificate | L1 r5 | snapshot poisoning: a wrong state above the pin is undetectable today | every voter executes natively (spec 07) | 8 to 12 | holder: a lock is a state lock; node joining from a snapshot: cannot be poisoned; cost: exec lag enters lock latency | the vote message, the pin rule, the snapshot check | fast-time: a poisoned snapshot node cannot join the quorum; honest lock delay rises by the measured exec lag only |
| 16 | `finality_provisional` reported beside `finality_active`, never as a lock, plus a detector-driven `security_alert` (correlated group over 1/3 of a day's blue blocks; pause; conflict) shown by wallets and the explorer as "confirm at 12 h" | L3 r4, L1 r7 | sim P: provisional conflicts in every partition (516 to 1,062 per 360 min), final 0; the exchange guidance exists only as text today | L3 4.1; the detector of counter-asic-3 | 3 + 4 to 6 | exchanges: a third row in the guidance; holder: told when to wait; home miner: a line in Ember | the RPC fields, the explorer and wallet copy, spec 3.9 row | forced pause: explorer and wallet show provisional and the alert; a healthy day shows neither |
| 17 | Every miner-signalled execution parameter enters the consensus digest the same release, with a `tools/ci` check that fails a `Params` field marked signalled and absent from the digest | L4 r5 | signal-then-defect on an execution parameter is a silent state fork unless the handshake refuses the defector; `Params.fees` is in the digest, nothing guarantees the next | `signal_game.py` section 3 | 6 | node operator: no quiet fork; every tier: a defector is refused at the handshake | the check, the digest rule in spec 5.8 | the check fails on a planted field and passes on the live set |
| 18 | The 80/20 split as a 60 percent parameter inside a hard band [10, 30] percent | L4 r4 | not load-bearing at launch traffic; 30 percent buys backlog relief at 100 shards a block (economy-2026-10-04 5.3) | `stress.py` | 10 | prover and miner: a split the market can move inside a band nobody can break; holder: the cap and the halving untouched | the band in spec 5.3 and 5.7 | harness: a 60 percent vote reaches 30 percent; a 100 percent vote cannot pass the band |
| 19 | A WebAssembly verifier of the wrapped block proof in the tab, with the millisecond count shown | L7 r4 | three working precedents (Helios WASM, ziren-wasm-verifier, wasm-groth16-verifier); the certificate half already runs at 58 to 68 ms warm, 139 to 155 ms cold (bench-log round 6, P3) | text | 16 | holder and rollup customer: verify in the browser; the P3 wrapper measurement comes forward | the Groth16 or Plonk wrapper, the WASM verifier on the site | a block proof verifies in the tab under 500 ms on a laptop; the measurement labelled on the page |
| 20 | Ember as node, wallet and light client for everyone | L7 r5 | 1,000 testnet miners become 1,000 verifying nodes; Monero shows about 5,000 peers (monero.fail), Ethereum 8,136 execution nodes (ethernodes), approximate | text | 20 | home miner: one app; node count = miner count; Sybil counts of nodes irrelevant by design; node cost per lane 5's tiers | the node and wallet surfaces inside Ember | a fresh Windows install mines, verifies and shows a balance in the first 60 s with no second download |
| 21 | Work-stake: vote weight as the external-job bond, with the spec sentence "no coin stake; the only thing at stake is 30 days of public work" written first | L7 r2 | a 0.1 percent key has about 16,427 IGN of 30-day pool income plus its vote at risk against a designed 0.0015 IGN coin bond per job | `frontier_model.py` section 3 | 24 (prototype) | prover: a bond nobody can buy; rollup customer: a griefing cost that scales with the prover's standing; holder: "no stake" stays true as "no coin stake" | the bond rule in the job claim | a griefed job costs the griefer its weight for 30 days in the fast-time harness; the spec sentence lands before the prototype |
| 22 | Client-shipped certified checkpoint (index, hash, voter-table digest) refused if missing from the DAG; exec generations spaced geometrically to the finality depth (about 16, 1.8 GB today) | L1 r9, r10 | long-range and seed attacks; a pause-time deep reorg needs a peer's snapshot today | assumevalid's shape; 114.8 MB per snapshot measured | 3 + 3 | every tier: a fresh install cannot be bootstrapped onto a private DAG; node operator: disk for generations, no blocked executor after a deep reorg | the checkpoint in the release, the generation schedule | a cold node refuses a DAG missing the checkpoint; a 5,000-block reorg re-executes from a generation with no snapshot request |
| 23 | Public-text corrections, one bundle: the prover's price as a formula with network hash as the input, never a number; the era draw and the reserve described as schedule changes against fixed datapaths, not unpredictability against a chip, with the chip's USD per MH/s-hour beside the honest cards'; F19's "day 19 to 20" is a v2 number (v3: a full window); funding.md section 4's dev-fee ceiling is 1 percent of the producer share (38,520 / 154,080 / 770,400); the dev fee is "default-on, switchable", not "optional"; "proofs at the cost of power" conditioned on hash; the three signalling thresholds in one sentence | L4 r3, r7; L2 r3; L3; L1 | each row cites its lane section | text | 1 to 3 each | holders and critics read claims that survive review | the litepaper, the customer brief, spec 5.7, funding.md, the ledger F19 | the site's forbidden-strings check and a reviewer's read |
| 24 | Measure the FPGA lane on AWS F2 (one VU47P, HBM2, about USD 1.98 an hour) and replace the ceiling row | L2 r2 | the measured 2.4 G reads/s equals the JEDEC tFAW ceiling; the old 1.9x row rests on a 12 ns tFAW the JEDEC HBM2 table does not give (28 ns); the soft overlay reads 0.30x to 0.47x of the 5090 per watt | L2 5.1 | 8 to 10 plus USD 2 to 8 | none today; the public FPGA claim becomes a measured number | the overlay bench on F2 | reads per second per watt at 1 GiB; the row replaced |
| 25 | Ember tune as the shipped default per card model, and the two fleet measurements every price rests on: the miner's hash loss while each tier proves, and a full 30 M-cycle shard beside the miner on 12 and 16 GB cards | L2 r7, L4 r8 | the 4070 at 3.65 uJ untuned and 2.57 tuned (-30 percent); the hybrid row is the only one that undercuts the market and rests on one 5090 measurement; the 4.7 M fixture is 16 percent of a full shard (linear scaling says 240 s on a 3060, outside the 120-s claim timeout) | L2 5.1; `utility.py hybrid_hash_loss` | 2 + 6 (fleet) | every NVIDIA tier gains 10 to 30 percent per joule; the 12 GB tier learns whether it can claim a full shard in time | the defaults table in the app from the fleet priors; eleven rows with both numbers in `prover-tiers-real-cards.md` | the rows land; the claim timeout is set from them |
Rows from lanes 5, 6 and 8 are inserted and the table re-ranked when they land (expected: the controller's red-aware correction and the block-rate recommendation from lane 5; the ten the project lead-visible polish rows from lane 6; the class v5 verdicts from lane 8).
### Not recommended, with the reason (as valuable as the list above)
| Item | Lanes | Why not |
|---|---|---|
| Prover attestations as a second finality leg | L1 r12, L3 r8 | provers are the miners (same vote keys), so no new party; proof coverage is 2.4 to 4.7 percent of blocks tonight with lag p99 62 s, so every lock would wait on proofs; revisit only after 99 percent of blocks are proven within 60 s for 7 days with 3 provers per block, and even then it adds about a minute of lock delay |
| Time-locked (vesting) weight | L1 r13 | a bought key transfers vested weight, so the acquired-keys bound is unchanged; honest new cohorts wait longer |
| Any automatic re-lock after an abrupt departure | L1 r14, L3 r7 | departure and partition are the same observation in one view; the decaying denominator produces 467 to 473 conflicting locks in a 360-minute 50/50 honest split, the hysteresis floor brings the 13.3 percent equivocator bound back (565 to 597 conflicts at 20 percent); the two-tier rule is a report, never a lock |
| Mining is proving (the lottery's work as a proving step) | L8 scheme A, L7 3.10 | dead on bytes (2.9 MB of openings per block), on the verifier (32 to 40 ms against 10), and on sampleability (Ball et al. 2017, Ofelimos 2022); re-opens Aleo's fastest-prover-wins |
| Proving others' chains as the main income | L7 3.11 | all of Ethereum L1's proving is about USD 36 a day at the Sep 2026 tracker cost against USD 13,700 a day of year-1 emission at USD 0.005; demand must grow 1,000x against a falling cost curve |
| Burn-redirect audit bounties, a review escrow by 60 percent signal | L7 3.6, L4 task 3 | a dev fund with a veto, the switch spec 5.5 removed; the honest payer for a second audit is the entity's own provers |
| Proof verification on a hardware wallet's secure element | L7 3.9 | a bn254 pairing on a Cortex-M-class element is seconds to minutes (approximate); do the companion verify |
| A general compute market for unverifiable work (rendering, inference) | L7 3.12 | an escrow without a verifier is a trust-me payment with lower fees; the verifiable subsets are named and kept |
## 3. What a 51 percent attacker can and cannot do
(Summary of `docs/analysis/51-percent.md`.)
The paper is `docs/analysis/51-percent.md` (lane 1). In one table, at the measured USD 11.7 per GH/s-hour:
| Hash share | What it buys | For how long | Cost at 1 GH/s / 1 TH/s of network | What it earns | Net |
|---|---|---|---|---|---|
| 20 percent | reorders only the last k = 18 blocks; nothing past a certificate | per attempt | subsidy forgone while withholding | its block share minus reds | loss |
| 34 percent | wins the selected-chain race only inside 60 s (40 percent of the time); holds the veto (blocks every lock) after 20 days of mining at 51 percent, or 30 days at 52 percent of the network's hash | 20 to 30 days to acquire, then free to hold while silent | about USD 6k / 5.8 M to acquire (half earned back as subsidy); a pause then costs nothing | subsidy; nothing from the pause itself unless it double-spends at the 12-h depth during the pause (USD 146 to 146k) | the one line vote-or-burn (rank 5) and weight-gated fork choice (rank 4) close |
| 45 to 51 percent | wins the selected-chain race over a 90-s hold 70 to 85 percent of the time (32 to 46 blocks at 1 bps); turns 26 to 28 percent of honest blocks red; lifts its weight share to about 56 percent, never 2/3 | the 63 to 93 s between a checkpoint block and its lock | USD 11.7 / 11,700 per hour of rented hash; the market could not supply a TH/s on 6 October (RunPod: 0 pods for 20 asks) | a quarter of honest subsidy while withholding; the pool capture of rank 1 (11,636 IGN/h) until the in-consensus verifier lands | loss on the chain; gain only through the pool, which rank 1 closes |
| 67 percent of weight | locks alone | 2.03 x N of hash for 30 days: USD 17,100 per GH/s of network | | subsidy | at rental equilibrium about 33 percent of 30 days of subsidy net |
| any share | a rule change | 95 percent of blue-block weight over the window, or the floor | 19 N for 24 h today (USD 534k at 100 GH/s); x7 under rank 3 | | |
What no share buys: a block the nodes do not re-execute (the native-execution veto), a state the aggregated proof chain does not commit to, a lock past a certificate under two thirds of weight, a parameter change without the signal.
The residual risks stated plainly: the first 20 days (no weight table yet); a pause after a sudden departure of a third of weight (30 days under v3 until rank 2 lands); the Sybil count of keys (X5's definition adopted, the measurement scheduled); the proving pool until rank 1; the 2/3-of-total rule under long churn (the window bound: a 50/50 honest partition locks on both sides from day 10).
## 4. Per lane: the three biggest findings
### Lane 1, consensus-security
1. The lock bounds a majority; the k-cluster does not (45 to 51 percent wins a 90-s race 70 to 85 percent of the time; the lock at 63 to 93 s is the bound).
2. The veto is cheap (20 days of 51 percent: USD 6k at 1 GH/s, 5.8 M at 1 TH/s, half earned back) and the pause is then free.
3. The proving pool is capturable today (11,636 IGN/h at 51 percent) with a correct-statement fake-proof record; the only attack that earns more than it costs.
### Lane 2, algorithm
1. Class v4's verifier measured on a 2022 server core: 4.90 / 5.06 / 8.23 ms (steady / cold / half-core); dr736 9.76 / 10.51 / 15.49, out; R0 is dr368.
2. The f = 1 GDDR7 chip: 5.7x per joule against the 5090 at v3, 2.1x at v4 with k = 1; USD 0.00021 per MH/s-hour against USD 0.0117 rented; the FPGA soft overlay 0.30x to 0.47x per watt.
3. The reserve and the era draw buy about nothing against a chip; the N ladder in the era draw at genesis is the lever, and lane 7's HBM4 column is reconciled with three named disagreements.
### Lane 3, finality-and-weight
1. Tonight's pause: the 2/3 rule at checkpoint 6843 (53.1 percent of total after a 42.7 percent departure), then the frozen table (Q5) holding it for a window; locks formed without the hub; topology refuted.
2. Only the departure announcement passes the line (0 conflicting locks everywhere, the pause under an hour); the decaying denominator, the hysteresis floor and the two-tier rule fail or are reports.
3. Weight capture costs USD 8,424 x N x W/(1 - W) to rent for the window: the veto 0.52 N for 30 days (USD 4,300 per GH/s of network), a lock alone 2.03 N (USD 17,100); lock delay measured p50 0.86 to 1.26 s at 4 to 93 voters; the path breaks near 1,000 voters on per-vote verification.
### Lane 4, economy-and-utility
1. The proving price is h/N: 100 to 300x Boundless at 1.16 GH/s, 0.2 to 0.4x at 100 GH/s beside the miner; the adopted floor overprices above USD 0.014 per IGN.
2. Fees are not a security budget for a decade (USD 450 a day at launch, 4,200 in year 5, against 54,800 and 13,700 of emission); sustained honest hash costs USD 24.8 per GH/s-day against USD 281 rented, so the 20-day veto costs 11.8x the honest fleet at every price and year.
3. The 80/20 survives every stress but a ten-day prover refusal, which strands 547,570 IGN a day of pool credit with no rule to return it.
### Lane 7, frontier
1. HBM4 raises the stored-dataset chip's edge (lane 2 bounds the figure at 4.9x to 12x bare by tFAW); the N schedule belongs in the era draw at genesis.
2. Vote weight is already a slashable, non-purchasable bond: work-stake for external jobs, with "no coin stake" written into the spec first.
3. The consensus proof can be incremental: the weight table inside the recursive segment proof, one mergeset per segment; the first measurement is the in-guest BLS verify and colouring cycle counts.
### Lanes 5, 6, 8
(Filled when they land.)
## 5. What was not run, and why
| Item | Lane | Why |
|---|---|---|
| The in-guest BLS verify and GHOSTDAG colouring cycle counts (rank 12's first gate) | 7, 3 | no 24 GB card free tonight: PC 2 and the fleet were on class v4 and Devnet 2 |
| The real 2019-class core (O-1.14) | 2 | the US laptop was not on the relay; the box's half-core proxy stands in |
| The FPGA soft overlay on real HBM2 | 2 | no FPGA in the fleet; AWS F2 plan written |
| The AMD watts at every N | 2 | the ADLX sampler row is owed on the runner's `--cards-off` mechanism (counter-asic-3-status 6a) |
| A `cargo bench` of `fast_aggregate_verify` at 93, 1,000 and 8,192 keys | 3 | the BLS figures are arithmetic on blst's published timings, approximate |
| The 10 bps runs A2 (mesh) and B (control) | 5 | landing during the night; lane 5 reads them before it closes |
| The full 30 M-cycle shard beside the miner on any tier; the hash loss while proving on Ampere and Ada | 4 | the fleet measured the 4.7 M fixture only |
| Market prices for Taiko-class batch proving and Bonsai | 4 | not published; marked approximate |
| The observed end of tonight's pause | 3 | expected at DAA 216,402, about 20:40Z; the timeline closes when the observer rows show the lock |
## 6. Rules and corrections for main
| # | Rule or correction | Lane | State |
|---|---|---|---|
| 1 | P21 is a priced economic hole until the in-consensus verifier lands; the 20 percent pool is not "paid to provers" without the caveat | 1 | relayed |
| 2 | The one-day P2 signalling window is buyable for a day; seven consecutive windows | 1, 4 | relayed |
| 3 | The DAG controller reads blue work only, so a withholder or a star eases difficulty 16 to 32 percent (run A's loop) | 1, 5 | relayed; lane 5 owns the fix |
| 4 | No `unwrap` on a peer-driven path; the sibling list; a sync-request fuzz in `tools/ci` | 1 | relayed |
| 5 | A standing box never leaves the live chain for an experiment; orchestrated departures over 10 percent staged under 10 percent an hour | 3 | relayed; the fleet library refusal is rank 8 |
| 6 | Under rule v3 any sudden departure of a third of weight is a 30-day mainnet pause; the leave item is the one safe way to shorten it | 3 | relayed |
| 7 | F19's "stalls until day 19 to 20" is a v2 number; under v3 a full window | 3 | relayed, ledger text owed |
| 8 | "No coin stake; the only thing at stake is 30 days of public work" into spec 03/05 and the litepaper before any work-stake prototype | 7 | relayed; routed to the site-miner agent |
| 9 | The litepaper's "proving: a second income" carries the USD 36 a day arithmetic | 7 | relayed; routed |
| 10 | Ember's updater installs nothing while finality is paused | 7 | relayed; sent to the Ember agent for 0.3.16 |
| 11 | The customer brief's "priced in dollars per proof" and the litepaper's "proofs at the cost of power" are conditioned on network hash | 4 | relayed |
| 12 | The three signalling thresholds (60 / 90 / 95) in one sentence across spec 5.7, CLAUDE.md and the litepaper | 4 | relayed |
| 13 | The spec is silent on pool credit nobody claims; today it is a burn nobody decided | 4 | relayed |
| 14 | funding.md section 4 overstates the dev-fee ceiling by a quarter | 4 | relayed |
| 15 | Any future miner-signalled execution parameter outside the consensus digest makes signal-then-defect a quiet state fork | 4 | relayed |
| 16 | The era draw and the reserve are not unpredictability against a chip; the public text should say what they buy | 2 | relayed |

View file

@ -0,0 +1,244 @@
# Horizon lane 5: network. Block rate, propagation, node cost, bandwidth, pruning and the snapshot path
6 October 2026, evening UK. Lane 5 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon`, branch `horizon`. Scripts in `sim/horizon/network/` (README there). Nothing live was touched: the Devnet 2 seed log on igneum-build-1 and `~/Desktop/fleet/bps/A.jsonl` were read, never written.
What was read: `docs/spec/02-consensus.md` (2.1 parameters, 2.3 the difficulty rule, 2.4 header, 2.5 emission), `08-client-security.md`, `10-light-client.md`; the fud-close worktree's `docs/spec/03-finality.md` C1 and 3.4.2 (vote item 281 bytes, the bitmap and per-block bounds); `docs/fud-ledger.md` M20, M21 (block sizes, k re-derived, the 490 KB body run), X20, M30; `docs/bench-log.md` entries "4 October 2026, cloud devnet" (inter-region RTT and propagation), "ledger M30" (RSS, the 256 MiB cache, the s8 steady slope), "6 October 2026, 12:25 to 13:20Z, the finality route" (the seed as the only peer, the route overflow), "Rental cost of hash, 6 October 2026"; `docs/plans/hands-on-build-1.md` (node 1 and the observer: data dirs 856 and 822 MB, the 127.6 MB exec snapshot), `seed-nodes.md`, `cloud-devnet.md`, `release-0.3.14.md` (the snapshot path and the restart pin), `release-0.3.13.md` 4a; the fleet worktree's `docs/analysis/block-rate-devnet2.md` (read at 19:5xZ and again at 20:1xZ: RUN_A, RUN_B, TIER_TABLE and RECOMMENDATION are still placeholders), `tools/fleet/bps-collect.py`, `box-dn2.sh` (one `--addpeer=<seed>`: the star), `devnet2-override.json`, `docs/bench-log.md` of that branch; the live record of run A: `~/Desktop/fleet/bps/A.jsonl` (43 rows, 19:18 to 19:51Z), `collect-A.log`, `runA-start2.log`, `runB.sh` (run B starts at 20:25Z) and the seed's log `/home/build/dn2seed-A.log` on igneum-build-1 (20,529 lines, 19:17 to 19:59Z, read over ssh); `vendor/rusty-kaspa` `consensus/core/src/config/bps.rs` (the k table, `calculate_ghostdag_k`, parents, mergeset, pruning depth), `constants.rs` (delay bound 5 s, delta 0.01, DAA window), `params.rs` (mainnet `BlockrateParams::new::<10>()`, Crescendo activation 110,165,000), `consensus/src/processes/difficulty.rs` and `window.rs` (the window holds every mergeset block, blue and red), `docs/crescendo-guide.md` (10-bps node requirements); the fork `vendor/igneum-node` (read-only) at `release-0.3.14-node`: `consensus/core/src/igneum.rs` `difficulty` (CAP_BLOCKS 20, the sanitised clock, the clamps), `consensus/src/processes/difficulty.rs` `igneum_difficulty_bits` (blue-work steps on the selected chain), `consensus/src/processes/finality.rs` (MAX_VOTES_PER_BLOCK 48, KEEP_CHECKPOINTS 2,000), `igneum/exec/src/proving.rs` `p2p_snapshot_gate` and `on_exec_snapshot`, `igneum/exec/src/snapshot.rs`; the fork branch `devnet2-bps` 1279a1d6 (`IGNEUMD_DEVNET_BPS`); lane 3's `finality-and-weight.md` sections 5.5 and 8 (the aggregation path tonight), lane 4's `sim/horizon/consensus-security/ghostdag_results_{1,10}bps.md`.
## 1. The question and the answer in one paragraph
the project lead asked for Kaspa's answer to solo-miner variance: a higher block rate. Run A ran Devnet 2 at 10 blocks per second through one seed and produced 77 percent red blocks, 321 tips and a 55-block reorg. The propagation model says the links and the star did not do that: with the measured latencies it predicts under 0.1 percent red at 10 bps in a star and in a mesh. What did it is the seed's CPU per block, measured at 61 ms (narrow DAG) to 345 ms (mergeset 150 to 200), against a budget of 100 ms per block at 10 bps; with that cost in the model the star gives 47 to 88 percent red and queueing waits of 26 to 1,769 s, which are the "Accepted 100 blocks via relay" batches in the log. The difficulty rule then read blue work over a chain step capped at 2 s and hardened until the DAG ran at 2 s / (chain-step spacing) of target (model 4 blocks/s at a 5-s spacing, record 3.3 to 3.6), while a narrower-but-still-wide DAG would have made it ease (the direction main reported); counting every mergeset block over the real span, as Kaspa's window does, is unbiased in both regimes. The block rate for the public testnet is 1 bps; 10 bps is a gated step that needs the per-block node cost under 50 ms on a laptop core at a mergeset of 248, the checkpoint interval and the clock cap re-denominated in DAA seconds, and vote aggregation, because with C1 in blue blocks 8,192 voters at 10 bps are 66 GB per node per day of votes.
## 2. Method
| Step | What | Where | Machine, lock |
|---|---|---|---|
| Record | run A's per-minute rows (blocks, blue score, tips, peers, exec tip, CPU, RSS, bytes), the seed log's `PoW accepted` per minute, `Processed` lines (parents, mergeset per 10 s), `Finality: checkpoint N determined` (blue score against DAA), reorg lines, route drops | `~/Desktop/fleet/bps/A.jsonl`; `/home/build/dn2seed-A.log` on igneum-build-1 | read only |
| Propagation | event simulation: Poisson production, star or mesh, lognormal links, inv/request/block hops, a hub (or every node) as a single server with a per-block cost, GHOSTDAG colouring with the first k-cluster condition, parent and mergeset caps, reorg depth at miner 0 | `sim/horizon/network/propagation.py`, `results.md`, `results-2.md` | Mac, `with-lock.sh run nice -n 19`, seed 7 |
| Controller | the fork's estimator against a wide DAG in closed loop with the 3% / 10% clamps, against the whole-DAG estimator and a red-corrected blue estimator | `controller.py`, `controller-10bps.md`, `controller-1bps.md` | Mac, seed-free (deterministic) |
| Arithmetic | k and parameters per rate, votes and bounds, bytes per node per day, CPU budget, RSS and disk, pruned and archival growth, subsidy and payout intervals, finality timing, light-client bytes | `cost.py`, `cost-tables.md` | Mac |
Nothing was built. No node ran. The fleet's run B (1 bps control, starts 20:25Z) and the mesh variant A2 (not scripted in the fleet worktree at 20:1xZ: no `mesh` or `A2` in `tools/fleet/` or its plans) had not landed when this file closed; section 7 names what they owe.
## 3. Evidence
### 3.1 Run A, measured (igneum-devnet-2, 10 bps, 41 miners through the seed on igneum-build-1, genesis bits 505413632)
Phases from the seed log's checkpoint series (blue score against DAA score; red share = 1 - blue / DAA over the interval) and `Processed` lines; CPU per block from `A.jsonl` `cpu_rss` (ps lifetime %CPU times elapsed, differenced) over the accepted-block deltas.
| Window (Z) | Production at the seed, blocks/s | Blue rate, blocks/s | Red share over the window | Mergeset per block (Processed) | Parents | Tips at the seed | Hub CPU per accepted block | Hub RSS | What the log shows |
|---|---|---|---|---|---|---|---|---|---|
| 19:23 to 19:29 | pods join (peers=1 each, blocks=0 synced=false on some), 26 blocks/s peak at 19:30 | | 33% (cp 1 to 29) | 1 to 35 | 1 to 16 | 6 to 30 (pods) | 61 ms | 0.45 to 1.9 GB | the 55-block reorg at 19:27:41Z "unwinding to height 1" (a late-joining pod's chain from genesis) |
| 19:29 to 19:31 | 19 to 26 | 1.3 | 77% (cp 29 to 37) | 29 to 36 | 15 | | 115 ms | 1.9 to 2.5 GB | |
| 19:31 to 19:37 | 13 to 19 | 0.7 | 94% (cp 37 to 45) | 53 to 197 | 12 to 15 | 295 | | 2.5 to 4.0 GB | `Accepted 97 / 100 / 31 blocks ... via relay` batches; headers ahead of blocks |
| 19:37 to 19:41 | 8 to 14 | 0.5 | 95% (cp 45 to 49) | 150 to 190 | 12 | 218 to 295 | 345 ms (19:37 to 19:51 mean) | 4.0 to 4.5 GB | window filled at 19:37Z; first lock cp 47 at 19:40:08Z |
| 19:41 to 19:47 | 3 to 7 | 1.0 | 50 to 88% (cp 49 to 61) | 80 to 125 | 13 to 15 | 230 to 350 | | 4.6 to 5.2 GB | `incoming route for IgneumFinality is full, message dropped ... the peer stays`: 109 to 2,547 drops per peer by 19:56Z |
| 19:47 to 19:59 | 3.3 to 3.7 | 1.4 to 1.8 | 41 to 56% (cp 61 to 92) | 47 to 90 | 15 to 16 | 348 to 396 | | 5.4 GB at 19:51 | 16 of 47 checkpoints after the window filled LOCKED (cp 47 to 79); cp 61 determined 19:47:00, locked 19:47:18 |
| Cumulative to 19:59:32Z | 6.1 (DAA 12,233 in 2,012 s) | 1.38 (blue 2,786) | 77.2% | | | | | | main's 12.4 blocks/s, 77 percent, 321 tips, max reorg 55 are the 19:3xZ to 19:45Z readings of the same record |
Other measured rows of the run: 421 selected-chain reorgs at the seed (132 of depth 1, 62 of 2, 42 of 3, 25 of 4, 19 of 5; 2 of 43, 1 of 44, 1 of 55); the seed's `rx_tx` counter (netns-wide, so an upper bound) 325 MB in and 2,133 MB out between 19:37:05 and 19:51:15Z (850 s, 4,284 accepted blocks): 2.5 MB/s out, 61 KB/s per peer, about 12 KB per block per peer (a block with its finality section of up to 48 votes at 281 bytes is about 14 KB); exec tip 154 chain blocks at 19:51Z against 11,239 blocks (the executor follows the selected chain, which advanced about one chain block per 10 s at the widest). The run's difficulty values are not in the record: `bps-collect.py` strips `difficulty=` from the watch line before storing it and the node log carries no bits; main's statement that the rule "lowered difficulty" is therefore unverified here, and the production curve (26 to 3.5 blocks/s at a hash the pods' peers=41 say stayed connected) is the measured fact section 5.4 reads.
### 3.2 Measured propagation and block sizes (the inputs the model takes)
| Quantity | Value | Source | Label |
|---|---|---|---|
| Inter-region RTT | 35 ms (hel1-fsn1) to 289 ms (sin-ash); 0.4 to 0.7 inside a location | bench-log, cloud devnet 4 Oct | measured |
| Block propagation to 80% of 12 nodes over about 3 hops, 723-byte bodies | p50 343, p90 497, p99 666, max 2,313 ms | same | measured |
| One relay hop on 100-ms proxied links (inv, request, block) | p50 318 to 329 ms; own-node processing 6 to 16 ms | fud-ledger M21 run, 5 Oct | measured |
| Two hops with 490 KB bodies | p50 641, p99 812, max 857 ms (59-byte bodies: 626 / 1,093 / 2,099) | same | measured |
| Live devnet block, 1 coinbase, 22 keys' votes partly | p50 723 B, p90 1,022, max 6,908; coinbase payload p50 395, max 6,580 | fud-ledger M21 sweep | measured |
| Vote item, certificate, proof record | 281 B; 273 B + V/8; 274 B | spec 03 3.4.2 (fud-close), fork | cited |
| k from the measured delays at 1 bps | p99 0.67 s gives k 5, max 2.3 s gives k 10; k 18 is the 5-s bound | fud-ledger M21, `calculate_ghostdag_k` | cited |
| Kaspa 10-bps node requirements | 8 cores, 16 GB RAM, 256 GB SSD, 40 Mbit/s minimum; 12 to 16 cores, 32 GB preferred | `vendor/rusty-kaspa/docs/crescendo-guide.md` | cited |
### 3.3 Measured node figures
| Quantity | Value | Source | Label |
|---|---|---|---|
| PoW cache | 256 MiB per day key, KEEP_DAYS 3, 768 MiB worst, 512 MiB around midnight UTC | bench-log M30 entry | measured |
| RSS slope, narrow DAG, 60x fast time, 1 block/s | 30.2 MB per 1,000 blocks (s8 steady, 1,500 blocks) | same | measured |
| The live app node on the devnet profile | 1,081 MB at 27 min, 2,258 MB at 4 h 14 min (about 80 KB per block, derived) | same | measured, derived |
| Run A's seed | 153 MB before the chain, 5.42 GB at 12,000 blocks (about 440 KB per block; 41 peers, mergeset up to 197) | A.jsonl | measured |
| Exec snapshot | 127,564,588 B at tip 141,700 chain blocks (0.9 KB per chain block); `ExecState.records` 1 to 2 KB per chain block | hands-on-build-1.md; M30 note | measured; approximate |
| Node 1 and observer data dirs after 3 days | 856 MB and 822 MB | hands-on-build-1.md | measured |
| Lottery verify per header | class v3 2.79 ms on a loaded M5 Max core; class v4 4.90 steady, 5.06 cold, 8.23 ms on a half core (box proxy); gate 10 ms | consequences C29; lane 2 section 5.5; spec 01 | measured |
| Hub CPU per accepted block at 10 bps | 61 ms (mergeset about 8), 115 ms (about 30), 345 ms (150 to 200) | A.jsonl, section 3.1 | measured |
### 3.4 What the fleet still owes (read `block-rate-devnet2.md` at 20:1xZ)
RUN_A, RUN_B, TIER_TABLE and RECOMMENDATION are placeholders. Run B (1 bps, same boxes, fresh genesis, 30 min) starts at 20:25Z by `runB.sh` and its rows land about 21:00Z; the mesh variant A2 is not scripted (`box-dn2.sh` takes one `--addpeer`). The collector's `red` field is 0 in every row (its grep pattern matches no log line), so the fleet's red share must come from blue score against DAA as section 3.1 does; its `--report` payout arithmetic uses 28 MH/s for a 4070 where the brief uses 25. Section 5.2 states this lane's prediction for A2 before its rows land.
## 4. Model
Inputs are labelled measured (M), cited (C), simulated (S) or approximate (A).
1. **k and the parameters per rate** (C, `bps.rs`): k = min k with P(Poisson(2 D lambda) > k) < 0.01 at D = 5 s: 18, 124, 362 at 1, 10, 32 bps; 1,074 at 100 bps (the table stops at 32; `calculate_ghostdag_k` in f64 underflows at x = 1,000, the lane's `cost.py` does it in log space). Parents = clamp(k/2, 10, 16); mergeset limit = clamp(2k, 180, 512); merge depth 3,600 bps; finality 43,200 bps; pruning max(108,000 bps, the Prunality lower bound); maturity 100 bps.
2. **Red share from topology and delay** (S, `propagation.py`): blocks arrive as Poisson(bps) split over miners; a block reaches a peer after one hop = 3 lognormal link latencies (median `--link`, sigma 0.29 from the cloud devnet's p50/p90) plus 10 ms processing; a star hub (or every node, `--node-s0`) is a single server with service s0 + s1 x mergeset ms and a FIFO queue; each block's colour is GHOSTDAG's first k-cluster condition over its own past (deterministic given parents); red share = reds in the mergesets of the final selected chain over merged blocks. The abstract rule behind it: reds appear when blocks in flight 2 d lambda exceed k, where d is the effective delay including queueing.
3. **Hub queue** (A, M/M/1 reading): utilisation rho = lambda x s; the knee is rho = 1: s = 100 ms at 10 bps, 1 s at 1 bps, 31 ms at 32 bps, 10 ms at 100 bps. Above the knee the wait grows without bound and d becomes the wait, not the link.
4. **The controller against a wide DAG** (C for the rule, `controller.py` for the loop): the fork walks the selected chain; a step carries work = blue_work(b) - blue_work(selected parent) (the mergeset blues only) and solvetime = min(clock step, 20 T) (`igneum.rs` CAP_BLOCKS, `difficulty.rs` `igneum_difficulty_bits`). With chain-step spacing sigma = max(1/lambda, d), mergeset m = lambda sigma and blues = min(m, k + 1): estimate / true hash = (blues / m) x (sigma / min(sigma, cap)). Two biases: when sigma > cap the step is capped and the rule over-reads by sigma / cap and hardens to a DAG rate of target x cap / sigma; when the DAG is wide (m > k + 1) and sigma <= cap it under-reads by (k + 1) / m and eases. Kaspa's window (`window.rs` `push_mergeset`: every mergeset block above the blue-score floor, blue and red; `difficulty.rs` `calculate_difficulty_bits`: average target x measured span / expected span) reads the whole-DAG rate over the real span: estimate / true = 1 in both regimes. A blue estimator divided by (1 - observed red share) is the same quantity.
5. **Bytes per node per day** (C+S+A, `cost.py` section 3): blocks per day x (header 286 + 32 x parents + body 300 + 274 / 8) + relay overhead (degree x 40 B inv + 40 B request per block, Kaspa's blockrelay flow, A) + votes V x checkpoints per day x 281 + one certificate (273 + V / 8) per block. Checkpoints per day = 2,880 x bps if C1 stays "every 30 blue blocks", 2,880 if it is re-denominated in DAA seconds.
6. **CPU budget** (A): the header pipeline validates in order, so per-block cost x bps must stay under about 0.8 core: 800 ms at 1 bps, 80 at 10, 25 at 32, 8 at 100. Cost = lottery verify (M) + BLS per carried vote (A 1.5 ms, unmeasured on this stack, O-10.3) + GHOSTDAG and reachability (O(k x mergeset) store reads; M at the hub only as a total) + exec and record checks.
7. **RSS and disk** (M slopes, A steady state): caches 768 MiB worst plus the DAG store's growth per block (30, 80 or 440 KB measured in three settings) over the pruning window; disk per day = blocks x bytes + votes + exec records; archival = the same per year without pruning.
8. **Payout and subsidy** (C): subsidy per block = 31.688 IGN / bps at full ramp, 80% to the producer; interval = 1 / (bps x miner hash / network hash).
9. **Finality timing** (C, spec 03 C1): checkpoint every 30 blue blocks, determination d = 60 blue (placeholder, 20 on the devnet), lock about 3 s after determination: cadence 30 / bps s, lock (60 / bps + 3) s if C1 and d stay in blue blocks.
## 5. Results
### 5.1 Propagation alone: red share and reorg depth per rate and delay (mesh of degree 8, 42 nodes, ideal nodes; `results.md` section D)
| bps | k | hop delay median ms | red share | mergeset mean / max | tips mean | reorg max / p99 at miner 0 | delay to 90% of nodes, s |
|---|---|---|---|---|---|---|---|
| 1 | 18 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 1.1 / 2 to 2.3 / 7 | 1.1 to 2.4 | 1 / 1 to 2 / 2 | 0.09 to 1.82 |
| 10 | 124 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 1.7 / 6 to 9.2 / 22 | 1.7 to 8.1 | 2 / 1 to 4 / 3 | 0.09 to 1.82 |
| 32 | 362 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 2.9 / 9 to 18.8 / 42 | 2.9 to 14.4 | 3 / 2 to 6 / 5 | 0.09 to 1.82 |
| 100 | 1,074 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 6.0 / 17 to 31.1 / 104 | 5.6 to 25.3 | 4 / 3 to 8 / 8 | 0.09 to 1.82 |
Reading. Kaspa's k is derived for a 5-s delay bound; at the measured delays (under 1 s per hop, under 2 s to 90 percent of nodes) blocks in flight stay far under k at every rate and no block turns red. The natural reorg depth is the DAG width: 2 to 4 chain blocks at 10 bps, 8 at 100 bps with 1-s hops. Lane 4's simulator (uniform one-way delay to every node, 8 miners) reads p99 11 at 10 bps with d = 0.35 s and 99 with d = 2 s (`ghostdag_results_10bps.md` section 1); the two agree in shape (depth grows with bps x d) and differ in the delay model, so the fleet's measured reorg distribution at 10 bps (section 3.1: 421 reorgs, p50 1, 4 over 40) is the arbiter: outside the start artefact and the saturated phase it sits at 1 to 8, inside this lane's model. Kaspa's published 10-bps red rates are not in the clone (`docs/crescendo-guide.md` carries requirements only); from the k derivation the design red rate is under 1 percent of blocks (delta 0.01 on anticones, approximate), which both simulators reproduce.
### 5.2 Run A predicted from topology, then from the hub's cost (`results.md` A to C, `results-2.md` E, F, I)
| Model input | Red share | Hub wait | Hub utilisation | Mergeset mean / max | Reorg max | What it says |
|---|---|---|---|---|---|---|
| Star, 41 miners, link 40 ms, ideal hub, run A's measured production schedule, join spread 360 s | 0.0% | 0 | 0 | 3.5 / 14 | 4 | topology and latency alone predict no reds at 10 bps |
| Star, hub cost 6 + 2 x mergeset ms (a linear GHOSTDAG cost), same schedule | 0.0% | 0.00 s | 0.14 | 3.7 / 15 | 3 | a hub under 20 ms per block keeps up |
| Star, hub cost 61 ms per block (M, mergeset 8), same schedule, 1,200 s | 46.8% | 26 s | 0.65 mean (1.6 during the 26 blocks/s burst) | 8.8 / 194 | 43 | the genesis burst alone saturates a 61-ms hub for 5 minutes |
| Star, hub cost 115 ms (M, mergeset 30) | 88.1% | 321 s | 1.23 | 20.7 / 199 | 2 | the measured mid-run regime |
| Star, hub cost 345 ms (M, wide DAG) | 82.6% | 1,769 s | 3.66 | 9.8 / 45 | 3 | relay in batches, most blocks unmerged at the end |
| Measured run A, cumulative | 77.2% | relay batches of 100 blocks | 180% of one core sustained (section 3.1) | 8 to 197 | 55 (start artefact) | |
| Star at a constant 10 bps, hub cost 20 / 40 / 60 / 80 ms | 0.0% | 0.00 / 0.01 / 0.05 / 0.18 s | 0.20 / 0.39 / 0.61 / 0.81 | 3.5 to 4.9 | 2 to 4 | under the knee |
| the same, 100 / 150 ms | 15.2% / 75.2% | 8.5 / 157 s | 1.01 / 1.51 | 20 / 116 and 27 / 66 | 8 / 3 | the knee is 100 ms per block at 10 bps |
| Star at 10 bps, ideal hub, link 100 / 200 / 300 ms | 0.0% | 0 | 0 | 5.8 / 15, 10 / 20, 12.4 / 23 | 3 to 4 | pure latency up to a 2.2-s delay to 90% of nodes gives no reds |
| Star with the 26 blocks/s burst for 5 min then 10 bps, hub 6 + 2 m | 0.0% | 0.01 s | 0.33 | 5.4 / 17 | 3 | the burst alone is harmless to a cheap hub |
So the model predicts run A's red share from the hub's measured per-block CPU and not from its topology: 47 to 88 percent against 77 percent measured, with the waits that the log's relay batches show. The prediction for the mesh variant A2 (`results-2.md` G): a mesh of degree 8 whose nodes each pay 40 or 80 ms per block runs 10 bps at 0 percent red (node wait 0.01 and 0.12 s); at 115 ms per block every node is its own hub and the mesh goes to 52 percent red with 26-s waits and reorgs of 16. The pods run the same binary as the seed on smaller CPUs, so A2 with tonight's genesis bits (a 26 blocks/s burst) is predicted red again; A2 with genesis bits set for 10 bps at 2 GH/s and every node started synced is predicted under 5 percent red if the per-block cost on a pod is under 80 ms, and 50 percent or more if it is 115 ms. The control at 1 bps (`results-2.md` H): 115 or 345 ms per block gives 0 percent red and waits of 0.01 to 0.07 s, which is the live devnet's experience.
### 5.3 What the hub's 61 to 345 ms per block is made of (the breakdown is not measured; the candidates and their bounds)
| Component | Per block at 10 bps | Label | Note |
|---|---|---|---|
| Lottery verify, class v3 (the Devnet 2 override activates v3 at epoch 1) | 2.8 ms | M | one warp per header |
| BLS verification of carried votes, up to 48 per block | up to 72 ms at 1.5 ms per verify | A | run A's blocks carried up to 48 of 42 voters' votes; the route drops say the finality path was the hot one |
| GHOSTDAG and reachability at mergeset m, k 124 | O(k x m) store reads: 1,000 at m 8, 25,000 at m 200 | C (protocol.rs shape) | the 61 to 345 ms rise tracks m |
| Relay to 41 peers (serialise 14 KB x 41, inv handling) | 2.5 MB/s out measured | M | a mesh node of degree 8 does one fifth of it |
| Exec of chain blocks, record checks | small: one chain block per 10 s at the widest | M | |
The gate that settles it is a profile (proposal 5). Whatever the split, the serial budget rule of section 4.6 is the design constraint for every block-rate step: at 10 bps the whole per-block path must stay under 80 ms on the slowest node the network wants to keep, at the mergeset limit 248, not at the narrow-DAG average.
### 5.4 The controller: what the record shows and what the model says (`controller-10bps.md`, `controller-1bps.md`)
| Regime (10 bps, k 124, cap 2 s) | Fork's estimator (blue work over the capped chain step) | Whole-DAG estimator (Kaspa's window shape) | Blue estimator corrected by (1 - r) |
|---|---|---|---|
| d = 0.3 / 1 / 2 s | DAG rate 10.00, red 0%, estimate 1.00x | 10.00, 1.00x | 10.00, 1.00x |
| d = 3 s | settles at 6.67 blocks/s, estimate 1.50x, difficulty 1.50x the correct value | 10.00 | 10.00 |
| d = 5 s | 4.00 blocks/s, 2.50x | 10.00 | 10.00 |
| d = 10 s | 2.00 blocks/s, 5.00x | 10.00 (red 0%, mergeset 100) | 10.00 |
| d = 30 s (a saturated hub) | 5.21 blocks/s, red 19.5%, estimate 12.1x, difficulty 1.93x | 10.00, red 58%, mergeset 300 | 10.00 |
| 1 bps, k 18, cap 20 s: d = 20 / 30 / 60 s | runs away upward: 179 / 43 / 10 blocks/s, red 97 to 99%, estimate 0.01 to 0.09x (the under-read, main's direction) | 1.00 / 1.00 / 1.17 blocks/s | 1.00 / 1.00 / 1.17 |
Reading against the record. Run A's production fell from 26 blocks/s to 3.3 to 3.6 while blue stayed 1.4: the DAG hardened. The fork's rule at a chain-step spacing of 5 to 6 s (the hub's queue made chain blocks 10 to 60 s apart at the widest, 3 to 6 s late in the run) settles at target x 2 s / spacing = 3.3 to 4 blocks/s, which is the record's late plateau. The ease direction main reported is the other bias (blue work only) and the model shows it where chain steps stay under the cap while the DAG is wider than k + 1: at 1 bps with d of 20 s or more, or at 10 bps when production exceeds k / (2 d). Either way the rule is reading the wrong quantity: a controller that counts every mergeset block's work over the real span (what Kaspa's `calculate_difficulty_bits` does over its sampled window of blue and red mergeset blocks) holds 10.00 in every cell. A second inconsistency at 10 bps: CAP_BLOCKS 20 is 2 s while FUTURE_TOLERANCE_MS and BACK_TOLERANCE_MS stay 10 s, so a forged stamp (10 s) no longer fits inside half a cap; spec 2.3 derived the 10 s as half the cap at 1 bps. The cap, the tolerances and the 60 T lag bound should be denominated in DAA seconds (20 s, 10 s, 60 s) at every rate, and the step of a chain block that merges m blocks should be allowed m x 20 T before clamping.
Cost of the bug tonight per tier: a home miner's blocks were 77 percent red (a red inside the DAA window pays its 80% to the merging miner, spec 2.5, so the solo miner lost the subsidy of 3 blocks in 4); a rig the same; a pool user nothing (Devnet 2 is a staging chain); the node operator saw 5.4 GB RSS and 180% CPU on a 96-thread box; a prover saw the exec tip at 154 chain blocks against 11,239 blocks; a holder nothing (no value on Devnet 2).
### 5.5 Node tiers per block rate (`cost-tables.md` 4 and 5)
| Rate | Per-block CPU budget (0.8 core) | Class v4 verify share of it (box proxy 4.9 ms; a 2019 laptop core about 2.5x, lane 2's rule: 12 ms) | Votes per block (V/30 if C1 stays in blue blocks) and their BLS cost at V = 1,000 | RSS: caches + DAG store over the 30-h window at 30 / 80 KB per block | Disk per day (blocks, votes at V = 1,000, exec records) | Verdict: laptop 2019-class 8 GB SATA | Raspberry-class 8 GB USB SSD |
|---|---|---|---|---|---|---|---|
| 1 bps | 800 ms | 0.6% (laptop 1.5%) | 33 votes, 50 ms | 0.77 + 3.3 to 8.6 GB | 0.92 GB | runs if the DAG store plateaus under about 4 GB (owed: the 30-h measurement); marginal at 80 KB per block | the same question; CPU fine (class v4 verify under 15 ms, GHOSTDAG at mergeset 1 to 2) |
| 10 bps | 80 ms | 6% (laptop 15%) | 33 votes, 50 ms: 62% of the budget on its own | 0.77 + 33 to 86 GB | 9.0 GB | no: the hub's measured 61 ms at a narrow DAG already uses 76% of the budget on a server core; RSS over 8 GB within hours unless the per-block footprint falls 10x | no |
| 32 bps | 25 ms | 20% (laptop 48%) | 33 votes, 50 ms: over budget | 0.77 + 104 to 276 GB | 29 GB | no | no |
| 100 bps | 8 ms | 61% (laptop 150%) | over budget | 0.77 + 326 to 864 GB | 91 GB | no: the verifier gate alone forbids it | no |
Pruning changes the disk, not the RSS and CPU: the pruned node keeps the 108,000 DAA-s window (136 MB of headers and bodies at 1 bps, 1.55 GB at 10 bps, `cost-tables.md` 6) plus the UTXO set, the exec state (128 MB measured plus 1.5 KB per chain block) and the finality store's 2,000 indices (KEEP_CHECKPOINTS). What must change for 10 bps is the per-block footprint in RAM (30 to 440 KB measured; Kaspa's 16 GB minimum at 10 bps says their footprint is near 10 KB per block, derived from 16 GB over 1,080,000 blocks, approximate) and the per-block CPU (under 50 ms at mergeset 248 on a laptop core).
### 5.6 Bandwidth per node per day (`cost-tables.md` 2 and 3)
| Rate | Blocks (header + body + records) | Relay overhead (degree 8) | Votes at V = 12 / 100 / 1,000 / 8,192, C1 in blue blocks | Certificates at V = 12 / 8,192 | Total at V = 100 | Total at V = 8,192 | Mean Mbit/s at 8,192 |
|---|---|---|---|---|---|---|---|
| 1 | 57 MB | 31 MB | 10 MB / 81 MB / 809 MB / 6.6 GB | 24 MB / 112 MB | 194 MB | 6.8 GB | 0.63 |
| 10 | 649 MB | 311 MB | 97 MB / 809 MB / 8.1 GB / 66 GB | 237 MB / 1.1 GB | 2.0 GB | 68 GB | 6.3 |
| 32 | 2.4 GB | 1.0 GB | 311 MB / 2.6 GB / 26 GB / 212 GB | 759 MB / 3.6 GB | 6.8 GB | 219 GB | 20 |
| 100 | 9.3 GB | 3.1 GB | 971 MB / 8.1 GB / 81 GB / 663 GB | 2.4 GB / 11 GB | 23 GB | 687 GB | 64 |
With C1 in DAA seconds the vote column is 81 MB / 809 MB / 6.6 GB per day at every rate; with lane 3's aggregated certificate (1.2 KB per checkpoint) in place of carried votes it is 3.5 MB per day. What carries it: a home connection (10 Mbit/s up, 50 down, approximate) carries 1 bps at any voter count and 10 bps at under 1,000 voters or with C1 in seconds; a Raspberry-class box on ethernet the same, bounded by its CPU not its link; a mobile node is a light client: 3.4 MB per day in checkpoint mode at 1 bps and 1,000 voters, 34 MB at 10 bps if C1 stays in blue blocks (`cost-tables.md` 10). A star hub pays its peer count times the block bytes in upload: run A's seed sent 2.5 MB/s (20 Mbit/s) to 41 peers; the three testnet seeds (cx23, 20 TB per month included, `seed-nodes.md`) would spend 6.5 TB per month each at that rate, inside the allowance and outside good sense; the peer floor of proposal 4 spreads it.
### 5.7 Votes and the 3.4.2 arithmetic per rate (`cost-tables.md` 2)
| Rate | Checkpoints per day (C1 in blue blocks) | Votes per block at V = 8,192 (V / 30) | Drain capacity per checkpoint at the cap 48 / 384 | Vote bytes per day at 8,192, single votes | Archival votes per year at 1,000 / 8,192 |
|---|---|---|---|---|---|
| 1 | 2,880 | 273 | 1,440 / 11,520 | 6.6 GB | 296 GB / 2.4 TB |
| 10 | 28,800 | 273 | 1,440 / 11,520 | 66 GB | 3.0 TB / 24 TB |
| 32 | 92,160 | 273 | 1,440 / 11,520 | 212 GB | 9.5 TB / 77 TB |
| 100 | 288,000 | 273 | 1,440 / 11,520 | 663 GB | 30 TB / 242 TB |
Because C1 counts blue blocks, the votes per block and the drain capacity per checkpoint are the same at every rate (the 3.4.2 arithmetic holds: 384 per block drains 8,192 in 21.3 blocks), while the bytes per day and the checkpoint cadence scale with bps: 3-s checkpoints at 10 bps, 0.3 s at 100. Tonight at 42 voters and 10 bps the seed's inbound IgneumFinality route (4,096 deep since the fin-route fix) overflowed at 109 to 2,547 drops per peer, and 16 of 47 determinable checkpoints locked; at 1 bps the same voters cost one tenth. Re-denominating C1 and d in DAA seconds (300 blue blocks and 600 at 10 bps) keeps the finality cost, the lock delay (63 s) and the light-client bytes at their 1-bps values across every rate step; it is a one-line spec change and a parameter in the fork.
### 5.8 Pruning, archival and the snapshot path
What a pruned node keeps (spec 02 2.1, `cost-tables.md` 6): headers and bodies back to the pruning point (108,000 DAA s, never past the latest certified checkpoint, F3), with their GHOSTDAG and reachability data (the 2x index factor is approximate); the pruning proof (levels of headers, `pruning_proof`); the UTXO set; the finality store's last 2,000 indices; the exec state (`exec-snapshot.bin` 128 MB measured at chain block 141,700, growing 0.9 KB per chain block, plus `ExecState.records` 1.5 KB per chain block until a window bounds it, M30 note); the proof-record window (`RECORD_WINDOW_CHAIN_BLOCKS`). An archival node keeps every block and its finality section: 40 GB per year of blocks at 1 bps (453 GB at 10 bps) plus votes as table 5.7, so the archival cost is the votes, not the blocks, until certificates replace carried votes (1.3 GB per year at 1 bps).
The p2p snapshot path as shipped in 0.3.14 (`proving.rs` `on_exec_snapshot`, `p2p_snapshot_gate`, read on `exec-sync-0313`): a peer's `ExecSnapshot` (version, chain id, genesis, tip number and hash, state root, records, the account and storage dump, fees, epoch, paid shards) is accepted only when its tip is a chain block known to this node's consensus, its tip is not 0, not below this node's exec restart, above this node's own executed tip, and only while the executor is blocked or has no state; and, when the restart pin is configured, the snapshot's record at the restart block must carry the pinned root (`exec_restart_state_root`, the fourteenth override field). The file is then written as the node's own resume point and served onward. Three things it does not check: the snapshot's state root at its tip against anything the chain commits to; the records between the restart block and the tip; agreement among peers. The poisoning attack: a peer of a blocked node (every node is blocked after a reorg deeper than its ring or after a restart below its retention root, the 6 October incident class) serves a snapshot whose tip is a real chain block above the victim's tip, whose record at the restart block is correct, and whose state at the tip is wrong. The victim loads it, executes forward from a false state, persists it, serves it to its own peers, and from then on refuses every honest proof record (`check_record`: the statement over its own records disagrees) while its own prover's records are refused by the network. Bound: it is a liveness attack on the victim and its downstream peers, not a consensus or a proof break (full nodes execute natively and a proof over the false state is a proof of the wrong statement; a light client verifies proofs against the chain's records, not against a node's state), and it needs the attacker among the victim's peers at the moment it is blocked, which the peer floor makes a 1-in-(peer count) race unless the attacker runs most of the victim's peers (an eclipse). The hardening: accept a snapshot only when its state root at the newest proven segment at or below its tip equals the `post_root` of the proof record the chain carries for that segment (the chain already carries 274-byte records in coinbase payloads, so this is a lookup, not a protocol change), and take the (tip, root) pair from N of M peers (the light client's 3 of 5, spec 10.6) before loading; a snapshot that fails either is refused and the peer is dropped for the session. Then poisoning needs a false proof record in the chain, which needs the proving key and a block that carries it, and the bound is the proof system's. Sizes per tier: the snapshot is the state (128 MB today, growing with accounts), so a home node's recovery is a 128 MB download and a sha256; an archival node's history is the table above; a light client never holds one.
### 5.9 Finality through this lane's eyes (cross-reference to lane 3)
Lane 3 (`finality-and-weight.md` 1 and 5.5) refutes the topology hypothesis for tonight's pause on the live devnet: certificates 6824 to 6842 formed while node 1 and the observer were down, the zero-aggregator fallback carried a quarter of the certificates, and the pause began at the checkpoint where signing weight fell to 53.1 percent of the frozen table, which is the two-thirds rule; the fleet's star is around the seed (the finality route entry: "on a Vast box the seed is the only peer"), not the Mac. This lane adds the measured shape of that star under load: 41 pods each with peers=1, every block and every vote through one process at 180 percent of one core, 2.5 MB/s of upload, the IgneumFinality route dropping thousands of messages per peer, 16 of 47 checkpoints locked. Lane 3's hub-cut harness case (its proposal 6) and this lane's peer floor (proposal 4) are one piece of work: the floor is what makes the harness case pass (a node with 4 outbound peers keeps blocks and votes when any one peer dies), and the fleet library is where both land.
### 5.10 Block rate: payout intervals, subsidy, lock delay and the solo-miner answer (`cost-tables.md` 7 to 9)
| Rate | Subsidy per block (full ramp), producer's 80% | 4070 (25 MH/s) at 1.16 GH/s / 100 GH/s / 1 TH/s / 10 TH/s | 5090 (128 MH/s) at the same | 8x 4090 rig (459 MH/s) at the same | Lock after a checkpoint if d stays 60 blue |
|---|---|---|---|---|---|
| 1 | 31.69 IGN, 25.35 | 46 s / 1.1 h / 11.1 h / 4.6 d | 9.1 s / 13 min / 2.2 h / 21.7 h | 2.5 s / 3.6 min / 36 min / 6.1 h | 63 s |
| 10 | 3.17, 2.54 | 4.6 s / 6.7 min / 1.1 h / 11.1 h | 0.9 s / 1.3 min / 13 min / 2.2 h | 0.3 s / 22 s / 3.6 min / 36 min | 9 s |
| 32 | 0.99, 0.79 | 1.4 s / 2.1 min / 21 min / 3.5 h | 0.3 s / 24 s / 4.1 min / 41 min | 0.1 s / 6.8 s / 1.1 min / 11 min | 4.9 s |
| 100 | 0.32, 0.25 | 0.5 s / 40 s / 6.7 min / 1.1 h | 0.1 s / 7.8 s / 1.3 min / 13 min | 0.0 s / 2.2 s / 22 s / 3.6 min | 3.6 s |
The solo-miner question: a 4070 at 25 MH/s sees 21.6 blocks a day at 1 bps on a 100 GH/s network (548 IGN a day at full ramp) and 2.16 a day at 1 TH/s (55 IGN); one block a day needs 0.046 bps at 100 GH/s, 0.46 bps at 1 TH/s and 4.6 bps at 10 TH/s. Kaspa's 10 bps buys the solo miner a daily block up to about 20 TH/s; Igneum at 1 bps gives it up to about 2 TH/s, which is 1,700x tonight's hash and USD 562,000 a day of rented hash at the bench entry's USD 281 per GH/s-day. What the rate costs the node, from 5.5 and 5.6: 10x the bytes (2 GB a day at 100 voters), a per-block CPU budget of 80 ms that the measured 61 to 345 ms does not meet, an RSS that leaves 8 GB within hours at the measured footprints, 3-s checkpoints and 66 GB a day of votes at 8,192 voters unless C1 moves to seconds, and the controller's cap inconsistency. Per tier: a home miner on one card gains variance relief it does not yet need (a 4070 at 1 bps sees a block an hour at 100 GH/s); a rig gains nothing (36 min per block at 1 TH/s already); a pool user nothing (pools remove variance); a prover gains nothing and pays the exec follower's 10x chain blocks; a holder gains faster locks (9 s) only if d is left in blue blocks, which costs the finality bytes of 5.7; a rollup customer the same lock question; the node operator pays all of it.
## 6. Ranked proposals
| Rank | Proposal | Evidence | Model | Hours | Consequence per tier | Gate |
|---|---|---|---|---|---|---|
| 1 | Block rate: 1 bps for the public testnet and for mainnet launch; 10 bps stays a planned step (spec 2.1) behind three gates: per-block node CPU under 50 ms at mergeset 248 on a 2019 laptop core, C1 / d / the clock cap in DAA seconds, vote aggregation; no 32 or 100 bps step is proposed | 5.2 (reds are node throughput), 5.5 (budget 80 ms against 61 to 345 measured), 5.7 (66 GB a day of votes), 5.10 (1 bps already gives a 4070 a block an hour at 100 GH/s) | `propagation.py` F and G, `cost.py` 4 and 7 | 1 (the spec line) plus the gates below | home miner: no node change, a block an hour at 100 GH/s; rig and pool: unchanged; prover: the exec follower stays at 1 chain block per second; holder and rollup: 63-s locks; node operator: today's node | the three gates pass on Devnet 2 at 10 bps with zero red over the knee, before any rate step |
| 2 | Controller: every lane counts every mergeset block's work (blue and red) over the real span; the clock cap, tolerances and lag bound in DAA seconds (20, 10, 60 s) at every rate; a chain step that merges m blocks may span m x 20 T before clamping | 3.1 (26 to 3.5 blocks/s with blue at 1.4), 5.4 (the fork over-reads by spacing / cap and under-reads by (k + 1) / m; Kaspa's window reads the whole DAG) | `controller.py`: 10.00 in every cell for the whole-DAG estimator | 8 (rule and unit tests in `difficulty.rs`, `igneum.rs`) + 3 (the harness case: fast-time 3 nodes, a 3-s proxy delay at 10 bps; pass = DAG rate within 10% of target and difficulty within 10% of the hash-implied value after 10 min, the same at 1 bps with a 30-s delay) | home miner: the subsidy stops tracking the controller's error (tonight 77% of blocks unpaid to their miner); node operator: no runaway DAG from a slow hub; holder: emission on schedule (spec 2.5's "when the controller lets the rate run" clause stops firing) | the harness case passes; `sim/difficulty/sim.py --dag-delay` replay of run A's production curve settles at 10 bps |
| 3 | Finality interval and determination in DAA seconds (C1: checkpoint every 30 DAA s; d in DAA s), one carriage per vote (a block carries a vote only if no block in its past does, which is the rule as written; the relay dedups by (key, index)), and lane 3's aggregated certificate as the archival object | 5.7 (votes and route load scale with bps under the blue-block rule), 3.1 (2,547 route drops per peer, 16 of 47 locks) | `cost.py` 2 and 10 | 4 (spec 03 C1, 3.10 table) + 8 (fork: `finality.rs` interval in DAA s, relay dedup) | home miner and rig: finality bytes stay at 81 MB a day at 100 voters whatever the rate; node operator: the route stays inside 4,096; light client: 3.4 MB a day at every rate; holder: 63-s locks at every rate | a Devnet 2 run at 10 bps with 42 voters shows 0 route drops and every determinable checkpoint locked |
| 4 | Peer floor and topology: a node reports synced and mines only with at least 4 outbound peers (8 on seeds); the fleet library gives every box the seed plus 2 random boxes; the seed list carries 3 seeds; the hub-cut harness case of lane 3's proposal 6 with the floor as its pass condition | 3.1 (peers=1 on every pod, 2.5 MB/s out of one process), lane 3 section 5.5 ("on a Vast box the seed is the only peer") | `propagation.py` B (mesh of degree 4 and 8 against the star: the same red share at an ideal hub, one fifth of the upload per node, no single queue) | 4 (fleet library and the synced rule in `tools/fleet/lib/`) + 3 (the harness case) | node operator and seed: upload spread 41 ways to 8 ways; home miner: a dead seed no longer stops blocks and votes; finality: votes reach aggregators by two paths | the hub-cut case passes: a lock within 2 checkpoints of the cut, 0 conflicting certificates at the hub's return, every box at 4 or more peers throughout |
| 5 | Measure the per-block node cost and its split (lottery verify, BLS per vote, GHOSTDAG and reachability, relay, exec) with `perf` on a Devnet 2 box and on a 2019-class laptop at 1 and 10 bps, at a narrow DAG and at mergeset 248; set the budget rule (cost x bps under 0.8 core) as a CI check on the fast-time network | 3.1 and 5.3 (61 to 345 ms measured as a total only) | section 4.6 | 3 (profile) + 4 (the CI check in `tools/ci`) | every node tier: a number per machine class instead of the hub's; the 8 GB laptop and the Raspberry-class verdicts of 5.5 become measurements | the profile lands in the bench log with per-component ms; the check fails a build whose per-block cost exceeds the budget at the network's rate |
| 6 | Snapshot path hardening: a p2p snapshot is accepted only when its root at the newest proven segment at or below its tip equals the chain's proof record for that segment, and (tip, root) agree on 3 of 5 peers; a failing peer is dropped for the session; the gate's unit test gains the two cases | 5.8 (the gate checks tips and the restart pin, never the state at the tip; the 6 October seed loaded a tip-0 snapshot from a fleet box before the gate) | section 5.8 | 6 (fork `proving.rs`, `snapshot.rs`; the record lookup exists in `check_record`) | node operator: recovery from a peer cannot be poisoned below an eclipse; prover: no false-state records from a poisoned node; home miner: the app's node recovers by itself after a deep reorg | `tools/exec-sync/reorg.mjs` gains a poisoned-snapshot case: refused, peer dropped, honest snapshot loaded after it |
| 7 | Devnet 2 genesis bits set for the fleet's hash at the run's rate (no 26 blocks/s burst), and the fleet rule: a box mines only after synced with the peer floor (run A's pods mined from genesis for up to 6 minutes before they saw the chain) | 3.1 (the 55-block reorg "unwinding to height 1", the 33% red first phase), 5.2 (the burst alone saturates a 61-ms hub for 5 minutes) | `propagation.py` C and E | 2 (`box-dn2.sh`, `start-seed.sh`, `devnet2-override.json` per rate) | fleet operator: run A2 and run B measure the rate, not the start; every other tier: none | the next Devnet 2 run shows no reorg over 5 in its first 10 minutes and a first-minute production within 2x of target |
| 8 | RSS steady state: run a node through a full 30-h pruning window at 1 bps (fast time) and at 10 bps on Devnet 2, read RSS every 10 minutes, bound the DAG store (reachability and GHOSTDAG per block) and `ExecState.records`; decide the 8 GB tier from the plateau | 3.3 (slopes 30, 80, 440 KB per block over short runs; none is a steady state), 5.5 (8 GB is marginal at 1 bps at 80 KB per block) | `cost.py` 5 | 3 (the run) + 8 (the bounds, if the plateau is over 4 GB) | home miner on an 8 GB laptop: a yes or no at 1 bps; Raspberry-class: the same; node operator: a RAM line on the requirements page | RSS plateau under 4 GB at 1 bps over 30 h; the requirements page carries the measured line |
| 9 | Node and client tiers published with sizes: full pruned node (1 bps: 0.9 GB a day, 136 MB window plus state, 4 GB RAM target), archival node (40 GB a year of blocks plus 1.3 GB of certificates once aggregated; 296 GB a year of votes until then at 1,000 voters), light client checkpoint mode (3.4 MB a day), phase two (2.3 MB a day) | 5.6, 5.8, `cost-tables.md` 6 and 10 | `cost.py` | 2 (the requirements page and spec 10.5's table at 1 bps, with the rate rows) | every tier knows its cost before the testnet | the page's numbers match `cost-tables.md` and the first testnet week's measured bytes within 30% |
| 10 | Bandwidth and route bounds per peer: at most 2 x V / 30 votes per peer per block interval accepted (a second belt behind the dedup), block relay to at most 16 peers per node, the IgneumFinality route sized from V and the rate | 3.1 (2,547 drops per peer), 5.6 | `cost.py` 3 | 4 (fork p2p flows) | seed and node operator: a bound on what one peer can make a node do; home miner: none | the s7 flood scenario gains a vote flood: 0 disconnects, bounded CPU |
**1. Block rate.** Everything measured tonight says 1 bps is the rate the current node can run and 10 bps is not: the hub spent 61 ms per block at a narrow DAG against a budget of 100, and the model turns that cost into tonight's red share (5.2). Nothing in propagation forbids 10 bps (5.1: zero reds at every measured delay with k 124), so the step stays on the plan (spec 2.1) as Kaspa took it, after a test campaign, with three gates: the per-block cost (proposal 5), the time denomination of finality and the controller (proposals 2 and 3), and vote aggregation (lane 3). The variance argument does not need the step yet: a 4070 sees a block an hour at 100 GH/s and two a day at 1 TH/s at 1 bps (5.10); a pool user never sees variance. Consequences: no node tier changes for the testnet; the testnet gives the measured bytes and RSS that fill proposals 8 and 9.
**2. Controller.** The rule walks the selected chain and sums blue-work increments over capped clock steps (`igneum_difficulty_bits`, section 4.4). In a wide DAG both parts mislead it: the cap turns a 5-s chain step into 2 s (over-read, harden, the record's fall to 3.5 blocks/s) and the blue-only work hides the reds (under-read, ease, the direction main reported). Kaspa's window pushes every mergeset block, red included, and divides by the real span (`window.rs`, `difficulty.rs`), which the model holds at target in every cell. The change is inside `igneum_difficulty_bits` and `igneum_target`: work = the mergeset's total work per chain step, the step allowed m x 20 T before the cap, the cap and tolerances in DAA seconds. The harness case is the proxy-delayed fast-time network of `sim/difficulty/attacks/README.md` at 10 bps with a 3-s hold, and `sim.py --dag-delay` replaying run A's production curve (the schedule file is in `sim/horizon/network/`).
**3. Finality interval in seconds.** C1 says 30 blue blocks, d says 60 blue blocks; at 10 bps that is a checkpoint every 3 s and ten times the votes, certificates, route load and light-client bytes per day (5.7), and tonight's seed showed the route overflowing at 42 voters. Written in DAA seconds the whole finality cost is rate-invariant and 3.4.2's arithmetic (384 votes per block drain 8,192 in 21 blocks) gets 10x more headroom at 10 bps. The one-carriage rule is already the spec's text ("not already in its past"); the relay dedup by (key, index) makes the wire match it.
**4. Peer floor.** Run A's pods had one peer each; the live fleet's boxes have the seed as their only peer; the hub paid 41x the upload and ran one queue for every block and vote. Four outbound peers before a node calls itself synced, the seed plus two random boxes in the fleet library, three seeds in the list: the mesh runs of 5.2 show the same zero red at an ideal hub with one fifth of the per-node upload and no single queue, and lane 3's hub-cut case becomes passable. This is the proposal that serves both lanes.
**5. The per-block profile.** The 61 to 345 ms is a total; the split decides which fix buys 10 bps: if BLS of carried votes dominates, aggregation and the dedup buy it; if GHOSTDAG at k 124 dominates, the mergeset limit and a cheaper reachability do; if relay dominates, the peer floor does. Three hours with `perf` on a Devnet 2 box, then a CI check that fails a build whose cost exceeds the budget at the network's rate, so the class is closed the way CLAUDE.md asks.
**6. Snapshot hardening.** The gate refuses tips, not states (5.8). The chain carries the proof records' roots, so a snapshot's root at its newest proven segment can be checked against them without a protocol change, and 3-of-5 peer agreement makes poisoning an eclipse. The attack is a liveness attack on the victim, never a consensus break, but a poisoned seed serving its state onward is the fleet-wide class the 6 October gate was written for.
**7 to 10.** Devnet 2's genesis bits and the start-synced rule make the next runs measure the rate instead of the start (the 55-block reorg and the first-phase reds were the start); the 30-h RSS run decides the 8 GB tier with a measurement instead of three slopes; the tier page and the per-peer bounds are two hours each and close open rows in spec 10.5 and the finality route entry.
## 7. Open questions and what could not be run
| Question | Why not tonight | What closes it |
|---|---|---|
| Run B (1 bps control) and A2 (mesh) rows | B starts at 20:25Z, rows about 21:00Z; A2 is not scripted | the fleet's RUN_B row; A2 built on `box-dn2.sh` with 3 `--addpeer` entries (seed plus two boxes) and genesis bits for 10 bps; this lane's prediction for A2 is in 5.2 |
| The difficulty trajectory of run A | the collector strips `difficulty=`; the node log has no bits | `bps-collect.py` keeps the field; or `getBlockDagInfo` difficulty per minute on the next run |
| The per-block CPU split | no profile; the hub's figure is a total that includes relay to 41 peers | proposal 5 |
| RSS steady state at 1 and 10 bps | three slopes over 1,500 to 15,000 blocks, none over a pruning window | proposal 8 |
| Kaspa's measured 10-bps red rate | not in the clone (`crescendo-guide.md` has requirements only) | the kaspanet KIP-14 text and the Crescendo testnet-10 reports, not cloned; approximate under 1 percent by the k derivation |
| BLS verify per vote on this stack | O-10.3 open; 1.5 ms is approximate | `fast_aggregate_verify` timing with the forked `blst` build |
| The pods' per-block cost (the A2 prediction's fork) | the pods' CPUs and their node logs were not read (fleet boxes are out of this lane's reach) | the fleet's collector reads `cpu_rss` on every box, not only the seed |
| GHOSTDAG's second k-cluster condition | `propagation.py` applies the candidate's own anticone bound only; lane 4's `ghostdag_sim.py` carries both | re-run the grid with lane 4's colouring if a cell is contested (every cell here is 0 percent, so the undercount cannot change the reading) |
| The 55-block reorg's cause | read from the log as a late-joining pod's chain from genesis ("unwinding to height 1") | the start-synced rule of proposal 7 removes the class; the next run's reorg distribution confirms |
## 8. Summary for the coordinator
Run A at 10 bps did not measure propagation; it measured a node that needs 61 to 345 ms per block through one hub whose budget at that rate is 100 ms. The propagation model with the measured links predicts under 0.1 percent red in the star and in the mesh; with the hub's measured per-block CPU it predicts 47 to 88 percent against the 77.2 percent measured, with the queueing waits the log's 100-block relay batches show, and it predicts that the mesh variant A2 goes red too unless every pod's per-block cost is under 80 ms and the genesis burst is removed. The controller then hardened (production 26 to 3.5 blocks/s with blue at 1.4) because its estimator reads blue work over a chain step capped at 2 s; the whole-DAG estimator Kaspa uses is unbiased in every regime the model covers. The block rate for the public testnet and mainnet launch is 1 bps; 10 bps stays a gated step, and the gates are a per-block cost under 50 ms on a laptop core at mergeset 248, the finality interval and the clock cap in DAA seconds, and vote aggregation. Lane 3's finding stands (the pause was the rule, the fleet's star is around the seed); the peer floor is the piece of work both lanes need.
1. Reds from node cost, not topology: red share 0.0 percent in every cell of the bps x delay grid (1 to 100 bps, 50 to 1,000 ms hops, k from Kaspa's table) and in the run A replay with an ideal hub; 47 / 88 / 83 percent with the hub at 61 / 115 / 345 ms per block (measured); the knee at 10 bps is 100 ms per block (`sim/horizon/network/propagation.py`, `results.md`, `results-2.md`).
2. The controller's two biases: estimate / true hash = (blues / m) x (spacing / cap); at a 5-s chain-step spacing the fork settles the DAG at 4.0 blocks/s against 10 (record 3.3 to 3.6), at a 20-s spacing at 1 bps it runs away to 179 blocks/s; the whole-DAG estimator holds 10.00 and 1.00 in every cell (`controller.py`).
3. Finality cost scales with the rate under C1 as written: 8,192 voters cost 6.6 GB a day per node at 1 bps and 66 GB at 10 bps (24 TB a year archival); in DAA seconds 6.6 GB at every rate, 3.5 MB a day with aggregated certificates; tonight's seed dropped up to 2,547 finality messages per peer and locked 16 of 47 checkpoints at 42 voters (`cost.py`, section 3.1).

View file

@ -0,0 +1,14 @@
# sim/horizon/network
Models behind `docs/analysis/horizon/network.md` (Horizon lane 5, network, 6 October 2026). Python 3.10, standard library only; the Mac, under the main checkout's run lock (`/Users/joshm/Projects/igneum/tools/lock/with-lock.sh run nice -n 19 ...`). Outputs are counts, shares and seconds, never timings of this machine.
| File | What | Run |
|---|---|---|
| `propagation.py` | Poisson block production, star or mesh topology, lognormal link latency, inv/request/block hops, a hub (or every node) with a per-block validation cost and a queue, GHOSTDAG colouring with the first k-cluster condition, mergeset and parent caps, the natural reorg depth at miner 0 | `python3 sim/horizon/network/propagation.py --grid` (the bps x delay grid); single runs with `--topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 115 --schedule sim/horizon/network/runA-schedule.csv --seconds 1200` |
| `run-batch.sh`, `run-batch2.sh` | the lane's runs, appended to `results.md` and `results-2.md` | `bash sim/horizon/network/run-batch.sh` (about 2 min), `run-batch2.sh` (about 2 min) |
| `runA-schedule.csv` | run A's measured production per minute (the Devnet 2 seed's `PoW accepted` lines on igneum-build-1, `/home/build/dn2seed-A.log`) | input to `--schedule` |
| `burst-schedule.csv` | a 26 blocks/s genesis burst for 5 min then 10 bps | input to `--schedule` |
| `controller.py` | the difficulty rule against a wide DAG: the fork's blue-work-over-capped-chain-step estimator, the whole-DAG estimator (Kaspa's window) and a red-corrected blue estimator, closed loop with the 3% / 10% clamps | `python3 sim/horizon/network/controller.py --delays 0.3,1,2,3,5,10,30` (10 bps); `--bps 1 --k 18 --delays 0.3,2,5,20,30,60 --lambda0 2.6` (1 bps); outputs `controller-10bps.md`, `controller-1bps.md` |
| `cost.py` | GHOSTDAG parameters per rate, the vote bounds, bytes per node per day, CPU per block, RSS and disk, pruned and archival storage, subsidy and payout intervals, the solo-miner table, finality timing, light-client bytes; every input labelled in `INPUTS` | `python3 sim/horizon/network/cost.py > sim/horizon/network/cost-tables.md` |
Seeds: 7 everywhere. Results files: `results.md`, `results-2.md`, `controller-10bps.md`, `controller-1bps.md`, `cost-tables.md`.

View file

@ -0,0 +1,11 @@
# burst then target
0,1559
1,1559
2,1559
3,1559
4,1559
5,600
6,600
7,600
8,600
9,600
1 # burst then target
2 0,1559
3 1,1559
4 2,1559
5 3,1559
6 4,1559
7 5,600
8 6,600
9 7,600
10 8,600
11 9,600

View file

@ -0,0 +1,25 @@
# controller.py: bps 10, k 124, cap 2 s, start at 26 blocks/s, 30 min; settled = mean of the last 5 minutes
| estimator | delay d, s | DAG rate settled, blocks/s | blue rate, blocks/s | red share | P(anticone > k) | mergeset per chain block | estimate / true hash | difficulty vs correct | time to within 10% of target DAG rate, min |
|---|---|---|---|---|---|---|---|---|---|
| igneum | 0.3 | 10.00 | 10.00 | 0.0% | 0.00 | 3.0 | 1.00x | 1.00x | 0.1 |
| igneum | 1 | 10.00 | 10.00 | 0.0% | 0.00 | 10.0 | 1.00x | 1.00x | 0.5 |
| igneum | 2 | 10.00 | 10.00 | 0.0% | 0.00 | 20.0 | 1.00x | 1.00x | 1.0 |
| igneum | 3 | 6.67 | 6.67 | 0.0% | 0.00 | 20.0 | 1.50x | 1.50x | 1.5 |
| igneum | 5 | 4.00 | 4.00 | 0.0% | 0.00 | 20.0 | 2.50x | 2.50x | 2.5 |
| igneum | 10 | 2.00 | 2.00 | 0.0% | 0.00 | 20.0 | 5.00x | 5.00x | 5.0 |
| igneum | 30 | 5.21 | 4.20 | 19.5% | 1.00 | 156.3 | 12.08x | 1.93x | 15.0 |
| whole | 0.3 | 10.00 | 10.00 | 0.0% | 0.00 | 3.0 | 1.00x | 1.00x | 0.1 |
| whole | 1 | 10.00 | 10.00 | 0.0% | 0.00 | 10.0 | 1.00x | 1.00x | 0.5 |
| whole | 2 | 10.00 | 10.00 | 0.0% | 0.00 | 20.0 | 1.00x | 1.00x | 1.0 |
| whole | 3 | 10.00 | 10.00 | 0.0% | 0.00 | 30.0 | 1.00x | 1.00x | 1.5 |
| whole | 5 | 10.00 | 10.00 | 0.0% | 0.01 | 50.0 | 1.00x | 1.00x | 2.5 |
| whole | 10 | 10.00 | 10.00 | 0.0% | 1.00 | 100.0 | 1.00x | 1.00x | 5.0 |
| whole | 30 | 10.00 | 4.17 | 58.3% | 1.00 | 300.0 | 1.00x | 1.00x | 15.0 |
| blue-rc | 0.3 | 10.00 | 10.00 | 0.0% | 0.00 | 3.0 | 1.00x | 1.00x | 0.1 |
| blue-rc | 1 | 10.00 | 10.00 | 0.0% | 0.00 | 10.0 | 1.00x | 1.00x | 0.5 |
| blue-rc | 2 | 10.00 | 10.00 | 0.0% | 0.00 | 20.0 | 1.00x | 1.00x | 1.0 |
| blue-rc | 3 | 10.00 | 10.00 | 0.0% | 0.00 | 30.0 | 1.00x | 1.00x | 1.5 |
| blue-rc | 5 | 10.00 | 10.00 | 0.0% | 0.01 | 50.0 | 1.00x | 1.00x | 2.5 |
| blue-rc | 10 | 10.00 | 10.00 | 0.0% | 1.00 | 100.0 | 1.00x | 1.00x | 5.0 |
| blue-rc | 30 | 10.00 | 4.17 | 58.3% | 1.00 | 300.0 | 1.00x | 1.00x | 15.0 |

View file

@ -0,0 +1,22 @@
# controller.py: bps 1, k 18, cap 20 s, start at 2.6 blocks/s, 30 min; settled = mean of the last 5 minutes
| estimator | delay d, s | DAG rate settled, blocks/s | blue rate, blocks/s | red share | P(anticone > k) | mergeset per chain block | estimate / true hash | difficulty vs correct | time to within 10% of target DAG rate, min |
|---|---|---|---|---|---|---|---|---|---|
| igneum | 0.3 | 1.00 | 1.00 | 0.0% | 0.00 | 1.0 | 1.00x | 1.00x | 0.3 |
| igneum | 2 | 1.00 | 1.00 | 0.0% | 0.00 | 2.0 | 1.00x | 1.00x | 1.0 |
| igneum | 5 | 1.00 | 1.00 | 0.0% | 0.01 | 5.0 | 1.00x | 1.00x | 2.5 |
| igneum | 20 | 178.75 | 1.00 | 99.4% | 1.00 | 3575.0 | 0.01x | 0.01x | never |
| igneum | 30 | 43.03 | 0.65 | 98.5% | 1.00 | 1290.8 | 0.02x | 0.02x | never |
| igneum | 60 | 10.41 | 0.32 | 96.9% | 1.00 | 624.8 | 0.09x | 0.10x | never |
| whole | 0.3 | 1.00 | 1.00 | 0.0% | 0.00 | 1.0 | 1.00x | 1.00x | 0.3 |
| whole | 2 | 1.00 | 1.00 | 0.0% | 0.00 | 2.0 | 1.00x | 1.00x | 1.0 |
| whole | 5 | 1.00 | 1.00 | 0.0% | 0.01 | 5.0 | 1.00x | 1.00x | 2.5 |
| whole | 20 | 1.00 | 0.95 | 5.0% | 1.00 | 20.0 | 1.00x | 1.00x | 10.0 |
| whole | 30 | 1.00 | 0.63 | 36.7% | 1.00 | 30.0 | 1.00x | 1.00x | 15.0 |
| whole | 60 | 1.17 | 0.32 | 72.9% | 1.00 | 70.3 | 1.00x | 0.86x | never |
| blue-rc | 0.3 | 1.00 | 1.00 | 0.0% | 0.00 | 1.0 | 1.00x | 1.00x | 0.3 |
| blue-rc | 2 | 1.00 | 1.00 | 0.0% | 0.00 | 2.0 | 1.00x | 1.00x | 1.0 |
| blue-rc | 5 | 1.00 | 1.00 | 0.0% | 0.01 | 5.0 | 1.00x | 1.00x | 2.5 |
| blue-rc | 20 | 1.00 | 0.95 | 5.0% | 1.00 | 20.0 | 1.00x | 1.00x | 10.0 |
| blue-rc | 30 | 1.00 | 0.63 | 36.7% | 1.00 | 30.0 | 1.00x | 1.00x | 15.0 |
| blue-rc | 60 | 1.17 | 0.32 | 72.9% | 1.00 | 70.3 | 1.00x | 0.86x | never |

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""The difficulty controller against a wide DAG: the coupling between red share, chain-step spacing and the estimator
(Horizon network lane, 6 October 2026).
The rule as the fork runs it (vendor/igneum-node release-0.3.14-node, consensus/src/processes/difficulty.rs
`igneum_difficulty_bits`, consensus/core/src/igneum.rs `difficulty`): the lanes walk the SELECTED CHAIN; every step
carries `work = blue_work(b) - blue_work(selected parent)` (the mergeset BLUES' work only, red work is not in blue
work) and `solvetime = min(c(b) - c(p), CAP_BLOCKS x T)` with CAP_BLOCKS = 20 (2 s at T = 100 ms); the target moves at
most HARDEN_PERCENT 3 (difficulty up) or EASE_PERCENT 10 (difficulty down) per chain block (spec 02 2.3).
Kaspa's rule (vendor/rusty-kaspa consensus/src/processes/window.rs `push_mergeset`, difficulty.rs
`calculate_difficulty_bits`): the DAA window holds every mergeset block, blue AND red, above the window's blue-score
floor, and the new target is the window's average target x measured span / (window size x T): the whole-DAG rate
over the real span.
The abstract DAG regime (no block-level simulation; the inputs are the propagation lane's):
spacing = max(1 / lambda, d) seconds between selected-chain blocks (every block a chain block when the DAG is
narrow; one chain block per propagation delay when it is wide)
m = lambda x spacing mergeset per chain block
blues = min(m, k + 1) the k-cluster admits at most k blues beside the selected parent per chain step
(protocol.rs); the rest of the mergeset is red: r = 1 - blues / m
(the PHANTOM tail P(Poisson(2 d lambda) > k) that k is derived from, bps.rs `calculate_ghostdag_k`, says when
reds appear at all; it is printed beside r)
Estimators of the network hash H (work per block = D, the difficulty):
igneum = blues x D / min(spacing, cap) (blue work over the capped chain step)
whole = m x D / spacing (every mergeset block's work over the real span) = H
blue-rc = blues x D / spacing / (1 - r_obs) (blue rate with the observed red share corrected) = H
The controller sets the next D toward estimate x T through the 3% / 10% clamps once per chain block.
Run: python3 sim/horizon/network/controller.py [--bps 10 --k 124 --delays 0.3,1,2,5,10 --minutes 30 --lambda0 26]
"""
import argparse, math
def pois_tail_gt(k, x):
"""P(Poisson(x) > k)"""
if x <= 0: return 0.0
# sum P(N <= k) in log space
lp = -x; s = math.exp(lp)
for i in range(1, k + 1):
lp += math.log(x / i); s += math.exp(lp)
if lp < -745: break
return max(0.0, min(1.0, 1.0 - s))
def run(est, bps, k, d, minutes, lam0, H=1e9):
T = 1.0 / bps; cap = 20 * T
D = H / lam0 # difficulty giving the initial production rate
t = 0.0; rows = []
while t < minutes * 60:
lam = H / D
x = 2 * d * lam
spacing = max(1.0 / lam, d)
m = lam * spacing
blues = min(m, k + 1)
r = 1 - blues / m
if est == "igneum":
h = blues * D / min(spacing, cap)
elif est == "whole":
h = m * D / spacing
else:
h = blues * D / spacing / max(1e-9, 1 - r)
want = h * T
D_new = min(D * 1.03, max(D * 0.90, want)) # harden at most 3%, ease at most 10%
rows.append((t, lam, r, m, blues, h / H, D, pois_tail_gt(k, x)))
D = D_new; t += spacing
return rows
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--bps", type=float, default=10); ap.add_argument("--k", type=int, default=124)
ap.add_argument("--delays", default="0.3,1,2,5,10"); ap.add_argument("--minutes", type=float, default=30)
ap.add_argument("--lambda0", type=float, default=26.0, help="initial production rate, blocks/s (run A's genesis burst was 26)")
ap.add_argument("--out", default=None)
a = ap.parse_args()
lines = []
def emit(s): print(s); lines.append(s)
emit(f"# controller.py: bps {a.bps:g}, k {a.k}, cap {20/a.bps:g} s, start at {a.lambda0:g} blocks/s, {a.minutes:g} min; settled = mean of the last 5 minutes")
emit("")
emit("| estimator | delay d, s | DAG rate settled, blocks/s | blue rate, blocks/s | red share | P(anticone > k) | mergeset per chain block | estimate / true hash | difficulty vs correct | time to within 10% of target DAG rate, min |")
emit("|---|---|---|---|---|---|---|---|---|---|")
for est in ("igneum", "whole", "blue-rc"):
for d in [float(v) for v in a.delays.split(",")]:
rows = run(est, a.bps, a.k, d, a.minutes, a.lambda0)
tail = [r for r in rows if r[0] >= (a.minutes - 5) * 60] or rows[-5:]
lam = sum(r[1] for r in tail) / len(tail); red = sum(r[2] for r in tail) / len(tail)
m = sum(r[3] for r in tail) / len(tail); ratio = sum(r[5] for r in tail) / len(tail)
Dcorrect = 1e9 / a.bps; Dratio = sum(r[6] for r in tail) / len(tail) / Dcorrect
within = next((r[0] / 60 for r in rows if abs(r[1] - a.bps) <= 0.1 * a.bps), None)
wtxt = f"{within:.1f}" if within is not None else "never"
tail_p = sum(r[7] for r in tail) / len(tail)
emit(f"| {est} | {d:g} | {lam:.2f} | {lam*(1-red):.2f} | {100*red:.1f}% | {tail_p:.2f} | {m:.1f} | {ratio:.2f}x | {Dratio:.2f}x | {wtxt} |")
if a.out:
with open(a.out, "w") as f: f.write("\n".join(lines) + "\n")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,84 @@
# cost.py tables (sim/horizon/network/cost.py). Labels: M = measured, C = cited, S = simulated, A = approximate; sources in the script's INPUTS.
## 1. GHOSTDAG parameters per block rate (C: bps.rs formulas; k at 100 bps computed with calculate_ghostdag_k since the table stops at 32)
| bps | k | max parents | mergeset limit | merge depth, blocks | finality depth, blocks | pruning depth, blocks | coinbase maturity, blocks | checkpoint cadence (30 blue), s | determination d = 60 blue, s | blocks per 30-s window |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 18 | 10 | 180 | 3,600 | 43,200 | 108,000 | 100 | 30 | 60 | 30 |
| 10 | 124 | 16 | 248 | 36,000 | 432,000 | 1,080,000 | 1,000 | 3 | 6 | 300 |
| 32 | 362 | 16 | 512 | 115,200 | 1,382,400 | 3,456,000 | 3,200 | 0.9375 | 1.875 | 960 |
| 100 | 1074 | 16 | 512 | 360,000 | 4,320,000 | 10,800,000 | 10,000 | 0.3 | 0.6 | 3000 |
## 2. Votes and the 3.4.2 bounds per block rate (C: 281 B per vote; checkpoint every 30 BLUE blocks as C1 is written)
| bps | checkpoints per day | votes per block at V voters (V/30, C1 in blue blocks) | drain capacity per checkpoint, cap 48 / 384 | vote bytes per day, V = 100 / 1,000 / 8,192 (single votes) | the same if C1 were 30 DAA seconds |
|---|---|---|---|---|---|
| 1 | 2,880 | 3.3 / 33.3 / 273.1 | 1,440 / 11,520 | 80.93 MB / 809.28 MB / 6.63 GB | 80.93 MB / 809.28 MB / 6.63 GB |
| 10 | 28,800 | 3.3 / 33.3 / 273.1 | 1,440 / 11,520 | 809.28 MB / 8.09 GB / 66.30 GB | 80.93 MB / 809.28 MB / 6.63 GB |
| 32 | 92,160 | 3.3 / 33.3 / 273.1 | 1,440 / 11,520 | 2.59 GB / 25.90 GB / 212.15 GB | 80.93 MB / 809.28 MB / 6.63 GB |
| 100 | 288,000 | 3.3 / 33.3 / 273.1 | 1,440 / 11,520 | 8.09 GB / 80.93 GB / 662.96 GB | 80.93 MB / 809.28 MB / 6.63 GB |
## 3. Bytes per node per day: receive every block once plus relay overhead (A: Kaspa's inv/request flow, degree 8), votes carried in bodies, one certificate per block
| bps | header bytes (C+S) | blocks per day | block bytes per day (header + body + records) | relay overhead per day | votes per day at V = 12 / 100 / 1,000 / 8,192 (C1 in blue blocks) | certificates per day at V = 12 / 8,192 | total per day at V = 100 | total at V = 8,192 | Mbit/s mean at V = 8,192 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 331 | 86,400 | 57.46 MB | 31.10 MB | 9.71 MB / 80.93 MB / 809.28 MB / 6.63 GB | 23.72 MB / 112.06 MB | 194.16 MB | 6.83 GB | 0.63 |
| 10 | 417 | 864,000 | 649.25 MB | 311.04 MB | 97.11 MB / 809.28 MB / 8.09 GB / 66.30 GB | 237.17 MB / 1.12 GB | 2.02 GB | 68.38 GB | 6.33 |
| 32 | 542 | 2,764,800 | 2.42 GB | 995.33 MB | 310.76 MB / 2.59 GB / 25.90 GB / 212.15 GB | 758.94 MB / 3.59 GB | 6.80 GB | 219.15 GB | 20.29 |
| 100 | 740 | 8,640,000 | 9.28 GB | 3.11 GB | 971.14 MB / 8.09 GB / 80.93 GB / 662.96 GB | 2.37 GB / 11.21 GB | 22.95 GB | 686.56 GB | 63.57 |
With certificates aggregated (lane 3's 1.2 KB per checkpoint) and votes NOT echoed in every body (one carriage), the vote column is the floor: a vote crosses the network once. A star hub multiplies its upload by its peer count: run A's hub sent 2.13 GB in 850 s to 41 peers (A.jsonl rx_tx, netns-wide counter, so an upper bound): 2.5 MB/s, 61 KB/s per peer.
## 4. Node CPU per block and the serial budget (the pipeline validates blocks in order; budget = 0.8 core)
| bps | budget per block, ms | lottery verify class v3 / v4 / v4 half core (M) | BLS for V/30 votes per block at V = 100 / 1,000 / 8,192, cap 384 (A: 1.5 ms per verify) | run A hub measured total at mergeset 8 / 30 / 150+ (M) | fits the budget? |
|---|---|---|---|---|---|
| 1 | 800 | 2.79 / 4.9 / 8.23 | 5 / 50 / 410 | 61 / 115 / 345 | yes with margin |
| 10 | 80 | 2.79 / 4.9 / 8.23 | 5 / 50 / 410 | 61 / 115 / 345 | only the verify and a narrow DAG (under 25 ms per block measured nowhere yet) |
| 32 | 25 | 2.79 / 4.9 / 8.23 | 5 / 50 / 410 | 61 / 115 / 345 | only the verify and a narrow DAG (under 25 ms per block measured nowhere yet) |
| 100 | 8 | 2.79 / 4.9 / 8.23 | 5 / 50 / 410 | 61 / 115 / 345 | no: class v4 verify alone (4.9 to 8.2 ms) plus GHOSTDAG exceeds it |
## 5. RSS and disk per block rate (M: cache 256 MiB x KEEP_DAYS 3; slopes per block from M30 s8, the live app node, run A's hub; all slopes are growth over a run, not a proven steady state)
| bps | caches | DAG store growth per day at 30 / 80 / 440 KB per block | growth over the 30-h pruning window at 30 / 80 KB per block | exec state (M: 128 MB snapshot at 141,700 chain blocks) | disk per day: blocks + votes at V = 1,000 + exec records |
|---|---|---|---|---|---|
| 1 | 768 MiB worst, 256 MiB flat | 2.61 GB / 6.91 GB / 38.02 GB | 3.26 GB / 8.64 GB | 128 MB + 1.5 KB per chain block | 917.78 MB |
| 10 | 768 MiB worst, 256 MiB flat | 26.09 GB / 69.12 GB / 380.16 GB | 32.62 GB / 86.40 GB | 128 MB + 1.5 KB per chain block | 8.97 GB |
| 32 | 768 MiB worst, 256 MiB flat | 83.50 GB / 221.18 GB / 1.22 TB | 104.37 GB / 276.48 GB | 128 MB + 1.5 KB per chain block | 28.69 GB |
| 100 | 768 MiB worst, 256 MiB flat | 260.93 GB / 691.20 GB / 3.80 TB | 326.16 GB / 864.00 GB | 128 MB + 1.5 KB per chain block | 90.77 GB |
## 6. Pruned and archival storage per year (C: pruning window 108,000 DAA s; A: 2x index overhead on blocks; votes single, one carriage)
| bps | pruned node keeps (window of headers + bodies, 2x) | plus votes in the window at V = 1,000 | archival per year: blocks (2x) | archival votes per year at V = 100 / 1,000 / 8,192 (C1 in blue blocks) | archival votes per year if certificates only (1.2 KB per checkpoint, lane 3) |
|---|---|---|---|---|---|
| 1 | 136.25 MB | 1.01 GB | 39.81 GB | 29.56 GB / 295.59 GB / 2.42 TB | 1.26 GB |
| 10 | 1.55 GB | 10.12 GB | 452.66 GB | 295.59 GB / 2.96 TB / 24.21 TB | 12.62 GB |
| 32 | 5.82 GB | 32.37 GB | 1.70 TB | 945.89 GB / 9.46 TB / 77.49 TB | 40.39 GB |
| 100 | 22.47 GB | 101.16 GB | 6.57 TB | 2.96 TB / 29.56 TB / 242.15 TB | 126.23 GB |
## 7. Subsidy per block and payout intervals (C: 31.688 IGN per DAA s at full ramp, 80% to the producer; interval = 1 / (bps x share))
| bps | subsidy per block, IGN | miner's 80%, IGN | 4070 at tonight 1.16 GH/s | 5090 at tonight 1.16 GH/s | rig at tonight 1.16 GH/s | 4070 at 10 GH/s | 5090 at 10 GH/s | rig at 10 GH/s | 4070 at 100 GH/s | 5090 at 100 GH/s | rig at 100 GH/s | 4070 at 1 TH/s | 5090 at 1 TH/s | rig at 1 TH/s | 4070 at 10 TH/s | 5090 at 10 TH/s | rig at 10 TH/s |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 31.688 | 25.350 | 46.4 s | 9.1 s | 2.5 s | 6.7 min | 1.3 min | 21.8 s | 1.1 h | 13.0 min | 3.6 min | 11.1 h | 2.2 h | 36.3 min | 4.6 d | 21.7 h | 6.1 h |
| 10 | 3.169 | 2.535 | 4.6 s | 0.9 s | 0.3 s | 40.0 s | 7.8 s | 2.2 s | 6.7 min | 1.3 min | 21.8 s | 1.1 h | 13.0 min | 3.6 min | 11.1 h | 2.2 h | 36.3 min |
| 32 | 0.990 | 0.792 | 1.4 s | 0.3 s | 0.1 s | 12.5 s | 2.4 s | 0.7 s | 2.1 min | 24.4 s | 6.8 s | 20.8 min | 4.1 min | 1.1 min | 3.5 h | 40.7 min | 11.3 min |
| 100 | 0.317 | 0.254 | 0.5 s | 0.1 s | 0.0 s | 4.0 s | 0.8 s | 0.2 s | 40.0 s | 7.8 s | 2.2 s | 6.7 min | 1.3 min | 21.8 s | 1.1 h | 13.0 min | 3.6 min |
## 8. The solo-miner question: a 4070 at 25 MH/s, blocks per day and the rate needed for one block a day
| network hash | share | blocks per day at 1 bps | bps for one block a day | IGN per day at 1 bps (full ramp, 80%) |
|---|---|---|---|---|
| tonight 1.16 GH/s | 2.16e-02 | 1862.07 | 0.0005 | 47,204.2 |
| 10 GH/s | 2.50e-03 | 216.00 | 0.0046 | 5,475.7 |
| 100 GH/s | 2.50e-04 | 21.60 | 0.0463 | 547.6 |
| 1 TH/s | 2.50e-05 | 2.16 | 0.4630 | 54.8 |
| 10 TH/s | 2.50e-06 | 0.22 | 4.6296 | 5.5 |
## 9. Finality timing per block rate if C1 and d stay in blue blocks (C: spec 03 C1; the lock about 3 s after determination, lane 4's reading of 3.11.3)
| bps | checkpoint cadence, s | determination after the checkpoint, s | lock after the checkpoint, s | checkpoints per presence window of 240 indices, minutes | 30-day window in blue blocks |
|---|---|---|---|---|---|
| 1 | 30 | 60 | 63 | 120 | 2,592,000 |
| 10 | 3 | 6 | 9 | 12 | 25,920,000 |
| 32 | 0.9375 | 1.875 | 4.875 | 3.75 | 82,944,000 |
| 100 | 0.3 | 0.6 | 3.6 | 1.2 | 259,200,000 |
## 10. Light client bytes per day (C: spec 10.5 shape; checkpoint mode 2,880 x (header 400 + certificate + proof 400) at 1 bps; scales with checkpoints per day if C1 stays in blue blocks)
| bps | checkpoints per day | checkpoint mode at 1,000 voters | at 10,000 voters | phase two (800 B per checkpoint) |
|---|---|---|---|---|
| 1 | 2,880 | 3.45 MB | 6.69 MB | 2.30 MB |
| 10 | 28,800 | 34.50 MB | 66.90 MB | 23.04 MB |
| 32 | 92,160 | 110.41 MB | 214.09 MB | 73.73 MB |
| 100 | 288,000 | 345.02 MB | 669.02 MB | 230.40 MB |

169
sim/horizon/network/cost.py Normal file
View file

@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Node cost, bandwidth, pruning and payout arithmetic per block rate (Horizon network lane, 6 October 2026).
Every input is labelled in INPUTS below: measured (entry named), cited (file named) or approximate.
Run: python3 sim/horizon/network/cost.py > sim/horizon/network/cost-tables.md
"""
import math
def calculate_ghostdag_k(x, delta):
"""vendor/rusty-kaspa/consensus/core/src/config/bps.rs `calculate_ghostdag_k`, in log space (the f64 form
underflows exp(-x) at x = 1000 and never terminates; same result where both run: 18 at x 10, 124 at x 100)"""
k_hat, sigma, lterm = 0, 0.0, -x
while True:
sigma += math.exp(lterm)
if 1.0 - sigma < delta: return k_hat
k_hat += 1; lterm += math.log(x / k_hat)
INPUTS = {
# cited: vendor/rusty-kaspa consensus/core/src/config/constants.rs and bps.rs
"delay_bound_s": 5, "delta": 0.01, "merge_depth_s": 3600, "finality_s": 43200, "pruning_s": 108000, "maturity_s": 100,
# cited: docs/spec/10-light-client.md 10.5 (286 fixed header bytes), 02-consensus.md 2.4
"header_fixed": 286,
# simulated: sim/horizon/network/results.md grid, tips mean at hop 300 ms (parents cap 10 at 1 bps, 16 above: bps.rs)
"parents_mean": {1: 1.4, 10: 4.1, 32: 8.0, 100: 14.2},
# measured: docs/fud-ledger.md M21 sweep, live devnet p50 block 723 B with 1 coinbase; the coinbase without votes about 300 B (approximate)
"body_no_votes": 300, "record_bytes": 274, "segment_chain_blocks": 8,
# cited: fud-close docs/spec/03-finality.md 3.4.2: vote item 281 B, certificate 273 B + V/8, bounds 48 (today) and 384 (proposed)
"vote_bytes": 281, "cert_fixed": 273, "vote_cap_today": 48, "vote_cap_proposed": 384,
# cited: spec 03 C1: checkpoint every 30 blue blocks, determination d = 60 blue (placeholder; 20 on devnet)
"cp_blue": 30, "det_blue": 60,
# approximate: Kaspa's relay flow (protocol/flows, blockrelay: inv, request, block): per block, degree x 40 B of inv plus one 40 B request
"degree": 8, "inv_bytes": 40,
# measured: docs/bench-log.md M30 entry: 256 MiB cache, KEEP_DAYS 3 (768 MiB worst), slope 30.2 MB per 1,000 blocks (s8 steady, narrow DAG);
# the live app node 1,081 MB at 27 min to 2,258 MB at 4 h 14 min (about 80 KB per block, derived); run A's hub 5.4 GB at 12,000 blocks (440 KB per block, A.jsonl)
"cache_mib": 256, "keep_days": 3, "rss_per_block_kb": {"narrow (s8 steady)": 30.2, "live devnet app node (derived)": 80, "run A hub, wide DAG, 41 peers": 440},
# measured: hands-on-build-1.md: exec-snapshot.bin 127,564,588 B at tip 141,700 chain blocks (0.9 KB per chain block); M30 note: ExecState.records 1 to 2 KB per chain block
"exec_snapshot_bytes": 127564588, "exec_snapshot_tip": 141700, "exec_record_kb": 1.5,
# measured: lottery verify per header: class v3 2.79 ms on a loaded M5 Max core (consequences C29); class v4 4.9 ms steady / 8.23 ms half-core on the box proxy (lane 2); gate 10 ms (spec 01)
"verify_ms": {"class v3 (M5 Max loaded core)": 2.79, "class v4 (box proxy)": 4.9, "class v4 half core": 8.23, "gate": 10.0},
# approximate: one BLS signature verify about 1.5 ms (unmeasured on this stack, O-10.3)
"bls_verify_ms": 1.5,
# measured: run A hub CPU per accepted block (A.jsonl cpu_rss deltas over accepted deltas): 61 ms (mergeset about 8), 115 ms (about 30), 345 ms (150 to 200)
"hub_ms_per_block": {"mergeset 8": 61, "mergeset 30": 115, "mergeset 150 to 200": 345},
# cited: spec 02 2.5: 31.688 IGN per DAA second at full ramp, 80% to the producer
"ign_per_s": 31.688, "miner_share": 0.8,
# the brief's tiers and networks; the rental entry: USD 0.0117 per MH/s-hour (bench-log "Rental cost of hash, 6 October 2026")
"tiers": {"4070 (25 MH/s)": 25e6, "5090 (128 MH/s)": 128e6, "8x 4090 rig (459 MH/s)": 459e6},
"networks": {"tonight 1.16 GH/s": 1.16e9, "10 GH/s": 1e10, "100 GH/s": 1e11, "1 TH/s": 1e12, "10 TH/s": 1e13},
}
BPS = [1, 10, 32, 100]
VOTERS = [12, 100, 1000, 8192]
def k_of(b): return {1: 18, 10: 124, 32: 362}.get(b) or calculate_ghostdag_k(2 * INPUTS["delay_bound_s"] * b, INPUTS["delta"])
def parents_cap(k): return max(10, min(16, k // 2))
def mergeset_cap(k): return max(180, min(512, 2 * k))
def header_bytes(b): return INPUTS["header_fixed"] + 32 * INPUTS["parents_mean"][b]
def fmt_bytes(n):
for u, d in (("TB", 1e12), ("GB", 1e9), ("MB", 1e6), ("KB", 1e3)):
if n >= d: return f"{n/d:.2f} {u}"
return f"{n:.0f} B"
def fmt_time(s):
if s < 60: return f"{s:.1f} s"
if s < 3600: return f"{s/60:.1f} min"
if s < 86400: return f"{s/3600:.1f} h"
return f"{s/86400:.1f} d"
out = []
def P(s=""): out.append(s)
P("# cost.py tables (sim/horizon/network/cost.py). Labels: M = measured, C = cited, S = simulated, A = approximate; sources in the script's INPUTS.")
P()
P("## 1. GHOSTDAG parameters per block rate (C: bps.rs formulas; k at 100 bps computed with calculate_ghostdag_k since the table stops at 32)")
P("| bps | k | max parents | mergeset limit | merge depth, blocks | finality depth, blocks | pruning depth, blocks | coinbase maturity, blocks | checkpoint cadence (30 blue), s | determination d = 60 blue, s | blocks per 30-s window |")
P("|---|---|---|---|---|---|---|---|---|---|---|")
for b in BPS:
k = k_of(b); ms = mergeset_cap(k)
lower = INPUTS["finality_s"] * b + 2 * INPUTS["merge_depth_s"] * b + 4 * ms * k + 2 * k + 2
prun = max(lower, INPUTS["pruning_s"] * b)
P(f"| {b} | {k} | {parents_cap(k)} | {ms} | {INPUTS['merge_depth_s']*b:,} | {INPUTS['finality_s']*b:,} | {prun:,} | {INPUTS['maturity_s']*b:,} | {INPUTS['cp_blue']/b:g} | {INPUTS['det_blue']/b:g} | {30*b} |")
P()
P("## 2. Votes and the 3.4.2 bounds per block rate (C: 281 B per vote; checkpoint every 30 BLUE blocks as C1 is written)")
P("| bps | checkpoints per day | votes per block at V voters (V/30, C1 in blue blocks) | drain capacity per checkpoint, cap 48 / 384 | vote bytes per day, V = 100 / 1,000 / 8,192 (single votes) | the same if C1 were 30 DAA seconds |")
P("|---|---|---|---|---|---|")
for b in BPS:
cps = 86400 * b / INPUTS["cp_blue"]
vpb = " / ".join(f"{v/30:.1f}" for v in (100, 1000, 8192))
cap = f"{30*48:,} / {30*384:,}"
vb = " / ".join(fmt_bytes(v * cps * INPUTS["vote_bytes"]) for v in (100, 1000, 8192))
vb2 = " / ".join(fmt_bytes(v * 2880 * INPUTS["vote_bytes"]) for v in (100, 1000, 8192))
P(f"| {b} | {cps:,.0f} | {vpb} | {cap} | {vb} | {vb2} |")
P()
P("## 3. Bytes per node per day: receive every block once plus relay overhead (A: Kaspa's inv/request flow, degree 8), votes carried in bodies, one certificate per block")
P("| bps | header bytes (C+S) | blocks per day | block bytes per day (header + body + records) | relay overhead per day | votes per day at V = 12 / 100 / 1,000 / 8,192 (C1 in blue blocks) | certificates per day at V = 12 / 8,192 | total per day at V = 100 | total at V = 8,192 | Mbit/s mean at V = 8,192 |")
P("|---|---|---|---|---|---|---|---|---|---|")
for b in BPS:
hb = header_bytes(b); n = 86400 * b
blocks = n * (hb + INPUTS["body_no_votes"] + INPUTS["record_bytes"] / INPUTS["segment_chain_blocks"])
relay = n * (INPUTS["degree"] * INPUTS["inv_bytes"] + INPUTS["inv_bytes"])
cps = n / INPUTS["cp_blue"]
votes = {v: v * cps * INPUTS["vote_bytes"] for v in VOTERS}
certs = {v: n * (INPUTS["cert_fixed"] + v / 8) for v in VOTERS}
tot100 = blocks + relay + votes[100] + certs[100]; tot8k = blocks + relay + votes[8192] + certs[8192]
P(f"| {b} | {hb:.0f} | {n:,} | {fmt_bytes(blocks)} | {fmt_bytes(relay)} | {' / '.join(fmt_bytes(votes[v]) for v in VOTERS)} | {fmt_bytes(certs[12])} / {fmt_bytes(certs[8192])} | {fmt_bytes(tot100)} | {fmt_bytes(tot8k)} | {tot8k*8/86400/1e6:.2f} |")
P()
P("With certificates aggregated (lane 3's 1.2 KB per checkpoint) and votes NOT echoed in every body (one carriage), the vote column is the floor: a vote crosses the network once. A star hub multiplies its upload by its peer count: run A's hub sent 2.13 GB in 850 s to 41 peers (A.jsonl rx_tx, netns-wide counter, so an upper bound): 2.5 MB/s, 61 KB/s per peer.")
P()
P("## 4. Node CPU per block and the serial budget (the pipeline validates blocks in order; budget = 0.8 core)")
P("| bps | budget per block, ms | lottery verify class v3 / v4 / v4 half core (M) | BLS for V/30 votes per block at V = 100 / 1,000 / 8,192, cap 384 (A: 1.5 ms per verify) | run A hub measured total at mergeset 8 / 30 / 150+ (M) | fits the budget? |")
P("|---|---|---|---|---|---|")
for b in BPS:
budget = 800 / b
bls = " / ".join(f"{min(v/30, 384)*INPUTS['bls_verify_ms']:.0f}" for v in (100, 1000, 8192))
hub = " / ".join(str(v) for v in INPUTS["hub_ms_per_block"].values())
fits = "yes with margin" if budget > 120 else ("only the verify and a narrow DAG (under 25 ms per block measured nowhere yet)" if budget > 20 else "no: class v4 verify alone (4.9 to 8.2 ms) plus GHOSTDAG exceeds it")
P(f"| {b} | {budget:.0f} | 2.79 / 4.9 / 8.23 | {bls} | {hub} | {fits} |")
P()
P("## 5. RSS and disk per block rate (M: cache 256 MiB x KEEP_DAYS 3; slopes per block from M30 s8, the live app node, run A's hub; all slopes are growth over a run, not a proven steady state)")
P("| bps | caches | DAG store growth per day at 30 / 80 / 440 KB per block | growth over the 30-h pruning window at 30 / 80 KB per block | exec state (M: 128 MB snapshot at 141,700 chain blocks) | disk per day: blocks + votes at V = 1,000 + exec records |")
P("|---|---|---|---|---|---|")
for b in BPS:
n = 86400 * b
g = [n * kb * 1e3 for kb in (30.2, 80, 440)]
w = [INPUTS["pruning_s"] * b * kb * 1e3 for kb in (30.2, 80)]
hb = header_bytes(b); cps = n / 30
chain_blocks = n / (1 + INPUTS["parents_mean"][b]) # A: one chain block per mergeset of about 1 + parents blocks
disk = n * (hb + INPUTS["body_no_votes"]) + 1000 * cps * INPUTS["vote_bytes"] + chain_blocks * INPUTS["exec_record_kb"] * 1e3
P(f"| {b} | 768 MiB worst, 256 MiB flat | {' / '.join(fmt_bytes(x) for x in g)} | {' / '.join(fmt_bytes(x) for x in w)} | 128 MB + 1.5 KB per chain block | {fmt_bytes(disk)} |")
P()
P("## 6. Pruned and archival storage per year (C: pruning window 108,000 DAA s; A: 2x index overhead on blocks; votes single, one carriage)")
P("| bps | pruned node keeps (window of headers + bodies, 2x) | plus votes in the window at V = 1,000 | archival per year: blocks (2x) | archival votes per year at V = 100 / 1,000 / 8,192 (C1 in blue blocks) | archival votes per year if certificates only (1.2 KB per checkpoint, lane 3) |")
P("|---|---|---|---|---|---|")
for b in BPS:
hb = header_bytes(b); win = INPUTS["pruning_s"] * b
kept = 2 * win * (hb + INPUTS["body_no_votes"]); kv = 1000 * (win / 30) * INPUTS["vote_bytes"]
yr = 31557600 * b
arch = 2 * yr * (hb + INPUTS["body_no_votes"]); cps_y = yr / 30
av = " / ".join(fmt_bytes(v * cps_y * INPUTS["vote_bytes"]) for v in (100, 1000, 8192))
P(f"| {b} | {fmt_bytes(kept)} | {fmt_bytes(kv)} | {fmt_bytes(arch)} | {av} | {fmt_bytes(cps_y * 1200)} |")
P()
P("## 7. Subsidy per block and payout intervals (C: 31.688 IGN per DAA s at full ramp, 80% to the producer; interval = 1 / (bps x share))")
P("| bps | subsidy per block, IGN | miner's 80%, IGN | " + " | ".join(f"{t} at {nname}" for nname in INPUTS["networks"] for t in ("4070", "5090", "rig")) + " |")
P("|---|---|---|" + "---|" * (3 * len(INPUTS["networks"])))
for b in BPS:
sub = INPUTS["ign_per_s"] / b
cells = []
for nname, nh in INPUTS["networks"].items():
for t, h in INPUTS["tiers"].items():
cells.append(fmt_time(1 / (b * h / nh)))
P(f"| {b} | {sub:.3f} | {sub*INPUTS['miner_share']:.3f} | " + " | ".join(cells) + " |")
P()
P("## 8. The solo-miner question: a 4070 at 25 MH/s, blocks per day and the rate needed for one block a day")
P("| network hash | share | blocks per day at 1 bps | bps for one block a day | IGN per day at 1 bps (full ramp, 80%) |")
P("|---|---|---|---|---|")
for nname, nh in INPUTS["networks"].items():
share = 25e6 / nh; bpd = 86400 * share
P(f"| {nname} | {share:.2e} | {bpd:.2f} | {1/bpd:.4f} | {bpd * INPUTS['ign_per_s'] * INPUTS['miner_share']:,.1f} |")
P()
P("## 9. Finality timing per block rate if C1 and d stay in blue blocks (C: spec 03 C1; the lock about 3 s after determination, lane 4's reading of 3.11.3)")
P("| bps | checkpoint cadence, s | determination after the checkpoint, s | lock after the checkpoint, s | checkpoints per presence window of 240 indices, minutes | 30-day window in blue blocks |")
P("|---|---|---|---|---|---|")
for b in BPS:
P(f"| {b} | {30/b:g} | {60/b:g} | {60/b + 3:g} | {240*30/b/60:g} | {2592000*b:,} |")
P()
P("## 10. Light client bytes per day (C: spec 10.5 shape; checkpoint mode 2,880 x (header 400 + certificate + proof 400) at 1 bps; scales with checkpoints per day if C1 stays in blue blocks)")
P("| bps | checkpoints per day | checkpoint mode at 1,000 voters | at 10,000 voters | phase two (800 B per checkpoint) |")
P("|---|---|---|---|---|")
for b in BPS:
cps = 2880 * b
P(f"| {b} | {cps:,} | {fmt_bytes(cps * (400 + 273 + 125 + 400))} | {fmt_bytes(cps * (400 + 273 + 1250 + 400))} | {fmt_bytes(cps * 800)} |")
print("\n".join(out))

View file

@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""Propagation and red-share model for the Horizon network lane (lane 5, 6 October 2026).
What it models:
* Poisson block production at `bps` blocks per second, split evenly over the mining nodes.
* A topology: `star` (every miner peers with one hub only, the Devnet 2 run A shape: tools/fleet/box-dn2.sh passes a
single --addpeer=<seed>, the collector read peers=41 at the seed and peers=1 at every pod) or `mesh` (every node
has `degree` random peers, blocks flood by gossip, each node forwards a block once to the peers that have not
announced it).
* One relay hop = inv, request, block: three link traversals (measured in the M21 run, docs/fud-ledger.md M21:
one hop p50 318 to 329 ms on 100-ms proxied links) plus the receiver's own processing (6 to 16 ms measured there).
The link latency is lognormal with median `--link` ms and sigma 0.29 (the cloud devnet's p50 343 / p90 497 ms
spread, docs/bench-log.md "4 October 2026, cloud devnet"). `--hop` sets the hop median directly instead.
* The hub (star only) is a single server: validating a block costs s0 + s1 x mergeset ms (`--hub-s0`, `--hub-s1`);
blocks queue behind it. Run A's hub went from about 6 ms of CPU per accepted block at mergeset 8 to about 400 ms
at mergeset 150 to 200 (A.jsonl cpu_rss against the seed log's Processed lines), so the defaults are 6 and 2.
`--hub-s1 0 --hub-s0 0` is an ideal hub.
* GHOSTDAG colouring: for every block, selected parent = max blue work (hash tiebreak), mergeset = past minus the
selected parent's past, candidates in topological order; a candidate is blue when its anticone inside the blue
set of the new block's chain is at most k (the first k-cluster condition of
vendor/igneum-node/consensus/src/processes/ghostdag/protocol.rs). The second condition (every blue peer's own
anticone bound) is approximated by the candidate's check, so this undercounts reds slightly. Parents capped at
`--parents` by blue work (Kaspa's max_block_parents, consensus/core/src/config/bps.rs). The mergeset size
limit is applied: candidates past the limit are left for a later block (they stay in the anticone).
* Miners join over `--join-spread` seconds with a view of genesis only and mine at once (the fleet's
--enable-unsynced-mining start; run A's pods joined 19:23 to 19:29Z).
* `--schedule a.csv` replaces the constant rate with a per-minute production schedule (blocks per minute).
What it reports: cumulative red share (reds in the mergesets of the final selected chain, over all merged blocks),
mean mergeset size, mean tips at a miner, the natural selected-chain reorg depth at miner 0 (max and p99), the hub's
mean queueing delay and utilisation, mean end-to-end delay to 90% of nodes.
Run (the lock is the main checkout's):
/Users/joshm/Projects/igneum/tools/lock/with-lock.sh run nice -n 19 python3 sim/horizon/network/propagation.py --grid
... --topology star --bps 10 --k 124 --nodes 41 --link 40 --hub-s0 6 --hub-s1 2 --seconds 600 --join-spread 360
"""
import argparse, heapq, math, random, sys, time
def lognormal(rng, median, sigma):
return median * math.exp(rng.gauss(0.0, sigma))
class Dag:
def __init__(self, k, parents_cap, mergeset_cap, rng):
self.k, self.pcap, self.mcap, self.rng = k, parents_cap, mergeset_cap, rng
self.parents = [()]; self.past = [0]; self.sp = [-1]
self.blueset = [1] # bitset: blues of the block's chain incl. itself
self.nblue = [0]; self.nred = [0]
self.blue_work = [0]; self.hash = [0]; self.time = [0.0]; self.miner = [-1]
self.children = [0]
def key(self, b): return (self.blue_work[b], self.hash[b])
def anc(self, a, b): return (self.past[b] >> a) & 1
def add(self, parents, t, miner):
parents = sorted(set(parents), key=self.key, reverse=True)[: self.pcap]
sp = parents[0]
past = 0
for p in parents: past |= self.past[p] | (1 << p)
x = past & ~self.past[sp] & ~(1 << sp)
ms = []
while x:
lsb = x & -x; ms.append(lsb.bit_length() - 1); x ^= lsb
ms.sort(key=self.key)
blueset = self.blueset[sp]; nb = 1; nr = 0; k = self.k
for c in ms[: self.mcap]:
cand = blueset & ~self.past[c] & ~(1 << c)
n = 0; ok = True
while cand:
lsb = cand & -cand; y = lsb.bit_length() - 1; cand ^= lsb
if not self.anc(c, y):
n += 1
if n > k: ok = False; break
if ok: blueset |= (1 << c); nb += 1
else: nr += 1
idx = len(self.parents)
self.parents.append(tuple(parents)); self.past.append(past); self.sp.append(sp)
self.blueset.append(blueset | (1 << idx)); self.nblue.append(nb); self.nred.append(nr)
self.blue_work.append(self.blue_work[sp] + nb); self.hash.append(self.rng.getrandbits(62))
self.time.append(t); self.miner.append(miner); self.children.append(0)
for p in parents: self.children[p] += 1
return idx
def reorg_depth(self, old_tip, new_tip):
"""chain blocks of old_tip's selected chain that are not ancestors (or self) of new_tip"""
if old_tip == new_tip or self.anc(old_tip, new_tip): return 0
d = 0; cur = old_tip
while cur > 0 and not self.anc(cur, new_tip) and cur != new_tip:
d += 1; cur = self.sp[cur]
return d
class Node:
__slots__ = ("id", "known", "tips", "peers", "tip", "reorgs", "joined", "ntips")
def __init__(self, i):
self.id = i; self.known = 1; self.tips = {0}; self.peers = []; self.tip = 0; self.reorgs = []; self.joined = False; self.ntips = []
def simulate(a, rng):
dag = Dag(a.k, a.parents, a.mergeset, rng)
hub = a.topology == "star"
N = a.nodes + (1 if hub else 0)
nodes = [Node(i) for i in range(N)]
miners = list(range(1, N)) if hub else list(range(N))
if hub:
for m in miners: nodes[m].peers = [0]
nodes[0].peers = miners[:]
else:
for i in range(N):
others = [j for j in range(N) if j != i]
for j in rng.sample(others, min(a.degree, len(others))):
if j not in nodes[i].peers: nodes[i].peers.append(j)
if i not in nodes[j].peers: nodes[j].peers.append(i)
joins = {m: rng.uniform(0, a.join_spread) for m in miners}
sigma = 0.29
def hop_delay():
if a.hop: return lognormal(rng, a.hop, sigma) / 1000.0
return (3 * lognormal(rng, a.link, sigma) + a.proc) / 1000.0
ev = [] # (time, seq, kind, payload)
seq = 0
def push(t, kind, payload):
nonlocal seq; seq += 1; heapq.heappush(ev, (t, seq, kind, payload))
# production schedule
sched = None
if a.schedule:
sched = [float(l.split(",")[-1]) for l in open(a.schedule) if l.strip() and not l.startswith("#")]
def rate_at(t):
if sched is None: return a.bps
i = int(t // 60); return (sched[i] if i < len(sched) else sched[-1]) / 60.0
t = 0.0
r0 = rate_at(0.0)
push(rng.expovariate(r0) if r0 > 0 else 1e9, "mine", None)
hub_busy_until = 0.0; hub_wait = 0.0; hub_served = 0; hub_busy_total = 0.0
node_busy_until = [0.0] * N; node_wait = 0.0; node_served = 0; node_queued = [0] * N
arrivals = {} # block -> list of arrival times
produced = 0
def receive(n, b, now):
node = nodes[n]
if (node.known >> b) & 1: return False
node.known |= (1 << b)
for p in dag.parents[b]: node.tips.discard(p)
# b is a tip at this node unless a known child exists (rare: children arrive after parents in this model)
node.tips.add(b)
old = node.tip
if dag.key(b) > dag.key(old):
d = dag.reorg_depth(old, b)
if d > 0: node.reorgs.append(d)
node.tip = b
arrivals.setdefault(b, []).append(now)
return True
def hub_process(b, now):
nonlocal hub_busy_until, hub_wait, hub_served, hub_busy_total
start = max(now, hub_busy_until)
m = dag.nblue[b] + dag.nred[b]
s = (a.hub_s0 + a.hub_s1 * m) / 1000.0
hub_busy_until = start + s; hub_wait += start - now; hub_served += 1; hub_busy_total += s
push(hub_busy_until, "hubdone", b)
while ev:
t, _, kind, payload = heapq.heappop(ev)
if t > a.seconds: break
if kind == "mine":
r = rate_at(t)
push(t + (rng.expovariate(r) if r > 0 else 1e9), "mine", None)
active = [m for m in miners if joins[m] <= t]
if not active: continue
m = rng.choice(active); node = nodes[m]
if not node.joined: node.joined = True
node.ntips.append(len(node.tips))
b = dag.add(list(node.tips), t, m); produced += 1
receive(m, b, t)
for p in node.peers: push(t + hop_delay(), "arrive", (p, b))
elif kind == "arrive":
n, b = payload
if hub and n == 0:
if (nodes[0].known >> b) & 1: continue
nodes[0].known |= (1 << b) # accepted into the queue once
hub_process(b, t)
elif a.node_s0 or a.node_s1:
if (nodes[n].known >> b) & 1 or (node_queued[n] >> b) & 1: continue
node_queued[n] |= (1 << b)
start = max(t, node_busy_until[n]); m = dag.nblue[b] + dag.nred[b]
node_busy_until[n] = start + (a.node_s0 + a.node_s1 * m) / 1000.0
node_wait += start - t; node_served += 1
push(node_busy_until[n], "nodedone", (n, b))
else:
if receive(n, b, t):
for p in nodes[n].peers: push(t + hop_delay(), "arrive", (p, b))
elif kind == "nodedone":
n, b = payload
if receive(n, b, t):
for p in nodes[n].peers:
if p != dag.miner[b]: push(t + hop_delay(), "arrive", (p, b))
elif kind == "hubdone":
b = payload
nodes[0].known &= ~(1 << b); receive(0, b, t)
for p in nodes[0].peers:
if p != dag.miner[b]: push(t + hop_delay(), "arrive", (p, b))
# metrics from the hub's (or node 0's) final selected chain
tip = max(range(len(dag.parents)), key=dag.key)
blues = reds = chain = 0; ms_sizes = []
cur = tip
while cur > 0:
blues += dag.nblue[cur]; reds += dag.nred[cur]; chain += 1; ms_sizes.append(dag.nblue[cur] + dag.nred[cur])
cur = dag.sp[cur]
merged = blues + reds
red_share = reds / merged if merged else 0.0
m0 = nodes[miners[0]]
reorgs = sorted(m0.reorgs)
p99 = reorgs[int(0.99 * (len(reorgs) - 1))] if reorgs else 0
# delay to 90% of nodes
d90 = []
for b, arr in arrivals.items():
if len(arr) >= 0.9 * len(miners):
arr = sorted(arr); d90.append(arr[int(0.9 * len(miners)) - 1] - dag.time[b])
tips_mean = sum(sum(n.ntips) for n in nodes) / max(1, sum(len(n.ntips) for n in nodes))
return dict(produced=produced, merged=merged, red_share=red_share, chain=chain, ms_mean=sum(ms_sizes) / max(1, len(ms_sizes)),
ms_max=max(ms_sizes) if ms_sizes else 0, tips_mean=tips_mean, reorg_max=max(reorgs) if reorgs else 0, reorg_p99=p99,
hub_wait=hub_wait / max(1, hub_served), hub_util=hub_busy_total / max(a.seconds, 1e-9), d90=sum(d90) / max(1, len(d90)),
node_wait=node_wait / max(1, node_served),
blocks_per_s=produced / a.seconds)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--topology", default="mesh", choices=["mesh", "star"])
ap.add_argument("--bps", type=float, default=10); ap.add_argument("--k", type=int, default=124)
ap.add_argument("--nodes", type=int, default=41); ap.add_argument("--degree", type=int, default=8)
ap.add_argument("--parents", type=int, default=16); ap.add_argument("--mergeset", type=int, default=248)
ap.add_argument("--link", type=float, default=40.0, help="one-way link latency median, ms")
ap.add_argument("--hop", type=float, default=0.0, help="hop delay median, ms (overrides --link)")
ap.add_argument("--proc", type=float, default=10.0, help="receiver processing per hop, ms")
ap.add_argument("--hub-s0", type=float, default=0.0); ap.add_argument("--hub-s1", type=float, default=0.0)
ap.add_argument("--node-s0", type=float, default=0.0, help="every other node's per-block validation cost, ms")
ap.add_argument("--node-s1", type=float, default=0.0, help="plus this many ms per mergeset block")
ap.add_argument("--seconds", type=float, default=300); ap.add_argument("--join-spread", type=float, default=0.0)
ap.add_argument("--schedule", default=None); ap.add_argument("--seed", type=int, default=7)
ap.add_argument("--grid", action="store_true", help="the lane's grid: bps x hop delay, mesh of degree 8, ideal links")
ap.add_argument("--out", default=None)
a = ap.parse_args()
lines = []
def emit(s): print(s, flush=True); lines.append(s)
if a.grid:
from_k = {1: 18, 10: 124, 32: 362}
emit("| bps | k | hop delay median ms | red share | mergeset mean / max | tips mean | reorg max / p99 (miner 0) | delay to 90% of nodes s | blocks/s |")
emit("|---|---|---|---|---|---|---|---|---|")
for bps in (1, 10, 32, 100):
k = from_k.get(bps) or 1074
for hop in (50, 100, 300, 1000):
rng = random.Random(a.seed)
b = argparse.Namespace(**vars(a)); b.bps = bps; b.k = k; b.hop = hop; b.topology = "mesh"; b.hub_s0 = 0; b.hub_s1 = 0
b.mergeset = 180 if bps == 1 else min(512, 2 * k); b.parents = 10 if bps == 1 else 16
b.seconds = min(a.seconds, max(60.0, 12000.0 / bps))
t0 = time.time(); r = simulate(b, rng)
emit(f"| {bps} | {k} | {hop} | {100*r['red_share']:.1f}% | {r['ms_mean']:.1f} / {r['ms_max']} | {r['tips_mean']:.1f} | {r['reorg_max']} / {r['reorg_p99']} | {r['d90']:.2f} | {r['blocks_per_s']:.1f} |")
print(f"# ({b.seconds:.0f} s simulated in {time.time()-t0:.0f} s wall)", file=sys.stderr)
else:
rng = random.Random(a.seed); r = simulate(a, rng)
emit(f"{a.topology} bps={a.bps} k={a.k} nodes={a.nodes} degree={a.degree} link={a.link} hop={a.hop} hub=({a.hub_s0},{a.hub_s1}) node=({a.node_s0},{a.node_s1}) join={a.join_spread} sched={a.schedule} seconds={a.seconds} seed={a.seed}")
emit(f"produced {r['produced']} ({r['blocks_per_s']:.2f}/s) merged {r['merged']} red share {100*r['red_share']:.1f}% chain {r['chain']} mergeset mean {r['ms_mean']:.1f} max {r['ms_max']} tips {r['tips_mean']:.1f} reorg max {r['reorg_max']} p99 {r['reorg_p99']} hub wait {r['hub_wait']:.2f}s util {r['hub_util']:.2f} node wait {r['node_wait']:.2f}s d90 {r['d90']:.2f}s")
if a.out:
with open(a.out, "a") as f: f.write("\n".join(lines) + "\n")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,48 @@
# propagation.py results, batch 2 (per-block node cost), 2026-10-06T20:07:52Z, Mac, under with-lock run, seed 7
## E. Run A replay with the hub's MEASURED per-block CPU (61, 115, 345 ms per accepted block; A.jsonl cpu_rss deltas over accepted deltas), link 40 ms
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(61.0,0.0) node=(0.0,0.0) join=0.0 sched=sim/horizon/network/runA-schedule.csv seconds=1200.0 seed=7
produced 12812 (10.68/s) merged 12812 red share 46.8% chain 1461 mergeset mean 8.8 max 194 tips 20.2 reorg max 43 p99 4 hub wait 26.35s util 0.65 node wait 0.00s d90 26.71s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(115.0,0.0) node=(0.0,0.0) join=0.0 sched=sim/horizon/network/runA-schedule.csv seconds=1200.0 seed=7
produced 12815 (10.68/s) merged 9108 red share 88.1% chain 441 mergeset mean 20.7 max 199 tips 21.6 reorg max 2 p99 2 hub wait 320.84s util 1.23 node wait 0.00s d90 276.55s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(345.0,0.0) node=(0.0,0.0) join=0.0 sched=sim/horizon/network/runA-schedule.csv seconds=1200.0 seed=7
produced 12746 (10.62/s) merged 3509 red share 82.6% chain 358 mergeset mean 9.8 max 45 tips 9.9 reorg max 3 p99 2 hub wait 1768.55s util 3.66 node wait 0.00s d90 439.87s
## F. The knee: star at a constant 10 bps, hub per-block cost 20 to 150 ms, 600 s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(20.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 5923 (9.87/s) merged 5916 red share 0.0% chain 1712 mergeset mean 3.5 max 10 tips 3.5 reorg max 2 p99 2 hub wait 0.00s util 0.20 node wait 0.00s d90 0.33s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(40.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 5906 (9.84/s) merged 5900 red share 0.0% chain 1592 mergeset mean 3.7 max 10 tips 3.7 reorg max 3 p99 2 hub wait 0.01s util 0.39 node wait 0.00s d90 0.36s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(60.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6072 (10.12/s) merged 6065 red share 0.0% chain 1478 mergeset mean 4.1 max 12 tips 4.1 reorg max 3 p99 2 hub wait 0.05s util 0.61 node wait 0.00s d90 0.41s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(80.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6101 (10.17/s) merged 6096 red share 0.0% chain 1244 mergeset mean 4.9 max 20 tips 5.2 reorg max 4 p99 2 hub wait 0.18s util 0.81 node wait 0.00s d90 0.57s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(100.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6089 (10.15/s) merged 5947 red share 15.2% chain 293 mergeset mean 20.3 max 116 tips 22.6 reorg max 8 p99 7 hub wait 8.50s util 1.01 node wait 0.00s d90 8.81s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(150.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6048 (10.08/s) merged 3986 red share 75.2% chain 146 mergeset mean 27.3 max 66 tips 21.8 reorg max 3 p99 3 hub wait 156.83s util 1.51 node wait 0.00s d90 105.57s
## G. Mesh of degree 8 where EVERY node pays the per-block cost (40, 80, 115 ms), 10 bps, 600 s
mesh bps=10.0 k=124 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) node=(40.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 5763 (9.61/s) merged 5763 red share 0.0% chain 1874 mergeset mean 3.1 max 9 tips 3.0 reorg max 3 p99 2 hub wait 0.00s util 0.00 node wait 0.01s d90 0.34s
mesh bps=10.0 k=124 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) node=(80.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6034 (10.06/s) merged 6030 red share 0.0% chain 1356 mergeset mean 4.4 max 18 tips 4.3 reorg max 3 p99 2 hub wait 0.00s util 0.00 node wait 0.12s d90 0.66s
mesh bps=10.0 k=124 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) node=(115.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6069 (10.12/s) merged 5318 red share 52.4% chain 159 mergeset mean 33.4 max 160 tips 15.2 reorg max 16 p99 11 hub wait 0.00s util 0.00 node wait 25.98s d90 46.48s
## H. Mesh at 1 bps with the same per-block costs (the control), 600 s
mesh bps=1.0 k=18 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) node=(115.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 645 (1.07/s) merged 645 red share 0.0% chain 455 mergeset mean 1.4 max 5 tips 1.4 reorg max 2 p99 1 hub wait 0.00s util 0.00 node wait 0.01s d90 0.49s
mesh bps=1.0 k=18 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) node=(345.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 562 (0.94/s) merged 562 red share 0.0% chain 341 mergeset mean 1.6 max 7 tips 1.7 reorg max 2 p99 2 hub wait 0.00s util 0.00 node wait 0.07s d90 1.08s
## I. Star at 10 bps, ideal hub, link latency 40 to 300 ms (pure latency)
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 5906 (9.84/s) merged 5901 red share 0.0% chain 1802 mergeset mean 3.3 max 9 tips 3.3 reorg max 3 p99 2 hub wait 0.00s util 0.00 node wait 0.00s d90 0.30s
star bps=10.0 k=124 nodes=41 degree=8 link=100.0 hop=0.0 hub=(0.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 5954 (9.92/s) merged 5948 red share 0.0% chain 1017 mergeset mean 5.8 max 15 tips 5.9 reorg max 3 p99 2 hub wait 0.00s util 0.00 node wait 0.00s d90 0.73s
star bps=10.0 k=124 nodes=41 degree=8 link=200.0 hop=0.0 hub=(0.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 6083 (10.14/s) merged 6075 red share 0.0% chain 609 mergeset mean 10.0 max 20 tips 9.4 reorg max 4 p99 3 hub wait 0.00s util 0.00 node wait 0.00s d90 1.45s
star bps=10.0 k=124 nodes=41 degree=8 link=300.0 hop=0.0 hub=(0.0,0.0) node=(0.0,0.0) join=0.0 sched=None seconds=600.0 seed=7
produced 5862 (9.77/s) merged 5847 red share 0.0% chain 472 mergeset mean 12.4 max 23 tips 11.8 reorg max 4 p99 3 hub wait 0.00s util 0.00 node wait 0.00s d90 2.16s
DONE 20:09:57

View file

@ -0,0 +1,48 @@
# propagation.py results, 2026-10-06T20:06:05Z, Mac, under with-lock run, seed 7
## A. Run A replay: star, 41 miners through one hub, measured per-minute production, join spread 360 s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(6.0,2.0) join=360.0 seconds=1500.0 seed=7
produced 14013 (9.34/s) merged 14011 red share 0.0% chain 3834 mergeset mean 3.7 max 15 tips 4.3 reorg max 3 p99 2 hub wait 0.00s util 0.14 d90 0.32s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) join=360.0 seconds=1500.0 seed=7
produced 13812 (9.21/s) merged 13811 red share 0.0% chain 3901 mergeset mean 3.5 max 14 tips 4.1 reorg max 4 p99 2 hub wait 0.00s util 0.00 d90 0.31s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(6.0,2.0) join=0.0 seconds=1500.0 seed=7
produced 13833 (9.22/s) merged 13833 red share 0.0% chain 3737 mergeset mean 3.7 max 19 tips 4.5 reorg max 3 p99 2 hub wait 0.01s util 0.14 d90 0.33s
## B. Star against mesh at 10 bps, constant rate, 600 s, link 40 ms
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) join=0.0 seconds=600.0 seed=7
produced 5906 (9.84/s) merged 5901 red share 0.0% chain 1802 mergeset mean 3.3 max 9 tips 3.3 reorg max 3 p99 2 hub wait 0.00s util 0.00 d90 0.30s
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(6.0,2.0) join=0.0 seconds=600.0 seed=7
produced 5973 (9.96/s) merged 5970 red share 0.0% chain 1741 mergeset mean 3.4 max 12 tips 3.5 reorg max 3 p99 2 hub wait 0.00s util 0.13 d90 0.32s
mesh bps=10.0 k=124 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) join=0.0 seconds=600.0 seed=7
produced 6038 (10.06/s) merged 6034 red share 0.0% chain 2298 mergeset mean 2.6 max 9 tips 2.6 reorg max 2 p99 2 hub wait 0.00s util 0.00 d90 0.24s
mesh bps=10.0 k=124 nodes=42 degree=4 link=40.0 hop=0.0 hub=(0.0,0.0) join=0.0 seconds=600.0 seed=7
produced 6003 (10.01/s) merged 6001 red share 0.0% chain 2048 mergeset mean 2.9 max 9 tips 2.9 reorg max 2 p99 2 hub wait 0.00s util 0.00 d90 0.32s
mesh bps=10.0 k=124 nodes=42 degree=8 link=150.0 hop=0.0 hub=(0.0,0.0) join=0.0 seconds=600.0 seed=7
produced 6090 (10.15/s) merged 6085 red share 0.0% chain 1066 mergeset mean 5.7 max 15 tips 5.3 reorg max 4 p99 3 hub wait 0.00s util 0.00 d90 0.84s
## C. Star at 10 bps with a genesis burst (26 blocks/s for 5 min) and the measured hub cost
star bps=10.0 k=124 nodes=41 degree=8 link=40.0 hop=0.0 hub=(6.0,2.0) join=0.0 seconds=600.0 seed=7
produced 10840 (18.07/s) merged 10836 red share 0.0% chain 1995 mergeset mean 5.4 max 17 tips 5.9 reorg max 3 p99 2 hub wait 0.01s util 0.33 d90 0.33s
mesh bps=10.0 k=124 nodes=42 degree=8 link=40.0 hop=0.0 hub=(0.0,0.0) join=0.0 seconds=600.0 seed=7
produced 10727 (17.88/s) merged 10724 red share 0.0% chain 2824 mergeset mean 3.8 max 11 tips 3.9 reorg max 3 p99 2 hub wait 0.00s util 0.00 d90 0.24s
## D. The grid: mesh of degree 8, 42 nodes, ideal links, hop delay median 50 to 1,000 ms
| bps | k | hop delay median ms | red share | mergeset mean / max | tips mean | reorg max / p99 (miner 0) | delay to 90% of nodes s | blocks/s |
|---|---|---|---|---|---|---|---|---|
| 1 | 18 | 50 | 0.0% | 1.1 / 2 | 1.1 | 1 / 1 | 0.09 | 1.0 |
| 1 | 18 | 100 | 0.0% | 1.2 / 3 | 1.2 | 1 / 1 | 0.18 | 1.0 |
| 1 | 18 | 300 | 0.0% | 1.4 / 5 | 1.4 | 1 / 1 | 0.55 | 1.0 |
| 1 | 18 | 1000 | 0.0% | 2.3 / 7 | 2.4 | 2 / 2 | 1.82 | 1.0 |
| 10 | 124 | 50 | 0.0% | 1.7 / 6 | 1.7 | 2 / 1 | 0.09 | 10.0 |
| 10 | 124 | 100 | 0.0% | 2.3 / 7 | 2.3 | 2 / 2 | 0.18 | 10.1 |
| 10 | 124 | 300 | 0.0% | 4.2 / 12 | 4.1 | 3 / 2 | 0.55 | 9.7 |
| 10 | 124 | 1000 | 0.0% | 9.2 / 22 | 8.1 | 4 / 3 | 1.82 | 9.9 |
| 32 | 362 | 50 | 0.0% | 2.9 / 9 | 2.9 | 3 / 2 | 0.09 | 32.1 |
| 32 | 362 | 100 | 0.0% | 4.4 / 14 | 4.3 | 3 / 2 | 0.18 | 31.7 |
| 32 | 362 | 300 | 0.0% | 9.2 / 26 | 8.0 | 5 / 3 | 0.55 | 32.1 |
| 32 | 362 | 1000 | 0.0% | 18.8 / 42 | 14.4 | 6 / 5 | 1.82 | 31.6 |
| 100 | 1074 | 50 | 0.0% | 6.0 / 17 | 5.6 | 4 / 3 | 0.09 | 100.4 |
| 100 | 1074 | 100 | 0.0% | 9.4 / 22 | 8.2 | 4 / 3 | 0.18 | 99.1 |
| 100 | 1074 | 300 | 0.0% | 18.5 / 40 | 14.2 | 6 / 5 | 0.55 | 101.4 |
| 100 | 1074 | 1000 | 0.0% | 31.1 / 104 | 25.3 | 8 / 8 | 1.82 | 100.5 |
DONE 20:08:58

View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# the lane's propagation runs; output appended to results.md (every line is the simulator's)
cd /Users/joshm/Projects/igneum-wt-horizon
P="python3 sim/horizon/network/propagation.py"
O=sim/horizon/network/results.md
echo "# propagation.py results, $(date -u +%FT%TZ), Mac, under with-lock run, seed 7" > $O
echo "" >> $O; echo "## A. Run A replay: star, 41 miners through one hub, measured per-minute production, join spread 360 s" >> $O
$P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 6 --hub-s1 2 --join-spread 360 --schedule sim/horizon/network/runA-schedule.csv --seconds 1500 --out $O
$P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 0 --hub-s1 0 --join-spread 360 --schedule sim/horizon/network/runA-schedule.csv --seconds 1500 --out $O
$P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 6 --hub-s1 2 --join-spread 0 --schedule sim/horizon/network/runA-schedule.csv --seconds 1500 --out $O
echo "" >> $O; echo "## B. Star against mesh at 10 bps, constant rate, 600 s, link 40 ms" >> $O
$P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 0 --hub-s1 0 --seconds 600 --out $O
$P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 6 --hub-s1 2 --seconds 600 --out $O
$P --topology mesh --nodes 42 --degree 8 --bps 10 --k 124 --link 40 --seconds 600 --out $O
$P --topology mesh --nodes 42 --degree 4 --bps 10 --k 124 --link 40 --seconds 600 --out $O
$P --topology mesh --nodes 42 --degree 8 --bps 10 --k 124 --link 150 --seconds 600 --out $O
echo "" >> $O; echo "## C. Star at 10 bps with a genesis burst (26 blocks/s for 5 min) and the measured hub cost" >> $O
printf '# burst then target\n0,1559\n1,1559\n2,1559\n3,1559\n4,1559\n5,600\n6,600\n7,600\n8,600\n9,600\n' > sim/horizon/network/burst-schedule.csv
$P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 6 --hub-s1 2 --schedule sim/horizon/network/burst-schedule.csv --seconds 600 --out $O
$P --topology mesh --nodes 42 --degree 8 --bps 10 --k 124 --link 40 --schedule sim/horizon/network/burst-schedule.csv --seconds 600 --out $O
echo "" >> $O; echo "## D. The grid: mesh of degree 8, 42 nodes, ideal links, hop delay median 50 to 1,000 ms" >> $O
$P --grid --nodes 42 --degree 8 --seconds 400 --out $O
echo "DONE $(date -u +%T)" >> $O

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
cd /Users/joshm/Projects/igneum-wt-horizon
P="python3 sim/horizon/network/propagation.py"
O=sim/horizon/network/results-2.md
echo "# propagation.py results, batch 2 (per-block node cost), $(date -u +%FT%TZ), Mac, under with-lock run, seed 7" > $O
echo "" >> $O; echo "## E. Run A replay with the hub's MEASURED per-block CPU (61, 115, 345 ms per accepted block; A.jsonl cpu_rss deltas over accepted deltas), link 40 ms" >> $O
for s0 in 61 115 345; do $P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 $s0 --schedule sim/horizon/network/runA-schedule.csv --seconds 1200 --out $O; done
echo "" >> $O; echo "## F. The knee: star at a constant 10 bps, hub per-block cost 20 to 150 ms, 600 s" >> $O
for s0 in 20 40 60 80 100 150; do $P --topology star --nodes 41 --bps 10 --k 124 --link 40 --hub-s0 $s0 --seconds 600 --out $O; done
echo "" >> $O; echo "## G. Mesh of degree 8 where EVERY node pays the per-block cost (40, 80, 115 ms), 10 bps, 600 s" >> $O
for s0 in 40 80 115; do $P --topology mesh --nodes 42 --degree 8 --bps 10 --k 124 --link 40 --node-s0 $s0 --seconds 600 --out $O; done
echo "" >> $O; echo "## H. Mesh at 1 bps with the same per-block costs (the control), 600 s" >> $O
for s0 in 115 345; do $P --topology mesh --nodes 42 --degree 8 --bps 1 --k 18 --parents 10 --mergeset 180 --link 40 --node-s0 $s0 --seconds 600 --out $O; done
echo "" >> $O; echo "## I. Star at 10 bps, ideal hub, link latency 40 to 300 ms (pure latency)" >> $O
for l in 40 100 200 300; do $P --topology star --nodes 41 --bps 10 --k 124 --link $l --seconds 600 --out $O; done
echo "DONE $(date -u +%T)" >> $O

View file

@ -0,0 +1,35 @@
# Devnet 2 run A, 6 October 2026: the seed's "PoW accepted" lines per minute from 19:25Z (box clock 21:25+02:00), /home/build/dn2seed-A.log on igneum-build-1
19:25,6
19:26,350
19:27,1223
19:28,672
19:29,1271
19:30,1559
19:31,1114
19:32,1113
19:33,934
19:34,765
19:35,389
19:36,510
19:37,503
19:38,487
19:39,466
19:40,349
19:41,311
19:42,233
19:43,394
19:44,239
19:45,235
19:46,198
19:47,215
19:48,238
19:49,208
19:50,236
19:51,178
19:52,185
19:53,181
19:54,182
19:55,199
19:56,206
19:57,224
19:58,158
Can't render this file because it contains an unexpected character in line 1 and column 46.