Przeglądaj źródła

contract/dao: Introduce money state snapshotting.

This prevents double-voting in the sense of minting new coins _after_
a proposal was created, meaning only the coins that have existed prior
to creating a certain proposal are able to vote on it.

This is done by marking down the latest Merkle root in the Money state
and attaching it to the proposal's metadata. Then this root is used for
verifying inclusion proofs when voting.
parazyd 3 lat temu
rodzic
commit
33c286f19f

+ 1 - 1
bin/drk/src/rpc_dao.rs

@@ -257,7 +257,7 @@ impl Drk {
             return Err(anyhow!("Proposal ID not found"))
             return Err(anyhow!("Proposal ID not found"))
         };
         };
 
 
-        let money_tree = self.get_money_tree().await?;
+        let money_tree = proposal.money_snapshot_tree.clone().unwrap();
 
 
         let mut coins: Vec<OwnCoin> =
         let mut coins: Vec<OwnCoin> =
             self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect();
             self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect();

+ 56 - 18
bin/drk/src/wallet_dao.rs

@@ -31,13 +31,14 @@ use darkfi_dao_contract::{
         DAO_DAOS_COL_NAME, DAO_DAOS_COL_PROPOSER_LIMIT, DAO_DAOS_COL_QUORUM, DAO_DAOS_COL_SECRET,
         DAO_DAOS_COL_NAME, DAO_DAOS_COL_PROPOSER_LIMIT, DAO_DAOS_COL_QUORUM, DAO_DAOS_COL_SECRET,
         DAO_DAOS_COL_TX_HASH, DAO_DAOS_TABLE, DAO_PROPOSALS_COL_AMOUNT,
         DAO_DAOS_COL_TX_HASH, DAO_DAOS_TABLE, DAO_PROPOSALS_COL_AMOUNT,
         DAO_PROPOSALS_COL_BULLA_BLIND, DAO_PROPOSALS_COL_CALL_INDEX, DAO_PROPOSALS_COL_DAO_ID,
         DAO_PROPOSALS_COL_BULLA_BLIND, DAO_PROPOSALS_COL_CALL_INDEX, DAO_PROPOSALS_COL_DAO_ID,
-        DAO_PROPOSALS_COL_LEAF_POSITION, DAO_PROPOSALS_COL_OUR_VOTE_ID,
-        DAO_PROPOSALS_COL_PROPOSAL_ID, DAO_PROPOSALS_COL_RECV_PUBLIC,
-        DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, DAO_PROPOSALS_COL_TX_HASH, DAO_PROPOSALS_TABLE,
-        DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE, DAO_TREES_TABLE,
-        DAO_VOTES_COL_ALL_VOTE_BLIND, DAO_VOTES_COL_ALL_VOTE_VALUE, DAO_VOTES_COL_CALL_INDEX,
-        DAO_VOTES_COL_PROPOSAL_ID, DAO_VOTES_COL_TX_HASH, DAO_VOTES_COL_VOTE_ID,
-        DAO_VOTES_COL_VOTE_OPTION, DAO_VOTES_COL_YES_VOTE_BLIND, DAO_VOTES_TABLE,
+        DAO_PROPOSALS_COL_LEAF_POSITION, DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
+        DAO_PROPOSALS_COL_OUR_VOTE_ID, DAO_PROPOSALS_COL_PROPOSAL_ID,
+        DAO_PROPOSALS_COL_RECV_PUBLIC, DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID,
+        DAO_PROPOSALS_COL_TX_HASH, DAO_PROPOSALS_TABLE, DAO_TREES_COL_DAOS_TREE,
+        DAO_TREES_COL_PROPOSALS_TREE, DAO_TREES_TABLE, DAO_VOTES_COL_ALL_VOTE_BLIND,
+        DAO_VOTES_COL_ALL_VOTE_VALUE, DAO_VOTES_COL_CALL_INDEX, DAO_VOTES_COL_PROPOSAL_ID,
+        DAO_VOTES_COL_TX_HASH, DAO_VOTES_COL_VOTE_ID, DAO_VOTES_COL_VOTE_OPTION,
+        DAO_VOTES_COL_YES_VOTE_BLIND, DAO_VOTES_TABLE,
     },
     },
     model::{DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams},
     model::{DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams},
     DaoFunction,
     DaoFunction,
@@ -200,6 +201,8 @@ pub struct DaoProposal {
     pub bulla_blind: pallas::Base,
     pub bulla_blind: pallas::Base,
     /// Leaf position of this proposal in the Merkle tree of proposals
     /// Leaf position of this proposal in the Merkle tree of proposals
     pub leaf_position: Option<bridgetree::Position>,
     pub leaf_position: Option<bridgetree::Position>,
+    /// Snapshotted Money Merkle tree
+    pub money_snapshot_tree: Option<MerkleTree>,
     /// Transaction hash where this proposal was proposed
     /// Transaction hash where this proposal was proposed
     pub tx_hash: Option<blake3::Hash>,
     pub tx_hash: Option<blake3::Hash>,
     /// call index in the transaction where this proposal was proposed
     /// call index in the transaction where this proposal was proposed
@@ -714,6 +717,8 @@ impl Drk {
             QueryType::OptionBlob as u8,
             QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_LEAF_POSITION,
             DAO_PROPOSALS_COL_LEAF_POSITION,
             QueryType::OptionBlob as u8,
             QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
+            QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_TX_HASH,
             DAO_PROPOSALS_COL_TX_HASH,
             QueryType::OptionInteger as u8,
             QueryType::OptionInteger as u8,
             DAO_PROPOSALS_COL_CALL_INDEX,
             DAO_PROPOSALS_COL_CALL_INDEX,
@@ -754,11 +759,14 @@ impl Drk {
             let bulla_blind = deserialize(&bulla_blind_bytes)?;
             let bulla_blind = deserialize(&bulla_blind_bytes)?;
 
 
             let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
             let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
-            let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
 
 
-            let call_index = serde_json::from_value(row[8].clone())?;
+            let money_snapshot_tree_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
+
+            let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[8].clone())?;
 
 
-            let vote_id_bytes: Vec<u8> = serde_json::from_value(row[9].clone())?;
+            let call_index = serde_json::from_value(row[9].clone())?;
+
+            let vote_id_bytes: Vec<u8> = serde_json::from_value(row[10].clone())?;
 
 
             let leaf_position = if leaf_position_bytes.is_empty() {
             let leaf_position = if leaf_position_bytes.is_empty() {
                 None
                 None
@@ -766,6 +774,12 @@ impl Drk {
                 Some(deserialize(&leaf_position_bytes)?)
                 Some(deserialize(&leaf_position_bytes)?)
             };
             };
 
 
+            let money_snapshot_tree = if money_snapshot_tree_bytes.is_empty() {
+                None
+            } else {
+                Some(deserialize(&money_snapshot_tree_bytes)?)
+            };
+
             let tx_hash =
             let tx_hash =
                 if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
                 if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
 
 
@@ -780,6 +794,7 @@ impl Drk {
                 token_id,
                 token_id,
                 bulla_blind,
                 bulla_blind,
                 leaf_position,
                 leaf_position,
+                money_snapshot_tree,
                 tx_hash,
                 tx_hash,
                 call_index,
                 call_index,
                 vote_id,
                 vote_id,
@@ -818,6 +833,8 @@ impl Drk {
             QueryType::OptionBlob as u8,
             QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_LEAF_POSITION,
             DAO_PROPOSALS_COL_LEAF_POSITION,
             QueryType::OptionBlob as u8,
             QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
+            QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_TX_HASH,
             DAO_PROPOSALS_COL_TX_HASH,
             QueryType::OptionInteger as u8,
             QueryType::OptionInteger as u8,
             DAO_PROPOSALS_COL_CALL_INDEX,
             DAO_PROPOSALS_COL_CALL_INDEX,
@@ -848,11 +865,14 @@ impl Drk {
         let bulla_blind = deserialize(&bulla_blind_bytes)?;
         let bulla_blind = deserialize(&bulla_blind_bytes)?;
 
 
         let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
         let leaf_position_bytes: Vec<u8> = serde_json::from_value(row[6].clone())?;
-        let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
 
 
-        let call_index = serde_json::from_value(row[8].clone())?;
+        let money_snapshot_tree_bytes: Vec<u8> = serde_json::from_value(row[7].clone())?;
 
 
-        let vote_id_bytes: Vec<u8> = serde_json::from_value(row[9].clone())?;
+        let tx_hash_bytes: Vec<u8> = serde_json::from_value(row[8].clone())?;
+
+        let call_index = serde_json::from_value(row[9].clone())?;
+
+        let vote_id_bytes: Vec<u8> = serde_json::from_value(row[10].clone())?;
 
 
         let leaf_position = if leaf_position_bytes.is_empty() {
         let leaf_position = if leaf_position_bytes.is_empty() {
             None
             None
@@ -863,6 +883,12 @@ impl Drk {
         let tx_hash =
         let tx_hash =
             if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
             if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
 
 
+        let money_snapshot_tree = if money_snapshot_tree_bytes.is_empty() {
+            None
+        } else {
+            Some(deserialize(&money_snapshot_tree_bytes)?)
+        };
+
         let vote_id =
         let vote_id =
             if vote_id_bytes.is_empty() { None } else { Some(deserialize(&vote_id_bytes)?) };
             if vote_id_bytes.is_empty() { None } else { Some(deserialize(&vote_id_bytes)?) };
 
 
@@ -876,6 +902,7 @@ impl Drk {
             token_id,
             token_id,
             bulla_blind,
             bulla_blind,
             leaf_position,
             leaf_position,
+            money_snapshot_tree,
             tx_hash,
             tx_hash,
             call_index,
             call_index,
             vote_id,
             vote_id,
@@ -974,7 +1001,12 @@ impl Drk {
         // DAOs that have been minted
         // DAOs that have been minted
         let mut new_dao_bullas: Vec<(DaoBulla, Option<blake3::Hash>, u32)> = vec![];
         let mut new_dao_bullas: Vec<(DaoBulla, Option<blake3::Hash>, u32)> = vec![];
         // DAO proposals that have been minted
         // DAO proposals that have been minted
-        let mut new_dao_proposals: Vec<(DaoProposeParams, Option<blake3::Hash>, u32)> = vec![];
+        let mut new_dao_proposals: Vec<(
+            DaoProposeParams,
+            Option<MerkleTree>,
+            Option<blake3::Hash>,
+            u32,
+        )> = vec![];
         let mut our_proposals: Vec<DaoProposal> = vec![];
         let mut our_proposals: Vec<DaoProposal> = vec![];
         // DAO votes that have been seen
         // DAO votes that have been seen
         let mut new_dao_votes: Vec<(DaoVoteParams, Option<blake3::Hash>, u32)> = vec![];
         let mut new_dao_votes: Vec<(DaoVoteParams, Option<blake3::Hash>, u32)> = vec![];
@@ -994,7 +1026,9 @@ impl Drk {
                 eprintln!("Found Dao::Propose in call {}", i);
                 eprintln!("Found Dao::Propose in call {}", i);
                 let params: DaoProposeParams = deserialize(&call.data[1..])?;
                 let params: DaoProposeParams = deserialize(&call.data[1..])?;
                 let tx_hash = if confirm { Some(blake3::hash(&serialize(tx))) } else { None };
                 let tx_hash = if confirm { Some(blake3::hash(&serialize(tx))) } else { None };
-                new_dao_proposals.push((params, tx_hash, i as u32));
+                // We need to clone the tree here for reproducing the snapshot Merkle root
+                let money_tree = if confirm { Some(self.get_money_tree().await?) } else { None };
+                new_dao_proposals.push((params, money_tree, tx_hash, i as u32));
                 continue
                 continue
             }
             }
 
 
@@ -1057,8 +1091,9 @@ impl Drk {
                             token_id: note.proposal.token_id,
                             token_id: note.proposal.token_id,
                             bulla_blind: note.proposal.blind,
                             bulla_blind: note.proposal.blind,
                             leaf_position: proposals_tree.mark(),
                             leaf_position: proposals_tree.mark(),
-                            tx_hash: proposal.1,
-                            call_index: Some(proposal.2),
+                            money_snapshot_tree: proposal.1,
+                            tx_hash: proposal.2,
+                            call_index: Some(proposal.3),
                             vote_id: None,
                             vote_id: None,
                         };
                         };
 
 
@@ -1186,7 +1221,7 @@ impl Drk {
             };
             };
 
 
             let query = format!(
             let query = format!(
-                "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
+                "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);",
                 DAO_PROPOSALS_TABLE,
                 DAO_PROPOSALS_TABLE,
                 DAO_PROPOSALS_COL_DAO_ID,
                 DAO_PROPOSALS_COL_DAO_ID,
                 DAO_PROPOSALS_COL_RECV_PUBLIC,
                 DAO_PROPOSALS_COL_RECV_PUBLIC,
@@ -1194,6 +1229,7 @@ impl Drk {
                 DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID,
                 DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID,
                 DAO_PROPOSALS_COL_BULLA_BLIND,
                 DAO_PROPOSALS_COL_BULLA_BLIND,
                 DAO_PROPOSALS_COL_LEAF_POSITION,
                 DAO_PROPOSALS_COL_LEAF_POSITION,
+                DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
                 DAO_PROPOSALS_COL_TX_HASH,
                 DAO_PROPOSALS_COL_TX_HASH,
                 DAO_PROPOSALS_COL_CALL_INDEX,
                 DAO_PROPOSALS_COL_CALL_INDEX,
             );
             );
@@ -1213,6 +1249,8 @@ impl Drk {
                 QueryType::Blob as u8,
                 QueryType::Blob as u8,
                 serialize(&proposal.leaf_position.unwrap()),
                 serialize(&proposal.leaf_position.unwrap()),
                 QueryType::Blob as u8,
                 QueryType::Blob as u8,
+                serialize(&proposal.money_snapshot_tree.clone().unwrap()),
+                QueryType::Blob as u8,
                 serialize(&proposal.tx_hash.unwrap()),
                 serialize(&proposal.tx_hash.unwrap()),
                 QueryType::Integer as u8,
                 QueryType::Integer as u8,
                 proposal.call_index,
                 proposal.call_index,

+ 0 - 1
src/contract/dao/src/client/vote.rs

@@ -55,7 +55,6 @@ pub struct DaoVoteInput {
     pub signature_secret: SecretKey,
     pub signature_secret: SecretKey,
 }
 }
 
 
-// TODO: should be token locking voting?
 // Inside ZKproof, check proposal is correct.
 // Inside ZKproof, check proposal is correct.
 pub struct DaoVoteCall {
 pub struct DaoVoteCall {
     pub inputs: Vec<DaoVoteInput>,
     pub inputs: Vec<DaoVoteInput>,

+ 1 - 2
src/contract/dao/src/entrypoint.rs

@@ -121,8 +121,7 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
         Err(_) => db_init(cid, DAO_CONTRACT_DB_PROPOSAL_BULLAS)?,
         Err(_) => db_init(cid, DAO_CONTRACT_DB_PROPOSAL_BULLAS)?,
     };
     };
 
 
-    // TODO: These nullifiers should exist per-proposal, we also need to snapshot
-    //       the money state do avoid double-vote
+    // TODO: These nullifiers should exist per-proposal
     let _ = match db_lookup(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS) {
     let _ = match db_lookup(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS) {
         Ok(v) => v,
         Ok(v) => v,
         Err(_) => db_init(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS)?,
         Err(_) => db_init(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS)?,

+ 5 - 5
src/contract/dao/src/entrypoint/exec.rs

@@ -29,7 +29,7 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
 
 use crate::{
 use crate::{
     error::DaoError,
     error::DaoError,
-    model::{DaoBlindAggregateVote, DaoExecParams, DaoExecUpdate},
+    model::{DaoExecParams, DaoExecUpdate, DaoProposalMetadata},
     DaoFunction, DAO_CONTRACT_DB_PROPOSAL_BULLAS, DAO_CONTRACT_ZKAS_DAO_EXEC_NS,
     DaoFunction, DAO_CONTRACT_DB_PROPOSAL_BULLAS, DAO_CONTRACT_ZKAS_DAO_EXEC_NS,
 };
 };
 
 
@@ -134,16 +134,16 @@ pub(crate) fn dao_exec_process_instruction(
         msg!("[Dao::Exec] Error: Proposal {:?} not found", params.proposal);
         msg!("[Dao::Exec] Error: Proposal {:?} not found", params.proposal);
         return Err(DaoError::ProposalNonexistent.into())
         return Err(DaoError::ProposalNonexistent.into())
     };
     };
-    let (proposal_votes, ended): (DaoBlindAggregateVote, bool) = deserialize(&data)?;
+    let proposal: DaoProposalMetadata = deserialize(&data)?;
 
 
-    if ended {
+    if proposal.ended {
         msg!("[Dao::Exec] Error: Proposal {:?} ended", params.proposal);
         msg!("[Dao::Exec] Error: Proposal {:?} ended", params.proposal);
         return Err(DaoError::ProposalEnded.into())
         return Err(DaoError::ProposalEnded.into())
     }
     }
 
 
     // 4. Check yes_vote commit and all_vote_commit are the same as in BlindAggregateVote
     // 4. Check yes_vote commit and all_vote_commit are the same as in BlindAggregateVote
-    if proposal_votes.yes_vote_commit != params.blind_total_vote.yes_vote_commit ||
-        proposal_votes.all_vote_commit != params.blind_total_vote.all_vote_commit
+    if proposal.vote_aggregate.yes_vote_commit != params.blind_total_vote.yes_vote_commit ||
+        proposal.vote_aggregate.all_vote_commit != params.blind_total_vote.all_vote_commit
     {
     {
         return Err(DaoError::VoteCommitMismatch.into())
         return Err(DaoError::VoteCommitMismatch.into())
     }
     }

+ 23 - 9
src/contract/dao/src/entrypoint/propose.rs

@@ -16,10 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use darkfi_money_contract::MONEY_CONTRACT_COIN_ROOTS_TREE;
+use darkfi_money_contract::{
+    MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT,
+};
 use darkfi_sdk::{
 use darkfi_sdk::{
-    crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, PublicKey},
-    db::{db_contains_key, db_lookup, db_set},
+    crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, MerkleNode, PublicKey},
+    db::{db_contains_key, db_get, db_lookup, db_set},
     error::{ContractError, ContractResult},
     error::{ContractError, ContractResult},
     msg,
     msg,
     pasta::pallas,
     pasta::pallas,
@@ -29,7 +31,7 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
 
 use crate::{
 use crate::{
     error::DaoError,
     error::DaoError,
-    model::{DaoBlindAggregateVote, DaoProposeParams, DaoProposeUpdate},
+    model::{DaoBlindAggregateVote, DaoProposalMetadata, DaoProposeParams, DaoProposeUpdate},
     DaoFunction, DAO_CONTRACT_DB_DAO_MERKLE_ROOTS, DAO_CONTRACT_DB_PROPOSAL_BULLAS,
     DaoFunction, DAO_CONTRACT_DB_DAO_MERKLE_ROOTS, DAO_CONTRACT_DB_PROPOSAL_BULLAS,
     DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS,
     DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS,
 };
 };
@@ -129,8 +131,17 @@ pub(crate) fn dao_propose_process_instruction(
         return Err(DaoError::ProposalAlreadyExists.into())
         return Err(DaoError::ProposalAlreadyExists.into())
     }
     }
 
 
+    // Snapshot the latest Money Mekrle tree
+    let money_info_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_INFO_TREE)?;
+    let Some(data) = db_get(money_info_db, &serialize(&MONEY_CONTRACT_LATEST_COIN_ROOT))? else {
+        msg!("[Dao::Propose] Error: Failed to fetch latest Money Merkle root");
+        return Err(ContractError::Internal);
+    };
+    let snapshot_root: MerkleNode = deserialize(&data)?;
+    msg!("[Dao::Propose] Snapshotting Money at Merkle root {}", snapshot_root);
+
     // Create state update
     // Create state update
-    let update = DaoProposeUpdate { proposal_bulla: params.proposal_bulla };
+    let update = DaoProposeUpdate { proposal_bulla: params.proposal_bulla, snapshot_root };
     let mut update_data = vec![];
     let mut update_data = vec![];
     update_data.write_u8(DaoFunction::Propose as u8)?;
     update_data.write_u8(DaoFunction::Propose as u8)?;
     update.encode(&mut update_data)?;
     update.encode(&mut update_data)?;
@@ -145,12 +156,15 @@ pub(crate) fn dao_propose_process_update(
     // Grab all db handles we want to work on
     // Grab all db handles we want to work on
     let proposal_vote_db = db_lookup(cid, DAO_CONTRACT_DB_PROPOSAL_BULLAS)?;
     let proposal_vote_db = db_lookup(cid, DAO_CONTRACT_DB_PROPOSAL_BULLAS)?;
 
 
-    // Initial vote aggregate
-    let pv = DaoBlindAggregateVote::default();
-    let ended = false;
+    // Build the proposal metadata
+    let proposal_metadata = DaoProposalMetadata {
+        vote_aggregate: DaoBlindAggregateVote::default(),
+        snapshot_root: update.snapshot_root,
+        ended: false,
+    };
 
 
     // Set the new proposal in the db
     // Set the new proposal in the db
-    db_set(proposal_vote_db, &serialize(&update.proposal_bulla), &serialize(&(pv, ended)))?;
+    db_set(proposal_vote_db, &serialize(&update.proposal_bulla), &serialize(&proposal_metadata))?;
 
 
     Ok(())
     Ok(())
 }
 }

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

@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use darkfi_money_contract::{MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_NULLIFIERS_TREE};
+use darkfi_money_contract::MONEY_CONTRACT_NULLIFIERS_TREE;
 use darkfi_sdk::{
 use darkfi_sdk::{
     crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, PublicKey},
     crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, PublicKey},
     db::{db_contains_key, db_get, db_lookup, db_set},
     db::{db_contains_key, db_get, db_lookup, db_set},
@@ -29,7 +29,7 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
 
 use crate::{
 use crate::{
     error::DaoError,
     error::DaoError,
-    model::{DaoBlindAggregateVote, DaoVoteParams, DaoVoteUpdate},
+    model::{DaoProposalMetadata, DaoVoteParams, DaoVoteUpdate},
     DaoFunction, DAO_CONTRACT_DB_PROPOSAL_BULLAS, DAO_CONTRACT_DB_VOTE_NULLIFIERS,
     DaoFunction, DAO_CONTRACT_DB_PROPOSAL_BULLAS, DAO_CONTRACT_DB_VOTE_NULLIFIERS,
     DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS,
     DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS,
 };
 };
@@ -64,6 +64,11 @@ pub(crate) fn dao_vote_get_metadata(
         let value_coords = input.vote_commit.to_affine().coordinates().unwrap();
         let value_coords = input.vote_commit.to_affine().coordinates().unwrap();
         let (sig_x, sig_y) = input.signature_public.xy();
         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((
         zk_public_inputs.push((
             DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS.to_string(),
             DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS.to_string(),
             vec![
             vec![
@@ -119,21 +124,25 @@ pub(crate) fn dao_vote_process_instruction(
 
 
     // Get the current votes, and additionally confirm proposal hasn't ended
     // Get the current votes, and additionally confirm proposal hasn't ended
     // TODO: Proposals should have a set length of time
     // TODO: Proposals should have a set length of time
-    let (mut proposal_votes, ended): (DaoBlindAggregateVote, bool) = deserialize(&data)?;
-    if ended {
+    let mut proposal_metadata: DaoProposalMetadata = deserialize(&data)?;
+
+    if proposal_metadata.ended {
         msg!("[Dao::Vote] Error: Proposal ended: {:?}", params.proposal_bulla);
         msg!("[Dao::Vote] Error: Proposal ended: {:?}", params.proposal_bulla);
         return Err(DaoError::ProposalEnded.into())
         return Err(DaoError::ProposalEnded.into())
     }
     }
 
 
-    // Check the Merkle roots and nullifiers for the input coins are valid
-    let money_roots_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+    // Check the Merkle root and nullifiers for the input coins are valid
     let money_nullifier_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIERS_TREE)?;
     let money_nullifier_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIERS_TREE)?;
     let dao_vote_nullifier_db = db_lookup(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS)?;
     let dao_vote_nullifier_db = db_lookup(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS)?;
     let mut vote_nullifiers = vec![];
     let mut vote_nullifiers = vec![];
 
 
     for input in &params.inputs {
     for input in &params.inputs {
-        if !db_contains_key(money_roots_db, &serialize(&input.merkle_root))? {
-            msg!("[Dao::Vote] Error: Invalid input Merkle root: {}", input.merkle_root);
+        if proposal_metadata.snapshot_root != input.merkle_root {
+            msg!(
+                "[Dao::Vote] Error: Invalid input Merkle root: {} (expected {})",
+                input.merkle_root,
+                proposal_metadata.snapshot_root
+            );
             return Err(DaoError::InvalidInputMerkleRoot.into())
             return Err(DaoError::InvalidInputMerkleRoot.into())
         }
         }
 
 
@@ -153,15 +162,15 @@ pub(crate) fn dao_vote_process_instruction(
             return Err(DaoError::DoubleVote.into())
             return Err(DaoError::DoubleVote.into())
         }
         }
 
 
-        proposal_votes.all_vote_commit += input.vote_commit;
+        proposal_metadata.vote_aggregate.all_vote_commit += input.vote_commit;
         vote_nullifiers.push(input.nullifier);
         vote_nullifiers.push(input.nullifier);
     }
     }
 
 
-    proposal_votes.yes_vote_commit += params.yes_vote_commit;
+    proposal_metadata.vote_aggregate.yes_vote_commit += params.yes_vote_commit;
 
 
     // Create state update
     // Create state update
     let update =
     let update =
-        DaoVoteUpdate { proposal_bulla: params.proposal_bulla, proposal_votes, vote_nullifiers };
+        DaoVoteUpdate { proposal_bulla: params.proposal_bulla, proposal_metadata, vote_nullifiers };
 
 
     let mut update_data = vec![];
     let mut update_data = vec![];
     update_data.write_u8(DaoFunction::Vote as u8)?;
     update_data.write_u8(DaoFunction::Vote as u8)?;
@@ -180,7 +189,7 @@ pub(crate) fn dao_vote_process_update(cid: ContractId, update: DaoVoteUpdate) ->
     db_set(
     db_set(
         proposal_vote_db,
         proposal_vote_db,
         &serialize(&update.proposal_bulla),
         &serialize(&update.proposal_bulla),
-        &serialize(&(update.proposal_votes, false)),
+        &serialize(&update.proposal_metadata),
     )?;
     )?;
 
 
     // We are essentially doing: vote_nulls.append(update_nulls)
     // We are essentially doing: vote_nulls.append(update_nulls)

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

@@ -103,6 +103,19 @@ pub struct DaoProposeParamsInput {
 pub struct DaoProposeUpdate {
 pub struct DaoProposeUpdate {
     /// Minted proposal bulla
     /// Minted proposal bulla
     pub proposal_bulla: pallas::Base,
     pub proposal_bulla: pallas::Base,
+    /// Snapshotted Merkle root in the Money state
+    pub snapshot_root: MerkleNode,
+}
+
+/// Metadata for a DAO proposal on the blockchain
+#[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)]
+pub struct DaoProposalMetadata {
+    /// Vote aggregate
+    pub vote_aggregate: DaoBlindAggregateVote,
+    /// Snapshotted Merkle root in the Money state
+    pub snapshot_root: MerkleNode,
+    /// Proposal closed
+    pub ended: bool,
 }
 }
 
 
 /// Parameters for `Dao::Vote`
 /// Parameters for `Dao::Vote`
@@ -138,8 +151,8 @@ pub struct DaoVoteParamsInput {
 pub struct DaoVoteUpdate {
 pub struct DaoVoteUpdate {
     /// The proposal bulla being voted on
     /// The proposal bulla being voted on
     pub proposal_bulla: pallas::Base,
     pub proposal_bulla: pallas::Base,
-    /// The proposal votes aggregate
-    pub proposal_votes: DaoBlindAggregateVote,
+    /// The updated proposal metadata
+    pub proposal_metadata: DaoProposalMetadata,
     /// Vote nullifiers,
     /// Vote nullifiers,
     pub vote_nullifiers: Vec<Nullifier>,
     pub vote_nullifiers: Vec<Nullifier>,
 }
 }

+ 8 - 3
src/contract/dao/tests/integration.rs

@@ -52,6 +52,7 @@ use harness::{init_logger, DaoTestHarness};
 // TODO: db_* errors returned from runtime should be more specific.
 // TODO: db_* errors returned from runtime should be more specific.
 // TODO: db_* functions should be consistently ordered
 // TODO: db_* functions should be consistently ordered
 // TODO: migrate rest of func calls below to make() format and cleanup
 // TODO: migrate rest of func calls below to make() format and cleanup
+// TODO: Migrate to test-harness
 
 
 #[async_std::test]
 #[async_std::test]
 async fn integration_test() -> Result<()> {
 async fn integration_test() -> Result<()> {
@@ -456,6 +457,10 @@ async fn integration_test() -> Result<()> {
 
 
     //// Wallet
     //// Wallet
 
 
+    // HACK: Here we clone the tree so we can reproduce the root for voting.
+    //       This should be done in a nicer way
+    let tree_at_proposal = cache.tree.clone();
+
     // Read received proposal
     // Read received proposal
     let (proposal, proposal_bulla) = {
     let (proposal, proposal_bulla) = {
         let note: client::DaoProposeNote = params.note.decrypt(&dao_th.dao_kp.secret).unwrap();
         let note: client::DaoProposeNote = params.note.decrypt(&dao_th.dao_kp.secret).unwrap();
@@ -504,7 +509,7 @@ async fn integration_test() -> Result<()> {
     // User 1: YES
     // User 1: YES
 
 
     let (money_leaf_position, money_merkle_path) = {
     let (money_leaf_position, money_merkle_path) = {
-        let tree = &cache.tree;
+        let tree = &tree_at_proposal;
         let leaf_position = gov_recv[0].leaf_position;
         let leaf_position = gov_recv[0].leaf_position;
         let merkle_path = tree.witness(leaf_position, 0).unwrap();
         let merkle_path = tree.witness(leaf_position, 0).unwrap();
         (leaf_position, merkle_path)
         (leaf_position, merkle_path)
@@ -575,7 +580,7 @@ async fn integration_test() -> Result<()> {
     // User 2: NO
     // User 2: NO
 
 
     let (money_leaf_position, money_merkle_path) = {
     let (money_leaf_position, money_merkle_path) = {
-        let tree = &cache.tree;
+        let tree = &tree_at_proposal;
         let leaf_position = gov_recv[1].leaf_position;
         let leaf_position = gov_recv[1].leaf_position;
         let merkle_path = tree.witness(leaf_position, 0).unwrap();
         let merkle_path = tree.witness(leaf_position, 0).unwrap();
         (leaf_position, merkle_path)
         (leaf_position, merkle_path)
@@ -643,7 +648,7 @@ async fn integration_test() -> Result<()> {
     // User 3: YES
     // User 3: YES
 
 
     let (money_leaf_position, money_merkle_path) = {
     let (money_leaf_position, money_merkle_path) = {
-        let tree = &cache.tree;
+        let tree = &tree_at_proposal;
         let leaf_position = gov_recv[2].leaf_position;
         let leaf_position = gov_recv[2].leaf_position;
         let merkle_path = tree.witness(leaf_position, 0).unwrap();
         let merkle_path = tree.witness(leaf_position, 0).unwrap();
         (leaf_position, merkle_path)
         (leaf_position, merkle_path)

+ 1 - 0
src/contract/dao/wallet.sql

@@ -142,6 +142,7 @@ CREATE TABLE IF NOT EXISTS dao_proposals (
     -- these values are NULL until the proposal is minted on chain
     -- these values are NULL until the proposal is minted on chain
     -- and received by the DAO
     -- and received by the DAO
 	leaf_position BLOB,
 	leaf_position BLOB,
+	money_snapshot_tree BLOB,
     tx_hash BLOB,
     tx_hash BLOB,
     call_index INTEGER,
     call_index INTEGER,
     -- this is NULL until we have voted on this proposal
     -- this is NULL until we have voted on this proposal

+ 1 - 1
src/runtime/vm_runtime.rs

@@ -41,7 +41,7 @@ use crate::{blockchain::BlockchainOverlayPtr, util::time::TimeKeeper, Error, Res
 const MEMORY: &str = "memory";
 const MEMORY: &str = "memory";
 
 
 /// Gas limit for a contract
 /// Gas limit for a contract
-const GAS_LIMIT: u64 = 400000000;
+const GAS_LIMIT: u64 = 400_000_000;
 
 
 /// The hardcoded db name for the zkas circuits database tree
 /// The hardcoded db name for the zkas circuits database tree
 pub const SMART_CONTRACT_ZKAS_DB_NAME: &str = "_zkas";
 pub const SMART_CONTRACT_ZKAS_DB_NAME: &str = "_zkas";