Merge master 125e16c3d into igneum-1p5x-research-plan-docs under the master-landing lock

This commit is contained in:
igneum-labs 2026-10-09 09:13:40 +00:00
commit 522f94238d
28 changed files with 8922 additions and 54 deletions

View file

@ -0,0 +1,26 @@
# X0 decider: class v4 against the frozen class v6 on one rented RTX 5090 (9 October 2026, 08:54 to 08:57 UTC)
One session, one card: a rented RTX 5090 (32 GB, driver 580.105.08, the provider's CUDA 12.8 image), the gen-6 Linux CUDA
worker (sha256 `804a6f7f…`, built from the frozen tree's proto-cuda), 250 batches of 2^24 at `--block-warps 1` per pack, nvidia-smi
board power at 1 Hz, watts = the mean of `power.draw` from 8 s in to the end of the bench, idle not subtracted. Stock only: the
provider refuses `-lgc` ("The current user does not have permission to change clocks"), as every rented host of 8 October did.
| Pack | Class | Id | MH/s | W | nJ per hash | Fingerprint | Check |
|---|---|---|---|---|---|---|---|
| v4-devnet-epoch0 (the 7 October numerator's pack) | mx8-erad810f22d+sh256x27, generator 4 | `0xa785001687d8688a` | 141.963 | 500.0 (the card's power limit) | 3,522 | `e370fb2080b7dbb1` | PASS |
| hl-v6-all (the l8off kit's export, the house knee rows' pack) | mx8-erad810f22d+sh256x27+state+reg64c+fold+rw | `0x9d40978601a7df2a` | 70.900 | 445.1 | 6,278 | `59e6708e46f1e87c` | PASS |
| hl-v6-all-cs (the signing object) | the same class, generator 6 | `0x2a1d6caab4c24564` | 70.650 | 457.1 | 6,470 | `01f51b9d4805e5e6` (first CUDA read) | PASS |
Ratios, class v6 over class v4: joules per hash 1.78x (the kit pack) and 1.84x (the signing object); rate 2.00x (the frozen
class draws twice the loads per hash). The house rig's RTX 5090 reads the same pair at 1.68x at stock (3,475 against 5,842 nJ,
the 7 October grid against the 9 October knee rows) and 1.73x at the 1,300 MHz knee (2,319 against 4,011), so across three
sessions the figure is 1.7 to 1.8x.
Meaning: class v6 costs the 5090 about 1.75x class v4's joules per hash and half its rate, so the 7 October numerator (2.33 µJ
per hash at the lock on class v4) understates the frozen object's card cost by about 1.7x; the board ratio moves toward the
2.6x side of the X0 order, and the chip's shadow op count is to be re-read on the class v6 draw (its window, index fold and
re-weight add card cost the class v4 count does not carry). The only lock row for the frozen object is the house 5090's
4.01 µJ per hash (the 9 October knee rows).
Evidence: build-1 `/srv/artefacts/tas/x0-pins/x0-run-fk-5090c/` (run-x0.log `9bc40685…`, smi.csv `7ab9f16f…`, the three bench
logs; SHA256SUMS.txt beside them); the bundle `x0-bundle.tgz` (`864470a7…`) with the run script.

View file

@ -13,18 +13,21 @@ build-1:/srv/queue/build-queue.md (the reader reads that one; this file is its s
- An entry carries its owner lane, its box, its pid-file path and its clock (UK). A box with no live pid file from its queue and
a load under 1.0 for a 30-minute read is a fault, reported by the reader to the coordinator (/srv/queue/faults.log).
- A lane that finishes an entry replaces it with the next or hands the box back here with a line; an empty box is the fault.
- Main's rule (9 October 2026, 09:5x UK): an owner starts its matrix job on its box under /srv/queue/pids within 15 minutes of a read that
found the box idle, or the reader marks the box unclaimed (open to any lane, written in status.json and faults.log); a box idle for
30 minutes is a red against its owner on the steward's board. A shared box names every owner and none ends another's pid.
## Owners (one lane per box)
| box | threads | owner lane | standing use |
|---|---|---|---|
| build-1 | 96 | build-server lane | cuts and kits, the hands (observer-node, node1), the capacity fuzz slices, the workers page, the queue reader |
| build-2 | 96 | site lane | the site gate (Playwright), the scene-parity suites; spare slots for the node lane's suites |
| build-2 | 96 | site lane and the adversary lane (shared; neither ends the other's pid) | the site gate (Playwright), the scene-parity suites; X5's dram design beside them |
| build-3 | 32 | node lane | the long consensus fuzz and property suites (kaspa-consensus, kaspa-consensus-core) |
| build-4 | 96 | adversary lane | the chip model (OpenROAD, kepler-formal): the 20 to 25 percent floorplan for the converged SPEF row |
| build-5 | 32 vCPU | research lane | TV-02's two independent supply-replay implementations, CPU only |
| build-6 | 32 vCPU | HEAL lane | the long p2p and exec property suites (kaspa-p2p-flows, igneum-exec) |
| build-7 | 96 | node lane | the 2.0.3 flows items: their suites and known-failed tests; the heal-on harness |
| build-5 | 32 vCPU | adversary lane (X5 hyb50), the TV-02 lane a light guest for minutes | X5's opponent-sweep variant; TV-02's last comparison run beside it (one core, under five minutes) |
| build-6 | 32 vCPU | adversary lane (X5 hyb75) and the HEAL lane (shared) | X5's opponent-sweep variant; the long p2p and exec property suites (kaspa-p2p-flows, igneum-exec) |
| build-7 | 96 | node lane and the relay lane (shared; neither ends the other's pid) | the 2.0.3 flows items: their suites and known-failed tests; the heal-on harness; the standing Windows canary VM (docker winvm, 4 vCPU, 8 GB, /srv/winvm/up.pid) |
| build-8 | 96 | fleet lane | 2.0.3 kit canaries the moment a chain moves; the heal-off harness; the dn4 roll node |
| build-9 | 96 | steward | the board's rows that need only a box (ZKP, EVM, VER fixtures), the pow fuzz on the side |
@ -36,11 +39,12 @@ build-1:/srv/queue/build-queue.md (the reader reads that one; this file is its s
| 2 | long fuzz and property suites, consensus (kaspa-consensus, kaspa-consensus-core) | node lane | build-3 | /srv/queue/pids/build-3-consensus-fuzz.pid | from 09:45, continuous |
| 3 | long fuzz and property suites, exec and p2p (igneum-exec, kaspa-p2p-flows, kaspa-p2p-lib) | HEAL lane | build-6 | /srv/queue/pids/build-6-exec-p2p-fuzz.pid | from 09:45, continuous |
| 4 | long fuzz and property suites, pow (kaspa-pow, igneum-pow mixer and scratch) | node lane | build-9 | /srv/queue/pids/build-9-pow-fuzz.pid | from 09:45, continuous |
| 5 | TV-02: two independent supply-replay implementations, CPU only | research lane | build-5 | /srv/queue/pids/build-5-tv02-replay.pid | from 10:00, until both agree |
| 5 | TV-02: two independent supply-replay implementations, CPU only: DONE 09:35 UK (A on build-5 in 38 s, B on build-6 in 3 min); the last comparison on the TV-01 spec runs as a guest under build-5-tv02.pid and build-6-tv02.pid by 11:30 | TV-02 lane | build-5, build-6 | /srv/queue/pids/build-5-tv02.pid, build-6-tv02.pid | by 11:30, minutes |
| 6 | chip model: the 20 to 25 percent floorplan for the converged SPEF row (about four host-hours) | adversary lane | build-4 | /srv/queue/pids/build-4-floorplan.pid | running (OpenROAD 844081, kepler-formal 1797623), about 13:30 |
| 7 | the board's rows that need only a box: ZKP, EVM and VER fixtures (the steward names them) | steward | build-9 | /srv/queue/pids/build-9-board-rows.pid | from 10:00, by row |
| 8 | 2.0.3 kit canaries the moment a chain moves (the evidence kits under /srv/workers/fleet) | fleet lane | build-8 | /srv/queue/pids/build-8-kit-canary.pid | on the chain's move |
| 9 | the site gate and the scene-parity suites | site lane | build-2 | /srv/queue/pids/build-2-site-gate.pid | on each landing |
| 11 | the standing Windows canary VM on build-7 (docker winvm; its collector posts to the intake as the Windows canary card) | relay lane | build-7 | /srv/queue/pids/build-7-winvm.pid (mirrors /srv/winvm/up.pid) | from 10:00, standing |
| 10 | cuts, kits, the hands, the capacity fuzz slices, the workers page, the queue reader | build-server lane | build-1 | /srv/queue/queue-reader.pid and the cut pid files under the cutter's scratch | continuous |
A lane starts its entry by writing its pid file (`<pid> <start UTC> <label>`, the format of the slot files) under /srv/queue/pids on

View file

@ -0,0 +1,47 @@
# TV-01, Phase 0: the supply spec
Gate TV-01 ("Resolve and freeze the monetary contract", rules-and-gates.json sha256 `10ce307329e1663dcb6dce849317c9276c19252de5f483e9ee06015fc77cbe3d`). Verdict: **NOT RUN**. This is a document and its tests. Nothing here activates anything.
## Files
| File | What it is |
|---|---|
| `supply-spec.json` | the machine-readable spec: a schema block, the cap, the rules, the activation clocks, two schedule profiles (each an ordered list of phases with start DAA score, per-block subsidy integer, rule name and source line), the coinbase split, fees, supply accounting, the superseded tail-vote text, the conflicts table, and the sources by sha |
| `supply-spec.md` | the same in words, with the conflicts table, the D01 resolution and the founder's four questions |
| `../../../../../../tools/token-value/tv-01/gen-spec.py` | the generator; every figure in both files is its output |
| `../../../../../../tools/token-value/tv-01/check-spec.py` | the checker: four known-failed cases first, then the spec summed to the cap in exact integers |
| `registry-batch-tv-01-20261009-01.json` | the registry batch for the CI steward (TV-01 NOT RUN with the evidence paths); kept here, not under tools/ci/batches/, because the map has no token-value cell until the steward adds one |
| `../../../../../../tools/token-value/tv-01/code-dump/` | the consensus code's own schedule (`tv01_schedule.rs.txt`, copied into the node tree as `consensus/core/examples/tv01_schedule.rs`, run on build-9 against bf60948a) and its output, `schedule-bf60948a.jsonl` (sha256 `3e4b5ac3c2f43057a13ace7d781ad714e385a94105623c0e0dac4adc417d8056`) |
## Sources
| Source | Revision | sha256 |
|---|---|---|
| site/litepaper.html | igneum master 9c4ed1f06100c27172b05373739c337a10cfd93e (C01's superseded sentence at 6824d49cc32def4463b643293a5622545618f7a9) | `aec583e6e868eb2d3aa8474df3f8892eeebba8a98c887a019ea86bd2efc2f786` |
| igneum-node: release-2.0.2-node a284380b, the shipped 2.0.2 commit 5d53a591, release-2.0.3-node-k6 bf60948a, release-2.0.3-node-k7 27f54124 | full shas in `supply-spec.json` `sources.node.revisions` | per file in `sources.node.files`; every cited line is anchored at all four |
| pool/src/payout.rs, config.rs, pplns.rs; docs/spec/05-fees-and-economics.md; docs/plans/ledger-decisions.md | igneum master 9c4ed1f0 | in `supply-spec.json` `sources.pool` and `sources.design` |
## Method
1. I read the volume's VR-01, VR-02, TV-01, TV-04 and D01, and the litepaper's supply, fee and security sections.
2. I grepped the node fork's consensus crate and execution crate for subsidy, coinbase, deflationary and emission, then read the emission table, the coinbase manager, the network objects, the genesis blocks and the EVM reward rule.
3. I transcribed the schedule constants into the generator. Each one is anchored to its line at both node revisions.
4. The generator computes both profiles in exact integers. It compares every phase, both ramps and both totals with the code dump, and they agree on every field.
5. The checker regenerates the phases from the spec's own rules, as a third implementation, and reconciles total + shortfall = cap exactly.
## Results
- **Mainnet as compiled (10 blocks a second)**: issuance totals 3,963,038,999.610200467816224 IGN. The shortfall under the cap is exactly 36,961,000,389,799,532,183,776,000 base units. The subsidy is zero from DAA 39,131,424,000, the start of year 125.
- **Devnet 4 profile (1 block a second)**: the same curve per second. The total differs from mainnet by 15,473,592,000 base units of per-block flooring.
- **Conflicts table**: 13 rows. Nine are resolved: C01 by D01, C12 by D01, and seven taken to the code with a text correction where one is owed. Four are BLOCKED on questions for the founder, each with a default: C04 the block rate, C08 the per-block crediting rule, C09 the mainnet genesis clock, C11 the pool's custody.
## Commands
```
python3 tools/token-value/tv-01/gen-spec.py --node-git <igneum-node clone holding both revisions> --igneum-root . \
--igneum-rev 9c4ed1f06100c27172b05373739c337a10cfd93e \
--code-dump tools/token-value/tv-01/code-dump/schedule-bf60948a.jsonl \
--out docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json \
--md docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.md
python3 tools/token-value/tv-01/check-spec.py
```

View file

@ -0,0 +1,19 @@
{
"run_id": "tv-01-20261009-01",
"manifest_sha": "6824d49cc",
"evidence_dir": "docs/plans/igneum-2.0-master/token-value/phase0/tv-01",
"cells": [
{
"cell": "token-value:tv-01",
"cases": [
"TV-01"
],
"status": "NOT RUN",
"in_progress": true,
"method": "static",
"evidence": "docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json; docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.md; tools/token-value/tv-01/check-spec.py; tools/token-value/tv-01/code-dump/schedule-bf60948a.jsonl",
"note": "Phase 0 supply spec (TV-01): the cap 4,000,000,000 IGN held as the invariant under D01 (capped issuance, subject to TV-04), the litepaper's year-five tail-vote sentence superseded; two schedule profiles generated in exact integers and equal on every field to the consensus code's own dump on bf60948a (release-2.0.2-node a284380b byte-identical for every cited file); total + shortfall = cap exactly; 13 litepaper-versus-code rows, 4 BLOCKED on the founder's questions (C04 block rate, C08 per-block crediting, C09 mainnet genesis clock, C11 pool custody). A document and its checks, not a verdict; the panel reads it.",
"claim_impact": "none: nothing activates; the litepaper sentences named in the conflicts table are the site lane's to correct"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,173 @@
# TV-01: the monetary contract as one supply spec
Igneum 2.0, Token Value Phase 0, item TV-01. 9 October 2026.
Gate TV-01: **NOT RUN**. This is the complete Phase 0 spec, landed under the coordinator's 11:30 UK clock. This is a Phase 0 document. It activates nothing and changes no node, chain or served page. The verdict stays NOT RUN until the independent panel reads it. Four rows are BLOCKED on questions for the founder (C04, C08, C09, C11), each with a default.
The machine-readable spec is `supply-spec.json` in this directory. This page says the same in words. Every figure on it comes from `tools/token-value/tv-01/gen-spec.py`, and the consensus code's own functions agree with every one (the code dump below). `tools/token-value/tv-01/check-spec.py` checks the json and sums it to the cap in exact integers.
## The D01 resolution
The founder ratified D01 on 9 October 2026, 09:2x UK: capped issuance, subject to TV-04.
The spec holds the cap as the invariant: **4,000,000,000 IGN, which is 4 x 10^27 base units**. There is no tail. There is no vote on emission, and no price or revenue trigger. There is no emergency mint.
D01 supersedes this sentence of the litepaper (site/litepaper.html line 630, igneum master 6824d49c, before the site lane's D01 edit):
> "One rule sits beside the cap. If external proving revenue is under one fifth of the block subsidy over any 90-day window after year 5, the question of a tail reward goes to the miners' signalling vote. The protocol never changes emission by itself."
The site lane has already rewritten the sentence (ad64e48fa, 09:16 UK). At igneum master 9c4ed1f0, line 630 reads: "The cap is fixed at 4 billion, with no tail emission and no vote that expands it (D01, subject to the pre-launch security-funding gate)." The year-five clause now says the answer "is the security-funding plan under D01, never a tail emission or a vote that expands the cap."
The code already matches D01:
- Mainnet carries `EmissionSchedule::CURRENT` (params.rs:1858), and that schedule's tail is `Tail::Cap` (emission.rs:109).
- The schedule is a compiled genesis parameter. The daemon refuses an override file on mainnet and the testnet (daemon.rs, `params_compiled`, network.rs:266).
- No signal path reads or writes the emission schedule.
"Subject to TV-04" means one thing. If TV-04's security-budget analysis shows capped security cannot pass, the volume's rule applies: delay the launch, or openly choose another monetary design. An undisclosed tail is never the answer. This landing edits no `site/` file.
## The contract
| Item | Value | Source (igneum-node; every cited line anchored at release-2.0.2-node a284380b, the shipped 2.0.2 commit 5d53a591, release-2.0.3-node-k6 bf60948a and release-2.0.3-node-k7 27f54124) |
|---|---|---|
| Coin | IGN | |
| Base unit | 10^-18 IGN; one consensus base unit is one EVM wei | unit.rs:38; params.rs:1816 |
| Cap | 4,000,000,000 IGN = 4,000,000,000,000,000,000,000,000,000 base units | igneum.rs:37, :154 |
| Time key | seconds = floor(DAA score / blocks per second) | emission.rs:329 to 331; coinbase.rs:265 |
| Launch rate | 31,688,087,810,000,000,000 base units a DAA second (floor(10^17 / 31,557,600) at 8 decimals, times 10^10) | emission.rs:104; params.rs:1858 |
| Launch ramp | linear from 10% at genesis to 100% at 2,592,000 DAA seconds (30 days) | emission.rs:105 to 106, :316 |
| Decay | every 63,115,200 DAA seconds (two years of 365.25 days): rate[k+1] = floor(rate[k] x 2^31 / 2^32), a halving | emission.rs:107 to 108, :225 |
| Per-block subsidy | per_block(d) = ramp(floor(rate[floor(s / 63,115,200)] / bps), s), s = floor(d / bps) | emission.rs:329 to 333 |
| Tail | cap: the curve runs to zero and stays there | emission.rs:109 |
| Block rate | mainnet as compiled: 10 blocks a second from genesis; Devnet 4: 1 (C04 BLOCKED) | params.rs:1789, :2208 |
| Genesis | DAA score 0 (C09 BLOCKED: the compiled mainnet genesis still carries Kaspa's 1,312,860) | genesis.rs:92 |
| Genesis is never rewarded | genesis never enters the DAA window and the coinbase pays only DAA-window blues; the genesis coinbase has no outputs. On a chain, block height h >= 1 carries DAA score h - 1, so DAA score 0 is the first paid block and the last paying block is at height = terminal DAA score (mainnet 39,131,424,000; Devnet 4 4,102,488,000) | window.rs:177; coinbase.rs:139; genesis.rs:26 |
### How the cap is enforced
No running counter enforces the cap. The schedule does:
- Every block's coinbase must carry exactly the scheduled subsidy (coinbase.rs:262).
- The EVM state credits by the same rule (executor.rs:181 to 211).
- The cap constant itself is only compared in tests (igneum.rs:154).
The schedule sums to the cap minus an exact shortfall, so issuance never reaches the cap.
## The schedule
`supply-spec.json` lists every phase with:
- its start and end DAA score
- its per-block subsidy as an integer
- its rule name
- its source line
There are two profiles, and their per-second curves are identical. **The mainnet profile is the monetary contract** (`contract_profile` in the json). The Devnet 4 profile is the chain TV-02 replays and the litepaper's launch rate (C04). It sums to the same 36,961,000.39 IGN under the cap, minus per-block flooring dust.
- **mainnet as compiled**: 10 blocks a second, 64 phases.
- **Devnet 4**: 1 block a second, the litepaper's launch rate, 67 phases.
The phases in order:
1. **Ramp** (DAA 0 to 25,920,000 at 10 blocks a second). The per-block subsidy is the ramp formula, and the phase sum is exact.
2. **Full rate after the ramp**, to the first halving.
3. **One constant phase per halving**, until the last one-base-unit subsidy.
4. **Terminal**: zero from that point on, for ever.
<!-- figures:begin (generated by tools/token-value/tv-01/gen-spec.py; do not edit by hand) -->
| Figure | Mainnet as compiled (10 blocks/s) | Devnet 4 (1 block/s) |
|---|---|---|
| Cap | 4,000,000,000 IGN (4000000000000000000000000000 base units) | the same |
| Total scheduled issuance | 3,963,038,999.610200467816224 IGN | 3,963,038,999.610200483289816 IGN |
| Shortfall under the cap (exact) | 36961000389799532183776000 base units (36,961,000.389799532183776 IGN) | 36961000389799516710184000 base units (36,961,000.389799516710184 IGN) |
| Ramp phase | DAA 0 to 25,920,000, 45,174,523.72229648548848 IGN | DAA 0 to 2,592,000, 45,174,523.722296485498848 IGN |
| First block | 0.3168808781 IGN | 3.168808781 IGN |
| Full-rate block (after the ramp) | 3.168808781 IGN | 31.68808781 IGN |
| Phases (ramp, constant, terminal) | 64 | 67 |
| Last non-zero subsidy | 1 base unit a block, DAA 38,500,272,000 to 39,131,424,000 | 1 base unit a block, DAA 4,039,372,800 to 4,102,488,000 |
| Terminal (zero for ever) from | DAA 39,131,424,000 (decay step 62, year 125) | DAA 4,102,488,000 (decay step 65, year 131) |
| Scheduled supply at the end of year 1 | 963,038,999.99163248548848 IGN | 963,038,999.991632485498848 IGN |
| Scheduled supply at the end of year 2 | 1,963,038,999.86448848548848 IGN | 1,963,038,999.864488485498848 IGN |
| Scheduled supply at the end of year 5 | 3,213,038,999.70555848548848 IGN | 3,213,038,999.705558485498848 IGN |
| Scheduled supply at the end of year 10 | 3,838,038,999.62609348548848 IGN | 3,838,038,999.626093485498848 IGN |
| Scheduled supply at the end of year 12 | 3,900,538,999.61814698548848 IGN | 3,900,538,999.618146985498848 IGN |
| Scheduled supply at the end of year 20 | 3,959,132,749.61069714173848 IGN | 3,959,132,749.610697141748848 IGN |
| Scheduled supply at the end of year 50 | 3,963,038,880.400910946483264 IGN | 3,963,038,880.400910949333816 IGN |
| Scheduled supply at the end of year 100 | 3,963,038,999.610196921373136 IGN | 3,963,038,999.6101969312294752 IGN |
| A 1-to-10 blocks/s step at DAA 31,557,600 under the current code | total 4,863,038,999.495770867826592 IGN, 863,038,999.495770867826592 IGN over the cap | |
| The compiled mainnet genesis (DAA 1,312,860) | first ramp block at 14.55%; 510,841.7849105123888436 IGN never scheduled | |
Code agreement: every field of 129 decay phases, both ramps and both totals equal to this generator's integers (dump sha256 `3e4b5ac3c2f43057a13ace7d781ad714e385a94105623c0e0dac4adc417d8056`).
<!-- figures:end -->
What the figures mean:
- **For a miner**: a full-rate block pays 3.168808781 IGN at 10 blocks a second, or 31.68808781 IGN at 1. The emission per second is the same either way, so a card's expected income does not depend on the block rate. In the first two years, 80% of each block goes to the block's producer and 20% to the proving escrow.
- **For a holder**: 963.04 million IGN are issued in year 1 and 1,963.04 million by the end of year 2. The total approaches 3,963,038,999.61 IGN and never reaches the cap. The 36,961,000.39 IGN shortfall is the launch ramp's withheld coins plus the floor of the per-second rate. Nobody holds them.
- **For security**: the subsidy halves every two years. It is one base unit a block by year 123 (10 blocks a second) and zero from the start of year 125. From then on, blocks are paid by fees alone. That is TV-04's question, and nothing in this spec answers it.
## The coinbase split the node enforces
| Rule | Effect | Source |
|---|---|---|
| Producer and proving pool | producer = subsidy - floor(subsidy x 20 / 100), pool = floor(subsidy x 20 / 100); fees go to the producer | igneum.rs:44, :201 to 208; coinbase.rs:106 |
| Signing bonus | a silent producer's share loses 10% (1,000 bps) to the pool; nothing destroyed; mainnet: off | igneum.rs:110; params.rs:1809 to 1810 |
| Native ledger (EVM state) | 80% to the block's `IGNA` address (or its `IGNW` split once the pool switch is on), 20% to the escrow 0x0000000000000000000000000000000000000220; blue blocks only | executor.rs:181 to 211; config.rs:59 |
| Shadow ledger (UTXO coinbase) | producer outputs plus one OP_RETURN output tagged `igneum-proving-pool-v0`; no UTXO transaction but the coinbase is valid, so these outputs are never spent | coinbase.rs:126 to 168; body_validation_in_isolation.rs:54 |
| Pool | none in consensus; the reference pool keeps `fee_percent` (default 1.0) of the 80% it receives and pays members PPLNS from its address | pool/src/payout.rs:1 to 6; pool/src/config.rs:58; pool/src/pplns.rs:76 |
| Dev fee | none in consensus; the miner software requests 1 template in 100 with the dev address (off with `--dev-fee 0`); it moves those blocks' producer share and creates nothing | litepaper line 625 |
| Treasury, foundation, team, stake | none | coinbase.rs:126 to 168; executor.rs:181 to 211 |
| Genesis | the genesis coinbase has no outputs; no premine | genesis.rs:26 |
| Burns | execution base fee and proving charge burned in full today; 20% of the tip goes to registered payees, and unregistered parts are burned. The design's route of 90% of the proving base fee to provers (docs/design/proving-payment.md; node branch proving-payment 930b6322) is not merged and has no activation | executor.rs:319 to 371; pgas.rs:290 |
## Supply accounting (VR-02, for TV-02)
- **Issued**: the EVM state's subsidy credits (producer plus escrow), for blue blocks only. The schedule's sum, at one subsidy per DAA score, is the upper bound.
- **Burned**: base fees, proving charges and unregistered developer shares. The UTXO OP_RETURN output is **not** a burn (C06).
- **Locked**: the proving escrow's balance, meaning credited but not yet paid against a valid proof. Unclaimed credit is stranded today, as the litepaper says at line 609.
- **Not supply**: the UTXO coinbase outputs, which are an unspendable shadow record, and any bridged or wrapped IGN on another chain.
- **Replay model**: one scheduled subsidy per DAA score. A block is paid at its own DAA score. Parallel blocks can share a score, and a score can have no block. So measured issuance differs from the model only at phase boundaries, and only by the blocks merged across them. TV-02 reconciles the measured ledgers to this model in exact integers.
## Conflicts between the litepaper and the code
The litepaper is site/litepaper.html at igneum master 9c4ed1f0 (sha256 `aec583e6e868eb2d3aa8474df3f8892eeebba8a98c887a019ea86bd2efc2f786`). The one exception is C01's superseded sentence, which is quoted at 6824d49c. The code is igneum-node at all four revisions. Every quote below is checked exact by the generator.
| # | The litepaper's words | The code's line and value | The spec takes, and why | Status |
|---|---|---|---|---|
| C01 | line 630 at 6824d49c, the tail-vote sentence quoted above (rewritten at ad64e48fa) | emission.rs:109 `Tail::Cap`; params.rs:1858; the override refused on mainnet; no vote path | The cap; the sentence is SUPERSEDED. D01, VR-01. | RESOLVED by D01, subject to TV-04; the text was already rewritten by the site lane |
| C02 | line 578 "emission starts at 1 billion a year"; line 602 "3,938M IGN in the first 12 years of a 4,000M cap." | emission.rs:104 to 106: year 1 issues 963,038,999.99 IGN; years 1 to 12 issue 3,900,538,999.62 IGN | The code. The chart ignores the ramp's withheld 36.96M IGN. | RESOLVED; text correction owed (site lane) |
| C03 | line 578 "halves every two years for ever" | emission.rs:225, :331: the per-block subsidy floors to 0 at DAA 39,131,424,000 (start of year 125, 10 blocks/s) or DAA 4,102,488,000 (start of year 131, 1 block/s) | The code: a finite schedule ending in a terminal zero phase | RESOLVED; text correction owed |
| C04 | line 542 "one block a second at launch with scheduled steps to four and ten"; line 668 "31.688 IGN a block, 86,400 blocks a day" | params.rs:1789: mainnet runs 10 blocks/s from genesis. coinbase.rs:31, :265: the subsidy keys time on one block rate with no history, so a 1-to-10 step one year in would issue 4,863,038,999.50 IGN, which is 863,038,999.50 over the cap (check-spec KF4) | Both profiles, identical per second. No live block-rate step is cap-safe under today's code. | **BLOCKED** |
| C05 | line 568 "the base unit is Open" | unit.rs:38; params.rs:1816: 18 decimals | 18 decimals (O-2.6, decided 6 October 2026) | RESOLVED; text correction owed |
| C06 | line 609: the 20% "goes to an unspendable script ... and is burned there. Provers are paid from a separate escrow in the execution state" | coinbase.rs:163 (OP_RETURN on the UTXO side); body_validation_in_isolation.rs:54 (UTXO never spent); executor.rs:181 to 211 (the EVM state credits 80% and 20%) | Native supply is the EVM state. The UTXO 20% is not a burn, and the escrow balance is issued and locked. | RESOLVED for the spec; TV-02 reconciles both ledgers |
| C07 | line 620 "80% the block's miner, 20% the proving pool for the provers of that block" | coinbase.rs:152 to 159 (the UTXO side pays DAA red blocks); executor.rs:181 (the EVM credits blue only) | Native issuance is the blue blocks' subsidies; the schedule sum is the upper bound | RESOLVED; TV-02 measures the red share |
| C08 | silent | params.rs:1812 and :2232: `subsidy_per_block_activation_daa` is never on mainnet and Devnet 4 (testnet-1: 0). The EVM credits a merged block the merging chain block's subsidy, while the UTXO side pays the block's own. | Each block's own DAA score (ledger N8) is normative | **BLOCKED** |
| C09 | line 775 "Genesis with no premine, 30-day ramp." | genesis.rs:92: the mainnet GENESIS carries DAA 1,312,860 and Kaspa's 2021 timestamp. The first block would pay 14.55% of full rate, not 10%. 510,841.78 IGN would never be scheduled, and the schedule would run 1.52 days early. | Genesis at DAA score 0, as Devnet 4 and testnet-1 have it | **BLOCKED** |
| C10 | line 614 "The priority fee splits two ways: 80% to the miner and provers of that block, 20% to the apps whose code ran"; line 622 "80% the block's miner and provers; 20% the apps whose code ran, per call frame" | Two separate 80/20 rules ship. The subsidy's 20% goes to the proving escrow and pays provers per accepted proof record (igneum.rs:44, :201 to 208; executor.rs:181 to 211). The tip's 80% goes to the block's miner alone, and 20% goes to developer registrations or is burned (executor.rs:321, :332 to 340, :362 to 371). No tip reaches a prover. The 90% proving-fee route is designed and unmerged, with no activation. spec 05 line 24: "No part of the tip reaches the provers: O-5.7 is closed at zero (8 October 2026 ...)" | The code, which is the design's closed decision O-5.7. The litepaper's "miner and provers" is SUPERSEDED. | RESOLVED by O-5.7; text corrections owed (litepaper lines 614 and 622; spec 05 lines 79 and 98) |
| C11 | line 647 "The reference pool never holds a member’s balance. Payouts come straight from the coinbase split" | pool/src/payout.rs:1 to 6 (the pool's address receives and pays); config.rs:58 (1% fee); params.rs:1851 (`pool_split_activation_daa` never) | The code: a custodial PPLNS pool with no supply effect | **BLOCKED** |
| C12 | line 630 "The cap is fixed at 4 billion, with no tail emission and no vote that expands it (D01, subject to the pre-launch security-funding gate)." | emission.rs:116 to 122; params.rs:2014: testnet-1's own approved genesis schedule, with a 1% a year tail and no hard cap (docs/plans/ledger-decisions.md line 110, ledger E22). It is active only when testnet-1's genesis is cut, which has not happened. Devnet 4 (params.rs:2372) and mainnet (params.rs:1858) carry `CURRENT` with `Tail::Cap`. | Testnet-only and outside the monetary contract. Binding the cap to the testnet too would change its approved genesis (E22) and move its digest: the founder's call. | RESOLVED by D01: testnet-only, named |
| C13 | silent | params.rs:1783 to 1784: Kaspa's `deflationary_phase_daa_score` and `pre_deflationary_phase_base_subsidy` sit in the mainnet object. Only the digest reads them (coinbase.rs:31). | Ignored: not part of the contract | RESOLVED; the node lane may annotate or remove them at the genesis cut |
### The four BLOCKED rows: the founder's questions
| Row | Question for the founder | Default if unanswered | Earliest clock |
|---|---|---|---|
| C04 | Does mainnet launch at the compiled 10 blocks a second, with the litepaper's 1-to-4-to-10 step plan removed? Or at 1 with steps? If steps, the node lane adds a block-rate-history subsidy rule before genesis, and TV-02 replays a step. | The spec freezes the compiled 10 blocks a second, and the step plan is treated as text to correct | the founder's answer |
| C08 | Should `subsidy_per_block_activation_daa` be 0 in the mainnet object, as testnet-1 has it, so both ledgers credit each block its own subsidy? (The node lane owns the change.) | N8 stays normative, and TV-02 reports the chain-block reading as a measured divergence | the founder's answer |
| C09 | Is the mainnet genesis cut at DAA score 0? | The spec freezes DAA score 0, and the genesis cut is checked against it | the founder's answer |
| C11 | Should the `IGNW` coinbase split be active at mainnet genesis (`pool_split_activation_daa` = 0), so the litepaper's custody sentence holds? Or should the sentence change to describe the custodial PPLNS pool and its 1% fee? | The sentence is corrected to match the code | the founder's answer |
## Reproduce
```
python3 tools/token-value/tv-01/gen-spec.py --node-git <igneum-node clone holding a284380b and bf60948a> \
--igneum-root . --igneum-rev 9c4ed1f06100c27172b05373739c337a10cfd93e \
--code-dump tools/token-value/tv-01/code-dump/schedule-bf60948a.jsonl \
--out docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json \
--md docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.md
python3 tools/token-value/tv-01/check-spec.py
```
The generator refuses to write if any cited line has lost its anchor text at any of the four node revisions, or if any litepaper quote is not exact. The code dump comes from `tools/token-value/tv-01/code-dump/tv01_schedule.rs.txt`. That program was copied into `consensus/core/examples/` of a scratch tree of bf60948a on build-9 and run there with `cargo run --release -p kaspa-consensus-core --example tv01_schedule`. It calls `Params::emission`, `EmissionTable::block_subsidy` and `Params::bps` for mainnet, Devnet 4 and testnet-1. The node fork itself is unchanged.

View file

@ -0,0 +1,193 @@
# TV-02: two independent supply replays, exact integer agreement
Token Value Phase 0, gate TV-02 ("Reproduce complete supply accounting"), 9 October 2026. Verdict: **NOT RUN**. The
independent panel reads this evidence; this lane writes no PASS on its own result.
## Result in one paragraph
Two implementations with no shared code replay the issued supply from genesis to the cap, block by block, on three
network objects of the node at bf60948a: the Igneum 2.0 devnet object (igneum-devnet-4: 1 block a second, 18
decimals), the mainnet object (10 blocks a second, 18 decimals) and the 8-decimal devnet object. With the rule set
v0.1 they agree to the integer at every one of 591 rows: every transition's block before, at and after (the ramp's
end and every halving step until the subsidy is 0), the terminal block and one full step past it. Their CSVs are
byte-identical. Both reorg fixtures agree with each other and with integers written by hand before either replay ran,
and the known-failed no-undo ledger is refused. Implementation B written from the published words alone (rule set
v0) diverges at the first row, genesis. The cause is one rule the words never state: genesis issues nothing, so the
block at height h carries DAA score h - 1. The total at the terminal is unchanged by it. No cap breach, no issuance
after the terminal block, and the node's two issuance code paths agree on all 46,010,980,803 blocks replayed.
| network object | blocks a second | decimals | terminal block (last with a subsidy) | issued supply at the terminal, base units | IGN | under the 4,000,000,000 cap by |
|---|---|---|---|---|---|---|
| igneum-devnet-4 | 1 | 18 | height 4,102,488,000 (step 65, year 130) | 3963038999610200483289816000 | 3,963,038,999.610200483289816 | 36,961,000.389799516710184 IGN |
| mainnet | 10 | 18 | height 39,131,424,000 (step 62, year 124) | 3963038999610200467816224000 | 3,963,038,999.610200467816224 | 36,961,000.389799532183776 IGN |
| devnet (8 decimals) | 1 | 8 | height 2,019,686,400 (step 32, year 64) | 396303898886765648 | 3,963,038,988.86765648 | 36,961,011.13234352 IGN |
What the gap under the cap is made of (devnet-4, evaluated with bc on build-5 from the closed forms, a third
computation beside A and B): the launch ramp withholds 36,960,999.881223514501152 IGN (2,592,000 x the full rate
minus the ramp's sum) and the floors of the launch rate and the 65 halvings withhold 0.508576002209032 IGN; the two
sum to A's and B's gap exactly.
What it means for a holder: the supply that will ever exist on the 2.0 devnet and mainnet objects is 3,963,038,999.61
IGN, reached at year 130 (devnet-4) or 124 (mainnet), with nothing minted after it and no path to the 4,000,000,000
figure; the first two years (DAA 0 to 63,115,199) issue 1,963,038,999.86 IGN (49.5 percent of the final supply). A miner sees the per-block
subsidy of the table below; a pool sees the same numbers per block found. Neither changes with this document.
## Sources, by sha
| source | identity |
|---|---|
| the node's issuance code | igneum-node bf60948a (release-2.0.3-node-k6); the six files that decide issuance are byte-identical in release-2.0.2-node a284380b: consensus/core/src/emission.rs, consensus/core/src/igneum.rs, consensus/src/processes/coinbase.rs, consensus/core/src/config/params.rs, consensus/src/processes/window.rs, consensus/src/processes/difficulty.rs |
| the volume's rules | docs/plans/igneum-2.0-master/token-value/rules-and-gates.json, sha256 10ce307329e1663d... (TV-02, VR-02, D01) |
| the published words | site/litepaper.html (sha256 35ac65e151339089..., blob db1b8bf7b1d2) and site/economics.html (sha256 449c20b85cc4eede..., blob b15926199f06), both at master 6824d49cc32d |
| the TV-01 supply spec | not landed at the time of this run (docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json, a first version due 13:00 UK). B ran on the two interim rule sets below; the re-run on the spec is the open row R1 |
| B's rules v0 (words only) | tools/token-value/tv-02/replay-spec/rules-v0-litepaper.json, sha256 67707b095584d8bb8bc99e799875b8d167512085385ea6883bc12575d98fbb33 |
| B's rules v0.1 (v0 plus the genesis rule) | tools/token-value/tv-02/replay-spec/rules-v0.1-genesis-rule.json, sha256 d72d5578dfe4e0c860b45fdb37a749dd7f1b0365175a38349dd413b1e93eb0b9; the genesis rule from the node lane's written answer (9 October 2026, 09:25 UK), cited by file and line |
## Method
**The model.** A straight chain. Genesis (height 0, DAA score 0) issues nothing: it never enters the DAA window
(window.rs 176-179 puts it in mergeset_non_daa) and the coinbase pays only blues outside that set (coinbase.rs 139).
A block's DAA score is its selected parent's plus its mergeset's DAA blocks (difficulty.rs 31-32), so block 1 has DAA
score 0 and block h >= 1 has DAA score h - 1. Each block issues the schedule's subsidy at its DAA score: the
per-second rate of its step (launch rate 31,688,087,810,000,000,000 base units at 18 decimals, halved and floored
every 63,115,200 DAA seconds), divided by the blocks a second (floor), then the 30-day ramp from 10 percent (floor).
"cumulative" at height h is the issued supply of heights 0..h, exact, in base units.
**Implementation A** (tools/token-value/tv-02/replay-node, Rust, on build-5). Calls the node's own code through
kaspa-consensus-core by path (vendor/igneum-node at bf60948a, read-only, no node file changed): the network objects
(devnet4_params(), MAINNET_PARAMS, DEVNET_PARAMS) and `EmissionTable::block_subsidy`, the value
`CoinbaseManager::calc_block_subsidy` returns and body validation compares every coinbase against. Every block from
DAA 0 to one step past the terminal is computed one by one (46,010,980,803 DAA scores over the three objects, 32 threads,
38 s wall in all) and checked against the node's second issuance path `igneum::block_subsidy_units` (0 mismatches)
and against the shape rule (non-decreasing through the ramp, non-increasing after it; 0 violations). Crate tests
(cargo test --release on build-5, 3 green): the no-undo ledger double-counts the orphan (the known-failed case first),
the hand-written boundary subsidies, the two node paths at every row.
**Implementation B** (tools/token-value/tv-02/replay-spec/replay_spec.py, Python 3, on build-6). Written from a rules
file in words, shares no code with A and uses another method: inside the ramp it sums DAA seconds and multiplies by
the blocks in each second; after the ramp it multiplies each step's subsidy by the step's block count, counting only
a partial second or step block by block. Its self-test (the known-failed grouping that drops a partial second is
caught; the grouped sum equals a block-by-block sum on three small networks) ran before any replay.
**The row set** (written into both independently): genesis; DAA 0, 1, 2; at 10 blocks a second the last block of
second 0 and the first two of second 1; the first block of every ramp day 1 to 29 and the ramp's midpoint; for every
transition (the ramp's end, then step k = 1, 2, ... up to the first step whose blocks pay 0) the blocks at DAA x - 1,
x and x + 1; the terminal block; one full step past the terminal. 232 rows on devnet-4, 226 on mainnet, 133 on devnet.
**The reorg fixtures** (tools/token-value/tv-02/fixtures/, committed in 4e206eaf8 before either replay ran). A common
ancestor, an orphan branch of k blocks, rolled back, replaced by k + 1, at a transition boundary on devnet-4. Every
expected integer was written by hand from the closed forms (the ramp's sum has an exact closed form at 18 decimals:
schedule(d) = F/10 + d q + floor(d/9) with q = 11,002,808,267,361) and its digits evaluated with bc on build-5. The
known-failed ledger (a rollback that forgets to subtract) is part of every fixture and must be refused.
| fixture | ancestor | orphan (k) | replacement (k+1) | final, hand-written | known-failed no-undo, hand-written |
|---|---|---|---|---|---|
| reorg-ramp-end-k1 | height 2,592,000 (the last ramp block), 45174523722296485498848000 | 1 block | 2 blocks | 45174587098472105498848000 (= the straight chain at height 2,592,002) | 45174618786559915498848000 (one full block too many) |
| reorg-first-step-k2 | height 63,115,199, 1963038968176400675498848000 | 2 blocks (full, then the first halved) | 3 blocks | 1963039031552576295498848000 (= the straight chain at height 63,115,202) | 1963039079084708010498848000 |
**The comparison** (tools/token-value/tv-02/compare.py). Reads A's and B's CSVs (height, daa_score, subsidy,
cumulative) and reports EQUAL or the first differing row with both values, the columns that differ, the heights only
one side has, and the cumulative offsets on shared heights; for a reorg it checks A against B row by row, both
against the fixture's hand-written integers, the final against the straight chain at the same height, and that the
no-undo ledger is refused. Its self-test runs first in every comparison (a corrupted cumulative, a dropped row and a
no-undo ledger are each reported; equal inputs read EQUAL).
## Results
Run 9 October 2026, 08:22 to 08:35 UTC. A on build-5, B on build-6, the comparison on build-5 over B's outputs
copied from build-6. A rebuilt from this landing's source re-ran byte-identical (the second run under rerun-final/).
| comparison | rules (B) | result | first difference |
|---|---|---|---|
| straight, devnet-4, 232 rows | v0.1 | EQUAL, byte-identical CSVs (sha256 6ede4f4a2fe3c551...) | none, through the ramp's end and steps 1 to 65, the terminal and one step past |
| straight, mainnet, 226 rows | v0.1 | EQUAL, byte-identical (sha256 3cf7f426d062722d...) | none, through steps 1 to 62 |
| straight, devnet, 133 rows | v0.1 | EQUAL, byte-identical (sha256 5679a4353380491f...) | none, through steps 1 to 32 |
| reorg-ramp-end-k1 | v0.1 | EQUAL, every row, and both equal the hand-written integers; no-undo refused | none |
| reorg-first-step-k2 | v0.1 | EQUAL, every row, and both equal the hand-written integers; no-undo refused | none |
| straight, all three objects | v0 (words only) | DIFFER | row 1, genesis: A (0, 0, subsidy 0, cumulative 0), B (0, 0, 3168808781000000000, 3168808781000000000) on devnet-4; 316880878100000000 on mainnet; 316880878 on devnet. Every later row is one block apart in height; the terminal total is identical |
| both reorg fixtures | v0 (words only) | DIFFER from A and from the fixture, by the same one-block shift | row 1, the ancestor: A DAA 2,591,999, cumulative 45174523722296485498848000; B DAA 2,592,000, cumulative 45174555410384295498848000 |
The rule that explains the v0 divergence: the published words (litepaper, economics page) never say what genesis
issues or which DAA score block h carries. The node's rule is that genesis never enters the DAA window
(consensus/src/processes/window.rs 176-179), is skipped by the coinbase (coinbase.rs 139) and is never rewarded, so
block 1 carries DAA score 0 (difficulty.rs 31-32). Written in as v0.1, A and B agree everywhere. The words need the
rule (row R2).
Devnet-4, selected rows (the full tables: build-5/table-devnet4.md, table-mainnet.md, table-devnet.md, generated by
tools/token-value/tv-02/table.py from A's CSV, which is byte-identical to B's):
| height | DAA score | row | subsidy (base units) | issued supply (base units) | issued supply (IGN) |
|---|---|---|---|---|---|
| 0 | 0 | genesis (issues nothing) | 0 | 0 | 0 |
| 1 | 0 | DAA 0, the ramp at 10 percent | 3168808781000000000 | 3168808781000000000 | 3.168808781 |
| 2,592,001 | 2,592,000 | at ramp end | 31688087810000000000 | 45174555410384295498848000 | 45,174,555.410384295498848 |
| 63,115,201 | 63,115,200 | at step 1 | 15844043905000000000 | 1963039015708532390498848000 | 1,963,039,015.708532390498848 |
| 126,230,401 | 126,230,400 | at step 2 | 7922021952500000000 | 2963039007659366437998848000 | 2,963,039,007.659366437998848 |
| 189,345,601 | 189,345,600 | at step 3 | 3961010976250000000 | 3463039003634783461748848000 | 3,463,039,003.634783461748848 |
| 252,460,801 | 252,460,800 | at step 4 | 1980505488125000000 | 3713039001622491973623848000 | 3,713,039,001.622491973623848 |
| 504,921,601 | 504,921,600 | at step 8 | 123781593007812500 | 3947413999735968703506660500 | 3,947,413,999.7359687035066605 |
| 1,009,843,201 | 1,009,843,200 | at step 16 | 483521847686767 | 3962977964454441767484072367 | 3,962,977,964.454441767484072367 |
| 2,019,686,401 | 2,019,686,400 | at step 32 | 7377957880 | 3963038998678877917554525880 | 3,963,038,998.67887791755452588 |
| 3,029,529,601 | 3,029,529,600 | at step 48 | 112578 | 3963038999610186273155109378 | 3,963,038,999.610186273155109378 |
| 4,039,372,801 | 4,039,372,800 | at step 64 | 1 | 3963038999610200483226700801 | 3,963,038,999.610200483226700801 |
| 4,102,488,000 | 4,102,487,999 | terminal (last block with a subsidy) | 1 | 3963038999610200483289816000 | 3,963,038,999.610200483289816 |
| 4,102,488,001 | 4,102,488,000 | at step 65 | 0 | 3963038999610200483289816000 | 3,963,038,999.610200483289816 |
| 4,165,603,201 | 4,165,603,200 | one step past the terminal | 0 | 3963038999610200483289816000 | 3,963,038,999.610200483289816 |
The height-1 row matches the team-reported first-block credit on the economics page (3.168808781 IGN at 18 decimals,
read back over the EVM RPC on 8 October 2026); this replay did not read the chain.
## Open rows
| id | row | state | waits on, earliest clock |
|---|---|---|---|
| R1 | Re-run B on the TV-01 supply spec and record its sha here | BLOCKED | the spec's first version, due 13:00 UK today; the re-run takes under a minute on build-6 |
| R2 | The published words (and the TV-01 spec) must state the genesis rule: genesis issues nothing, block h carries DAA score h - 1. Without it a reader reproduces the supply one block early (the v0 divergence above) | BLOCKED | TV-01's spec text |
| R3 | The words must state the per-block order at more than one block a second: the per-second rate divided by the blocks a second and floored, then the ramp (B assumed it; it matches the node). The other order changes the mainnet ramp's sum | BLOCKED | TV-01's spec text |
| R4 | The mainnet object's 10 blocks a second against the litepaper's "1 / s blocks, rising to 10 (designed)": the supply at 10 differs from 1 by 15,473,592,000 base units (0.000000015 IGN) and ends six years earlier (year 124, not 130); which rate mainnet launches at is a spec question | BLOCKED | TV-01, D01 |
| R5 | The 8-decimal and 18-decimal schedules are not the same coin amounts after the first halving (3,168,808,781 is odd, so the 8-decimal halving floors and the 18-decimal one does not): the 18-decimal supply is 10.74 IGN larger. igneum.rs's comment that the 18-decimal numbers are "the 8-decimal ones times 10^10" holds for the launch rate only. Informational; no network mixes the two | NOT RUN | none |
| R6 | The testnet-1 object carries a percent tail (`EmissionSchedule::TESTNET_1`, no cap). D01 ratified capped issuance; that object is outside it and was not replayed | BLOCKED | TV-01, D01 |
| R7 | DEVNET4_GENESIS carries the 8-byte subsidy layout on an 18-decimal network: read at 16 bytes its subsidy field is 134825277635862783248051596202800963840 base units (A's genesis-payload output). The window rule keeps it from ever being paid (the node lane, 09:25 UK); any other reader of that field (an explorer, an executor path) must apply the same rule. The node lane takes the B10 test case for devnet-3 and devnet-4 as a 2.0.3 test item | NOT RUN | the node lane's 2.0.3 test |
| R8 | The rest of the gate's procedure: paid (the 80/20 split is by subtraction, so producer plus pool equals the subsidy per block by construction), burned (the pool's OP_RETURN script on devnet v0), locked, outstanding, red and non-DAA blocks in a DAG, the execution layer's ledger, duplicate records, wrappers. This replay covers issued supply on a straight chain and its reorg only | NOT RUN | a later TV-02 item |
| R9 | Independence: A and B share no code, language or method, but one lane wrote both and B's rules were written after reading the node; the panel should weigh that | NOT RUN | the independent panel |
## Run commands
On build-5 (A), with the igneum repo at this landing and the node fork checked out at bf60948a as vendor/igneum-node:
```
tools/token-value/tv-02/run-a.sh <repo> /srv/artefacts/token-value/tv-02 build
tools/token-value/tv-02/run-a.sh <repo> /srv/artefacts/token-value/tv-02 genesis
tools/token-value/tv-02/run-a.sh <repo> /srv/artefacts/token-value/tv-02 straight devnet4 # and mainnet, devnet
tools/token-value/tv-02/run-a.sh <repo> /srv/artefacts/token-value/tv-02 reorg tools/token-value/tv-02/fixtures/reorg-first-step-k2.json
(cd tools/token-value/tv-02/replay-node && cargo test --release)
```
On build-6 (B), from tools/token-value/tv-02 alone (no node code on the box):
```
./run-b.sh <tv-02 dir> /srv/artefacts/token-value/tv-02 replay-spec/rules-v0.1-genesis-rule.json selftest
./run-b.sh <tv-02 dir> /srv/artefacts/token-value/tv-02 replay-spec/rules-v0.1-genesis-rule.json straight devnet4
./run-b.sh <tv-02 dir> /srv/artefacts/token-value/tv-02 replay-spec/rules-v0.1-genesis-rule.json reorg fixtures/reorg-ramp-end-k1.json
```
The comparison, on build-5 with B's outputs copied to <out>/from-build-6:
```
tools/token-value/tv-02/compare-all.sh <tv-02 dir> /srv/artefacts/token-value/tv-02 rules-v0.1-genesis-rule
python3 tools/token-value/tv-02/table.py a-devnet4.csv a-devnet4-labels.csv 18
```
Each run writes its pid file first (run-a.pid, run-b.pid, compare.pid in the out dir) and removes it on exit.
## Evidence
| what | where |
|---|---|
| A's outputs, summaries, genesis-payload read, crate test log, build log | build-5:/srv/artefacts/token-value/tv-02/ (a-*.csv, a-*-summary.json, a-genesis-payload.json, a-cargo-test.log, build-a.log, a-run.log); the rebuild from this landing under rerun-final/ |
| B's outputs, both rule sets, self-test log | build-6:/srv/artefacts/token-value/tv-02/ (b-rules-v0.1-genesis-rule-*, b-rules-v0-litepaper-*, b-selftest.log, b-run.log) |
| the comparison logs and tables | build-5:/srv/artefacts/token-value/tv-02/compare-rules-v0.1-genesis-rule.log (sha256 81545cc811ff208b...), compare-rules-v0-litepaper.log (sha256 f743ecf3a323afba...), table-*.md |
| all of the above, copied | build-1:/srv/artefacts/token-value/tv-02/build-5/ and build-1:/srv/artefacts/token-value/tv-02/build-6/ |
| the binary that ran | tv02-replay-node sha256 ef3af79157947584... (source at 4e206eaf8); rebuilt from this landing 25ff78695bd9d972..., outputs byte-identical |
| B's code that ran | replay_spec.py sha256 acd68e1b8f9f4d1a591791caa408ba539680125db1af763fe0a7fb7e1e25eaf4 (this landing's file) |
| the registry batch | docs/plans/igneum-2.0-master/token-value/phase0/tv-02/registry-batch-tv-02.json (TV-02 NOT RUN, the proposed cell native:tv-02; the CI steward records it and names the cell on the map) |

View file

@ -0,0 +1,30 @@
{
"run_id": "tv-02-20261009-01",
"manifest_sha": "af2c23bc",
"method": "native",
"evidence_dir": "build-1:/srv/artefacts/token-value/tv-02",
"release_identity": {
"commit": "af2c23bc (branch tv-02-supply-replay: the two implementations, the fixtures from 4e206eaf8, the compare harness)",
"lockfile": "tools/token-value/tv-02/replay-node/Cargo.lock",
"binary": "tv02-replay-node sha256 ef3af79157947584... (the run), 25ff78695bd9d972... (rebuilt from af2c23bc, outputs byte-identical)",
"network_object": "the compiled objects of igneum-node bf60948a read by A (devnet4_params, MAINNET_PARAMS, DEVNET_PARAMS); no chain state read or written",
"activation": "none (Phase 0 lands documents and tests only)",
"profile_hashes": "igneum-node bf60948a (emission.rs, igneum.rs, coinbase.rs, params.rs, window.rs, difficulty.rs byte-identical in release-2.0.2-node a284380b); rules-and-gates.json 10ce3073...; B rules v0 67707b09..., v0.1 d72d5578...; replay_spec.py acd68e1b..."
},
"claim_impact": "none: no served figure moves; the supply ever issued on the 2.0 objects is 3,963,038,999.61 IGN under the 4,000,000,000 cap, with nothing after the terminal block",
"reviewer": "",
"note": "for the CI steward: the proposed cell native:tv-02 (cases TV-02, coverage partial: issued supply on a straight chain and its reorg; paid, burned, locked, outstanding, DAG reds and duplicate records open) is not yet on the map; this file is the batch to record.",
"cells": [
{
"cell": "native:tv-02",
"cases": [
"TV-02"
],
"status": "NOT RUN",
"in_progress": true,
"method": "native",
"evidence": "docs/plans/igneum-2.0-master/token-value/phase0/tv-02/README.md; build-1:/srv/artefacts/token-value/tv-02/build-5/compare-rules-v0.1-genesis-rule.log; build-1:/srv/artefacts/token-value/tv-02/build-5/compare-rules-v0-litepaper.log; build-1:/srv/artefacts/token-value/tv-02/build-5/a-*.csv; build-1:/srv/artefacts/token-value/tv-02/build-6/b-*.csv",
"note": "two supply replays, A (Rust on the node's issuance code at bf60948a, build-5) and B (Python from the rules in words, build-6), no shared code: with rule set v0.1 (the words plus the genesis rule, window.rs 176-179) EQUAL to the integer, byte-identical CSVs, at every transition's before/at/after, the terminal and one step past, on the devnet-4 (1 bps, 18 decimals), mainnet (10 bps, 18) and devnet (8) objects, and on two hand-written reorg fixtures with the no-undo ledger refused; with the words alone (v0) DIFFER at genesis (A 0, B 3168808781000000000 on devnet-4), a one-block shift with the terminal total unchanged (3963038999610200483289816000 base units on devnet-4); the TV-01 spec re-run, paid/burned/locked accounting, DAG reds and independent signoff are open rows; NOT RUN until the independent panel reads it"
}
]
}

View file

@ -649,22 +649,22 @@
"owner": "GPU lead + three independent operators",
"manual_page": 21,
"owner_lane": "research lane (ad6a2bd47d4a46105)",
"run_status": "NOT RUN",
"evidence_path": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"run_id": "p02-fleet-pods-20261008-01",
"updated": "2026-10-08T21:49:19.980Z",
"run_status": "PASS",
"evidence_path": "docs/analysis/class-v6/rows/x0-5090-20261009.md; build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"run_id": "hash-lane-20261009-batch5",
"updated": "2026-10-09T09:08:49.974Z",
"evidence_record": {
"requirement_id": "GPU-01",
"decision": "NOT RUN",
"decision": "PASS",
"method": "GPU",
"cell": "bench:fleet-pods",
"manifest_sha": "417c4a57",
"run_id": "p02-fleet-pods-20261008-01",
"evidence": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"manifest_sha": "1a938abe4",
"run_id": "hash-lane-20261009-batch5",
"evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md",
"in_progress": false,
"coverage": "partial: the NVIDIA cells of P02 that RunPod carries",
"release_identity": {
"commit": "417c4a57",
"commit": "1a938abe4",
"lockfile": "",
"binary": "",
"network_object": "",
@ -673,9 +673,8 @@
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-08T21:49:19.980Z"
"at": "2026-10-09T09:08:49.974Z"
},
"in_progress_since": "2026-10-08T19:59:34.316Z",
"approvals": {
"scope_approved": null,
"implementation_complete": null,
@ -684,6 +683,28 @@
},
"evidence_records": {
"bench:fleet-pods": {
"requirement_id": "GPU-01",
"decision": "PASS",
"method": "GPU",
"cell": "bench:fleet-pods",
"manifest_sha": "1a938abe4",
"run_id": "hash-lane-20261009-batch5",
"evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md",
"in_progress": false,
"coverage": "partial: the NVIDIA cells of P02 that RunPod carries",
"release_identity": {
"commit": "1a938abe4",
"lockfile": "",
"binary": "",
"network_object": "",
"activation": "",
"profile_hashes": ""
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-09T09:08:49.974Z"
},
"bench:fleet-pods@p02-fleet-pods-20261008-01": {
"requirement_id": "GPU-01",
"decision": "NOT RUN",
"method": "GPU",
@ -821,19 +842,19 @@
"manual_page": 21,
"owner_lane": "hash lane (a690540514aa453d7)",
"run_status": "NOT RUN",
"evidence_path": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt; docs/analysis/class-v6/amd-intel-energy.md; docs/analysis/class-v6/rows/pc1-l8off-ds2g-20261009.md",
"run_id": "hash-lane-20261009-batch4",
"updated": "2026-10-09T03:58:33.097Z",
"evidence_path": "docs/analysis/class-v6/rows/x0-5090-20261009.md; docs/analysis/class-v6/amd-intel-energy.md; docs/analysis/class-v6/rows/pc1-l8off-ds2g-20261009.md; build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"run_id": "hash-lane-20261009-batch5",
"updated": "2026-10-09T09:08:49.974Z",
"evidence_record": {
"requirement_id": "GPU-03",
"decision": "PASS",
"method": "GPU",
"cell": "bench:pc1-packs",
"cell": "bench:fleet-pods",
"manifest_sha": "1a938abe4",
"run_id": "hash-lane-20261009-batch4",
"evidence": "docs/analysis/class-v6/rows/pc1-l8off-ds2g-20261009.md",
"run_id": "hash-lane-20261009-batch5",
"evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md",
"in_progress": false,
"coverage": "partial: hl-v6-all against hl-v6-foldrw on the 5090, the window's cost per unit of work, registers and blocks per SM, no spill; the mandatory-SKU set is owed",
"coverage": "partial: the 4090 cell",
"release_identity": {
"commit": "1a938abe4",
"lockfile": "",
@ -844,7 +865,7 @@
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-09T03:58:33.097Z"
"at": "2026-10-09T09:08:49.974Z"
},
"in_progress_since": "2026-10-08T19:59:34.316Z",
"approvals": {
@ -856,16 +877,16 @@
"evidence_records": {
"bench:fleet-pods": {
"requirement_id": "GPU-03",
"decision": "NOT RUN",
"decision": "PASS",
"method": "GPU",
"cell": "bench:fleet-pods",
"manifest_sha": "417c4a57",
"run_id": "p02-fleet-pods-20261008-01",
"evidence": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"manifest_sha": "1a938abe4",
"run_id": "hash-lane-20261009-batch5",
"evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md",
"in_progress": false,
"coverage": "partial: the 4090 cell",
"release_identity": {
"commit": "417c4a57",
"commit": "1a938abe4",
"lockfile": "",
"binary": "",
"network_object": "",
@ -874,7 +895,7 @@
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-08T21:49:19.980Z"
"at": "2026-10-09T09:08:49.974Z"
},
"bench:amd-intel-energy": {
"requirement_id": "GPU-03",
@ -919,6 +940,28 @@
"claim_impact": "",
"reviewer": "",
"at": "2026-10-09T03:58:33.097Z"
},
"bench:fleet-pods@p02-fleet-pods-20261008-01": {
"requirement_id": "GPU-03",
"decision": "NOT RUN",
"method": "GPU",
"cell": "bench:fleet-pods",
"manifest_sha": "417c4a57",
"run_id": "p02-fleet-pods-20261008-01",
"evidence": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"in_progress": false,
"coverage": "partial: the 4090 cell",
"release_identity": {
"commit": "417c4a57",
"lockfile": "",
"binary": "",
"network_object": "",
"activation": "",
"profile_hashes": ""
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-08T21:49:19.980Z"
}
}
},
@ -2200,19 +2243,19 @@
"manual_page": 26,
"owner_lane": "hash lane (a690540514aa453d7)",
"run_status": "NOT RUN",
"evidence_path": "build-1:/srv/artefacts/tas/202-5d53a591-1176efb9/miner-77b5acc5/box4-pow.log; build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt; docs/analysis/class-v6/rows/pc1-l8off-ds2g-20261009.md",
"run_id": "hash-lane-20261009-batch4",
"updated": "2026-10-09T03:58:33.097Z",
"evidence_path": "build-1:/srv/artefacts/tas/202-5d53a591-1176efb9/miner-77b5acc5/box4-pow.log; docs/analysis/class-v6/rows/x0-5090-20261009.md; docs/analysis/class-v6/rows/pc1-l8off-ds2g-20261009.md; build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"run_id": "hash-lane-20261009-batch5",
"updated": "2026-10-09T09:08:49.974Z",
"evidence_record": {
"requirement_id": "POW-08",
"decision": "PASS",
"method": "GPU",
"cell": "bench:pc1-packs",
"cell": "bench:fleet-pods",
"manifest_sha": "1a938abe4",
"run_id": "hash-lane-20261009-batch4",
"evidence": "docs/analysis/class-v6/rows/pc1-l8off-ds2g-20261009.md",
"run_id": "hash-lane-20261009-batch5",
"evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md",
"in_progress": false,
"coverage": "partial: rw-w4, rw-w32, rw-w64 and the 1p5x long-program packs as regression controls for the excluded levers",
"coverage": "partial: W = 16 on four cards",
"release_identity": {
"commit": "1a938abe4",
"lockfile": "",
@ -2223,7 +2266,7 @@
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-09T03:58:33.097Z"
"at": "2026-10-09T09:08:49.974Z"
},
"approvals": {
"scope_approved": null,
@ -2257,16 +2300,16 @@
},
"bench:fleet-pods": {
"requirement_id": "POW-08",
"decision": "NOT RUN",
"decision": "PASS",
"method": "GPU",
"cell": "bench:fleet-pods",
"manifest_sha": "417c4a57",
"run_id": "p02-fleet-pods-20261008-01",
"evidence": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"manifest_sha": "1a938abe4",
"run_id": "hash-lane-20261009-batch5",
"evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md",
"in_progress": false,
"coverage": "partial: W = 16 on four cards",
"release_identity": {
"commit": "417c4a57",
"commit": "1a938abe4",
"lockfile": "",
"binary": "",
"network_object": "",
@ -2275,7 +2318,7 @@
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-08T21:49:19.980Z"
"at": "2026-10-09T09:08:49.974Z"
},
"bench:pc1-packs": {
"requirement_id": "POW-08",
@ -2298,6 +2341,28 @@
"claim_impact": "",
"reviewer": "",
"at": "2026-10-09T03:58:33.097Z"
},
"bench:fleet-pods@p02-fleet-pods-20261008-01": {
"requirement_id": "POW-08",
"decision": "NOT RUN",
"method": "GPU",
"cell": "bench:fleet-pods",
"manifest_sha": "417c4a57",
"run_id": "p02-fleet-pods-20261008-01",
"evidence": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
"in_progress": false,
"coverage": "partial: W = 16 on four cards",
"release_identity": {
"commit": "417c4a57",
"lockfile": "",
"binary": "",
"network_object": "",
"activation": "",
"profile_hashes": ""
},
"claim_impact": "",
"reviewer": "",
"at": "2026-10-08T21:49:19.980Z"
}
}
}
@ -17506,6 +17571,8 @@
"threshold_status": "PROPOSED - FREEZE BEFORE TEST",
"run_status": "NOT RUN",
"master_status": "NOT RUN",
"run_id": "tv-d02-20261009-01",
"evidence_path": "docs/plans/igneum-2.0-master/token-value/phase0/d02/README.md",
"updated": "2026-10-09T09:00:20.590Z",
"evidence_record": {
"requirement_id": "TV-01",
@ -17555,15 +17622,13 @@
"note": "TV-01: the D02 comparison the decision register asks for (BLOCKED rows B0 to B4 in the README: the founder's pick, TV-01's supply spec, TV-04's budget base, TV-03's cohort inputs, D01's tail wording). TV-03: issuance timing under alternative same-cap schedules and the subsidy a delayed entrant sees (tables 1, 4, 5); arrival times as people wait on TV-03's inputs. TV-04: falling issuance at horizons 1, 5, 10 and 20 and the fee-dependence year at the .25x/1x/4x/10x multiples against a stated 1 percent yardstick, the fee series a parameter (table 6); TV-04's role budget replaces the yardstick. | D02, the same-cap emission comparison (Token Value Phase 0, the D02 lane, 9 October 2026): the node fork's emission rule at release-2.0.2-node a284380b replayed by tools/token-value/d02/d02_emission.py for the current pacing (A), the volume's longer same-cap distribution (B, time axis x2, with x3 and x4 swept) and the lane's smooth-glide alternative (C); years 1, 2, 5, 10 and 20; the node's own EmissionTable prints byte-identical rows (evidence/node-crosscheck.log); the outputs regenerate byte-identically (d02-manifest.sha256). No schedule is picked: the founder's word reads 'the schedule pending evidence'. NOT RUN until the independent panel reads it. Recorded by the CI steward at 09:5x UK from docs/plans/igneum-2.0-master/token-value/phase0/d02/registry-batch-tv-d02-20261009-01.json (master 0673ecf8, sha256 f21ffda7...), the requested map cell added; in progress until the decision's evidence is complete."
}
},
"in_progress_since": "2026-10-09T09:00:20.590Z",
"approvals": {
"scope_approved": null,
"implementation_complete": null,
"evidence_reproduced": null,
"claim_authorised": null
},
"run_id": "tv-d02-20261009-01",
"evidence_path": "docs/plans/igneum-2.0-master/token-value/phase0/d02/README.md",
"in_progress_since": "2026-10-09T09:00:20.590Z"
}
},
{
"id": "TV-02",
@ -17666,6 +17731,8 @@
"threshold_status": "PROPOSED - FREEZE BEFORE TEST",
"run_status": "NOT RUN",
"master_status": "NOT RUN",
"run_id": "tv-d02-20261009-01",
"evidence_path": "docs/plans/igneum-2.0-master/token-value/phase0/d02/README.md",
"updated": "2026-10-09T09:00:20.590Z",
"evidence_record": {
"requirement_id": "TV-03",
@ -17715,15 +17782,13 @@
"note": "TV-01: the D02 comparison the decision register asks for (BLOCKED rows B0 to B4 in the README: the founder's pick, TV-01's supply spec, TV-04's budget base, TV-03's cohort inputs, D01's tail wording). TV-03: issuance timing under alternative same-cap schedules and the subsidy a delayed entrant sees (tables 1, 4, 5); arrival times as people wait on TV-03's inputs. TV-04: falling issuance at horizons 1, 5, 10 and 20 and the fee-dependence year at the .25x/1x/4x/10x multiples against a stated 1 percent yardstick, the fee series a parameter (table 6); TV-04's role budget replaces the yardstick. | D02, the same-cap emission comparison (Token Value Phase 0, the D02 lane, 9 October 2026): the node fork's emission rule at release-2.0.2-node a284380b replayed by tools/token-value/d02/d02_emission.py for the current pacing (A), the volume's longer same-cap distribution (B, time axis x2, with x3 and x4 swept) and the lane's smooth-glide alternative (C); years 1, 2, 5, 10 and 20; the node's own EmissionTable prints byte-identical rows (evidence/node-crosscheck.log); the outputs regenerate byte-identically (d02-manifest.sha256). No schedule is picked: the founder's word reads 'the schedule pending evidence'. NOT RUN until the independent panel reads it. Recorded by the CI steward at 09:5x UK from docs/plans/igneum-2.0-master/token-value/phase0/d02/registry-batch-tv-d02-20261009-01.json (master 0673ecf8, sha256 f21ffda7...), the requested map cell added; in progress until the decision's evidence is complete."
}
},
"in_progress_since": "2026-10-09T09:00:20.590Z",
"approvals": {
"scope_approved": null,
"implementation_complete": null,
"evidence_reproduced": null,
"claim_authorised": null
},
"run_id": "tv-d02-20261009-01",
"evidence_path": "docs/plans/igneum-2.0-master/token-value/phase0/d02/README.md",
"in_progress_since": "2026-10-09T09:00:20.590Z"
}
},
{
"id": "TV-04",

View file

@ -0,0 +1,8 @@
{
"run_id": "hash-lane-20261009-batch5",
"manifest_sha": "1a938abe4",
"evidence_dir": "docs/analysis/class-v6/rows",
"cells": [
{"cell": "bench:fleet-pods", "status": "PASS", "method": "GPU", "evidence": "docs/analysis/class-v6/rows/x0-5090-20261009.md"}
]
}

View file

@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""TV-01 checker: reads supply-spec.json, checks it is well-formed, and sums every schedule to the cap in exact integers.
python3 tools/token-value/tv-01/check-spec.py [docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json]
Order of work (the known-failed cases first, the standing rule that a check is trusted only after it has fired):
1. four known-failed specs, built in memory from the real one, must each be REFUSED for the stated reason:
KF1 a tail (the terminal phase pays 1 base unit a block for ever): unbounded, the overshoot is infinite;
KF2 an emergency mint (the first full-rate phase doubled, every sum made self-consistent): overshoot over the cap;
KF3 the declared shortfall one base unit off: the exact reconciliation fails;
KF4 a block-rate step from 1 to 10 blocks a second one year in, keyed as the code keys time today: overshoot;
2. the real spec must be well-formed and reconcile exactly: total + shortfall = cap, total <= cap, the phases
regenerated from the spec's own rules equal the listed phases, tail = cap, the tail-vote text SUPERSEDED.
Exit 0 only when every known-failed case was refused and the real spec checked clean. The output never says PASS: the
gate TV-01 reads NOT RUN until the independent panel reads the evidence. Standalone Python 3, deterministic.
"""
import copy
import json
import sys
DEFAULT = "docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json"
TOP = ["schema", "gate", "verdict", "decision", "invariant", "units", "cap", "rules", "activation_clocks", "schedules",
"coinbase_split", "fees", "supply_accounting", "superseded", "conflicts", "sources", "generator"]
CONFLICT_KEYS = ["id", "topic", "litepaper", "code", "spec_takes", "why", "status"]
class Refused(Exception):
pass
def need(cond, why):
if not cond:
raise Refused(why)
def amount(x, where):
need(isinstance(x, str) and x.isdigit() and (x == "0" or not x.startswith("0")), f"{where}: amount {x!r} is not a decimal string of base units")
return int(x)
# ---- the schedule regenerated from the spec's own rules (a third implementation beside the code and the generator) --
def rules_of(spec):
r = spec["rules"]
return {"L": amount(r["launch_rate_per_second"], "rules.launch_rate_per_second"), "R": r["ramp_seconds"],
"P": r["ramp_start_percent"], "S": r["step_seconds"], "Q": r["step_decay_q32"]}
def rate(rr, k, cache={}):
key = (rr["L"], rr["Q"], k)
if key not in cache:
x = rr["L"]
for _ in range(k):
x = (x * rr["Q"]) >> 32
cache[key] = x
return cache[key]
def per_block(rr, daa, bps):
s = daa // bps
full = rate(rr, s // rr["S"]) // bps
if s >= rr["R"]:
return full
return full * (rr["P"] * rr["R"] + (100 - rr["P"]) * s) // (100 * rr["R"])
def ramp_sum(rr, bps):
full = rr["L"] // bps
R, P = rr["R"], rr["P"]
return sum(full * (P * R + (100 - P) * s) // (100 * R) for s in range(R)) * bps
def supply_to(rr, bps, daa):
"""Scheduled issuance over DAA scores [0, daa) at one subsidy per DAA score."""
total, R = 0, rr["R"] * bps
full = rr["L"] // bps
secs = min(daa, R) // bps
total += sum(full * (rr["P"] * rr["R"] + (100 - rr["P"]) * s) // (100 * rr["R"]) for s in range(secs)) * bps
total += (min(daa, R) - secs * bps) * per_block(rr, secs * bps, bps)
d = R
while d < daa:
k = (d // bps) // rr["S"]
end = min((k + 1) * rr["S"] * bps, daa)
pb = per_block(rr, d, bps)
if pb == 0:
break
total += pb * (end - d)
d = end
return total
# ---- the checks ----------------------------------------------------------------------------------------------
def check(spec, regen=True):
"""Returns a list of report lines; raises Refused on the first fault."""
out = []
for k in TOP:
need(k in spec, f"top-level field {k!r} missing")
need(spec["gate"] == "TV-01", "gate is not TV-01")
need(spec["verdict"] in ("NOT RUN", "BLOCKED", "FAIL"), f"verdict {spec['verdict']!r}: a lane never writes PASS on its own result")
need(spec["decision"].get("id") == "D01", "decision is not D01")
need(spec["rules"].get("genesis_rule", {}).get("rule") == "genesis-unrewarded", "the genesis rule (genesis never rewarded) is missing")
need(spec["rules"].get("tail") == "cap", f"tail is {spec['rules'].get('tail')!r}: under D01 the only tail is the cap")
cap = amount(spec["cap"]["base_units"], "cap.base_units")
unit = amount(spec["units"]["base_units_per_coin"], "units.base_units_per_coin")
need(unit == 10 ** spec["units"]["decimals"], "base_units_per_coin is not 10^decimals")
need(cap == int(spec["cap"]["coins"]) * unit, "cap.base_units is not cap.coins x base_units_per_coin")
sup = spec["superseded"]
need(any(s.get("status") == "SUPERSEDED" and "tail reward" in s.get("text", "") and s.get("by", "").startswith("D01") for s in sup),
"the litepaper's tail-vote sentence is not named SUPERSEDED by D01")
ids = set()
for c in spec["conflicts"]:
for k in CONFLICT_KEYS:
need(k in c, f"conflict {c.get('id')}: field {k!r} missing")
need(c["id"] not in ids, f"conflict id {c['id']} repeated")
ids.add(c["id"])
need(c["status"].startswith("RESOLVED") or c["status"] == "BLOCKED", f"conflict {c['id']}: status {c['status']!r}")
if c["status"] == "BLOCKED":
need(c.get("question", "").startswith("For the founder"), f"conflict {c['id']}: BLOCKED without the founder's question")
rr = rules_of(spec)
need(rr["Q"] < 1 << 32 and rr["Q"] > 0, "step_decay_q32 outside 1..2^32: the curve would never fall")
for sc in spec["schedules"]:
name, bps, ph = sc["profile"], sc["bps"], sc["phases"]
need(isinstance(bps, int) and bps > 0, f"{name}: bps {bps!r}")
need(sc["genesis_daa_score"] == 0, f"{name}: genesis DAA score is not 0")
need(len(ph) == sc["phase_count"] and len(ph) >= 2, f"{name}: phase_count disagrees with the list")
need(ph[0]["start_daa_score"] == 0, f"{name}: the first phase does not start at DAA 0")
total, prev_pb = 0, None
for i, p in enumerate(ph):
need(p["index"] == i, f"{name}: phase {i} carries index {p['index']}")
need(p["source"], f"{name} phase {i}: no source line")
if i:
need(p["start_daa_score"] == ph[i - 1]["end_daa_score"], f"{name}: phase {i} does not start where phase {i - 1} ends (a gap or an overlap)")
s = amount(p["phase_sum"], f"{name} phase {i}.phase_sum")
if p["kind"] == "ramp":
need(i == 0, f"{name}: a ramp phase at index {i}")
rs = ramp_sum(rr, bps)
need(s == rs, f"{name}: the ramp's declared sum {s} is not its formula's {rs} (off by {s - rs})")
need(amount(p["ramp"]["full_per_block"], "ramp.full_per_block") == rr["L"] // bps, f"{name}: ramp full_per_block is not launch_rate / bps")
elif p["kind"] == "constant":
pb = amount(p["per_block_subsidy"], f"{name} phase {i}.per_block_subsidy")
need(pb > 0, f"{name}: constant phase {i} pays 0 (that is the terminal)")
need(p["end_daa_score"] > p["start_daa_score"], f"{name}: phase {i} is empty")
need(s == pb * (p["end_daa_score"] - p["start_daa_score"]), f"{name}: phase {i} sum is not per_block x length")
need(prev_pb is None or pb <= prev_pb, f"{name}: phase {i} pays more than phase {i - 1} (issuance rises)")
prev_pb = pb
elif p["kind"] == "terminal":
need(i == len(ph) - 1, f"{name}: a terminal phase before the end")
need(p["end_daa_score"] is None, f"{name}: the terminal phase has an end")
tb = amount(p["per_block_subsidy"], f"{name} terminal per_block_subsidy")
need(tb == 0, f"{name}: the terminal phase pays {tb} base units a block for ever: issuance is unbounded, the overshoot over the cap is infinite")
else:
raise Refused(f"{name}: phase {i} kind {p['kind']!r}")
total += s
need(ph[-1]["kind"] == "terminal", f"{name}: the schedule has no terminal phase (issuance is not shown to end)")
need(sc["transition_count"] == len(ph) - 1, f"{name}: transition_count is not phases - 1")
need(sc["last_paying_daa_score"] == ph[-1]["start_daa_score"] - 1 and sc["last_paying_block_height"] == ph[-1]["start_daa_score"],
f"{name}: the last paying DAA score or height disagrees with the terminal phase (genesis is unrewarded: height = DAA score + 1)")
need(sc["terminal_supply"] == sc["total_issuance"], f"{name}: terminal_supply is not the total issuance")
need(total == amount(sc["total_issuance"], f"{name}.total_issuance"), f"{name}: the phases sum to {total}, the spec says {sc['total_issuance']}")
if total > cap:
raise Refused(f"{name}: overshoot {total - cap} base units ({(total - cap) / unit:,.6f} IGN) over the cap")
short = cap - total
declared = amount(sc["cap_shortfall"], f"{name}.cap_shortfall")
need(short == declared, f"{name}: total + declared shortfall = {total + declared}, not the cap (off by {declared - short} base units)")
if regen:
for p in ph:
if p["kind"] == "ramp":
continue
a = p["start_daa_score"]
pb = per_block(rr, a, bps)
need(str(pb) == p["per_block_subsidy"], f"{name}: phase {p['index']} lists {p['per_block_subsidy']}, the rules give {pb}")
if p["end_daa_score"] is not None:
need(per_block(rr, p["end_daa_score"] - 1, bps) == pb, f"{name}: phase {p['index']} is not constant under the rules")
nb = per_block(rr, p["end_daa_score"], bps)
need(nb != pb, f"{name}: phase {p['index']} ends where the rules do not change")
out.append(f"{name}: {len(ph)} phases, contiguous from DAA 0, terminal zero from DAA {ph[-1]['start_daa_score']:,}; "
f"total {total} + shortfall {short} = cap {cap} exactly (shortfall {short / unit:,.6f} IGN)")
return out
def bps_step_case(spec):
"""KF4: the schedule as the code would pay it if the block rate stepped from 1 to 10 a second at DAA one year in."""
rr = rules_of(spec)
cap = int(spec["cap"]["base_units"])
y = spec["rules"]["year_seconds"]
step = y * 1
s10 = int(next(s for s in spec["schedules"] if s["bps"] == 10)["total_issuance"])
issued = supply_to(rr, 1, step) + (s10 - supply_to(rr, 10, step))
need(issued <= cap, f"a 1-to-10 block-rate step at DAA {step:,} keyed as daa / bps: issuance {issued}, overshoot {issued - cap} base units over the cap")
return issued
def main():
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT
spec = json.load(open(path))
fails = 0
def known_failed(label, mutate, run=None):
nonlocal fails
s = copy.deepcopy(spec)
mutate(s)
try:
(run or (lambda x: check(x, regen=False)))(s)
except Refused as e:
print(f"known-failed {label}: refused as expected: {e}")
return
print(f"known-failed {label}: NOT refused (the checker is broken)")
fails += 1
def kf1(s):
sc = s["schedules"][0]
sc["phases"][-1]["per_block_subsidy"] = "1"
def kf2(s):
sc = s["schedules"][0]
p = sc["phases"][1]
old = int(p["phase_sum"])
pb = int(p["per_block_subsidy"]) * 2
p["per_block_subsidy"] = str(pb)
p["phase_sum"] = str(pb * (p["end_daa_score"] - p["start_daa_score"]))
sc["total_issuance"] = str(int(sc["total_issuance"]) - old + int(p["phase_sum"]))
sc["cap_shortfall"] = str(int(s["cap"]["base_units"]) - int(sc["total_issuance"])) # may go negative: str(-x) fails too
def kf2_run(s):
sc = s["schedules"][0]
if sc["cap_shortfall"].startswith("-"):
sc["cap_shortfall"] = "0" # make the field well-formed so the overshoot itself is what refuses it
return check(s, regen=False)
def kf3(s):
sc = s["schedules"][0]
sc["cap_shortfall"] = str(int(sc["cap_shortfall"]) + 1)
known_failed("KF1 (a tail: the terminal pays 1 base unit a block for ever)", kf1)
known_failed("KF2 (an emergency mint: the first full-rate phase doubled, sums self-consistent)", kf2, kf2_run)
known_failed("KF3 (the declared shortfall one base unit off)", kf3)
known_failed("KF4 (a 1-to-10 block-rate step one year in, time keyed as daa / bps)", lambda s: None, bps_step_case)
try:
lines = check(spec)
except Refused as e:
print(f"spec {path}: REFUSED: {e}")
return 1
for l in lines:
print(l)
blocked = [c["id"] for c in spec["conflicts"] if c["status"] == "BLOCKED"]
print(f"conflicts: {len(spec['conflicts'])} rows, BLOCKED: {', '.join(blocked) or 'none'}; gate TV-01 verdict as written: {spec['verdict']}")
if fails:
print(f"checker: {fails} known-failed case(s) were not refused; the result above is not trusted")
return 2
print("check: well-formed and exact (a check result, not a gate verdict)")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,139 @@
{"network":"mainnet","bps":10,"decimals":18,"launch_rate":"31688087810000000000","ramp_seconds":2592000,"ramp_start_percent":10,"step_seconds":63115200,"step_decay_q32":2147483648,"tail":"cap","steps":66,"genesis_daa_score":1312860}
{"network":"mainnet","phase":"ramp","start_daa":0,"end_daa":25920000,"first_block":"316880878100000000","last_block":"3168807680719173263","sum":"45174523722296485488480000"}
{"network":"mainnet","phase":"step","k":0,"start_daa":25920000,"end_daa":631152000,"per_block":"3168808781000000000","rate_per_second":"31688087810000000000","sum":"1917864476142192000000000000"}
{"network":"mainnet","phase":"step","k":1,"start_daa":631152000,"end_daa":1262304000,"per_block":"1584404390500000000","rate_per_second":"15844043905000000000","sum":"999999999872856000000000000"}
{"network":"mainnet","phase":"step","k":2,"start_daa":1262304000,"end_daa":1893456000,"per_block":"792202195250000000","rate_per_second":"7922021952500000000","sum":"499999999936428000000000000"}
{"network":"mainnet","phase":"step","k":3,"start_daa":1893456000,"end_daa":2524608000,"per_block":"396101097625000000","rate_per_second":"3961010976250000000","sum":"249999999968214000000000000"}
{"network":"mainnet","phase":"step","k":4,"start_daa":2524608000,"end_daa":3155760000,"per_block":"198050548812500000","rate_per_second":"1980505488125000000","sum":"124999999984107000000000000"}
{"network":"mainnet","phase":"step","k":5,"start_daa":3155760000,"end_daa":3786912000,"per_block":"99025274406250000","rate_per_second":"990252744062500000","sum":"62499999992053500000000000"}
{"network":"mainnet","phase":"step","k":6,"start_daa":3786912000,"end_daa":4418064000,"per_block":"49512637203125000","rate_per_second":"495126372031250000","sum":"31249999996026750000000000"}
{"network":"mainnet","phase":"step","k":7,"start_daa":4418064000,"end_daa":5049216000,"per_block":"24756318601562500","rate_per_second":"247563186015625000","sum":"15624999998013375000000000"}
{"network":"mainnet","phase":"step","k":8,"start_daa":5049216000,"end_daa":5680368000,"per_block":"12378159300781250","rate_per_second":"123781593007812500","sum":"7812499999006687500000000"}
{"network":"mainnet","phase":"step","k":9,"start_daa":5680368000,"end_daa":6311520000,"per_block":"6189079650390625","rate_per_second":"61890796503906250","sum":"3906249999503343750000000"}
{"network":"mainnet","phase":"step","k":10,"start_daa":6311520000,"end_daa":6942672000,"per_block":"3094539825195312","rate_per_second":"30945398251953125","sum":"1953124999751671559424000"}
{"network":"mainnet","phase":"step","k":11,"start_daa":6942672000,"end_daa":7573824000,"per_block":"1547269912597656","rate_per_second":"15472699125976562","sum":"976562499875835779712000"}
{"network":"mainnet","phase":"step","k":12,"start_daa":7573824000,"end_daa":8204976000,"per_block":"773634956298828","rate_per_second":"7736349562988281","sum":"488281249937917889856000"}
{"network":"mainnet","phase":"step","k":13,"start_daa":8204976000,"end_daa":8836128000,"per_block":"386817478149414","rate_per_second":"3868174781494140","sum":"244140624968958944928000"}
{"network":"mainnet","phase":"step","k":14,"start_daa":8836128000,"end_daa":9467280000,"per_block":"193408739074707","rate_per_second":"1934087390747070","sum":"122070312484479472464000"}
{"network":"mainnet","phase":"step","k":15,"start_daa":9467280000,"end_daa":10098432000,"per_block":"96704369537353","rate_per_second":"967043695373535","sum":"61035156242239420656000"}
{"network":"mainnet","phase":"step","k":16,"start_daa":10098432000,"end_daa":10729584000,"per_block":"48352184768676","rate_per_second":"483521847686767","sum":"30517578121119394752000"}
{"network":"mainnet","phase":"step","k":17,"start_daa":10729584000,"end_daa":11360736000,"per_block":"24176092384338","rate_per_second":"241760923843383","sum":"15258789060559697376000"}
{"network":"mainnet","phase":"step","k":18,"start_daa":11360736000,"end_daa":11991888000,"per_block":"12088046192169","rate_per_second":"120880461921691","sum":"7629394530279848688000"}
{"network":"mainnet","phase":"step","k":19,"start_daa":11991888000,"end_daa":12623040000,"per_block":"6044023096084","rate_per_second":"60440230960845","sum":"3814697265139608768000"}
{"network":"mainnet","phase":"step","k":20,"start_daa":12623040000,"end_daa":13254192000,"per_block":"3022011548042","rate_per_second":"30220115480422","sum":"1907348632569804384000"}
{"network":"mainnet","phase":"step","k":21,"start_daa":13254192000,"end_daa":13885344000,"per_block":"1511005774021","rate_per_second":"15110057740211","sum":"953674316284902192000"}
{"network":"mainnet","phase":"step","k":22,"start_daa":13885344000,"end_daa":14516496000,"per_block":"755502887010","rate_per_second":"7555028870105","sum":"476837158142135520000"}
{"network":"mainnet","phase":"step","k":23,"start_daa":14516496000,"end_daa":15147648000,"per_block":"377751443505","rate_per_second":"3777514435052","sum":"238418579071067760000"}
{"network":"mainnet","phase":"step","k":24,"start_daa":15147648000,"end_daa":15778800000,"per_block":"188875721752","rate_per_second":"1888757217526","sum":"119209289535218304000"}
{"network":"mainnet","phase":"step","k":25,"start_daa":15778800000,"end_daa":16409952000,"per_block":"94437860876","rate_per_second":"944378608763","sum":"59604644767609152000"}
{"network":"mainnet","phase":"step","k":26,"start_daa":16409952000,"end_daa":17041104000,"per_block":"47218930438","rate_per_second":"472189304381","sum":"29802322383804576000"}
{"network":"mainnet","phase":"step","k":27,"start_daa":17041104000,"end_daa":17672256000,"per_block":"23609465219","rate_per_second":"236094652190","sum":"14901161191902288000"}
{"network":"mainnet","phase":"step","k":28,"start_daa":17672256000,"end_daa":18303408000,"per_block":"11804732609","rate_per_second":"118047326095","sum":"7450580595635568000"}
{"network":"mainnet","phase":"step","k":29,"start_daa":18303408000,"end_daa":18934560000,"per_block":"5902366304","rate_per_second":"59023663047","sum":"3725290297502208000"}
{"network":"mainnet","phase":"step","k":30,"start_daa":18934560000,"end_daa":19565712000,"per_block":"2951183152","rate_per_second":"29511831523","sum":"1862645148751104000"}
{"network":"mainnet","phase":"step","k":31,"start_daa":19565712000,"end_daa":20196864000,"per_block":"1475591576","rate_per_second":"14755915761","sum":"931322574375552000"}
{"network":"mainnet","phase":"step","k":32,"start_daa":20196864000,"end_daa":20828016000,"per_block":"737795788","rate_per_second":"7377957880","sum":"465661287187776000"}
{"network":"mainnet","phase":"step","k":33,"start_daa":20828016000,"end_daa":21459168000,"per_block":"368897894","rate_per_second":"3688978940","sum":"232830643593888000"}
{"network":"mainnet","phase":"step","k":34,"start_daa":21459168000,"end_daa":22090320000,"per_block":"184448947","rate_per_second":"1844489470","sum":"116415321796944000"}
{"network":"mainnet","phase":"step","k":35,"start_daa":22090320000,"end_daa":22721472000,"per_block":"92224473","rate_per_second":"922244735","sum":"58207660582896000"}
{"network":"mainnet","phase":"step","k":36,"start_daa":22721472000,"end_daa":23352624000,"per_block":"46112236","rate_per_second":"461122367","sum":"29103829975872000"}
{"network":"mainnet","phase":"step","k":37,"start_daa":23352624000,"end_daa":23983776000,"per_block":"23056118","rate_per_second":"230561183","sum":"14551914987936000"}
{"network":"mainnet","phase":"step","k":38,"start_daa":23983776000,"end_daa":24614928000,"per_block":"11528059","rate_per_second":"115280591","sum":"7275957493968000"}
{"network":"mainnet","phase":"step","k":39,"start_daa":24614928000,"end_daa":25246080000,"per_block":"5764029","rate_per_second":"57640295","sum":"3637978431408000"}
{"network":"mainnet","phase":"step","k":40,"start_daa":25246080000,"end_daa":25877232000,"per_block":"2882014","rate_per_second":"28820147","sum":"1818988900128000"}
{"network":"mainnet","phase":"step","k":41,"start_daa":25877232000,"end_daa":26508384000,"per_block":"1441007","rate_per_second":"14410073","sum":"909494450064000"}
{"network":"mainnet","phase":"step","k":42,"start_daa":26508384000,"end_daa":27139536000,"per_block":"720503","rate_per_second":"7205036","sum":"454746909456000"}
{"network":"mainnet","phase":"step","k":43,"start_daa":27139536000,"end_daa":27770688000,"per_block":"360251","rate_per_second":"3602518","sum":"227373139152000"}
{"network":"mainnet","phase":"step","k":44,"start_daa":27770688000,"end_daa":28401840000,"per_block":"180125","rate_per_second":"1801259","sum":"113686254000000"}
{"network":"mainnet","phase":"step","k":45,"start_daa":28401840000,"end_daa":29032992000,"per_block":"90062","rate_per_second":"900629","sum":"56842811424000"}
{"network":"mainnet","phase":"step","k":46,"start_daa":29032992000,"end_daa":29664144000,"per_block":"45031","rate_per_second":"450314","sum":"28421405712000"}
{"network":"mainnet","phase":"step","k":47,"start_daa":29664144000,"end_daa":30295296000,"per_block":"22515","rate_per_second":"225157","sum":"14210387280000"}
{"network":"mainnet","phase":"step","k":48,"start_daa":30295296000,"end_daa":30926448000,"per_block":"11257","rate_per_second":"112578","sum":"7104878064000"}
{"network":"mainnet","phase":"step","k":49,"start_daa":30926448000,"end_daa":31557600000,"per_block":"5628","rate_per_second":"56289","sum":"3552123456000"}
{"network":"mainnet","phase":"step","k":50,"start_daa":31557600000,"end_daa":32188752000,"per_block":"2814","rate_per_second":"28144","sum":"1776061728000"}
{"network":"mainnet","phase":"step","k":51,"start_daa":32188752000,"end_daa":32819904000,"per_block":"1407","rate_per_second":"14072","sum":"888030864000"}
{"network":"mainnet","phase":"step","k":52,"start_daa":32819904000,"end_daa":33451056000,"per_block":"703","rate_per_second":"7036","sum":"443699856000"}
{"network":"mainnet","phase":"step","k":53,"start_daa":33451056000,"end_daa":34082208000,"per_block":"351","rate_per_second":"3518","sum":"221534352000"}
{"network":"mainnet","phase":"step","k":54,"start_daa":34082208000,"end_daa":34713360000,"per_block":"175","rate_per_second":"1759","sum":"110451600000"}
{"network":"mainnet","phase":"step","k":55,"start_daa":34713360000,"end_daa":35344512000,"per_block":"87","rate_per_second":"879","sum":"54910224000"}
{"network":"mainnet","phase":"step","k":56,"start_daa":35344512000,"end_daa":35975664000,"per_block":"43","rate_per_second":"439","sum":"27139536000"}
{"network":"mainnet","phase":"step","k":57,"start_daa":35975664000,"end_daa":36606816000,"per_block":"21","rate_per_second":"219","sum":"13254192000"}
{"network":"mainnet","phase":"step","k":58,"start_daa":36606816000,"end_daa":37237968000,"per_block":"10","rate_per_second":"109","sum":"6311520000"}
{"network":"mainnet","phase":"step","k":59,"start_daa":37237968000,"end_daa":37869120000,"per_block":"5","rate_per_second":"54","sum":"3155760000"}
{"network":"mainnet","phase":"step","k":60,"start_daa":37869120000,"end_daa":38500272000,"per_block":"2","rate_per_second":"27","sum":"1262304000"}
{"network":"mainnet","phase":"step","k":61,"start_daa":38500272000,"end_daa":39131424000,"per_block":"1","rate_per_second":"13","sum":"631152000"}
{"network":"mainnet","phase":"step","k":62,"start_daa":39131424000,"end_daa":39762576000,"per_block":"0","rate_per_second":"6","sum":"0"}
{"network":"mainnet","phase":"terminal","start_daa":39131424000,"per_block":"0"}
{"network":"mainnet","total":"3963038999610200467816224000","cap":"4000000000000000000000000000","shortfall":"36961000389799532183776000"}
{"network":"devnet-4","bps":1,"decimals":18,"launch_rate":"31688087810000000000","ramp_seconds":2592000,"ramp_start_percent":10,"step_seconds":63115200,"step_decay_q32":2147483648,"tail":"cap","steps":66,"genesis_daa_score":0}
{"network":"devnet-4","phase":"ramp","start_daa":0,"end_daa":2592000,"first_block":"3168808781000000000","last_block":"31688076807191732638","sum":"45174523722296485498848000"}
{"network":"devnet-4","phase":"step","k":0,"start_daa":2592000,"end_daa":63115200,"per_block":"31688087810000000000","rate_per_second":"31688087810000000000","sum":"1917864476142192000000000000"}
{"network":"devnet-4","phase":"step","k":1,"start_daa":63115200,"end_daa":126230400,"per_block":"15844043905000000000","rate_per_second":"15844043905000000000","sum":"999999999872856000000000000"}
{"network":"devnet-4","phase":"step","k":2,"start_daa":126230400,"end_daa":189345600,"per_block":"7922021952500000000","rate_per_second":"7922021952500000000","sum":"499999999936428000000000000"}
{"network":"devnet-4","phase":"step","k":3,"start_daa":189345600,"end_daa":252460800,"per_block":"3961010976250000000","rate_per_second":"3961010976250000000","sum":"249999999968214000000000000"}
{"network":"devnet-4","phase":"step","k":4,"start_daa":252460800,"end_daa":315576000,"per_block":"1980505488125000000","rate_per_second":"1980505488125000000","sum":"124999999984107000000000000"}
{"network":"devnet-4","phase":"step","k":5,"start_daa":315576000,"end_daa":378691200,"per_block":"990252744062500000","rate_per_second":"990252744062500000","sum":"62499999992053500000000000"}
{"network":"devnet-4","phase":"step","k":6,"start_daa":378691200,"end_daa":441806400,"per_block":"495126372031250000","rate_per_second":"495126372031250000","sum":"31249999996026750000000000"}
{"network":"devnet-4","phase":"step","k":7,"start_daa":441806400,"end_daa":504921600,"per_block":"247563186015625000","rate_per_second":"247563186015625000","sum":"15624999998013375000000000"}
{"network":"devnet-4","phase":"step","k":8,"start_daa":504921600,"end_daa":568036800,"per_block":"123781593007812500","rate_per_second":"123781593007812500","sum":"7812499999006687500000000"}
{"network":"devnet-4","phase":"step","k":9,"start_daa":568036800,"end_daa":631152000,"per_block":"61890796503906250","rate_per_second":"61890796503906250","sum":"3906249999503343750000000"}
{"network":"devnet-4","phase":"step","k":10,"start_daa":631152000,"end_daa":694267200,"per_block":"30945398251953125","rate_per_second":"30945398251953125","sum":"1953124999751671875000000"}
{"network":"devnet-4","phase":"step","k":11,"start_daa":694267200,"end_daa":757382400,"per_block":"15472699125976562","rate_per_second":"15472699125976562","sum":"976562499875835905942400"}
{"network":"devnet-4","phase":"step","k":12,"start_daa":757382400,"end_daa":820497600,"per_block":"7736349562988281","rate_per_second":"7736349562988281","sum":"488281249937917952971200"}
{"network":"devnet-4","phase":"step","k":13,"start_daa":820497600,"end_daa":883612800,"per_block":"3868174781494140","rate_per_second":"3868174781494140","sum":"244140624968958944928000"}
{"network":"devnet-4","phase":"step","k":14,"start_daa":883612800,"end_daa":946728000,"per_block":"1934087390747070","rate_per_second":"1934087390747070","sum":"122070312484479472464000"}
{"network":"devnet-4","phase":"step","k":15,"start_daa":946728000,"end_daa":1009843200,"per_block":"967043695373535","rate_per_second":"967043695373535","sum":"61035156242239736232000"}
{"network":"devnet-4","phase":"step","k":16,"start_daa":1009843200,"end_daa":1072958400,"per_block":"483521847686767","rate_per_second":"483521847686767","sum":"30517578121119836558400"}
{"network":"devnet-4","phase":"step","k":17,"start_daa":1072958400,"end_daa":1136073600,"per_block":"241760923843383","rate_per_second":"241760923843383","sum":"15258789060559886721600"}
{"network":"devnet-4","phase":"step","k":18,"start_daa":1136073600,"end_daa":1199188800,"per_block":"120880461921691","rate_per_second":"120880461921691","sum":"7629394530279911803200"}
{"network":"devnet-4","phase":"step","k":19,"start_daa":1199188800,"end_daa":1262304000,"per_block":"60440230960845","rate_per_second":"60440230960845","sum":"3814697265139924344000"}
{"network":"devnet-4","phase":"step","k":20,"start_daa":1262304000,"end_daa":1325419200,"per_block":"30220115480422","rate_per_second":"30220115480422","sum":"1907348632569930614400"}
{"network":"devnet-4","phase":"step","k":21,"start_daa":1325419200,"end_daa":1388534400,"per_block":"15110057740211","rate_per_second":"15110057740211","sum":"953674316284965307200"}
{"network":"devnet-4","phase":"step","k":22,"start_daa":1388534400,"end_daa":1451649600,"per_block":"7555028870105","rate_per_second":"7555028870105","sum":"476837158142451096000"}
{"network":"devnet-4","phase":"step","k":23,"start_daa":1451649600,"end_daa":1514764800,"per_block":"3777514435052","rate_per_second":"3777514435052","sum":"238418579071193990400"}
{"network":"devnet-4","phase":"step","k":24,"start_daa":1514764800,"end_daa":1577880000,"per_block":"1888757217526","rate_per_second":"1888757217526","sum":"119209289535596995200"}
{"network":"devnet-4","phase":"step","k":25,"start_daa":1577880000,"end_daa":1640995200,"per_block":"944378608763","rate_per_second":"944378608763","sum":"59604644767798497600"}
{"network":"devnet-4","phase":"step","k":26,"start_daa":1640995200,"end_daa":1704110400,"per_block":"472189304381","rate_per_second":"472189304381","sum":"29802322383867691200"}
{"network":"devnet-4","phase":"step","k":27,"start_daa":1704110400,"end_daa":1767225600,"per_block":"236094652190","rate_per_second":"236094652190","sum":"14901161191902288000"}
{"network":"devnet-4","phase":"step","k":28,"start_daa":1767225600,"end_daa":1830340800,"per_block":"118047326095","rate_per_second":"118047326095","sum":"7450580595951144000"}
{"network":"devnet-4","phase":"step","k":29,"start_daa":1830340800,"end_daa":1893456000,"per_block":"59023663047","rate_per_second":"59023663047","sum":"3725290297944014400"}
{"network":"devnet-4","phase":"step","k":30,"start_daa":1893456000,"end_daa":1956571200,"per_block":"29511831523","rate_per_second":"29511831523","sum":"1862645148940449600"}
{"network":"devnet-4","phase":"step","k":31,"start_daa":1956571200,"end_daa":2019686400,"per_block":"14755915761","rate_per_second":"14755915761","sum":"931322574438667200"}
{"network":"devnet-4","phase":"step","k":32,"start_daa":2019686400,"end_daa":2082801600,"per_block":"7377957880","rate_per_second":"7377957880","sum":"465661287187776000"}
{"network":"devnet-4","phase":"step","k":33,"start_daa":2082801600,"end_daa":2145916800,"per_block":"3688978940","rate_per_second":"3688978940","sum":"232830643593888000"}
{"network":"devnet-4","phase":"step","k":34,"start_daa":2145916800,"end_daa":2209032000,"per_block":"1844489470","rate_per_second":"1844489470","sum":"116415321796944000"}
{"network":"devnet-4","phase":"step","k":35,"start_daa":2209032000,"end_daa":2272147200,"per_block":"922244735","rate_per_second":"922244735","sum":"58207660898472000"}
{"network":"devnet-4","phase":"step","k":36,"start_daa":2272147200,"end_daa":2335262400,"per_block":"461122367","rate_per_second":"461122367","sum":"29103830417678400"}
{"network":"devnet-4","phase":"step","k":37,"start_daa":2335262400,"end_daa":2398377600,"per_block":"230561183","rate_per_second":"230561183","sum":"14551915177281600"}
{"network":"devnet-4","phase":"step","k":38,"start_daa":2398377600,"end_daa":2461492800,"per_block":"115280591","rate_per_second":"115280591","sum":"7275957557083200"}
{"network":"devnet-4","phase":"step","k":39,"start_daa":2461492800,"end_daa":2524608000,"per_block":"57640295","rate_per_second":"57640295","sum":"3637978746984000"}
{"network":"devnet-4","phase":"step","k":40,"start_daa":2524608000,"end_daa":2587723200,"per_block":"28820147","rate_per_second":"28820147","sum":"1818989341934400"}
{"network":"devnet-4","phase":"step","k":41,"start_daa":2587723200,"end_daa":2650838400,"per_block":"14410073","rate_per_second":"14410073","sum":"909494639409600"}
{"network":"devnet-4","phase":"step","k":42,"start_daa":2650838400,"end_daa":2713953600,"per_block":"7205036","rate_per_second":"7205036","sum":"454747288147200"}
{"network":"devnet-4","phase":"step","k":43,"start_daa":2713953600,"end_daa":2777068800,"per_block":"3602518","rate_per_second":"3602518","sum":"227373644073600"}
{"network":"devnet-4","phase":"step","k":44,"start_daa":2777068800,"end_daa":2840184000,"per_block":"1801259","rate_per_second":"1801259","sum":"113686822036800"}
{"network":"devnet-4","phase":"step","k":45,"start_daa":2840184000,"end_daa":2903299200,"per_block":"900629","rate_per_second":"900629","sum":"56843379460800"}
{"network":"devnet-4","phase":"step","k":46,"start_daa":2903299200,"end_daa":2966414400,"per_block":"450314","rate_per_second":"450314","sum":"28421658172800"}
{"network":"devnet-4","phase":"step","k":47,"start_daa":2966414400,"end_daa":3029529600,"per_block":"225157","rate_per_second":"225157","sum":"14210829086400"}
{"network":"devnet-4","phase":"step","k":48,"start_daa":3029529600,"end_daa":3092644800,"per_block":"112578","rate_per_second":"112578","sum":"7105382985600"}
{"network":"devnet-4","phase":"step","k":49,"start_daa":3092644800,"end_daa":3155760000,"per_block":"56289","rate_per_second":"56289","sum":"3552691492800"}
{"network":"devnet-4","phase":"step","k":50,"start_daa":3155760000,"end_daa":3218875200,"per_block":"28144","rate_per_second":"28144","sum":"1776314188800"}
{"network":"devnet-4","phase":"step","k":51,"start_daa":3218875200,"end_daa":3281990400,"per_block":"14072","rate_per_second":"14072","sum":"888157094400"}
{"network":"devnet-4","phase":"step","k":52,"start_daa":3281990400,"end_daa":3345105600,"per_block":"7036","rate_per_second":"7036","sum":"444078547200"}
{"network":"devnet-4","phase":"step","k":53,"start_daa":3345105600,"end_daa":3408220800,"per_block":"3518","rate_per_second":"3518","sum":"222039273600"}
{"network":"devnet-4","phase":"step","k":54,"start_daa":3408220800,"end_daa":3471336000,"per_block":"1759","rate_per_second":"1759","sum":"111019636800"}
{"network":"devnet-4","phase":"step","k":55,"start_daa":3471336000,"end_daa":3534451200,"per_block":"879","rate_per_second":"879","sum":"55478260800"}
{"network":"devnet-4","phase":"step","k":56,"start_daa":3534451200,"end_daa":3597566400,"per_block":"439","rate_per_second":"439","sum":"27707572800"}
{"network":"devnet-4","phase":"step","k":57,"start_daa":3597566400,"end_daa":3660681600,"per_block":"219","rate_per_second":"219","sum":"13822228800"}
{"network":"devnet-4","phase":"step","k":58,"start_daa":3660681600,"end_daa":3723796800,"per_block":"109","rate_per_second":"109","sum":"6879556800"}
{"network":"devnet-4","phase":"step","k":59,"start_daa":3723796800,"end_daa":3786912000,"per_block":"54","rate_per_second":"54","sum":"3408220800"}
{"network":"devnet-4","phase":"step","k":60,"start_daa":3786912000,"end_daa":3850027200,"per_block":"27","rate_per_second":"27","sum":"1704110400"}
{"network":"devnet-4","phase":"step","k":61,"start_daa":3850027200,"end_daa":3913142400,"per_block":"13","rate_per_second":"13","sum":"820497600"}
{"network":"devnet-4","phase":"step","k":62,"start_daa":3913142400,"end_daa":3976257600,"per_block":"6","rate_per_second":"6","sum":"378691200"}
{"network":"devnet-4","phase":"step","k":63,"start_daa":3976257600,"end_daa":4039372800,"per_block":"3","rate_per_second":"3","sum":"189345600"}
{"network":"devnet-4","phase":"step","k":64,"start_daa":4039372800,"end_daa":4102488000,"per_block":"1","rate_per_second":"1","sum":"63115200"}
{"network":"devnet-4","phase":"step","k":65,"start_daa":4102488000,"end_daa":4165603200,"per_block":"0","rate_per_second":"0","sum":"0"}
{"network":"devnet-4","phase":"terminal","start_daa":4102488000,"per_block":"0"}
{"network":"devnet-4","total":"3963038999610200483289816000","cap":"4000000000000000000000000000","shortfall":"36961000389799516710184000"}
{"network":"testnet-1","bps":1,"decimals":18,"launch_rate":"100000000000000000000","ramp_seconds":7776000,"ramp_start_percent":10,"step_seconds":2629800,"step_decay_q32":4172697914,"tail":"percent:100","steps":125,"genesis_daa_score":0}
{"network":"testnet-1","note":"not a cap schedule; phases not dumped"}

View file

@ -0,0 +1,60 @@
//! TV-01 (token value phase 0, 9 October 2026): the per-block subsidy schedule of each compiled network, read from the
//! consensus code's own functions (`Params::emission`, `EmissionTable::block_subsidy`, `Params::bps`), as JSON lines.
//! Lane scratch file on build-9 only; not part of the node fork.
use kaspa_consensus_core::config::params::{devnet4_params, Params, MAINNET_PARAMS, TESTNET_PARAMS};
use kaspa_consensus_core::emission::Tail;
fn dump(name: &str, p: &Params) {
let t = p.emission.table();
let s = p.emission;
let bps = p.bps();
let tail = match s.tail { Tail::Cap => "cap".to_string(), Tail::Fixed { rate } => format!("fixed:{rate}"), Tail::Percent { bps_per_year } => format!("percent:{bps_per_year}") };
println!(
"{{\"network\":\"{name}\",\"bps\":{bps},\"decimals\":{},\"launch_rate\":\"{}\",\"ramp_seconds\":{},\"ramp_start_percent\":{},\"step_seconds\":{},\"step_decay_q32\":{},\"tail\":\"{tail}\",\"steps\":{},\"genesis_daa_score\":{}}}",
p.base_unit_decimals, s.launch_rate, s.ramp_seconds, s.ramp_start_percent, s.step_seconds, s.step_decay_q32, t.step_rate.len(), p.genesis.daa_score
);
if s.tail != Tail::Cap {
println!("{{\"network\":\"{name}\",\"note\":\"not a cap schedule; phases not dumped\"}}");
return;
}
// the ramp: every block of DAA second sec pays block_subsidy(sec * bps); sum per DAA score
let mut ramp_sum: u128 = 0;
for sec in 0..s.ramp_seconds {
let a = t.block_subsidy(sec * bps, bps);
let b = t.block_subsidy(sec * bps + bps - 1, bps);
assert_eq!(a, b, "constant inside a DAA second");
ramp_sum += a * bps as u128;
}
let first = t.block_subsidy(0, bps);
let last_ramp = t.block_subsidy(s.ramp_seconds * bps - 1, bps);
println!("{{\"network\":\"{name}\",\"phase\":\"ramp\",\"start_daa\":0,\"end_daa\":{},\"first_block\":\"{first}\",\"last_block\":\"{last_ramp}\",\"sum\":\"{ramp_sum}\"}}", s.ramp_seconds * bps);
let mut total = ramp_sum;
let mut k: u64 = 0;
loop {
let start = if k == 0 { s.ramp_seconds * bps } else { k * s.step_seconds * bps };
let end = (k + 1) * s.step_seconds * bps;
let per = t.block_subsidy(start, bps);
let per_end = t.block_subsidy(end - 1, bps);
assert_eq!(per, per_end, "constant inside step {k}");
let n = (end - start) as u128;
total += per * n;
println!("{{\"network\":\"{name}\",\"phase\":\"step\",\"k\":{k},\"start_daa\":{start},\"end_daa\":{end},\"per_block\":\"{per}\",\"rate_per_second\":\"{}\",\"sum\":\"{}\"}}", t.rate_at(start / bps), per * n);
if per == 0 {
// the terminal: zero at every later DAA score the table can reach
for probe in [end, end * 2, u64::MAX / bps * bps - 1, u64::MAX] {
assert_eq!(t.block_subsidy(probe, bps), 0);
}
println!("{{\"network\":\"{name}\",\"phase\":\"terminal\",\"start_daa\":{start},\"per_block\":\"0\"}}");
break;
}
k += 1;
}
let cap = kaspa_consensus_core::igneum::supply_cap_units(p.base_unit_decimals);
println!("{{\"network\":\"{name}\",\"total\":\"{total}\",\"cap\":\"{cap}\",\"shortfall\":\"{}\"}}", cap - total);
}
fn main() {
dump("mainnet", &MAINNET_PARAMS);
dump("devnet-4", &devnet4_params());
dump("testnet-1", &TESTNET_PARAMS);
}

View file

@ -0,0 +1,613 @@
#!/usr/bin/env python3
"""TV-01 (Igneum 2.0 Token Value, Phase 0): generates the machine-readable supply spec.
Reads nothing it does not cite. The schedule constants are transcribed from the node fork's consensus crate and every
transcription is anchored: for each cited line the generator reads the file at each named revision (git show) and
refuses to write the spec when the line no longer carries its anchor text. The schedule is then computed here in exact
integers (Python ints), and, when --code-dump is given, compared field by field with the dump the consensus code's own
functions printed on the build box (tools/token-value/tv-01/code-dump/tv01_schedule.rs.txt), so every figure in the spec is
one the code agrees with.
python3 tools/token-value/tv-01/gen-spec.py \
--node-git <igneum-node clone holding bf60948a and a284380b> \
--igneum-root . --igneum-rev 6824d49cc32def4463b643293a5622545618f7a9 \
--code-dump tools/token-value/tv-01/code-dump/schedule-bf60948a.jsonl \
--out docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json \
--md docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.md
Deterministic: the same inputs give the same bytes. Nothing here activates anything.
"""
import argparse
import hashlib
import json
import subprocess
import sys
NODE_REVS = {
"release-2.0.2-node": "a284380bdd62d5d0d972cb38f2cbd1daeedda69d",
"node 2.0.2 shipped (detached-5d53a591)": "5d53a591469b3f2b31eb87ec793faa582e95cb18",
"release-2.0.3-node-k6": "bf60948a01467bb54eaab9e50d32b91058f11242",
"release-2.0.3-node-k7": "27f54124375ca0901ea0ead30eb4773413f6602f",
}
# the litepaper revision the tail-vote sentence is quoted from (before the site lane's D01 edit, ad64e48fa)
LITEPAPER_SUPERSEDED_REV = "6824d49cc32def4463b643293a5622545618f7a9"
# ---- The constants, as the code writes them (each one anchored below) -------------------------------------------
YEAR_SECONDS = 31_557_600 # emission.rs:32
LAUNCH_RATE_8 = 3_168_808_781 # emission.rs:104 (10^17 / 31,557,600 floored)
RAMP_SECONDS = 2_592_000 # emission.rs:105
RAMP_START_PERCENT = 10 # emission.rs:106
STEP_SECONDS = 2 * YEAR_SECONDS # emission.rs:107
STEP_DECAY_Q32 = 1 << 31 # emission.rs:108
DECIMALS = 18 # unit.rs:38, params.rs:1816
UNIT = 10 ** DECIMALS
LAUNCH_RATE = LAUNCH_RATE_8 * 10 ** (DECIMALS - 8) # emission.rs rescaled(8, 18); params.rs:1858
CAP = 4_000_000_000 * UNIT # igneum.rs:154 supply_cap_units(18)
PROVING_POOL_SHARE_PERCENT = 20 # igneum.rs:44
SIGNING_BONUS_BPS = 1_000 # params.rs:1810
DEVELOPER_SHARE_PERCENT = 20 # exec config (pgas.rs:290 reads it)
MAINNET_BPS = 10 # params.rs:1789 BlockrateParams::new::<10>()
DEVNET4_BPS = 1 # params.rs:2208 (DEVNET_PARAMS, inherited by devnet3/devnet4)
MAINNET_GENESIS_DAA_COMPILED = 1_312_860 # genesis.rs:92
# (id, repo, path, line, anchor text the line must contain)
CITATIONS = [
("year", "node", "consensus/core/src/emission.rs", 32, "pub const YEAR_SECONDS: u64 = 31_557_600;"),
("current", "node", "consensus/core/src/emission.rs", 103, "pub const CURRENT: EmissionSchedule"),
("launch_rate", "node", "consensus/core/src/emission.rs", 104, "launch_rate: 3_168_808_781"),
("ramp_seconds", "node", "consensus/core/src/emission.rs", 105, "ramp_seconds: 2_592_000"),
("ramp_start", "node", "consensus/core/src/emission.rs", 106, "ramp_start_percent: 10"),
("step_seconds", "node", "consensus/core/src/emission.rs", 107, "step_seconds: 2 * YEAR_SECONDS"),
("decay", "node", "consensus/core/src/emission.rs", 108, "step_decay_q32: 1 << 31"),
("tail_cap", "node", "consensus/core/src/emission.rs", 109, "tail: Tail::Cap"),
("testnet1", "node", "consensus/core/src/emission.rs", 116, "pub const TESTNET_1: EmissionSchedule"),
("testnet1_tail", "node", "consensus/core/src/emission.rs", 122, "tail: Tail::Percent { bps_per_year: 100 }"),
("decay_rule", "node", "consensus/core/src/emission.rs", 225, "rate = rate.saturating_mul(s.step_decay_q32 as u128) >> 32;"),
("ramp_fn", "node", "consensus/core/src/emission.rs", 316, "pub fn ramp(&self, full: u128, seconds: u64) -> u128"),
("block_subsidy", "node", "consensus/core/src/emission.rs", 329, "pub fn block_subsidy(&self, daa_score: u64, bps: u64) -> u128"),
("per_block_div", "node", "consensus/core/src/emission.rs", 331, "let per_block = self.rate_at(seconds) / bps as u128;"),
("cap_const", "node", "consensus/core/src/igneum.rs", 37, "pub const SUPPLY_CAP_SOMPI: u64 = 4_000_000_000 * SOMPI_PER_KASPA;"),
("pool_share", "node", "consensus/core/src/igneum.rs", 44, "pub const PROVING_POOL_SHARE_PERCENT: u64 = 20;"),
("silent_split", "node", "consensus/core/src/igneum.rs", 110, "pub fn silent_split_units("),
("cap_units", "node", "consensus/core/src/igneum.rs", 154, "pub const fn supply_cap_units(decimals: u8) -> u128"),
("pool_units", "node", "consensus/core/src/igneum.rs", 201, "pub fn proving_pool_share_units("),
("producer_units", "node", "consensus/core/src/igneum.rs", 206, "pub fn producer_share_units("),
("pool_script", "node", "consensus/core/src/igneum.rs", 213, "pub fn proving_pool_script_public_key()"),
("evm_decimals", "node", "consensus/core/src/unit.rs", 38, "pub const EVM_DECIMALS: u8 = 18;"),
("mainnet_deflationary", "node", "consensus/core/src/config/params.rs", 1783, "deflationary_phase_daa_score: 15778800 - 259200,"),
("mainnet_pre_deflationary", "node", "consensus/core/src/config/params.rs", 1784, "pre_deflationary_phase_base_subsidy: 50000000000,"),
("mainnet_bps", "node", "consensus/core/src/config/params.rs", 1789, "blockrate: BlockrateParams::new::<10>(),"),
("mainnet_bonus_off", "node", "consensus/core/src/config/params.rs", 1809, "signing_bonus_activation_daa: u64::MAX,"),
("mainnet_bonus_bps", "node", "consensus/core/src/config/params.rs", 1810, "signing_bonus_bps: 1_000,"),
("mainnet_n8_off", "node", "consensus/core/src/config/params.rs", 1812, "subsidy_per_block_activation_daa: u64::MAX,"),
("mainnet_decimals", "node", "consensus/core/src/config/params.rs", 1816, "base_unit_decimals: crate::unit::EVM_DECIMALS"),
("mainnet_pool_split_off", "node", "consensus/core/src/config/params.rs", 1851, "pool_split_activation_daa: u64::MAX,"),
("mainnet_emission", "node", "consensus/core/src/config/params.rs", 1858, "emission: EmissionSchedule::CURRENT.rescaled(crate::unit::DEVNET_DECIMALS, crate::unit::EVM_DECIMALS),"),
("testnet_bonus_on", "node", "consensus/core/src/config/params.rs", 1955, "signing_bonus_activation_daa: 0,"),
("testnet_n8_on", "node", "consensus/core/src/config/params.rs", 1960, "subsidy_per_block_activation_daa: 0,"),
("testnet_emission", "node", "consensus/core/src/config/params.rs", 2014, "emission: EmissionSchedule::TESTNET_1.rescaled("),
("devnet_bps", "node", "consensus/core/src/config/params.rs", 2208, "blockrate: BlockrateParams::new::<1>(),"),
("devnet_n8_off", "node", "consensus/core/src/config/params.rs", 2232, "subsidy_per_block_activation_daa: u64::MAX,"),
("devnet_pool_split_off", "node", "consensus/core/src/config/params.rs", 2270, "pool_split_activation_daa: u64::MAX,"),
("devnet4_fn", "node", "consensus/core/src/config/params.rs", 2342, "pub fn devnet4_params() -> Params {"),
("devnet4_emission", "node", "consensus/core/src/config/params.rs", 2372, "emission: EmissionSchedule::CURRENT.rescaled(crate::unit::DEVNET_DECIMALS, crate::unit::EVM_DECIMALS),"),
("mainnet_genesis_daa", "node", "consensus/core/src/config/genesis.rs", 92, "daa_score: 1312860, // Checkpoint DAA score"),
("genesis_no_outputs", "node", "consensus/core/src/config/genesis.rs", 26, "Transaction::new(0, Vec::new(), Vec::new(), 0, SUBNETWORK_ID_COINBASE"),
("cb_header", "node", "consensus/src/processes/coinbase.rs", 31, "Kaspa's pre-deflationary phase, 426-month table and Crescendo `bps_history` rescaling are gone."),
("cb_split", "node", "consensus/src/processes/coinbase.rs", 106, "fn producer_and_pool(&self, daa_score: u64, reward: &BlockRewardData)"),
("cb_reds", "node", "consensus/src/processes/coinbase.rs", 152, "for red in ghostdag_data.mergeset_reds.iter()"),
("cb_pool_output", "node", "consensus/src/processes/coinbase.rs", 163, "outputs.push(TransactionOutput::new_units(proving_pool_reward, igneum::proving_pool_script_public_key()));"),
("cb_calc", "node", "consensus/src/processes/coinbase.rs", 262, "pub fn calc_block_subsidy(&self, daa_score: u64) -> Amount"),
("cb_calc_body", "node", "consensus/src/processes/coinbase.rs", 265, "Amount(self.emission.block_subsidy(daa_score, self.bps))"),
("genesis_not_in_window", "node", "consensus/src/processes/window.rs", 177, "Special case: Genesis does not enter the DAA window"),
("cb_daa_blues_only", "node", "consensus/src/processes/coinbase.rs", 139, "for blue in ghostdag_data.mergeset_blues.iter().filter(|h| !mergeset_non_daa.contains(h))"),
("utxo_retired", "node", "consensus/src/pipeline/body_processor/body_validation_in_isolation.rs", 54, "RuleError::UtxoTransactionsRetired"),
("exec_blue_only", "node", "igneum/exec/src/executor.rs", 181, "for b in blocks.iter().filter(|b| b.is_blue)"),
("exec_split", "node", "igneum/exec/src/executor.rs", 189, "silent_split_units(subsidy, b.silent, env.signing_bonus_bps)"),
("exec_miner_credit", "node", "igneum/exec/src/executor.rs", 208, "db.add_balance(b.miner, producer_wei);"),
("exec_pool_credit", "node", "igneum/exec/src/executor.rs", 211, "db.add_balance(PROVING_POOL_ADDRESS, pool_wei);"),
("exec_tip_miner", "node", "igneum/exec/src/executor.rs", 362, "let tip_total = (gas_used as u128) * (price - env.base_fee_exec);"),
("exec_dev_shares", "node", "igneum/exec/src/pgas.rs", 290, "pub fn developer_shares("),
("pool_address", "node", "igneum/exec/src/config.rs", 59, "pub const PROVING_POOL_ADDRESS: Address"),
("override_refused", "node", "kaspad/src/daemon.rs", None, "if network.params_compiled() {"),
("params_compiled", "node", "consensus/core/src/network.rs", 266, "NetworkType::Mainnet | NetworkType::Testnet => true,"),
("lp_cap", "igneum", "site/litepaper.html", 578, "Hard cap of 4 billion IGN, approached and never reached, because emission starts at 1 billion a year and halves every two years for ever"),
("lp_caption", "igneum", "site/litepaper.html", 602, "3,938M IGN in the first 12 years of a 4,000M cap."),
("lp_proving_pool", "igneum", "site/litepaper.html", 609, "is burned there"),
("lp_tail", "igneum-old", "site/litepaper.html", 630, "One rule sits beside the cap. If external proving revenue is under one fifth of the block subsidy over any 90-day window after year 5, the question of a tail reward goes to the miners' signalling vote. The protocol never changes emission by itself."),
("lp_tail_now", "igneum", "site/litepaper.html", 630, "the answer is the security-funding plan under D01, never a tail emission or a vote that expands the cap."),
("lp_cap_now", "igneum", "site/litepaper.html", 630, "The cap is fixed at 4 billion, with no tail emission and no vote that expands it (D01, subject to the pre-launch security-funding gate)."),
("lp_tip_prose", "igneum", "site/litepaper.html", 614, "The priority fee splits two ways: 80% to the miner and provers of that block, 20% to the apps whose code ran"),
("ledger_e22", "igneum", "docs/plans/ledger-decisions.md", 110, "The testnet genesis carries `EmissionSchedule::TESTNET_1`"),
("lp_block_rate", "igneum", "site/litepaper.html", 542, "one block a second at launch with scheduled steps to four and ten"),
("lp_unit", "igneum", "site/litepaper.html", 568, "the base unit is Open"),
("lp_emission_route", "igneum", "site/litepaper.html", 620, "80% the block's miner, 20% the proving pool for the provers of that block"),
("lp_tip_route", "igneum", "site/litepaper.html", 622, "80% the block's miner and provers; 20% the apps whose code ran, per call frame"),
("lp_external_job", "igneum", "site/litepaper.html", 624, "90% the provers who delivered"),
("lp_dev_fee", "igneum", "site/litepaper.html", 625, "1 block template in 100 requested with the dev address"),
("lp_pool_custody", "igneum", "site/litepaper.html", 647, "The reference pool never holds a member"),
("lp_per_block_figure", "igneum", "site/litepaper.html", 668, "31.688 IGN a block, 86,400 blocks a day"),
("lp_mainnet_launch", "igneum", "site/litepaper.html", 775, "Genesis with no premine, 30-day ramp."),
("spec_tip_no_provers", "igneum", "docs/spec/05-fees-and-economics.md", 24, "No part of the tip reaches the provers: O-5.7 is closed at zero (8 October 2026"),
("spec_tip_table_stale", "igneum", "docs/spec/05-fees-and-economics.md", 79, "80 / 20 (producer and provers / developer)"),
("pool_payout_doc", "igneum", "pool/src/payout.rs", 3, "so the pool receives"),
("pool_fee_default", "igneum", "pool/src/config.rs", 58, "fee_percent: 1.0,"),
("pool_distribute", "igneum", "pool/src/pplns.rs", 76, "pub fn distribute(reward_wei: u128, fee_percent: f64, payees: &[Payee])"),
]
def sh(cmd, cwd):
return subprocess.run(cmd, cwd=cwd, check=True, capture_output=True).stdout
def read_node(node_git, rev, path):
return sh(["git", "show", f"{rev}:{path}"], node_git)
def read_igneum(root, rev, path):
return sh(["git", "show", f"{rev}:{path}"], root)
def anchor(citations, node_git, igneum_root, igneum_rev):
"""Every cited line must carry its anchor at every named revision; returns the resolved citation table and the
sha256 of every cited file per revision."""
out, files = [], {}
for cid, repo, path, line, text in citations:
revs = NODE_REVS.items() if repo == "node" else [("igneum master", igneum_rev)] if repo == "igneum" else [("igneum before ad64e48fa", LITEPAPER_SUPERSEDED_REV)]
lines_at = {}
for name, rev in revs:
blob = read_node(node_git, rev, path) if repo == "node" else read_igneum(igneum_root, rev, path)
files.setdefault(f"{repo}:{path}", {})[name] = hashlib.sha256(blob).hexdigest()
src = blob.decode("utf-8").split("\n")
if line is None:
hits = [i + 1 for i, l in enumerate(src) if text in l]
if len(hits) != 1:
sys.exit(f"anchor {cid}: {path} at {name} has {len(hits)} lines carrying {text!r}")
lines_at[name] = hits[0]
else:
if line > len(src) or text not in src[line - 1]:
sys.exit(f"anchor {cid}: {path}:{line} at {name} no longer carries {text!r}")
lines_at[name] = line
ref_lines = sorted(set(lines_at.values()))
out.append({"id": cid, "repo": repo, "path": path, "line": ref_lines[0] if len(ref_lines) == 1 else lines_at,
"anchor": text})
return out, files
# ---- The schedule, in exact integers ---------------------------------------------------------------------------
def step_rate(k):
r = LAUNCH_RATE
for _ in range(k):
r = (r * STEP_DECAY_Q32) >> 32
return r
def ramp(full, seconds):
if seconds >= RAMP_SECONDS:
return full
num = RAMP_START_PERCENT * RAMP_SECONDS + (100 - RAMP_START_PERCENT) * seconds
return full * num // (100 * RAMP_SECONDS)
def block_subsidy(daa, bps):
seconds = daa // bps
return ramp(step_rate(seconds // STEP_SECONDS) // bps, seconds)
def ramp_sum(bps, upto_seconds=RAMP_SECONDS):
full = LAUNCH_RATE // bps
return sum(ramp(full, s) for s in range(min(upto_seconds, RAMP_SECONDS))) * bps
def schedule(bps):
phases = []
rs = ramp_sum(bps)
phases.append({
"index": 0, "kind": "ramp", "rule": "launch-ramp",
"start_daa_score": 0, "end_daa_score": RAMP_SECONDS * bps,
"per_block_subsidy": None,
"ramp": {"full_per_block": str(LAUNCH_RATE // bps), "start_percent": RAMP_START_PERCENT,
"ramp_seconds": RAMP_SECONDS, "first_block": str(block_subsidy(0, bps)),
"last_block": str(block_subsidy(RAMP_SECONDS * bps - 1, bps)),
"formula": "per_block(d) = floor(full_per_block x (start_percent x ramp_seconds + (100 - start_percent) x s) / (100 x ramp_seconds)), s = floor(d / bps)"},
"phase_sum": str(rs),
"source": ["consensus/core/src/emission.rs:316", "consensus/core/src/emission.rs:329"],
})
k = 0
while True:
start = RAMP_SECONDS * bps if k == 0 else k * STEP_SECONDS * bps
end = (k + 1) * STEP_SECONDS * bps
per = block_subsidy(start, bps)
assert per == block_subsidy(end - 1, bps)
if per == 0:
phases.append({
"index": len(phases), "kind": "terminal", "rule": "terminal-zero",
"start_daa_score": start, "end_daa_score": None, "per_block_subsidy": "0",
"decay_step": k, "rate_per_second": str(step_rate(k)), "phase_sum": "0",
"source": ["consensus/core/src/emission.rs:109", "consensus/core/src/emission.rs:225", "consensus/core/src/emission.rs:331"],
})
break
phases.append({
"index": len(phases), "kind": "constant",
"rule": "full-rate-after-ramp" if k == 0 else f"halving-{k}",
"start_daa_score": start, "end_daa_score": end, "per_block_subsidy": str(per),
"decay_step": k, "rate_per_second": str(step_rate(k)), "phase_sum": str(per * (end - start)),
"source": ["consensus/core/src/emission.rs:107", "consensus/core/src/emission.rs:225", "consensus/core/src/emission.rs:331"],
})
k += 1
total = sum(int(p["phase_sum"]) for p in phases)
return phases, total
def supply_at(phases, daa, bps):
"""Scheduled issuance over DAA scores [0, daa): one subsidy per DAA score (the replay model)."""
s = 0
for p in phases:
a, b = p["start_daa_score"], p["end_daa_score"]
if daa <= a:
break
if p["kind"] == "ramp":
hi = min(daa, b)
full_secs, rem = divmod(hi, bps)
s += ramp_sum(bps, full_secs) + rem * block_subsidy(full_secs * bps, bps)
elif p["kind"] == "constant":
s += int(p["per_block_subsidy"]) * (min(daa, b) - a)
return s
def bps_step_overshoot(step_daa, from_bps, to_bps, total_to):
"""The code keys time as daa / bps with ONE bps per network (coinbase.rs:31, :265). If the block rate stepped on a
live chain at `step_daa` with no history rule, every block from the step on would be paid as if the clock read
daa / to_bps: issuance = sum_{d < step} sub_from(d) + sum_{d >= step} sub_to(d)."""
ph_from, _ = schedule(from_bps)
ph_to, _ = schedule(to_bps)
issued = supply_at(ph_from, step_daa, from_bps) + (total_to - supply_at(ph_to, step_daa, to_bps))
return issued
def coins(x):
q, r = divmod(x, UNIT)
return f"{q:,}.{r:018d}".rstrip("0").rstrip(".")
# ---- The conflicts and the texts ------------------------------------------------------------------------------
TAIL_SENTENCE = ("One rule sits beside the cap. If external proving revenue is under one fifth of the block subsidy over "
"any 90-day window after year 5, the question of a tail reward goes to the miners' signalling vote. "
"The protocol never changes emission by itself.")
def conflicts(fig):
m, d = fig["mainnet"], fig["devnet-4"]
return [
{"id": "C01", "topic": "cap versus the year-five tail vote",
"litepaper": {"line": 630, "rev": LITEPAPER_SUPERSEDED_REV, "words": TAIL_SENTENCE,
"now_reads": {"rev": "current", "line": 630, "words": "the answer is the security-funding plan under D01, never a tail emission or a vote that expands the cap."}},
"code": {"where": "consensus/core/src/emission.rs:109 (Tail::Cap in EmissionSchedule::CURRENT); consensus/core/src/config/params.rs:1858 (mainnet carries CURRENT); kaspad/src/daemon.rs (params_compiled refuses an override file on mainnet and testnet); no signal path reads or writes the emission schedule",
"value": "tail = cap; the schedule is a compiled genesis parameter; no vote, no revenue trigger, no tail"},
"spec_takes": "the cap as the invariant; the tail-vote sentence is SUPERSEDED",
"why": "D01 (capped issuance, subject to TV-04), ratified by the founder 9 October 2026 09:2x UK; VR-01 (no unexplained tail escape, no price- or revenue-triggered issuance); the code already enforces only the capped schedule. If TV-04 shows capped security cannot pass, the volume's rule applies: delay launch or openly choose another design, never an undisclosed tail.",
"status": "RESOLVED (by D01, subject to TV-04)",
"follow_up": "done by the site lane at ad64e48fa (9 October 2026, 09:16 UK): line 630 now reads 'The cap is fixed at 4 billion, with no tail emission and no vote that expands it (D01, subject to the pre-launch security-funding gate)' and the year-five clause routes to the security-funding plan; this landing edits no site/ file"},
{"id": "C02", "topic": "year-one emission and the twelve-year figure",
"litepaper": {"line": 578, "words": "emission starts at 1 billion a year", "line_2": 602, "words_2": "3,938M IGN in the first 12 years of a 4,000M cap."},
"code": {"where": "consensus/core/src/emission.rs:104 to 106 (launch rate floored, the 30-day ramp from 10%)",
"value": f"year 1 issues {coins(m['year_1'])} IGN at 10 blocks/s ({coins(d['year_1'])} at 1 block/s); the first 12 years issue {coins(m['year_12'])} IGN"},
"spec_takes": "the code's figures",
"why": "the litepaper chart and caption ignore the launch ramp's withheld 36,961,000 IGN and the floor of the per-second rate; the spec carries what nodes pay",
"status": "RESOLVED (text correction owed by the site lane)"},
{"id": "C03", "topic": "terminal behaviour",
"litepaper": {"line": 578, "words": "halves every two years for ever"},
"code": {"where": "consensus/core/src/emission.rs:225 (rate[k+1] = rate[k] x 2^31 >> 32, floored), :331 (per block = rate / bps, floored)",
"value": f"the per-block subsidy floors to zero at decay step {m['terminal_step']} (DAA score {m['terminal_daa']:,}, the start of year {m['terminal_year'] + 1}) at 10 blocks/s and at step {d['terminal_step']} (DAA score {d['terminal_daa']:,}, the start of year {d['terminal_year'] + 1}) at 1 block/s; zero for ever after"},
"spec_takes": "the code: a finite schedule ending in a terminal zero phase",
"why": "integer halving ends; the last subsidy is 1 base unit (10^-18 IGN) per block; after it the security budget is fees alone (TV-04's question)",
"status": "RESOLVED (text correction owed: 'until the subsidy floors to zero')"},
{"id": "C04", "topic": "block rate at launch and its steps",
"litepaper": {"line": 542, "words": "one block a second at launch with scheduled steps to four and ten", "line_2": 668, "words_2": "31.688 IGN a block, 86,400 blocks a day"},
"code": {"where": "consensus/core/src/config/params.rs:1789 (mainnet BlockrateParams::new::<10>()); consensus/src/processes/coinbase.rs:31 (the bps_history rescaling is gone) and :265 (subsidy keyed on daa / ONE bps)",
"value": f"mainnet as compiled runs 10 blocks/s from genesis, 3.1688087810 IGN a block; the subsidy has no block-rate history, so a step from 1 to 10 blocks/s at DAA score {fig['bps_step']['step_daa']:,} (one year in) would issue {coins(fig['bps_step']['issued'])} IGN in all, an overshoot of {coins(fig['bps_step']['issued'] - CAP)} IGN over the cap"},
"spec_takes": "both profiles (10 blocks/s as compiled; 1 block/s as Devnet 4), identical per second; NO live block-rate step is cap-safe under the current code",
"why": "a block-rate step re-keys the schedule clock (seconds = daa / bps) and re-pays elapsed time",
"status": "BLOCKED",
"question": "For the founder: does mainnet launch at the compiled 10 blocks a second (the litepaper's 1-to-4-to-10 step plan removed), or at 1 with steps (then the node lane adds a block-rate-history subsidy rule before genesis, and TV-02 replays a step)? Default if unanswered: the spec freezes the compiled 10 blocks a second and the step plan is treated as text to correct."},
{"id": "C05", "topic": "unit",
"litepaper": {"line": 568, "words": "the base unit is Open"},
"code": {"where": "consensus/core/src/unit.rs:38; consensus/core/src/config/params.rs:1816",
"value": "18 decimals on mainnet, testnet and Devnet 4: 1 IGN = 10^18 base units; one base unit is one EVM wei"},
"spec_takes": "18 decimals",
"why": "decided 6 October 2026 (O-2.6) and compiled; the cap is 4 x 10^27 base units",
"status": "RESOLVED (text correction owed)"},
{"id": "C06", "topic": "where the 20% proving share goes, and which ledger is native supply",
"litepaper": {"line": 609, "words": "the coinbase's 20% output goes to an unspendable script tagged igneum-proving-pool-v0 and is burned there. Provers are paid from a separate escrow in the execution state"},
"code": {"where": "consensus/src/processes/coinbase.rs:163 (UTXO pool output to the OP_RETURN script); consensus/src/pipeline/body_processor/body_validation_in_isolation.rs:54 (no UTXO transaction but the coinbase: UTXO outputs can never be spent); igneum/exec/src/executor.rs:181 to 211 (the EVM state credits 80% to the producer and 20% to the escrow 0x...0220 for every blue block)",
"value": "two ledgers record each subsidy: the UTXO coinbase (unspendable by rule) and the EVM state (spendable)"},
"spec_takes": "native supply is the EVM state's credit; the UTXO coinbase is a non-spendable shadow record and its OP_RETURN 20% is NOT a burn of native supply; the escrow balance is issued and locked until paid per proof",
"why": "counting both ledgers doubles supply; counting the OP_RETURN as burned and the escrow as issued would understate and overstate at once (VR-02)",
"status": "RESOLVED (for the spec; TV-02 reconciles both ledgers to this rule)"},
{"id": "C07", "topic": "red blocks",
"litepaper": {"line": 620, "words": "80% the block's miner, 20% the proving pool for the provers of that block"},
"code": {"where": "consensus/src/processes/coinbase.rs:152 to 159 (the UTXO coinbase pays a DAA red block's subsidy to the merging miner and the pool); igneum/exec/src/executor.rs:181 (the EVM credits blue blocks only)",
"value": "a red block's subsidy exists on the shadow ledger and not in native supply"},
"spec_takes": "native issuance = the blue blocks' subsidies; the per-DAA-score schedule sum is the upper bound",
"why": "the EVM ledger is native supply (C06); the bound keeps the cap invariant whatever the red share",
"status": "RESOLVED (TV-02 measures the red share; no text change needed beyond 'blue block')"},
{"id": "C08", "topic": "which DAA score a merged block is credited at",
"litepaper": {"line": None, "words": "(silent)"},
"code": {"where": "consensus/core/src/config/params.rs:1812 (mainnet subsidy_per_block_activation_daa = never), :2232 (Devnet 4 inherits never), :1960 (testnet-1: 0)",
"value": "on mainnet and Devnet 4 the EVM credits a merged block the merging chain block's subsidy while the UTXO coinbase pays the block's own: one ramp second high per merged block, a whole halving step at a boundary"},
"spec_takes": "each block's own DAA score (ledger N8) as the normative rule",
"why": "exact integer agreement between ledgers and replays (TV-02) needs one rule; N8 is the one the testnet object already carries",
"status": "BLOCKED",
"question": "For the founder (the node lane owns the change): set subsidy_per_block_activation_daa = 0 in the mainnet object, as testnet-1 has it? Default if unanswered: the spec keeps N8 as normative and TV-02 reports the chain-block reading as a measured divergence."},
{"id": "C09", "topic": "the mainnet genesis clock",
"litepaper": {"line": 775, "words": "Genesis with no premine, 30-day ramp."},
"code": {"where": "consensus/core/src/config/genesis.rs:92 (mainnet GENESIS daa_score 1312860, Kaspa's 2021 timestamp); the subsidy reads the absolute DAA score (emission.rs:329)",
"value": f"as compiled, the first mainnet block would sit 131,286 DAA seconds into the ramp (paying {fig['genesis_offset']['first_ramp_percent']} of full rate, not 10%) and the whole schedule would run 1.52 days early; issuance would be {coins(fig['genesis_offset']['issuance_lost'])} IGN lower"},
"spec_takes": "genesis at DAA score 0 (as Devnet 4 and testnet-1); the activation clocks below are counted from it",
"why": "the mainnet genesis is a Kaspa placeholder not yet cut; every Igneum network so far starts at DAA score 0",
"status": "BLOCKED",
"question": "For the founder: confirm the mainnet genesis is cut at DAA score 0. Default if unanswered: the spec freezes DAA score 0 and the genesis cut is checked against it."},
{"id": "C10", "topic": "priority fee to provers",
"litepaper": {"line": 622, "words": "80% the block's miner and provers; 20% the apps whose code ran, per call frame",
"line_2": 614, "words_2": "The priority fee splits two ways: 80% to the miner and provers of that block, 20% to the apps whose code ran"},
"code": {"where": "igneum/exec/src/executor.rs:321, :332 to 340 and :362 to 371 (the tip less the developer share is credited to the block's miner alone); igneum/exec/src/pgas.rs:290 (20% developer share, unregistered parts burned)",
"value": "two separate 80/20 rules ship: the SUBSIDY's 20% goes to the proving pool escrow and pays provers per accepted proof record (igneum.rs:44, :201 to 208; executor.rs:181 to 211); the TIP's 80% goes to the block's miner alone and 20% to developer registrations or burned. No tip reaches a prover. The design's route of 90% of the proving base fee to provers (docs/design/proving-payment.md; node branch proving-payment 930b6322, flag 67912c80, behind proving_payment_activation_daa) is not merged into any shipped or 2.0.3 line and has no activation"},
"spec_takes": "the code, which is the design's own closed decision",
"why": "docs/spec/05-fees-and-economics.md line 24 (section 5.2): 'No part of the tip reaches the provers: O-5.7 is closed at zero (8 October 2026, docs/design/proving-payment.md); the provers' user-funded payment is the proving base fee'. The code (executor.rs:362 to 371, revm's beneficiary credit) matches it. The litepaper line 622 and the spec's own summary rows (05 line 79, line 98) still say 'producer and provers' and are stale. Fees move existing supply and burn some; they never create supply.",
"status": "RESOLVED (by O-5.7, closed at zero 8 October 2026; the litepaper's 'miner and provers' at lines 614 and 622 is SUPERSEDED; text corrections owed there and at spec 05 lines 79 and 98)"},
{"id": "C11", "topic": "the reference pool's custody and fee",
"litepaper": {"line": 647, "words": "The reference pool never holds a member’s balance. Payouts come straight from the coinbase split"},
"code": {"where": "pool/src/payout.rs:1 to 6 (the pool's address receives the 80% and pays members by transfers); pool/src/config.rs:58 (fee_percent 1.0); pool/src/pplns.rs:76; consensus/core/src/config/params.rs:1851 (mainnet pool_split_activation_daa = never: the IGNW coinbase split is off)",
"value": "the reference pool holds members' balances between rounds and keeps a 1% fee; the consensus split that would make the sentence true is off on mainnet and Devnet 4"},
"spec_takes": "the code (custodial PPLNS, 1% default fee, no supply effect)",
"why": "the payout route moves issued supply; it creates none",
"status": "BLOCKED",
"question": "For the founder: activate the IGNW coinbase split at mainnet genesis (pool_split_activation_daa = 0) so the litepaper sentence holds, or change the sentence to the custodial PPLNS pool with its 1% fee? Default if unanswered: the sentence is corrected to the code."},
{"id": "C12", "topic": "a tail schedule compiled into testnet-1",
"litepaper": {"line": 630, "words": "The cap is fixed at 4 billion, with no tail emission and no vote that expands it (D01, subject to the pre-launch security-funding gate)."},
"code": {"where": "consensus/core/src/emission.rs:116 to 122 (TESTNET_1: 100 IGN a block, monthly glide, a 1% a year tail); consensus/core/src/config/params.rs:2014 (testnet-1 carries it)",
"value": "testnet-1's own approved genesis schedule (docs/plans/ledger-decisions.md line 110, ledger E22: 'No hard cap'): kept on every node line, active only when igneum-testnet-1's genesis is cut (none has been), never on Devnet 4 (params.rs:2372) or mainnet (params.rs:1858), which carry CURRENT with Tail::Cap"},
"spec_takes": "testnet-only and outside the monetary contract; the contract is EmissionSchedule::CURRENT with Tail::Cap on mainnet",
"why": "D01 binds the value-bearing contract; testnet-1 carries no value. Binding the cap to the testnet as well would change its approved genesis (E22) and move its digest: the founder's call, not this spec's",
"status": "RESOLVED (by D01: testnet-only, named)"},
{"id": "C13", "topic": "dead Kaspa subsidy fields in the mainnet object",
"litepaper": {"line": None, "words": "(silent)"},
"code": {"where": "consensus/core/src/config/params.rs:1783 to 1784 (deflationary_phase_daa_score 15,519,600; pre_deflationary_phase_base_subsidy 50,000,000,000)",
"value": "read only into the consensus digest; the subsidy never reads them (coinbase.rs:31)"},
"spec_takes": "ignored: they are not part of the monetary contract",
"why": "a reader grepping 'deflationary' would otherwise find a second schedule that does not exist",
"status": "RESOLVED (the node lane may annotate or remove them at the genesis cut)"},
]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--node-git", required=True)
ap.add_argument("--igneum-root", required=True)
ap.add_argument("--igneum-rev", default="HEAD")
ap.add_argument("--code-dump")
ap.add_argument("--out", required=True)
ap.add_argument("--md")
a = ap.parse_args()
igneum_rev = sh(["git", "rev-parse", a.igneum_rev], a.igneum_root).decode().strip()
cites, files = anchor(CITATIONS, a.node_git, a.igneum_root, igneum_rev)
profiles, fig = [], {}
for name, network, bps in [("mainnet", "mainnet (MAINNET_PARAMS as compiled)", MAINNET_BPS), ("devnet-4", "Devnet 4 (devnet4_params, 1 block a second, the litepaper's launch rate)", DEVNET4_BPS)]:
phases, total = schedule(bps)
term = phases[-1]
f = {
"year_1": supply_at(phases, YEAR_SECONDS * bps, bps),
"year_12": supply_at(phases, 12 * YEAR_SECONDS * bps, bps),
"terminal_step": term["decay_step"], "terminal_daa": term["start_daa_score"],
"terminal_year": term["start_daa_score"] // bps // YEAR_SECONDS,
"total": total,
}
fig[name] = f
years = {}
for y in [1, 2, 5, 10, 12, 20, 50, 100]:
years[str(y)] = str(supply_at(phases, y * YEAR_SECONDS * bps, bps))
last_nonzero = phases[-2]
profiles.append({
"profile": name, "network": network, "bps": bps, "decimals": DECIMALS,
"is_mainnet_contract": name == "mainnet",
"genesis_daa_score": 0,
"time_key": "seconds = floor(daa_score / bps); the per-block subsidy is constant inside a DAA second",
"phases": phases,
"phase_count": len(phases),
"last_nonzero_phase": {"index": last_nonzero["index"], "per_block_subsidy": last_nonzero["per_block_subsidy"],
"start_daa_score": last_nonzero["start_daa_score"], "end_daa_score": last_nonzero["end_daa_score"]},
"transition_count": len(phases) - 1,
"last_paying_daa_score": term["start_daa_score"] - 1,
"last_paying_block_height": term["start_daa_score"],
"terminal_supply": str(total),
"terminal": {"start_daa_score": term["start_daa_score"], "first_unpaid_block_height": term["start_daa_score"] + 1, "decay_step": term["decay_step"],
"start_year": f["terminal_year"], "behaviour": "per-block subsidy 0 at every later DAA score; the cap is never reached; blocks are paid by fees alone"},
"total_issuance": str(total),
"cap_shortfall": str(CAP - total),
"scheduled_supply_at_year_end": years,
})
# the block-rate step demonstration (C04) and the compiled genesis offset (C09)
step_daa = YEAR_SECONDS * DEVNET4_BPS
fig["bps_step"] = {"step_daa": step_daa, "issued": bps_step_overshoot(step_daa, 1, 10, fig["mainnet"]["total"])}
ph10, _ = schedule(MAINNET_BPS)
off = MAINNET_GENESIS_DAA_COMPILED
s0 = off // MAINNET_BPS
num = RAMP_START_PERCENT * RAMP_SECONDS + (100 - RAMP_START_PERCENT) * s0
fig["genesis_offset"] = {"issuance_lost": supply_at(ph10, off, MAINNET_BPS),
"first_ramp_percent": f"{num * 100 // (100 * RAMP_SECONDS)}.{(num * 10000 // (100 * RAMP_SECONDS)) % 100:02d}%"}
dump_check = None
if a.code_dump:
raw = open(a.code_dump, "rb").read()
rows = [json.loads(l) for l in raw.decode().splitlines() if l.strip()]
n = 0
for prof in profiles:
net = prof["profile"]
head = next(r for r in rows if r.get("network") == net and "bps" in r)
assert head["bps"] == prof["bps"] and head["decimals"] == DECIMALS and int(head["launch_rate"]) == LAUNCH_RATE
assert head["ramp_seconds"] == RAMP_SECONDS and head["step_seconds"] == STEP_SECONDS and head["step_decay_q32"] == STEP_DECAY_Q32 and head["tail"] == "cap"
rp = next(r for r in rows if r.get("network") == net and r.get("phase") == "ramp")
p0 = prof["phases"][0]
assert int(rp["sum"]) == int(p0["phase_sum"]) and rp["first_block"] == p0["ramp"]["first_block"] and rp["last_block"] == p0["ramp"]["last_block"] and rp["end_daa"] == p0["end_daa_score"]
steps = [r for r in rows if r.get("network") == net and r.get("phase") == "step"]
for r, p in zip(steps, prof["phases"][1:]):
assert r["start_daa"] == p["start_daa_score"], (net, r, p)
assert r["per_block"] == p["per_block_subsidy"], (net, r, p)
assert int(r["rate_per_second"]) == int(p["rate_per_second"]), (net, r, p)
if p["kind"] == "constant":
assert r["end_daa"] == p["end_daa_score"] and r["sum"] == p["phase_sum"], (net, r, p)
n += 1
assert len(steps) == len(prof["phases"]) - 1, (net, len(steps), len(prof["phases"]))
tot = next(r for r in rows if r.get("network") == net and "total" in r)
assert int(tot["total"]) == int(prof["total_issuance"]) and int(tot["cap"]) == CAP and int(tot["shortfall"]) == int(prof["cap_shortfall"])
assert head["genesis_daa_score"] == (MAINNET_GENESIS_DAA_COMPILED if net == "mainnet" else 0)
t1 = next(r for r in rows if r.get("network") == "testnet-1" and "bps" in r)
assert t1["tail"] == "percent:100"
dump_check = {"path": "tools/token-value/tv-01/code-dump/schedule-bf60948a.jsonl", "sha256": hashlib.sha256(raw).hexdigest(),
"program": "tools/token-value/tv-01/code-dump/tv01_schedule.rs.txt",
"run": "build-9, release-2.0.3-node-k6 bf60948a: cargo run --release -p kaspa-consensus-core --example tv01_schedule (the program copied into consensus/core/examples of a scratch tree; the node fork is unchanged)",
"agreement": f"every field of {n} decay phases, both ramps and both totals equal to this generator's integers"}
spec = {
"schema": {
"name": "igneum-supply-spec", "version": "1.0.0",
"integers": "every amount is a decimal string of base units (amounts reach 4 x 10^27, past JSON's exact range); DAA scores are JSON integers (all under 2^53)",
"phase_fields": {"index": "0-based, contiguous", "kind": "ramp | constant | terminal", "rule": "the rule's name", "start_daa_score": "inclusive", "end_daa_score": "exclusive; null for the terminal phase", "per_block_subsidy": "base units per block at every DAA score of the phase (null for the ramp, whose per-block value is its formula)", "phase_sum": "base units the phase issues at one subsidy per DAA score", "source": "file:line in the node fork at both named revisions"},
"checker": "tools/token-value/tv-01/check-spec.py",
},
"gate": "TV-01", "verdict": "NOT RUN",
"status": "Phase 0 document. Activates nothing; changes no node, no chain and no served page. The verdict stays NOT RUN until the independent panel reads it.",
"decision": {"id": "D01", "title": "Cap and tail", "ruling": "capped issuance, subject to TV-04 (the founder's ratification, 9 October 2026, 09:2x UK)",
"effect": "the cap is the invariant; the litepaper's year-five tail-vote sentence is superseded (C01)"},
"invariant": {"rule": "sum over all blocks of the per-block subsidy <= cap, with no tail, no vote, no price or revenue trigger and no emergency mint",
"how_enforced": "by the schedule, not by a running counter: every block's coinbase must carry exactly the scheduled subsidy (coinbase.rs:262) and the EVM state credits by the same rule (executor.rs:181 to 211); the cap constant itself is only compared in tests (igneum.rs:154). The schedule sums to the cap minus the shortfall below, so the cap is never reached.",
"change_policy": "the schedule is a compiled genesis parameter on mainnet; the daemon refuses an override file there (kaspad/src/daemon.rs, network.rs:266); a change is a new release with a new consensus digest and an explicit incompatible-policy process (VR-06)"},
"units": {"coin": "IGN", "base_unit": "wei (one consensus base unit is one EVM wei)", "decimals": DECIMALS, "base_units_per_coin": str(UNIT),
"source": ["consensus/core/src/unit.rs:38", "consensus/core/src/config/params.rs:1816"]},
"cap": {"base_units": str(CAP), "coins": "4000000000", "kind": "invariant",
"source": ["consensus/core/src/igneum.rs:37", "consensus/core/src/igneum.rs:154"]},
"rules": {
"launch_rate_per_second": str(LAUNCH_RATE), "launch_rate_note": "floor(10^9 x 10^8 / 31,557,600) = 3,168,808,781 at 8 decimals, times 10^10: 31.68808781 IGN a DAA second",
"year_seconds": YEAR_SECONDS, "ramp_seconds": RAMP_SECONDS, "ramp_start_percent": RAMP_START_PERCENT,
"step_seconds": STEP_SECONDS, "step_decay_q32": STEP_DECAY_Q32, "decay_rule": "rate[k+1] = floor(rate[k] x step_decay_q32 / 2^32) (a halving every two years)",
"per_block": "per_block(d) = ramp(floor(rate[floor(s / step_seconds)] / bps), s), s = floor(d / bps)",
"tail": "cap",
"genesis_rule": {"rule": "genesis-unrewarded",
"text": "the genesis block is never rewarded: it never enters the DAA window (window.rs:177), the coinbase pays only the mergeset blues inside the DAA window (coinbase.rs:139), and the genesis coinbase has no outputs (genesis.rs:26). A block's DAA score counts its DAA-window mergeset, so on a chain the block at height h >= 1 carries DAA score h - 1: DAA score 0 is the first paid block and every schedule below is indexed by DAA score, not height",
"height_of_daa_score": "height = DAA score + 1 (a chain of blocks; in the DAG several blocks may share a DAA score)",
"source": ["consensus/src/processes/window.rs:177", "consensus/src/processes/coinbase.rs:139", "consensus/core/src/config/genesis.rs:26"]},
"source": ["consensus/core/src/emission.rs:103 to 110", "consensus/core/src/emission.rs:225", "consensus/core/src/emission.rs:316 to 333", "consensus/core/src/config/params.rs:1858"],
},
"activation_clocks": {
"genesis": "DAA score 0 (C09: the compiled mainnet genesis still carries Kaspa's 1,312,860; BLOCKED)",
"ramp_end": {"mainnet": RAMP_SECONDS * MAINNET_BPS, "devnet-4": RAMP_SECONDS * DEVNET4_BPS, "seconds": RAMP_SECONDS},
"halvings": "every 63,115,200 DAA seconds (2 years of 365.25 days): DAA score k x 631,152,000 at 10 blocks/s, k x 63,115,200 at 1 block/s",
"terminal": {"mainnet": fig["mainnet"]["terminal_daa"], "devnet-4": fig["devnet-4"]["terminal_daa"]},
"signing_bonus": "mainnet never (params.rs:1809); testnet-1 from 0",
"pool_split": "mainnet never (params.rs:1851)",
"per_block_rule_N8": "mainnet never (params.rs:1812; C08 BLOCKED); testnet-1 from 0",
},
"contract_profile": "mainnet",
"contract_profile_note": "mainnet's schedule (MAINNET_PARAMS, params.rs:1858, 10 blocks a second) is the monetary contract under the 4,000,000,000 cap; the Devnet 4 profile (params.rs:2372, 1 block a second, 18 decimals) is the same per-second curve, carried because it is the chain TV-02 replays and the litepaper's launch rate (C04)",
"schedules": profiles,
"coinbase_split": [
{"rule": "producer and proving pool", "producer": "subsidy - floor(subsidy x 20 / 100)", "proving_pool": "floor(subsidy x 20 / 100)", "source": ["consensus/core/src/igneum.rs:44", "consensus/core/src/igneum.rs:201 to 208"]},
{"rule": "signing bonus", "effect": "a silent producer's share loses floor(share x 1,000 / 10,000) to the proving pool; nothing destroyed; fees untouched", "mainnet": "off (never)", "source": ["consensus/core/src/igneum.rs:110", "consensus/core/src/config/params.rs:1809 to 1810"]},
{"rule": "native ledger", "effect": "the EVM state credits the producer share to the block's IGNA address (or by its IGNW split once pool_split is active) and the pool share to escrow 0x0000000000000000000000000000000000000220, blue blocks only", "source": ["igneum/exec/src/executor.rs:181 to 211", "igneum/exec/src/config.rs:59"]},
{"rule": "shadow ledger", "effect": "the UTXO coinbase pays the producer outputs and one OP_RETURN pool output tagged igneum-proving-pool-v0; no UTXO transaction but the coinbase is valid, so none of it is ever spent", "source": ["consensus/src/processes/coinbase.rs:126 to 168", "consensus/src/pipeline/body_processor/body_validation_in_isolation.rs:54"]},
{"rule": "pool", "effect": "none in consensus; the reference pool keeps fee_percent (default 1.0) of the 80% it receives and pays members PPLNS from its address", "source": ["pool/src/payout.rs:1 to 6", "pool/src/config.rs:58", "pool/src/pplns.rs:76"]},
{"rule": "dev fee", "effect": "none in consensus; the miner software requests 1 template in 100 with the dev address (off with --dev-fee 0); it moves the producer share of those blocks, creates nothing", "source": ["site/litepaper.html:625"]},
{"rule": "treasury, foundation, team, stake", "effect": "none: no output, credit or address in consensus", "source": ["consensus/src/processes/coinbase.rs:126 to 168", "igneum/exec/src/executor.rs:181 to 211"]},
{"rule": "genesis", "effect": "the genesis coinbase has no outputs; its payload subsidy field is data; no premine", "source": ["consensus/core/src/config/genesis.rs:26"]},
],
"fees": [
{"route": "execution base fee", "effect": "burned (not credited)", "source": "igneum/exec/src/executor.rs:319 to 321"},
{"route": "proving charge (proving base fee)", "effect": "burned in full today; the 90% route to provers is designed (docs/design/proving-payment.md, node branch proving-payment 930b6322), not merged, no activation scheduled", "source": "igneum/exec/src/executor.rs:319 to 321, :361"},
{"route": "priority fee", "effect": "80% to the block's miner; 20% to the payees registered for the frames' code by own gas, unregistered parts burned", "source": "igneum/exec/src/executor.rs:362 to 371; igneum/exec/src/pgas.rs:290"},
{"route": "external job after the proof bridge", "effect": "not in code (Designed, phase two): 90% provers, 10% burned per the litepaper", "source": "site/litepaper.html:624"},
],
"supply_accounting": {
"issued": "the EVM state's subsidy credits (producer plus escrow), blue blocks only; bounded above by the schedule's per-DAA-score sum",
"burned": "execution base fees, proving charges and unregistered developer shares (EVM side); the UTXO OP_RETURN output is NOT counted as a burn (C06)",
"locked": "the proving escrow's balance: credited, not yet paid against a valid proof; unclaimed credit is stranded today (litepaper line 609, no rule returns it)",
"not_supply": "UTXO coinbase outputs (unspendable shadow record); any bridged or wrapped IGN on another chain (VR-02)",
"replay_model": "one scheduled subsidy per DAA score (the schedules above); a block is paid at its own DAA score, parallel blocks may share a score and a score may have no block, so the measured issuance differs from the model only near phase boundaries and only by the blocks merged across them; TV-02 reconciles the measured ledgers to this model in exact integers",
},
"superseded": [
{"text": TAIL_SENTENCE, "where": "site/litepaper.html:630 at " + LITEPAPER_SUPERSEDED_REV, "by": "D01 (capped issuance, subject to TV-04)", "status": "SUPERSEDED", "text_now": "rewritten by the site lane at ad64e48fa (C01)"},
{"text": "The priority fee splits two ways: 80% to the miner and provers of that block, 20% to the apps whose code ran", "where": "site/litepaper.html:614 (and the route table at :622)", "by": "O-5.7 closed at zero, 8 October 2026 (docs/spec/05-fees-and-economics.md line 24); the code pays the tip's 80% to the miner alone", "status": "SUPERSEDED"},
],
"conflicts": conflicts(fig),
"sources": {
"volume": {"path": "docs/plans/igneum-2.0-master/token-value/rules-and-gates.json", "sha256": hashlib.sha256(read_igneum(a.igneum_root, igneum_rev, "docs/plans/igneum-2.0-master/token-value/rules-and-gates.json")).hexdigest()},
"litepaper": {"path": "site/litepaper.html", "igneum_commit": igneum_rev, "sha256": files["igneum:site/litepaper.html"]["igneum master"],
"superseded_text_commit": LITEPAPER_SUPERSEDED_REV, "superseded_text_sha256": files["igneum-old:site/litepaper.html"]["igneum before ad64e48fa"]},
"node": {"repo": "igneum-node (build-1:/srv/igneum-node.git)", "revisions": NODE_REVS,
"files": {k.split(":", 1)[1]: v for k, v in sorted(files.items()) if k.startswith("node:")},
"note": "every cited line carries its anchor at all four revisions (the generator refuses otherwise); the shipped 2.0.2 commit 5d53a591 and the 2.0.3 lines k6 and k7 change no issuance rule"},
"pool": {k.split(":", 1)[1]: v["igneum master"] for k, v in sorted(files.items()) if k.startswith("igneum:pool/")},
"design": {k.split(":", 1)[1]: v["igneum master"] for k, v in sorted(files.items()) if k.startswith("igneum:docs/")},
"citations": cites,
"code_dump": dump_check,
},
"generator": {"path": "tools/token-value/tv-01/gen-spec.py",
"command": "python3 tools/token-value/tv-01/gen-spec.py --node-git <igneum-node clone holding both revisions> --igneum-root . --igneum-rev " + igneum_rev + " --code-dump tools/token-value/tv-01/code-dump/schedule-bf60948a.jsonl --out docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.json --md docs/plans/igneum-2.0-master/token-value/phase0/tv-01/supply-spec.md"},
}
# every litepaper quote in the conflicts table is exact (tags stripped, entities decoded) or nothing is written
import html as _html, re as _re
lps = {}
def lp_line(rev, ln):
if rev not in lps:
lps[rev] = read_igneum(a.igneum_root, rev, "site/litepaper.html").decode("utf-8").split("\n")
return _html.unescape(_re.sub("<[^>]+>", "", lps[rev][ln - 1]))
for c in spec["conflicts"]:
rev = c["litepaper"].get("rev", igneum_rev)
for lk, wk in (("line", "words"), ("line_2", "words_2")):
ln = c["litepaper"].get(lk)
if ln and c["litepaper"][wk] not in lp_line(rev, ln):
sys.exit(f"{c['id']}: the litepaper's line {ln} at {rev[:9]} does not carry the quoted words exactly")
now = c["litepaper"].get("now_reads")
if now and now["words"] not in lp_line(igneum_rev, now["line"]):
sys.exit(f"{c['id']}: the current litepaper line {now['line']} does not carry the quoted words")
if now:
now["rev"] = igneum_rev
c["litepaper"].setdefault("rev", igneum_rev)
with open(a.out, "w", encoding="utf-8") as fh:
json.dump(spec, fh, indent=1, ensure_ascii=False)
fh.write("\n")
if a.md:
m, d = profiles[0], profiles[1]
L = ["<!-- figures:begin (generated by tools/token-value/tv-01/gen-spec.py; do not edit by hand) -->", ""]
L.append("| Figure | Mainnet as compiled (10 blocks/s) | Devnet 4 (1 block/s) |")
L.append("|---|---|---|")
L.append(f"| Cap | {coins(CAP)} IGN ({CAP} base units) | the same |")
L.append(f"| Total scheduled issuance | {coins(int(m['total_issuance']))} IGN | {coins(int(d['total_issuance']))} IGN |")
L.append(f"| Shortfall under the cap (exact) | {m['cap_shortfall']} base units ({coins(int(m['cap_shortfall']))} IGN) | {d['cap_shortfall']} base units ({coins(int(d['cap_shortfall']))} IGN) |")
L.append(f"| Ramp phase | DAA 0 to {m['phases'][0]['end_daa_score']:,}, {coins(int(m['phases'][0]['phase_sum']))} IGN | DAA 0 to {d['phases'][0]['end_daa_score']:,}, {coins(int(d['phases'][0]['phase_sum']))} IGN |")
L.append(f"| First block | {coins(int(m['phases'][0]['ramp']['first_block']))} IGN | {coins(int(d['phases'][0]['ramp']['first_block']))} IGN |")
L.append(f"| Full-rate block (after the ramp) | {coins(int(m['phases'][1]['per_block_subsidy']))} IGN | {coins(int(d['phases'][1]['per_block_subsidy']))} IGN |")
L.append(f"| Phases (ramp, constant, terminal) | {m['phase_count']} | {d['phase_count']} |")
L.append(f"| Last non-zero subsidy | {m['last_nonzero_phase']['per_block_subsidy']} base unit a block, DAA {m['last_nonzero_phase']['start_daa_score']:,} to {m['last_nonzero_phase']['end_daa_score']:,} | {d['last_nonzero_phase']['per_block_subsidy']} base unit a block, DAA {d['last_nonzero_phase']['start_daa_score']:,} to {d['last_nonzero_phase']['end_daa_score']:,} |")
L.append(f"| Terminal (zero for ever) from | DAA {m['terminal']['start_daa_score']:,} (decay step {m['terminal']['decay_step']}, year {m['terminal']['start_year'] + 1}) | DAA {d['terminal']['start_daa_score']:,} (decay step {d['terminal']['decay_step']}, year {d['terminal']['start_year'] + 1}) |")
for y in ["1", "2", "5", "10", "12", "20", "50", "100"]:
L.append(f"| Scheduled supply at the end of year {y} | {coins(int(m['scheduled_supply_at_year_end'][y]))} IGN | {coins(int(d['scheduled_supply_at_year_end'][y]))} IGN |")
L.append(f"| A 1-to-10 blocks/s step at DAA {fig['bps_step']['step_daa']:,} under the current code | total {coins(fig['bps_step']['issued'])} IGN, {coins(fig['bps_step']['issued'] - CAP)} IGN over the cap | |")
L.append(f"| The compiled mainnet genesis (DAA 1,312,860) | first ramp block at {fig['genesis_offset']['first_ramp_percent']}; {coins(fig['genesis_offset']['issuance_lost'])} IGN never scheduled | |")
L.append("")
if dump_check:
L.append(f"Code agreement: {dump_check['agreement']} (dump sha256 `{dump_check['sha256']}`).")
L.append("")
L.append("<!-- figures:end -->")
txt = open(a.md).read()
b, e = txt.index("<!-- figures:begin"), txt.index("<!-- figures:end -->") + len("<!-- figures:end -->")
with open(a.md, "w") as fh:
fh.write(txt[:b] + "\n".join(L) + txt[e:])
print(f"wrote {a.out}: mainnet total {fig['mainnet']['total']}, shortfall {CAP - fig['mainnet']['total']}; devnet-4 total {fig['devnet-4']['total']}; code dump {'agrees' if dump_check else 'not given'}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# TV-02: every comparison of A's outputs with B's, on A's box (build-5) with B's outputs copied from build-6 into
# <out>/from-build-6. Usage: compare-all.sh <tv-02 dir> <out dir> <B rules tag> ; writes <out>/compare-<tag>.log
set -u
D="$1"; O="$2"; TAG="$3"
PID="$O/compare.pid"; echo "$$ $(date -u +%FT%TZ) tv02-compare $TAG" > "$PID"; trap 'rm -f "$PID"' EXIT
B="$O/from-build-6"; LOG="$O/compare-$TAG.log"; : > "$LOG"
python3 "$D/compare.py" selftest >> "$LOG" 2>&1
for N in devnet4 mainnet devnet; do
echo "== straight $N" >> "$LOG"
python3 "$D/compare.py" straight "$O/a-$N.csv" "$B/b-$TAG-$N.csv" >> "$LOG" 2>&1; echo "exit $?" >> "$LOG"
done
for F in reorg-ramp-end-k1 reorg-first-step-k2; do
echo "== reorg $F" >> "$LOG"
python3 "$D/compare.py" reorg "$O/a-reorg-$F.csv" "$B/b-$TAG-reorg-$F.csv" "$D/fixtures/$F.json" "$O/a-devnet4.csv" >> "$LOG" 2>&1; echo "exit $?" >> "$LOG"
done
cat "$LOG"

View file

@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""TV-02 comparison harness: reads implementation A's and B's outputs and reports exact equality or the first
differing row. Standalone Python 3, no third-party modules, shares no code with A or B.
compare.py straight <a.csv> <b.csv> CSV: height,daa_score,subsidy,cumulative
compare.py reorg <a.csv> <b.csv> <fixture.json> <straight.csv>
CSV: step,height,daa_score,subsidy,cumulative
compare.py selftest the known-failed cases first, then a known-equal case
Exit 0 when everything compared is equal, 1 on any difference, 2 on bad input. The verdict line starts with
EQUAL or DIFFER (never PASS: the independent panel reads the evidence).
"""
import csv
import io
import json
import sys
STRAIGHT_HEADER = ["height", "daa_score", "subsidy", "cumulative"]
REORG_HEADER = ["step", "height", "daa_score", "subsidy", "cumulative"]
def read_rows(text, header):
r = list(csv.reader(io.StringIO(text)))
if not r or r[0] != header:
raise ValueError("header %r is not %r" % (r[0] if r else None, header))
out = []
for i, row in enumerate(r[1:], start=2):
if len(row) != len(header):
raise ValueError("line %d has %d fields" % (i, len(row)))
out.append(row)
return out
def ints(row, first_int):
return row[:first_int] + [int(x) for x in row[first_int:]]
def compare_straight_text(a_text, b_text):
a = [ints(r, 0) for r in read_rows(a_text, STRAIGHT_HEADER)]
b = [ints(r, 0) for r in read_rows(b_text, STRAIGHT_HEADER)]
res = {"rows_a": len(a), "rows_b": len(b)}
for i in range(max(len(a), len(b))):
ra = a[i] if i < len(a) else None
rb = b[i] if i < len(b) else None
if ra != rb:
cols = [] if ra is None or rb is None else [STRAIGHT_HEADER[j] for j in range(4) if ra[j] != rb[j]]
res.update(equal=False, first_difference={"row": i + 1, "a": ra, "b": rb, "columns": cols})
# diagnostic: is B - A a constant offset in the cumulative over the rows both share by height?
ah = {r[0]: r for r in a}
bh = {r[0]: r for r in b}
common = sorted(set(ah) & set(bh))
offs = sorted({bh[h][3] - ah[h][3] for h in common})
res["heights_only_in_a"] = len(set(ah) - set(bh))
res["heights_only_in_b"] = len(set(bh) - set(ah))
res["cumulative_offsets_b_minus_a_on_shared_heights"] = [str(x) for x in offs[:5]] + (["..."] if len(offs) > 5 else [])
return res
res.update(equal=True, last_row=a[-1] if a else None)
return res
def compare_reorg_text(a_text, b_text, fixture, straight_text):
a = [ints(r, 1) for r in read_rows(a_text, REORG_HEADER)]
b = [ints(r, 1) for r in read_rows(b_text, REORG_HEADER)]
res = {"fixture": fixture.get("id"), "checks": []}
def check(name, ok, detail):
res["checks"].append({"check": name, "ok": bool(ok), "detail": detail})
first = None
for i in range(max(len(a), len(b))):
ra = a[i] if i < len(a) else None
rb = b[i] if i < len(b) else None
if ra != rb:
first = {"row": i + 1, "a": ra, "b": rb}
break
check("A equals B, every ledger row", first is None, first or {"rows": len(a)})
for name, rows in (("A", a), ("B", b)):
by = {}
for r in rows:
by.setdefault(r[0], []).append(r)
e = fixture["expected"]
anc = by["ancestor"][0]
check(name + " ancestor = fixture", str(anc[4]) == e["ancestor"]["cumulative"] and str(anc[3]) == e["ancestor"]["subsidy"]
and anc[1] == e["ancestor"]["height"], {"got": anc, "want": e["ancestor"]})
otip = by["orphan"][-1]
check(name + " orphan tip = fixture", str(otip[4]) == e["orphan_tip"]["cumulative"] and str(otip[3]) == e["orphan_tip"]["subsidy"]
and otip[1] == e["orphan_tip"]["height"], {"got": otip, "want": e["orphan_tip"]})
rb_ = by["rollback"][-1]
check(name + " after rollback = fixture", str(rb_[4]) == e["after_rollback"]["cumulative"], {"got": rb_, "want": e["after_rollback"]})
fin = by["final"][0]
check(name + " final = fixture", str(fin[4]) == e["final"]["cumulative"] and str(fin[3]) == e["final"]["subsidy"]
and fin[1] == e["final"]["height"], {"got": fin, "want": e["final"]})
nv = by["known_failed_no_undo"][0]
check(name + " known-failed ledger = fixture's known-failed value", str(nv[4]) == e["known_failed_no_undo"]["cumulative"],
{"got": nv, "want": e["known_failed_no_undo"]})
check(name + " known-failed ledger is refused (differs from final)", nv[4] != fin[4], {"naive": nv[4], "final": fin[4]})
if straight_text is not None:
s = {r[0]: r for r in (ints(x, 0) for x in read_rows(straight_text, STRAIGHT_HEADER))}
fin = [r for r in a if r[0] == "final"][0]
srow = s.get(fin[1])
check("final = straight chain at the same height", srow is not None and srow[3] == fin[4] and srow[2] == fin[3],
{"final": fin, "straight": srow})
res["equal"] = all(c["ok"] for c in res["checks"])
return res
def verdict(res):
return ("EQUAL" if res["equal"] else "DIFFER") + " " + json.dumps(res)
def selftest():
"""Known-failed first: a corrupted cumulative, a dropped row and a no-undo ledger must each be reported."""
good = "height,daa_score,subsidy,cumulative\n0,0,0,0\n1,0,10,10\n2,1,11,21\n"
bad_cum = "height,daa_score,subsidy,cumulative\n0,0,0,0\n1,0,10,10\n2,1,11,22\n"
short = "height,daa_score,subsidy,cumulative\n0,0,0,0\n1,0,10,10\n"
r = compare_straight_text(good, bad_cum)
assert not r["equal"] and r["first_difference"]["row"] == 3 and r["first_difference"]["columns"] == ["cumulative"], r
r = compare_straight_text(good, short)
assert not r["equal"] and r["first_difference"]["b"] is None, r
fx = {"id": "t", "expected": {"ancestor": {"height": 1, "subsidy": "10", "cumulative": "10"},
"orphan_tip": {"height": 2, "subsidy": "11", "cumulative": "21"},
"after_rollback": {"cumulative": "10"},
"final": {"height": 3, "subsidy": "12", "cumulative": "33"},
"known_failed_no_undo": {"cumulative": "44"}}}
led = ("step,height,daa_score,subsidy,cumulative\nancestor,1,0,10,10\norphan,2,1,11,21\nrollback,2,1,11,10\n"
"replacement,2,1,11,21\nreplacement,3,2,12,33\nfinal,3,2,12,33\nknown_failed_no_undo,3,2,12,44\n")
straight3 = good + "3,2,12,33\n"
naive_led = led.replace("rollback,2,1,11,10\nreplacement,2,1,11,21\nreplacement,3,2,12,33\nfinal,3,2,12,33",
"rollback,2,1,11,21\nreplacement,2,1,11,32\nreplacement,3,2,12,44\nfinal,3,2,12,44")
r = compare_reorg_text(led, naive_led, fx, straight3)
assert not r["equal"], r
r = compare_reorg_text(naive_led, naive_led, fx, straight3)
assert not r["equal"] and any(not c["ok"] and "final = fixture" in c["check"] for c in r["checks"]), r
# then the known-equal case
assert compare_straight_text(good, good)["equal"]
r = compare_reorg_text(led, led, fx, straight3)
assert r["equal"], r
print("selftest: corrupted cumulative, dropped row and no-undo ledger each reported; equal inputs read EQUAL")
def main(a):
try:
if len(a) == 4 and a[1] == "straight":
res = compare_straight_text(open(a[2]).read(), open(a[3]).read())
res.update(a=a[2], b=a[3])
elif len(a) in (5, 6) and a[1] == "reorg":
fx = json.load(open(a[4]))
st = open(a[5]).read() if len(a) == 6 else None
res = compare_reorg_text(open(a[2]).read(), open(a[3]).read(), fx, st)
res.update(a=a[2], b=a[3])
elif len(a) == 2 and a[1] == "selftest":
selftest()
return 0
else:
print(__doc__, file=sys.stderr)
return 2
except (ValueError, KeyError, IndexError) as e:
print("BAD INPUT %s" % e)
return 2
print(verdict(res))
return 0 if res["equal"] else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))

View file

@ -0,0 +1,47 @@
{
"network": "devnet4",
"written": "2026-10-09T08:40Z, by hand from the rules (rules-v0.1: the CURRENT schedule at 18 decimals, 1 block a second, genesis issues nothing, block h has DAA score h - 1), before either implementation ran a replay; the closed forms below were written by hand and their digits evaluated with bc on build-5 (a calculator on the written expressions, no replay code)",
"symbols": {
"F": "31688087810000000000 (the launch rate per DAA second, 3,168,808,781 x 10^10; at 1 block a second the full-rate block)",
"H": "15844043905000000000 (F halved once at 18 decimals, floor(F / 2); F is even at 18 decimals, so no floor is lost here, unlike the 8-decimal 1,584,404,390)",
"R": "2592000 (ramp seconds)",
"X": "63115200 (the first step, two years of DAA seconds)",
"ramp_term": "schedule(d) = floor(F (10R + 90d) / (100R)) = F/10 + floor(d G / 9) for d < R, G = F / 320000 = 99025274406250 = 9q + 1, q = 11002808267361, so schedule(d) = F/10 + d q + floor(d / 9)",
"ramp_sum": "sum over d = 0..R-1 = R F / 10 + q R (R - 1) / 2 + 9 (0 + 1 + ... + 287999) = 45174523722296485498848000 (R = 9 x 288000)"
},
"id": "reorg-first-step-k2",
"description": "the first halving at DAA X: the ancestor is height X-1 (DAA X-2); an orphan branch of 2 blocks (heights X, X+1: DAA X-1 full rate, DAA X the first halved block) is replaced by 3 (heights X, X+1, X+2)",
"ancestor_height": 63115199,
"orphan_len": 2,
"replacement_len": 3,
"expected": {
"ancestor": {
"height": 63115199,
"daa_score": 63115198,
"subsidy": "31688087810000000000",
"cumulative": "1963038968176400675498848000",
"derivation": "ramp_sum + (X - 1 - R) F: DAA 0..X-2"
},
"orphan_tip": {
"height": 63115201,
"subsidy": "15844043905000000000",
"cumulative": "1963039015708532390498848000",
"derivation": "ancestor + F + H"
},
"after_rollback": {
"height": 63115199,
"cumulative": "1963038968176400675498848000"
},
"final": {
"height": 63115202,
"daa_score": 63115201,
"subsidy": "15844043905000000000",
"cumulative": "1963039031552576295498848000",
"derivation": "ancestor + F + 2H; equals the straight chain's cumulative at height X+2"
},
"known_failed_no_undo": {
"cumulative": "1963039079084708010498848000",
"derivation": "ancestor + (F + H) + (F + 2H): the orphan's F + H counted twice; must be refused"
}
}
}

View file

@ -0,0 +1,47 @@
{
"network": "devnet4",
"written": "2026-10-09T08:40Z, by hand from the rules (rules-v0.1: the CURRENT schedule at 18 decimals, 1 block a second, genesis issues nothing, block h has DAA score h - 1), before either implementation ran a replay; the closed forms below were written by hand and their digits evaluated with bc on build-5 (a calculator on the written expressions, no replay code)",
"symbols": {
"F": "31688087810000000000 (the launch rate per DAA second, 3,168,808,781 x 10^10; at 1 block a second the full-rate block)",
"H": "15844043905000000000 (F halved once at 18 decimals, floor(F / 2); F is even at 18 decimals, so no floor is lost here, unlike the 8-decimal 1,584,404,390)",
"R": "2592000 (ramp seconds)",
"X": "63115200 (the first step, two years of DAA seconds)",
"ramp_term": "schedule(d) = floor(F (10R + 90d) / (100R)) = F/10 + floor(d G / 9) for d < R, G = F / 320000 = 99025274406250 = 9q + 1, q = 11002808267361, so schedule(d) = F/10 + d q + floor(d / 9)",
"ramp_sum": "sum over d = 0..R-1 = R F / 10 + q R (R - 1) / 2 + 9 (0 + 1 + ... + 287999) = 45174523722296485498848000 (R = 9 x 288000)"
},
"id": "reorg-ramp-end-k1",
"description": "ramp end at DAA R: the ancestor is the last ramp block (height R, DAA R-1); an orphan branch of 1 block (height R+1, DAA R, the first full-rate block) is replaced by 2 (heights R+1, R+2)",
"ancestor_height": 2592000,
"orphan_len": 1,
"replacement_len": 2,
"expected": {
"ancestor": {
"height": 2592000,
"daa_score": 2591999,
"subsidy": "31688076807191732638",
"cumulative": "45174523722296485498848000",
"derivation": "subsidy F/10 + (R-1) q + floor((R-1)/9) = F/10 + 2591999 q + 287999; cumulative ramp_sum"
},
"orphan_tip": {
"height": 2592001,
"subsidy": "31688087810000000000",
"cumulative": "45174555410384295498848000",
"derivation": "ramp_sum + F"
},
"after_rollback": {
"height": 2592000,
"cumulative": "45174523722296485498848000"
},
"final": {
"height": 2592002,
"daa_score": 2592001,
"subsidy": "31688087810000000000",
"cumulative": "45174587098472105498848000",
"derivation": "ramp_sum + 2F; equals the straight chain's cumulative at height R+2"
},
"known_failed_no_undo": {
"cumulative": "45174618786559915498848000",
"derivation": "ramp_sum + 3F: a ledger that forgets to subtract the orphan counts F twice; must be refused"
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
[package]
name = "tv02-replay-node"
version = "0.1.0"
edition = "2024"
description = "TV-02 implementation A: the issued-supply replay in the node's language, calling the node's own issuance code (kaspa-consensus-core) per block"
license = "MIT"
publish = false
# A standalone workspace. The node fork is read by path from the repository's vendor/igneum-node (the fork checkout at
# the commit the run names, outside version control, as pool/ and app/igneum-wallet read it). Nothing here changes the
# node: the crate only calls kaspa-consensus-core's public issuance functions.
[workspace]
[[bin]]
name = "tv02-replay-node"
path = "src/main.rs"
[dependencies]
kaspa-consensus-core = { path = "../../../../vendor/igneum-node/consensus/core" }
serde_json = "1"
[profile.release]
overflow-checks = true
debug-assertions = true

View file

@ -0,0 +1,430 @@
//! TV-02 implementation A: the issued-supply replay in the node's language.
//!
//! Every per-block subsidy here is the node's own function, called through its public API at the commit the run
//! names: `EmissionTable::block_subsidy` (the path `CoinbaseManager::calc_block_subsidy` takes, consensus/src/
//! processes/coinbase.rs 262-266, and the value body validation compares the coinbase payload against). Every block
//! is also read through the node's second, independent issuance path, `igneum::block_subsidy_units` (the u128 twin of
//! the 33-entry table rule); a block where the two differ is counted and named in the summary.
//!
//! Model: a straight chain. Genesis (height 0, DAA score 0) issues nothing: it never enters the DAA window
//! (consensus/src/processes/window.rs 176-179 puts it in mergeset_non_daa) and expected_coinbase_transaction skips
//! non-DAA blues (coinbase.rs 139). So block 1 has DAA score 0 (difficulty.rs 31-32: the selected parent's score plus
//! the mergeset's DAA blocks, none for block 1) and the block at height h >= 1 has DAA score h - 1, issuing the
//! schedule's subsidy at that score. "cumulative" at height h is the issued supply of the blocks at heights 0..=h,
//! the sum of the schedule over DAA scores 0..=h-1, in base units, exact (u128, overflow checks on). The node lane
//! confirmed the genesis rule on bf60948a (9 October 2026, 09:25 UK).
//!
//! Commands (outputs are CSV with the header height,daa_score,subsidy,cumulative):
//! straight <network> <out.csv> <labels.csv> <summary.json>
//! reorg <fixture.json> <out.csv>
//! genesis-payload <out.json>
//! Networks: devnet4 (devnet4_params()), mainnet (MAINNET_PARAMS), devnet (DEVNET_PARAMS).
use kaspa_consensus_core::{
config::params::{DEVNET_PARAMS, MAINNET_PARAMS, Params, TESTNET_PARAMS, devnet3_params, devnet4_params},
emission::{EmissionTable, Tail},
igneum,
unit::{self, Amount},
};
use std::collections::BTreeMap;
use std::io::Write;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
struct Net {
name: String,
params: Params,
table: EmissionTable,
bps: u64,
decimals: u8,
}
impl Net {
fn new(name: &str) -> Net {
let params = match name {
"devnet4" => devnet4_params(),
"mainnet" => MAINNET_PARAMS,
"devnet" => DEVNET_PARAMS,
other => panic!("unknown network {other} (devnet4, mainnet, devnet)"),
};
let table = params.emission.table();
let bps = params.bps();
let decimals = params.base_unit_decimals;
Net { name: name.to_string(), params, table, bps, decimals }
}
/// The node's subsidy for a block at `daa` (the coinbase manager's path).
#[inline]
fn subsidy(&self, daa: u64) -> u128 {
self.table.block_subsidy(daa, self.bps)
}
/// The node's second path (igneum.rs `block_subsidy_units`), defined for the CURRENT shape only.
#[inline]
fn subsidy_units(&self, daa: u64) -> u128 {
igneum::block_subsidy_units(daa, self.bps, self.decimals).0
}
}
/// The row set's DAA scores (the README's rule, implemented here and independently in B): the probes, every
/// transition's before/at/after, and one step past the terminal transition. Returns (sorted DAA scores, labels, the
/// first DAA score that pays nothing). The CSV row of DAA score d is the block at height d + 1; genesis is its own row.
fn rows(net: &Net) -> (Vec<u64>, BTreeMap<u64, String>, u64) {
let s = &net.params.emission;
let bps = net.bps;
let mut labels: BTreeMap<u64, String> = BTreeMap::new();
let add = |h: u64, l: String, labels: &mut BTreeMap<u64, String>| {
labels.entry(h).and_modify(|e| { e.push_str("; "); e.push_str(&l) }).or_insert(l);
};
add(0, "daa 0 (block 1)".into(), &mut labels);
add(1, "probe 1".into(), &mut labels);
add(2, "probe 2".into(), &mut labels);
if bps > 1 {
add(bps - 1, "second 0 last block".into(), &mut labels);
add(bps, "second 1 first block".into(), &mut labels);
add(bps + 1, "second 1 second block".into(), &mut labels);
}
for d in 1..30u64 {
add(d * 86_400 * bps, format!("ramp day {d}"), &mut labels);
}
add(bps * s.ramp_seconds / 2, "ramp midpoint".into(), &mut labels);
let mut transitions: Vec<(u64, String)> = vec![(bps * s.ramp_seconds, "ramp end".into())];
let mut zero = 0u64;
for k in 1..=200u64 {
let x = k * s.step_seconds * bps;
transitions.push((x, format!("step {k}")));
if net.subsidy(x) == 0 {
zero = x;
break;
}
}
assert!(zero > 0, "the curve did not reach zero within 200 steps");
for (x, l) in &transitions {
add(x - 1, format!("before {l}"), &mut labels);
add(*x, format!("at {l}"), &mut labels);
add(x + 1, format!("after {l}"), &mut labels);
}
add(zero - 1, "terminal (last block with a subsidy)".into(), &mut labels);
add(zero + s.step_seconds * bps, "one step past the terminal".into(), &mut labels);
let heights: Vec<u64> = labels.keys().copied().collect();
(heights, labels, zero)
}
#[derive(Default, Clone, Copy)]
struct Chunk {
sum: u128,
path_mismatch: u64,
first_mismatch: Option<u64>,
shape_violation: u64,
}
/// Sums blocks `a..=b` one by one, with the two-path cross-check and the shape check (non-decreasing through the
/// ramp, non-increasing after it).
fn sum_chunk(net: &Net, a: u64, b: u64) -> Chunk {
let ramp_end = net.bps * net.params.emission.ramp_seconds;
let check_units = matches!(net.params.emission.tail, Tail::Cap);
let mut c = Chunk::default();
let mut prev = if a > 0 { Some(net.subsidy(a - 1)) } else { None };
let mut d = a;
loop {
let s = net.subsidy(d);
if check_units {
let u = net.subsidy_units(d);
if u != s {
c.path_mismatch += 1;
if c.first_mismatch.is_none() {
c.first_mismatch = Some(d);
}
}
}
if let Some(p) = prev {
let ok = if d < ramp_end { s >= p } else if d - 1 >= ramp_end { s <= p } else { true };
if !ok {
c.shape_violation += 1;
}
}
c.sum = c.sum.checked_add(s).expect("sum overflows u128");
prev = Some(s);
if d == b {
break;
}
d += 1;
}
c
}
/// Cumulative issued supply at each height of `heights` (sorted, distinct), every block from genesis summed once.
fn cumulative_at(net: &Net, heights: &[u64]) -> (Vec<u128>, Chunk) {
const CHUNK: u64 = 1 << 24;
// segments: [0, h0], [h0+1, h1], ...; each split into chunks of at most CHUNK blocks
let mut jobs: Vec<(usize, u64, u64)> = Vec::new();
let mut lo = 0u64;
for (i, &h) in heights.iter().enumerate() {
if h < lo {
continue;
}
let mut a = lo;
while a <= h {
let b = (a.saturating_add(CHUNK - 1)).min(h);
jobs.push((i, a, b));
a = b + 1;
}
lo = h + 1;
}
let next = AtomicUsize::new(0);
let results: Mutex<Vec<(usize, Chunk)>> = Mutex::new(Vec::with_capacity(jobs.len()));
let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
std::thread::scope(|sc| {
for _ in 0..threads {
sc.spawn(|| {
loop {
let j = next.fetch_add(1, Ordering::Relaxed);
if j >= jobs.len() {
break;
}
let (i, a, b) = jobs[j];
let c = sum_chunk(net, a, b);
results.lock().unwrap().push((i, c));
}
});
}
});
let mut seg = vec![0u128; heights.len()];
let mut total = Chunk::default();
for (i, c) in results.into_inner().unwrap() {
seg[i] = seg[i].checked_add(c.sum).unwrap();
total.path_mismatch += c.path_mismatch;
total.shape_violation += c.shape_violation;
total.first_mismatch = match (total.first_mismatch, c.first_mismatch) {
(Some(x), Some(y)) => Some(x.min(y)),
(x, y) => x.or(y),
};
}
let mut cum = Vec::with_capacity(heights.len());
let mut run = 0u128;
for s in seg {
run = run.checked_add(s).unwrap();
cum.push(run);
}
(cum, total)
}
fn write_csv(path: &str, rows: &[(u64, u64, u128, u128)]) {
let mut f = std::fs::File::create(path).unwrap();
writeln!(f, "height,daa_score,subsidy,cumulative").unwrap();
for (h, d, s, c) in rows {
writeln!(f, "{h},{d},{s},{c}").unwrap();
}
}
fn straight(name: &str, out: &str, labels_out: &str, summary_out: &str) {
let t0 = Instant::now();
let net = Net::new(name);
let (daas, labels, zero) = rows(&net);
let (cum, chk) = cumulative_at(&net, &daas);
// genesis issues nothing; the block of DAA score d sits at height d + 1
let mut rows: Vec<(u64, u64, u128, u128)> = vec![(0, 0, 0, 0)];
rows.extend(daas.iter().zip(cum.iter()).map(|(&d, &c)| (d + 1, d, net.subsidy(d), c)));
write_csv(out, &rows);
let mut lf = std::fs::File::create(labels_out).unwrap();
writeln!(lf, "height,label").unwrap();
writeln!(lf, "0,\"genesis (issues nothing)\"").unwrap();
for (d, l) in &labels {
writeln!(lf, "{},\"{l}\"", d + 1).unwrap();
}
let terminal = zero; // the height of the block of DAA zero - 1, the last that pays
let issued_at_terminal = rows.iter().find(|r| r.0 == terminal).unwrap().3;
let last = rows.last().unwrap();
let cap = igneum::supply_cap_units(net.decimals);
let s = &net.params.emission;
let summary = serde_json::json!({
"implementation": "A (Rust, the node's issuance code by path)",
"node_commit": std::env::var("TV02_NODE_COMMIT").unwrap_or_default(),
"network": net.name,
"network_id": net.params.net.to_string(),
"bps": net.bps,
"base_unit_decimals": net.decimals,
"schedule": {
"launch_rate": s.launch_rate.to_string(),
"ramp_seconds": s.ramp_seconds,
"ramp_start_percent": s.ramp_start_percent,
"step_seconds": s.step_seconds,
"step_decay_q32": s.step_decay_q32,
"tail": format!("{:?}", s.tail),
},
"blocks_replayed_per_block": last.0 + 1,
"genesis_rule": "genesis issues nothing; height h >= 1 has DAA score h - 1 (window.rs 176-179, difficulty.rs 31-32, coinbase.rs 139)",
"rows": rows.len(),
"first_zero_subsidy_daa": zero,
"first_zero_subsidy_height": zero + 1,
"terminal_height": terminal,
"issued_at_terminal": issued_at_terminal.to_string(),
"issued_one_step_past_terminal": last.3.to_string(),
"supply_cap_units": cap.to_string(),
"cap_minus_issued": (cap as i128 - issued_at_terminal as i128).to_string(),
"cap_breach": issued_at_terminal > cap,
"issuance_after_terminal": (last.3 - issued_at_terminal).to_string(),
"node_two_paths_mismatched_blocks": chk.path_mismatch,
"node_two_paths_first_mismatch_height": chk.first_mismatch,
"shape_violations": chk.shape_violation,
"far_future_subsidy_u64_max_div_2": net.subsidy(u64::MAX / 2).to_string(),
"seconds": t0.elapsed().as_secs_f64(),
});
std::fs::write(summary_out, serde_json::to_string_pretty(&summary).unwrap() + "\n").unwrap();
println!("{}", serde_json::to_string(&summary).unwrap());
}
/// The reorg fixture: the common ancestor, an orphan branch of k blocks, rolled back and replaced by k+1. The
/// ledger keeps one record per applied block and a rollback subtracts exactly the records it pops. The known-failed
/// ledger (rollback that forgets to subtract) is run beside it and written as its own row.
fn reorg(fixture: &str, out: &str) {
let fx: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(fixture).unwrap()).unwrap();
let net = Net::new(fx["network"].as_str().unwrap());
let anc = fx["ancestor_height"].as_u64().unwrap();
let k = fx["orphan_len"].as_u64().unwrap();
let k1 = fx["replacement_len"].as_u64().unwrap();
assert_eq!(k1, k + 1, "the fixture replaces k blocks by k+1");
assert!(anc >= 1, "the ancestor is a block after genesis");
// heights; the block at height h >= 1 has DAA score h - 1
let (cum, chk) = cumulative_at(&net, &[anc - 1]);
assert_eq!(chk.path_mismatch, 0);
let mut rows: Vec<(String, u64, u64, u128, u128)> = Vec::new();
let mut ledger: Vec<(u64, u128)> = Vec::new();
let mut total = cum[0];
let mut naive = cum[0];
rows.push(("ancestor".into(), anc, anc - 1, net.subsidy(anc - 1), total));
for h in anc + 1..=anc + k {
let s = net.subsidy(h - 1);
ledger.push((h, s));
total += s;
naive += s;
rows.push(("orphan".into(), h, h - 1, s, total));
}
for _ in 0..k {
let (h, s) = ledger.pop().unwrap();
total -= s;
rows.push(("rollback".into(), h, h - 1, s, total));
}
for h in anc + 1..=anc + k1 {
let s = net.subsidy(h - 1);
ledger.push((h, s));
total += s;
naive += s;
rows.push(("replacement".into(), h, h - 1, s, total));
}
let tip = anc + k1;
rows.push(("final".into(), tip, tip - 1, net.subsidy(tip - 1), total));
rows.push(("known_failed_no_undo".into(), tip, tip - 1, net.subsidy(tip - 1), naive));
let mut f = std::fs::File::create(out).unwrap();
writeln!(f, "step,height,daa_score,subsidy,cumulative").unwrap();
for (st, h, d, s, c) in &rows {
writeln!(f, "{st},{h},{d},{s},{c}").unwrap();
}
println!("reorg {} final {} naive {}", fx["id"].as_str().unwrap_or(""), total, naive);
}
/// What the node reads as the subsidy field of each compiled genesis coinbase payload at that network's own unit
/// (the slicing of `CoinbaseManager::deserialize_coinbase_payload`, coinbase.rs 231-236: blue score u64, the subsidy
/// at `amount_wire_len()`, script version u16, script length u8), beside the schedule's subsidy at DAA 0.
fn genesis_payload(out: &str) {
let mut v = Vec::new();
for (name, p) in [
("mainnet", MAINNET_PARAMS),
("testnet-1", TESTNET_PARAMS),
("devnet", DEVNET_PARAMS),
("devnet-3", devnet3_params()),
("devnet-4", devnet4_params()),
] {
let payload = p.genesis.coinbase_payload;
let row = unit::with_decimals_for_test(p.base_unit_decimals, || {
let w = unit::amount_wire_len();
let field = Amount::from_consensus_bytes(&payload[8..8 + w]).map(|a| a.0);
let version = u16::from_le_bytes([payload[8 + w], payload[9 + w]]);
let spk_len = payload[10 + w];
serde_json::json!({
"network": name,
"base_unit_decimals": p.base_unit_decimals,
"amount_wire_len": w,
"payload_len": payload.len(),
"subsidy_field_read_at_network_unit": field.map(|x| x.to_string()),
"subsidy_field_equals_one_ign_at_network_unit": field == Some(unit::unit(p.base_unit_decimals)),
"script_version_read": version,
"script_len_read": spk_len,
"one_ign_at_network_unit": unit::unit(p.base_unit_decimals).to_string(),
"max_amount_at_network_unit": unit::max_amount().to_string(),
"schedule_subsidy_at_daa_0": p.emission.table().block_subsidy(0, p.bps()).to_string(),
})
});
v.push(row);
}
let s = serde_json::to_string_pretty(&v).unwrap();
std::fs::write(out, s.clone() + "\n").unwrap();
println!("{s}");
}
fn main() {
let a: Vec<String> = std::env::args().collect();
match a.get(1).map(|s| s.as_str()) {
Some("straight") if a.len() == 6 => straight(&a[2], &a[3], &a[4], &a[5]),
Some("reorg") if a.len() == 4 => reorg(&a[2], &a[3]),
Some("genesis-payload") if a.len() == 3 => genesis_payload(&a[2]),
_ => {
eprintln!("usage: straight <network> <out.csv> <labels.csv> <summary.json> | reorg <fixture.json> <out.csv> | genesis-payload <out.json>");
std::process::exit(2);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const F: u128 = 31_688_087_810_000_000_000;
const H: u128 = 15_844_043_905_000_000_000;
const RAMP_SUM: u128 = 45_174_523_722_296_485_498_848_000; // the fixture's hand-written closed form
/// The known-failed case first: a ledger that forgets to subtract the orphan on rollback ends above the straight
/// chain, by exactly the orphan's subsidy; the ledger here subtracts it and lands on the straight chain.
#[test]
fn a_rollback_that_forgets_the_orphan_double_counts_it() {
let net = Net::new("devnet4");
let r = 2_592_000u64;
let (cum, _) = cumulative_at(&net, &[r - 1, r + 1]);
assert_eq!(cum[0], RAMP_SUM, "the ramp's closed form, written by hand");
let orphan = net.subsidy(r);
let naive = cum[0] + orphan + net.subsidy(r) + net.subsidy(r + 1);
assert_ne!(naive, cum[1], "the no-undo ledger must not equal the straight chain");
assert_eq!(naive - cum[1], orphan);
assert_eq!(cum[0] + net.subsidy(r) + net.subsidy(r + 1), cum[1]);
}
/// The hand-written boundary values of the fixtures: the last ramp block, the full rate, the first halving at 18
/// decimals (F is even there, so floor(F / 2) loses nothing, unlike 3,168,808,781 >> 1 at 8 decimals).
#[test]
fn boundary_subsidies_are_the_hand_written_ones() {
let net = Net::new("devnet4");
assert_eq!(net.subsidy(0), F / 10);
assert_eq!(net.subsidy(2_591_999), 31_688_076_807_191_732_638);
assert_eq!(net.subsidy(2_592_000), F);
assert_eq!(net.subsidy(63_115_199), F);
assert_eq!(net.subsidy(63_115_200), H);
let dev = Net::new("devnet");
assert_eq!(dev.subsidy(63_115_200), 1_584_404_390);
assert_ne!(dev.subsidy(63_115_200) * 10_000_000_000, H, "the 8-decimal halving floors; the 18-decimal one does not");
}
/// The node's two issuance paths agree at every transition's before/at/after on the three networks.
#[test]
fn the_node_paths_agree_at_every_row() {
for n in ["devnet4", "mainnet", "devnet"] {
let net = Net::new(n);
let (daas, _, zero) = rows(&net);
for d in daas {
assert_eq!(net.subsidy(d), net.subsidy_units(d), "{n} daa {d}");
}
assert_eq!(net.subsidy(zero), 0);
assert!(net.subsidy(zero - 1) > 0);
}
}
}

View file

@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""TV-02 implementation B: the issued-supply replay written from the supply rules in words, standalone Python 3.
No code is shared with implementation A (the Rust crate that calls the node). B reads a rules file (the interim
transcription of the litepaper and economics page, rules-v0-litepaper.json, or the TV-01 lane's supply-spec.json
through `load_rules`) and computes every quantity with Python integers.
Method (deliberately not A's): A sums every block one by one. B groups blocks. Inside the ramp every block of one
DAA second pays the same, so B sums seconds and multiplies by the blocks in each second; after the ramp every block
of one step pays the same, so B multiplies the step's subsidy by the number of its blocks. Only the partial second
or partial step at the end of a range is counted block by block.
Model: a straight chain, genesis at height 0. Two rules the words must state, read from the rules file:
genesis_issues ("schedule_at_daa_0" or "nothing") and daa_of_height ("height": block h has DAA score h, or
"height_minus_one": block h >= 1 has DAA score h - 1). cumulative(h) is the issued supply of the blocks at heights 0..h.
Usage:
replay_spec.py straight <rules.json> <network> <out.csv> <labels.csv> <summary.json>
replay_spec.py reorg <rules.json> <fixture.json> <out.csv>
replay_spec.py selftest
"""
import csv
import json
import sys
import time
class Schedule:
def __init__(self, rules, net):
r = rules["rules"]
n = rules["networks"][net]
self.net = net
self.bps = int(n["bps"])
self.decimals = int(n["decimals"])
self.year = int(r["year_seconds"])
fd = int(r["launch_rate_floor_decimals"])
floor_rate = int(r["year_one_emission_ign"]) * 10 ** fd // self.year
if self.decimals >= fd:
self.rate0 = floor_rate * 10 ** (self.decimals - fd)
else:
self.rate0 = int(r["year_one_emission_ign"]) * 10 ** self.decimals // self.year
self.step = int(r["step_seconds"])
self.ramp_s = int(r["ramp_seconds"])
self.ramp_start = int(r["ramp_start_percent"])
self.order = r["per_block_order"]
if r["tail"] != "cap":
raise SystemExit("B replays a capped schedule only; tail %r is outside TV-02's ratified D01 policy" % r["tail"])
self.cap = int(r["supply_cap_ign"]) * 10 ** self.decimals
self.genesis_issues = r.get("genesis_issues", "schedule_at_daa_0")
self.daa_of_height = r.get("daa_of_height", "height")
if self.genesis_issues not in ("schedule_at_daa_0", "nothing"):
raise SystemExit("unknown genesis_issues %r" % self.genesis_issues)
if self.daa_of_height not in ("height", "height_minus_one"):
raise SystemExit("unknown daa_of_height %r" % self.daa_of_height)
# --- the per-block rule, from the words ---
def rate(self, k):
"""Per-second rate in step k: the launch rate halved k times, floored each time (equal to rate0 // 2**k)."""
x = self.rate0
for _ in range(k):
x //= 2
if x == 0:
break
return x
def ramp(self, x, s):
if s >= self.ramp_s:
return x
num = self.ramp_start * self.ramp_s + (100 - self.ramp_start) * s
return x * num // (100 * self.ramp_s)
def subsidy_of_second(self, s):
k = s // self.step
if self.order == "divide_then_ramp":
return self.ramp(self.rate(k) // self.bps, s)
if self.order == "ramp_then_divide":
return self.ramp(self.rate(k), s) // self.bps
raise SystemExit("unknown per_block_order %r" % self.order)
def subsidy(self, d):
return self.subsidy_of_second(d // self.bps)
def zero_step(self):
"""The first step k >= 1 whose blocks pay nothing."""
k = 1
while self.subsidy_of_second(k * self.step) != 0:
k += 1
if k > 1000:
raise SystemExit("the curve does not reach zero")
return k
# --- grouped sums ---
def blocks_in_seconds(self, s0, s1, h):
"""Number of blocks with height <= h whose second lies in [s0, s1)."""
lo = s0 * self.bps
hi = min(s1 * self.bps - 1, h)
return max(0, hi - lo + 1)
def genesis_amount(self):
return self.subsidy(0) if self.genesis_issues == "schedule_at_daa_0" else 0
def daa(self, h):
"""DAA score of the block at height h >= 1."""
return h if self.daa_of_height == "height" else h - 1
def height_of_daa(self, d):
return d if self.daa_of_height == "height" else d + 1
def block_subsidy_at_height(self, h):
return self.genesis_amount() if h == 0 else self.subsidy(self.daa(h))
def issued(self, h):
"""Issued supply of the blocks at heights 0..h: genesis, then blocks 1..h at their DAA scores."""
if h == 0:
return self.genesis_amount()
lo, hi = self.daa(1), self.daa(h)
return self.genesis_amount() + self.cumulative(hi) - self.cumulative(lo - 1)
def cumulative(self, h):
"""Sum of the schedule over DAA scores 0..h."""
if h < 0:
return 0
last_s = h // self.bps
total = 0
# the ramp, second by second (the whole ramp summed once and kept, when h lies past it)
if last_s >= self.ramp_s:
if getattr(self, "_ramp_total", None) is None:
self._ramp_total = sum(self.bps * self.subsidy_of_second(s) for s in range(self.ramp_s))
total += self._ramp_total
else:
for s in range(0, last_s + 1):
total += self.blocks_in_seconds(s, s + 1, h) * self.subsidy_of_second(s)
# after the ramp, step by step
k = self.ramp_s // self.step
while True:
s0 = max(self.ramp_s, k * self.step)
s1 = (k + 1) * self.step
if s0 > last_s:
break
n = self.blocks_in_seconds(s0, s1, h)
total += n * self.subsidy_of_second(s0)
k += 1
return total
# --- the row set (the README's rule) ---
def rows(self):
bps = self.bps
labels = {}
def add(h, label):
labels[h] = labels[h] + "; " + label if h in labels else label
add(0, "daa 0")
add(1, "probe 1")
add(2, "probe 2")
if bps > 1:
add(bps - 1, "second 0 last block")
add(bps, "second 1 first block")
add(bps + 1, "second 1 second block")
for d in range(1, 30):
add(d * 86400 * bps, "ramp day %d" % d)
add(bps * self.ramp_s // 2, "ramp midpoint")
kz = self.zero_step()
transitions = [(bps * self.ramp_s, "ramp end")] + [(k * self.step * bps, "step %d" % k) for k in range(1, kz + 1)]
for x, l in transitions:
add(x - 1, "before " + l)
add(x, "at " + l)
add(x + 1, "after " + l)
zero = kz * self.step * bps
add(zero - 1, "terminal (last block with a subsidy)")
add(zero + self.step * bps, "one step past the terminal")
hl = {}
for d, l in labels.items():
h = self.height_of_daa(d)
hl[h] = (hl[h] + "; " + l) if h in hl else l
hl[0] = ("genesis; " + hl[0]) if 0 in hl else "genesis (issues nothing)"
return sorted(hl), hl, zero
def load_rules(path):
with open(path) as f:
doc = json.load(f)
if "rules" in doc and "networks" in doc:
return doc
raise SystemExit("rules file %s has no rules/networks block; the TV-01 spec adapter maps it in tv01_adapter.py" % path)
def straight(rules_path, net, out, labels_out, summary_out):
t0 = time.time()
rules = load_rules(rules_path)
sc = Schedule(rules, net)
heights, labels, zero = sc.rows()
with open(out, "w", newline="") as f:
w = csv.writer(f, lineterminator="\n")
w.writerow(["height", "daa_score", "subsidy", "cumulative"])
cum = {}
for h in heights:
cum[h] = sc.issued(h)
w.writerow([h, 0 if h == 0 else sc.daa(h), sc.block_subsidy_at_height(h), cum[h]])
with open(labels_out, "w", newline="") as f:
w = csv.writer(f, lineterminator="\n", quoting=csv.QUOTE_NONNUMERIC)
f.write("height,label\n")
for h in heights:
f.write('%d,"%s"\n' % (h, labels[h]))
term = sc.height_of_daa(zero - 1)
summary = {
"implementation": "B (Python 3, from the rules in words)",
"rules_file": rules_path,
"rules_id": rules.get("id"),
"network": net,
"bps": sc.bps,
"base_unit_decimals": sc.decimals,
"launch_rate": str(sc.rate0),
"first_zero_subsidy_daa": zero,
"first_zero_subsidy_height": sc.height_of_daa(zero),
"genesis_issues": sc.genesis_issues,
"daa_of_height": sc.daa_of_height,
"terminal_height": term,
"issued_at_terminal": str(cum[term]),
"issued_one_step_past_terminal": str(cum[heights[-1]]),
"supply_cap_units": str(sc.cap),
"cap_minus_issued": str(sc.cap - cum[term]),
"cap_breach": cum[term] > sc.cap,
"issuance_after_terminal": str(cum[heights[-1]] - cum[term]),
"rows": len(heights),
"seconds": round(time.time() - t0, 3),
}
with open(summary_out, "w") as f:
json.dump(summary, f, indent=2)
f.write("\n")
print(json.dumps(summary))
def reorg(rules_path, fixture_path, out):
rules = load_rules(rules_path)
with open(fixture_path) as f:
fx = json.load(f)
sc = Schedule(rules, fx["network"])
anc = int(fx["ancestor_height"])
k = int(fx["orphan_len"])
k1 = int(fx["replacement_len"])
if k1 != k + 1:
raise SystemExit("the fixture replaces k blocks by k+1")
total = sc.issued(anc)
naive = total
ledger = []
rows = [("ancestor", anc, sc.block_subsidy_at_height(anc), total)]
for h in range(anc + 1, anc + k + 1):
s = sc.block_subsidy_at_height(h)
ledger.append((h, s))
total += s
naive += s
rows.append(("orphan", h, s, total))
for _ in range(k):
h, s = ledger.pop()
total -= s
rows.append(("rollback", h, s, total))
for h in range(anc + 1, anc + k1 + 1):
s = sc.block_subsidy_at_height(h)
ledger.append((h, s))
total += s
naive += s
rows.append(("replacement", h, s, total))
tip = anc + k1
rows.append(("final", tip, sc.block_subsidy_at_height(tip), total))
rows.append(("known_failed_no_undo", tip, sc.block_subsidy_at_height(tip), naive))
with open(out, "w") as f:
f.write("step,height,daa_score,subsidy,cumulative\n")
for st, h, s, c in rows:
f.write("%s,%d,%d,%d,%d\n" % (st, h, 0 if h == 0 else sc.daa(h), s, c))
print("reorg %s final %d naive %d" % (fx.get("id", ""), total, naive))
def selftest():
"""B's own known-failed case first, then its cross-check of the grouped sum against a block-by-block sum."""
rules = {
"rules": {"year_seconds": 31557600, "year_one_emission_ign": 1000000000, "launch_rate_floor_decimals": 8,
"step_seconds": 600, "ramp_seconds": 50, "ramp_start_percent": 10, "per_block_order": "divide_then_ramp",
"tail": "cap", "supply_cap_ign": 4000000000},
"networks": {"t1": {"bps": 1, "decimals": 18}, "t3": {"bps": 3, "decimals": 18}, "t10": {"bps": 10, "decimals": 8}},
}
# known-failed: a grouped sum that forgets the partial last second must disagree with the block-by-block sum
sc = Schedule(rules, "t3")
h = 3 * 20 + 1
brute = sum(sc.subsidy(d) for d in range(h + 1))
wrong = sum(3 * sc.subsidy_of_second(s) for s in range(h // 3))
assert wrong != brute, "the known-failed grouping was not caught"
for net in ("t1", "t3", "t10"):
sc = Schedule(rules, net)
run = 0
end = sc.bps * sc.step * 4 + 7
for d in range(end):
run += sc.subsidy(d)
if d % 97 == 0 or d in (sc.bps * sc.ramp_s - 1, sc.bps * sc.ramp_s, sc.bps * sc.step - 1, sc.bps * sc.step):
assert sc.cumulative(d) == run, (net, d, sc.cumulative(d), run)
assert sc.rate(3) == sc.rate0 // 8
print("selftest: known-failed grouping caught; grouped sum equals the block-by-block sum on 3 networks")
def main(a):
if len(a) == 7 and a[1] == "straight":
straight(a[2], a[3], a[4], a[5], a[6])
elif len(a) == 5 and a[1] == "reorg":
reorg(a[2], a[3], a[4])
elif len(a) == 2 and a[1] == "selftest":
selftest()
else:
print(__doc__, file=sys.stderr)
sys.exit(2)
if __name__ == "__main__":
main(sys.argv)

View file

@ -0,0 +1,47 @@
{
"id": "tv02-rules-v0-litepaper",
"status": "interim: written from the published words before the TV-01 supply spec landed; replaced by the spec when it lands",
"sources": [
"site/litepaper.html, Economics > Supply: hard cap of 4 billion IGN, emission starts at 1 billion a year and halves every two years for ever, ramps from 10% to 100% over the first 30 days",
"site/litepaper.html, Finality: emission per block is a schedule keyed to difficulty-adjusted time, every block in the window is paid at that rate",
"site/economics.html, Emission table: a year is 31,557,600 seconds; launch rate the literal 3,168,808,781 (10^9 IGN x 10^8 / 31,557,600 floored) widened to 18 decimals; ramp 2,592,000 s from 10 percent; step 63,115,200 s, the rate halved each step; tail none, the curve runs to zero",
"site/economics.html, Units and decimals: the Igneum 2.0 devnet and mainnet are 18 decimals"
],
"rules": {
"year_seconds": 31557600,
"year_one_emission_ign": 1000000000,
"launch_rate_floor_decimals": 8,
"launch_rate_rule": "rate0 = floor(year_one_emission_ign * 10^launch_rate_floor_decimals / year_seconds) * 10^(decimals - launch_rate_floor_decimals)",
"step_seconds": 63115200,
"step_rule": "rate(k) = floor(rate(k-1) / 2), at the network's unit",
"ramp_seconds": 2592000,
"ramp_start_percent": 10,
"ramp_rule": "ramped = floor(x * (start * R + (100 - start) * s) / (100 * R)) for s < R, else x",
"seconds_rule": "s = floor(daa_score / bps)",
"per_block_order": "divide_then_ramp",
"per_block_order_source": "ASSUMED: the words give a per-second rate and blocks per second but not the order of the floor; the natural reading (the second's per-block share, then the ramp) is taken and flagged for the spec",
"tail": "cap",
"supply_cap_ign": 4000000000,
"genesis_issues": "schedule_at_daa_0",
"genesis_issues_source": "ASSUMED: the words say nothing about genesis; read as a block like every other, paying the schedule at DAA 0",
"daa_of_height": "height",
"daa_of_height_source": "ASSUMED: the words say one block a second and a schedule keyed to DAA time; read as the block at height h carrying DAA score h"
},
"networks": {
"devnet4": {
"bps": 1,
"decimals": 18,
"source": "site/economics.html: the Igneum 2.0 devnet, 18 decimals, one block a second"
},
"mainnet": {
"bps": 10,
"decimals": 18,
"source": "site/litepaper.html: 1 / s blocks, rising to 10 (designed); site/economics.html: mainnet 18 decimals"
},
"devnet": {
"bps": 1,
"decimals": 8,
"source": "site/economics.html: an earlier devnet paid in 8 decimals"
}
}
}

View file

@ -0,0 +1,48 @@
{
"id": "tv02-rules-v0.1-litepaper-plus-genesis-rule",
"status": "interim: v0 with the two rules the words leave out filled by the node lane's written answer (9 October 2026, 09:25 UK), cited by file and line, not by code; replaced by the TV-01 spec when it lands",
"sources": [
"site/litepaper.html, Economics > Supply: hard cap of 4 billion IGN, emission starts at 1 billion a year and halves every two years for ever, ramps from 10% to 100% over the first 30 days",
"site/litepaper.html, Finality: emission per block is a schedule keyed to difficulty-adjusted time, every block in the window is paid at that rate",
"site/economics.html, Emission table: a year is 31,557,600 seconds; launch rate the literal 3,168,808,781 (10^9 IGN x 10^8 / 31,557,600 floored) widened to 18 decimals; ramp 2,592,000 s from 10 percent; step 63,115,200 s, the rate halved each step; tail none, the curve runs to zero",
"site/economics.html, Units and decimals: the Igneum 2.0 devnet and mainnet are 18 decimals",
"the node lane's answer to TV-02, 9 October 2026 09:25 UK (window.rs 176-179, coinbase.rs 139, difficulty.rs 31-32 at bf60948a)"
],
"rules": {
"year_seconds": 31557600,
"year_one_emission_ign": 1000000000,
"launch_rate_floor_decimals": 8,
"launch_rate_rule": "rate0 = floor(year_one_emission_ign * 10^launch_rate_floor_decimals / year_seconds) * 10^(decimals - launch_rate_floor_decimals)",
"step_seconds": 63115200,
"step_rule": "rate(k) = floor(rate(k-1) / 2), at the network's unit",
"ramp_seconds": 2592000,
"ramp_start_percent": 10,
"ramp_rule": "ramped = floor(x * (start * R + (100 - start) * s) / (100 * R)) for s < R, else x",
"seconds_rule": "s = floor(daa_score / bps)",
"per_block_order": "divide_then_ramp",
"per_block_order_source": "ASSUMED: the words give a per-second rate and blocks per second but not the order of the floor; the natural reading (the second's per-block share, then the ramp) is taken and flagged for the spec",
"tail": "cap",
"supply_cap_ign": 4000000000,
"genesis_issues": "nothing",
"genesis_issues_source": "the node lane, 9 October 2026 09:25 UK: genesis never enters the DAA window (consensus/src/processes/window.rs 176-179) and the coinbase pays only blues outside mergeset_non_daa (coinbase.rs 139): genesis is never rewarded",
"daa_of_height": "height_minus_one",
"daa_of_height_source": "consequence of the same rule: a block's DAA score is its selected parent's plus its mergeset's DAA blocks (difficulty.rs 31-32), so block 1 has DAA score 0 and block h has h - 1"
},
"networks": {
"devnet4": {
"bps": 1,
"decimals": 18,
"source": "site/economics.html: the Igneum 2.0 devnet, 18 decimals, one block a second"
},
"mainnet": {
"bps": 10,
"decimals": 18,
"source": "site/litepaper.html: 1 / s blocks, rising to 10 (designed); site/economics.html: mainnet 18 decimals"
},
"devnet": {
"bps": 1,
"decimals": 8,
"source": "site/economics.html: an earlier devnet paid in 8 decimals"
}
}
}

View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# TV-02 implementation A, on its build box (build-5), never on the Mac. Records its pid file before any work and
# removes it on exit. Usage: run-a.sh <repo root on the box> <out dir> [build|straight <net>|reorg <fixture>|genesis]
# The node fork is read from <repo>/vendor/igneum-node at the commit checked out there (the run prints it).
set -euo pipefail
REPO="$1"; OUT="$2"; shift 2
PID="$OUT/run-a.pid"
mkdir -p "$OUT"
echo "$$ $(date -u +%FT%TZ) tv02-a $*" > "$PID"
trap 'rm -f "$PID"' EXIT
export PATH="$HOME/.cargo/bin:$PATH"
CRATE="$REPO/tools/token-value/tv-02/replay-node"
export TV02_NODE_COMMIT="$(git -C "$REPO/vendor/igneum-node" rev-parse HEAD)"
BIN="$CRATE/target/release/tv02-replay-node"
echo "node commit $TV02_NODE_COMMIT"
case "$1" in
build)
(cd "$CRATE" && cargo build --release 2>&1)
sha256sum "$BIN" ;;
straight)
"$BIN" straight "$2" "$OUT/a-$2.csv" "$OUT/a-$2-labels.csv" "$OUT/a-$2-summary.json" ;;
reorg)
id="$(basename "$2" .json)"
"$BIN" reorg "$2" "$OUT/a-reorg-$id.csv" ;;
genesis)
"$BIN" genesis-payload "$OUT/a-genesis-payload.json" ;;
*) echo "unknown command $1" >&2; exit 2 ;;
esac

View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# TV-02 implementation B, on its build box (build-6), never on the Mac. Records its pid file before any work and
# removes it on exit. Usage: run-b.sh <tv-02 dir on the box> <out dir> <rules.json> [selftest|straight <net>|reorg <fixture>]
# Output names carry the rules id's tag (the third argument's basename) so runs on different rules never overwrite.
set -euo pipefail
DIR="$1"; OUT="$2"; RULES="$3"; shift 3
PID="$OUT/run-b.pid"
mkdir -p "$OUT"
echo "$$ $(date -u +%FT%TZ) tv02-b $*" > "$PID"
trap 'rm -f "$PID"' EXIT
TAG="$(basename "$RULES" .json)"
echo "rules $RULES sha256 $(sha256sum "$RULES" | cut -c1-64)"
echo "replay_spec.py sha256 $(sha256sum "$DIR/replay-spec/replay_spec.py" | cut -c1-64)"
case "$1" in
selftest) python3 "$DIR/replay-spec/replay_spec.py" selftest; python3 "$DIR/compare.py" selftest ;;
straight) python3 "$DIR/replay-spec/replay_spec.py" straight "$RULES" "$2" "$OUT/b-$TAG-$2.csv" "$OUT/b-$TAG-$2-labels.csv" "$OUT/b-$TAG-$2-summary.json" ;;
reorg) id="$(basename "$2" .json)"; python3 "$DIR/replay-spec/replay_spec.py" reorg "$RULES" "$2" "$OUT/b-$TAG-reorg-$id.csv" ;;
*) echo "unknown command $1" >&2; exit 2 ;;
esac

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""TV-02: the README's result tables, generated from a replay CSV and its labels (A's and B's CSVs are byte-identical
when the comparison reads EQUAL, so either serves). Prints Markdown rows: the transitions' at-rows (the first block
of each new subsidy), the ramp's end and the terminal.
table.py <replay.csv> <labels.csv> <decimals>
"""
import csv
import sys
def ign(units, decimals):
q, r = divmod(units, 10 ** decimals)
return "{:,}".format(q) + ("." + str(r).rjust(decimals, "0").rstrip("0") if r else "")
def main(path, labels_path, decimals):
rows = {int(r["height"]): r for r in csv.DictReader(open(path))}
labels = {int(r["height"]): r["label"] for r in csv.DictReader(open(labels_path))}
print("| height | DAA score | rows' label | subsidy (base units) | issued supply (base units) | issued supply (IGN) |")
print("|---|---|---|---|---|---|")
for h in sorted(rows):
l = labels.get(h, "")
if not (h == 0 or "at ramp end" in l or "at step" in l or "terminal" in l or "past the terminal" in l):
continue
r = rows[h]
print("| {:,} | {:,} | {} | {} | {} | {} |".format(h, int(r["daa_score"]), l, r["subsidy"], r["cumulative"], ign(int(r["cumulative"]), decimals)))
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2], int(sys.argv[3]))

View file

@ -332,8 +332,10 @@ function renderCrew() {
for (const pc of pcs) {
const r = pc.running;
const pcAge = pc.report_age_s === undefined || pc.report_age_s === null ? pcsAge : pc.report_age_s;
const pcAgeText = pcAge === null ? 'report age unknown' : `report ${fmtDurShort(pcAge)} old${pcAge > 600 ? ', STALE' : ''}${pc.source === 'intake' ? ', from the PC' : ''}`;
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(pc.name)}</div><div class="meta">${esc(pc.machine || pc.id)} · ${esc(pc.role)} · <span ${pcAge !== null && pcAge > 600 ? 'style="color:var(--warn)"' : ''}>${esc(pcAgeText)}</span></div></div>${pill(r ? 'running' : pc.queue.length ? 'queued' : 'idle', r ? 'running' : pc.queue.length ? 'queued' : '')}</div><div class="doing">${pcReportLine(pc) ? pcReportLine(pc) + '<br>' : ''}${r ? `${esc(r.kind)} ${esc(r.job)}${r.title ? '<br>' + esc(r.title) : ''}${r.stage ? `<br><span class="pill">stage ${esc(r.stage)}</span>` : ''}` : pc.queue.length ? `Next: ${esc(pc.queue[0].kind)} ${esc(pc.queue[0].job)}${pc.queue[0].title ? ', ' + esc(pc.queue[0].title) : ''}` : esc(pc.note || 'idle on jobs; the relay polls every 10 min')}</div><div class="foot"><span>${pc.recent.length} reports kept</span><span>${pc.last_report_at ? 'last report ' + esc(ago(pc.last_report_at)) : 'no report in 7 days'}</span></div></div>`);
// a rig card reads "agent not reporting since <time>" until a report arrives through the intake (the coordinator, 9 October 2026)
const pcLast = pc.collected_at || (D.pcs && D.pcs.collected_at) || null;
const pcAgeText = pc.source === 'intake' ? (pcAge === null ? 'report age unknown' : `report ${fmtDurShort(pcAge)} old${pcAge > 600 ? ', STALE' : ''}, from the rig`) : pc.source === 'none' ? 'no report yet' : `agent not reporting since ${pcLast ? esc(ukTime(pcLast)) : 'an unknown time'}`;
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(pc.name)}</div><div class="meta">${esc(pc.machine || pc.id)} · ${esc(pc.role)} · <span ${(pc.source !== 'intake') || (pcAge !== null && pcAge > 600) ? 'style="color:var(--warn)"' : ''}>${pcAgeText}</span></div></div>${pill(r ? 'running' : pc.queue.length ? 'queued' : 'idle', r ? 'running' : pc.queue.length ? 'queued' : '')}</div><div class="doing">${pcReportLine(pc) ? pcReportLine(pc) + '<br>' : ''}${r ? `${esc(r.kind)} ${esc(r.job)}${r.title ? '<br>' + esc(r.title) : ''}${r.stage ? `<br><span class="pill">stage ${esc(r.stage)}</span>` : ''}` : pc.queue.length ? `Next: ${esc(pc.queue[0].kind)} ${esc(pc.queue[0].job)}${pc.queue[0].title ? ', ' + esc(pc.queue[0].title) : ''}` : esc(pc.note || 'idle on jobs; the relay polls every 10 min')}</div><div class="foot"><span>${pc.recent.length} reports kept</span><span>${pc.last_report_at ? 'last report ' + esc(ago(pc.last_report_at)) : 'no report in 7 days'}</span></div></div>`);
}
if (!pcs.length) cards.push(`<div class="w"><div class="top"><div><div class="card">rig 1 and rig 2</div><div class="meta">relay jobs</div></div>${pill('no data', 'error')}</div><div class="doing">${esc(D.sources && D.sources.pcs && D.sources.pcs.error || 'the Mac pusher reads the relay intake; nothing has arrived')}</div></div>`);
$('crew').innerHTML = cards.join('');