From 39008cb1a108f1f9e8ad93d0d5572a458481adeb Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:18:44 +0000 Subject: [PATCH] RTX 5090: memory-hard pack 96/96 PASS, cache bit-exact with the Mac; ignore pycache Co-Authored-By: Claude Fable 5.1 --- .gitignore | 2 + docs/bench-log.md | 12 + proto-vdf/Cargo.lock | 180 +++++++++ proto-vdf/src/classgroup.rs | 8 +- proto-vdf/src/main.rs | 426 ++++++++++++++++++++ sim/__pycache__/finality_v2.cpython-310.pyc | Bin 42840 -> 0 bytes 6 files changed, 624 insertions(+), 4 deletions(-) create mode 100644 proto-vdf/Cargo.lock create mode 100644 proto-vdf/src/main.rs delete mode 100644 sim/__pycache__/finality_v2.cpython-310.pyc diff --git a/.gitignore b/.gitignore index 74eccc96..86499d41 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ vendor/ # secrets never live here *.env .env* +sim/__pycache__/ +*.pyc diff --git a/docs/bench-log.md b/docs/bench-log.md index b95bbad0..9279dc85 100644 --- a/docs/bench-log.md +++ b/docs/bench-log.md @@ -134,3 +134,15 @@ Blocks per second over the 180 s run: 31.76 on all three nodes (1,691 to 7,409 b Propagation: block counts, DAA scores and sink hash were identical on all three nodes at 18 of 19 ten-second samples; the one miss was node 2 trailing by a single block for one sample. Tips stayed at 1 because a single serial miner never produced parallel blocks, so GHOSTDAG k was not exercised; a second miner is the next step for that. Earlier 30 s warm-up run: 1,690 blocks, 56.3 blocks/s on all three nodes, 28.1 MH/s. Fork points mapped with line numbers in `docs/fork-map.md` (hash, coinbase, DAA, header, depth constants, BPS and k). All nodes stopped at the end. Miner source kept outside the repo (scratchpad); re-create from `testing/integration/src/common/utils.rs:271` if needed. + +## 3 October 2026, RTX 5090, memory-hard dataset (pack igneum-genesis-mh) + +| Check | Result | +|---|---| +| 256 MiB cache, GPU vs host, all 67,108,864 words | PASS, FNV-1a 48c4f5bf24166b2e matches the Mac | +| Cache fill | 0.67 ms GPU, 223 ms one host thread | +| Dataset build from the cache, 1 GiB | 13.4 ms, 1,253 M items/s | +| Vectors, 3 warps, standalone and in batch | 96/96 PASS | +| Hash rate at 1 GiB | 228.95 Mhash/s, 95.2 GB/s useful, 23.8 G random loads/s | + +Reading: the memory-hard construction is now bit-exact across Apple Metal, NVIDIA CUDA and the CPU reference, cache and dataset included. Hash rate is unchanged from the closed-form dataset on both vendors, as expected, since the hash kernel only loads; what changed is that computing items on the fly is now slower than loading them (4.8x slower measured on Apple, not yet measured on NVIDIA). Still unmeasured: the inline shortcut ratio on NVIDIA, and AMD on any dataset. diff --git a/proto-vdf/Cargo.lock b/proto-vdf/Cargo.lock new file mode 100644 index 00000000..795a3588 --- /dev/null +++ b/proto-vdf/Cargo.lock @@ -0,0 +1,180 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13eabc29d16e4a621b495e3919c71ebb7caaed24380955671e7d417370fea95d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "igneum-vdf" +version = "0.1.0" +dependencies = [ + "az", + "gmp-mpfr-sys", + "rug", + "sha2", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "rug" +version = "1.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555e8b44763d034526db899c88cd56ccc4486cd38b444c8aa0e79d4e70ae5a34" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/proto-vdf/src/classgroup.rs b/proto-vdf/src/classgroup.rs index 4d358821..d84aac74 100644 --- a/proto-vdf/src/classgroup.rs +++ b/proto-vdf/src/classgroup.rs @@ -20,6 +20,7 @@ use crate::group::Group; use crate::hash::{bytes_to_int, hash_prime, signed_to_bytes}; +use rug::ops::{NegAssign, RemRounding}; use rug::Integer; use std::cmp::Ordering; @@ -149,8 +150,8 @@ impl ClassGroup { // Step 4. let v1 = Integer::from(&f1.a / &d1); let v2 = Integer::from(&f2.a / &d1); - let mut r = Integer::from(&y1 * &y2) * &n - Integer::from(&x2 * &f2.c); - r.rem_euc_mut(&v1); + let r = Integer::from(&y1 * &y2) * &n - Integer::from(&x2 * &f2.c); + let r = r.rem_euc(&v1); let v2r = Integer::from(&v2 * &r); let b3 = Integer::from(&f2.b + Integer::from(&v2r << 1u32)); let a3 = Integer::from(&v1 * &v2); @@ -170,8 +171,7 @@ impl ClassGroup { *f = r; return; } - let mut u = Integer::from(&f.c * &s); - u.rem_euc_mut(&f.a); + let u = Integer::from(&f.c * &s).rem_euc(&f.a); let au = Integer::from(&f.a * &u); let bu_c = Integer::from(&f.b * &u) - &f.c; let q = bu_c / &f.a; // exact: bu = c mod a diff --git a/proto-vdf/src/main.rs b/proto-vdf/src/main.rs new file mode 100644 index 00000000..7a883c6b --- /dev/null +++ b/proto-vdf/src/main.rs @@ -0,0 +1,426 @@ +mod classgroup; +mod grind; +mod group; +mod hash; +mod rsa; +mod seed; +mod wesolowski; + +use classgroup::ClassGroup; +use group::Group; +use hash::{from_hex, hex, sha256}; +use rsa::RsaGroup; +use rug::Integer; +use std::time::Instant; + +const DECISION_WINDOW_S: f64 = 2.0; +const EPOCH_DELAY_S: f64 = 600.0; +const ERA_DELAY_S: f64 = 3600.0; + +fn arg(args: &[String], name: &str) -> Option { + args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned()) +} + +fn arg_u64(args: &[String], name: &str, default: u64) -> u64 { + arg(args, name).map(|v| v.replace('_', "").parse().expect("number")).unwrap_or(default) +} + +fn arg_f64(args: &[String], name: &str, default: f64) -> f64 { + arg(args, name).map(|v| v.parse().expect("number")).unwrap_or(default) +} + +fn default_threads() -> usize { + std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).min(12) +} + +fn usage() { + eprintln!( + "usage: vdf [options] + selftest group laws, proof round trips, tamper checks + bench [--seconds S] [--threads N] + squarings/s per group, T for 10 and 60 min, prove and verify at a short T + eval --group rsa|class (--minutes M | --t T) [--threads N] [--kappa K] + run a full-length delay, then prove and verify, report times + demo [--checkpoint HEX] [--t T] [--threads N] + epoch_seed / verify_epoch_seed, determinism, tamper tests + grind [--epochs N] grinding revenue table with and without the delay" + ); +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + match args.first().map(|s| s.as_str()) { + Some("selftest") => selftest(), + Some("bench") => bench(&args), + Some("eval") => eval_cmd(&args), + Some("demo") => demo(&args), + Some("grind") => grind_cmd(&args), + _ => usage(), + } +} + +// ---------------------------------------------------------------- selftest + +fn selftest() { + println!("== class group laws (1024-bit prime discriminant from a test seed) =="); + let g = ClassGroup::from_seed(b"igneum-selftest-discriminant", 1024); + println!("D bits {} -D mod 8 = {}", g.d.significant_bits(), Integer::from(-&g.d) % 8u32); + let id = g.identity(); + let gen = g.generator(); + assert!(ClassGroup::is_reduced(&gen) && ClassGroup::discriminant_of(&gen) == g.d); + // Random-ish elements: generator to random exponents. + let mut elems = Vec::new(); + for i in 0..6u32 { + let e = Integer::from_digits(&sha256(&[b"exp", &i.to_be_bytes()]), rug::integer::Order::MsfBe); + let f = g.pow(&gen, &e); + assert!(g.is_valid(&f), "pow output not valid"); + elems.push(f); + } + let mut checks = 0; + for a in &elems { + assert_eq!(g.mul(a, &id), *a); + assert_eq!(g.mul(&id, a), *a); + assert_eq!(g.mul(a, &g.inverse(a)), id); + let mut sq = a.clone(); + g.square(&mut sq); + assert_eq!(sq, g.compose(a, a), "square != compose(a,a)"); + for b in &elems { + assert_eq!(g.mul(a, b), g.mul(b, a)); + for c in &elems { + assert_eq!(g.mul(&g.mul(a, b), c), g.mul(a, &g.mul(b, c))); + checks += 1; + } + } + } + // Exponent laws: (gen^e1)^e2 == gen^(e1 e2), gen^(2^k) by squaring == pow. + let e1 = Integer::from(123456789u64); + let e2 = Integer::from(987654321u64); + assert_eq!(g.pow(&g.pow(&gen, &e1), &e2), g.pow(&gen, &Integer::from(&e1 * &e2))); + assert_eq!(g.pow2k(&gen, 40), g.pow(&gen, &Integer::from(Integer::u_pow_u(2, 40)))); + // Serialization round trip. + for a in &elems { + assert_eq!(g.deserialize(&g.serialize(a)).unwrap(), *a); + } + println!("identity, inverse, commutativity, {} associativity triples, square==compose, exponent laws, serialization: PASS", checks); + + println!("== RSA stand-in group =="); + let r = RsaGroup::from_public_seed("selftest"); + let x = r.hash_to_elem(b"x"); + let e = Integer::from(Integer::u_pow_u(2, 1000)); + let want = x.clone().pow_mod(&e, &r.n).unwrap(); + assert_eq!(r.pow2k(&x, 1000), want); + assert_eq!(r.pow(&x, &e), want); + println!("x^(2^1000) by squaring == GMP powm: PASS"); + + println!("== Wesolowski round trips =="); + for (name, t, spacing, kappa) in [("tiny", 37u64, 12u64, 4u32), ("small", 5000, 240, 8), ("medium", 100_000, 1200, 12)] { + // class group + let ev = wesolowski::eval(&g, &gen, t, spacing); + let pf = wesolowski::prove(&g, &gen, &ev, kappa, 4); + let naive = wesolowski::prove_naive(&g, &gen, &ev.y, t); + assert_eq!(pf.pi, naive.pi, "block proof != naive proof ({})", name); + assert_eq!(ev.y, g.pow2k(&gen, t)); + assert!(wesolowski::verify(&g, &gen, &pf, t), "class verify failed ({})", name); + assert!(!wesolowski::verify(&g, &gen, &pf, t + 1), "wrong T accepted"); + let mut bad = pf.clone(); + bad.pi = g.mul(&bad.pi, &gen); + assert!(!wesolowski::verify(&g, &gen, &bad, t), "tampered pi accepted"); + let mut bad = pf.clone(); + bad.y = g.mul(&bad.y, &gen); + assert!(!wesolowski::verify(&g, &gen, &bad, t), "tampered y accepted"); + // rsa + let ev = wesolowski::eval(&r, &x, t, spacing); + let pf = wesolowski::prove(&r, &x, &ev, kappa, 4); + let naive = wesolowski::prove_naive(&r, &x, &ev.y, t); + assert_eq!(pf.pi, naive.pi, "rsa block proof != naive ({})", name); + assert!(wesolowski::verify(&r, &x, &pf, t)); + let mut bad = pf.clone(); + bad.pi = r.mul(&bad.pi, &x); + assert!(!wesolowski::verify(&r, &x, &bad, t)); + println!("T={:>7} spacing={:>5} kappa={:>2}: class and RSA proofs match the naive O(T) prover, verify PASS, 3 tamper cases rejected", t, spacing, kappa); + } + + println!("== seed pipeline =="); + let h = sha256(&[b"checkpoint"]); + let (s1, p1) = seed::epoch_seed(&h, 20_000, 4); + let (s2, _p2) = seed::epoch_seed(&h, 20_000, 4); + assert_eq!(s1, s2); + assert!(seed::verify_epoch_seed(&h, &s1, &p1)); + let mut h2 = h; + h2[0] ^= 1; + assert!(!seed::verify_epoch_seed(&h2, &s1, &p1)); + let mut s3 = s1; + s3[31] ^= 1; + assert!(!seed::verify_epoch_seed(&h, &s3, &p1)); + println!("same checkpoint twice -> same seed; verify PASS; wrong checkpoint and wrong seed rejected"); + + println!("== grinding model =="); + for s in [0.1, 0.3] { + let row = grind::analytic(s); + let (mc, _) = grind::monte_carlo(s, 200_000, 7); + assert!((row.gain_no_delay - mc).abs() < 0.15, "analytic {} vs MC {}", row.gain_no_delay, mc); + } + println!("analytic gain within 0.15 blocks of Monte Carlo at s=0.1 and 0.3"); + println!("ALL SELFTESTS PASS"); +} + +// ---------------------------------------------------------------- bench + +fn measure_rate(g: &G, x: &G::Elem, seconds: f64) -> f64 { + let mut cur = x.clone(); + // warm-up + for _ in 0..2000 { + g.square(&mut cur); + } + let start = Instant::now(); + let mut n: u64 = 0; + let batch = 2000; + while start.elapsed().as_secs_f64() < seconds { + for _ in 0..batch { + g.square(&mut cur); + } + n += batch; + } + n as f64 / start.elapsed().as_secs_f64() +} + +struct ProofTiming { + t: u64, + eval_s: f64, + prove_1_s: f64, + prove_n_s: f64, + threads: usize, + verify_ms: f64, + proof_bytes: usize, + kappa: u32, + spacing: u64, +} + +fn time_proof(g: &G, x: &G::Elem, t: u64, threads: usize, kappa_opt: Option) -> ProofTiming { + let spacing0 = wesolowski::choose_spacing(t, 1 << 16); + let kappa = kappa_opt.unwrap_or_else(|| seed::pick_kappa(spacing0)); + let spacing = (spacing0 - spacing0 % kappa as u64).max(kappa as u64); + let ev = wesolowski::eval(g, x, t, spacing); + let s = Instant::now(); + let pf1 = wesolowski::prove(g, x, &ev, kappa, 1); + let prove_1_s = s.elapsed().as_secs_f64(); + let s = Instant::now(); + let pf = wesolowski::prove(g, x, &ev, kappa, threads); + let prove_n_s = s.elapsed().as_secs_f64(); + assert_eq!(pf1.pi, pf.pi); + let reps = 20; + let s = Instant::now(); + let mut ok = true; + for _ in 0..reps { + ok &= wesolowski::verify(g, x, &pf, t); + } + let verify_ms = s.elapsed().as_secs_f64() * 1000.0 / reps as f64; + assert!(ok, "verification failed"); + ProofTiming { + t, + eval_s: ev.seconds, + prove_1_s, + prove_n_s, + threads, + verify_ms, + proof_bytes: g.serialize(&pf.y).len() + g.serialize(&pf.pi).len(), + kappa, + spacing, + } +} + +fn print_timing(pt: &ProofTiming) { + println!( + " T={} spacing={} kappa={}: eval {:.2} s, prove {:.2} s (1 thread) / {:.2} s ({} threads), verify {:.2} ms, proof {} bytes", + pt.t, pt.spacing, pt.kappa, pt.eval_s, pt.prove_1_s, pt.prove_n_s, pt.threads, pt.verify_ms, pt.proof_bytes + ); + println!( + " prove/eval ratio {:.3} (1 thread), {:.3} ({} threads)", + pt.prove_1_s / pt.eval_s, + pt.prove_n_s / pt.eval_s, + pt.threads + ); +} + +fn attacker_lines(rate: f64, label: &str) { + let t10 = (rate * EPOCH_DELAY_S).round() as u64; + let t60 = (rate * ERA_DELAY_S).round() as u64; + println!(" {}: {:.0} squarings/s single core", label, rate); + println!(" T(10 min) = {} T(60 min) = {}", t10, t60); + println!(" speed-up needed to evaluate inside the {} s decision window: {:.0}x (epoch), {:.0}x (era)", + DECISION_WINDOW_S, EPOCH_DELAY_S / DECISION_WINDOW_S, ERA_DELAY_S / DECISION_WINDOW_S); + for f in [2.0f64, 10.0, 100.0] { + println!(" attacker {:>3}x faster: epoch delay {:>6.1} s, era delay {:>7.1} s, grinding {}", + f, EPOCH_DELAY_S / f, ERA_DELAY_S / f, + if EPOCH_DELAY_S / f > DECISION_WINDOW_S { "still impossible" } else { "POSSIBLE" }); + } +} + +fn bench(args: &[String]) { + let seconds = arg_f64(args, "--seconds", 5.0); + let threads = arg_u64(args, "--threads", default_threads() as u64) as usize; + println!("Machine: {}", machine_string()); + println!("Decision window {} s, epoch delay {} s, era delay {} s\n", DECISION_WINDOW_S, EPOCH_DELAY_S, ERA_DELAY_S); + + println!("== {} ==", "RSA-2048 stand-in"); + let r = RsaGroup::from_public_seed("igneum-bench"); + let x = r.hash_to_elem(b"bench"); + let rate_rsa = measure_rate(&r, &x, seconds); + attacker_lines(rate_rsa, "RSA-2048"); + let pt = time_proof(&r, &x, 1 << 20, threads, None); + print_timing(&pt); + println!(" projected prove time at T(10 min), {} threads: {:.0} s; at T(60 min): {:.0} s", + threads, pt.prove_n_s / pt.eval_s * EPOCH_DELAY_S, pt.prove_n_s / pt.eval_s * ERA_DELAY_S); + + for bits in [1024u32, 2048] { + println!("\n== class group, {}-bit prime discriminant ==", bits); + let s = Instant::now(); + let g = ClassGroup::from_seed(&sha256(&[b"igneum-bench-disc", &bits.to_be_bytes()]), bits); + println!(" discriminant derivation {:.3} s", s.elapsed().as_secs_f64()); + let gen = g.generator(); + let rate = measure_rate(&g, &gen, seconds); + attacker_lines(rate, &format!("class group {} bits", bits)); + let t = if bits == 1024 { 1 << 17 } else { 1 << 15 }; + let pt = time_proof(&g, &gen, t, threads, None); + print_timing(&pt); + println!(" projected prove time at T(10 min), {} threads: {:.0} s; at T(60 min): {:.0} s", + threads, pt.prove_n_s / pt.eval_s * EPOCH_DELAY_S, pt.prove_n_s / pt.eval_s * ERA_DELAY_S); + } +} + +fn machine_string() -> String { + let cpu = std::process::Command::new("sysctl") + .args(["-n", "machdep.cpu.brand_string"]) + .output() + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "unknown".into()); + let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0); + format!("{} ({} logical cores), rustc {}, GMP via rug", cpu, cores, rustc_version()) +} + +fn rustc_version() -> String { + std::process::Command::new("rustc") + .arg("--version") + .output() + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "?".into()) +} + +// ---------------------------------------------------------------- eval + +fn eval_cmd(args: &[String]) { + let group = arg(args, "--group").unwrap_or_else(|| "class".into()); + let threads = arg_u64(args, "--threads", default_threads() as u64) as usize; + let kappa = arg(args, "--kappa").map(|k| k.parse::().unwrap()); + let minutes = arg_f64(args, "--minutes", 0.0); + println!("Machine: {}", machine_string()); + match group.as_str() { + "rsa" => { + let r = RsaGroup::from_public_seed("igneum-bench"); + let x = r.hash_to_elem(b"eval"); + let t = if minutes > 0.0 { + let rate = measure_rate(&r, &x, 3.0); + let t = (rate * minutes * 60.0).round() as u64; + println!("rate {:.0} sq/s, T for {} min = {}", rate, minutes, t); + t + } else { + arg_u64(args, "--t", 1 << 22) + }; + println!("== {} ==", r.name()); + let pt = time_proof(&r, &x, t, threads, kappa); + print_timing(&pt); + println!(" effective rate during eval {:.0} sq/s", pt.t as f64 / pt.eval_s); + } + _ => { + let g = ClassGroup::from_seed(b"igneum-eval-disc", 1024); + let gen = g.generator(); + let t = if minutes > 0.0 { + let rate = measure_rate(&g, &gen, 3.0); + let t = (rate * minutes * 60.0).round() as u64; + println!("rate {:.0} sq/s, T for {} min = {}", rate, minutes, t); + t + } else { + arg_u64(args, "--t", 1 << 20) + }; + println!("== {} ==", g.name()); + let pt = time_proof(&g, &gen, t, threads, kappa); + print_timing(&pt); + println!(" effective rate during eval {:.0} sq/s", pt.t as f64 / pt.eval_s); + } + } +} + +// ---------------------------------------------------------------- demo + +fn demo(args: &[String]) { + let threads = arg_u64(args, "--threads", default_threads() as u64) as usize; + let t = arg_u64(args, "--t", 200_000); + let h: [u8; 32] = match arg(args, "--checkpoint") { + Some(hx) => { + let v = from_hex(&hx).expect("hex"); + assert_eq!(v.len(), 32, "checkpoint hash must be 32 bytes"); + let mut a = [0u8; 32]; + a.copy_from_slice(&v); + a + } + None => sha256(&[b"igneum devnet checkpoint 0000"]), + }; + println!("checkpoint_hash = {}", hex(&h)); + println!("T = {} squarings (demo value; production T is set from the reference core, see README)", t); + let g = seed::class_group_for(&h); + println!("discriminant: {} bits, -D mod 8 = {}", g.d.significant_bits(), Integer::from(-&g.d) % 8u32); + let s = Instant::now(); + let (seed1, proof) = seed::epoch_seed(&h, t, threads); + let e1 = s.elapsed().as_secs_f64(); + println!("run 1: program_seed = {} ({:.2} s eval+prove, proof {} bytes)", hex(&seed1), e1, proof.wire_size()); + let s = Instant::now(); + let (seed2, proof2) = seed::epoch_seed(&h, t, threads); + let e2 = s.elapsed().as_secs_f64(); + println!("run 2: program_seed = {} ({:.2} s)", hex(&seed2), e2); + println!("same seed on both runs: {}", seed1 == seed2); + println!("same proof bytes on both runs: {}", proof.y == proof2.y && proof.pi == proof2.pi); + let s = Instant::now(); + let ok = seed::verify_epoch_seed(&h, &seed1, &proof); + let v = s.elapsed().as_secs_f64() * 1000.0; + println!("verify_epoch_seed: {} in {:.2} ms (includes deriving the discriminant from the hash)", ok, v); + let mut h2 = h; + h2[5] ^= 0x80; + println!("verify with a different checkpoint hash: {}", seed::verify_epoch_seed(&h2, &seed1, &proof)); + let mut bad = seed1; + bad[0] ^= 1; + println!("verify with a bit flipped in the seed: {}", seed::verify_epoch_seed(&h, &bad, &proof)); + let mut badp = proof.clone(); + badp.t += 1; + println!("verify with T+1 claimed: {}", seed::verify_epoch_seed(&h, &seed1, &badp)); + println!("proof: t={} y={}... pi={}...", proof.t, hex(&proof.y[..16]), hex(&proof.pi[..16])); +} + +// ---------------------------------------------------------------- grind + +fn grind_cmd(args: &[String]) { + let epochs = arg_u64(args, "--epochs", 1_000_000); + println!("Grinding model: {} blocks/epoch, advantage uniform on [0, {}], grinder keeps top quartile (a >= {:.4}),", + grind::BLOCKS_PER_EPOCH, grind::ADV_MAX, grind::TOP_QUARTILE); + println!("one block reward per withheld block, {} Monte Carlo epochs per share.\n", epochs); + println!("| share | honest rev/epoch | P(grind lands) | withheld blocks | gain NO delay (analytic) | gain NO delay (MC) | gain % | gain WITH delay | gain:cost |"); + println!("|---|---|---|---|---|---|---|---|---|"); + for row in grind::table(epochs) { + println!( + "| {:.1} | {:.1} | {:.3} | {:.3} | {:+.2} | {:+.2} | {:+.3}% | {:+.2} | {:.1} : 1 |", + row.share, + row.honest, + row.p_grind_success, + row.withheld_no_delay, + row.gain_no_delay, + row.mc_gain_no_delay, + 100.0 * row.gain_no_delay / row.honest, + row.gain_with_delay, + (row.gain_no_delay + row.withheld_no_delay) / row.withheld_no_delay + ); + } + println!("\ngain:cost = gross gain from the better program divided by the block rewards burned withholding."); + println!("With the delay the candidate's program is unknown for {} s, {}x the decision window, so the grinder must publish: gain 0, nothing burned.", + EPOCH_DELAY_S, EPOCH_DELAY_S / DECISION_WINDOW_S); +} diff --git a/sim/__pycache__/finality_v2.cpython-310.pyc b/sim/__pycache__/finality_v2.cpython-310.pyc deleted file mode 100644 index ec0d5a41c6f005da05614f68123b0ea58bff8175..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42840 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