Merge pull request #9 from logos-co/pol/risc0

Proof of Leadership in risc0
This commit is contained in:
davidrusu 2024-07-25 16:29:09 +04:00 committed by GitHub
commit 182db86af3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 418 additions and 7 deletions

4
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "proof_of_leadership/circomlib"]
path = proof_of_leadership/circomlib
[submodule "proof_of_leadership/circom/circomlib"]
path = proof_of_leadership/circom/circomlib
url = https://github.com/iden3/circomlib.git

View File

@ -41,6 +41,15 @@ impl InputWitness {
}
}
pub fn evolve_output(&self, balance_blinding: BalanceWitness) -> crate::OutputWitness {
crate::OutputWitness {
note: self.note,
balance_blinding,
nf_pk: self.nf_sk.commit(),
nonce: self.nonce.evolve(&self.nf_sk),
}
}
pub fn nullifier(&self) -> Nullifier {
Nullifier::new(self.nf_sk, self.nonce)
}

View File

@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
// Maintained privately by note holder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct NullifierSecret([u8; 16]);
pub struct NullifierSecret(pub [u8; 16]);
// Nullifier commitment is public information that
// can be provided to anyone wishing to transfer
@ -29,7 +29,7 @@ pub struct NullifierCommitment([u8; 32]);
// provide a nonce to differentiate notes controlled by the same
// secret. Each note is assigned a unique nullifier nonce.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct NullifierNonce([u8; 16]);
pub struct NullifierNonce([u8; 32]);
// The nullifier attached to input notes to prove an input has not
// already been spent.
@ -69,18 +69,28 @@ impl NullifierCommitment {
impl NullifierNonce {
pub fn random(mut rng: impl RngCore) -> Self {
let mut nonce = [0u8; 16];
let mut nonce = [0u8; 32];
rng.fill_bytes(&mut nonce);
Self(nonce)
}
pub fn as_bytes(&self) -> &[u8; 16] {
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn from_bytes(bytes: [u8; 16]) -> Self {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn evolve(&self, nf_sk: &NullifierSecret) -> Self {
let mut hasher = Sha256::new();
hasher.update(b"NOMOS_COIN_EVOLVE");
hasher.update(&self.0);
hasher.update(nf_sk.0);
let nonce_bytes: [u8; 32] = hasher.finalize().into();
Self(nonce_bytes)
}
}
impl Nullifier {

2
proof_of_leadership/risc0/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
Cargo.lock
target/

View File

@ -0,0 +1,11 @@
[workspace]
resolver = "2"
members = [ "prover", "proof_statements", "risc0_proofs"]
# Always optimize; building and running the risc0_proofs takes much longer without optimization.
[profile.dev]
opt-level = 3
[profile.release]
debug = 1
lto = true

View File

@ -0,0 +1,10 @@
[package]
name = "proof_statements"
version = "0.1.0"
edition = "2021"
[dependencies]
cl = { path = "../../../goas/cl/cl" }
serde = { version = "1.0", features = ["derive"] }
crypto-bigint = { version = "0.5.5", features = ["serde"] }
sha2 = "0.10"

View File

@ -0,0 +1 @@
pub mod proof_of_leadership;

View File

@ -0,0 +1,89 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crypto_bigint::{U256, Encoding, CheckedMul, CheckedSub};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaderPublic {
pub cm_root: [u8; 32],
pub epoch_nonce: [u8; 32],
pub slot: u64,
pub scaled_phi_approx: (U256, U256),
pub nullifier: cl::Nullifier,
pub updated_commitment: cl::NoteCommitment,
}
impl LeaderPublic {
pub fn new(
cm_root: [u8; 32],
epoch_nonce: [u8; 32],
slot: u64,
active_slot_coefficient: f64,
total_stake: u64,
nullifier: cl::Nullifier,
updated_commitment: cl::NoteCommitment
) -> Self
{
let total_stake_big = U256::from_u64(total_stake);
let total_stake_sq_big = total_stake_big.checked_mul(&total_stake_big).unwrap();
let double_total_stake_sq_big = total_stake_sq_big.checked_mul(&U256::from_u64(2)).unwrap();
let precision_u64 = u64::MAX;
let precision_big = U256::from_u64(u64::MAX);
let precision_f64 = precision_u64 as f64;
let order: U256 = U256::MAX;
let order_div_precision = order.checked_div(&precision_big).unwrap();
let order_div_precision_sq = order_div_precision.checked_div(&precision_big).unwrap()
;
let neg_f_ln: U256 = U256::from_u64(((-f64::ln(1f64 - active_slot_coefficient)) * precision_f64) as u64);
let neg_f_ln_sq = neg_f_ln.checked_mul(&neg_f_ln).unwrap();
let neg_f_ln_order: U256 = order_div_precision.checked_mul(&neg_f_ln).unwrap();
let t0 = neg_f_ln_order.checked_div(&total_stake_big).unwrap();
let t1 = order_div_precision_sq.checked_mul(&neg_f_ln_sq).unwrap().checked_div(&double_total_stake_sq_big).unwrap();
Self {
cm_root,
epoch_nonce,
slot,
nullifier,
updated_commitment,
scaled_phi_approx: (t0, t1),
}
}
pub fn check_winning(&self, input: &cl::InputWitness) -> bool {
let threshold = phi_approx(U256::from_u64(input.note.value), self.scaled_phi_approx);
let ticket = ticket(&input, self.epoch_nonce, self.slot);
ticket < threshold
}
}
fn phi_approx(stake: U256, approx: (U256, U256)) -> U256 {
// stake * (t0 - t1 * stake)
stake.checked_mul(&approx.0.checked_sub(&approx.1.checked_mul(&stake).unwrap()).unwrap()).unwrap()
}
fn ticket(input: &cl::InputWitness, epoch_nonce: [u8;32], slot: u64) -> U256 {
let mut hasher = Sha256::new();
hasher.update(b"NOMOS_LEAD");
hasher.update(epoch_nonce);
hasher.update(slot.to_be_bytes());
hasher.update(input.note_commitment().as_bytes());
hasher.update(input.nf_sk.0);
let ticket_bytes: [u8; 32] = hasher.finalize().into();
U256::from_be_bytes(ticket_bytes)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaderPrivate {
pub input: cl::InputWitness,
pub input_cm_path: Vec<cl::merkle::PathNode>,
}

View File

@ -0,0 +1,15 @@
[package]
name = "nomos_pol_prover"
version = "0.1.0"
edition = "2021"
[dependencies]
cl = { path = "../../../goas/cl/cl" }
proof_statements = { path = "../proof_statements" }
nomos_pol_risc0_proofs = { path = "../risc0_proofs" }
risc0-zkvm = { version = "1.0", features = ["prove", "metal"] }
risc0-groth16 = { version = "1.0" }
rand = "0.8.5"
rand_core = "0.6.0"
thiserror = "1.0.62"
curve25519-dalek = {version = "4.1", features = ["serde", "digest", "rand_core"]}

View File

@ -0,0 +1,9 @@
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("risc0 failed to serde")]
Risc0Serde(#[from] risc0_zkvm::serde::Error),
}

View File

@ -0,0 +1,182 @@
use crate::error::Result;
use curve25519_dalek::Scalar;
use proof_statements::proof_of_leadership::{LeaderPrivate, LeaderPublic};
const MAX_NOTE_COMMS: usize = 2usize.pow(8);
pub struct ProvedLeader {
pub leader: LeaderPublic,
pub risc0_receipt: risc0_zkvm::Receipt,
}
impl ProvedLeader {
pub fn prove(input: &cl::InputWitness, epoch_nonce: [u8;32], slot: u64, active_slot_coefficient: f64, total_stake: u64, note_commitments: &[cl::NoteCommitment]) -> Self {
let note_cm = input.note_commitment();
let cm_leaves = note_commitment_leaves(note_commitments);
let cm_idx = note_commitments
.iter()
.position(|c| c == &note_cm)
.unwrap();
let note_cm_path = cl::merkle::path(cm_leaves, cm_idx);
let cm_root = cl::merkle::root(cm_leaves);
let leader_private = LeaderPrivate {
input: *input,
input_cm_path: note_cm_path,
};
let leader_public = LeaderPublic::new(
cm_root,
epoch_nonce,
slot,
active_slot_coefficient,
total_stake,
input.nullifier(),
input.evolve_output(cl::BalanceWitness::new(Scalar::ZERO)).commit_note(),
);
let env = risc0_zkvm::ExecutorEnv::builder()
.write(&leader_public)
.unwrap()
.write(&leader_private)
.unwrap()
.build()
.unwrap();
// Obtain the default prover.
let prover = risc0_zkvm::default_prover();
let start_t = std::time::Instant::now();
// Proof information by proving the specified ELF binary.
// This struct contains the receipt along with statistics about execution of the guest
let opts = risc0_zkvm::ProverOpts::succinct();
let prove_info = prover
.prove_with_opts(env, nomos_pol_risc0_proofs::PROOF_OF_LEADERSHIP_ELF, &opts)
.unwrap();
println!(
"STARK prover time: {:.2?}, total_cycles: {}",
start_t.elapsed(),
prove_info.stats.total_cycles
);
// extract the receipt.
let receipt = prove_info.receipt;
Self {
leader: leader_public,
risc0_receipt: receipt,
}
}
pub fn public(&self) -> Result<LeaderPublic> {
Ok(self.risc0_receipt.journal.decode()?)
}
pub fn verify(&self) -> bool {
let Ok(proved_public_inputs) = self.public() else {
return false;
};
self.leader == proved_public_inputs
&& self
.risc0_receipt
.verify(nomos_pol_risc0_proofs::PROOF_OF_LEADERSHIP_ID)
.is_ok()
}
}
fn note_commitment_leaves(note_commitments: &[cl::NoteCommitment]) -> [[u8; 32]; MAX_NOTE_COMMS] {
let note_comm_bytes = Vec::from_iter(note_commitments.iter().map(|c| c.as_bytes().to_vec()));
let cm_leaves = cl::merkle::padded_leaves::<MAX_NOTE_COMMS>(&note_comm_bytes);
cm_leaves
}
#[cfg(test)]
mod test {
use rand::thread_rng;
use super::*;
#[test]
fn test_leader_prover() {
let mut rng = thread_rng();
let input = cl::InputWitness {
note: cl::NoteWitness::basic(32, "NMO"),
balance_blinding: cl::BalanceWitness::random(&mut rng),
nf_sk: cl::NullifierSecret::random(&mut rng),
nonce: cl::NullifierNonce::random(&mut rng),
};
let notes = vec![input.note_commitment()];
let epoch_nonce = [0u8; 32];
let slot = 0;
let active_slot_coefficient = 0.05;
let total_stake = 1000;
let mut expected_public_inputs = LeaderPublic::new(
cl::merkle::root(note_commitment_leaves(&notes)),
epoch_nonce,
slot,
active_slot_coefficient,
total_stake,
input.nullifier(),
input.evolve_output(cl::BalanceWitness::new(Scalar::ZERO)).commit_note(),
);
while !expected_public_inputs.check_winning(&input) {
expected_public_inputs.slot += 1;
}
println!("slot={}", expected_public_inputs.slot);
let proved_leader = ProvedLeader::prove(&input, expected_public_inputs.epoch_nonce, expected_public_inputs.slot, active_slot_coefficient, total_stake, &notes);
assert_eq!(proved_leader.leader, expected_public_inputs);
assert!(proved_leader.verify());
// let wrong_public_inputs = [
// InputPublic {
// cm_root: cl::merkle::root([cl::merkle::leaf(b"bad_root")]),
// ..expected_public_inputs
// },
// InputPublic {
// input: cl::Input {
// nullifier: cl::Nullifier::new(
// cl::NullifierSecret::random(&mut rng),
// cl::NullifierNonce::random(&mut rng),
// ),
// ..expected_public_inputs.input
// },
// ..expected_public_inputs
// },
// InputPublic {
// input: cl::Input {
// death_cm: cl::note::death_commitment(b"wrong death vk"),
// ..expected_public_inputs.input
// },
// ..expected_public_inputs
// },
// InputPublic {
// input: cl::Input {
// balance: cl::BalanceWitness::random(&mut rng)
// .commit(&cl::NoteWitness::basic(32, "NMO")),
// ..expected_public_inputs.input
// },
// ..expected_public_inputs
// },
// ];
// for wrong_input in wrong_public_inputs {
// proved_input.input = wrong_input;
// assert!(!proved_input.verify());
// }
}
}

View File

@ -0,0 +1,2 @@
pub mod error;
pub mod leader;

View File

@ -0,0 +1,10 @@
[package]
name = "nomos_pol_risc0_proofs"
version = "0.1.0"
edition = "2021"
[build-dependencies]
risc0-build = { version = "1.0" }
[package.metadata.risc0]
methods = ["proof_of_leadership"]

View File

@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}

View File

@ -0,0 +1,23 @@
[package]
name = "proof_of_leadership"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] }
serde = { version = "1.0", features = ["derive"] }
cl = { path = "../../../../goas/cl/cl" }
proof_statements = { path = "../../proof_statements" }
curve25519-dalek = {version = "4.1", features = ["serde", "digest", "rand_core"]}
sha2 = "0.10"
crypto-bigint = "0.5.5"
[patch.crates-io]
# add RISC Zero accelerator support for all downstream usages of the following crates.
sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" }
crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" }
curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" }

View File

@ -0,0 +1,34 @@
/// Proof of Leadership
use cl::merkle;
use curve25519_dalek::Scalar;
use proof_statements::proof_of_leadership::{LeaderPrivate, LeaderPublic};
use risc0_zkvm::guest::env;
fn main() {
let public_inputs: LeaderPublic = env::read();
let LeaderPrivate {
input,
input_cm_path,
} = env::read();
// Lottery checks
assert!(public_inputs.check_winning(&input));
// Ensure note is valid
let note_cm = input.note_commitment();
let note_cm_leaf = merkle::leaf(note_cm.as_bytes());
let note_cm_root = merkle::path_root(note_cm_leaf, &input_cm_path);
assert_eq!(note_cm_root, public_inputs.cm_root);
// Public input constraints
assert_eq!(input.nullifier(), public_inputs.nullifier);
let evolved_output = input.evolve_output(cl::BalanceWitness::new(Scalar::ZERO));
assert_eq!(evolved_output.commit_note(), public_inputs.updated_commitment);
env::commit(&public_inputs);
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));