فهرست منبع

make DAO nullifiers the same as money, otherwise we can't detect whether the coins we're using were already spent or not. Having access to a set non-membership merkle tree here would fix this.

zero 2 سال پیش
والد
کامیت
9fd2e2e467

+ 16 - 17
src/contract/dao/proof/dao-propose-burn.zk

@@ -22,20 +22,6 @@ witness "DaoProposeInput" {
 }
 
 circuit "DaoProposeInput" {
-    #nullifier = poseidon_hash(secret, serial);
-    #constrain_instance(nullifier);
-
-    # Pedersen commitment for coin's value
-    vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
-    vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
-    value_commit = ec_add(vcv, vcr);
-    constrain_instance(ec_get_x(value_commit));
-    constrain_instance(ec_get_y(value_commit));
-
-    # Commitment for coin's token ID
-    token_commit = poseidon_hash(token, token_blind);
-    constrain_instance(token_commit);
-
     # Coin hash
     pub = ec_mul_base(secret, NULLIFIER_K);
     pub_x = ec_get_x(pub);
@@ -50,9 +36,22 @@ circuit "DaoProposeInput" {
         user_data,
     );
 
-    # BUG: doesn't check if coin was spent.
-    # we need to fix this.
-    # ideas???
+    # We need this to detect whether the above coin was already spent.
+    # To avoid leaking timing & other info, we can just make a
+    # money::transfer() call within the same tx.
+    nullifier = poseidon_hash(secret, coin);
+    constrain_instance(nullifier);
+
+    # Pedersen commitment for coin's value
+    vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
+    vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
+    value_commit = ec_add(vcv, vcr);
+    constrain_instance(ec_get_x(value_commit));
+    constrain_instance(ec_get_y(value_commit));
+
+    # Commitment for coin's token ID
+    token_commit = poseidon_hash(token, token_blind);
+    constrain_instance(token_commit);
 
     # Merkle root
     root = merkle_root(leaf_pos, path, coin);

+ 8 - 4
src/contract/dao/proof/dao-vote-burn.zk

@@ -19,8 +19,6 @@ witness "DaoVoteInput" {
     Uint32 leaf_pos,
     MerklePath path,
     Base signature_secret,
-
-    Base proposal_bulla,
 }
 
 circuit "DaoVoteInput" {
@@ -36,8 +34,14 @@ circuit "DaoVoteInput" {
         spend_hook,
         user_data,
     );
-    nullifier = poseidon_hash(serial, coin, proposal_bulla);
-    constrain_instance(proposal_bulla);
+
+    # 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.
+    nullifier = poseidon_hash(secret, coin);
     constrain_instance(nullifier);
 
     vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);

+ 8 - 4
src/contract/dao/src/client/propose.rs

@@ -22,7 +22,7 @@ use darkfi_sdk::{
     bridgetree::Hashable,
     crypto::{
         note::AeadEncryptedNote, pasta_prelude::*, pedersen::pedersen_commitment_u64,
-        poseidon_hash, MerkleNode, PublicKey, SecretKey,
+        poseidon_hash, MerkleNode, Nullifier, PublicKey, SecretKey,
     },
     pasta::pallas,
 };
@@ -83,8 +83,8 @@ impl DaoProposeCall {
             let prover_witnesses = vec![
                 Witness::Base(Value::known(input.secret.inner())),
                 Witness::Base(Value::known(note.serial)),
-                Witness::Base(Value::known(pallas::Base::from(0))),
-                Witness::Base(Value::known(pallas::Base::from(0))),
+                Witness::Base(Value::known(pallas::Base::ZERO)),
+                Witness::Base(Value::known(pallas::Base::ZERO)),
                 Witness::Base(Value::known(pallas::Base::from(note.value))),
                 Witness::Base(Value::known(note.token_id.inner())),
                 Witness::Scalar(Value::known(funds_blind)),
@@ -121,6 +121,8 @@ impl DaoProposeCall {
                 current
             };
 
+            let nullifier: Nullifier = poseidon_hash([input.secret.inner(), coin.inner()]).into();
+
             let token_commit = poseidon_hash([note.token_id.inner(), gov_token_blind]);
             assert_eq!(self.dao.gov_token_id, note.token_id);
 
@@ -130,6 +132,7 @@ impl DaoProposeCall {
             let (sig_x, sig_y) = signature_public.xy();
 
             let public_inputs = vec![
+                nullifier.inner(),
                 *value_coords.x(),
                 *value_coords.y(),
                 token_commit,
@@ -144,7 +147,8 @@ impl DaoProposeCall {
                 .expect("DAO::propose() proving error!");
             proofs.push(input_proof);
 
-            let input = DaoProposeParamsInput { value_commit, merkle_root, signature_public };
+            let input =
+                DaoProposeParamsInput { nullifier, value_commit, merkle_root, signature_public };
             inputs.push(input);
         }
 

+ 6 - 8
src/contract/dao/src/client/vote.rs

@@ -77,9 +77,6 @@ impl DaoVoteCall {
         debug!(target: "dao", "build()");
         let mut proofs = vec![];
 
-        assert_eq!(self.dao.to_bulla(), self.proposal.dao_bulla);
-        let proposal_bulla = self.proposal.to_bulla();
-
         let gov_token_blind = pallas::Base::random(&mut OsRng);
 
         let mut inputs = vec![];
@@ -101,8 +98,8 @@ impl DaoVoteCall {
             let prover_witnesses = vec![
                 Witness::Base(Value::known(input.secret.inner())),
                 Witness::Base(Value::known(note.serial)),
-                Witness::Base(Value::known(pallas::Base::from(0))),
-                Witness::Base(Value::known(pallas::Base::from(0))),
+                Witness::Base(Value::known(pallas::Base::ZERO)),
+                Witness::Base(Value::known(pallas::Base::ZERO)),
                 Witness::Base(Value::known(pallas::Base::from(note.value))),
                 Witness::Base(Value::known(note.token_id.inner())),
                 Witness::Scalar(Value::known(all_vote_blind)),
@@ -110,7 +107,6 @@ impl DaoVoteCall {
                 Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())),
                 Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())),
                 Witness::Base(Value::known(input.signature_secret.inner())),
-                Witness::Base(Value::known(proposal_bulla.inner())),
             ];
 
             let public_key = PublicKey::from_secret(input.secret);
@@ -141,7 +137,7 @@ 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([note.serial, coin.inner(), proposal_bulla.inner()]);
+            let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]);
 
             let vote_commit = pedersen_commitment_u64(note.value, all_vote_blind);
             let vote_commit_coords = vote_commit.to_affine().coordinates().unwrap();
@@ -149,7 +145,6 @@ impl DaoVoteCall {
             let (sig_x, sig_y) = signature_public.xy();
 
             let public_inputs = vec![
-                proposal_bulla.inner(),
                 nullifier,
                 *vote_commit_coords.x(),
                 *vote_commit_coords.y(),
@@ -221,6 +216,9 @@ impl DaoVoteCall {
             Witness::Base(Value::known(current_day)),
         ];
 
+        assert_eq!(self.dao.to_bulla(), self.proposal.dao_bulla);
+        let proposal_bulla = self.proposal.to_bulla();
+
         let public_inputs = vec![
             token_commit,
             proposal_bulla.inner(),

+ 10 - 1
src/contract/dao/src/entrypoint/propose.rs

@@ -18,6 +18,7 @@
 
 use darkfi_money_contract::{
     MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT,
+    MONEY_CONTRACT_NULLIFIERS_TREE,
 };
 use darkfi_sdk::{
     crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, MerkleNode, PublicKey},
@@ -71,6 +72,7 @@ pub(crate) fn dao_propose_get_metadata(
         zk_public_inputs.push((
             DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS.to_string(),
             vec![
+                input.nullifier.inner(),
                 *value_coords.x(),
                 *value_coords.y(),
                 params.token_commit,
@@ -113,13 +115,20 @@ pub(crate) fn dao_propose_process_instruction(
     let self_ = &calls[call_idx as usize].data;
     let params: DaoProposeParams = deserialize(&self_.data[1..])?;
 
-    // Check the Merkle roots for the input coins are valid
     let coin_roots_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+    let money_nullifier_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIERS_TREE)?;
     for input in &params.inputs {
+        // Check the Merkle roots for the input coins are valid
         if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
             msg!("[Dao::Propose] Error: Invalid input Merkle root: {}", input.merkle_root);
             return Err(DaoError::InvalidInputMerkleRoot.into())
         }
+
+        // Check the coins weren't already spent
+        if db_contains_key(money_nullifier_db, &serialize(&input.nullifier))? {
+            msg!("[Dao::Vote] Error: Coin is already spent");
+            return Err(DaoError::CoinAlreadySpent.into())
+        }
     }
 
     // Is the DAO bulla generated in the ZK proof valid

+ 0 - 7
src/contract/dao/src/entrypoint/vote.rs

@@ -66,15 +66,9 @@ pub(crate) fn dao_vote_get_metadata(
         let value_coords = input.vote_commit.to_affine().coordinates().unwrap();
         let (sig_x, sig_y) = input.signature_public.xy();
 
-        // TODO: Here we "trust" the input param's merkle root. Instead we compare
-        // that this root equals to the proposal's snapshotted root later in the
-        // `process_instruction`. Should we just enforce it here instead/aswell?
-        // The reason is because ZK proofs are verified afterwards, so by checking
-        // in wasm first, we can potentially bail out more quickly.
         zk_public_inputs.push((
             DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS.to_string(),
             vec![
-                params.proposal_bulla.inner(),
                 input.nullifier.inner(),
                 *value_coords.x(),
                 *value_coords.y(),
@@ -129,7 +123,6 @@ pub(crate) fn dao_vote_process_instruction(
     };
 
     // Get the current votes
-    // TODO: Proposals should have a set length of time
     let mut proposal_metadata: DaoProposalMetadata = deserialize(&data)?;
 
     // Check the Merkle root and nullifiers for the input coins are valid

+ 1 - 0
src/contract/dao/src/model.rs

@@ -240,6 +240,7 @@ pub struct DaoProposeParams {
 // ANCHOR: dao-propose-input-params
 /// Input for a DAO proposal
 pub struct DaoProposeParamsInput {
+    pub nullifier: Nullifier,
     /// Value commitment for the input
     pub value_commit: pallas::Point,
     /// Merkle root for the input's inclusion proof

+ 2 - 2
src/contract/test-harness/src/vks.rs

@@ -50,8 +50,8 @@ use darkfi_serial::{deserialize, serialize};
 use log::debug;
 
 /// Update this if any circuits are changed
-const VKS_HASH: &str = "c863e90ea555cad8d1294ac95bd4600b86c16def186e6dc1b43de10cf0d6fd4b";
-const PKS_HASH: &str = "ea7017dd7eb2471604f04f8f1be78f0927ad1037009986d3b917680a61d323fc";
+const VKS_HASH: &str = "05141828ac8c0a9cdee54a9885024c889048507f84546c92b75c7ca6d6ad2814";
+const PKS_HASH: &str = "a4571b06b7b875e5c22bc1da538d636cd375cbb41a921b6d7fc5620044b8e9b5";
 
 fn pks_path(typ: &str) -> Result<PathBuf> {
     let output = Command::new("git").arg("rev-parse").arg("--show-toplevel").output()?.stdout;