diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea5ce0451..9879a98b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,11 @@ jobs: run: bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh - name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors) run: node --test site/api/faucet.test.mjs + - name: explorer and public stats unit tests (search router, formatters, emission rule against the node's own test values, the documented API fields from a fixture) + run: node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/api/public-stats.test.mjs + - name: public stats API answers with the documented fields (the live site; master only, the endpoints exist there after the merge) + if: github.ref == 'refs/heads/master' + run: node tools/ci/public-api-check.mjs https://igneum.network - name: ship tool self-test (version bump, the dl-both and public manifest helpers) run: node tools/ship-app.mjs --self-test - name: relay unit tests (parsers, secret compare, the wake endpoint) diff --git a/docs/analysis/card-lifetime-2026-10-05.md b/docs/analysis/card-lifetime-2026-10-05.md new file mode 100644 index 000000000..4a6d565a2 --- /dev/null +++ b/docs/analysis/card-lifetime-2026-10-05.md @@ -0,0 +1,85 @@ +# Card lifetime per tier: how many years a card keeps mining + +5 October 2026. Consequences review, sub-agent of the consequences reviewer. Desk arithmetic only; nothing was run. + +## 1. Inputs + +| Input | Source | Value used | +|---|---|---| +| Dataset schedule | `docs/spec/01-lottery-hash.md` 432 to 437 | 2 GiB at genesis plus 0.5 GiB a year (2,048 + 512 x years MiB) | +| Index mapping (a) | same file, line 442 | multiply-shift: the dataset grows every day, continuous | +| Index mapping (b) | same file, line 442 | power-of-two steps 2, 4, 8 GiB on the schedule's average: 4 GiB at year 4, 8 GiB at year 12; my extrapolation: 16 GiB at year 28, 32 GiB at year 60 | +| Scratch per resident warp | `igneum-wt-ca2-cache/docs/plans/hot-table.md` 66 to 73 | 32 or 128 KiB per warp; 5090 = 170 SMs x 48 warps = 8,160 (approximate, from memory) | +| Hot table, buffers | same file, 70 | hot table 32, 64 or 96 MiB (96 used here); buffers 128 MiB | +| Cache | hot-table.md 70 (resident, 256 MiB in every total) against `igneum-wt-ca2-era/docs/plans/era-layout.md` 93 ("resident only while the day's dataset is built, then free") | both readings carried: resident = worst case, freed = best case. The two plans disagree and gate 1 should say which | +| Cache growth | `igneum-wt-ca2-coord/docs/plans/counter-asic-2-status.md` 79 (layer 6 option C) | 256 MiB at genesis, 512 MiB at year 4, 1 GiB at year 12; by the same rule 2 GiB at year 28, 4 GiB at year 60 | +| Budget rule | same file, 17: the whole working set stays under 6 GB on an 8 GB card | my reading: 75% of card memory at every tier. Apple: 50% of unified memory, because macOS, the display and the node share it; that share is my assumption | +| Public claims | `site/index.html` 443, 461; `site/litepaper.html` 560; `docs/evidence.md` | quoted in Table 3. evidence.md has no row on card lifetime | + +Card memory is binary (8 GB = 8,192 MiB). The hot-table row "An 8 GB card at 5090 occupancy" (line 73) counts 8,160 warps; a real 8 GB card has 20 to 24 SMs, so its scratch is about a tenth of that row. Resident warps below are SMs x 48 (NVIDIA Ampere and later), SM counts from memory, approximate; Apple uses the 2,048 warps the Metal harness launches (hot-table.md 66). + +## 2. Table 1: non-dataset working set per tier (MiB) + +Worst = scratch 128 KiB, cache resident. Columns g / y4 / y12 = genesis, year 4, year 12 (the cache doublings). Freed = era-layout's reading, constant over the years. + +| Tier | Card assumed (SMs, approximate) | Warps | Scratch 128 KiB | Scratch 32 KiB | Cache resident, 128 KiB: g / y4 / y12 | Cache resident, 32 KiB: g / y4 / y12 | Cache freed: 128 / 32 KiB | +|---|---|---|---|---|---|---|---| +| 4 GB | GTX 1650 (14 SMs x 32 warps, Turing) | 448 | 56 | 14 | 536 / 792 / 1,304 | 494 / 750 / 1,262 | 280 / 238 | +| 8 GB | RTX 3050 (20) | 960 | 120 | 30 | 600 / 856 / 1,368 | 510 / 766 / 1,278 | 344 / 254 | +| 12 GB | RTX 3060 (28) | 1,344 | 168 | 42 | 648 / 904 / 1,416 | 522 / 778 / 1,290 | 392 / 266 | +| 16 GB | RTX 5060 Ti (36) | 1,728 | 216 | 54 | 696 / 952 / 1,464 | 534 / 790 / 1,302 | 440 / 278 | +| 24 GB | RTX 4090 (128) | 6,144 | 768 | 192 | 1,248 / 1,504 / 2,016 | 672 / 928 / 1,440 | 992 / 416 | +| 32 GB | RTX 5090 (170) | 8,160 | 1,020 | 255 | 1,500 / 1,756 / 2,268 | 735 / 991 / 1,503 | 1,244 / 479 | +| Apple 8 to 64 GB | M-series, harness launch count | 2,048 | 256 | 64 | 736 / 992 / 1,504 | 544 / 800 / 1,312 | 480 / 288 | + +Every row = scratch + 96 (hot table) + 128 (buffers) + cache (256 / 512 / 1,024 when resident). The freed reading still peaks at dataset + cache during the daily build, but that peak is smaller than the resident total whenever hashing pauses for the build, so the freed column is the steady-state set. + +## 3. Table 2: dataset room and the year the dataset outgrows it + +Room = usable memory (75%, Apple 50%) minus Table 1. Worst = 128 KiB scratch, cache resident (room shrinks at years 4, 12, 28, 60). Best = 32 KiB scratch, cache freed. Option (a): the year 2,048 + 512 x y exceeds the room. Option (b): the first step the room cannot hold; the card mines up to that day. + +| Tier | Usable MiB (share) | Room at genesis, worst / best | (a) ends, years, worst / best | (b) ends, year, worst / best | +|---|---|---|---|---| +| 4 GB | 3,072 (75%) | 2,536 / 2,834 | 1.0 / 1.5 | 4 / 4 | +| 8 GB | 6,144 (75%) | 5,544 / 5,890 | 6.3 / 7.5 | 12 / 12 | +| 12 GB | 9,216 (75%) | 8,568 / 8,950 | 12.0 / 13.5 | 12 / 28 | +| 16 GB | 12,288 (75%) | 11,592 / 12,010 | 17.1 / 19.5 | 28 / 28 | +| 24 GB | 18,432 (75%) | 17,184 / 18,016 | 28.0 / 31.2 | 28 / 60 | +| 32 GB | 24,576 (75%) | 23,076 / 24,097 | 37.6 / 43.1 | 60 / 60 | +| Apple 8 GB | 4,096 (50%) | 3,360 / 3,808 | 2.6 / 3.4 | 4 / 4 | +| Apple 16 GB | 8,192 (50%) | 7,456 / 7,904 | 10.1 / 11.4 | 12 / 12 | +| Apple 32 GB | 16,384 (50%) | 15,648 / 16,096 | 25.1 / 27.4 | 28 / 28 | +| Apple 64 GB | 32,768 (50%) | 32,032 / 32,480 | 55.1 / 59.4 | 60 / 60 | + +What the table says per tier: + +| Tier | Reading | +|---|---| +| 4 GB | Mines at genesis with 488 to 786 MiB spare. Under (a) it is out within 1 to 1.5 years. Under (b) it lasts to the year-4 step, as the spec's own remark says (line 442) | +| 8 GB | 6 to 7.5 years under (a). 12 years under (b): "more than a decade" is true only under (b), and only just | +| 12 GB | The year-12 cache doubling (1 GiB resident) is what ends it, under both options, if the cache stays resident. With the cache freed it reaches year 28 under (b). This tier's lifetime is decided by the cache residency question, not by the dataset | +| 16 GB | 17 to 19.5 years under (a), year 28 under (b) | +| 24 GB | Under the resident reading the year-28 cache doubling (2 GiB) ends it the same day under both options. Freed: 31 years or year 60 | +| 32 GB | 38 to 43 years under (a), year 60 under (b). Not a constraint for any plan | +| Apple 8 GB | 2.6 to 3.4 years under (a), year 4 under (b). The base MacBook Air is a short-lived miner | +| Apple 16 GB | 10 to 11.4 years under (a), year 12 under (b): the same shape as an 8 GB card | +| Apple 32 / 64 GB | 25 years and 55 years or more. No constraint | + +Proving is a separate budget (the 15.6 GB peak the 12 GB mine-and-prove question came from); this file covers mining only. + +## 4. Table 3: the public sentences against the numbers + +| Where | Sentence now | What the tables give | Proposed sentence (the project lead decides the wording) | +|---|---|---|---| +| `site/index.html` 443 | Memory: "2 GB, fixed" (RandomX) / "2 GB, growing" (Igneum) | 2 GiB at genesis, plus 0.5 GiB a year on average under either option | "2 GB, growing 0.5 GB a year". The row is right; the rate is the useful addition | +| `site/index.html` 461 | "Any 4 GB card, approximate." | True at genesis (2,584 to 2,834 MiB of a 3,072 MiB budget). Ends at 1 to 1.5 years under (a), year 4 under (b) | "Any 4 GB card at launch, 8 GB for the long run, approximate." | +| `site/litepaper.html` 560 | "a 4 GB card mines for about four years and an 8 GB card for more than a decade, approximate." | 4 GB: 1 to 1.5 years (a) or 4 years (b). 8 GB: 6.3 to 7.5 years (a) or 12 years (b). Both numbers hold only under option (b) | If gate 1 picks (b): "a 4 GB card mines until the first dataset step at year 4, an 8 GB card until the second at year 12 and a 16 GB card until year 28, approximate." If (a): "a 4 GB card mines for about a year, an 8 GB card for about seven and a 16 GB card for about seventeen, approximate." | +| `site/litepaper.html` 560 | "12 GB or more proves full shards." | Not a lifetime claim; left as is. For mining, 12 GB lasts 12 years with the cache resident, year 28 with it freed under (b) | No change from this file | +| `docs/evidence.md` | No row on card lifetime | The litepaper sentence is a public claim with no row | Add a row, label "designed", sources: spec 1.13.3 and this file; status moves to "tested" once a 4 GB and an 8 GB card run the genesis working set under the cap | + +## 5. Reading + +- The two index-mapping options end on the same day where a cache doubling takes the last of the room. With the cache resident that is the 12 GB tier at year 12 and the 24 GB tier at year 28 (Table 2, worst column). Under option (b) every tier ends on a step day by construction, so a tier ends on the same day under both options exactly when option (a) also ends it on a doubling day. +- Everywhere else option (b) is kinder: 4 GB gains about 2.5 years, 8 GB about 5, 16 GB about 10. The site and litepaper numbers are option (b) numbers. If gate 1 picks (a), both public sentences are wrong today by 2.5 to 5 years. +- The cache residency disagreement (hot-table.md 70 against era-layout.md 93) decides the 12 GB tier's lifetime (12 against 28 years) and nothing else. It should be settled at gate 1 beside the mapping choice. +- The 75% rule is my generalisation of "under 6 GB on an 8 GB card"; at 4 GB it leaves 1 GB for the driver and the display, which a headless rig would not need. A 4 GB card on a bare Linux rig might hold out to year 2 under (a). Not measured. diff --git a/docs/api/public-stats.md b/docs/api/public-stats.md new file mode 100644 index 000000000..d41fb08ae --- /dev/null +++ b/docs/api/public-stats.md @@ -0,0 +1,227 @@ +# Public stats API + +5 October 2026. Two JSON endpoints on the site for profitability sites, pool software and anyone who wants the +network numbers without running a node: `/api/stats` and `/api/supply`. WhatToMine's listing form asks for an +explorer or pool with an API, the reward halving schedule and a source to fetch total coins from; this is that source. +A third endpoint, `/api/explorer`, feeds the explorer pages (docs/plans/explorer.md). + +Both are served by Vercel functions (`site/api/stats.mjs`, `site/api/supply.mjs`) that read what the devnet observer +(`tools/observer/observer.mjs`) wrote to Neon; no secret is involved beyond the database connection the site already +holds. Cached 10 s at the edge (`Cache-Control: public, max-age=10, s-maxage=10`), CORS open (`Access-Control-Allow-Origin: *`), +GET only. A failed read answers 500 with `{ok: false, error}` and `Cache-Control: no-store`. + +The contract is the `FIELDS` list exported by each handler. `site/api/public-stats.test.mjs` checks a fixture against +it without a database, and `tools/ci/public-api-check.mjs ` checks a deployment (CI runs it against +https://igneum.network on master). + +## /api/stats + +| Field | Meaning | Source | +|---|---|---| +| `network`, `chain_id`, `node_version` | Network name, EVM chain id (4461 mainnet, 4462 testnet, 4463 devnet, design 8.1), the node's version | observer `live_state` | +| `algorithm` | The lottery hash, named | fixed text | +| `stale`, `age_s`, `observer_updated_at` | `stale` when the observer has not written for 30 s; treat every number as last known then | observer | +| `height` | The chain block number (the EVM block number): the number of the newest chain block the observer has a shard plan for | `live_blocks.number` | +| `block_count`, `header_count` | Every DAG block the node holds | `getBlockDagInfo` | +| `daa`, `blue_score` | DAA score and blue score of the newest block | `live_blocks`, `getSinkBlueScore` | +| `difficulty` | The node's difficulty (target per block) | `getBlockDagInfo` | +| `hashrate`, `hashrate_unit`, `hashrate_source` | H/s: the node's `estimateNetworkHashesPerSecond` over 1,000 blocks, else blue work added per second over 10 min | observer | +| `block_time_target_s`, `block_time_measured_s` | 1 s by design (spec 2.1); 60 / DAG blocks in the last 60 s | observer | +| `blocks_per_day_target`, `blocks_per_day_measured` | 86,400; the last hour's DAG blocks x 24 (null until the observer has an hour) | observer | +| `block_reward` | `E(daa)` of spec 2.5 at the newest DAA score: `sompi` (8 decimals), `ign`, the 80% `miner_ign` and 20% `proving_pool_ign`, `ramp_factor`, `halving_period`, `next_halving_daa`, `next_halving_in_s` | `site/lib/emission.mjs` | +| `last_block` | Hash, time, age, blue score, DAA, coinbase address, vote key id, EVM transaction count, whether it is a chain block | `live_blocks` | +| `finality` | Finality v2: active, the latest locked checkpoint index and blue score | observer | +| `peers`, `mempool`, `miners_10m` | Connected peers, mempool size, distinct vote keys in 10 min (a card runs several) | observer | + +Note for a profitability calculator: `block_reward` is per blue block merged; at the 1 block per second target that is +also the reward per DAA second. The 20% proving-pool part is paid to provers, not to the miner of the block, so a +miner's expected income per block is `miner_ign`. During the 30-day launch ramp the reward climbs from 10% to 100% +of the schedule (`ramp_factor`); `/api/supply` shows where the ramp stands. + +Example, the devnet on 5 October 2026 (through the local preview against a test observer, so `block_time_measured_s` +reflects a one-minute window): + +``` +{ + "ok": true, + "now": "2026-10-05T19:32:09.321Z", + "network": "igneum-devnet", + "chain_id": 4463, + "node_version": "2.1.0", + "algorithm": "Igneum lottery hash: random-program GPU hash, new program every hour, generator v2 (docs/spec/01-lottery-hash.md)", + "stale": false, + "age_s": 0.9, + "height": 82145, + "block_count": 126358, + "header_count": 126358, + "daa": 126357, + "blue_score": 123504, + "difficulty": 125543017.00697394, + "hashrate": 258756880, + "hashrate_unit": "H/s", + "hashrate_source": "the node's estimateNetworkHashesPerSecond over a 1,000-block window; blue work added per second over 10 min when the node refuses the window", + "block_time_target_s": 1, + "block_time_measured_s": 0.952, + "blocks_per_day_target": 86400, + "blocks_per_day_measured": 14040, + "block_reward": { + "sompi": "455909062", + "ign": "4.55909062", + "miner_ign": "3.6472725", + "proving_pool_ign": "0.91181812", + "split": "80% block producer, 20% proving pool", + "daa_used": 126357, + "ramp_factor": 0.143874, + "halving_period": 0, + "next_halving_daa": 63115200, + "next_halving_in_s": 62988843 + }, + "last_block": { + "hash": "bbec3139d3f38e3517716374707998e77f536a67813edfb619c5bed93a5779ab", + "time": "2026-10-05T19:32:06.157Z", + "ts_ms": 1791228726157, + "age_s": 3.2, + "blue_score": 123504, + "daa": 126357, + "miner": "igneumdev:qrt8nzgrghr2a2xuc7lstzclcnt3n632d2chhc946rphkc6flcyswvkrym8s4", + "miner_id": "7b8ef6fd", + "tx_count": 0, + "chain": true + }, + "finality": { + "active": true, + "latest_locked_index": 4116, + "latest_locked_blue_score": 123480, + "chain_id": "igneum-devnet" + }, + "peers": 4, + "mempool": 0, + "miners_10m": 21, + "observer_updated_at": "2026-10-05T19:32:08.394726+00:00", + "source": "tools/observer reading one node every 2 s; reward from docs/spec/02-consensus.md 2.5 at the node's DAA score" +} +``` + +## /api/supply + +| Field | Meaning | +|---|---| +| `unit` | IGN; the coinbase pays in 8-decimal units (open item O-2.6), the EVM shows 18 | +| `daa` | The newest DAA score the observer stored | +| `max_supply_ign` | 4,000,000,000, the hard cap (spec 2.5, no tail emission: spec 5.10) | +| `circulating_ign`, `circulating_sompi` | Minted so far by the rule: `E(t)` summed over every DAA second from 0 to `daa`, exact (floor sum, `mintedByRule`) | +| `minted_at_end_ign`, `never_minted_ign` | What the schedule reaches when the per-second rate hits 0 (period 32), and the part of the cap the ramp and the floors never mint | +| `emission_per_second_ign`, `block_reward_ign` | `E(daa)` now | +| `halving` | Interval 63,115,200 DAA s (two years), current period, the next halving's DAA score, seconds to it, a date estimate at one DAA second per second | +| `ramp` | 10% at genesis to 100% at DAA 2,592,000 (30 days), the factor now, whether it is complete | +| `schedule` | 33 rows: period, start and end DAA, years from genesis, IGN per second, IGN per block at 1 BPS, minted by the end of the period, share of the cap | +| `check` | The observer's hourly comparison of the chain against the rule over its newest 500 blocks: `rule_match` counts blocks whose coinbase payload declares exactly `E(daa)`; `sum_match` counts blocks whose coinbase outputs equal the declared subsidies of the blocks they merge; `examples` names mismatches | +| `rule`, `source`, `note` | The formula, where it lives, and the caveat: the chain pays per block merged, so a block rate above target mints above the schedule for as long as it lasts | + +Example (schedule cut to five rows here): + +``` +{ + "ok": true, + "now": "2026-10-05T19:32:09.374Z", + "network": "igneum-devnet", + "chain_id": 4463, + "unit": { + "symbol": "IGN", + "decimals_consensus": 8, + "decimals_evm": 18, + "note": "the coinbase pays in 8-decimal units (open item O-2.6 keeps Kaspa's SOMPI_PER_KASPA); the EVM shows the same amount at 18 decimals" + }, + "daa": 126357, + "max_supply_ign": "4000000000", + "circulating_ign": "488236.39686436", + "circulating_sompi": "48823639686436", + "minted_at_end_ign": "3963038988.86765648", + "never_minted_ign": "36961011.13234352", + "emission_per_second_ign": "4.55909062", + "block_reward_ign": "4.55909062", + "halving": { + "interval_daa_s": 63115200, + "interval_years": 2, + "period": 0, + "next_halving_daa": 63115200, + "next_halving_in_s": 62988843, + "next_halving_estimate": "2028-10-03T20:26:12.374Z", + "estimate_note": "the estimate assumes one DAA second per wall-clock second from now" + }, + "ramp": { + "start_percent": 10, + "length_daa_s": 2592000, + "length_days": 30, + "factor_now": 0.143874, + "complete": false, + "remaining_s": 2465643, + "withheld_ign": "36961011.13234352", + "withheld_note": "the ramp withholds about 37 million IGN that are never minted; integer floors withhold the rest (spec 2.5)" + }, + "schedule": [ + { + "period": 0, + "start_daa": 0, + "end_daa": 63115200, + "years_from_genesis": "0 to 2", + "per_second_ign": "31.68808781", + "per_block_ign_at_1bps": "31.68808781", + "minted_by_end_ign": "1963038999.85152848", + "share_of_cap_by_end": 49.0759 + }, + { + "period": 1, + "start_daa": 63115200, + "end_daa": 126230400, + "years_from_genesis": "2 to 4", + "per_second_ign": "15.8440439", + "per_block_ign_at_1bps": "15.8440439", + "minted_by_end_ign": "2963038999.40880848", + "share_of_cap_by_end": 74.0759 + }, + { + "period": 2, + "start_daa": 126230400, + "end_daa": 189345600, + "years_from_genesis": "4 to 6", + "per_second_ign": "7.92202195", + "per_block_ign_at_1bps": "7.92202195", + "minted_by_end_ign": "3463038999.18744848", + "share_of_cap_by_end": 86.5759 + }, + "... 29 more rows ...", + { + "period": 32, + "start_daa": 2019686400, + "end_daa": 2082801600, + "years_from_genesis": "64 to 66", + "per_second_ign": "0", + "per_block_ign_at_1bps": "0", + "minted_by_end_ign": "3963038988.86765648", + "share_of_cap_by_end": 99.0759 + } + ], + "check": { + "bps": 1, + "sampled": 470, + "examples": [], + "sum_match": 466, + "checked_at": "2026-10-05T19:30:28.385Z", + "rule_match": 470, + "sum_skipped": 4, + "sum_mismatch": 0, + "rule_mismatch": 0 + }, + "rule": "E(t) = ramp(t) * floor(10^9 * UNIT / 31,557,600) >> floor(t / 63,115,200); ramp(t) = min(1, 1/10 + 9/10 * t / 2,592,000); t = DAA score / bps, bps = 1", + "source": "docs/spec/02-consensus.md 2.5; vendor/igneum-node consensus/core/src/igneum.rs block_subsidy and launch_ramp; circulating = the rule summed over every DAA second from 0 to the node's DAA score (site/lib/emission.mjs mintedByRule, exact)", + "note": "circulating is the schedule at this DAA score. The chain pays E per blue block it merges and per red inside the DAA window, which tracks the schedule one block per DAA step; the devnet of 3 October 2026 ran 4.7x the schedule for eight minutes during a retarget lag (spec 2.5). check reports what the observer measured on the newest blocks." +} +``` + +## /api/explorer + +The explorer's own feed, cached 5 s: `?blocks=N[&before=ms]` (latest blocks), `?block=hash`, `?height=N`, +`?address=0x..|igneumdev:..`, `?search=q`. Shapes are in `site/api/explorer.mjs`; the pages are the reference client. +Balances need `EXPLORER_EVM_RPC` on the deployment (a public EVM JSON-RPC); without it `balance.available` is false +with the reason. diff --git a/docs/plans/counter-asic-2.md b/docs/plans/counter-asic-2.md new file mode 100644 index 000000000..f6598d19b --- /dev/null +++ b/docs/plans/counter-asic-2.md @@ -0,0 +1,33 @@ +# Counter ASIC 2.0 + +Internal name, set by the project lead on 5 October 2026 (evening), for the second set of chip-resistance layers on the lottery hash. The first set is live: a random program every hour from a VDF seed, era parameters drawn every six months, a dataset that grows on a genesis schedule, instruction families that unlock by height, warp-unit CPU verification. The public claim stays as it is: a chip gains under 2x over a GPU, the model is published, the bounty stands. Nothing here is active; every layer is measured first and decided by the project lead, and every one that goes in goes in before the public testnet, as a genesis rule or a reserved family. + +Trigger: the 9070 XT measurement of 5 October (bench-log "the 9070 XT on the eGPU"): the hash is bound by dependent random 4-byte reads, AMD fetches a 64-byte line per read, so AMD sits at a seventh of the 5090 and the three-vendor claim is bit-exact but not fair. Read-only data can also be mirrored into a chip's SRAM (256 MB is about 45 mm2 at a leading node, approximate). + +## The layers + +| # | Layer | What it takes from a chip | GPU cost | Step | +|---|---|---|---|---| +| 1 | Load width 4, 16 or 64 bytes, every byte folded into the state | a fixed-width memory pipeline tuned to 4-byte reads | none if latency-bound stays | measure (running) | +| 2 | Load width drawn per program from the seed, era-fixed mix | any fixed-width pipeline; vendor balance averages across hours | none | measure (running) | +| 3 | Per-warp scratch in VRAM with read-modify-writes | the SRAM mirror of read-only data buys nothing for written scratch | measured share, expected 10 to 15% at 25% RMW (approximate) | measure (running), then soundness | +| 4 | Table layout drawn per era: item size, stride, interleave | a layout-tuned chip loses at the next draw | none | design, era draw exists | +| 5 | A second table sized to GPU cache, read beside the 1 GiB table | GPU-class SRAM and DRAM latency at once | small | next experiment | +| 6 | Cache growth on the genesis schedule | the SRAM mirror stays unaffordable | none | already in the design; confirm the schedule against SRAM density | +| 7 | Integer matrix ops in the program (INT8 x INT8 into INT32, exact) | matrix hardware at GPU scale | none on NVIDIA and AMD; Apple to check | reserved family, not at launch | +| 8 | Working-set size drawn per program | one memory design cannot fit every hour | none | folds into 4 and 5 | + +Not added: divergent data-dependent branches (cost GPUs more than chips), anything floating point (bit-exactness across vendors). + +## The plan, in order + +1. Experiment on branch `readwidth` (running): variants 1, 2, 3 on the M5 Max, the RTX 5090 and the RX 9070 XT; hash rate, bit-exactness across Metal, CUDA and OpenCL, CPU verifier cost, bytes per hash, latency-bound share, the chip model re-run per variant. Table in the bench log, recommendation in `docs/plans/read-width.md`. Decision rule: the widest read that keeps every card latency-bound with margin on the 5090. +2. Decision 1 (the project lead): the width and whether the per-program mix goes in. Consequences: new test vectors, new program id, spec sections on the hash and the litepaper Mining section rewritten, the soundness checks (uniformity, no out-of-bounds, fuzz) re-run on the new class. +3. Experiment 2: layer 5 (the cache-sized second table) on the three cards, same measurements, plus layer 6's schedule checked against SRAM density per node (cite the source). +4. Soundness project for layer 3 with the cryptographer role: what is written is uniform, no short-cut avoids the writes, the verifier's scratch simulation is exact; only then a vector. +5. Decision 2 (the project lead): layers 3, 5 and the era draws of 4 and 8, as genesis rules; layer 7 as a named reserved family in the genesis reserve, unlockable by height or by 90% signal. +6. One generator change ships them all at once, before the public testnet, with the chip model and the before-and-after numbers published beside the litepaper claim. + +## What stays true at every step + +The latency bound is the property that matters, because DRAM latency is the same physics for everyone and a GPU already keeps thousands of loads in flight. Bandwidth is the property to avoid leaning on, because bandwidth per watt is what a custom memory chip buys (Ethash's chips got about 3x that way). Every layer above is checked against that rule by the latency-bound share in the measurements. diff --git a/docs/plans/explorer.md b/docs/plans/explorer.md new file mode 100644 index 000000000..29b0ac4c5 --- /dev/null +++ b/docs/plans/explorer.md @@ -0,0 +1,129 @@ +# Explorer: plan and recommendation + +5 October 2026, after the project lead read WhatToMine's listing requirements ("build it, and do we build our own explorer? who +built etherscan?"). Branch `explorer`. What exists tonight: the public stats API (`docs/api/public-stats.md`) and the +first DAG explorer pages on the site, fed by the observer. What is recommended: Blockscout for the EVM side, our own +DAG and mining pages, one shared search box. + +## 1. Who built Etherscan, and the open-source routes + +| Explorer | What it is | Licence and cost | Fit for Igneum | +|---|---|---|---| +| Etherscan | Built and launched in 2015 by Matthew Tan (CEO and founder); the office since January 2017 (etherscan.io/aboutus, read 5 October 2026; the page does not name the city, the project lead's brief says Kuala Lumpur). Since 2020 it sells "Explorer as a Service", a white-label instance for other chains, 40 clients by 2025 (same page) | Closed source, a private company; a chain pays for an instance. Price not published; not asked | Not for us: closed, paid, and it would show nothing of the DAG, the finality or the proving layer | +| Blockscout | Open-source EVM explorer: blocks, transactions, accounts, verified contracts, token pages, an API in Etherscan's shape. Elixir (Phoenix) backend, PostgreSQL, a separate frontend; "several hundred chains and rollups" use it (README, read 5 October 2026) | "Blockscout Software Licence" (the README badge; the licence text was not read line by line, so what it permits for a hosted instance is unverified) | The EVM side for free: contracts, transactions, logs, tokens, an API developers already know | +| Otterscan | "open-source, fast, local, laptop-friendly Ethereum block explorer": a React app over an Erigon archive node, using Erigon's custom `ots_` JSON-RPC methods (github.com/otterscan/otterscan, read 5 October 2026) | MIT (the app); the `ots_` API lives inside Erigon under its licence | Not for us: it needs Erigon's RPC extensions, which the Igneum node does not have, and it has no contract verification | + +## 2. What Blockscout gives us for free, and what it costs to run + +Blockscout indexes through standard JSON-RPC. Its documented requirements (docs.blockscout.com, read 5 October 2026): + +| Item | Blockscout's figure | Igneum node today (`vendor/igneum-node` 0.3.6 fork, `igneum/exec/src/rpc.rs`, 938 lines) | +|---|---|---| +| Software | Erlang/OTP 26, Elixir 1.15.x, Postgres 14+, Node.js 18.x.x (docs: setup/requirements/requirements) | n/a | +| Hardware, the docs' base line | "16 core, 32 thread", "128GB" RAM; the AWS example is one m5a.xlarge (4 vCPU, 16 GB) application server with 8 GB EBS and one db.t3.large RDS Postgres 14+ with 500 GB "depending on chain size" (docs: setup/requirements/resource-requirements) | A devnet at 0 EVM transactions per block needs nothing like the base line; the AWS example is the honest size for a small chain | +| Database | Ethereum mainnet 21,000 GiB, Sepolia 5,200 GiB, Ethereum Classic 555 GiB, Gnosis Chiado 470 GiB (docs: setup/requirements/database-storage-requirements, figures dated 23 December 2024) | Unmeasured for Igneum. At one chain block per second with empty blocks the row count is 86,400 blocks a day; the byte size per block is the thing to measure in the first week | +| RPC it needs from every client | `eth_blockNumber`, `eth_call`, `eth_getBalance`, `eth_getCode`, `eth_getBlockByHash`, `eth_getBlockByNumber`, `eth_getTransactionByHash`, `eth_getTransactionByBlockHashAndIndex`, `eth_getTransactionByBlockNumberAndIndex`, `eth_getTransactionReceipt`, `eth_getUncleByBlockHashAndIndex`, `eth_getLogs` (docs: setup/requirements/node-tracing-json-rpc-requirements) | The fork answers `eth_blockNumber`, `eth_call`, `eth_getBalance`, `eth_getCode`, `eth_getBlockByHash`, `eth_getBlockByNumber`, `eth_getTransactionByHash`, `eth_getTransactionByBlockNumberAndIndex`, `eth_getTransactionReceipt`, `eth_getLogs` (the match arms of `rpc.rs`). MISSING: `eth_getTransactionByBlockHashAndIndex`, `eth_getUncleByBlockHashAndIndex` (design 8.2 says uncles are "always empty": the method still has to exist). Also absent from the fork but in the design table: `eth_getStorageAt` is present; `eth_getProof`, `eth_feeHistory` present; `web3_clientVersion`, `net_version`, `net_peerCount`, `net_listening`, `eth_syncing`, `eth_mining`, `eth_protocolVersion`, `eth_accounts`, `eth_getBlockReceipts`, `eth_getBlockTransactionCountByNumber`, `eth_maxPriorityFeePerGas`, `eth_gasPrice`, `eth_estimateGas`, `eth_sendRawTransaction` present | +| Pending transactions | `txpool_content` (geth, erigon) or `parity_pendingTransactions` | Neither exists in the fork. Blockscout runs without it (the pending view stays empty) | +| Internal transactions and block rewards | `debug_traceBlockByNumber` and `debug_traceTransaction` with `callTracer` (geth variant), or `trace_replayBlockTransactions` and `trace_block` (erigon, nethermind) | None of the four exist in the fork. Design 8.2 lists them as "Supported, revm inspectors"; `rpc.rs` has no `debug_` or `trace_` method today. Without them Blockscout shows no internal transactions and no block-reward rows, and the indexer's trace fetcher must be switched off (`INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER`, Blockscout's env; unverified against the current version) | + +Cost of one instance on Hetzner (the price list the seeds are on, `docs/plans/seed-nodes.md`: cx23 2 vCPU 4 GB at USD 6.49 net a month; larger types not priced here): Blockscout's own AWS example is 4 vCPU 16 GB plus a 2 vCPU 8 GB database. The matching Hetzner shape is one box in the 8 GB to 16 GB class plus Postgres on the same box for a devnet, a second box for the database when the chain carries real traffic. Price it from the Hetzner API when the box is ordered; the figure here is approximate: USD 15 to 40 a month for the single box, under USD 80 for two. Plus an Igneum node on the same box or next to it (Blockscout wants a local, unlimited RPC; the public `rpc.testnet.igneum.network` is rate limited to 20 req/s, `docs/plans/testnet-go.md`). + +What it costs in work, in hours not weeks: the two missing `eth_` methods (small, same shape as their by-number siblings); a decision on tracing (the `debug_` namespace with revm inspectors, design 8.2, is the larger piece and is not needed to run Blockscout without internal transactions); Blockscout's env file and a Docker compose on the box; the chain's entry in its config (chain id 4463 devnet, 4462 testnet, 4461 mainnet, design 8.1); contract verification through Sourcify or Blockscout's own verifier microservice. + +## 3. What Igneum needs that must be ours + +Blockscout shows a chain: numbered blocks, one parent, transactions, accounts. Igneum's execution layer is such a chain (RPC "blocks" are chain blocks, design 8.2), so Blockscout is right for it. Everything the consensus layer adds is invisible to it: + +| Need | Where it comes from | State tonight | +|---|---|---| +| The DAG: every block, its parents, blue or red, pending, the selected chain, the mergeset of each chain block | the observer's `blockAdded` feed (`live_blocks`, `detail.mergeset`) | `/explorer` table and `/block/` built; the live DAG picture stays on `/live` | +| Blue score and DAA score per block, the miner's vote key, the engine tag | the block header and coinbase | built | +| Miners: payout address (the coinbase's `IGNA` tag) and the coinbase address, blocks mined, what they earned, balance | observer columns `evm_miner`, `miner_address`; `eth_getBalance` through `EXPLORER_EVM_RPC` | `/address/` built; balance shows when the deployment has an EVM RPC (none public for the devnet; `rpc.testnet.igneum.network` for the testnet) | +| The lottery program per epoch (which generated program is live, the epoch seed, the VDF) | the node reports `epochSeed` per shard plan (`igneum_getShardPlan`); the program id and the epoch boundary are not in any RPC the observer reads | not built; needs an RPC for the current program id and epoch (open) | +| Finality: checkpoints every 30 blue score, locks at two thirds of all 30-day weight, certificates, voter tables, the in-browser verifier | `live_checkpoints`, `live_certificates`, `/api/checkpoint`, `site/verify` | the block page shows a block's checkpoint state and certificate; a checkpoints list page is not built | +| Proof records and shards: the plan per chain block, who proved what, lag, payout | `live_proofs`, `igneum_getProofRecords` | the block page shows the shards and the records a block carries; a provers page (per prover: shards, lag, income) is not built | +| Pool payouts | a public pool does not exist (section 5) | not built | +| The switches: `difficulty_v2_activation_daa`, `proving_v0_activation_daa`, `fees_v1_activation_daa`, `finality_v3_activation_daa` (`/tmp/igneum-devnet/override-v3.json` on the devnet: 33,000, 84,100, 210,000, 135,200) | the params file; `igneum_getProvingStatus.activationDaa`; no RPC lists them all | not built; a "network parameters" card on `/explorer` reading a params RPC is the clean way | + +## 4. Recommendation + +Confirmed from the code and the RPC surface: Blockscout for contracts, transactions and accounts; our own DAG, mining, +finality and proving pages on the site, fed by the observer; one search box that routes a transaction or contract to +Blockscout and a block hash, a chain block number or a miner to our pages. Two things qualify it: + +1. Blockscout cannot run against the fork as it is: `eth_getTransactionByBlockHashAndIndex` and + `eth_getUncleByBlockHashAndIndex` are missing (an hour), and there is no tracing (`debug_traceTransaction`), so + internal transactions stay off until the revm inspectors of design 8.2 exist. The execution engineer owns both. +2. One box of our own and Postgres on it, next to a node with an unlimited local RPC. Not before the public testnet + has transactions worth looking at; the devnet's blocks are empty and our pages already show them. + +The search box: `site/lib/explorer.mjs` `classify()` already routes 64-hex to a block (the API falls through to a +transaction hash in the last 24 hours), 0x40 and bech32 to an address, digits to a chain block number. When Blockscout +is up, a 64-hex that is not a DAG block and a 0x40 that is a contract go to it instead of a 404. + +## 5. What was built tonight + +| Item | Where | +|---|---| +| `/api/stats`, `/api/supply` | `site/api/stats.mjs`, `site/api/supply.mjs`, `site/api/_neon.mjs`; the emission rule in `site/lib/emission.mjs`; `docs/api/public-stats.md` with example responses | +| `/api/explorer` | `site/api/explorer.mjs`: latest blocks, one block, height, address, search | +| Pages | `site/explorer.html`, `site/block.html`, `site/address.html`; `site/vercel.json` rewrites `/block/:id` and `/address/:addr`; the footer carries an Explorer link (the nav is unchanged: its eight items are measured to fit at 941 px, a ninth is a layout decision for the site owner) | +| Observer | `tools/observer/observer.mjs`: per-block explorer columns and `detail`, `number` from the shard plan, `rpc_load`, hourly `supply_check` | +| Preview | `node tools/site-serve.mjs` (clean URLs, the rewrites, the API functions in-process; `LIVE_TABLE_PREFIX`, `EXPLORER_EVM_RPC`, `PORT`) | +| Tests | `site/lib/explorer.test.mjs` (router, formatters), `site/lib/emission.test.mjs` (the node's own test values from `igneum.rs`, a devnet coinbase, the floor sum against a loop), `site/api/public-stats.test.mjs` (every documented field from a fixture); CI runs them, and on master `tools/ci/public-api-check.mjs https://igneum.network` | +| Screenshots | `docs/plans/explorer/explorer.png`, `block.png`, `address.png` (local preview against a test observer, 5 October 2026) | + +### Observer load, measured + +Two copies of the observer ran side by side against the observer node (`ws://127.0.0.1:28640`, EVM RPC +`http://127.0.0.1:26790`) with test table prefixes, 19:21 to 19:28 UTC on 5 October 2026: master's code with only a +call counter added, and this branch. Calls per wall-clock minute, as each process counted them: + +| Minute (UTC) | Before, wRPC | After, wRPC | Before, EVM | After, EVM | +|---|---|---|---|---| +| 19:21 to 19:22 (partial, 50 s) | 283 | 282 | 776 | 785 | +| 19:22 | 211 | 211 | 2,119 | 2,145 | +| 19:23 | 252 | 253 | 2,487 | 2,493 | +| 19:24 | 232 | 232 | 2,487 | 2,492 | +| 19:25 | 266 | 266 | 2,491 | 2,497 | +| 19:26 | 210 | 210 | 2,485 | 2,481 | + +Ratio after to before: 1.00 on both. The explorer reads everything from the notification the observer already +receives; the only new work is one SQL update per shard-plan batch. The EVM figure is the proving feed's record +polling (80 blocks per 2 s tick while proving is active, master behaviour); the observer that is live tonight points +at `http://127.0.0.1:26800` (the app's node, down), so its EVM load is zero and its proving feed reads "unreachable". +The observer node answers the proving RPCs on 26790 (`eth_chainId` 0x116f, `igneum_getProvingStatus` active); pointing +`IGNEUM_EVM_RPC` there when the observer is next restarted is a one-line change to the launch environment, not to the code. + +### Supply check, measured + +The first sample after start: "21 blocks, payload subsidy = rule 21/21, outputs = merged subsidies 20/20 (1 without a +verdict)". The rule in `site/lib/emission.mjs` reproduces the node's own test values (`igneum.rs`: 3,168,808,781 at +DAA 2,592,000; 1,584,404,390 at the first halving; 0 at the 32nd) and the devnet coinbase of block `2622db76` (payload +subsidy 454,486,399 at DAA 125,064; outputs 363,588,240 + 90,897,059 = 454,485,299 = E(125,063), what its merged +parent declared: `utxo_validation.rs:176` pays a merged block the subsidy its own payload carries). + +## 6. A public pool with a stats API (separate item, not built) + +The app carries a pool mode in the Hive package: the Flight Sheet's Pool URL is `grpc://:26610` for solo +mining or `local` to run the bundled node on the rig (`packaging/hive/README.md`); "there is no pool". A public pool +needs, in order: the pool protocol of `docs/spec/09-pool-protocol.md` implemented (templates, shares, the member's +own vote key and votes relayed, vardiff, the `stats` message: member `hashrate`, `workers`, `refusals`, `shares`, pool +`members`, `hashrate`, `blocks_24h`, `declared_share`); a pool server on its own box with a node; a payout scheme (the +spec leaves PPLNS windows and PPS fees to the pool); a stats API in the shape WhatToMine and MiningPoolStats read +(pool hashrate, miners, workers, blocks found with heights and times, fee, minimum payout, luck); and a page on the +site. None of this is in the repo; the stats API of this branch is the network side of what those sites ask for. + +## 7. Unverified + +- Vercel's `cleanUrls` with a rewrite destination of `/block` and `/address` (the clean names of `block.html` and + `address.html`): checked locally through `tools/site-serve.mjs`, not on a Vercel preview, because nothing was pushed. +- `/api/stats` and `/api/supply` on the live tables: the live observer has not been restarted on this code, so the + live `live_blocks` has no `number`, `tx_count` or `detail` columns yet; the handlers answer with nulls there until + the restart (the schema adds itself on start, `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`). The examples in + `docs/api/public-stats.md` come from the test observer's tables. +- Blockscout's licence terms for a hosted instance, and the exact env flag that disables its trace fetcher. +- The database size per Igneum block in Blockscout, and the Hetzner price of the box: approximate above, measure and + price when ordered. +- `EXPLORER_EVM_RPC` on Vercel: no public devnet EVM RPC exists, so balances show "no EVM RPC configured" on the devnet + deployment; the testnet's `https://rpc.testnet.igneum.network` is the value for the testnet. diff --git a/docs/plans/explorer/address.png b/docs/plans/explorer/address.png new file mode 100644 index 000000000..9f3a47dad Binary files /dev/null and b/docs/plans/explorer/address.png differ diff --git a/docs/plans/explorer/block.png b/docs/plans/explorer/block.png new file mode 100644 index 000000000..031cb2773 Binary files /dev/null and b/docs/plans/explorer/block.png differ diff --git a/docs/plans/explorer/explorer.png b/docs/plans/explorer/explorer.png new file mode 100644 index 000000000..a493c9277 Binary files /dev/null and b/docs/plans/explorer/explorer.png differ diff --git a/site/404.html b/site/404.html index 321532bd4..076e9b219 100644 --- a/site/404.html +++ b/site/404.html @@ -172,6 +172,7 @@ p{margin:0;color:var(--ink-2);max-width:52ch}