Project file: commit email is the GitHub-matched address everywhere
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
6b942f4fbd
commit
5f0efda007
2 changed files with 251 additions and 0 deletions
138
proto-vdf/src/grind.rs
Normal file
138
proto-vdf/src/grind.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
//! Seed-grinding model.
|
||||
//!
|
||||
//! One epoch is one hourly program. The program is drawn from a seed that depends on a
|
||||
//! certified checkpoint, and the miner who produces the last block before the checkpoint
|
||||
//! decides whether that block is published. Each block the miner produces at that point is a
|
||||
//! candidate seed.
|
||||
//!
|
||||
//! Model (from the review's measurement): a miner's hash-rate advantage on a given program is
|
||||
//! uniform on [0, ADV_MAX]. Honest miners get a random draw. A grinder with no delay compiles
|
||||
//! and benchmarks the candidate's program inside the 2 second decision window and withholds
|
||||
//! the block unless the advantage is in the top quartile, paying one block reward per
|
||||
//! withheld block. With probability (1 - s) somebody else's block becomes the seed and the
|
||||
//! grinder is stuck with a random draw.
|
||||
//!
|
||||
//! Revenue in an epoch with advantage a, for a miner with share s of the network:
|
||||
//! blocks * s(1+a) / (s(1+a) + (1-s)) = blocks * s(1+a) / (1 + s a).
|
||||
//!
|
||||
//! With the delay the grinder cannot learn the program inside the window (the VDF takes
|
||||
//! 10 minutes on a reference core), so it must publish and its gain is zero. Withholding
|
||||
//! blindly has the same expected program and only costs the block.
|
||||
|
||||
pub const BLOCKS_PER_EPOCH: f64 = 3600.0;
|
||||
pub const ADV_MAX: f64 = 0.15;
|
||||
pub const TOP_QUARTILE: f64 = 0.75 * ADV_MAX;
|
||||
|
||||
pub struct Row {
|
||||
pub share: f64,
|
||||
pub honest: f64,
|
||||
pub gain_no_delay: f64,
|
||||
pub withheld_no_delay: f64,
|
||||
pub gain_with_delay: f64,
|
||||
pub mc_gain_no_delay: f64,
|
||||
pub mc_withheld: f64,
|
||||
pub p_grind_success: f64,
|
||||
}
|
||||
|
||||
fn revenue(share: f64, a: f64) -> f64 {
|
||||
BLOCKS_PER_EPOCH * share * (1.0 + a) / (1.0 + share * a)
|
||||
}
|
||||
|
||||
/// Expected revenue for a uniform draw on [lo, hi], by midpoint integration.
|
||||
fn expected_revenue(share: f64, lo: f64, hi: f64) -> f64 {
|
||||
let n = 20_000;
|
||||
let w = (hi - lo) / n as f64;
|
||||
(0..n).map(|i| revenue(share, lo + (i as f64 + 0.5) * w)).sum::<f64>() / n as f64
|
||||
}
|
||||
|
||||
pub fn analytic(share: f64) -> Row {
|
||||
let p_keep = 0.25 * share; // own block and top quartile
|
||||
let p_withhold = 0.75 * share;
|
||||
let p_other = 1.0 - share;
|
||||
let p_top = p_keep / (1.0 - p_withhold);
|
||||
let p_random = p_other / (1.0 - p_withhold);
|
||||
let e_withheld = p_withhold / (1.0 - p_withhold);
|
||||
let honest = expected_revenue(share, 0.0, ADV_MAX);
|
||||
let top = expected_revenue(share, TOP_QUARTILE, ADV_MAX);
|
||||
let gain = p_top * top + p_random * honest - honest - e_withheld;
|
||||
Row {
|
||||
share,
|
||||
honest,
|
||||
gain_no_delay: gain,
|
||||
withheld_no_delay: e_withheld,
|
||||
gain_with_delay: 0.0,
|
||||
mc_gain_no_delay: 0.0,
|
||||
mc_withheld: 0.0,
|
||||
p_grind_success: p_top,
|
||||
}
|
||||
}
|
||||
|
||||
struct Xoshiro(u64, u64, u64, u64);
|
||||
impl Xoshiro {
|
||||
fn new(seed: u64) -> Self {
|
||||
let mut s = seed;
|
||||
let mut next = || {
|
||||
s = s.wrapping_add(0x9E3779B97F4A7C15);
|
||||
let mut z = s;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
|
||||
z ^ (z >> 31)
|
||||
};
|
||||
Xoshiro(next(), next(), next(), next())
|
||||
}
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let result = self.1.wrapping_mul(5).rotate_left(7).wrapping_mul(9);
|
||||
let t = self.1 << 17;
|
||||
self.2 ^= self.0;
|
||||
self.3 ^= self.1;
|
||||
self.1 ^= self.2;
|
||||
self.0 ^= self.3;
|
||||
self.2 ^= t;
|
||||
self.3 = self.3.rotate_left(45);
|
||||
result
|
||||
}
|
||||
fn uniform(&mut self) -> f64 {
|
||||
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Monte Carlo over `epochs` epochs. Returns (mean gain in blocks, mean withheld blocks).
|
||||
pub fn monte_carlo(share: f64, epochs: u64, seed: u64) -> (f64, f64) {
|
||||
let mut rng = Xoshiro::new(seed ^ (share * 1e6) as u64);
|
||||
let honest = expected_revenue(share, 0.0, ADV_MAX);
|
||||
let mut total_gain = 0.0;
|
||||
let mut total_withheld = 0u64;
|
||||
for _ in 0..epochs {
|
||||
let mut withheld = 0u64;
|
||||
let a;
|
||||
loop {
|
||||
let mine = rng.uniform() < share;
|
||||
let draw = rng.uniform() * ADV_MAX;
|
||||
if !mine {
|
||||
a = draw;
|
||||
break;
|
||||
}
|
||||
if draw >= TOP_QUARTILE {
|
||||
a = draw;
|
||||
break;
|
||||
}
|
||||
withheld += 1;
|
||||
}
|
||||
total_gain += revenue(share, a) - honest - withheld as f64;
|
||||
total_withheld += withheld;
|
||||
}
|
||||
(total_gain / epochs as f64, total_withheld as f64 / epochs as f64)
|
||||
}
|
||||
|
||||
pub fn table(epochs: u64) -> Vec<Row> {
|
||||
[0.1, 0.2, 0.3, 0.4]
|
||||
.iter()
|
||||
.map(|&s| {
|
||||
let mut row = analytic(s);
|
||||
let (g, w) = monte_carlo(s, epochs, 0x1A9E);
|
||||
row.mc_gain_no_delay = g;
|
||||
row.mc_withheld = w;
|
||||
row
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
113
proto-vdf/src/seed.rs
Normal file
113
proto-vdf/src/seed.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//! Seed pipeline: certified checkpoint hash -> VDF -> program seed.
|
||||
//!
|
||||
//! program_seed = SHA256("igneum-program-seed" || checkpoint_hash || T || serialize(y))
|
||||
//! where y = x^(2^T) in the class group whose discriminant is derived from the checkpoint
|
||||
//! hash, and x is the group's generator (2, 1, (1-D)/8). Because D is fresh per checkpoint
|
||||
//! nobody can precompute anything before the checkpoint is certified, and because the group
|
||||
//! order is unknown nobody can shortcut the T squarings.
|
||||
//!
|
||||
//! The RSA stand-in path exists for timing comparison only.
|
||||
|
||||
use crate::classgroup::{ClassGroup, Form};
|
||||
use crate::group::Group;
|
||||
use crate::hash::sha256;
|
||||
use crate::rsa::RsaGroup;
|
||||
use crate::wesolowski::{self, Proof};
|
||||
|
||||
pub const DISCRIMINANT_BITS: u32 = 1024;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EpochProof {
|
||||
pub t: u64,
|
||||
pub y: Vec<u8>,
|
||||
pub pi: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EpochProof {
|
||||
pub fn wire_size(&self) -> usize {
|
||||
8 + self.y.len() + self.pi.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_group_for(checkpoint_hash: &[u8; 32]) -> ClassGroup {
|
||||
let mut seed = Vec::with_capacity(64);
|
||||
seed.extend_from_slice(b"igneum-epoch-discriminant");
|
||||
seed.extend_from_slice(checkpoint_hash);
|
||||
ClassGroup::from_seed(&seed, DISCRIMINANT_BITS)
|
||||
}
|
||||
|
||||
fn derive_seed(checkpoint_hash: &[u8; 32], t: u64, y_bytes: &[u8]) -> [u8; 32] {
|
||||
sha256(&[b"igneum-program-seed", checkpoint_hash, &t.to_be_bytes(), y_bytes])
|
||||
}
|
||||
|
||||
/// Run the delay and produce (program_seed, proof). Anyone can do this; it takes T squarings.
|
||||
pub fn epoch_seed(checkpoint_hash: &[u8; 32], t: u64, threads: usize) -> ([u8; 32], EpochProof) {
|
||||
let g = class_group_for(checkpoint_hash);
|
||||
let x: Form = g.generator();
|
||||
let spacing = wesolowski::choose_spacing(t, 1 << 16);
|
||||
let kappa = pick_kappa(spacing);
|
||||
let spacing = spacing - spacing % kappa as u64;
|
||||
let spacing = spacing.max(kappa as u64);
|
||||
let ev = wesolowski::eval(&g, &x, t, spacing);
|
||||
let proof = wesolowski::prove(&g, &x, &ev, kappa, threads);
|
||||
let y_bytes = g.serialize(&proof.y);
|
||||
let seed = derive_seed(checkpoint_hash, t, &y_bytes);
|
||||
(seed, EpochProof { t, y: y_bytes, pi: g.serialize(&proof.pi) })
|
||||
}
|
||||
|
||||
/// Verify a (checkpoint, seed, proof) triple in milliseconds without running the delay.
|
||||
pub fn verify_epoch_seed(checkpoint_hash: &[u8; 32], program_seed: &[u8; 32], proof: &EpochProof) -> bool {
|
||||
let g = class_group_for(checkpoint_hash);
|
||||
let x = g.generator();
|
||||
let y = match g.deserialize(&proof.y) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
let pi = match g.deserialize(&proof.pi) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
if !wesolowski::verify(&g, &x, &Proof { y, pi }, proof.t) {
|
||||
return false;
|
||||
}
|
||||
derive_seed(checkpoint_hash, proof.t, &proof.y) == *program_seed
|
||||
}
|
||||
|
||||
/// Same pipeline over the RSA stand-in, for timing comparison only.
|
||||
pub fn epoch_seed_rsa(g: &RsaGroup, checkpoint_hash: &[u8; 32], t: u64, threads: usize) -> ([u8; 32], EpochProof) {
|
||||
let x = g.hash_to_elem(checkpoint_hash);
|
||||
let spacing = wesolowski::choose_spacing(t, 1 << 16);
|
||||
let kappa = pick_kappa(spacing);
|
||||
let spacing = (spacing - spacing % kappa as u64).max(kappa as u64);
|
||||
let ev = wesolowski::eval(g, &x, t, spacing);
|
||||
let proof = wesolowski::prove(g, &x, &ev, kappa, threads);
|
||||
let y_bytes = g.serialize(&proof.y);
|
||||
let seed = derive_seed(checkpoint_hash, t, &y_bytes);
|
||||
(seed, EpochProof { t, y: y_bytes, pi: g.serialize(&proof.pi) })
|
||||
}
|
||||
|
||||
pub fn verify_epoch_seed_rsa(g: &RsaGroup, checkpoint_hash: &[u8; 32], program_seed: &[u8; 32], proof: &EpochProof) -> bool {
|
||||
let x = g.hash_to_elem(checkpoint_hash);
|
||||
let y = match g.deserialize(&proof.y) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
let pi = match g.deserialize(&proof.pi) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
if !wesolowski::verify(g, &x, &Proof { y, pi }, proof.t) {
|
||||
return false;
|
||||
}
|
||||
derive_seed(checkpoint_hash, proof.t, &proof.y) == *program_seed
|
||||
}
|
||||
|
||||
/// Digit width: 12 bits when the spacing allows it, smaller for tiny T.
|
||||
pub fn pick_kappa(spacing: u64) -> u32 {
|
||||
for k in [12u32, 10, 8, 6, 4, 2, 1] {
|
||||
if spacing >= (k as u64) * 4 || k == 1 {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
Loading…
Reference in a new issue