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 ec0d5a41..00000000 Binary files a/sim/__pycache__/finality_v2.cpython-310.pyc and /dev/null differ