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

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 лет назад
Родитель
Сommit
33c286f19f

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

@@ -257,7 +257,7 @@ impl Drk {
             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> =
             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_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_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},
     DaoFunction,
@@ -200,6 +201,8 @@ pub struct DaoProposal {
     pub bulla_blind: pallas::Base,
     /// Leaf position of this proposal in the Merkle tree of proposals
     pub leaf_position: Option<bridgetree::Position>,
+    /// Snapshotted Money Merkle tree
+    pub money_snapshot_tree: Option<MerkleTree>,
     /// Transaction hash where this proposal was proposed
     pub tx_hash: Option<blake3::Hash>,
     /// call index in the transaction where this proposal was proposed
@@ -714,6 +717,8 @@ impl Drk {
             QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_LEAF_POSITION,
             QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
+            QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_TX_HASH,
             QueryType::OptionInteger as u8,
             DAO_PROPOSALS_COL_CALL_INDEX,
@@ -754,11 +759,14 @@ impl Drk {
             let bulla_blind = deserialize(&bulla_blind_bytes)?;
 
             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() {
                 None
@@ -766,6 +774,12 @@ impl Drk {
                 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 =
                 if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) };
 
@@ -780,6 +794,7 @@ impl Drk {
                 token_id,
                 bulla_blind,
                 leaf_position,
+                money_snapshot_tree,
                 tx_hash,
                 call_index,
                 vote_id,
@@ -818,6 +833,8 @@ impl Drk {
             QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_LEAF_POSITION,
             QueryType::OptionBlob as u8,
+            DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
+            QueryType::OptionBlob as u8,
             DAO_PROPOSALS_COL_TX_HASH,
             QueryType::OptionInteger as u8,
             DAO_PROPOSALS_COL_CALL_INDEX,
@@ -848,11 +865,14 @@ impl Drk {
         let bulla_blind = deserialize(&bulla_blind_bytes)?;
 
         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() {
             None
@@ -863,6 +883,12 @@ impl Drk {
         let tx_hash =
             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 =
             if vote_id_bytes.is_empty() { None } else { Some(deserialize(&vote_id_bytes)?) };
 
@@ -876,6 +902,7 @@ impl Drk {
             token_id,
             bulla_blind,
             leaf_position,
+            money_snapshot_tree,
             tx_hash,
             call_index,
             vote_id,
@@ -974,7 +1001,12 @@ impl Drk {
         // DAOs that have been minted
         let mut new_dao_bullas: Vec<(DaoBulla, Option<blake3::Hash>, u32)> = vec![];
         // 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![];
         // DAO votes that have been seen
         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);
                 let params: DaoProposeParams = deserialize(&call.data[1..])?;
                 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
             }
 
@@ -1057,8 +1091,9 @@ impl Drk {
                             token_id: note.proposal.token_id,
                             bulla_blind: note.proposal.blind,
                             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,
                         };
 
@@ -1186,7 +1221,7 @@ impl Drk {
             };
 
             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_COL_DAO_ID,
                 DAO_PROPOSALS_COL_RECV_PUBLIC,
@@ -1194,6 +1229,7 @@ impl Drk {
                 DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID,
                 DAO_PROPOSALS_COL_BULLA_BLIND,
                 DAO_PROPOSALS_COL_LEAF_POSITION,
+                DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE,
                 DAO_PROPOSALS_COL_TX_HASH,
                 DAO_PROPOSALS_COL_CALL_INDEX,
             );
@@ -1213,6 +1249,8 @@ impl Drk {
                 QueryType::Blob as u8,
                 serialize(&proposal.leaf_position.unwrap()),
                 QueryType::Blob as u8,
+                serialize(&proposal.money_snapshot_tree.clone().unwrap()),
+                QueryType::Blob as u8,
                 serialize(&proposal.tx_hash.unwrap()),
                 QueryType::Integer as u8,
                 proposal.call_index,

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

@@ -55,7 +55,6 @@ pub struct DaoVoteInput {
     pub signature_secret: SecretKey,
 }
 
-// TODO: should be token locking voting?
 // Inside ZKproof, check proposal is correct.
 pub struct DaoVoteCall {
     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)?,
     };
 
-    // 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) {
         Ok(v) => v,
         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::{
     error::DaoError,
-    model::{DaoBlindAggregateVote, DaoExecParams, DaoExecUpdate},
+    model::{DaoExecParams, DaoExecUpdate, DaoProposalMetadata},
     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);
         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);
         return Err(DaoError::ProposalEnded.into())
     }
 
     // 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())
     }

+ 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/>.
  */
 
-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::{
-    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},
     msg,
     pasta::pallas,
@@ -29,7 +31,7 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
 use crate::{
     error::DaoError,
-    model::{DaoBlindAggregateVote, DaoProposeParams, DaoProposeUpdate},
+    model::{DaoBlindAggregateVote, DaoProposalMetadata, DaoProposeParams, DaoProposeUpdate},
     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,
 };
@@ -129,8 +131,17 @@ pub(crate) fn dao_propose_process_instruction(
         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
-    let update = DaoProposeUpdate { proposal_bulla: params.proposal_bulla };
+    let update = DaoProposeUpdate { proposal_bulla: params.proposal_bulla, snapshot_root };
     let mut update_data = vec![];
     update_data.write_u8(DaoFunction::Propose as u8)?;
     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
     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
-    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(())
 }

+ 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/>.
  */
 
-use darkfi_money_contract::{MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_NULLIFIERS_TREE};
+use darkfi_money_contract::MONEY_CONTRACT_NULLIFIERS_TREE;
 use darkfi_sdk::{
     crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, PublicKey},
     db::{db_contains_key, db_get, db_lookup, db_set},
@@ -29,7 +29,7 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
 use crate::{
     error::DaoError,
-    model::{DaoBlindAggregateVote, DaoVoteParams, DaoVoteUpdate},
+    model::{DaoProposalMetadata, DaoVoteParams, DaoVoteUpdate},
     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,
 };
@@ -64,6 +64,11 @@ 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![
@@ -119,21 +124,25 @@ pub(crate) fn dao_vote_process_instruction(
 
     // Get the current votes, and additionally confirm proposal hasn't ended
     // 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);
         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 dao_vote_nullifier_db = db_lookup(cid, DAO_CONTRACT_DB_VOTE_NULLIFIERS)?;
     let mut vote_nullifiers = vec![];
 
     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())
         }
 
@@ -153,15 +162,15 @@ pub(crate) fn dao_vote_process_instruction(
             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);
     }
 
-    proposal_votes.yes_vote_commit += params.yes_vote_commit;
+    proposal_metadata.vote_aggregate.yes_vote_commit += params.yes_vote_commit;
 
     // Create state 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![];
     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(
         proposal_vote_db,
         &serialize(&update.proposal_bulla),
-        &serialize(&(update.proposal_votes, false)),
+        &serialize(&update.proposal_metadata),
     )?;
 
     // 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 {
     /// Minted proposal bulla
     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`
@@ -138,8 +151,8 @@ pub struct DaoVoteParamsInput {
 pub struct DaoVoteUpdate {
     /// The proposal bulla being voted on
     pub proposal_bulla: pallas::Base,
-    /// The proposal votes aggregate
-    pub proposal_votes: DaoBlindAggregateVote,
+    /// The updated proposal metadata
+    pub proposal_metadata: DaoProposalMetadata,
     /// Vote nullifiers,
     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_* functions should be consistently ordered
 // TODO: migrate rest of func calls below to make() format and cleanup
+// TODO: Migrate to test-harness
 
 #[async_std::test]
 async fn integration_test() -> Result<()> {
@@ -456,6 +457,10 @@ async fn integration_test() -> Result<()> {
 
     //// 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
     let (proposal, proposal_bulla) = {
         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
 
     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 merkle_path = tree.witness(leaf_position, 0).unwrap();
         (leaf_position, merkle_path)
@@ -575,7 +580,7 @@ async fn integration_test() -> Result<()> {
     // User 2: NO
 
     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 merkle_path = tree.witness(leaf_position, 0).unwrap();
         (leaf_position, merkle_path)
@@ -643,7 +648,7 @@ async fn integration_test() -> Result<()> {
     // User 3: YES
 
     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 merkle_path = tree.witness(leaf_position, 0).unwrap();
         (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
     -- and received by the DAO
 	leaf_position BLOB,
+	money_snapshot_tree BLOB,
     tx_hash BLOB,
     call_index INTEGER,
     -- 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";
 
 /// 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
 pub const SMART_CONTRACT_ZKAS_DB_NAME: &str = "_zkas";