From 7bcb6d004cb26db8abbd95f4aaef4986c8fdbd81 Mon Sep 17 00:00:00 2001 From: parazyd Date: Mon, 23 Jan 2023 19:42:27 +0100 Subject: [PATCH] research/rln: Full POC --- script/research/rln/.gitignore | 3 +- script/research/rln/Cargo.toml | 1 + script/research/rln/signal.zk | 34 ++++++ script/research/rln/src/main.rs | 209 ++++++++++++++++++++++---------- 4 files changed, 184 insertions(+), 63 deletions(-) create mode 100644 script/research/rln/signal.zk diff --git a/script/research/rln/.gitignore b/script/research/rln/.gitignore index b354aec7b..786e5f76c 100644 --- a/script/research/rln/.gitignore +++ b/script/research/rln/.gitignore @@ -1,2 +1,3 @@ Cargo.lock -target/ \ No newline at end of file +target/ +*.bin diff --git a/script/research/rln/Cargo.toml b/script/research/rln/Cargo.toml index 2739eef3f..3ffd8ae99 100644 --- a/script/research/rln/Cargo.toml +++ b/script/research/rln/Cargo.toml @@ -9,4 +9,5 @@ edition = "2021" [dependencies] darkfi-sdk = {path = "../../../src/sdk"} +darkfi = {path = "../../../", features = ["zk"]} rand = "0.8.5" diff --git a/script/research/rln/signal.zk b/script/research/rln/signal.zk new file mode 100644 index 000000000..247f53c96 --- /dev/null +++ b/script/research/rln/signal.zk @@ -0,0 +1,34 @@ +constant "RlnSignal" {} + +contract "RlnSignal" { + Base secret_key, + MerklePath identity_path, + Uint32 identity_leaf_pos, + + # These are public so have to be properly constructed + Base message_hash, # x + Base epoch, + Base rln_identifier, +} + +circuit "RlnSignal" { + constrain_instance(epoch); + constrain_instance(rln_identifier); + constrain_instance(message_hash); + + # This has to be the same constant used outside + key_derivation_path = witness_base(0); + nf_derivation_path = witness_base(1); + identity_commit = poseidon_hash(key_derivation_path, secret_key); + root = merkle_root(identity_leaf_pos, identity_path, identity_commit); + constrain_instance(root); + + external_nullifier = poseidon_hash(epoch, rln_identifier); + a1 = poseidon_hash(secret_key, external_nullifier); + internal_nullifier = poseidon_hash(nf_derivation_path, a1); + constrain_instance(internal_nullifier); + + y_a = base_mul(a1, message_hash); + y = base_add(y_a, secret_key); + constrain_instance(y); +} diff --git a/script/research/rln/src/main.rs b/script/research/rln/src/main.rs index 553931a31..7cf1d8120 100644 --- a/script/research/rln/src/main.rs +++ b/script/research/rln/src/main.rs @@ -35,80 +35,165 @@ //! registered users, therefore disabling their ability to send future //! messages and requiring them to register with a new key. +use darkfi::{ + zk::{ + empty_witnesses, halo2::Value, proof::VerifyingKey, Proof, ProvingKey, Witness, ZkCircuit, + }, + zkas::ZkBinary, +}; use darkfi_sdk::{ - crypto::{pasta_prelude::*, poseidon_hash, MerkleNode, MerkleTree, SecretKey}, + crypto::{pasta_prelude::*, poseidon_hash, MerkleNode, MerkleTree}, incrementalmerkletree::Tree, - pasta::pallas, + pasta::{arithmetic::CurveExt, pallas}, }; use rand::rngs::OsRng; -const IDENTITY_COMMITMENT_PREFIX: u64 = 42; - -#[derive(Copy, Clone, Debug)] -struct ShamirPoint { - pub x: pallas::Base, - pub y: pallas::Base, -} - -fn sss_share(secret: pallas::Base, n_shares: usize, threshold: usize) -> Vec { - assert!(threshold > 2 && n_shares > threshold); - - let mut coefficients = vec![secret]; - for _ in 0..threshold - 1 { - coefficients.push(pallas::Base::random(&mut OsRng)); - } - - let mut shares = Vec::with_capacity(n_shares); - - for x in 1..n_shares + 1 { - let x = pallas::Base::from(x as u64); - let mut y = pallas::Base::zero(); - for coeff in coefficients.iter().rev() { - y *= x; - y += coeff; - } - - shares.push(ShamirPoint { x, y }) - } - - shares -} - -fn sss_recover(shares: &[ShamirPoint]) -> pallas::Base { - assert!(shares.len() > 1); - - let mut secret = pallas::Base::zero(); - - for (j, share_j) in shares.iter().enumerate() { - let mut prod = pallas::Base::one(); - - for (i, share_i) in shares.iter().enumerate() { - if i != j { - prod *= share_i.x * (share_i.x - share_j.x).invert().unwrap(); - } - } - - prod *= share_j.y; - secret += prod; - } - - secret -} - fn main() { - let mut membership_tree = MerkleTree::new(100); + // This should be unique constant per application + let rln_identifier = pallas::Base::from(42); + let key_derivation_path = pallas::Base::from(0); + let nf_derivation_path = pallas::Base::from(1); + + let epoch = pallas::Base::from(1674495551); + let external_nullifier = poseidon_hash([epoch, rln_identifier]); // The identity commitment should be something that cannot be precalculated // for usage in the future, and possibly also has to be some kind of puzzle // that is costly to precalculate. // Alternatively, it could be economic stake of funds which could then be // lost if spam is detected and acted upon. - let alice_secret_key = SecretKey::random(&mut OsRng); - let alice_identity_commitment = - poseidon_hash([pallas::Base::from(IDENTITY_COMMITMENT_PREFIX), alice_secret_key.inner()]); + let alice_secret_key = pallas::Base::random(&mut OsRng); + let alice_identity_commitment = poseidon_hash([key_derivation_path, alice_secret_key]); - // The user registers, and their identity commitment gets added into the - // membership tree: + // ============ + // Registration + // ============ + let mut membership_tree = MerkleTree::new(100); membership_tree.append(&MerkleNode::from(alice_identity_commitment)); - let alice_identity_leaf_pos = membership_tree.witness().unwrap(); + let alice_identity_leafpos = membership_tree.witness().unwrap(); + + // ========== + // Signalling + // ========== + + // Our secret-sharing polynomial will be A(x) = a_1*x + a_0, where: + // a_0 = secret_key, + // a_1 = Poseidon(a_0, external_nullifier) + // To send a message, the user has to come up with a share - an (x, y) on the polynomial. + // x = Poseidon(message), y = A(x) + // Thus, if the same epoch user sends more than one message, their secret can be recovered. + + // TODO: I don't know a better way to do this: + let message = b"hello i wanna spam"; + let hasher = pallas::Point::hash_to_curve("ircd_domain"); + let message_point = hasher(message); + let message_coords = message_point.to_affine().coordinates().unwrap(); + let x = poseidon_hash([*message_coords.x(), *message_coords.y()]); + let y = poseidon_hash([alice_secret_key, external_nullifier]) * x + alice_secret_key; + + let internal_nullifier = + poseidon_hash([nf_derivation_path, poseidon_hash([alice_secret_key, external_nullifier])]); + + let identity_root = membership_tree.root(0).unwrap(); + let alice_identity_path = + membership_tree.authentication_path(alice_identity_leafpos, &identity_root).unwrap(); + + // NIZK stuff + let zkbin = include_bytes!("../signal.zk.bin"); + let rln_zkbin = ZkBinary::decode(zkbin).unwrap(); + let rln_empty_circuit = ZkCircuit::new(empty_witnesses(&rln_zkbin), rln_zkbin.clone()); + + println!("Building Proving key..."); + let rln_pk = ProvingKey::build(13, &rln_empty_circuit); + println!("Building Verifying key..."); + let rln_vk = VerifyingKey::build(13, &rln_empty_circuit); + + // Alice has her witnesses and creates a NIZK proof + let prover_witnesses = vec![ + Witness::Base(Value::known(alice_secret_key)), + Witness::MerklePath(Value::known(alice_identity_path.clone().try_into().unwrap())), + Witness::Uint32(Value::known(u64::from(alice_identity_leafpos).try_into().unwrap())), + Witness::Base(Value::known(x)), + Witness::Base(Value::known(epoch)), + Witness::Base(Value::known(rln_identifier)), + ]; + + let rln_circuit = ZkCircuit::new(prover_witnesses, rln_zkbin.clone()); + let public_inputs = vec![ + epoch, + rln_identifier, + x, // <-- message hash + identity_root.inner(), + internal_nullifier, + y, + ]; + + println!("Creating ZK proof..."); + let proof = Proof::create(&rln_pk, &[rln_circuit], &public_inputs, &mut OsRng).unwrap(); + + // ============ + // Verification + // ============ + println!("Verifying ZK proof..."); + assert!(proof.verify(&rln_vk, &public_inputs).is_ok()); + + let mut alice_shares = vec![(public_inputs[2], public_inputs[5])]; + + // Now if Alice sends another message in the same epoch, we should be able to + // get their secret key and ban them + let message = b"hello i'm spamming"; + let hasher = pallas::Point::hash_to_curve("ircd_domain"); + let message_point = hasher(message); + let message_coords = message_point.to_affine().coordinates().unwrap(); + let x = poseidon_hash([*message_coords.x(), *message_coords.y()]); + let y = poseidon_hash([alice_secret_key, external_nullifier]) * x + alice_secret_key; + + // Same epoch and account, different message + let prover_witnesses = vec![ + Witness::Base(Value::known(alice_secret_key)), + Witness::MerklePath(Value::known(alice_identity_path.try_into().unwrap())), + Witness::Uint32(Value::known(u64::from(alice_identity_leafpos).try_into().unwrap())), + Witness::Base(Value::known(x)), + Witness::Base(Value::known(epoch)), + Witness::Base(Value::known(rln_identifier)), + ]; + + let rln_circuit = ZkCircuit::new(prover_witnesses, rln_zkbin); + + let public_inputs = vec![ + epoch, + rln_identifier, + x, // <-- message hash + identity_root.inner(), + internal_nullifier, + y, + ]; + + println!("Creating ZK proof..."); + let proof = Proof::create(&rln_pk, &[rln_circuit], &public_inputs, &mut OsRng).unwrap(); + + println!("Verifying ZK proof..."); + assert!(proof.verify(&rln_vk, &public_inputs).is_ok()); + alice_shares.push((public_inputs[2], public_inputs[5])); + + // ======== + // Slashing + // ======== + // We should be able to retrieve Alice's secret key because she sent two + // messages in the same epoch. + let mut secret = pallas::Base::zero(); + for (j, share_j) in alice_shares.iter().enumerate() { + let mut prod = pallas::Base::one(); + for (i, share_i) in alice_shares.iter().enumerate() { + if i != j { + prod *= share_i.0 * (share_i.0 - share_j.0).invert().unwrap(); + } + } + + prod *= share_j.1; + secret += prod; + } + + assert_eq!(secret, alice_secret_key); + println!("u banned"); }