Przeglądaj źródła

contract/dao: Rewrite integration test using the test harness.

parazyd 3 lat temu
rodzic
commit
c359b02dd7

+ 1 - 0
Cargo.lock

@@ -1641,6 +1641,7 @@ dependencies = [
  "bs58",
  "chacha20poly1305",
  "darkfi",
+ "darkfi-contract-test-harness",
  "darkfi-money-contract",
  "darkfi-sdk",
  "darkfi-serial",

+ 2 - 2
src/contract/dao/Cargo.toml

@@ -27,10 +27,10 @@ rand = { version = "0.8.5", optional = true }
 [dev-dependencies]
 async-std = {version = "1.12.0", features = ["attributes"]}
 darkfi = {path = "../../../", features = ["tx", "blockchain"]}
-darkfi-money-contract = { path = "../money", features = ["client", "no-entrypoint"] }
+darkfi-money-contract = {path = "../money", features = ["client", "no-entrypoint"]}
 simplelog = "0.12.1"
 sled = "0.34.7"
-#sqlx = {version = "0.6.3", features = ["runtime-async-std-rustls", "sqlite"]}
+darkfi-contract-test-harness = {path = "../test-harness"}
 
 # We need to disable random using "custom" which makes the crate a noop
 # so the wasm32-unknown-unknown target is enabled.

+ 4 - 4
src/contract/dao/src/client/exec.rs

@@ -32,7 +32,7 @@ use darkfi::{
 };
 
 use super::{DaoInfo, DaoProposalInfo};
-use crate::model::{DaoBlindAggregateVote, DaoExecParams};
+use crate::model::{DaoBlindAggregateVote, DaoExecParams, DaoProposalBulla};
 
 pub struct DaoExecCall {
     pub proposal: DaoProposalInfo,
@@ -85,14 +85,14 @@ impl DaoExecCall {
             self.dao.bulla_blind,
         ]);
 
-        let proposal_bulla = poseidon_hash::<6>([
+        let proposal_bulla = DaoProposalBulla::from(poseidon_hash::<6>([
             proposal_dest_x,
             proposal_dest_y,
             proposal_amount,
             self.proposal.token_id.inner(),
             dao_bulla,
             self.proposal.blind,
-        ]);
+        ]));
 
         let coin_0 = poseidon_hash::<7>([
             proposal_dest_x,
@@ -157,7 +157,7 @@ impl DaoExecCall {
 
         debug!(target: "dao", "proposal_bulla: {:?}", proposal_bulla);
         let public_inputs = vec![
-            proposal_bulla,
+            proposal_bulla.inner(),
             coin_0,
             coin_1,
             *yes_vote_commit_coords.x(),

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

@@ -34,7 +34,7 @@ use darkfi::{
     Result,
 };
 
-use crate::model::{DaoProposeParams, DaoProposeParamsInput};
+use crate::model::{DaoProposalBulla, DaoProposeParams, DaoProposeParamsInput};
 
 use super::DaoInfo;
 
@@ -195,14 +195,14 @@ impl DaoProposeCall {
 
         let dao_leaf_position: u64 = self.dao_leaf_position.into();
 
-        let proposal_bulla = poseidon_hash::<6>([
+        let proposal_bulla = DaoProposalBulla::from(poseidon_hash::<6>([
             proposal_dest_x,
             proposal_dest_y,
             proposal_amount,
             self.proposal.token_id.inner(),
             dao_bulla,
             self.proposal.blind,
-        ]);
+        ]));
 
         let prover_witnesses = vec![
             // Proposers total number of gov tokens
@@ -231,7 +231,7 @@ impl DaoProposeCall {
         let public_inputs = vec![
             token_commit,
             self.dao_merkle_root.inner(),
-            proposal_bulla,
+            proposal_bulla.inner(),
             *total_funds_coords.x(),
             *total_funds_coords.y(),
         ];

+ 4 - 4
src/contract/dao/src/client/vote.rs

@@ -36,7 +36,7 @@ use darkfi::{
 };
 
 use super::{DaoInfo, DaoProposalInfo};
-use crate::model::{DaoVoteParams, DaoVoteParamsInput};
+use crate::model::{DaoProposalBulla, DaoVoteParams, DaoVoteParamsInput};
 
 #[derive(SerialEncodable, SerialDecodable)]
 pub struct DaoVoteNote {
@@ -196,14 +196,14 @@ impl DaoVoteCall {
             self.dao.bulla_blind,
         ]);
 
-        let proposal_bulla = poseidon_hash::<6>([
+        let proposal_bulla = DaoProposalBulla::from(poseidon_hash::<6>([
             proposal_dest_x,
             proposal_dest_y,
             proposal_amount,
             self.proposal.token_id.inner(),
             dao_bulla,
             self.proposal.blind,
-        ]);
+        ]));
 
         let vote_option = self.vote_option as u64;
         assert!(vote_option == 0 || vote_option == 1);
@@ -243,7 +243,7 @@ impl DaoVoteCall {
 
         let public_inputs = vec![
             token_commit,
-            proposal_bulla,
+            proposal_bulla.inner(),
             // this should be a value commit??
             *yes_vote_commit_coords.x(),
             *yes_vote_commit_coords.y(),

+ 6 - 3
src/contract/dao/src/entrypoint/exec.rs

@@ -55,7 +55,7 @@ pub(crate) fn dao_exec_get_metadata(
     zk_public_inputs.push((
         DAO_CONTRACT_ZKAS_DAO_EXEC_NS.to_string(),
         vec![
-            params.proposal,
+            params.proposal.inner(),
             params.coin_0.inner(),
             params.coin_1.inner(),
             *yes_vote_coords.x(),
@@ -110,8 +110,11 @@ pub(crate) fn dao_exec_process_instruction(
     // Checks
     // ======
     // 1. Check coins in MoneyTransfer are the same as our coin 0 and coin 1
-    if mt_params.outputs[0].coin != params.coin_0 ||
-        mt_params.outputs[1].coin != params.coin_1 ||
+    // * outputs[0] is the change returned to DAO
+    // * outputs[1] is the value being sent to the recipient
+    // (This is how it's done in the client API of Money::Transfer)
+    if mt_params.outputs[0].coin != params.coin_1 ||
+        mt_params.outputs[1].coin != params.coin_0 ||
         mt_params.outputs.len() != 2
     {
         msg!("[Dao::Exec] Error: Coin commitments mismatch");

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

@@ -85,7 +85,7 @@ pub(crate) fn dao_propose_get_metadata(
         vec![
             params.token_commit,
             params.dao_merkle_root.inner(),
-            params.proposal_bulla,
+            params.proposal_bulla.inner(),
             *total_funds_coords.x(),
             *total_funds_coords.y(),
         ],

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

@@ -90,7 +90,7 @@ pub(crate) fn dao_vote_get_metadata(
         DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS.to_string(),
         vec![
             params.token_commit,
-            params.proposal_bulla,
+            params.proposal_bulla.inner(),
             *yes_vote_commit_coords.x(),
             *yes_vote_commit_coords.y(),
             *all_vote_commit_coords.x(),

+ 0 - 6
src/contract/dao/src/lib.rs

@@ -57,12 +57,6 @@ pub mod entrypoint;
 /// Client API for interaction with this smart contract
 pub mod client;
 
-// TODO: Delete these and use the proper API
-#[cfg(feature = "client")]
-pub mod money_client;
-#[cfg(feature = "client")]
-pub mod wallet_cache;
-
 // These are the different sled trees that will be created
 pub const DAO_CONTRACT_DB_INFO_TREE: &str = "dao_info";
 pub const DAO_CONTRACT_DB_DAO_BULLAS: &str = "dao_bullas";

+ 51 - 7
src/contract/dao/src/model.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use core::str::FromStr;
+
 use darkfi_money_contract::model::Coin;
 use darkfi_sdk::{
     crypto::{note::AeadEncryptedNote, pasta_prelude::*, MerkleNode, Nullifier, PublicKey},
@@ -51,11 +53,53 @@ impl DaoBulla {
     }
 }
 
-use core::str::FromStr;
+impl std::hash::Hash for DaoBulla {
+    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
+        state.write(&self.to_bytes());
+    }
+}
+
 darkfi_sdk::fp_from_bs58!(DaoBulla);
 darkfi_sdk::fp_to_bs58!(DaoBulla);
 darkfi_sdk::ty_from_fp!(DaoBulla);
 
+/// A `DaoProposalBulla` represented in the state
+#[derive(Debug, Copy, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)]
+pub struct DaoProposalBulla(pallas::Base);
+
+impl DaoProposalBulla {
+    /// Reference the raw inner base field element
+    pub fn inner(&self) -> pallas::Base {
+        self.0
+    }
+
+    /// Create a `DaoBulla` object from given bytes, erroring if the
+    /// input bytes are noncanonical.
+    pub fn from_bytes(x: [u8; 32]) -> Result<Self, ContractError> {
+        match pallas::Base::from_repr(x).into() {
+            Some(v) => Ok(Self(v)),
+            None => Err(ContractError::IoError(
+                "Failed to instantiate DaoProposalBulla from bytes".to_string(),
+            )),
+        }
+    }
+
+    /// Convert the `DaoBulla` type into 32 raw bytes
+    pub fn to_bytes(&self) -> [u8; 32] {
+        self.0.to_repr()
+    }
+}
+
+impl std::hash::Hash for DaoProposalBulla {
+    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
+        state.write(&self.to_bytes());
+    }
+}
+
+darkfi_sdk::fp_from_bs58!(DaoProposalBulla);
+darkfi_sdk::fp_to_bs58!(DaoProposalBulla);
+darkfi_sdk::ty_from_fp!(DaoProposalBulla);
+
 /// Parameters for `Dao::Mint`
 #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)]
 pub struct DaoMintParams {
@@ -80,7 +124,7 @@ pub struct DaoProposeParams {
     /// Token ID commitment for the proposal
     pub token_commit: pallas::Base,
     /// Bulla of the DAO proposal
-    pub proposal_bulla: pallas::Base,
+    pub proposal_bulla: DaoProposalBulla,
     /// Encrypted note
     pub note: AeadEncryptedNote,
     /// Inputs for the proposal
@@ -102,7 +146,7 @@ pub struct DaoProposeParamsInput {
 #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)]
 pub struct DaoProposeUpdate {
     /// Minted proposal bulla
-    pub proposal_bulla: pallas::Base,
+    pub proposal_bulla: DaoProposalBulla,
     /// Snapshotted Merkle root in the Money state
     pub snapshot_root: MerkleNode,
 }
@@ -124,7 +168,7 @@ pub struct DaoVoteParams {
     /// Token commitment for the vote inputs
     pub token_commit: pallas::Base,
     /// Proposal bulla being voted on
-    pub proposal_bulla: pallas::Base,
+    pub proposal_bulla: DaoProposalBulla,
     /// Commitment for yes votes
     pub yes_vote_commit: pallas::Point,
     /// Encrypted note
@@ -150,7 +194,7 @@ pub struct DaoVoteParamsInput {
 #[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
 pub struct DaoVoteUpdate {
     /// The proposal bulla being voted on
-    pub proposal_bulla: pallas::Base,
+    pub proposal_bulla: DaoProposalBulla,
     /// The updated proposal metadata
     pub proposal_metadata: DaoProposalMetadata,
     /// Vote nullifiers,
@@ -188,7 +232,7 @@ impl Default for DaoBlindAggregateVote {
 #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)]
 pub struct DaoExecParams {
     /// The proposal bulla
-    pub proposal: pallas::Base,
+    pub proposal: DaoProposalBulla,
     /// The output coin for the proposal recipient
     pub coin_0: Coin,
     /// The output coin for the change returned to DAO
@@ -203,5 +247,5 @@ pub struct DaoExecParams {
 #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)]
 pub struct DaoExecUpdate {
     /// The proposal bulla
-    pub proposal: pallas::Base,
+    pub proposal: DaoProposalBulla,
 }

+ 0 - 227
src/contract/dao/src/money_client.rs

@@ -1,227 +0,0 @@
-/* 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/>.
- */
-
-//! TODO: This file should be deleted and the API from money::client
-//!       should be used directly.
-
-use darkfi::{
-    zk::{Proof, ProvingKey},
-    zkas::ZkBinary,
-    Result,
-};
-use darkfi_sdk::{
-    bridgetree,
-    crypto::{
-        note::AeadEncryptedNote, pasta_prelude::*, MerkleNode, PublicKey, SecretKey, TokenId,
-        ValueBlind,
-    },
-    pasta::pallas,
-};
-
-use rand::rngs::OsRng;
-
-use darkfi_money_contract::{
-    client::{
-        transfer_v1::{
-            create_transfer_burn_proof, create_transfer_mint_proof, TransactionBuilderInputInfo,
-            TransactionBuilderOutputInfo,
-        },
-        MoneyNote,
-    },
-    model::{ClearInput, Input, MoneyTransferParamsV1, Output},
-};
-
-pub struct TransferCall {
-    pub clear_inputs: Vec<TransferClearInput>,
-    pub inputs: Vec<TransferInput>,
-    pub outputs: Vec<TransferOutput>,
-}
-
-pub struct TransferClearInput {
-    pub value: u64,
-    pub token_id: TokenId,
-    pub signature_secret: SecretKey,
-}
-
-pub struct TransferInput {
-    pub leaf_position: bridgetree::Position,
-    pub merkle_path: Vec<MerkleNode>,
-    pub secret: SecretKey,
-    pub note: MoneyNote,
-    pub user_data_blind: pallas::Base,
-    pub value_blind: ValueBlind,
-    pub signature_secret: SecretKey,
-}
-
-pub struct TransferOutput {
-    pub value: u64,
-    pub token_id: TokenId,
-    pub public: PublicKey,
-    pub serial: pallas::Base,
-    pub spend_hook: pallas::Base,
-    pub user_data: pallas::Base,
-}
-
-impl TransferCall {
-    fn compute_remainder_blind(
-        clear_inputs: &[ClearInput],
-        input_blinds: &[ValueBlind],
-        output_blinds: &[ValueBlind],
-    ) -> ValueBlind {
-        let mut total = ValueBlind::zero();
-
-        for input in clear_inputs {
-            total += input.value_blind;
-        }
-
-        for input_blind in input_blinds {
-            total += input_blind;
-        }
-
-        for output_blind in output_blinds {
-            total -= output_blind;
-        }
-
-        total
-    }
-
-    pub fn make(
-        self,
-        mint_zkbin: &ZkBinary,
-        mint_pk: &ProvingKey,
-        burn_zkbin: &ZkBinary,
-        burn_pk: &ProvingKey,
-    ) -> Result<(MoneyTransferParamsV1, Vec<Proof>)> {
-        assert!(self.clear_inputs.len() + self.inputs.len() > 0);
-
-        let mut clear_inputs = vec![];
-        let token_blind = ValueBlind::random(&mut OsRng);
-        for input in &self.clear_inputs {
-            let signature_public = PublicKey::from_secret(input.signature_secret);
-            let value_blind = ValueBlind::random(&mut OsRng);
-
-            let clear_input = ClearInput {
-                value: input.value,
-                token_id: input.token_id,
-                value_blind,
-                token_blind,
-                signature_public,
-            };
-            clear_inputs.push(clear_input);
-        }
-
-        let mut proofs = vec![];
-        let mut inputs = vec![];
-        let mut input_blinds = vec![];
-
-        for input in self.inputs {
-            let value_blind = input.value_blind;
-            input_blinds.push(value_blind);
-
-            // FIXME: Just an API hack
-            let _input = TransactionBuilderInputInfo {
-                leaf_position: input.leaf_position,
-                merkle_path: input.merkle_path,
-                secret: input.secret,
-                note: input.note,
-            };
-
-            let (proof, revealed) = create_transfer_burn_proof(
-                burn_zkbin,
-                burn_pk,
-                &_input,
-                value_blind,
-                token_blind,
-                input.user_data_blind,
-                input.signature_secret,
-            )?;
-
-            proofs.push(proof);
-
-            let input = Input {
-                value_commit: revealed.value_commit,
-                token_commit: revealed.token_commit,
-                nullifier: revealed.nullifier,
-                merkle_root: revealed.merkle_root,
-                spend_hook: revealed.spend_hook,
-                user_data_enc: revealed.user_data_enc,
-                signature_public: revealed.signature_public,
-            };
-            inputs.push(input);
-        }
-
-        let mut outputs = vec![];
-        let mut output_blinds = vec![];
-        // This value_blind calc assumes there will always be at least a single output
-        assert!(!self.outputs.is_empty());
-
-        for (i, output) in self.outputs.iter().enumerate() {
-            let value_blind = if i == self.outputs.len() - 1 {
-                Self::compute_remainder_blind(&clear_inputs, &input_blinds, &output_blinds)
-            } else {
-                ValueBlind::random(&mut OsRng)
-            };
-            output_blinds.push(value_blind);
-
-            let serial = output.serial;
-
-            // FIXME: This is a hack between the two APIs
-            let _output = TransactionBuilderOutputInfo {
-                value: output.value,
-                token_id: output.token_id,
-                public_key: output.public,
-            };
-
-            let (proof, revealed) = create_transfer_mint_proof(
-                mint_zkbin,
-                mint_pk,
-                &_output,
-                value_blind,
-                token_blind,
-                serial,
-                output.spend_hook,
-                output.user_data,
-            )?;
-
-            proofs.push(proof);
-
-            let note = MoneyNote {
-                serial,
-                value: output.value,
-                token_id: output.token_id,
-                spend_hook: output.spend_hook,
-                user_data: output.user_data,
-                value_blind,
-                token_blind,
-                memo: Vec::new(),
-            };
-
-            let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public, &mut OsRng)?;
-
-            let output = Output {
-                value_commit: revealed.value_commit,
-                token_commit: revealed.token_commit,
-                coin: revealed.coin,
-                note: encrypted_note,
-            };
-            outputs.push(output);
-        }
-
-        Ok((MoneyTransferParamsV1 { clear_inputs, inputs, outputs }, proofs))
-    }
-}

+ 0 - 85
src/contract/dao/src/wallet_cache.rs

@@ -1,85 +0,0 @@
-/* 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/>.
- */
-
-use darkfi_sdk::{
-    bridgetree,
-    crypto::{note::AeadEncryptedNote, pasta_prelude::Field, MerkleNode, MerkleTree, SecretKey},
-    pasta::pallas,
-};
-
-use darkfi_money_contract::{client::MoneyNote, model::Coin};
-
-pub struct OwnCoin {
-    pub coin: Coin,
-    pub note: MoneyNote,
-    pub leaf_position: bridgetree::Position,
-}
-
-pub struct WalletCache {
-    // Normally this would be a HashMap, but SecretKey is not Hash-able
-    // TODO: This can be HashableBase
-    cache: Vec<(SecretKey, Vec<OwnCoin>)>,
-    /// The entire Money Merkle tree state
-    pub tree: MerkleTree,
-}
-
-impl Default for WalletCache {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl WalletCache {
-    pub fn new() -> Self {
-        let mut tree = MerkleTree::new(100);
-        tree.append(MerkleNode::from(pallas::Base::ZERO));
-        let _ = tree.mark().unwrap();
-        Self { cache: Vec::new(), tree }
-    }
-
-    /// Must be called at the start to begin tracking received coins for this secret.
-    pub fn track(&mut self, secret: SecretKey) {
-        self.cache.push((secret, Vec::new()));
-    }
-
-    /// Get all coins received by this secret key
-    /// track() must be called on this secret before calling this or the function will panic.
-    pub fn get_received(&mut self, secret: &SecretKey) -> Vec<OwnCoin> {
-        for (other_secret, own_coins) in self.cache.iter_mut() {
-            if *secret == *other_secret {
-                // clear own_coins vec, and return current contents
-                return std::mem::take(own_coins)
-            }
-        }
-        panic!("you forget to track() this secret!");
-    }
-
-    pub fn try_decrypt_note(&mut self, coin: Coin, ciphertext: &AeadEncryptedNote) {
-        // Add the new coins to the Merkle tree
-        self.tree.append(MerkleNode::from(coin.inner()));
-
-        // Loop through all our secret keys...
-        for (secret, own_coins) in self.cache.iter_mut() {
-            // .. attempt to decrypt the note ...
-            if let Ok(note) = ciphertext.decrypt(secret) {
-                let leaf_position = self.tree.mark().expect("coin should be in tree");
-                own_coins.push(OwnCoin { coin, note, leaf_position });
-            }
-        }
-    }
-}

+ 343 - 731
src/contract/dao/tests/integration.rs

@@ -16,702 +16,385 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::{Duration, Instant};
-
-use darkfi::{tx::Transaction, Result};
+use darkfi::Result;
+use darkfi_contract_test_harness::{init_logger, Holder, TestHarness};
+use darkfi_dao_contract::{
+    client::{DaoInfo, DaoVoteNote},
+    model::DaoBlindAggregateVote,
+};
 use darkfi_sdk::{
-    crypto::{
-        pasta_prelude::*, pedersen_commitment_u64, poseidon_hash, Keypair, MerkleNode, MerkleTree,
-        SecretKey, TokenId, DAO_CONTRACT_ID, DARK_TOKEN_ID, MONEY_CONTRACT_ID,
-    },
+    crypto::{pasta_prelude::Field, pedersen_commitment_u64, DAO_CONTRACT_ID, DARK_TOKEN_ID},
     pasta::pallas,
-    ContractCall,
 };
-use darkfi_serial::{Decodable, Encodable};
-use log::debug;
+use log::info;
 use rand::rngs::OsRng;
 
-use darkfi_dao_contract::{client, model, money_client, wallet_cache::WalletCache, DaoFunction};
-
-use darkfi_money_contract::{
-    client::token_mint_v1::TokenMintCallBuilder,
-    model::{Coin, MoneyTokenMintParamsV1, MoneyTransferParamsV1},
-    MoneyFunction,
-};
-
-mod harness;
-use harness::{init_logger, DaoTestHarness};
-
-// TODO: Anonymity leaks in this proof of concept:
-//
-// * Vote updates are linked to the proposal_bulla
-// * Nullifier of vote will link vote with the coin when it's spent
-
-// TODO: strategize and cleanup Result/Error usage
-// TODO: fix up code doc
-// 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<()> {
-    init_logger()?;
-
-    // Some benchmark averages
-    let mut mint_verify_times = vec![];
-    let mut propose_verify_times = vec![];
-    let mut vote_verify_times = vec![];
-    let mut exec_verify_times = vec![];
+    init_logger();
+
+    // Holders this test will use:
+    // * Faucet airdrops DRK
+    // * Alice, Bob, and Charlie are members of the DAO.
+    // * Rachel is the proposal recipient.
+    // * Dao is the DAO wallet
+    const HOLDERS: [Holder; 6] =
+        [Holder::Faucet, Holder::Alice, Holder::Bob, Holder::Charlie, Holder::Rachel, Holder::Dao];
+
+    // Initialize harness
+    let mut th = TestHarness::new(&["money".to_string(), "dao".to_string()]).await?;
+
+    // We'll use the ALICE token as the DAO governance token
+    let gov_token_id = th.token_id(&Holder::Alice);
+    const ALICE_GOV_SUPPLY: u64 = 100_000_000;
+    const BOB_GOV_SUPPLY: u64 = 100_000_000;
+    const CHARLIE_GOV_SUPPLY: u64 = 100_000_000;
+    // And the DRK token as the treasury token
+    let drk_token_id = *DARK_TOKEN_ID;
+    const DRK_TOKEN_SUPPLY: u64 = 1_000_000_000;
+    // The tokens we want to send via the proposal
+    const PROPOSAL_AMOUNT: u64 = 250_000_000;
 
     // Slot to verify against
     let current_slot = 0;
 
-    let dao_th = DaoTestHarness::new().await?;
-
-    // Money parameters
-    let xdrk_supply = 1_000_000;
-    let xdrk_token_id = *DARK_TOKEN_ID;
-
-    // Governance token parameters
-    let gdrk_mint_auth = Keypair::random(&mut OsRng);
-    let gdrk_supply = 1_000_000;
-    let gdrk_token_id = TokenId::derive(gdrk_mint_auth.secret);
-
     // DAO parameters
-    let dao = client::DaoInfo {
-        proposer_limit: 110,
-        quorum: 110,
+    let dao_keypair = th.holders.get(&Holder::Dao).unwrap().keypair;
+    let dao = DaoInfo {
+        proposer_limit: 100_000_000,
+        quorum: 199_999_999,
         approval_ratio_base: 2,
         approval_ratio_quot: 1,
-        gov_token_id: gdrk_token_id,
-        public_key: dao_th.dao_kp.public,
+        gov_token_id,
+        public_key: dao_keypair.public,
         bulla_blind: pallas::Base::random(&mut OsRng),
     };
 
-    // We use this to receive coins
-    let mut cache = WalletCache::new();
-
-    // =======================================================
+    // ====================
     // Dao::Mint
-    //
     // Create the DAO bulla
-    // =======================================================
-    debug!(target: "dao", "Stage 1. Creating DAO bulla");
-
-    let (params, proofs) = client::make_mint_call(
-        &dao,
-        &dao_th.dao_kp.secret,
-        &dao_th.dao_mint_zkbin,
-        &dao_th.dao_mint_pk,
-    )?;
+    // ====================
+    info!("Stage 1. Creating DAO bulla");
 
-    let mut data = vec![DaoFunction::Mint as u8];
-    params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id: dao_th.dao_contract_id, data }];
-    let proofs = vec![proofs];
-    let mut tx = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx.create_sigs(&mut OsRng, &[dao_th.dao_kp.secret])?;
-    tx.signatures = vec![sigs];
-
-    let timer = Instant::now();
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
-    mint_verify_times.push(timer.elapsed());
-    // TODO: Witness and add to wallet merkle tree?
-
-    let mut dao_tree = MerkleTree::new(100);
-    let dao_leaf_position = {
-        let node = MerkleNode::from(params.dao_bulla.inner());
-        dao_tree.append(node);
-        dao_tree.mark().unwrap()
-    };
-    let dao_bulla = params.dao_bulla;
-    debug!(target: "dao", "Created DAO bulla: {:?}", dao_bulla.inner());
-
-    // =======================================================
-    // Money::Transfer
-    //
-    // Mint the initial supply of treasury token
-    // and send it all to the DAO directly
-    // =======================================================
-    debug!(target: "dao", "Stage 2. Minting treasury token");
-
-    cache.track(dao_th.dao_kp.secret);
-
-    // Address of deployed contract in our example is dao::exec::FUNC_ID
-    // This field is public, you can see it's being sent to a DAO
-    // but nothing else is visible.
-    //
-    // In the python code we wrote:
-    //
-    //   spend_hook = b"0xdao_ruleset"
-    //
-    // TODO: this should be the contract/func ID
-    let spend_hook = DAO_CONTRACT_ID.inner();
-    // The user_data can be a simple hash of the items passed into the ZK proof
-    // up to corresponding linked ZK proof to interpret however they need.
-    // In out case, it's the bulla for the DAO
-    let user_data = dao_bulla.inner();
-
-    let call = money_client::TransferCall {
-        clear_inputs: vec![money_client::TransferClearInput {
-            value: xdrk_supply,
-            token_id: xdrk_token_id,
-            signature_secret: dao_th.faucet_kp.secret,
-        }],
-        inputs: vec![],
-        outputs: vec![money_client::TransferOutput {
-            value: xdrk_supply,
-            token_id: xdrk_token_id,
-            public: dao_th.dao_kp.public,
-            serial: pallas::Base::random(&mut OsRng),
-            spend_hook,
-            user_data,
-        }],
-    };
-    let (params, proofs) = call.make(
-        &dao_th.money_mint_zkbin,
-        &dao_th.money_mint_pk,
-        &dao_th.money_burn_zkbin,
-        &dao_th.money_burn_pk,
+    info!("[Dao] Building DAO mint tx");
+    let (dao_mint_tx, dao_mint_params) = th.dao_mint(&dao, &dao_keypair)?;
+
+    info!("[Faucet] Executing DAO Mint tx");
+    th.execute_dao_mint_tx(Holder::Faucet, &dao_mint_tx, &dao_mint_params, current_slot).await?;
+
+    info!("[Alice] Executing DAO Mint tx");
+    th.execute_dao_mint_tx(Holder::Alice, &dao_mint_tx, &dao_mint_params, current_slot).await?;
+
+    info!("[Bob] Executing DAO Mint tx");
+    th.execute_dao_mint_tx(Holder::Bob, &dao_mint_tx, &dao_mint_params, current_slot).await?;
+
+    info!("[Charlie] Executing DAO Mint tx");
+    th.execute_dao_mint_tx(Holder::Charlie, &dao_mint_tx, &dao_mint_params, current_slot).await?;
+
+    info!("[Rachel] Executing DAO Mint tx");
+    th.execute_dao_mint_tx(Holder::Rachel, &dao_mint_tx, &dao_mint_params, current_slot).await?;
+
+    info!("[Dao] Executing DAO Mint tx");
+    th.execute_dao_mint_tx(Holder::Dao, &dao_mint_tx, &dao_mint_params, current_slot).await?;
+
+    // TODO: assert_trees
+
+    // =======================================
+    // Airdrop some treasury tokens to the DAO
+    // =======================================
+    info!("Stage 2. Send Treasury token");
+
+    info!("[Faucet] Building DAO airdrop tx");
+    let (airdrop_tx, airdrop_params) = th.airdrop_native(
+        DRK_TOKEN_SUPPLY,
+        Holder::Dao,
+        Some(DAO_CONTRACT_ID.inner()),           // spend_hook
+        Some(dao_mint_params.dao_bulla.inner()), // user_data
+        None,
+        None,
     )?;
 
-    let contract_id = *MONEY_CONTRACT_ID;
+    info!("[Faucet] Executing DAO airdrop tx");
+    th.execute_airdrop_native_tx(Holder::Faucet, &airdrop_tx, &airdrop_params, current_slot)
+        .await?;
 
-    let mut data = vec![MoneyFunction::TransferV1 as u8];
-    params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id, data }];
-    let proofs = vec![proofs];
-    let mut tx = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx.create_sigs(&mut OsRng, &vec![dao_th.faucet_kp.secret])?;
-    tx.signatures = vec![sigs];
+    info!("[Alice] Executing DAO airdrop tx");
+    th.execute_airdrop_native_tx(Holder::Alice, &airdrop_tx, &airdrop_params, current_slot).await?;
 
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
+    info!("[Bob] Executing DAO airdrop tx");
+    th.execute_airdrop_native_tx(Holder::Bob, &airdrop_tx, &airdrop_params, current_slot).await?;
 
-    // Wallet stuff
+    info!("[Charlie] Executing DAO airdrop tx");
+    th.execute_airdrop_native_tx(Holder::Charlie, &airdrop_tx, &airdrop_params, current_slot)
+        .await?;
 
-    // DAO reads the money received from the encrypted note
-    {
-        assert_eq!(tx.calls.len(), 1);
-        let calldata = &tx.calls[0].data;
-        let params_data = &calldata[1..];
-        let params: MoneyTransferParamsV1 = Decodable::decode(params_data)?;
+    info!("[Rachel] Executing DAO airdrop tx");
+    th.execute_airdrop_native_tx(Holder::Rachel, &airdrop_tx, &airdrop_params, current_slot)
+        .await?;
 
-        for output in params.outputs {
-            let coin = Coin::from(output.coin);
-            cache.try_decrypt_note(coin, &output.note);
-        }
-    }
+    info!("[Dao] Executing DAO airdrop tx");
+    th.execute_airdrop_native_tx(Holder::Dao, &airdrop_tx, &airdrop_params, current_slot).await?;
 
-    let mut recv_coins = cache.get_received(&dao_th.dao_kp.secret);
-    assert_eq!(recv_coins.len(), 1);
-    let dao_recv_coin = recv_coins.pop().unwrap();
-    let treasury_note = dao_recv_coin.note;
-
-    // Check the actual coin received is valid before accepting it
-
-    let coords = dao_th.dao_kp.public.inner().to_affine().coordinates().unwrap();
-    let coin = poseidon_hash::<7>([
-        *coords.x(),
-        *coords.y(),
-        pallas::Base::from(treasury_note.value),
-        treasury_note.token_id.inner(),
-        treasury_note.serial,
-        treasury_note.spend_hook,
-        treasury_note.user_data,
-    ]);
-    assert_eq!(coin, dao_recv_coin.coin.inner());
-
-    assert_eq!(treasury_note.spend_hook, spend_hook);
-    assert_eq!(treasury_note.user_data, dao_bulla.inner());
-
-    debug!(target: "dao", "DAO received a coin worth {} xDRK", treasury_note.value);
-
-    // =======================================================
-    // Money::Transfer
-    //
-    // Mint the governance token
-    // Send it to three hodlers
-    // =======================================================
-    debug!(target: "dao", "Stage 3. Minting governance token");
-
-    cache.track(dao_th.alice_kp.secret);
-    cache.track(dao_th.bob_kp.secret);
-    cache.track(dao_th.charlie_kp.secret);
-
-    // TODO: Clean this whole test up
-    let token_mint_zkbin = include_bytes!("../../money/proof/token_mint_v1.zk.bin");
-    let token_mint_zkbin = darkfi::zkas::ZkBinary::decode(token_mint_zkbin)?;
-    let token_mint_empty_wit = darkfi::zk::empty_witnesses(&token_mint_zkbin);
-    let token_mint_circuit =
-        darkfi::zk::ZkCircuit::new(token_mint_empty_wit, token_mint_zkbin.clone());
-    let token_mint_pk = darkfi::zk::ProvingKey::build(13, &token_mint_circuit);
-
-    // Spend hook and user data disabled
-    let spend_hook = pallas::Base::from(0);
-    let user_data = pallas::Base::from(0);
-
-    let mut builder = TokenMintCallBuilder {
-        mint_authority: gdrk_mint_auth,
-        recipient: dao_th.alice_kp.public,
-        amount: 400000,
-        spend_hook,
-        user_data,
-        token_mint_zkbin,
-        token_mint_pk,
-    };
-    let debris1 = builder.build()?;
-
-    builder.recipient = dao_th.bob_kp.public;
-    let debris2 = builder.build()?;
-
-    builder.amount = 200000;
-    builder.recipient = dao_th.charlie_kp.public;
-    let debris3 = builder.build()?;
-
-    assert!(2 * 400000 + 200000 == gdrk_supply);
-
-    // This should actually be 3 calls in a single tx, but w/e.
-    let mut data = vec![MoneyFunction::TokenMintV1 as u8];
-    debris1.params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
-    let proofs = vec![debris1.proofs];
-    let mut tx1 = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx1.create_sigs(&mut OsRng, &[gdrk_mint_auth.secret])?;
-    tx1.signatures = vec![sigs];
-
-    let mut data = vec![MoneyFunction::TokenMintV1 as u8];
-    debris2.params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
-    let proofs = vec![debris2.proofs];
-    let mut tx2 = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx2.create_sigs(&mut OsRng, &[gdrk_mint_auth.secret])?;
-    tx2.signatures = vec![sigs];
-
-    let mut data = vec![MoneyFunction::TokenMintV1 as u8];
-    debris3.params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
-    let proofs = vec![debris3.proofs];
-    let mut tx3 = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx3.create_sigs(&mut OsRng, &[gdrk_mint_auth.secret])?;
-    tx3.signatures = vec![sigs];
-
-    dao_th
-        .alice_validator
-        .read()
-        .await
-        .add_transactions(&[tx1.clone(), tx2.clone(), tx3.clone()], current_slot, true)
-        .await?;
-
-    // Wallet
-    {
-        for tx in [tx1, tx2, tx3] {
-            assert_eq!(tx.calls.len(), 1);
-            let calldata = &tx.calls[0].data;
-            let params_data = &calldata[1..];
-            let params: MoneyTokenMintParamsV1 = Decodable::decode(params_data)?;
-            cache.try_decrypt_note(params.output.coin, &params.output.note);
-        }
-    }
+    th.assert_trees(&HOLDERS);
 
-    let gov_keypairs = vec![dao_th.alice_kp, dao_th.bob_kp, dao_th.charlie_kp];
-    let mut gov_recv = vec![None, None, None];
-    // Check that each person received one coin
-    for (i, key) in gov_keypairs.iter().enumerate() {
-        let gov_recv_coin = {
-            let mut recv_coins = cache.get_received(&key.secret);
-            assert_eq!(recv_coins.len(), 1);
-            let recv_coin = recv_coins.pop().unwrap();
-            let note = &recv_coin.note;
-
-            assert_eq!(note.token_id, gdrk_token_id);
-            // Normal payment
-            assert_eq!(note.spend_hook, pallas::Base::from(0));
-            assert_eq!(note.user_data, pallas::Base::from(0));
-
-            let (pub_x, pub_y) = key.public.xy();
-            let coin = poseidon_hash::<7>([
-                pub_x,
-                pub_y,
-                pallas::Base::from(note.value),
-                note.token_id.inner(),
-                note.serial,
-                note.spend_hook,
-                note.user_data,
-            ]);
-            assert_eq!(coin, recv_coin.coin.inner());
-
-            debug!(target: "dao", "Holder{} received a coin worth {} gDRK", i, note.value);
-
-            recv_coin
-        };
-        gov_recv[i] = Some(gov_recv_coin);
-    }
-    // unwrap them for this demo
-    let gov_recv: Vec<_> = gov_recv.into_iter().map(|r| r.unwrap()).collect();
+    // Gather the DAO owncoin
+    th.gather_owncoin(Holder::Dao, airdrop_params.outputs[0].clone(), None)?;
 
-    // =======================================================
-    // Dao::Propose
-    //
-    // Propose the vote
-    // In order to make a valid vote, first the proposer must
-    // meet a criteria for a minimum number of gov tokens
-    //
-    // DAO rules:
-    // 1. gov token IDs must match on all inputs
-    // 2. proposals must be submitted by minimum amount
-    // 3. all votes >= quorum
-    // 4. outcome > approval_ratio
-    // 5. structure of outputs
-    //   output 0: value and address
-    //   output 1: change address
-    // =======================================================
-    debug!(target: "dao", "Stage 4. Propose the vote");
+    // ======================================
+    // Mint the governance token to 3 holders
+    // ======================================
+    info!("Stage 3. Minting governance token");
 
-    // TODO: look into proposal expiry once time for voting has finished
+    info!("[Alice] Building governance token mint tx for Alice");
+    let (a_token_mint_tx, a_token_mint_params) =
+        th.token_mint(ALICE_GOV_SUPPLY, Holder::Alice, Holder::Alice, None, None)?;
 
-    let receiver_keypair = Keypair::random(&mut OsRng);
+    info!("[Faucet] Executing governance token mint tx for Alice");
+    th.execute_token_mint_tx(Holder::Faucet, &a_token_mint_tx, &a_token_mint_params, current_slot)
+        .await?;
 
-    let (money_leaf_position, money_merkle_path) = {
-        let tree = &cache.tree;
-        let leaf_position = gov_recv[0].leaf_position;
-        let merkle_path = tree.witness(leaf_position, 0).unwrap();
-        (leaf_position, merkle_path)
-    };
+    info!("[Alice] Executing governance token mint tx for Alice");
+    th.execute_token_mint_tx(Holder::Alice, &a_token_mint_tx, &a_token_mint_params, current_slot)
+        .await?;
 
-    // TODO: is it possible for an invalid transfer() to be constructed on exec()?
-    //       need to look into this
-    let signature_secret = SecretKey::random(&mut OsRng);
-    let input = client::DaoProposeStakeInput {
-        secret: dao_th.alice_kp.secret,
-        note: gov_recv[0].note.clone(),
-        leaf_position: money_leaf_position,
-        merkle_path: money_merkle_path,
-        signature_secret,
-    };
+    info!("[Bob] Executing governance token mint tx for Alice");
+    th.execute_token_mint_tx(Holder::Bob, &a_token_mint_tx, &a_token_mint_params, current_slot)
+        .await?;
 
-    let (dao_merkle_path, dao_merkle_root) = {
-        let tree = &dao_tree;
-        let root = tree.root(0).unwrap();
-        let merkle_path = tree.witness(dao_leaf_position, 0).unwrap();
-        (merkle_path, root)
-    };
+    info!("[Charlie] Executing governance token mint tx for Alice");
+    th.execute_token_mint_tx(Holder::Charlie, &a_token_mint_tx, &a_token_mint_params, current_slot)
+        .await?;
 
-    let proposal = client::DaoProposalInfo {
-        dest: receiver_keypair.public,
-        amount: 1000,
-        token_id: xdrk_token_id,
-        blind: pallas::Base::random(&mut OsRng),
-    };
+    info!("[Rachel] Executing governance token mint tx for Alice");
+    th.execute_token_mint_tx(Holder::Rachel, &a_token_mint_tx, &a_token_mint_params, current_slot)
+        .await?;
 
-    let call = client::DaoProposeCall {
-        inputs: vec![input],
-        proposal,
-        dao: dao.clone(),
-        dao_leaf_position,
-        dao_merkle_path,
-        dao_merkle_root,
-    };
-    let (params, proofs) = call.make(
-        &dao_th.dao_propose_burn_zkbin,
-        &dao_th.dao_propose_burn_pk,
-        &dao_th.dao_propose_main_zkbin,
-        &dao_th.dao_propose_main_pk,
-    )?;
+    info!("[Dao] Executing governance token mint tx for Alice");
+    th.execute_token_mint_tx(Holder::Dao, &a_token_mint_tx, &a_token_mint_params, current_slot)
+        .await?;
 
-    let contract_id = *DAO_CONTRACT_ID;
+    th.assert_trees(&HOLDERS);
 
-    let mut data = vec![DaoFunction::Propose as u8];
-    params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id, data }];
-    let proofs = vec![proofs];
-    let mut tx = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?;
-    tx.signatures = vec![sigs];
+    // Gather owncoin
+    th.gather_owncoin(Holder::Alice, a_token_mint_params.output, None)?;
 
-    let timer = Instant::now();
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
-    propose_verify_times.push(timer.elapsed());
+    info!("[Alice] Building governance token mint tx for Bob");
+    let (b_token_mint_tx, b_token_mint_params) =
+        th.token_mint(BOB_GOV_SUPPLY, Holder::Alice, Holder::Bob, None, None)?;
 
-    //// Wallet
+    info!("[Faucet] Executing governance token mint tx for Bob");
+    th.execute_token_mint_tx(Holder::Faucet, &b_token_mint_tx, &b_token_mint_params, current_slot)
+        .await?;
 
-    // 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();
+    info!("[Alice] Executing governance token mint tx for Bob");
+    th.execute_token_mint_tx(Holder::Alice, &b_token_mint_tx, &b_token_mint_params, current_slot)
+        .await?;
 
-    // Read received proposal
-    let (proposal, proposal_bulla) = {
-        let note: client::DaoProposeNote = params.note.decrypt(&dao_th.dao_kp.secret).unwrap();
+    info!("[Bob] Executing governance token mint tx for Bob");
+    th.execute_token_mint_tx(Holder::Bob, &b_token_mint_tx, &b_token_mint_params, current_slot)
+        .await?;
 
-        // TODO: check it belongs to DAO bulla
+    info!("[Charlie] Executing governance token mint tx for Bob");
+    th.execute_token_mint_tx(Holder::Charlie, &b_token_mint_tx, &b_token_mint_params, current_slot)
+        .await?;
 
-        // Return the proposal info
-        (note.proposal, params.proposal_bulla)
-    };
-    debug!(target: "dao", "Proposal now active!");
-    debug!(target: "dao", "  destination: {:?}", proposal.dest);
-    debug!(target: "dao", "  amount: {}", proposal.amount);
-    debug!(target: "dao", "  token_id: {:?}", proposal.token_id);
-    debug!(target: "dao", "  dao_bulla: {:?}", dao_bulla.inner());
-    debug!(target: "dao", "Proposal bulla: {:?}", proposal_bulla);
-
-    // =======================================================
-    // Proposal is accepted!
-    // Start the voting
-    // =======================================================
-
-    // Copying these schizo comments from python code:
-    // Lets the voting begin
-    // Voters have access to the proposal and dao data
-    //   vote_state = VoteState()
-    // We don't need to copy nullifier set because it is checked from gov_state
-    // in vote_state_transition() anyway
-    //
-    // TODO: what happens if voters don't unblind their vote
-    // Answer:
-    //   1. there is a time limit
-    //   2. both the MPC or users can unblind
-    //
-    // TODO: bug if I vote then send money, then we can double vote
-    // TODO: all timestamps missing
-    //       - timelock (future voting starts in 2 days)
-    // Fix: use nullifiers from money gov state only from
-    // beginning of gov period
-    // Cannot use nullifiers from before voting period
-
-    debug!(target: "dao", "Stage 5. Start voting");
-
-    // We were previously saving updates here for testing
-    // let mut updates = vec![];
-
-    // User 1: YES
-
-    let (money_leaf_position, money_merkle_path) = {
-        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)
-    };
+    info!("[Rachel] Executing governance token mint tx for Bob");
+    th.execute_token_mint_tx(Holder::Rachel, &b_token_mint_tx, &b_token_mint_params, current_slot)
+        .await?;
 
-    let signature_secret = SecretKey::random(&mut OsRng);
-    let input = client::DaoVoteInput {
-        secret: dao_th.alice_kp.secret,
-        note: gov_recv[0].note.clone(),
-        leaf_position: money_leaf_position,
-        merkle_path: money_merkle_path,
-        signature_secret,
-    };
+    info!("[Dao] Executing governance token mint tx for Bob");
+    th.execute_token_mint_tx(Holder::Dao, &b_token_mint_tx, &b_token_mint_params, current_slot)
+        .await?;
 
-    let vote_option: bool = true;
-    // assert!(vote_option || !vote_option); // wtf
+    th.assert_trees(&HOLDERS);
 
-    // We create a new keypair to encrypt the vote.
-    // For the demo MVP, you can just use the dao_keypair secret
-    let vote_keypair_1 = Keypair::random(&mut OsRng);
+    // Gather owncoin
+    th.gather_owncoin(Holder::Bob, b_token_mint_params.output, None)?;
 
-    let call = client::DaoVoteCall {
-        inputs: vec![input],
-        vote_option,
-        yes_vote_blind: pallas::Scalar::random(&mut OsRng),
-        vote_keypair: vote_keypair_1,
-        proposal: proposal.clone(),
-        dao: dao.clone(),
-    };
-    let (params, proofs) = call.make(
-        &dao_th.dao_vote_burn_zkbin,
-        &dao_th.dao_vote_burn_pk,
-        &dao_th.dao_vote_main_zkbin,
-        &dao_th.dao_vote_main_pk,
-    )?;
+    info!("[Alice] Building governance token mint tx for Charlie");
+    let (c_token_mint_tx, c_token_mint_params) =
+        th.token_mint(CHARLIE_GOV_SUPPLY, Holder::Alice, Holder::Charlie, None, None)?;
 
-    let contract_id = *DAO_CONTRACT_ID;
-
-    let mut data = vec![DaoFunction::Vote as u8];
-    params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id, data }];
-    let proofs = vec![proofs];
-    let mut tx = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?;
-    tx.signatures = vec![sigs];
-
-    let timer = Instant::now();
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
-    vote_verify_times.push(timer.elapsed());
-
-    // Secret vote info. Needs to be revealed at some point.
-    // TODO: look into verifiable encryption for notes
-    // TODO: look into timelock puzzle as a possibility
-    let vote_note_1 = {
-        let note: client::DaoVoteNote = params.note.decrypt(&vote_keypair_1.secret).unwrap();
-        note
-    };
-    debug!(target: "dao", "User 1 voted!");
-    debug!(target: "dao", "  vote_option: {}", vote_note_1.vote_option);
-    debug!(target: "dao", "  value: {}", vote_note_1.all_vote_value);
+    info!("[Faucet] Executing governance token mint tx for Charlie");
+    th.execute_token_mint_tx(Holder::Faucet, &c_token_mint_tx, &c_token_mint_params, current_slot)
+        .await?;
 
-    // User 2: NO
+    info!("[Alice] Executing governance token mint tx for Charlie");
+    th.execute_token_mint_tx(Holder::Alice, &c_token_mint_tx, &c_token_mint_params, current_slot)
+        .await?;
 
-    let (money_leaf_position, money_merkle_path) = {
-        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)
-    };
+    info!("[Bob] Executing governance token mint tx for Charlie");
+    th.execute_token_mint_tx(Holder::Bob, &c_token_mint_tx, &c_token_mint_params, current_slot)
+        .await?;
 
-    let signature_secret = SecretKey::random(&mut OsRng);
-    let input = client::DaoVoteInput {
-        //secret: gov_keypair_2.secret,
-        secret: dao_th.bob_kp.secret,
-        note: gov_recv[1].note.clone(),
-        leaf_position: money_leaf_position,
-        merkle_path: money_merkle_path,
-        signature_secret,
-    };
+    info!("[Charlie] Executing governance token mint tx for Charlie");
+    th.execute_token_mint_tx(Holder::Charlie, &c_token_mint_tx, &c_token_mint_params, current_slot)
+        .await?;
 
-    let vote_option: bool = false;
-    // assert!(vote_option || !vote_option); // wtf
+    info!("[Rachel] Executing governance token mint tx for Charlie");
+    th.execute_token_mint_tx(Holder::Rachel, &c_token_mint_tx, &c_token_mint_params, current_slot)
+        .await?;
 
-    // We create a new keypair to encrypt the vote.
-    let vote_keypair_2 = Keypair::random(&mut OsRng);
+    info!("[Dao] Executing governance token mint tx for Charlie");
+    th.execute_token_mint_tx(Holder::Dao, &c_token_mint_tx, &c_token_mint_params, current_slot)
+        .await?;
 
-    let call = client::DaoVoteCall {
-        inputs: vec![input],
-        vote_option,
-        yes_vote_blind: pallas::Scalar::random(&mut OsRng),
-        vote_keypair: vote_keypair_2,
-        proposal: proposal.clone(),
-        dao: dao.clone(),
-    };
-    let (params, proofs) = call.make(
-        &dao_th.dao_vote_burn_zkbin,
-        &dao_th.dao_vote_burn_pk,
-        &dao_th.dao_vote_main_zkbin,
-        &dao_th.dao_vote_main_pk,
+    th.assert_trees(&HOLDERS);
+
+    // Gather owncoin
+    th.gather_owncoin(Holder::Charlie, c_token_mint_params.output, None)?;
+
+    // ================
+    // Dao::Propose
+    // Propose the vote
+    // ================
+    info!("Stage 4. Propose the vote");
+    // TODO: look into proposal expiry once time for voting has finished
+    // TODO: Is it possible for an invalid transfer() to be constructed on exec()?
+    //       Need to look into this.
+    info!("[Alice] Building DAO proposal tx");
+    let (propose_tx, propose_params, propose_info) = th.dao_propose(
+        Holder::Alice,
+        Holder::Rachel,
+        PROPOSAL_AMOUNT,
+        drk_token_id,
+        dao.clone(),
+        dao_mint_params.dao_bulla,
     )?;
 
-    let contract_id = *DAO_CONTRACT_ID;
+    info!("[Faucet] Executing DAO proposal tx");
+    th.execute_dao_propose_tx(Holder::Faucet, &propose_tx, &propose_params, current_slot).await?;
 
-    let mut data = vec![DaoFunction::Vote as u8];
-    params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id, data }];
-    let proofs = vec![proofs];
-    let mut tx = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?;
-    tx.signatures = vec![sigs];
+    info!("[Alice] Executing DAO proposal tx");
+    th.execute_dao_propose_tx(Holder::Alice, &propose_tx, &propose_params, current_slot).await?;
 
-    let timer = Instant::now();
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
-    vote_verify_times.push(timer.elapsed());
+    info!("[Bob] Executing DAO proposal tx");
+    th.execute_dao_propose_tx(Holder::Bob, &propose_tx, &propose_params, current_slot).await?;
 
-    let vote_note_2 = {
-        let note: client::DaoVoteNote = params.note.decrypt(&vote_keypair_2.secret).unwrap();
-        note
-    };
-    debug!(target: "dao", "User 2 voted!");
-    debug!(target: "dao", "  vote_option: {}", vote_note_2.vote_option);
-    debug!(target: "dao", "  value: {}", vote_note_2.all_vote_value);
+    info!("[Charlie] Executing DAO proposal tx");
+    th.execute_dao_propose_tx(Holder::Charlie, &propose_tx, &propose_params, current_slot).await?;
 
-    // User 3: YES
+    info!("[Rachel] Executing DAO proposal tx");
+    th.execute_dao_propose_tx(Holder::Rachel, &propose_tx, &propose_params, current_slot).await?;
 
-    let (money_leaf_position, money_merkle_path) = {
-        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)
-    };
+    info!("[Dao] Executing DAO proposal tx");
+    th.execute_dao_propose_tx(Holder::Dao, &propose_tx, &propose_params, current_slot).await?;
 
-    let signature_secret = SecretKey::random(&mut OsRng);
-    let input = client::DaoVoteInput {
-        //secret: gov_keypair_3.secret,
-        secret: dao_th.charlie_kp.secret,
-        note: gov_recv[2].note.clone(),
-        leaf_position: money_leaf_position,
-        merkle_path: money_merkle_path,
-        signature_secret,
-    };
+    th.assert_trees(&HOLDERS);
 
-    let vote_option: bool = true;
-    // assert!(vote_option || !vote_option); // wtf
+    // =====================================
+    // Dao::Vote
+    // Proposal is accepted. Start the vote.
+    // =====================================
+    info!("Stage 5. Start voting");
 
-    // We create a new keypair to encrypt the vote.
-    let vote_keypair_3 = Keypair::random(&mut OsRng);
+    info!("[Alice] Building vote tx (yes)");
+    let (alice_vote_tx, alice_vote_params) = th.dao_vote(
+        Holder::Alice,
+        &dao_keypair,
+        true,
+        dao.clone(),
+        propose_info.clone(),
+        propose_params.proposal_bulla,
+    )?;
 
-    let call = client::DaoVoteCall {
-        inputs: vec![input],
-        vote_option,
-        yes_vote_blind: pallas::Scalar::random(&mut OsRng),
-        vote_keypair: vote_keypair_3,
-        proposal: proposal.clone(),
-        dao: dao.clone(),
-    };
-    let (params, proofs) = call.make(
-        &dao_th.dao_vote_burn_zkbin,
-        &dao_th.dao_vote_burn_pk,
-        &dao_th.dao_vote_main_zkbin,
-        &dao_th.dao_vote_main_pk,
+    info!("[Bob] Building vote tx (no)");
+    let (bob_vote_tx, bob_vote_params) = th.dao_vote(
+        Holder::Bob,
+        &dao_keypair,
+        false,
+        dao.clone(),
+        propose_info.clone(),
+        propose_params.proposal_bulla,
     )?;
 
-    let contract_id = *DAO_CONTRACT_ID;
-
-    let mut data = vec![DaoFunction::Vote as u8];
-    params.encode(&mut data)?;
-    let calls = vec![ContractCall { contract_id, data }];
-    let proofs = vec![proofs];
-    let mut tx = Transaction { calls, proofs, signatures: vec![] };
-    let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?;
-    tx.signatures = vec![sigs];
-
-    let timer = Instant::now();
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
-    vote_verify_times.push(timer.elapsed());
-
-    // Secret vote info. Needs to be revealed at some point.
-    // TODO: look into verifiable encryption for notes
-    // TODO: look into timelock puzzle as a possibility
-    let vote_note_3 = {
-        let note: client::DaoVoteNote = params.note.decrypt(&vote_keypair_3.secret).unwrap();
-        note
-    };
-    debug!(target: "dao", "User 3 voted!");
-    debug!(target: "dao", "  vote_option: {}", vote_note_3.vote_option);
-    debug!(target: "dao", "  value: {}", vote_note_3.all_vote_value);
-
-    // Every votes produces a semi-homomorphic encryption of their vote.
-    // Which is either yes or no
-    // We copy the state tree for the governance token so coins can be used
-    // to vote on other proposals at the same time.
-    // With their vote, they produce a ZK proof + nullifier
-    // The votes are unblinded by MPC to a selected party at the end of the
-    // voting period.
-    // (that's if we want votes to be hidden during voting)
+    info!("[Charlie] Building vote tx (yes)");
+    let (charlie_vote_tx, charlie_vote_params) = th.dao_vote(
+        Holder::Charlie,
+        &dao_keypair,
+        true,
+        dao.clone(),
+        propose_info.clone(),
+        propose_params.proposal_bulla,
+    )?;
 
-    let mut total_yes_vote_value = 0;
-    let mut total_all_vote_value = 0;
+    info!("[Faucet] Executing Alice vote tx");
+    th.execute_dao_vote_tx(Holder::Faucet, &alice_vote_tx, &alice_vote_params, current_slot)
+        .await?;
+    info!("[Faucet] Executing Bob vote tx");
+    th.execute_dao_vote_tx(Holder::Faucet, &bob_vote_tx, &bob_vote_params, current_slot).await?;
+    info!("[Faucet] Executing Charlie vote tx");
+    th.execute_dao_vote_tx(Holder::Faucet, &charlie_vote_tx, &charlie_vote_params, current_slot)
+        .await?;
 
-    let mut blind_total_vote = model::DaoBlindAggregateVote::default();
+    info!("[Alice] Executing Alice vote tx");
+    th.execute_dao_vote_tx(Holder::Alice, &alice_vote_tx, &alice_vote_params, current_slot).await?;
+    info!("[Alice] Executing Bob vote tx");
+    th.execute_dao_vote_tx(Holder::Alice, &bob_vote_tx, &bob_vote_params, current_slot).await?;
+    info!("[Alice] Executing Charlie vote tx");
+    th.execute_dao_vote_tx(Holder::Alice, &charlie_vote_tx, &charlie_vote_params, current_slot)
+        .await?;
 
-    // Just keep track of these for the assert statements after the for loop
-    // but they aren't needed otherwise.
-    let mut total_yes_vote_blind = pallas::Scalar::from(0);
-    let mut total_all_vote_blind = pallas::Scalar::from(0);
+    info!("[Bob] Executing Alice vote tx");
+    th.execute_dao_vote_tx(Holder::Bob, &alice_vote_tx, &alice_vote_params, current_slot).await?;
+    info!("[Bob] Executing Bob vote tx");
+    th.execute_dao_vote_tx(Holder::Bob, &bob_vote_tx, &bob_vote_params, current_slot).await?;
+    info!("[Bob] Executing Charlie vote tx");
+    th.execute_dao_vote_tx(Holder::Bob, &charlie_vote_tx, &charlie_vote_params, current_slot)
+        .await?;
+
+    info!("[Charlie] Executing Alice vote tx");
+    th.execute_dao_vote_tx(Holder::Charlie, &alice_vote_tx, &alice_vote_params, current_slot)
+        .await?;
+    info!("[Charlie] Executing Bob vote tx");
+    th.execute_dao_vote_tx(Holder::Charlie, &bob_vote_tx, &bob_vote_params, current_slot).await?;
+    info!("[Charlie] Executing Charlie vote tx");
+    th.execute_dao_vote_tx(Holder::Charlie, &charlie_vote_tx, &charlie_vote_params, current_slot)
+        .await?;
+
+    info!("[Rachel] Executing Alice vote tx");
+    th.execute_dao_vote_tx(Holder::Rachel, &alice_vote_tx, &alice_vote_params, current_slot)
+        .await?;
+    info!("[Rachel] Executing Bob vote tx");
+    th.execute_dao_vote_tx(Holder::Rachel, &bob_vote_tx, &bob_vote_params, current_slot).await?;
+    info!("[Rachel] Executing Charlie vote tx");
+    th.execute_dao_vote_tx(Holder::Rachel, &charlie_vote_tx, &charlie_vote_params, current_slot)
+        .await?;
+
+    info!("[Dao] Executing Alice vote tx");
+    th.execute_dao_vote_tx(Holder::Dao, &alice_vote_tx, &alice_vote_params, current_slot).await?;
+    info!("[Dao] Executing Bob vote tx");
+    th.execute_dao_vote_tx(Holder::Dao, &bob_vote_tx, &bob_vote_params, current_slot).await?;
+    info!("[Dao] Executing Charlie vote tx");
+    th.execute_dao_vote_tx(Holder::Dao, &charlie_vote_tx, &charlie_vote_params, current_slot)
+        .await?;
+
+    // Gather and decrypt all vote notes
+    let vote_note_1: DaoVoteNote = alice_vote_params.note.decrypt(&dao_keypair.secret).unwrap();
+    let vote_note_2: DaoVoteNote = bob_vote_params.note.decrypt(&dao_keypair.secret).unwrap();
+    let vote_note_3: DaoVoteNote = charlie_vote_params.note.decrypt(&dao_keypair.secret).unwrap();
+
+    // Count the votes
+    let mut total_yes_vote_value = 0;
+    let mut total_all_vote_value = 0;
+    let mut blind_total_vote = DaoBlindAggregateVote::default();
+    let mut total_yes_vote_blind = pallas::Scalar::ZERO;
+    let mut total_all_vote_blind = pallas::Scalar::ZERO;
 
     for (i, note) in [vote_note_1, vote_note_2, vote_note_3].iter().enumerate() {
         total_yes_vote_blind += note.yes_vote_blind;
         total_all_vote_blind += note.all_vote_blind;
 
         // Update private values
-
         // vote_option is either 0 or 1
         let yes_vote_value = note.vote_option as u64 * note.all_vote_value;
         total_yes_vote_value += yes_vote_value;
         total_all_vote_value += note.all_vote_value;
 
         // Update public values
-
         let yes_vote_commit = pedersen_commitment_u64(yes_vote_value, note.yes_vote_blind);
         let all_vote_commit = pedersen_commitment_u64(note.all_vote_value, note.all_vote_blind);
-
-        let blind_vote = model::DaoBlindAggregateVote { yes_vote_commit, all_vote_commit };
+        let blind_vote = DaoBlindAggregateVote { yes_vote_commit, all_vote_commit };
         blind_total_vote.aggregate(blind_vote);
 
         // Just for the debug
@@ -719,150 +402,79 @@ async fn integration_test() -> Result<()> {
             true => "yes",
             false => "no",
         };
-        debug!(
-            target: "dao",
-            "Voter {} voted {} with {} gDRK",
-            i,
-            vote_result,
-            note.all_vote_value,
-        );
+
+        info!("Voter {} voted {} with {} tokens", i, vote_result, note.all_vote_value);
     }
 
-    debug!(target: "dao", "Outcome = {} / {}", total_yes_vote_value, total_all_vote_value);
+    info!("Outcome = {} / {}", total_yes_vote_value, total_all_vote_value);
 
     assert!(
         blind_total_vote.all_vote_commit ==
-            pedersen_commitment_u64(total_all_vote_value, total_all_vote_blind),
+            pedersen_commitment_u64(total_all_vote_value, total_all_vote_blind)
     );
+
     assert!(
         blind_total_vote.yes_vote_commit ==
-            pedersen_commitment_u64(total_yes_vote_value, total_yes_vote_blind),
+            pedersen_commitment_u64(total_yes_vote_value, total_yes_vote_blind)
     );
 
-    // =======================================================
+    // ================
+    // Dao::Exec
     // Execute the vote
-    // =======================================================
+    // ================
+    info!("Stage 6. Execute the vote");
 
-    debug!(target: "dao", "Stage 6. Execute vote");
-
-    // Used to export user_data from this coin so it can be accessed by DAO::exec()
-    let user_data_blind = pallas::Base::random(&mut OsRng);
-
-    let user_serial = pallas::Base::random(&mut OsRng);
-    let dao_serial = pallas::Base::random(&mut OsRng);
-    let input_value = treasury_note.value;
-    let input_value_blind = pallas::Scalar::random(&mut OsRng);
-    let xfer_signature_secret = SecretKey::random(&mut OsRng);
-    let exec_signature_secret = SecretKey::random(&mut OsRng);
+    info!("[Dao] Building Dao::Exec tx");
+    let (exec_tx, xfer_params, exec_params) = th.dao_exec(
+        dao,
+        dao_mint_params.dao_bulla,
+        propose_info,
+        total_yes_vote_value,
+        total_all_vote_value,
+        total_yes_vote_blind,
+        total_all_vote_blind,
+    )?;
 
-    let (treasury_leaf_position, treasury_merkle_path) = {
-        let tree = &cache.tree;
-        let leaf_position = dao_recv_coin.leaf_position;
-        let merkle_path = tree.witness(leaf_position, 0).unwrap();
-        (leaf_position, merkle_path)
-    };
+    info!("[Faucet] Executing Dao::Exec tx");
+    th.execute_dao_exec_tx(Holder::Faucet, &exec_tx, &xfer_params, &exec_params, current_slot)
+        .await?;
 
-    // TODO: this should be the contract/func ID
-    //let spend_hook = pallas::Base::from(110);
-    let spend_hook = DAO_CONTRACT_ID.inner();
-    // The user_data can be a simple hash of the items passed into the ZK proof
-    // up to corresponding linked ZK proof to interpret however they need.
-    // In out case, it's the bulla for the DAO
-    let user_data = dao_bulla.inner();
-
-    let xfer_call = money_client::TransferCall {
-        clear_inputs: vec![],
-        inputs: vec![money_client::TransferInput {
-            leaf_position: treasury_leaf_position,
-            merkle_path: treasury_merkle_path,
-            secret: dao_th.dao_kp.secret,
-            note: treasury_note,
-            user_data_blind,
-            value_blind: input_value_blind,
-            signature_secret: xfer_signature_secret,
-        }],
-        outputs: vec![
-            // Sending money
-            money_client::TransferOutput {
-                value: 1000,
-                token_id: xdrk_token_id,
-                //public: user_keypair.public,
-                public: receiver_keypair.public,
-                serial: user_serial,
-                spend_hook: pallas::Base::from(0),
-                user_data: pallas::Base::from(0),
-            },
-            // Change back to DAO
-            money_client::TransferOutput {
-                value: xdrk_supply - 1000,
-                token_id: xdrk_token_id,
-                public: dao_th.dao_kp.public,
-                serial: dao_serial,
-                spend_hook,
-                user_data,
-            },
-        ],
-    };
-    let (xfer_params, xfer_proofs) = xfer_call.make(
-        &dao_th.money_mint_zkbin,
-        &dao_th.money_mint_pk,
-        &dao_th.money_burn_zkbin,
-        &dao_th.money_burn_pk,
-    )?;
+    info!("[Alice] Executing Dao::Exec tx");
+    th.execute_dao_exec_tx(Holder::Alice, &exec_tx, &xfer_params, &exec_params, current_slot)
+        .await?;
 
-    let mut data = vec![MoneyFunction::TransferV1 as u8];
-    xfer_params.encode(&mut data)?;
-    let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+    info!("[Bob] Executing Dao::Exec tx");
+    th.execute_dao_exec_tx(Holder::Bob, &exec_tx, &xfer_params, &exec_params, current_slot).await?;
 
-    let call = client::DaoExecCall {
-        proposal,
-        dao,
-        yes_vote_value: total_yes_vote_value,
-        all_vote_value: total_all_vote_value,
-        yes_vote_blind: total_yes_vote_blind,
-        all_vote_blind: total_all_vote_blind,
-        user_serial,
-        dao_serial,
-        input_value,
-        input_value_blind,
-        hook_dao_exec: spend_hook,
-        signature_secret: exec_signature_secret,
-    };
-    let (exec_params, exec_proofs) = call.make(&dao_th.dao_exec_zkbin, &dao_th.dao_exec_pk)?;
+    info!("[Charlie] Executing Dao::Exec tx");
+    th.execute_dao_exec_tx(Holder::Charlie, &exec_tx, &xfer_params, &exec_params, current_slot)
+        .await?;
 
-    let mut data = vec![DaoFunction::Exec as u8];
-    exec_params.encode(&mut data)?;
-    let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
+    info!("[Rachel] Executing Dao::Exec tx");
+    th.execute_dao_exec_tx(Holder::Rachel, &exec_tx, &xfer_params, &exec_params, current_slot)
+        .await?;
 
-    let mut tx = Transaction {
-        calls: vec![xfer_call, exec_call],
-        proofs: vec![xfer_proofs, exec_proofs],
-        signatures: vec![],
-    };
-    let xfer_sigs = tx.create_sigs(&mut OsRng, &vec![xfer_signature_secret])?;
-    let exec_sigs = tx.create_sigs(&mut OsRng, &vec![exec_signature_secret])?;
-    tx.signatures = vec![xfer_sigs, exec_sigs];
+    info!("[Dao] Executing Dao::Exec tx");
+    th.execute_dao_exec_tx(Holder::Dao, &exec_tx, &xfer_params, &exec_params, current_slot).await?;
 
-    let timer = Instant::now();
-    dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?;
-    exec_verify_times.push(timer.elapsed());
+    th.assert_trees(&HOLDERS);
 
-    // Statistics
-    let mint_avg = mint_verify_times.iter().sum::<Duration>();
-    let mint_avg = mint_avg / mint_verify_times.len() as u32;
-    println!("Average Mint verification time: {:?}", mint_avg);
+    // Gather the coins
+    th.gather_owncoin(Holder::Dao, xfer_params.outputs[0].clone(), None)?;
+    th.gather_owncoin(Holder::Rachel, xfer_params.outputs[1].clone(), None)?;
 
-    let propose_avg = propose_verify_times.iter().sum::<Duration>();
-    let propose_avg = propose_avg / propose_verify_times.len() as u32;
-    println!("Average Propose verification time: {:?}", propose_avg);
+    let rachel_wallet = th.holders.get(&Holder::Rachel).unwrap();
+    assert!(rachel_wallet.unspent_money_coins[0].note.value == PROPOSAL_AMOUNT);
+    assert!(rachel_wallet.unspent_money_coins[0].note.token_id == drk_token_id);
 
-    let vote_avg = vote_verify_times.iter().sum::<Duration>();
-    let vote_avg = vote_avg / vote_verify_times.len() as u32;
-    println!("Average Vote verification time: {:?}", vote_avg);
+    // FIXME: The harness doesn't register that we spent the first coin on the proposal.
+    let dao_wallet = th.holders.get(&Holder::Dao).unwrap();
+    assert!(dao_wallet.unspent_money_coins[1].note.value == DRK_TOKEN_SUPPLY - PROPOSAL_AMOUNT);
+    assert!(dao_wallet.unspent_money_coins[1].note.token_id == drk_token_id);
 
-    let exec_avg = exec_verify_times.iter().sum::<Duration>();
-    let exec_avg = exec_avg / exec_verify_times.len() as u32;
-    println!("Average Exec verification time: {:?}", exec_avg);
+    // Stats
+    th.statistics();
 
+    // Thanks for reading
     Ok(())
 }

+ 4 - 2
src/contract/money/tests/verification_bench.rs

@@ -53,7 +53,8 @@ async fn alice2alice_random_amounts() -> Result<()> {
     info!(target: "money", "[Faucet] ========================");
     info!(target: "money", "[Faucet] Building Alice's airdrop");
     info!(target: "money", "[Faucet] ========================");
-    let (airdrop_tx, airdrop_params) = th.airdrop_native(ALICE_AIRDROP, Holder::Alice)?;
+    let (airdrop_tx, airdrop_params) =
+        th.airdrop_native(ALICE_AIRDROP, Holder::Alice, None, None, None, None)?;
 
     info!(target: "money", "[Faucet] ==========================");
     info!(target: "money", "[Faucet] Executing Alice airdrop tx");
@@ -151,7 +152,8 @@ async fn alice2alice_multiplecoins_random_amounts() -> Result<()> {
         info!(target: "money", "[Faucet] ===================================================");
         info!(target: "money", "[Faucet] Building Money::Mint params for Alice's mint for token {} and amount {}", i, amount);
         info!(target: "money", "[Faucet] ===================================================");
-        let (mint_tx, mint_params) = th.token_mint(amount, Holder::Alice, Holder::Alice)?;
+        let (mint_tx, mint_params) =
+            th.token_mint(amount, Holder::Alice, Holder::Alice, None, None)?;
 
         info!(target: "money", "[Faucet] =======================");
         info!(target: "money", "[Faucet] Executing Alice mint tx");