diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index c95f3e846..7eabfeb1d 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -29,7 +29,7 @@ Evidence locations referenced below: `docs/bench-log.md`, `proto-metal/TESTS.md` ### M1. The program space is tiny "Eleven integer ops, 64 instructions, 8 iterations, a splitmix register init. That is not RandomX. RandomX leans on a superscalar out-of-order CPU with floating point and branches. Yours is a sea of 32-bit ALUs and a memory bus. A chip for that is a weekend." -Status: Decided (6 October 2026, 17:35 UTC, by the owner; decisions item 1, corrected): no standing bounty. A team with a real 2x chip earns more by mining than any bounty pays, so a device bounty attracts nobody; the tiers announced at 17:25 UTC are withdrawn. The claim "under 2x" is backed by the paid independent cryptanalysis (the Monero route: four paid reviews, no bounty) and by the public benchmark with M22's metrics. Optional, the owner's call later: one cryptanalysis prize of USD 50,000 for a published 2x or better shortcut in the mixer, the chained cache or the acceptance rule, escrowed before it is named; nothing public before that. The claim stays a target until the audit and the benchmark have reported. Was: Open, program space counted, the claim stands as a target (5 October 2026, evening sweep). Was: Open, experiment scheduled. Sweep (5 October 2026): nothing runnable; the bounty, the public benchmark and a chip design are M22's decisions and hardware. The nearest number stays M16's arithmetic. +Status: Decided (6 October 2026, 17:35 UTC, by the owner; decisions item 1, corrected): no standing bounty. A team with a real 2x chip earns more by mining than any bounty pays, so a device bounty attracts nobody; the tiers announced at 17:25 UTC are withdrawn. The claim "under 2x" is backed by the in-house adversarial pass and by the public benchmark with M22's metrics. The claim stays a target until the audit and the benchmark have reported. Was: Open, program space counted, the claim stands as a target (5 October 2026, evening sweep). Was: Open, experiment scheduled. Sweep (5 October 2026): nothing runnable; the bounty, the public benchmark and a chip design are M22's decisions and hardware. The nearest number stays M16's arithmetic. Decision owner: the founder, decided 6 October 2026 (`docs/plans/ledger-decisions.md`, outcomes section). Sweep (5 October 2026, evening): the program space, swept as arithmetic from the version 2 generator (`igneum-pow/src/generator.rs`: 16 load slots as a uniform 16-subset of instructions 1 to 63, then nine draws per instruction, 592 draws per program; 10 non-load operations with weights 12/10/8/8/8/7/6/6/6/4; 8 registers; a 31-way rotation, a 32-way bit and a 5-way mask per instruction; two 32-bit immediates). Counting choices: the slot subset is 48.4 bits; the operation draw carries 3.26 bits of entropy (of log2 10 = 3.32); operations and registers alone give about 770 bits per program; with the rotation, bit and mask fields about 1,550 bits; with the immediates about 5,650 bits, all capped by the 256-bit seed, so the space a chip has to serve is 2^256 distinct programs drawn from a structure of about 2^1550 shapes, and the critic is right that it is a small instruction set: 12 operations, 8 registers, no floating point, no branches, by design (vendor-identical rounding). What the sweep adds to the ledger's answer is the number that matters for a fixed-function design, the spread a chip must absorb: under generator version 2 every accepted program does 120 to 128 distinct loads per hash (median 128.00, 20,000-program census), so the per-program hash-rate spread on a memory-bound device is the 1.10x residual the census measured, not the 2.7x of version 1; the chip's advantage therefore cannot come from the program (it is one fixed memory-bound shape) and must come from the memory system or from the recompute route, which is M16's cost model (`docs/analysis/m16-recompute-attacker-2026-10-05.md`: 1.5x to 2.4x at equal integer budget before any fixed-function factor, 3x to 6x with one, and the mixer-cost lever that cuts it below 1x). The experiment that closes M1 is unchanged: the bounty and the public benchmark (M22, decision owner the founder). @@ -1587,7 +1587,7 @@ Run (5 October 2026, night): `python3 sim/difficulty/record_report.py devnet-202 ### M22. The ASIC challenge has no scoring rules, and 2x is not the economic line "Your bounty says 'beats a GPU by more than 2x'. Per what? Hashes per second, per joule, per dollar, on the average program or the worst hour? A chip at 1.6x with half the capital cost wins. And nobody has published eligible hardware, funding or a judge." -Status: Decided (6 October 2026, 17:35 UTC, by the owner; decisions item 1, corrected): no bounty, so no bounty terms; M22's metrics (hashes per second and per joule per program over at least 100 epochs as a distribution, capital cost per unit of hash rate at a stated volume, the longevity term, the shortcut classes scored separately) become the public benchmark's scoring rules and the brief of the paid cryptanalysis; the optional USD 50,000 cryptanalysis prize, if ever set, is escrowed before it is named. Was: Open, decision for the founder (terms, judge and funding) and experiment scheduled (extends O-1.17). Sweep (5 October 2026): nothing runnable; the terms, judge and funding are the founder's decision. +Status: Decided (6 October 2026, 17:35 UTC, by the owner; decisions item 1, corrected): no bounty, so no bounty terms; M22's metrics (hashes per second and per joule per program over at least 100 epochs as a distribution, capital cost per unit of hash rate at a stated volume, the longevity term, the shortcut classes scored separately) become the public benchmark's scoring rules and the brief of the in-house adversarial pass. Was: Open, decision for the founder (terms, judge and funding) and experiment scheduled (extends O-1.17). Sweep (5 October 2026): nothing runnable; the terms, judge and funding are the founder's decision. Decision owner: the founder, decided 6 October 2026 (`docs/plans/ledger-decisions.md`, outcomes section). Answer: Correct. M1 and O-1.17 name a bounty for "more than 2x" without a metric, a judge or a fund, and 2x is a design target for the hash, not an economic threshold: a chip at 1.5x per joule and 3x per dollar of capital is an economic ASIC whatever the hash-rate ratio says. The scoring rules go out with the January 2027 benchmark: hashes per second and per joule measured per program over a published set of at least 100 epochs, reported as a distribution (worst decile, median), never as one average; capital cost per unit of hash rate at a stated volume; a longevity term against the instruction-family reserve and the dataset growth of spec 1.13; and the shortcut classes (recomputation, partial storage, weak-program selection) scored separately. Eligible hardware assumptions, the judge, the reward and the payer are the founder's decisions (fud-fixes row 50: the payer is the entity). The supportable public claim until a design has been scored: across the tested workloads and the stated economic assumptions, consumer GPUs remain competitive against the best independently proposed specialised design. Extends M1, M16 and C13. diff --git a/docs/spec/01-lottery-hash.md b/docs/spec/01-lottery-hash.md index 7b03ea9bf..63744afe4 100644 --- a/docs/spec/01-lottery-hash.md +++ b/docs/spec/01-lottery-hash.md @@ -265,29 +265,32 @@ Under class v2 and v3 an implementation MAY treat the cap as a consensus fault. Program id. `FNV-1a-64("igneum-program/" || generator_le32 || seed words as little-endian bytes || attempt_le32 || suffix)` with `generator = 2` under class v2 and `generator = 3` under class v3 (no suffix), and `generator = 4` under class v4 with the suffix `"sub/" || sub_version_le16` (the class v4 stream's sub-version, `PROGRAM_SUBVERSION_V4 = 3` since 7 October 2026: `IGNEUM_PROGRAM_SUBVERSION` in program.h, `"sub_version"` in program.json; without the suffix Devnet 3's epoch-0 id reads 30956569d8f3d8d7 where the chain and the packs say fce15bf61030be57); class v5 is `generator = 5` with no suffix (section 1.4.7). A class v4 program at rung 0 of the ladder uses that form; a program of any other class or rung (a read-width, scratch, mixer, derivation, era, shadow or hot rung other than class v4's rung 0) uses the tag `"igneum-program-rw/"` and appends the class's fields after `attempt_le32` (`generator::program_id_class`). Every pack prints its own derivation as `program_id_derivation` in program.json, built from the byte recipe the id is hashed from, and a worker MUST refuse a pack whose generator version, class, era seed or sub-version is not its own (`packcheck.rs`). Two implementations that agree on the id agree on the generator version, the seed words, the attempt and, under class v4, the sub-version. -Constants of the shipped rule. One row per constant the rule depends on, the value as the crate has it; a reader implementing from this text uses these and nothing else, and the crate's own test reads this table back against its `pub const` items. +Constants of the shipped rule. One row per constant the rule depends on, the value as the crate has it; a reader implementing from this text uses these and nothing else. `tools/ci/spec-constants-check.mjs` (the pre-push gate) reads every table of this shape in this file back against the crate's `pub const` items and fails on a difference; `igneum-pow/tests/spec_readback.rs` derives every pinned id below through the crate. | Constant | Value | Where | |---|---|---| | generator::INSTR_COUNT | 64 | instructions per base program | | generator::LOAD_SLOTS | 16 | load slots per program | | generator::ITERATIONS | 8 | iterations per hash | +| generator::LANES | 32 | lanes per unit | | generator::GENERATOR_VERSION_V4 | 4 | the class v4 generator | | generator::PROGRAM_SUBVERSION_V4 | 3 | the id suffix "sub/" || le16 | | generator::MAX_ATTEMPTS | 32 | the cap under class v2 and v3 | | generator::MAX_ATTEMPTS_V4 | 256 | the cap under the class v4 shape | | generator::V4_SHADOW_INSTRS | 256 | shadow instructions per program | | generator::V4_SHADOW_REPS | 27 | shadow passes per iteration at rung 0 | +| accept::ACCEPT_TAG | "igneum-accept/" | the domain tag of the base-nonce stream | | accept::ACCEPT_UNITS | 64 | (c) units | | accept::ACCEPT_HASHES | 2048 | (c) hashes | | accept::ACCEPT_DATASET_LOG2 | 28 | log2 of the closed-form dataset | -| accept::MAX_SATURATED | 164 | (c) saturated finals and (c') saturated sources, exclusive limit | +| accept::MAX_SATURATED | 164 | (c) saturated finals and (c') saturated sources: the first refused count (the reject message prints 163, the last allowed) | | accept::BIAS_TOLERANCE | 136 | (c) output bias, inclusive | | accept::MIN_DISTINCT_SUM | 245760 | (c) distinct-address sum, exclusive | | accept::ACCEPT_UNITS_DISTINCT_V4 | 4096 | (c'') units, 2^20 evaluations per site | | accept::MIN_DISTINCT_RATIO_V4 | 0.98 | (c'') ratio floor, inclusive | +| accept::distinct_ratio_pass | 2 | the window cap of (c''): `W_s = 2^28 >> min(k_off_s, 2)`, a literal inside the function | -Pinned program ids. A reader who follows 1.4.3 and this section reproduces these from the seed, the attempt and the sub-version above; the crate's test derives each from the recipe and from the printed derivation text. +Pinned program ids. A reader who follows 1.4.3 and this section reproduces these from the seed, the attempt and the sub-version above. The era seed of each chain row is the same 32 bytes as its program seed (the devnet stand-in of 1.13.1). `igneum-pow/tests/spec_readback.rs` derives each id through the crate and draws each class v4 row with the era to its stated attempt; a "must differ" row asserts the current derivation gives another id. | Seed | Attempt | Id | Note | |---|---|---|---| diff --git a/igneum-pow/tests/spec_readback.rs b/igneum-pow/tests/spec_readback.rs new file mode 100644 index 000000000..8f2a1725b --- /dev/null +++ b/igneum-pow/tests/spec_readback.rs @@ -0,0 +1,115 @@ +//! Spec read-back, the ids half (adv-accept-3 finding 3, ledger AP-F8-5, 7 October 2026): every table of +//! `docs/spec/01-lottery-hash.md` headed `| Seed | Attempt | Id | Note |` names program ids a reader of sections 1.4.3 +//! and 1.4.6 must reproduce. This test derives each one through the crate (never by re-hashing the hex in a script): +//! a row whose Note says "must differ" asserts the current derivation gives another id (an earlier stream or +//! sub-version of the same seed); every other row asserts `program_id` over `attempt_words(seed, attempt)` under the +//! row's generator (3 for "class v3", 5 for "generator 5", else 4) equals the id, and, for a class v4 row, that the +//! chain's own draw with the era seed (the same bytes unless the Note names another) accepts at that attempt. +//! The constants half is `tools/ci/spec-constants-check.mjs` (the pre-push gate; no build). +use igneum_pow::generator::{attempt_words, generate_from_seed_bytes_program_class, program_id, ProgramClass, GENERATOR_VERSION_V3, GENERATOR_VERSION_V4}; +use std::path::PathBuf; + +const SPEC: &str = "../docs/spec/01-lottery-hash.md"; + +struct Row { + line: usize, + seed: String, + attempt: u32, + id: u64, + note: String, +} + +fn rows() -> Vec { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SPEC); + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + let lines: Vec<&str> = text.lines().collect(); + let header = |l: &str| { + let cells: Vec = l.trim().trim_matches('|').split('|').map(|c| c.trim().to_string()).collect(); + cells == ["Seed", "Attempt", "Id", "Note"] + }; + let mut out = Vec::new(); + let mut i = 0; + while i < lines.len() { + if !header(lines[i]) { + i += 1; + continue; + } + let mut j = i + 1; + if j < lines.len() && lines[j].trim_start().starts_with("|---") { + j += 1; + } + while j < lines.len() && lines[j].trim_start().starts_with('|') { + let cells: Vec = lines[j].trim().trim_matches('|').split('|').map(|c| c.trim().trim_matches('`').to_string()).collect(); + assert!(cells.len() >= 4, "{}:{}: a Seed | Attempt | Id | Note row needs four cells", SPEC, j + 1); + let attempt: u32 = cells[1].parse().unwrap_or_else(|_| panic!("{}:{}: attempt {:?} is not an integer", SPEC, j + 1, cells[1])); + let id = u64::from_str_radix(cells[2].trim_start_matches("0x"), 16).unwrap_or_else(|_| panic!("{}:{}: id {:?} is not 16 hex", SPEC, j + 1, cells[2])); + out.push(Row { line: j + 1, seed: cells[0].clone(), attempt, id, note: cells[3..].join("|") }); + j += 1; + } + i = j; + } + assert!(!out.is_empty(), "{SPEC}: no table headed | Seed | Attempt | Id | Note |"); + out +} + +fn seed_bytes(seed: &str) -> Vec { + let hex = seed.len() == 64 && seed.chars().all(|c| c.is_ascii_hexdigit()); + if hex { + (0..32).map(|k| u8::from_str_radix(&seed[2 * k..2 * k + 2], 16).unwrap()).collect() + } else { + seed.as_bytes().to_vec() + } +} + +fn generator_of(note: &str) -> u32 { + if note.contains("class v3") { + GENERATOR_VERSION_V3 + } else if note.contains("generator 5") { + 5 + } else { + GENERATOR_VERSION_V4 + } +} + +#[test] +fn every_pinned_id_of_the_spec_derives_from_the_crate() { + let rows = rows(); + let mut checked = 0; + for r in &rows { + let bytes = seed_bytes(&r.seed); + let words = attempt_words(&bytes, r.attempt); + let g = generator_of(&r.note); + let derived = program_id(g, &words, r.attempt); + if r.note.contains("must differ") { + assert_ne!(derived, r.id, "{}:{}: the must-differ id {:016x} equals the current derivation for seed {} attempt {}", SPEC, r.line, r.id, r.seed, r.attempt); + } else { + assert_eq!(derived, r.id, "{}:{}: the crate derives {:016x} for seed {} attempt {} under generator {g}, the spec says {:016x}", SPEC, r.line, derived, r.seed, r.attempt, r.id); + } + checked += 1; + } + assert!(checked >= 4, "the spec carries only {checked} pinned ids; the table has shrunk"); +} + +/// The chain's own draw (the era layout over the class v3 base, the shadow block, the full rule) accepts the class v4 +/// rows at the attempt the spec states; the era seed is the program seed unless the Note names another 64-hex value +/// after "era". +#[test] +fn the_chain_draw_accepts_each_class_v4_row_at_its_attempt() { + let rows = rows(); + let mut drawn = 0; + for r in rows.iter().filter(|r| !r.note.contains("must differ") && generator_of(&r.note) == GENERATOR_VERSION_V4) { + let bytes = seed_bytes(&r.seed); + let era: Vec = match r.note.find("era ") { + Some(k) => { + let hex: String = r.note[k + 4..].chars().take_while(|c| c.is_ascii_hexdigit()).collect(); + if hex.len() == 64 { seed_bytes(&hex) } else { bytes.clone() } + } + None => bytes.clone(), + }; + let p = generate_from_seed_bytes_program_class(&format!("spec:{}", &r.seed[..16.min(r.seed.len())]), &bytes, ProgramClass::V4, Some(&era)); + assert_eq!(p.attempt, r.attempt, "{}:{}: the draw accepted seed {} at attempt {}, the spec says {}", SPEC, r.line, r.seed, p.attempt, r.attempt); + assert_eq!(p.program_id(), r.id, "{}:{}: the drawn program's id is {:016x}, the spec says {:016x}", SPEC, r.line, p.program_id(), r.id); + drawn += 1; + } + assert!(drawn >= 1, "no class v4 row to draw"); +} diff --git a/site/ledger.html b/site/ledger.html index b5d65f082..e912bd22c 100644 --- a/site/ledger.html +++ b/site/ledger.html @@ -4,13 +4,13 @@ Igneum ledger: every criticism, answered - + - + @@ -18,7 +18,7 @@ - + @@ -53,7 +53,7 @@