Просмотр исходного кода

research/rlnv2: Add signalling with SMT proofs

x 9 месяцев назад
Родитель
Сommit
0b7e2f53ec

+ 1 - 1
script/research/rlnv2/Makefile

@@ -6,7 +6,7 @@ CARGO = cargo
 # Compile target
 RUST_TARGET = $(shell rustc -Vv | grep '^host: ' | cut -d' ' -f2)
 
-PROOFS_SRC = signal.zk slash.zk
+PROOFS_SRC = register.zk signal.zk slash.zk
 PROOFS_BIN = $(PROOFS_SRC:=.bin)
 
 ZKAS = ../../../../zkas

+ 3 - 3
script/research/rlnv2/register.zk

@@ -1,16 +1,16 @@
 k = 13;
 field = "pallas";
 
-constant "RlnRegister" {}
+constant "Rlnv2Diff_Register" {}
 
-witness "RlnRegister" {
+witness "Rlnv2Diff_Register" {
 	Base identity_nullifier,
 	Base identity_trapdoor,
 	Base user_message_limit,
 	Base message_limit,
 }
 
-circuit "RlnRegister" {
+circuit "Rlnv2Diff_Register" {
 	identity_secret = poseidon_hash(identity_nullifier, identity_trapdoor);
 	identity_secret_hash = poseidon_hash(identity_secret, user_message_limit);
 	identity_commitment = poseidon_hash(identity_secret_hash);

+ 39 - 35
script/research/rlnv2/signal.zk

@@ -1,44 +1,48 @@
-k = 13;
+k = 14;
 field = "pallas";
 
-constant "RlnSignal" {}
+constant "Rlnv2Diff_Signal" {}
 
-witness "RlnSignal" {
-    Base identity_nullifier,
-    Base identity_trapdoor,
+witness "Rlnv2Diff_Signal" {
+	Base identity_nullifier,
+	Base identity_trapdoor,
+	Base user_message_limit,
 
-    MerklePath identity_path,
-    Uint32 identity_leaf_pos,
+	# Inclusion proof, the leaf is the identity_commitment
+	SparseMerklePath path,
 
-    Base x, # The message hash
-    Base external_nullifier, # Hash(Epoch, RLN identifier)
+	# The message hash
+	Base x,
+	Base message_id,
 
-    Base message_id,
-    Base user_message_limit,
-
-    Base epoch,
+	Base epoch,
 }
 
-circuit "RlnSignal" {
-    constrain_instance(epoch);
-    constrain_instance(external_nullifier);
-
-    less_than_strict(message_id, user_message_limit);
-
-    # Identity secret hash
-    a_0 = poseidon_hash(identity_nullifier, identity_trapdoor);
-    a_1 = poseidon_hash(a_0, external_nullifier, message_id);
-
-    # y = a_0 + x * a_1
-    x_a_1 = base_mul(x, a_1);
-    y = base_add(a_0, x_a_1);
-    constrain_instance(x);
-    constrain_instance(y);
-
-    internal_nullifier = poseidon_hash(a_1);
-    constrain_instance(internal_nullifier);
-
-    identity_commitment = poseidon_hash(a_0, user_message_limit);
-    root = merkle_root(identity_leaf_pos, identity_path, identity_commitment);
-    constrain_instance(root);
+circuit "Rlnv2Diff_Signal" {
+	# Identity inclusion proof
+	identity_secret = poseidon_hash(identity_nullifier, identity_trapdoor);
+	identity_secret_hash = poseidon_hash(identity_secret, user_message_limit);
+	identity_commitment = poseidon_hash(identity_secret_hash);
+	root = sparse_merkle_root(identity_commitment, path, identity_commitment);
+	constrain_instance(root);
+
+	# External nullifier is created from epoch and app identifier
+	app_id = witness_base(1000);
+	external_nullifier = poseidon_hash(epoch, app_id);
+	constrain_instance(external_nullifier);
+
+	# Calculating internal nullifier
+	# a_0 = identity_secret_hash
+	a_0 = poseidon_hash(identity_nullifier, identity_trapdoor);
+	a_1 = poseidon_hash(a_0, external_nullifier, message_id);
+	x_a_1  = base_mul(x, a_1);
+	y = base_add(a_0, x_a_1);
+	constrain_instance(x);
+	constrain_instance(y);
+
+	# Constrain message_id to be lower than actual message limit.
+	less_than_strict(message_id, user_message_limit);
+
+	internal_nullifier = poseidon_hash(a_1);
+	constrain_instance(internal_nullifier);
 }

+ 312 - 10
script/research/rlnv2/src/main.rs

@@ -16,7 +16,10 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
+use std::{
+    collections::BTreeMap,
+    time::{Instant, UNIX_EPOCH},
+};
 
 use darkfi::{
     zk::{empty_witnesses, halo2::Value, Proof, ProvingKey, VerifyingKey, Witness, ZkCircuit},
@@ -32,6 +35,7 @@ use darkfi_sdk::{
 };
 use rand::rngs::OsRng;
 
+#[derive(Copy, Clone)]
 struct Identity {
     identity_nullifier: pallas::Base,
     identity_trapdoor: pallas::Base,
@@ -55,6 +59,78 @@ impl Identity {
     }
 }
 
+#[derive(Debug, Clone)]
+struct ShareData {
+    pub x_shares: Vec<pallas::Base>,
+    pub y_shares: Vec<pallas::Base>,
+}
+
+impl ShareData {
+    fn new() -> Self {
+        Self { x_shares: vec![], y_shares: vec![] }
+    }
+}
+
+#[derive(Debug, Default)]
+struct MessageMetadata {
+    data: BTreeMap<pallas::Base, BTreeMap<pallas::Base, ShareData>>,
+}
+
+impl MessageMetadata {
+    fn new() -> Self {
+        Self { data: BTreeMap::new() }
+    }
+
+    fn add_share(
+        &mut self,
+        external_nullifier: pallas::Base,
+        internal_nullifier: pallas::Base,
+        x: pallas::Base,
+        y: pallas::Base,
+    ) {
+        let inner_map = self.data.entry(external_nullifier).or_insert_with(BTreeMap::new);
+        let share_data = inner_map.entry(internal_nullifier).or_insert_with(ShareData::new);
+
+        share_data.x_shares.push(x);
+        share_data.y_shares.push(y);
+    }
+
+    fn get_shares(
+        &self,
+        external_nullifier: &pallas::Base,
+        internal_nullifier: &pallas::Base,
+    ) -> Vec<(pallas::Base, pallas::Base)> {
+        if let Some(inner_map) = self.data.get(external_nullifier) {
+            if let Some(share_data) = inner_map.get(internal_nullifier) {
+                return share_data
+                    .x_shares
+                    .iter()
+                    .cloned()
+                    .zip(share_data.y_shares.iter().cloned())
+                    .collect()
+            }
+        }
+
+        vec![]
+    }
+
+    fn is_duplicate(
+        &self,
+        external_nullifier: &pallas::Base,
+        internal_nullifier: &pallas::Base,
+        x: &pallas::Base,
+        y: &pallas::Base,
+    ) -> bool {
+        if let Some(inner_map) = self.data.get(external_nullifier) {
+            if let Some(share_data) = inner_map.get(internal_nullifier) {
+                return share_data.x_shares.contains(x) && share_data.y_shares.contains(y);
+            }
+        }
+
+        false
+    }
+}
+
 /// Hash message modulo Fp
 /// In DarkIRC/eventgraph this could be the event ID
 fn hash_message(msg: &str) -> pallas::Base {
@@ -65,6 +141,23 @@ fn hash_message(msg: &str) -> pallas::Base {
     pallas::Base::from_uniform_bytes(&buf)
 }
 
+fn sss_recover(shares: &[(pallas::Base, pallas::Base)]) -> pallas::Base {
+    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.0 * (share_i.0 - share_j.0).invert().unwrap();
+            }
+        }
+
+        prod *= share_j.1;
+        secret += prod;
+    }
+
+    secret
+}
+
 fn main() {
     // There exists a Sparse Merkle Tree of identity commitments that
     // serves as the user registry. If a leaf is NULL, it should mean
@@ -74,12 +167,11 @@ fn main() {
     let mut identity_tree = SmtMemoryFp::new(store, hasher.clone(), &EMPTY_NODES_FP);
 
     // Per-app identifier
-    let rln_identifier = pallas::Base::from(42);
+    let rln_identifier = pallas::Base::from(1000);
 
-    // Create three accounts
-    let id0 = Identity::new(pallas::Base::from(5));
-    let id1 = Identity::new(pallas::Base::from(2));
-    let id2 = Identity::new(pallas::Base::from(1));
+    // Create two accounts
+    let id0 = Identity::new(pallas::Base::from(2));
+    let id1 = Identity::new(pallas::Base::from(1));
 
     // ============
     // Registration
@@ -99,7 +191,7 @@ fn main() {
     let register_vk = VerifyingKey::build(register_zkbin.k, &register_empty_circuit);
     println!("[{:?}]", now.elapsed());
 
-    for (i, id) in [id0, id1, id2].iter().enumerate() {
+    for (i, id) in [id0, id1].iter().enumerate() {
         // Create ZK proof
         // This 6 message limit is arbitrary and should likely be based on stake.
         let witnesses = vec![
@@ -119,17 +211,227 @@ fn main() {
 
         // Verify ZK proof
         print!("[Register] Verifying ZK proof for id{i}... ");
+        let now = Instant::now();
         assert!(proof.verify(&register_vk, &public_inputs).is_ok());
         println!("[{:?}]", now.elapsed());
 
         let leaf = vec![id.commitment()];
         let leaf: Vec<_> = leaf.into_iter().map(|l| (l, l)).collect();
-        // TODO: Should verify that identity doesn't exist yet before insert.
+        // TODO: Recipients should verify that identity doesn't exist already before insert.
         identity_tree.insert_batch(leaf.clone()).unwrap(); // leaf == pos
         assert_eq!(leaf[0].0, id.commitment());
         assert_eq!(leaf[0].1, id.commitment());
     }
 
-    // At this point we have 3 identities registered. They also all have
-    // different message limits per epoch.
+    // At this point we have 2 identities registered.
+
+    // ==========
+    // Signalling
+    // ==========
+    let signal_zkbin = include_bytes!("../signal.zk.bin");
+    let signal_zkbin = ZkBinary::decode(signal_zkbin).unwrap();
+    let signal_empty_circuit =
+        ZkCircuit::new(empty_witnesses(&signal_zkbin).unwrap(), &signal_zkbin);
+
+    print!("[Signal] Building Proving key... ");
+    let now = Instant::now();
+    let signal_pk = ProvingKey::build(signal_zkbin.k, &signal_empty_circuit);
+    println!("[{:?}]", now.elapsed());
+
+    print!("[Signal] Building Verifying key... ");
+    let now = Instant::now();
+    let signal_vk = VerifyingKey::build(signal_zkbin.k, &signal_empty_circuit);
+    println!("[{:?}]", now.elapsed());
+
+    // Our epoch length will be 10s. This normally means one message every
+    // 10 seconds. In RLNv2-DIFF we have N messages every epoch.
+    // This works because of integer division, so for example:
+    // 1697472000, 1697472005, and 1697472009 are the same epoch, but
+    // 1697472010 would be the new epoch.
+    // In practice, if our client realizes we're sending too fast, we could
+    // also queue it.
+    let epoch_len = 10_u64;
+
+    // =========================
+    // Account 0 sends a message
+    // =========================
+
+    // 1. Construct share
+    let epoch = pallas::Base::from(UNIX_EPOCH.elapsed().unwrap().as_secs() as u64 / epoch_len);
+    let message_id = pallas::Base::from(0); // This should increment each msg
+
+    let external_nullifier = poseidon_hash([epoch, rln_identifier]);
+    let a_0 = poseidon_hash([id0.identity_nullifier, id0.identity_trapdoor]);
+    let a_1 = poseidon_hash([a_0, external_nullifier, message_id]);
+    let x = hash_message("hello");
+    let y = a_0 + x * a_1;
+
+    let internal_nullifier = poseidon_hash([a_1]);
+
+    // 2. Inclusion proof
+    let root = identity_tree.root();
+    let path = identity_tree.prove_membership(&id0.commitment());
+    assert!(path.verify(&root, &id0.commitment(), &id0.commitment()));
+
+    // 3. ZK proof
+    let witnesses = vec![
+        Witness::Base(Value::known(id0.identity_nullifier)),
+        Witness::Base(Value::known(id0.identity_trapdoor)),
+        Witness::Base(Value::known(id0.user_message_limit)),
+        Witness::SparseMerklePath(Value::known(path.path)),
+        Witness::Base(Value::known(x)),
+        Witness::Base(Value::known(message_id)),
+        Witness::Base(Value::known(epoch)),
+    ];
+
+    let public_inputs = vec![root, external_nullifier, x, y, internal_nullifier];
+
+    print!("[Signal] Creating ZK proof for 0:0... ");
+    let now = Instant::now();
+    let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);
+    let proof = Proof::create(&signal_pk, &[signal_circuit], &public_inputs, &mut OsRng).unwrap();
+    print!("[{:?}] ", now.elapsed());
+    println!("({} bytes)", proof.as_ref().len());
+
+    // ============
+    // Verification
+    // ============
+    print!("[Signal] Verifying ZK proof for 0:0... ");
+    let now = Instant::now();
+    assert!(proof.verify(&signal_vk, &public_inputs).is_ok());
+    println!("[{:?}]", now.elapsed());
+
+    // Each user of the protocol must store metadata for each message
+    // received by each user, for the given epoch. The data can be
+    // deleted when the epoch passes.
+    let mut metadata = MessageMetadata::new();
+    if metadata.is_duplicate(&external_nullifier, &internal_nullifier, &x, &y) {
+        println!("[Signal] Duplicate Message!");
+        return
+    }
+
+    // Add share
+    metadata.add_share(external_nullifier, internal_nullifier, x, y);
+
+    // Now let's try to send another message in the same epoch.
+    // id0 has a limit of 2 so it should pass since the ZK circuit will
+    // allow this.
+    let message_id = message_id + pallas::Base::from(1);
+    let a_0 = poseidon_hash([id0.identity_nullifier, id0.identity_trapdoor]);
+    let a_1 = poseidon_hash([a_0, external_nullifier, message_id]);
+    let x = hash_message("hello again");
+    let y = a_0 + x * a_1;
+
+    let internal_nullifier = poseidon_hash([a_1]);
+
+    // Skip the inclusion proof for the demo since we have it above.
+    // Make the ZK proof.
+    let witnesses = vec![
+        Witness::Base(Value::known(id0.identity_nullifier)),
+        Witness::Base(Value::known(id0.identity_trapdoor)),
+        Witness::Base(Value::known(id0.user_message_limit)),
+        Witness::SparseMerklePath(Value::known(path.path)),
+        Witness::Base(Value::known(x)),
+        Witness::Base(Value::known(message_id)),
+        Witness::Base(Value::known(epoch)),
+    ];
+
+    let public_inputs = vec![root, external_nullifier, x, y, internal_nullifier];
+
+    print!("[Signal] Creating ZK proof for 0:1... ");
+    let now = Instant::now();
+    let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);
+    let proof = Proof::create(&signal_pk, &[signal_circuit], &public_inputs, &mut OsRng).unwrap();
+    print!("[{:?}] ", now.elapsed());
+    println!("({} bytes)", proof.as_ref().len());
+
+    print!("[Signal] Verifying ZK proof for 0:1... ");
+    let now = Instant::now();
+    assert!(proof.verify(&signal_vk, &public_inputs).is_ok());
+    println!("[{:?}]", now.elapsed());
+
+    // Each user of the protocol must store metadata for each message
+    // received by each user, for the given epoch. The data can be
+    // deleted when the epoch passes.
+    if metadata.is_duplicate(&external_nullifier, &internal_nullifier, &x, &y) {
+        println!("[Signal] Duplicate Message!");
+        return
+    }
+
+    // Add share
+    metadata.add_share(external_nullifier, internal_nullifier, x, y);
+
+    // Now we shouldn't be able to create more proofs unless we reuse message_id.
+    // This means that some internal_nullifier will have >1 shares, and it should
+    // be possible to recover the secret.
+    // We reuse the above, just try a different message.
+    let x = hash_message("hello again, i'm reusing a message_id");
+    let y = a_0 + x * a_1;
+
+    // ZK proof:
+    let witnesses = vec![
+        Witness::Base(Value::known(id0.identity_nullifier)),
+        Witness::Base(Value::known(id0.identity_trapdoor)),
+        Witness::Base(Value::known(id0.user_message_limit)),
+        Witness::SparseMerklePath(Value::known(path.path)),
+        Witness::Base(Value::known(x)),
+        Witness::Base(Value::known(message_id)),
+        Witness::Base(Value::known(epoch)),
+    ];
+    let public_inputs = vec![root, external_nullifier, x, y, internal_nullifier];
+
+    print!("[Signal] Creating ZK proof for 0:1 (reused message_id) ... ");
+    let now = Instant::now();
+    let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);
+    let proof = Proof::create(&signal_pk, &[signal_circuit], &public_inputs, &mut OsRng).unwrap();
+    print!("[{:?}] ", now.elapsed());
+    println!("({} bytes)", proof.as_ref().len());
+
+    print!("[Signal] Verifying ZK proof for 0:1 (reused message_id) ... ");
+    let now = Instant::now();
+    assert!(proof.verify(&signal_vk, &public_inputs).is_ok());
+    println!("[{:?}]", now.elapsed());
+
+    // Add share
+    metadata.add_share(external_nullifier, internal_nullifier, x, y);
+
+    // Now the internal_nullifier should have been repeated, and the internal
+    // nullifier should have 2 (or more) shares.
+    // Let's recover them.
+    let shares = metadata.get_shares(&external_nullifier, &internal_nullifier);
+    println!("{:#?}", shares);
+    let secret = sss_recover(&shares);
+    println!("secret: {:?}", secret);
+    println!("a_0:    {:?}", a_0);
+    assert_eq!(secret, a_0);
+
+    // Additionally, it should not be possible to produce (or verify) a ZK
+    // proof that exceeds the set message limit for an identity.
+    let message_id = message_id + pallas::Base::from(2);
+    let a_0 = poseidon_hash([id0.identity_nullifier, id0.identity_trapdoor]);
+    let a_1 = poseidon_hash([a_0, external_nullifier, message_id]);
+    let x = hash_message("hello again");
+    let y = a_0 + x * a_1;
+
+    let internal_nullifier = poseidon_hash([a_1]);
+
+    // Skip the inclusion proof for the demo since we have it above.
+    // Make the ZK proof.
+    let witnesses = vec![
+        Witness::Base(Value::known(id0.identity_nullifier)),
+        Witness::Base(Value::known(id0.identity_trapdoor)),
+        Witness::Base(Value::known(id0.user_message_limit)),
+        Witness::SparseMerklePath(Value::known(path.path)),
+        Witness::Base(Value::known(x)),
+        Witness::Base(Value::known(message_id)),
+        Witness::Base(Value::known(epoch)),
+    ];
+
+    let public_inputs = vec![root, external_nullifier, x, y, internal_nullifier];
+
+    println!("[Signal] Creating ZK proof for 0:2 msgid={:?}... ", message_id);
+    let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);
+    let proof = Proof::create(&signal_pk, &[signal_circuit], &public_inputs, &mut OsRng).unwrap();
+    assert!(proof.verify(&signal_vk, &public_inputs).is_err());
+    println!("[Signal] ZK proof for 0:2 failed as expected");
 }