-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for different strategies for PoW. (#17)
In some contexts, you want to use different strategies for PoW. For example, in an EVM context you would like to use Keccak hashes. In a recursion context, Poseidon, ecc ecc. This supports it.
- Loading branch information
1 parent
9b3793f
commit 1654090
Showing
3 changed files
with
220 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
use super::PowStrategy; | ||
|
||
#[derive(Clone, Copy)] | ||
pub struct KeccakPoW { | ||
challenge: [u64; 4], | ||
threshold: u64, | ||
state: [u64; 25], | ||
} | ||
|
||
impl PowStrategy for KeccakPoW { | ||
fn new(challenge: [u8; 32], bits: f64) -> Self { | ||
let threshold = (64.0 - bits).exp2().ceil() as u64; | ||
Self { | ||
challenge: bytemuck::cast(challenge), | ||
threshold, | ||
state: [0; 25], | ||
} | ||
} | ||
|
||
fn check(&mut self, nonce: u64) -> bool { | ||
self.state[..4].copy_from_slice(&self.challenge); | ||
self.state[4] = nonce; | ||
for s in self.state.iter_mut().skip(5) { | ||
*s = 0; | ||
} | ||
keccak::f1600(&mut self.state); | ||
self.state[0] < self.threshold | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_pow_keccak() { | ||
use crate::{ | ||
plugins::pow::{PoWChallenge, PoWIOPattern}, | ||
ByteIOPattern, ByteReader, ByteWriter, IOPattern, | ||
}; | ||
|
||
const BITS: f64 = 10.0; | ||
|
||
let iopattern = IOPattern::new("the proof of work lottery 🎰") | ||
.add_bytes(1, "something") | ||
.challenge_pow("rolling dices"); | ||
|
||
let mut prover = iopattern.to_merlin(); | ||
prover.add_bytes(b"\0").expect("Invalid IOPattern"); | ||
prover.challenge_pow::<KeccakPoW>(BITS).unwrap(); | ||
|
||
let mut verifier = iopattern.to_arthur(prover.transcript()); | ||
let byte = verifier.next_bytes::<1>().unwrap(); | ||
assert_eq!(&byte, b"\0"); | ||
verifier.challenge_pow::<KeccakPoW>(BITS).unwrap(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
mod blake3; | ||
mod keccak; | ||
|
||
use crate::{ | ||
Arthur, ByteChallenges, ByteIOPattern, ByteReader, ByteWriter, IOPattern, Merlin, ProofError, | ||
ProofResult, | ||
}; | ||
/// [`IOPattern`] for proof-of-work challenges. | ||
pub trait PoWIOPattern { | ||
/// Adds a [`PoWChal`] to the [`IOPattern`]. | ||
/// | ||
/// In order to squeeze a proof-of-work challenge, we extract a 32-byte challenge using | ||
/// the byte interface, and then we find a 16-byte nonce that satisfies the proof-of-work. | ||
/// The nonce a 64-bit integer encoded as an unsigned integer and written in big-endian and added | ||
/// to the protocol transcript as the nonce for the proof-of-work. | ||
/// | ||
/// The number of bits used for the proof of work are **not** encoded within the [`IOPattern`]. | ||
/// It is up to the implementor to change the domain separator or the label in order to reflect changes in the proof | ||
/// in order to preserve simulation extractability. | ||
fn challenge_pow(self, label: &str) -> Self; | ||
} | ||
|
||
impl PoWIOPattern for IOPattern { | ||
fn challenge_pow(self, label: &str) -> Self { | ||
// 16 bytes challenge and 16 bytes nonce (that will be written) | ||
self.challenge_bytes(32, label).add_bytes(8, "pow-nonce") | ||
} | ||
} | ||
|
||
pub trait PoWChallenge { | ||
/// Extension trait for generating a proof-of-work challenge. | ||
fn challenge_pow<S: PowStrategy>(&mut self, bits: f64) -> ProofResult<()>; | ||
} | ||
|
||
impl PoWChallenge for Merlin | ||
where | ||
Merlin: ByteWriter, | ||
{ | ||
fn challenge_pow<S: PowStrategy>(&mut self, bits: f64) -> ProofResult<()> { | ||
let challenge = self.challenge_bytes()?; | ||
let nonce = S::new(challenge, bits) | ||
.solve() | ||
.ok_or(ProofError::InvalidProof)?; | ||
self.add_bytes(&nonce.to_be_bytes())?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl<'a> PoWChallenge for Arthur<'a> | ||
where | ||
Arthur<'a>: ByteReader, | ||
{ | ||
fn challenge_pow<S: PowStrategy>(&mut self, bits: f64) -> ProofResult<()> { | ||
let challenge = self.challenge_bytes()?; | ||
let nonce = u64::from_be_bytes(self.next_bytes()?); | ||
if S::new(challenge, bits).check(nonce) { | ||
Ok(()) | ||
} else { | ||
Err(ProofError::InvalidProof) | ||
} | ||
} | ||
} | ||
|
||
pub trait PowStrategy: Clone + Sync { | ||
/// Creates a new proof-of-work challenge. | ||
/// The `challenge` is a 32-byte array that represents the challenge. | ||
/// The `bits` is the binary logarithm of the expected amount of work. | ||
/// When `bits` is large (i.e. close to 64), a valid solution may not be found. | ||
fn new(challenge: [u8; 32], bits: f64) -> Self; | ||
|
||
/// Check if the `nonce` satisfies the challenge. | ||
fn check(&mut self, nonce: u64) -> bool; | ||
|
||
/// Finds the minimal `nonce` that satisfies the challenge. | ||
#[cfg(not(feature = "parallel"))] | ||
fn solve(&mut self) -> Option<u64> { | ||
// TODO: Parallel default impl | ||
(0u64..).find_map(|nonce| if self.check(nonce) { Some(nonce) } else { None }) | ||
} | ||
|
||
#[cfg(feature = "parallel")] | ||
fn solve(&mut self) -> Option<u64> { | ||
// Split the work across all available threads. | ||
// Use atomics to find the unique deterministic lowest satisfying nonce. | ||
|
||
use std::sync::atomic::{AtomicU64, Ordering}; | ||
|
||
use rayon::broadcast; | ||
let global_min = AtomicU64::new(u64::MAX); | ||
let _ = broadcast(|ctx| { | ||
let mut worker = self.clone(); | ||
let nonces = (ctx.index() as u64..).step_by(ctx.num_threads()); | ||
for nonce in nonces { | ||
// Use relaxed ordering to eventually get notified of another thread's solution. | ||
// (Propagation delay should be in the order of tens of nanoseconds.) | ||
if nonce >= global_min.load(Ordering::Relaxed) { | ||
break; | ||
} | ||
if worker.check(nonce) { | ||
// We found a solution, store it in the global_min. | ||
// Use fetch_min to solve race condition with simultaneous solutions. | ||
global_min.fetch_min(nonce, Ordering::SeqCst); | ||
break; | ||
} | ||
} | ||
}); | ||
match global_min.load(Ordering::SeqCst) { | ||
u64::MAX => self.check(u64::MAX).then_some(u64::MAX), | ||
nonce => Some(nonce), | ||
} | ||
} | ||
} |