diff --git a/Cargo.toml b/Cargo.toml index 887943d80..165843b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ postcard = { version = "1.1.1", features = ["use-std"] } zstd = "0.13.3" bytes = "1.10.1" proptest = "1.6.0" +zerocopy = "0.8.25" # Noir lang: make sure it matches installed version `noirup -C v1.0.0-beta.3` diff --git a/noir-r1cs/Cargo.toml b/noir-r1cs/Cargo.toml index a9eae8d9c..7cf12d065 100644 --- a/noir-r1cs/Cargo.toml +++ b/noir-r1cs/Cargo.toml @@ -26,6 +26,8 @@ postcard.workspace = true zstd.workspace = true bytes.workspace = true bytemuck.workspace = true +skyscraper = { path = "../skyscraper" } +zerocopy.workspace = true # Ark rand08 = { package = "rand", version = "0.8" } diff --git a/noir-r1cs/src/skyscraper/skyscraper_pow.rs b/noir-r1cs/src/skyscraper/skyscraper_pow.rs index 6ff041f1d..d5476baee 100644 --- a/noir-r1cs/src/skyscraper/skyscraper_pow.rs +++ b/noir-r1cs/src/skyscraper/skyscraper_pow.rs @@ -1,141 +1,31 @@ use { - crate::{ - skyscraper::skyscraper::{bigint_from_bytes_le, compress}, - utils::uint_to_field, - }, - ruint::{aliases::U256, uint}, + skyscraper::pow::{solve, verify}, spongefish_pow::PowStrategy, - whir::crypto::fields::Field256, + zerocopy::transmute, }; /// Skyscraper proof of work #[derive(Clone, Copy)] pub struct SkyscraperPoW { - challenge: Field256, - threshold: Field256, + challenge: [u64; 4], + bits: f64, } -const D0: Field256 = uint_to_field(uint!( - 21888242871839275222246405745257275088548364400416034343698204186575808495617_U256 -)); -const D1: Field256 = uint_to_field(uint!( - 10944121435919637611123202872628637544274182200208017171849102093287904247808_U256 -)); -const D2: Field256 = uint_to_field(uint!( - 5472060717959818805561601436314318772137091100104008585924551046643952123904_U256 -)); -const D3: Field256 = uint_to_field(uint!( - 2736030358979909402780800718157159386068545550052004292962275523321976061952_U256 -)); -const D4: Field256 = uint_to_field(uint!( - 1368015179489954701390400359078579693034272775026002146481137761660988030976_U256 -)); -const D5: Field256 = uint_to_field(uint!( - 684007589744977350695200179539289846517136387513001073240568880830494015488_U256 -)); -const D6: Field256 = uint_to_field(uint!( - 342003794872488675347600089769644923258568193756500536620284440415247007744_U256 -)); -const D7: Field256 = uint_to_field(uint!( - 171001897436244337673800044884822461629284096878250268310142220207623503872_U256 -)); -const D8: Field256 = uint_to_field(uint!( - 85500948718122168836900022442411230814642048439125134155071110103811751936_U256 -)); -const D9: Field256 = uint_to_field(uint!( - 42750474359061084418450011221205615407321024219562567077535555051905875968_U256 -)); -const D10: Field256 = uint_to_field(uint!( - 21375237179530542209225005610602807703660512109781283538767777525952937984_U256 -)); -const D11: Field256 = uint_to_field(uint!( - 10687618589765271104612502805301403851830256054890641769383888762976468992_U256 -)); -const D12: Field256 = uint_to_field(uint!( - 5343809294882635552306251402650701925915128027445320884691944381488234496_U256 -)); -const D13: Field256 = uint_to_field(uint!( - 2671904647441317776153125701325350962957564013722660442345972190744117248_U256 -)); -const D14: Field256 = uint_to_field(uint!( - 1335952323720658888076562850662675481478782006861330221172986095372058624_U256 -)); -const D15: Field256 = uint_to_field(uint!( - 667976161860329444038281425331337740739391003430665110586493047686029312_U256 -)); -const D16: Field256 = uint_to_field(uint!( - 333988080930164722019140712665668870369695501715332555293246523843014656_U256 -)); -const D17: Field256 = uint_to_field(uint!( - 166994040465082361009570356332834435184847750857666277646623261921507328_U256 -)); -const D18: Field256 = uint_to_field(uint!( - 83497020232541180504785178166417217592423875428833138823311630960753664_U256 -)); -const D19: Field256 = uint_to_field(uint!( - 41748510116270590252392589083208608796211937714416569411655815480376832_U256 -)); -const D20: Field256 = uint_to_field(uint!( - 20874255058135295126196294541604304398105968857208284705827907740188416_U256 -)); -const D21: Field256 = uint_to_field(uint!( - 10437127529067647563098147270802152199052984428604142352913953870094208_U256 -)); -const D22: Field256 = uint_to_field(uint!( - 5218563764533823781549073635401076099526492214302071176456976935047104_U256 -)); -const D23: Field256 = uint_to_field(uint!( - 2609281882266911890774536817700538049763246107151035588228488467523552_U256 -)); -const D24: Field256 = uint_to_field(uint!( - 1304640941133455945387268408850269024881623053575517794114244233761776_U256 -)); -const D25: Field256 = uint_to_field(uint!( - 652320470566727972693634204425134512440811526787758897057122116880888_U256 -)); -const D26: Field256 = uint_to_field(uint!( - 326160235283363986346817102212567256220405763393879448528561058440444_U256 -)); -const D27: Field256 = uint_to_field(uint!( - 163080117641681993173408551106283628110202881696939724264280529220222_U256 -)); - -const DIFFICULTY_ARRAY: [Field256; 28] = [ - D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D14, D15, D16, D17, D18, D19, D20, - D21, D22, D23, D24, D25, D26, D27, -]; - impl PowStrategy for SkyscraperPoW { fn new(challenge: [u8; 32], bits: f64) -> Self { assert!((0.0..60.0).contains(&bits), "bits must be smaller than 60"); - let threshold = bits.ceil() as usize; - Self { - challenge: Field256::new(bigint_from_bytes_le(&challenge)), - threshold: DIFFICULTY_ARRAY[threshold], + challenge: transmute!(challenge), + bits, } } fn check(&mut self, nonce: u64) -> bool { - let res = compress(self.challenge, uint_to_field(U256::from(nonce))); - res < self.threshold + verify(self.challenge, self.bits, nonce) } fn solve(&mut self) -> Option { - // TODO: Parallel solve - (0u64..) - .step_by(1) - .find_map(|nonce| self.check_single(nonce)) - } -} - -impl SkyscraperPoW { - fn check_single(&mut self, nonce: u64) -> Option { - let res = compress(self.challenge, uint_to_field(U256::from(nonce))); - if res < self.threshold { - return Some(nonce); - } - None + Some(solve(self.challenge, self.bits)) } } diff --git a/skyscraper/Cargo.toml b/skyscraper/Cargo.toml index 4b85b5fb5..178a9326e 100644 --- a/skyscraper/Cargo.toml +++ b/skyscraper/Cargo.toml @@ -13,9 +13,10 @@ block-multiplier = { path = "../block-multiplier" } fp-rounding = { path = "../fp-rounding" } ark-ff.workspace = true ark-bn254.workspace = true -zerocopy = "0.8.25" +zerocopy.workspace = true seq-macro = "0.3.6" proptest.workspace = true +rayon.workspace = true [dev-dependencies] rand.workspace = true diff --git a/skyscraper/benches/bench.rs b/skyscraper/benches/bench.rs index b165c2e74..f8d96716b 100644 --- a/skyscraper/benches/bench.rs +++ b/skyscraper/benches/bench.rs @@ -51,6 +51,47 @@ mod reduce { } } +#[divan::bench_group] +mod pow { + use {super::*, skyscraper::pow::solve}; + + #[divan::bench] + fn bits_05(bencher: Bencher) { + bencher + .with_inputs(|| rng().random()) + .bench_local_values(|challenge| solve(challenge, 05.0)) + } + + #[divan::bench] + fn bits_10(bencher: Bencher) { + bencher + .with_inputs(|| rng().random()) + .bench_local_values(|challenge| solve(challenge, 10.0)) + } + + #[divan::bench] + fn bits_15(bencher: Bencher) { + bencher + .with_inputs(|| rng().random()) + .bench_local_values(|challenge| solve(challenge, 15.0)) + } + + #[divan::bench] + fn bits_20(bencher: Bencher) { + bencher + .with_inputs(|| rng().random()) + .bench_local_values(|challenge| solve(challenge, 20.0)) + } + + #[divan::bench] + #[ignore] + fn bits_25(bencher: Bencher) { + bencher + .with_inputs(|| rng().random()) + .bench_local_values(|challenge| solve(challenge, 25.0)) + } +} + #[divan::bench_group] mod compress_many { use super::*; diff --git a/skyscraper/src/lib.rs b/skyscraper/src/lib.rs index d3fa92b5d..1472cdebe 100644 --- a/skyscraper/src/lib.rs +++ b/skyscraper/src/lib.rs @@ -7,11 +7,21 @@ pub mod bar; pub mod block3; pub mod block4; pub mod constants; +pub mod pow; pub mod reduce; pub mod reference; pub mod simple; pub mod v1; +/// The least common multiple of the implementation widths. +/// +/// Doing this many compressions in parallel will make optimal use of resources +/// in all implementations. +/// +/// Note you might want to pick a multiple as block size to amortize the setting +/// of rounding mode. +pub const WIDTH_LCM: usize = 12; + pub type CompressManyFn = fn(&[u8], &mut [u8]); // TODO: Some autotune method that does a small benchmark on target hardware and diff --git a/skyscraper/src/pow.rs b/skyscraper/src/pow.rs new file mode 100644 index 000000000..b84f05234 --- /dev/null +++ b/skyscraper/src/pow.rs @@ -0,0 +1,147 @@ +use { + crate::{arithmetic::less_than, simple::compress, WIDTH_LCM}, + ark_ff::Zero, + core::{ + array, + sync::atomic::{AtomicU64, Ordering}, + }, + rayon, + zerocopy::IntoBytes as _, +}; + +const PROVER_BIAS: f64 = 0.01; + +/// Returns a threshold for a given security target in bits. +/// +/// The probability that a uniform random element from the field is less than +/// the threshold is at least 2 ^ -difficulty. i.e.: +/// +/// |{x:F | x < threshold}| / |F| < 2^-difficulty +pub fn threshold(difficulty: f64) -> [u64; 4] { + assert!( + (0.0..80.0).contains(&difficulty), + "Difficulty must be in the range [0, 80)" + ); + let modulus = (crate::constants::MODULUS[1][3] as f64) * 2.0f64.powi(192); + let prob = (-difficulty).exp2(); + f64_to_u256(prob * modulus) +} + +pub fn verify(challenge: [u64; 4], difficulty: f64, nonce: u64) -> bool { + difficulty.is_zero() || less_than(compress(challenge, [nonce, 0, 0, 0]), threshold(difficulty)) +} + +/// Multi-threaded proof of work solver. +/// +/// It will add a slight bias to the difficulty to make sure the prover +/// threshold is higher than the verifier threshold and there are not rounding +/// issues affecting completeness. +pub fn solve(challenge: [u64; 4], difficulty: f64) -> u64 { + const WIDTH: usize = WIDTH_LCM * 10; + if difficulty.is_zero() { + return 0; + } + let threshold = threshold(difficulty + PROVER_BIAS); + let compress_many = crate::block4::compress_many; // TODO: autotune + let best = AtomicU64::new(u64::MAX); + rayon::broadcast(|ctx| { + let mut input: [[[u64; 4]; 2]; WIDTH] = array::from_fn(|_| [challenge, [0; 4]]); + let mut hashes = [[0_u64; 4]; WIDTH]; + + // Find the thread specific subset of nonces + for nonce in (0..) + .step_by(WIDTH) + .skip(ctx.index()) + .step_by(ctx.num_threads()) + { + // Stop if another thread found a better solution + if nonce > best.load(Ordering::Acquire) { + return; + } + for i in 0..WIDTH { + input[i][1][0] = nonce + i as u64; + } + compress_many(input.as_bytes(), hashes.as_mut_bytes()); + for i in 0..WIDTH { + if less_than(hashes[i], threshold) { + best.fetch_min(nonce + i as u64, Ordering::AcqRel); + return; + } + } + } + }); + let nonce = best.load(Ordering::Acquire); + debug_assert!(verify(challenge, difficulty, nonce)); + nonce +} + +/// Returns sign, exponent and significand of an `f64` +/// +/// The significand has the implicit leading one added for normal floats. +fn f64_parts(f: f64) -> (bool, i16, u64) { + let bits = f.to_bits(); + let sign = (bits >> 63) != 0; + let exp_bits = ((bits >> 52) & 0x7ff) as i16; + let frac = bits & ((1 << 52) - 1); + if exp_bits == 0 { + // Subnormals and zero (no implicit 1) + (sign, -1022, frac) + } else { + // Normal: add the implicit 1 at bit 52 + (sign, exp_bits - 1023, frac + (1 << 52)) + } +} + +/// Convert a float to the nearest u256, clamping to zero and MAX. +fn f64_to_u256(f: f64) -> [u64; 4] { + let (sign, exp, significand) = f64_parts(f); + if sign { + return [0; 4]; + } + if exp > 256 { + return [u64::MAX; 4]; + } + let mut result = [0; 4]; + let shift = exp - 52; + if shift < 0 { + result[0] = f.round() as u64; + } else { + let shift = shift as u32; + let (limb, shift) = ((shift / 64) as usize, shift % 64); + result[limb] = significand << shift; + if shift != 0 && limb < 3 { + result[limb + 1] = significand >> (64 - shift); + } + } + result +} + +#[cfg(test)] +mod tests { + use {super::*, core::f64}; + + #[test] + fn test_f64_to_u256() { + assert_eq!(f64_to_u256(0.0), [0; 4]); + assert_eq!(f64_to_u256(f64::MIN), [0; 4]); + assert_eq!(f64_to_u256(0.49), [0; 4]); + assert_eq!(f64_to_u256(0.50), [1, 0, 0, 0]); + assert_eq!(f64_to_u256(1.0), [1, 0, 0, 0]); + assert_eq!(f64_to_u256(2.0_f64.powi(128)), [0, 0, 1, 0]); + assert_eq!(f64_to_u256(f64::INFINITY), [u64::MAX; 4]); + assert_eq!(f64_to_u256(-42.0), [0; 4]); + assert_eq!( + f64_to_u256(f64::from_bits(0x7ff0000000000001)), + [u64::MAX; 4] + ); // NaN + } + + #[test] + fn test_solve_verify() { + for difficulty in [0.0_f64, f64::consts::PI] { + let challenge = [u64::MAX; 4]; + let nonce = solve(challenge, difficulty); + assert!(verify(challenge, difficulty, nonce)); + } + } +}