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

research/rln: Write spec in the mdbook, and clean up protocol.

parazyd 3 лет назад
Родитель
Сommit
6e6eb38056

+ 1 - 0
doc/src/SUMMARY.md

@@ -26,6 +26,7 @@
   - [FFT](crypto/fft.md)
   - [ZK explainer](crypto/zk_explainer.md)
   - [Research](crypto/research.md)
+  - [Rate-Limit Nullifiers](crypto/rln.md)
 - [p2p Api Tutorial](learn/dchat/dchat.md)
   - [Deployment](learn/dchat/deployment/part-1.md)
     - [Getting started](learn/dchat/deployment/getting-started.md)

+ 132 - 0
doc/src/crypto/rln.md

@@ -0,0 +1,132 @@
+# Rate-limit Nullifiers
+
+For an application, each user maintains:
+
+* User registration
+* User interactions
+* User removal
+
+## User registration
+
+* There exists a Merkle tree of published and valid registrations.
+* There exists a set of identity commitments in order to maintain
+  and avoid duplicates.
+* There exists a set of Merkle roots from the above tree which acts
+  as a set of registrations that have been rate-limited/banned.
+
+### Registration process
+
+Let $K$ be a constant identity derivation path.
+
+1. Alice generates a secret key $a_0$ and derives an identity
+   commitment:
+$$ Poseidon(K, a_0) $$
+
+2. Alice publishes the identity commitment.
+3. The Network verifies that the identity commitment is not part of the
+   set of identity commitments, providing the ability to append it to
+   the membership Merkle tree.
+4. Alice and the Network append the identity commitment to the set of
+   identity commitments, and to the membership Merkle tree.
+5. Alice notes down the leaf position in the Merkle tree in order to be
+   able to produce valid authentication paths for future interactions.
+
+## User interaction
+
+For each interaction, Alice must create a _ZK_ proof which ensures
+the other participants (verifiers) that she is a valid member of the
+app and her identity commitment is part of the membership Merkle tree.
+
+The anti-spam rule is also introduced in the protocol. e.g.:
+
+> Users must not make more than N interactions per epoch.
+
+In other words:
+
+> Users must not send more than one message per second.
+
+The anti-spam rule is implemented with a Shamir Secret Sharing
+Scheme[^1]. In our case the secret is the user's secret key, and
+the shares are parts of the secret key. If Alice sends more than one
+message per second, her key can be reconstructed by the Network, and
+thus she can be banned. For these claims to hold true, Alice's _ZK_
+proof must also include shares of her secret key and the epoch.
+
+### Interaction process
+
+For secret-sharing, we'll use a linear polynomial:
+
+$$ A(x) = a_1 x + a_0 $$
+
+Where:
+
+$$ a_1 = Poseidon(a_0, \text{external\_nullifier}) $$
+
+$$ \mathrm{external\_nullifier} = Poseidon(epoch, \text{rln\_identifier}) $$
+
+$\text{rln\_identifier}$ is a unique constant per application.
+
+We will also use $\text{internal\_nullifier}$ as a mechanism to make a
+connection between a person and their messages without revealing their
+identity:
+
+$$ \mathrm{internal\_nullifier} = Poseidon(a_1, \text{rln\_identifier}) $$
+
+To send a message, we must come up with a share $(x, y)$, given the
+above polynomial.
+
+$$ x = Poseidon(message) $$
+$$ y = A(x) $$
+
+We must also use a _zkSNARK_ to prove correctness of the share.
+
+1. Alice wants to send a message `hello`.
+2. Alice calculates the field point $(x, y)$.
+3. Alice proves correctness using a _zkSNARK_.
+4. Alice sends the message and the proof (plus necessary metadata) to
+   the Network.
+5. The Network verifies membership, the ZK proof, and if the rate-limit
+   was reached (by seeing if Alice's secret can be reconstructed).
+6. If the key cannot be reconstructed, the message is valid and relayed.
+   Otherwise, the Network proceeds with User removal/Slashing.
+
+## User removal
+
+In the case of spam, the secret key can be retrieved from the _SSS_
+shares and the Network can use this to add the Merkle root into the
+set of slashed users, therefore disabling their ability to send future
+messages and requiring them to register with a new key.
+
+### Slashing process
+
+1. Alice sends two messages in the same epoch.
+2. The network now has two shares of Alice's secret key:
+
+$$ (x_1, y_1) $$
+$$ (x_2, y_2) $$
+
+3. The Network is able to reconstruct the secret key ($k=2$):
+
+$$ a_0 = \sum_{j=0}^{k-1} y_j \prod_{\begin{smallmatrix} m\,=\,0 \\ m\,\ne\,j \end{smallmatrix}}^{k-1} \frac{x_m}{x_m - x_j} $$ 
+
+4. Given $a_0$, a _zkSNARK_ can be produced to add the Merkle root from
+   the membership tree to the banned set.
+
+5. Further messages from the given key will not be accepted for as long
+   as this root is part of that set.
+
+## Circuits
+
+### Interaction
+
+```
+{{#include ../../../script/research/rln/signal.zk}}
+```
+
+### Slashing
+
+```
+{{#include ../../../script/research/rln/slash.zk}}
+```
+
+[^1]: <https://en.wikipedia.org/wiki/Shamir's_Secret_Sharing>

+ 1 - 0
script/research/rln/Cargo.toml

@@ -10,4 +10,5 @@ edition = "2021"
 [dependencies]
 darkfi-sdk = {path = "../../../src/sdk"}
 darkfi = {path = "../../../", features = ["zk"]}
+lazy_static = "1.4.0"
 rand = "0.8.5"

+ 7 - 6
script/research/rln/signal.zk

@@ -17,18 +17,19 @@ circuit "RlnSignal" {
 	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);
+	identity_derivation_path = witness_base(11);
+	nullifier_derivation_path = witness_base(12);
+
+	identity_commit = poseidon_hash(identity_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);
+	a_1 = poseidon_hash(secret_key, external_nullifier);
+	internal_nullifier = poseidon_hash(nullifier_derivation_path, a_1);
 	constrain_instance(internal_nullifier);
 
-	y_a = base_mul(a1, message_hash);
+	y_a = base_mul(a_1, message_hash);
 	y = base_add(y_a, secret_key);
 	constrain_instance(y);
 }

+ 15 - 0
script/research/rln/slash.zk

@@ -0,0 +1,15 @@
+constant "RlnSlash" {}
+
+contract "RlnSlash" {
+	Base secret_key,
+	MerklePath identity_path,
+	Uint32 identity_leaf_pos,
+}
+
+circuit "RlnSlash" {
+	identity_derivation_path = witness_base(11);
+
+	identity_commit = poseidon_hash(identity_derivation_path, secret_key);
+	root = merkle_root(identity_leaf_pos, identity_path, identity_commit);
+	constrain_instance(root);
+}

+ 189 - 127
script/research/rln/src/main.rs

@@ -1,199 +1,261 @@
-//! Rate-limit nullifiers, to be implemented in ircd for spam protection.
-//!
-//! For an application, each user maintains:
-//! - User registration
-//! - User interactions
-//! - User removal
-//!
-//! # User registration
-//! 1. Derive an identity commitment: poseidon_hash(secret_key)
-//! 2. Register by providing the commitment
-//! 3. Store the commitment in the Merkle tree of registered users
-//!
-//! # User interaction
-//! For each interaction, the user must create a ZK proof which ensures
-//! the other participants (verifiers) that they are a valid member of
-//! the application and their identity commitment is part of the membership
-//! Merkle tree.
-//! The anti-spam rule is also introduced in the protocol, e.g.:
-//!
-//! > Users must not make more than X interactions per epoch.
-//! In other words:
-//! > Users must not send more than one message per second.
-//!
-//! The anti-spam rule is implemented with Shamir-Secret-Sharing Scheme.
-//! In our case the secret is the user's secret key, and the shares are
-//! parts of the secret key. In a 2/3 case, this means the user's secret
-//! key can be reconstructed if they send two messages per epoch.
-//! For these claims to hold true, the user's ZK proof must also include
-//! shares of their secret key and the epoch. By not having any of these
-//! fields included, the ZK proof will be treated as invalid.
-//!
-//! # User removal
-//! In the case of spam, the secret key can be retrieved from the SSS
-//! shares and a user can use this to remove the key from the set of
-//! registered users, therefore disabling their ability to send future
-//! messages and requiring them to register with a new key.
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//! <https://darkrenaissance.github.io/darkfi/crypto/rln.html>
+
+use std::{collections::HashMap, time::Instant};
 
 use darkfi::{
-    zk::{
-        empty_witnesses, halo2::Value, proof::VerifyingKey, Proof, ProvingKey, Witness, ZkCircuit,
-    },
+    zk::{empty_witnesses, halo2::Value, Proof, ProvingKey, VerifyingKey, Witness, ZkCircuit},
     zkas::ZkBinary,
 };
 use darkfi_sdk::{
     crypto::{pasta_prelude::*, poseidon_hash, MerkleNode, MerkleTree},
     incrementalmerkletree::Tree,
-    pasta::{arithmetic::CurveExt, pallas},
+    pasta::{Ep, Fp},
 };
+use lazy_static::lazy_static;
 use rand::rngs::OsRng;
 
+// These should be unique constants per application.
+lazy_static! {
+    static ref RLN_IDENTIFIER: Fp = Fp::from(42);
+    static ref IDENTITY_DERIVATION_PATH: Fp = Fp::from(11);
+    static ref NULLIFIER_DERIVATION_PATH: Fp = Fp::from(12);
+}
+
+fn hash_message(message: &[u8]) -> Fp {
+    let hasher = Ep::hash_to_curve("rln-domain:demoapp");
+    let message_point = hasher(message);
+    let message_coords = message_point.to_affine().coordinates().unwrap();
+    poseidon_hash([*message_coords.x(), *message_coords.y()])
+}
+
+fn sss_recover(shares: &[(Fp, Fp)]) -> Fp {
+    let mut secret = Fp::zero();
+    for (j, share_j) in shares.iter().enumerate() {
+        let mut prod = Fp::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() {
-    // 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 = pallas::Base::random(&mut OsRng);
-    let alice_identity_commitment = poseidon_hash([key_derivation_path, alice_secret_key]);
+    let epoch = Fp::from(1674509414);
+    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 (pre)calculate.
+    // Alternatively, it could be economic stake of funds which could
+    // then be lost if spam is detected and acted upon.
+    let secret_key = Fp::random(&mut OsRng);
+    let identity_commitment = poseidon_hash([*IDENTITY_DERIVATION_PATH, secret_key]);
 
     // ============
     // Registration
     // ============
     let mut membership_tree = MerkleTree::new(100);
-    membership_tree.append(&MerkleNode::from(alice_identity_commitment));
-    let alice_identity_leafpos = membership_tree.witness().unwrap();
+    let mut identity_roots: Vec<MerkleNode> = vec![];
+    let mut banned_roots: Vec<MerkleNode> = vec![];
+    let mut identities = HashMap::new();
+
+    // Everyone needs to maintain the leaf positions, because to slash, we
+    // need to provide a valid authentication path. Therefore, the easiest
+    // way is to store a hashmap.
+    assert!(!identities.contains_key(&identity_commitment.to_repr()));
+    membership_tree.append(&MerkleNode::from(identity_commitment));
+    let leaf_pos = membership_tree.witness().unwrap();
+    identities.insert(identity_commitment.to_repr(), leaf_pos);
+    identity_roots.push(membership_tree.root(0).unwrap());
 
     // ==========
     // Signalling
     // ==========
+    let a_1 = poseidon_hash([secret_key, external_nullifier]);
 
-    // 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;
+    // Construct share
+    let x = hash_message(b"hello i wanna spam");
+    let y = a_1 * x + secret_key;
 
-    let internal_nullifier =
-        poseidon_hash([nf_derivation_path, poseidon_hash([alice_secret_key, external_nullifier])]);
+    // Construct internal nullifier
+    let internal_nullifier = poseidon_hash([*NULLIFIER_DERIVATION_PATH, a_1]);
 
     let identity_root = membership_tree.root(0).unwrap();
-    let alice_identity_path =
-        membership_tree.authentication_path(alice_identity_leafpos, &identity_root).unwrap();
+    let identity_path = membership_tree.authentication_path(leaf_pos, &identity_root);
+    let identity_path = identity_path.unwrap();
 
-    // NIZK stuff
-    let zkbin = include_bytes!("../signal.zk.bin");
-    let rln_zkbin = ZkBinary::decode(zkbin).unwrap();
+    // zkSNARK things
+    let signal_zkbin = include_bytes!("../signal.zk.bin");
+    let rln_zkbin = ZkBinary::decode(signal_zkbin).unwrap();
     let rln_empty_circuit = ZkCircuit::new(empty_witnesses(&rln_zkbin), rln_zkbin.clone());
 
-    println!("Building Proving key...");
+    print!("[Interaction] Building Proving key... ");
+    let now = Instant::now();
     let rln_pk = ProvingKey::build(13, &rln_empty_circuit);
-    println!("Building Verifying key...");
+    println!("[{:?}]", now.elapsed());
+
+    print!("[Interaction] Building Verifying key... ");
+    let now = Instant::now();
     let rln_vk = VerifyingKey::build(13, &rln_empty_circuit);
+    println!("[{:?}]", now.elapsed());
 
-    // 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())),
+    // Prover's witnesses and public inputs
+    let witnesses = vec![
+        Witness::Base(Value::known(secret_key)),
+        Witness::MerklePath(Value::known(identity_path.clone().try_into().unwrap())),
+        Witness::Uint32(Value::known(u64::from(leaf_pos).try_into().unwrap())),
         Witness::Base(Value::known(x)),
         Witness::Base(Value::known(epoch)),
-        Witness::Base(Value::known(rln_identifier)),
+        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
+        *RLN_IDENTIFIER,
+        x, // <-- Message hash
         identity_root.inner(),
         internal_nullifier,
         y,
     ];
 
-    println!("Creating ZK proof...");
+    // Build a circuit with these witnesses
+    print!("[Interaction] Creating ZK proof... ");
+    let now = Instant::now();
+    let rln_circuit = ZkCircuit::new(witnesses, rln_zkbin.clone());
     let proof = Proof::create(&rln_pk, &[rln_circuit], &public_inputs, &mut OsRng).unwrap();
+    println!("[{:?}]", now.elapsed());
 
     // ============
     // Verification
     // ============
-    println!("Verifying ZK proof...");
+    print!("[Interaction] Verifying ZK proof... ");
+    let now = Instant::now();
     assert!(proof.verify(&rln_vk, &public_inputs).is_ok());
+    assert!(!banned_roots.contains(&MerkleNode::from(public_inputs[3])));
+    assert!(identity_roots.contains(&MerkleNode::from(public_inputs[3])));
+    println!("[{:?}]", now.elapsed());
 
-    let mut alice_shares = vec![(public_inputs[2], public_inputs[5])];
+    // NOTE: These shares should actually be tracked through the internal nullifier.
+    let mut 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;
+    // Now if another message is sent in the same epoch, we should be able to
+    // recover the secret key and ban the sender.
+    let x = hash_message(b"hello i'm spamming");
+    let y = a_1 * x + 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())),
+    let witnesses = vec![
+        Witness::Base(Value::known(secret_key)),
+        Witness::MerklePath(Value::known(identity_path.try_into().unwrap())),
+        Witness::Uint32(Value::known(u64::from(leaf_pos).try_into().unwrap())),
         Witness::Base(Value::known(x)),
         Witness::Base(Value::known(epoch)),
-        Witness::Base(Value::known(rln_identifier)),
+        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
+        *RLN_IDENTIFIER,
+        x, // <-- Message hash
         identity_root.inner(),
         internal_nullifier,
         y,
     ];
 
-    println!("Creating ZK proof...");
+    // Build a circuit with these witnesses
+    print!("[Interaction] Creating ZK proof... ");
+    let now = Instant::now();
+    let rln_circuit = ZkCircuit::new(witnesses, rln_zkbin);
     let proof = Proof::create(&rln_pk, &[rln_circuit], &public_inputs, &mut OsRng).unwrap();
+    println!("[{:?}]", now.elapsed());
 
-    println!("Verifying ZK proof...");
+    print!("[Interaction] Verifying ZK proof... ");
+    let now = Instant::now();
     assert!(proof.verify(&rln_vk, &public_inputs).is_ok());
-    alice_shares.push((public_inputs[2], public_inputs[5]));
+    assert!(!banned_roots.contains(&MerkleNode::from(public_inputs[3])));
+    assert!(identity_roots.contains(&MerkleNode::from(public_inputs[3])));
+    println!("[{:?}]", now.elapsed());
+
+    // NOTE: These shares should actually be tracked through the internal nullifier.
+    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;
-    }
+    // We should be able to retrieve the secret key because two messages were
+    // sent in the same epoch.
+    let recovered_secret = sss_recover(&shares);
+    assert_eq!(recovered_secret, secret_key);
+
+    // Create a slash proof
+    let slash_zkbin = include_bytes!("../slash.zk.bin");
+    let slash_zkbin = ZkBinary::decode(slash_zkbin).unwrap();
+    let slash_empty_circuit = ZkCircuit::new(empty_witnesses(&slash_zkbin), slash_zkbin.clone());
+
+    print!("[Slash] Building Proving key... ");
+    let now = Instant::now();
+    let slash_pk = ProvingKey::build(13, &slash_empty_circuit);
+    println!("[{:?}]", now.elapsed());
+
+    print!("[Slash] Building Verifying key... ");
+    let now = Instant::now();
+    let slash_vk = VerifyingKey::build(13, &slash_empty_circuit);
+    println!("[{:?}]", now.elapsed());
+
+    // Find the leaf position in the hashmap of identity commitments
+    let identity_commitment = poseidon_hash([*IDENTITY_DERIVATION_PATH, recovered_secret]);
+    let leaf_pos = identities.get(&identity_commitment.to_repr()).unwrap();
+    let identity_root = membership_tree.root(0).unwrap();
+    let identity_path = membership_tree.authentication_path(*leaf_pos, &identity_root);
+    let identity_path = identity_path.unwrap();
+
+    // Witnesses & public inputs
+    let witnesses = vec![
+        Witness::Base(Value::known(recovered_secret)),
+        Witness::MerklePath(Value::known(identity_path.try_into().unwrap())),
+        Witness::Uint32(Value::known(u64::from(*leaf_pos).try_into().unwrap())),
+    ];
+
+    let public_inputs = vec![identity_root.inner()];
+
+    print!("[Slash] Creating ZK proof... ");
+    let now = Instant::now();
+    let slash_circuit = ZkCircuit::new(witnesses, slash_zkbin);
+    let proof = Proof::create(&slash_pk, &[slash_circuit], &public_inputs, &mut OsRng).unwrap();
+    println!("[{:?}]", now.elapsed());
+
+    print!("[Slash] Verifying ZK proof... ");
+    let now = Instant::now();
+    assert!(!banned_roots.contains(&MerkleNode::from(public_inputs[0])));
+    assert!(identity_roots.contains(&MerkleNode::from(public_inputs[0]))); // <- Will this be true?
+    assert!(proof.verify(&slash_vk, &public_inputs).is_ok());
+    println!("[{:?}]", now.elapsed());
+    banned_roots.push(MerkleNode::from(public_inputs[0]));
 
-    assert_eq!(secret, alice_secret_key);
-    println!("u banned");
+    println!("boi u banned");
 }

+ 1 - 1
src/sdk/src/crypto/mod.rs

@@ -82,7 +82,7 @@ pub use pasta_curves::{pallas, vesta};
 /// You still have to import the curves.
 pub mod pasta_prelude {
     pub use pasta_curves::{
-        arithmetic::CurveAffine,
+        arithmetic::{CurveAffine, CurveExt},
         group::{
             ff::{Field, PrimeField},
             Curve, Group,

+ 1 - 1
src/zk/mod.rs

@@ -28,7 +28,7 @@ pub mod gadget;
 
 /// Proof creation API
 pub mod proof;
-pub use proof::{Proof, ProvingKey};
+pub use proof::{Proof, ProvingKey, VerifyingKey};
 
 pub mod halo2 {
     pub use halo2_proofs::{