init
Signed-off-by: Rin Cat (鈴猫) <rincat@rincat.dev>
This commit is contained in:
commit
4b71ad4d75
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "chidori-pow"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
num-traits = "0.2"
|
||||
num-integer = "0.1.46"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
cfg-if = "1.0.0"
|
||||
base64 = "0.22.1"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
num-bigint = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
num-bigint = { version="0.4", features = ["rand", "serde"] }
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2.1.1", features = ["std", "rand_core"] }
|
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 RinCat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
src/ed25519.rs
Normal file
28
src/ed25519.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use ed25519_dalek::Signature;
|
||||
use ed25519_dalek::Signer;
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
||||
pub struct ChallengeSigner {
|
||||
signing_key: SigningKey,
|
||||
}
|
||||
|
||||
impl ChallengeSigner {
|
||||
pub fn new() -> Self {
|
||||
let signing_key = SigningKey::generate(&mut rand::thread_rng());
|
||||
ChallengeSigner { signing_key }
|
||||
}
|
||||
|
||||
pub fn new_from_bytes(bytes: [u8; 32]) -> Self {
|
||||
let signing_key = SigningKey::from_bytes(&bytes);
|
||||
ChallengeSigner { signing_key }
|
||||
}
|
||||
|
||||
pub fn sign(&self, message: &[u8]) -> Signature {
|
||||
self.signing_key.sign(message)
|
||||
}
|
||||
|
||||
pub fn verify(&self, message: &[u8], signature: &[u8; 64]) -> bool {
|
||||
let signature_result = Signature::from_bytes(signature);
|
||||
self.signing_key.verify(message, &signature_result).is_ok()
|
||||
}
|
||||
}
|
401
src/lib.rs
Normal file
401
src/lib.rs
Normal file
@ -0,0 +1,401 @@
|
||||
pub mod sloth;
|
||||
|
||||
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||
use num_bigint::BigUint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
} else {
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use rand::Rng;
|
||||
pub mod prime;
|
||||
pub mod ed25519;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
static DIFFICULTY_SCALE: f64 = 0.04;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Puzzle {
|
||||
pub ticket: u64,
|
||||
pub issued_at: u64,
|
||||
pub prime: BigUint,
|
||||
pub challenge: BigUint,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct ChallengerBuilder {
|
||||
difficulty: Option<u32>,
|
||||
algorithm: Option<crate::sloth::Sloth>,
|
||||
signer: Option<crate::ed25519::ChallengeSigner>,
|
||||
valid_time_window: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl ChallengerBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
difficulty: None,
|
||||
algorithm: None,
|
||||
signer: None,
|
||||
valid_time_window: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_difficulty(mut self, difficulty: u32) -> Self {
|
||||
self.difficulty = Some((difficulty as f64 * DIFFICULTY_SCALE) as u32);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_prime(mut self, prime: BigUint) -> Self {
|
||||
self.algorithm = Some(crate::sloth::Sloth::new(prime));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_sign_key(mut self, sign_key: [u8; 32]) -> Self {
|
||||
self.signer = Some(crate::ed25519::ChallengeSigner::new_from_bytes(sign_key));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_valid_time_window(mut self, valid_time_window: u64) -> Self {
|
||||
self.valid_time_window = Some(valid_time_window);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Challenger {
|
||||
let difficulty = self
|
||||
.difficulty
|
||||
.unwrap_or((1000.0 * DIFFICULTY_SCALE) as u32);
|
||||
|
||||
let algorithm = self.algorithm.unwrap_or_else(|| {
|
||||
crate::sloth::Sloth::new(crate::prime::generate_prime_mod_3_4(4096, 64))
|
||||
});
|
||||
|
||||
let signer = self
|
||||
.signer
|
||||
.unwrap_or_else(|| crate::ed25519::ChallengeSigner::new());
|
||||
|
||||
let tickets_current = Arc::new(RwLock::new(HashSet::new()));
|
||||
let tickets_previous = Arc::new(RwLock::new(HashSet::new()));
|
||||
|
||||
let valid_time_window = self.valid_time_window.unwrap_or(300);
|
||||
let last_rotation = Arc::new(RwLock::new(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
));
|
||||
|
||||
Challenger {
|
||||
difficulty,
|
||||
algorithm,
|
||||
signer,
|
||||
tickets_current,
|
||||
tickets_previous,
|
||||
valid_time_window,
|
||||
last_rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct Challenger {
|
||||
pub difficulty: u32,
|
||||
algorithm: crate::sloth::Sloth,
|
||||
signer: crate::ed25519::ChallengeSigner,
|
||||
tickets_current: Arc<RwLock<HashSet<u64>>>,
|
||||
tickets_previous: Arc<RwLock<HashSet<u64>>>,
|
||||
valid_time_window: u64,
|
||||
last_rotation: Arc<RwLock<u64>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Challenger {
|
||||
pub fn set_difficulty(&mut self, difficulty: u32) {
|
||||
self.difficulty = (difficulty as f64 * DIFFICULTY_SCALE) as u32;
|
||||
}
|
||||
|
||||
pub fn issue_challenge(&mut self) -> String {
|
||||
let (_x, y, p) = self.algorithm.create(self.difficulty);
|
||||
|
||||
let puzzle = Puzzle {
|
||||
ticket: rand::thread_rng().gen(),
|
||||
issued_at: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
prime: p,
|
||||
challenge: y,
|
||||
difficulty: self.difficulty,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&puzzle).unwrap();
|
||||
let json_payload = BASE64_URL_SAFE.encode(json);
|
||||
let signature = self.signer.sign(json_payload.as_bytes());
|
||||
let signature_payload = BASE64_URL_SAFE.encode(signature.to_bytes());
|
||||
|
||||
let challenge = json_payload + "." + signature_payload.as_str();
|
||||
challenge
|
||||
}
|
||||
|
||||
pub fn verify_challenge(&mut self, challenge: &str, solution: &str) -> bool {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let need_rotation = {
|
||||
let last_rotation = self.last_rotation.read().unwrap();
|
||||
now - *last_rotation > self.valid_time_window
|
||||
};
|
||||
|
||||
if need_rotation {
|
||||
let mut last_rotation = self.last_rotation.write().unwrap();
|
||||
let mut tickets_previous = self.tickets_previous.write().unwrap();
|
||||
let mut tickets_current = self.tickets_current.write().unwrap();
|
||||
*tickets_previous = std::mem::take(&mut *tickets_current);
|
||||
*last_rotation = now;
|
||||
}
|
||||
|
||||
// Reject challenges that are too long
|
||||
if challenge.len() > 10240 || solution.len() > 1024 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split the challenge into the JSON payload and the signature
|
||||
let parts: Vec<&str> = challenge.split('.').collect();
|
||||
if parts.len() != 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let json_payload = parts[0];
|
||||
let signature_payload = parts[1];
|
||||
|
||||
// Decode the signature
|
||||
let signature_bytes: [u8; 64] = match BASE64_URL_SAFE.decode(signature_payload.as_bytes()) {
|
||||
Ok(bytes) => match bytes.try_into() {
|
||||
Ok(b) => b,
|
||||
Err(_) => return false,
|
||||
},
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Verify the signature on the JSON payload
|
||||
if !self
|
||||
.signer
|
||||
.verify(json_payload.as_bytes(), &signature_bytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode the JSON payload
|
||||
let puzzle: Puzzle = match BASE64_URL_SAFE.decode(json_payload.as_bytes()) {
|
||||
Ok(bytes) => match serde_json::from_slice(&bytes) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
},
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Reject challenges that are too old
|
||||
if puzzle.issued_at < now - self.valid_time_window {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject challenges that have already been solved
|
||||
{
|
||||
if self
|
||||
.tickets_current
|
||||
.read()
|
||||
.unwrap()
|
||||
.contains(&puzzle.ticket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if self
|
||||
.tickets_previous
|
||||
.read()
|
||||
.unwrap()
|
||||
.contains(&puzzle.ticket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the solution
|
||||
let solution: BigUint = match BASE64_URL_SAFE.decode(solution.as_bytes()) {
|
||||
Ok(bytes) => BigUint::from_bytes_be(&bytes),
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Verify the solution
|
||||
let solution_result =
|
||||
self.algorithm
|
||||
.verify(&solution, &puzzle.challenge, puzzle.difficulty);
|
||||
|
||||
if solution_result {
|
||||
self.tickets_current.write().unwrap().insert(puzzle.ticket);
|
||||
}
|
||||
|
||||
solution_result
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn solve_challenge(challenge: &str) -> String {
|
||||
let parts: Vec<&str> = challenge.split('.').collect();
|
||||
if parts.len() != 2 {
|
||||
return "ERROR".to_string();
|
||||
}
|
||||
|
||||
let json_payload = parts[0];
|
||||
let puzzle: Puzzle = match BASE64_URL_SAFE.decode(json_payload.as_bytes()) {
|
||||
Ok(bytes) => match serde_json::from_slice(&bytes) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return "ERROR".to_string(),
|
||||
},
|
||||
Err(_) => return "ERROR".to_string(),
|
||||
};
|
||||
|
||||
let y = puzzle.challenge.clone();
|
||||
let p = puzzle.prime.clone();
|
||||
let t = puzzle.difficulty;
|
||||
|
||||
let solution = crate::sloth::Sloth::decode(&y, &p, t);
|
||||
|
||||
BASE64_URL_SAFE.encode(solution.to_bytes_be())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_challenge() {
|
||||
let p = crate::prime::generate_prime_mod_3_4(4096, 64);
|
||||
println!("prime: {}", p);
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(p)
|
||||
.with_difficulty(1000)
|
||||
.build();
|
||||
let challenge = challenger.issue_challenge();
|
||||
let solution = solve_challenge(&challenge);
|
||||
println!("challenge: {}", challenge);
|
||||
println!("solution: {}", solution);
|
||||
assert!(challenger.verify_challenge(&challenge, &solution));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_incorrect() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.build();
|
||||
let challenge = challenger.issue_challenge();
|
||||
let mut solution = solve_challenge(&challenge);
|
||||
solution.replace_range(1..4, "abcd");
|
||||
assert_eq!(false, challenger.verify_challenge(&challenge, &solution));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_invaild_challenge() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.build();
|
||||
let mut challenge = challenger.issue_challenge();
|
||||
let solution = solve_challenge(&challenge);
|
||||
challenge.replace_range(1..4, "abcd");
|
||||
assert_eq!(false, challenger.verify_challenge(&challenge, &solution));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_invaild_signature() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.build();
|
||||
let mut challenge = challenger.issue_challenge();
|
||||
let solution = solve_challenge(&challenge);
|
||||
challenge.replace_range(challenge.len() - 5..challenge.len() - 1, "abcd");
|
||||
assert_eq!(false, challenger.verify_challenge(&challenge, &solution));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_reuse_attack() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.build();
|
||||
let challenge = challenger.issue_challenge();
|
||||
let solution = solve_challenge(&challenge);
|
||||
|
||||
assert!(challenger.verify_challenge(&challenge, &solution));
|
||||
assert_eq!(false, challenger.verify_challenge(&challenge, &solution));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_invaild_data() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
false,
|
||||
challenger.verify_challenge(&"".to_string(), &"".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
false,
|
||||
challenger.verify_challenge(&"rin".to_string(), &"cat".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_expired() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.with_valid_time_window(1)
|
||||
.build();
|
||||
|
||||
let challenge = challenger.issue_challenge();
|
||||
let solution = solve_challenge(&challenge);
|
||||
|
||||
thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
assert_eq!(false, challenger.verify_challenge(&challenge, &solution));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_challenge_tickets() {
|
||||
let mut challenger = ChallengerBuilder::new()
|
||||
.with_prime(crate::prime::generate_prime_mod_3_4(2048, 64))
|
||||
.with_valid_time_window(5)
|
||||
.build();
|
||||
|
||||
// Normal case
|
||||
let challenge = challenger.issue_challenge();
|
||||
let solution = solve_challenge(&challenge);
|
||||
|
||||
assert_eq!(true, challenger.verify_challenge(&challenge, &solution));
|
||||
|
||||
// expired case
|
||||
let challenge1 = challenger.issue_challenge();
|
||||
let solution1 = solve_challenge(&challenge1);
|
||||
|
||||
thread::sleep(std::time::Duration::from_secs(6));
|
||||
|
||||
// Normal case
|
||||
let challenge2 = challenger.issue_challenge();
|
||||
let solution2 = solve_challenge(&challenge2);
|
||||
|
||||
assert_eq!(false, challenger.verify_challenge(&challenge1, &solution1));
|
||||
assert_eq!(true, challenger.verify_challenge(&challenge2, &solution2));
|
||||
}
|
||||
}
|
161
src/prime.rs
Normal file
161
src/prime.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use num_bigint::{BigUint, RandBigInt, ToBigUint};
|
||||
use num_integer::Integer;
|
||||
use num_traits::One;
|
||||
use rand::thread_rng;
|
||||
|
||||
/// Miller–Rabin primality test
|
||||
/// https://en.wikipedia.org/wiki/Miller-Rabin_primality_test
|
||||
///
|
||||
/// `k` is the number of iterations (more iterations → higher confidence).
|
||||
/// This is a basic implementation for odd candidate `n` greater than 2.
|
||||
pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
||||
// Handle small numbers.
|
||||
if n == &2u32.to_biguint().unwrap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if n < &2u32.to_biguint().unwrap() || n.is_even() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write n − 1 as d * 2^r.
|
||||
let one: BigUint = One::one();
|
||||
let two: BigUint = 2u32.to_biguint().unwrap();
|
||||
|
||||
let n_minus_one = n - &one;
|
||||
|
||||
let mut d = n_minus_one.clone();
|
||||
let mut r = 0u32;
|
||||
|
||||
while d.is_even() {
|
||||
d /= &two;
|
||||
r += 1;
|
||||
}
|
||||
|
||||
// Try k random witnesses.
|
||||
let mut rng = thread_rng();
|
||||
|
||||
'witness_loop: for _ in 0..k {
|
||||
// Choose a random a in [2, n-2]
|
||||
let a = rng.gen_biguint_range(&two, &(n - &two));
|
||||
|
||||
// Compute x = a^d mod n.
|
||||
let mut x = a.modpow(&d, n);
|
||||
|
||||
if x == one || x == n_minus_one {
|
||||
continue 'witness_loop;
|
||||
}
|
||||
|
||||
// Repeat squaring x up to r − 1 times.
|
||||
for _ in 0..(r - 1) {
|
||||
x = x.modpow(&two, n);
|
||||
if x == n_minus_one {
|
||||
continue 'witness_loop;
|
||||
}
|
||||
}
|
||||
|
||||
// If we never hit n − 1, then composite.
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn generate_prime_mod_3_4(bits: u64, rounds: u32) -> BigUint {
|
||||
let mut rng = thread_rng();
|
||||
let four: BigUint = 4u32.to_biguint().unwrap();
|
||||
let three: BigUint = 3u32.to_biguint().unwrap();
|
||||
|
||||
loop {
|
||||
// Generate a random number of the required bit-length.
|
||||
let mut candidate = rng.gen_biguint(bits);
|
||||
|
||||
// Force the most significant bit to ensure it is exactly `bits` long.
|
||||
candidate.set_bit(bits - 1, true);
|
||||
// Force the number to be odd.
|
||||
candidate.set_bit(0, true);
|
||||
|
||||
// Adjust candidate so that candidate mod 4 == 3.
|
||||
// Compute candidate mod 4.
|
||||
let rem = &candidate % &four;
|
||||
if rem != three {
|
||||
// Calculate the adjustment needed.
|
||||
// We want candidate + adj ≡ 3 (mod 4); that is, adj ≡ (3 - rem) mod 4.
|
||||
let adj = if three >= rem {
|
||||
&three - &rem
|
||||
} else {
|
||||
&three + &four - &rem
|
||||
};
|
||||
candidate += &adj;
|
||||
}
|
||||
|
||||
// Now candidate mod 4 should equal 3.
|
||||
if &candidate % &four != three {
|
||||
continue; // Should not happen, but be safe.
|
||||
}
|
||||
|
||||
// Use Miller–Rabin to test candidate for primality.
|
||||
if is_probably_prime(&candidate, rounds) {
|
||||
return candidate;
|
||||
}
|
||||
// Otherwise, try another candidate.
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// This test has chance of false positives since it's probabilistic.
|
||||
#[test]
|
||||
fn test_is_probably_prime() {
|
||||
let primes: Vec<BigUint> = vec![
|
||||
BigUint::parse_bytes(b"723646214863847842402314246121044767400617866176733021174245260448070467161519753555151305391831172396032179266088879736498934532967238875067731186605319314486487094813782345277515046149035823394700558031365128080643117834402421935144013956523482034192169360458395261772557972018417296402072764848759", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"268263962333296278340388301081833650583348564229009436402694247537863120457689419730666587700766950987474095807568632415133217325374788947918148879191904084190506645642271822238922848768059332086841470078498514866531550241226838886983034780850983546266212727522552823301120544245546076442247019621281", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"780316979179005788838703471892068793371544768939561076378203820641657870400087138649016384860083841124919558376637164999621109684339044217326208775683279745886968384731662626620658025388083028243454834224644661932974516055783125538555017192574377520720286008648938568231979020364719643484647961902129", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"783671025198992682112959634964787905698619719557738373194519536366273453103042115144772610507228390203926825435595621395808116267173935555322692156198469029023864294375800526944184191931093378457858882431220722817775243587676744651835166948761514278051800658105050374249135721906202152414030775826309", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"114288406837499406547552732741383061052894863572677735626102568465969919170114402741547085384743856605779595326952640244199273564193341729111945397295523754661407891360178372372116312851760319552575377963248694758420962490016988707765874984334711407905393818994588867816690596598910287994599226773623", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"961901548037893583081258840295866034390773310176366253775445667810599851064250586050396339468859430600645234941487066774638210060293941746865942337333205982508422664447189855574952850359167644652778348230933876780699478432665987194022576502478048244264535150875567551113136844280086912348903874971289", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"810327961783413779923342902853291047828538610393840061747638982695517616363172610276265437838324082720309171557216227906072585908285037423898327771759304451581073313718250825722376959089099901152629294365285556214142316224671295775214712025501417340129162614998356447407556284614940830419343154596069", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"454714144122862528239309790149196885616593324828166685793258814905365447620370501511767118061263049961557700030750456903650018330037078841617668619030424125469578524711042124583419218861229437961029104448364686795365893249168551162671874789888134646816513322705015243576954880991642886515757494254667", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"299912922453745379330945382700853905095713797966780365239274055194104040679024227632579691012245229549449540626544013218573948479074373395273033811581117143614653378585775286148244890322917470335843727405772749436431799738228530579087880286816355000236580795379259714642327676881458540815945300666161", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"376003947134918788492667049006858112525261477602024892881243407277975729638079808355736777378039431492585839429794604176393826090578267002411236819553297345047777740428504027180799354733010246832276383653403857249078308266896127980484004668688670243017355407377733246174535348005452263544141849401979", 10).unwrap(),
|
||||
];
|
||||
|
||||
for p in primes.iter() {
|
||||
assert!(is_probably_prime(&p, 64));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_probably_prime_composites() {
|
||||
let composites: Vec<BigUint> = vec![
|
||||
BigUint::parse_bytes(b"376003947134918788492667049006858112525261477602024892881243407277975729638079808355736777378039431492585839429794604176393826090578267002411236819553297345047777740428504027180799354733010246832276383653403857249078308266896127980484004668688670243017355407377733246174535348005452263544141849401978", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"215231561558920190548208026347283855893349614579652886715109041246408707968244796209282350597895810733095104924634951652047270639587011501367107805226413278625593440341692931130648759932592989139786464960914139157114834353651728654113090571685005692273343539839816029944416477359630133856900278127196", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"749740332669263695852915686823796627478248903975029332069160620969447428491611565267586339403720492925006681992807328998947128762023955011291116754751950431300094251835880417511251321687004478207880237612242291350451356839420408200814283441229483559638270099621526259859618448556951589488935694922070", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"169634000834057905687114185586149242948139045677318813563962611831640519981755545740448510103217026033366710697221717847963076219297087916994382041061663714750161811176003440851268811309999453614154110754445690358783609141283608528146586702857821814537749897378979649737926839924019514134727091160596", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"391405423376058365603029493927540848246012397355482118997842261901662598117428494623814607612916906669639217808344280832395767938093281898200653708872133101732068607931282089810121182927184303240249371585726676226781755240078640338182784311458749444597839572385973551700960543208703821367720798778072", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"145039293606441299096899867755014610341218018203667264841882003871966393556065187657468909971071812669502147831045878456015615749981859567162502647235340765709384621383450245978156289943326007863506929089750917534169817946092363165262671439744821303778355013113872010222371955198429114197736184765704", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"338068116382585626410927100767745326752329150652235801367160858233547248828235927245043642851496668587705981602011646708907509708706737409743516100542296115834435744682149323258036975223060910642424686044835405231352594163800211063010578142263828931218684092895126045712869105445627601057623777552090", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"733953124218193165395301551993038545406364629775221343783594613066305176245028773006556151249847554247355736690261867825939967080139988834452933842378528382325603700037881199289040333697671258970839307337751547799788219379733886335171402446580393040976829094147103849993165402411253055490791297710398", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"913203980385282094763740857489413119990150673887971726908661546534253494037072205386715229605079865032590651982202933180796243044488005878787438030787026922448019409247593693512864665427511952738234654431289764435314410753974607679676812065736404760888000826732781882360969951775729150509866946286836", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"232672468411571864128377684111610045835673835338188777956578101157986193934981310713021354012943502884801943521144084387464428198189142123559463576210566925881741632803965311463508844160091019108613782710387285819179522210515150555890570342556044250014755588708967402313183475521240406597634471243814", 10).unwrap(),
|
||||
];
|
||||
|
||||
for c in composites.iter() {
|
||||
assert!(!is_probably_prime(&c, 64));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_prime_mod_3_4() {
|
||||
let bits = 2048;
|
||||
let rounds = 64;
|
||||
let prime = generate_prime_mod_3_4(bits, rounds);
|
||||
assert!(is_probably_prime(&prime, rounds));
|
||||
assert_eq!(prime.bits(), bits);
|
||||
assert_eq!(
|
||||
&prime % 4u32.to_biguint().unwrap(),
|
||||
3u32.to_biguint().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
111
src/sloth.rs
Normal file
111
src/sloth.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use num_bigint::{BigUint, ToBigUint};
|
||||
use num_traits::One;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rand::thread_rng;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use num_bigint::RandBigInt;
|
||||
|
||||
/// Sloth is a verifiable delay function (VDF) that is designed to enforce
|
||||
/// a predetermined amount of sequential work.
|
||||
|
||||
pub struct Sloth {
|
||||
/// The modulus `p` is a large prime number.
|
||||
pub p: BigUint,
|
||||
}
|
||||
|
||||
impl Sloth {
|
||||
/// Create a new Sloth VDF with the given modulus `p` and iteration count `t`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new(p: BigUint) -> Self {
|
||||
Sloth { p }
|
||||
}
|
||||
|
||||
/// Encode a message `x` using the Sloth VDF.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn encode(&self, x: &BigUint, t: u32) -> BigUint {
|
||||
let mut y = x.clone();
|
||||
|
||||
// Repeatedly square `x` T times mod `p`.
|
||||
for _ in 0..t {
|
||||
// y = y^2 mod p
|
||||
y = (&y * &y) % &self.p;
|
||||
}
|
||||
y
|
||||
}
|
||||
|
||||
/// Decode a message `y` using the Sloth VDF.
|
||||
pub fn decode(y: &BigUint, p: &BigUint, t: u32) -> BigUint {
|
||||
let mut x = y.clone();
|
||||
for _ in 0..t {
|
||||
x = Sloth::mod_sqrt(&x, p);
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// Create a new tuple `(x, y)` where `x` is a random secret number
|
||||
/// and `y` is the encoded challenge value.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn create(&self, t: u32) -> (BigUint, BigUint, BigUint) {
|
||||
let x = thread_rng().gen_biguint_range(&BigUint::one(), &self.p);
|
||||
let y = self.encode(&x, t);
|
||||
(x, y, self.p.clone())
|
||||
}
|
||||
|
||||
/// Verify that the encoded value `y` was computed from the secret `x`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn verify(&self, x: &BigUint, y: &BigUint, t: u32) -> bool {
|
||||
let check = self.encode(x, t);
|
||||
&check == y
|
||||
}
|
||||
|
||||
/// Compute a modular square root when p ≡ 3 mod 4:
|
||||
/// sqrt(a) mod p = a^((p+1)/4) mod p
|
||||
/// We'll pick the "lower root" consistently to ensure uniqueness.
|
||||
fn mod_sqrt(a: &BigUint, p: &BigUint) -> BigUint {
|
||||
// exponent = (p + 1) / 4
|
||||
let exp = (p + BigUint::one()) / 4_u32.to_biguint().unwrap();
|
||||
let root = a.modpow(&exp, p);
|
||||
|
||||
// A prime p ≡ 3 mod 4 has exactly two roots: `r` and `p-r`.
|
||||
// We'll choose the smaller one to ensure uniqueness.
|
||||
let p_minus_root = (p - &root) % p;
|
||||
if root <= p_minus_root {
|
||||
root
|
||||
} else {
|
||||
p_minus_root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sloth() {
|
||||
let p = crate::prime::generate_prime_mod_3_4(2048, 64);
|
||||
let t = 100;
|
||||
let sloth = Sloth::new(p);
|
||||
|
||||
let (x, y, p) = sloth.create(t);
|
||||
assert!(sloth.verify(&x, &y, t));
|
||||
|
||||
let decoded = Sloth::decode(&y, &p, t);
|
||||
assert!(sloth.verify(&decoded, &y, t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sloth_incorrect() {
|
||||
let p = crate::prime::generate_prime_mod_3_4(2048, 64);
|
||||
let t = 100;
|
||||
let sloth = Sloth::new(p);
|
||||
|
||||
let (x, y, _p) = sloth.create(t);
|
||||
assert!(sloth.verify(&x, &y, t));
|
||||
|
||||
let decoded = 1024_u32.to_biguint().unwrap();
|
||||
assert!(!sloth.verify(&decoded, &y, t));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user