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

DAO::vote(): now with SMT flavor

zero 2 лет назад
Родитель
Сommit
83f5898de5

+ 27 - 11
src/contract/dao/proof/vote-input.zk

@@ -1,4 +1,4 @@
-k = 13;
+k = 14;
 field = "pallas";
 
 constant "VoteInput" {
@@ -15,11 +15,16 @@ witness "VoteInput" {
     Base coin_user_data,
     Base coin_blind,
 
+    Base proposal_bulla,
+
     Scalar value_blind,
     Base gov_token_blind,
 
     Uint32 leaf_pos,
-    MerklePath path,
+    MerklePath coin_path,
+
+    Base null_tree_root,
+    SparseMerklePath null_path,
 
     Base signature_secret,
 }
@@ -38,14 +43,25 @@ circuit "VoteInput" {
         coin_blind,
     );
 
-    # This is the same as for money::transfer() calls. We could use
-    # a set non-membership proof here, or alternatively just add a
-    # money::transfer() call for every DAO::vote() call. There's a
-    # limitation where votes across proposals are linked where this
-    # coin is active. The best fix would be the set non-membership,
-    # but that possibly has scaling issues.
+    # We need this to detect whether the above coin was already spent.
+    # Use a SMT, and show that at this position, the leaf is ZERO
+    ZERO = witness_base(0);
+    ONE = witness_base(1);
     nullifier = poseidon_hash(coin_secret, coin);
-    constrain_instance(nullifier);
+    is_member = sparse_tree_is_member(
+        null_tree_root,         # Expected root
+        null_path,              # Path to root
+        ZERO,                   # Leaf value
+        nullifier               # Position
+    );
+    constrain_equal_base(is_member, ONE);
+    constrain_instance(null_tree_root);
+
+    # Include some secret information in vote nullifier to defeat correlation
+    # attacks. We reveal the proposal_bulla in vote-main.zk as well.
+    vote_nullifier = poseidon_hash(nullifier, coin_secret, proposal_bulla);
+    constrain_instance(proposal_bulla);
+    constrain_instance(vote_nullifier);
 
     vcv = ec_mul_short(coin_value, VALUE_COMMIT_VALUE);
     vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
@@ -57,8 +73,8 @@ circuit "VoteInput" {
     constrain_instance(token_commit);
 
     # Merkle root
-    root = merkle_root(leaf_pos, path, coin);
-    constrain_instance(root);
+    merkle_coin_root = merkle_root(leaf_pos, coin_path, coin);
+    constrain_instance(merkle_coin_root);
 
     signature_public = ec_mul_base(signature_secret, NULLIFIER_K);
     constrain_instance(ec_get_x(signature_public));

+ 36 - 23
src/contract/dao/src/client/vote.rs

@@ -16,13 +16,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use darkfi_money_contract::model::{CoinAttributes, Nullifier};
+use darkfi_money_contract::model::CoinAttributes;
 use darkfi_sdk::{
     bridgetree,
     bridgetree::Hashable,
     crypto::{
         note::ElGamalEncryptedNote, pasta_prelude::*, pedersen_commitment_u64, poseidon_hash,
-        util::fv_mod_fp_unsafe, Blind, FuncId, Keypair, MerkleNode, PublicKey, SecretKey,
+        smt::SmtMemoryFp, util::fv_mod_fp_unsafe, Blind, FuncId, Keypair, MerkleNode, PublicKey,
+        SecretKey,
     },
     pasta::pallas,
 };
@@ -46,7 +47,8 @@ pub struct DaoVoteInput {
 }
 
 // Inside ZK proof, check proposal is correct.
-pub struct DaoVoteCall {
+pub struct DaoVoteCall<'a> {
+    pub money_null_smt: &'a SmtMemoryFp,
     pub inputs: Vec<DaoVoteInput>,
     pub vote_option: bool,
     pub proposal: DaoProposal,
@@ -55,7 +57,7 @@ pub struct DaoVoteCall {
     pub current_day: u64,
 }
 
-impl DaoVoteCall {
+impl<'a> DaoVoteCall<'a> {
     pub fn make(
         self,
         burn_zkbin: &ZkBinary,
@@ -64,6 +66,10 @@ impl DaoVoteCall {
         main_pk: &ProvingKey,
     ) -> Result<(DaoVoteParams, Vec<Proof>)> {
         debug!(target: "dao", "build()");
+
+        assert_eq!(self.dao.to_bulla(), self.proposal.dao_bulla);
+        let proposal_bulla = self.proposal.to_bulla();
+
         let mut proofs = vec![];
 
         let gov_token_blind = pallas::Base::random(&mut OsRng);
@@ -105,6 +111,22 @@ impl DaoVoteCall {
             let note = input.note;
             let leaf_pos: u64 = input.leaf_position.into();
 
+            let public_key = PublicKey::from_secret(input.secret);
+            let coin = CoinAttributes {
+                public_key,
+                value: note.value,
+                token_id: note.token_id,
+                spend_hook: FuncId::none(),
+                user_data: pallas::Base::ZERO,
+                blind: note.coin_blind,
+            }
+            .to_coin();
+            let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]);
+
+            let smt_null_root = self.money_null_smt.root();
+            let smt_null_path = self.money_null_smt.prove_membership(&nullifier);
+            assert!(smt_null_path.verify(&smt_null_root, &pallas::Base::ZERO, &nullifier));
+
             let prover_witnesses = vec![
                 Witness::Base(Value::known(input.secret.inner())),
                 Witness::Base(Value::known(pallas::Base::from(note.value))),
@@ -112,24 +134,16 @@ impl DaoVoteCall {
                 Witness::Base(Value::known(pallas::Base::ZERO)),
                 Witness::Base(Value::known(pallas::Base::ZERO)),
                 Witness::Base(Value::known(note.coin_blind.inner())),
+                Witness::Base(Value::known(proposal_bulla.inner())),
                 Witness::Scalar(Value::known(value_blind)),
                 Witness::Base(Value::known(gov_token_blind)),
                 Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())),
                 Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())),
+                Witness::Base(Value::known(smt_null_root)),
+                Witness::SparseMerklePath(Value::known(smt_null_path.path)),
                 Witness::Base(Value::known(input.signature_secret.inner())),
             ];
 
-            let public_key = PublicKey::from_secret(input.secret);
-            let coin = CoinAttributes {
-                public_key,
-                value: note.value,
-                token_id: note.token_id,
-                spend_hook: FuncId::none(),
-                user_data: pallas::Base::ZERO,
-                blind: note.coin_blind,
-            }
-            .to_coin();
-
             let merkle_root = {
                 let position: u64 = input.leaf_position.into();
                 let mut current = MerkleNode::from(coin.inner());
@@ -147,15 +161,18 @@ impl DaoVoteCall {
             let token_commit = poseidon_hash([note.token_id.inner(), gov_token_blind]);
             assert_eq!(self.dao.gov_token_id, note.token_id);
 
-            let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]);
-
             let vote_commit = pedersen_commitment_u64(note.value, Blind(value_blind));
             let vote_commit_coords = vote_commit.to_affine().coordinates().unwrap();
 
             let (sig_x, sig_y) = signature_public.xy();
 
+            let vote_nullifier =
+                poseidon_hash([nullifier, input.secret.inner(), proposal_bulla.inner()]);
+
             let public_inputs = vec![
-                nullifier,
+                smt_null_root,
+                proposal_bulla.inner(),
+                vote_nullifier,
                 *vote_commit_coords.x(),
                 *vote_commit_coords.y(),
                 token_commit,
@@ -171,9 +188,8 @@ impl DaoVoteCall {
             proofs.push(input_proof);
 
             let input = DaoVoteParamsInput {
-                nullifier: Nullifier::from(nullifier),
                 vote_commit,
-                merkle_root,
+                vote_nullifier: vote_nullifier.into(),
                 signature_public,
             };
             inputs.push(input);
@@ -246,9 +262,6 @@ impl DaoVoteCall {
             Witness::Base(Value::known(ephem_secret.inner())),
         ];
 
-        assert_eq!(self.dao.to_bulla(), self.proposal.dao_bulla);
-        let proposal_bulla = self.proposal.to_bulla();
-
         let note = [vote_option, yes_vote_blind.inner(), all_vote_value_fp, all_vote_blind.inner()];
         let enc_note =
             ElGamalEncryptedNote::encrypt_unsafe(note, &ephem_secret, &self.dao_keypair.public)?;

+ 16 - 21
src/contract/dao/src/entrypoint/vote.rs

@@ -39,7 +39,7 @@ use crate::{
 
 /// `get_metdata` function for `Dao::Vote`
 pub(crate) fn dao_vote_get_metadata(
-    _cid: ContractId,
+    cid: ContractId,
     call_idx: u32,
     calls: Vec<DarkLeaf<ContractCall>>,
 ) -> Result<Vec<u8>, ContractError> {
@@ -59,6 +59,14 @@ pub(crate) fn dao_vote_get_metadata(
     // Commitment calculation for all votes
     let mut all_vote_commit = pallas::Point::identity();
 
+    let proposal_votes_db = db_lookup(cid, DAO_CONTRACT_DB_PROPOSAL_BULLAS)?;
+    let Some(data) = db_get(proposal_votes_db, &serialize(&params.proposal_bulla))? else {
+        msg!("[Dao::Vote] Error: Proposal doesn't exist: {:?}", params.proposal_bulla);
+        return Err(DaoError::ProposalNonexistent.into())
+    };
+    // Get the current votes
+    let mut proposal_metadata: DaoProposalMetadata = deserialize(&data)?;
+
     // Iterate through inputs
     for input in &params.inputs {
         signature_pubkeys.push(input.signature_public);
@@ -70,11 +78,13 @@ pub(crate) fn dao_vote_get_metadata(
         zk_public_inputs.push((
             DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS.to_string(),
             vec![
-                input.nullifier.inner(),
+                proposal_metadata.snapshot_nulls,
+                params.proposal_bulla.inner(),
+                input.vote_nullifier.inner(),
                 *value_coords.x(),
                 *value_coords.y(),
                 params.token_commit,
-                input.merkle_root.inner(),
+                proposal_metadata.snapshot_coins.inner(),
                 sig_x,
                 sig_y,
             ],
@@ -139,26 +149,11 @@ pub(crate) fn dao_vote_process_instruction(
     let mut vote_nullifiers = vec![];
 
     for input in &params.inputs {
-        // TODO: remove merkle_coins entirely from input. It's not needed.
-        if proposal_metadata.snapshot_coins != input.merkle_root {
-            msg!(
-                "[Dao::Vote] Error: Invalid input Merkle root: {} (expected {})",
-                input.merkle_root,
-                proposal_metadata.snapshot_coins
-            );
-            return Err(DaoError::InvalidInputMerkleRoot.into())
-        }
-
-        if db_contains_key(money_nullifier_db, &serialize(&input.nullifier))? {
-            msg!("[Dao::Vote] Error: Coin is already spent");
-            return Err(DaoError::CoinAlreadySpent.into())
-        }
-
         // Prefix nullifier with proposal bulla so nullifiers from different proposals
         // don't interfere with each other.
-        let null_key = serialize(&(params.proposal_bulla, input.nullifier));
+        let null_key = serialize(&(params.proposal_bulla, input.vote_nullifier));
 
-        if vote_nullifiers.contains(&input.nullifier) ||
+        if vote_nullifiers.contains(&input.vote_nullifier) ||
             db_contains_key(dao_vote_nullifier_db, &null_key)?
         {
             msg!("[Dao::Vote] Error: Attempted double vote");
@@ -166,7 +161,7 @@ pub(crate) fn dao_vote_process_instruction(
         }
 
         proposal_metadata.vote_aggregate.all_vote_commit += input.vote_commit;
-        vote_nullifiers.push(input.nullifier);
+        vote_nullifiers.push(input.vote_nullifier);
     }
 
     proposal_metadata.vote_aggregate.yes_vote_commit += params.yes_vote_commit;

+ 2 - 4
src/contract/dao/src/model.rs

@@ -296,12 +296,10 @@ pub struct DaoVoteParams {
 // ANCHOR: dao-vote-params-input
 /// Input for a DAO proposal vote
 pub struct DaoVoteParamsInput {
-    /// Revealed nullifier
-    pub nullifier: Nullifier,
     /// Vote commitment
     pub vote_commit: pallas::Point,
-    /// Merkle root for the input's inclusion proof
-    pub merkle_root: MerkleNode,
+    /// Vote nullifier
+    pub vote_nullifier: Nullifier,
     /// Public key used for signing
     pub signature_public: PublicKey,
 }

+ 0 - 1
src/contract/money/src/entrypoint.rs

@@ -21,7 +21,6 @@ use darkfi_sdk::{
     dark_tree::DarkLeaf,
     db::{db_init, db_lookup, db_set, zkas_db_set},
     error::ContractResult,
-    msg,
     pasta::pallas,
     util::{get_call_index, set_return_data},
     ContractCall,

+ 2 - 0
src/contract/test-harness/src/dao_propose.rs

@@ -202,6 +202,8 @@ impl TestHarness {
         // Execute the transaction
         wallet.add_transaction("dao::propose", tx, block_height, self.verify_fees).await?;
 
+        wallet.money_null_smt_snapshot = Some(wallet.money_null_smt.clone());
+
         if !append {
             return Ok(vec![])
         }

+ 1 - 0
src/contract/test-harness/src/dao_vote.rs

@@ -83,6 +83,7 @@ impl TestHarness {
 
         let current_day = blockwindow(block_height);
         let call = DaoVoteCall {
+            money_null_smt: &wallet.money_null_smt_snapshot.as_ref().unwrap(),
             inputs: vec![input],
             vote_option,
             proposal: proposal.clone(),

+ 4 - 1
src/contract/test-harness/src/lib.rs

@@ -125,8 +125,10 @@ pub struct Wallet {
     pub validator: ValidatorPtr,
     /// Holder's instance of the Merkle tree for the `Money` contract
     pub money_merkle_tree: MerkleTree,
-    /// Holder's instance of the Merkle tree for the `Money` contract
+    /// Holder's instance of the SMT tree for the `Money` contract
     pub money_null_smt: SmtMemoryFp,
+    /// Holder's instance of the SMT tree for the `Money` contract (snapshotted for DAO::propose())
+    pub money_null_smt_snapshot: Option<SmtMemoryFp>,
     /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO bullas)
     pub dao_merkle_tree: MerkleTree,
     /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO proposals)
@@ -186,6 +188,7 @@ impl Wallet {
             validator,
             money_merkle_tree,
             money_null_smt,
+            money_null_smt_snapshot: None,
             dao_merkle_tree: MerkleTree::new(100),
             dao_proposals_tree: MerkleTree::new(100),
             unspent_money_coins: vec![],

+ 2 - 2
src/sdk/src/crypto/smt/mod.rs

@@ -101,7 +101,7 @@ pub trait StorageAdapter {
 }
 
 /// An in-memory storage, useful for unit tests and smaller trees.
-#[derive(Default)]
+#[derive(Default, Clone)]
 pub struct MemoryStorage<F: FieldElement> {
     tree: HashMap<BigUint, F>,
 }
@@ -131,7 +131,7 @@ impl<F: FieldElement> StorageAdapter for MemoryStorage<F> {
 ///
 /// The trait param `N` is the depth of the tree. A tree with a depth of `N`
 /// will have `N + 1` levels.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct SparseMerkleTree<
     'a,
     const N: usize,