Pārlūkot izejas kodu

contract/test-harness: Cleanup and addition of tx fees.

parazyd 2 gadi atpakaļ
vecāks
revīzija
8828438d8f

+ 0 - 1
Cargo.lock

@@ -1926,7 +1926,6 @@ name = "darkfi-contract-test-harness"
 version = "0.4.1"
 dependencies = [
  "blake3 1.5.0",
- "bs58",
  "darkfi",
  "darkfi-sdk",
  "darkfi-serial",

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

@@ -23,7 +23,7 @@ use darkfi_sdk::{
     crypto::{
         note::{AeadEncryptedNote, ElGamalEncryptedNote},
         pasta_prelude::*,
-        poseidon_hash, BaseBlind, MerkleNode, PublicKey,
+        poseidon_hash, ContractId, MerkleNode, Nullifier, PublicKey, TokenId,
     },
     error::ContractError,
     pasta::pallas,
@@ -108,7 +108,7 @@ darkfi_sdk::ty_from_fp!(DaoBulla);
 #[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
 // ANCHOR: dao-auth-call
 pub struct DaoAuthCall {
-    pub contract_id: pallas::Base,
+    pub contract_id: ContractId,
     pub function_code: u8,
     pub auth_data: Vec<u8>,
 }

+ 1 - 1
src/contract/money/src/client/fee_v1.rs

@@ -122,7 +122,7 @@ pub async fn append_fee_call(
 
     let input_value_blind = Blind::random(&mut OsRng);
     let fee_value_blind = Blind::random(&mut OsRng);
-    let output_value_blind = compute_remainder_blind(&[], &[input_value_blind], &[fee_value_blind]);
+    let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
 
     let signature_secret = SecretKey::random(&mut OsRng);
 

+ 13 - 10
src/contract/money/src/client/mod.rs

@@ -30,7 +30,10 @@ use std::hash::{Hash, Hasher};
 
 use darkfi_sdk::{
     bridgetree,
-    crypto::{BaseBlind, Blind, FuncId, ScalarBlind, SecretKey},
+    crypto::{
+        pasta_prelude::{Field, PrimeField},
+        poseidon_hash, BaseBlind, Blind, FuncId, Nullifier, ScalarBlind, SecretKey, TokenId,
+    },
     pasta::pallas,
 };
 use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
@@ -99,12 +102,17 @@ pub struct OwnCoin {
     pub note: MoneyNote,
     /// Coin's secret key
     pub secret: SecretKey,
-    /// Coin's nullifier
-    pub nullifier: Nullifier,
     /// Coin's leaf position in the Merkle tree of coins
     pub leaf_position: bridgetree::Position,
 }
 
+impl OwnCoin {
+    /// Derive the [`Nullifier`] for this [`OwnCoin`]
+    pub fn nullifier(&self) -> Nullifier {
+        Nullifier::from(poseidon_hash([self.secret.inner(), self.coin.inner()]))
+    }
+}
+
 impl Hash for OwnCoin {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.coin.inner().to_repr().hash(state);
@@ -112,15 +120,10 @@ impl Hash for OwnCoin {
 }
 
 pub fn compute_remainder_blind(
-    clear_inputs: &[crate::model::ClearInput],
     input_blinds: &[ScalarBlind],
     output_blinds: &[ScalarBlind],
-) -> ScalarBlind {
-    let mut total = pallas::Scalar::zero();
-
-    for input in clear_inputs {
-        total += input.value_blind.inner();
-    }
+) -> pallas::Scalar {
+    let mut total = pallas::Scalar::ZERO;
 
     for input_blind in input_blinds {
         total += input_blind.inner();

+ 2 - 5
src/contract/money/src/client/transfer_v1/builder.rs

@@ -22,9 +22,7 @@ use darkfi::{
 };
 use darkfi_sdk::{
     bridgetree,
-    crypto::{
-        note::AeadEncryptedNote, pasta_prelude::*, MerkleNode, Nullifier, SecretKey, TokenId,
-    },
+    crypto::{note::AeadEncryptedNote, pasta_prelude::*, MerkleNode, SecretKey, TokenId},
     pasta::pallas,
 };
 use log::{debug, info};
@@ -122,7 +120,7 @@ impl TransferCallBuilder {
 
         for (i, output) in self.outputs.iter().enumerate() {
             let value_blind = if i == self.outputs.len() - 1 {
-                compute_remainder_blind(&[], &input_blinds, &output_blinds)
+                compute_remainder_blind(&input_blinds, &output_blinds)
             } else {
                 Blind::random(&mut OsRng)
             };
@@ -202,7 +200,6 @@ impl TransferCallSecrets {
                 coin: output.coin,
                 note: output_note.clone(),
                 secret: SecretKey::from(pallas::Base::ZERO),
-                nullifier: Nullifier::from(pallas::Base::ZERO),
                 leaf_position: 0.into(),
             });
         }

+ 1 - 1
src/contract/test-harness/Cargo.toml

@@ -9,13 +9,13 @@ edition = "2021"
 darkfi = {path = "../../../", features = ["validator"]}
 darkfi-sdk = {path = "../../../src/sdk"}
 darkfi-serial = {path = "../../../src/serial", features = ["crypto"]}
+
 darkfi_dao_contract = {path = "../dao", features = ["client", "no-entrypoint"]}
 darkfi_money_contract = {path = "../money", features = ["client", "no-entrypoint"]}
 darkfi_deployooor_contract = {path = "../deployooor", features = ["client", "no-entrypoint"]}
 
 num-bigint = "0.4.4"
 blake3 = "1.5.0"
-bs58 = "0.5.0"
 log = "0.4.20"
 rand = "0.8.5"
 simplelog = "0.12.1"

+ 0 - 74
src/contract/test-harness/src/benchmarks.rs

@@ -1,74 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2024 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 log::info;
-use std::time::Duration;
-
-use crate::TxAction;
-
-/// Auxiliary struct to calculate transaction actions benchmarks
-pub struct TxActionBenchmarks {
-    /// Vector holding each transaction size in Bytes
-    pub sizes: Vec<usize>,
-    /// Vector holding each transaction broadcasted size in Bytes
-    pub broadcasted_sizes: Vec<usize>,
-    /// Vector holding each transaction creation time
-    pub creation_times: Vec<Duration>,
-    /// Vector holding each transaction verify time
-    pub verify_times: Vec<Duration>,
-}
-
-impl TxActionBenchmarks {
-    pub fn new() -> Self {
-        Self {
-            sizes: vec![],
-            broadcasted_sizes: vec![],
-            creation_times: vec![],
-            verify_times: vec![],
-        }
-    }
-
-    pub fn statistics(&self, action: &TxAction) {
-        if !self.sizes.is_empty() {
-            let avg = self.sizes.iter().sum::<usize>();
-            let avg = avg / self.sizes.len();
-            info!("Average {:?} size: {:?} Bytes", action, avg);
-        }
-        if !self.broadcasted_sizes.is_empty() {
-            let avg = self.broadcasted_sizes.iter().sum::<usize>();
-            let avg = avg / self.broadcasted_sizes.len();
-            info!("Average {:?} broadcasted size: {:?} Bytes", action, avg);
-        }
-        if !self.creation_times.is_empty() {
-            let avg = self.creation_times.iter().sum::<Duration>();
-            let avg = avg / self.creation_times.len() as u32;
-            info!("Average {:?} creation time: {:?}", action, avg);
-        }
-        if !self.verify_times.is_empty() {
-            let avg = self.verify_times.iter().sum::<Duration>();
-            let avg = avg / self.verify_times.len() as u32;
-            info!("Average {:?} verification time: {:?}", action, avg);
-        }
-    }
-}
-
-impl Default for TxActionBenchmarks {
-    fn default() -> Self {
-        Self::new()
-    }
-}

+ 89 - 11
src/contract/test-harness/src/contract_deploy.rs

@@ -23,24 +23,38 @@ use darkfi::{
 use darkfi_deployooor_contract::{
     client::deploy_v1::DeployCallBuilder, DeployFunction, DEPLOY_CONTRACT_ZKAS_DERIVE_NS_V1,
 };
-use darkfi_sdk::{crypto::DEPLOYOOOR_CONTRACT_ID, deploy::DeployParamsV1, ContractCall};
-use darkfi_serial::Encodable;
+use darkfi_money_contract::{
+    client::{MoneyNote, OwnCoin},
+    model::MoneyFeeParamsV1,
+};
+use darkfi_sdk::{
+    crypto::{contract_id::DEPLOYOOOR_CONTRACT_ID, MerkleNode},
+    deploy::DeployParamsV1,
+    ContractCall,
+};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
 use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn deploy_contract(
+    /// Create a `Deployooor::Deploy` transaction with the given WASM bincode.
+    ///
+    /// Returns the [`Transaction`], and necessary parameters.
+    pub async fn deploy_contract(
         &mut self,
         holder: &Holder,
         wasm_bincode: Vec<u8>,
-    ) -> Result<(Transaction, DeployParamsV1)> {
+        block_height: u64,
+    ) -> Result<(Transaction, DeployParamsV1, Option<MoneyFeeParamsV1>)> {
         let wallet = self.holders.get(holder).unwrap();
         let deploy_keypair = wallet.contract_deploy_authority;
 
         let (derivecid_pk, derivecid_zkbin) =
             self.proving_keys.get(&DEPLOY_CONTRACT_ZKAS_DERIVE_NS_V1.to_string()).unwrap();
 
+        // Build the contract call
         let builder = DeployCallBuilder {
             deploy_keypair,
             wasm_bincode,
@@ -48,32 +62,96 @@ impl TestHarness {
             derivecid_zkbin: derivecid_zkbin.clone(),
             derivecid_pk: derivecid_pk.clone(),
         };
-
         let debris = builder.build()?;
 
+        // Encode the call
         let mut data = vec![DeployFunction::DeployV1 as u8];
-        debris.params.encode(&mut data)?;
+        debris.params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data };
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
+
+        // If we have tx fees enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&mut OsRng, &[deploy_keypair.secret])?;
+            tx.signatures = vec![sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[deploy_keypair.secret])?;
         tx.signatures = vec![sigs];
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, debris.params))
+        Ok((tx, debris.params, fee_params))
     }
 
+    /// Execute the transaction created by `deploy_contract()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_deploy_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
+        tx: Transaction,
         _params: &DeployParamsV1,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        if !append {
+            return Ok(vec![])
+        }
+
+        if let Some(ref fee_params) = fee_params {
+            if let Some(spent_coin) = wallet
+                .unspent_money_coins
+                .iter()
+                .find(|x| x.nullifier() == fee_params.input.nullifier)
+                .cloned()
+            {
+                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                wallet.unspent_money_coins.retain(|x| x.nullifier() != fee_params.input.nullifier);
+                wallet.spent_money_coins.push(spent_coin.clone());
+            }
+
+            wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
+
+            let Ok(note) = fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+            else {
+                return Ok(vec![])
+            };
+
+            let owncoin = OwnCoin {
+                coin: fee_params.output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            return Ok(vec![owncoin])
+        }
 
-        Ok(())
+        Ok(vec![])
     }
 }

+ 120 - 57
src/contract/test-harness/src/dao_exec.rs

@@ -16,9 +16,11 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
-use darkfi::{tx::Transaction, Result};
+use darkfi::{
+    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    zk::halo2::Field,
+    Result,
+};
 use darkfi_dao_contract::{
     client::{DaoAuthMoneyTransferCall, DaoExecCall},
     model::{Dao, DaoBulla, DaoExecParams, DaoProposal},
@@ -26,27 +28,29 @@ use darkfi_dao_contract::{
     DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS, DAO_CONTRACT_ZKAS_DAO_EXEC_NS,
 };
 use darkfi_money_contract::{
-    client::transfer_v1 as xfer,
-    model::{CoinAttributes, MoneyTransferParamsV1},
+    client::{transfer_v1 as xfer, MoneyNote, OwnCoin},
+    model::{CoinAttributes, MoneyFeeParamsV1, MoneyTransferParamsV1},
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
-    crypto::{
-        pedersen_commitment_u64, Blind, FuncRef, MerkleNode, ScalarBlind, SecretKey,
-        DAO_CONTRACT_ID, MONEY_CONTRACT_ID,
-    },
-    dark_tree::DarkLeaf,
-    ContractCall,
+    contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
+    crypto::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
+    dark_tree::{DarkLeaf, DarkTree},
+    pasta::pallas,
+    pedersen_commitment_u64, ContractCall, FuncRef, MerkleNode, SecretKey,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
+    /// Create a `Dao::Exec` transaction.
     #[allow(clippy::too_many_arguments)]
-    pub fn dao_exec(
+    pub async fn dao_exec(
         &mut self,
+        holder: &Holder,
         dao: &Dao,
         dao_bulla: &DaoBulla,
         proposal: &DaoProposal,
@@ -55,7 +59,8 @@ impl TestHarness {
         all_vote_value: u64,
         yes_vote_blind: ScalarBlind,
         all_vote_blind: ScalarBlind,
-    ) -> Result<(Transaction, MoneyTransferParamsV1, DaoExecParams)> {
+        block_height: u64,
+    ) -> Result<(Transaction, MoneyTransferParamsV1, DaoExecParams, Option<MoneyFeeParamsV1>)> {
         let dao_wallet = self.holders.get(&Holder::Dao).unwrap();
 
         let (mint_pk, mint_zkbin) =
@@ -73,9 +78,6 @@ impl TestHarness {
             .get(&DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS.to_string())
             .unwrap();
 
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoExec).unwrap();
-        let timer = Instant::now();
-
         let input_user_data_blind = Blind::random(&mut OsRng);
         let exec_signature_secret = SecretKey::random(&mut OsRng);
 
@@ -139,7 +141,7 @@ impl TestHarness {
 
         let (xfer_params, xfer_secrets) = xfer_builder.build()?;
         let mut data = vec![MoneyFunction::TransferV1 as u8];
-        xfer_params.encode(&mut data)?;
+        xfer_params.encode_async(&mut data).await?;
         let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
         // We need to extract stuff from the inputs and outputs that we'll also
@@ -171,7 +173,7 @@ impl TestHarness {
 
         let (exec_params, exec_proofs) = exec_builder.make(dao_exec_zkbin, dao_exec_pk)?;
         let mut data = vec![DaoFunction::Exec as u8];
-        exec_params.encode(&mut data)?;
+        exec_params.encode_async(&mut data).await?;
         let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
 
         // Auth module
@@ -189,7 +191,7 @@ impl TestHarness {
             dao_auth_xfer_enc_coin_pk,
         )?;
         let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
-        auth_xfer_params.encode(&mut data)?;
+        auth_xfer_params.encode_async(&mut data).await?;
         let auth_xfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
 
         // We need to construct this tree, where exec is the parent:
@@ -199,61 +201,122 @@ impl TestHarness {
         //       xfer
         //
 
-        //let mut tx_builder = TransactionBuilder::new(
-        //    ContractCallLeaf { call: exec_call, proofs: exec_proofs },
-        //    vec![],
-        //)?;
-        //tx_builder
-        //    .append(ContractCallLeaf { call: auth_xfer_call, proofs: auth_xfer_proofs }, vec![])?;
-        //tx_builder
-        //let mut tx = tx_builder.build()?;
-
-        let mut tx = Transaction {
-            calls: vec![
-                DarkLeaf { data: auth_xfer_call, parent_index: Some(2), children_indexes: vec![] },
-                DarkLeaf { data: xfer_call, parent_index: Some(2), children_indexes: vec![] },
-                DarkLeaf { data: exec_call, parent_index: None, children_indexes: vec![0, 1] },
+        let mut tx_builder = TransactionBuilder::new(
+            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
+            vec![
+                DarkTree::new(
+                    ContractCallLeaf { call: auth_xfer_call, proofs: auth_xfer_proofs },
+                    vec![],
+                    None,
+                    None,
+                ),
+                DarkTree::new(
+                    ContractCallLeaf { call: xfer_call, proofs: xfer_secrets.proofs },
+                    vec![],
+                    None,
+                    None,
+                ),
             ],
-            proofs: vec![auth_xfer_proofs, xfer_secrets.proofs, exec_proofs],
-            signatures: vec![],
-        };
+        )?;
+
+        // If fees are enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let auth_xfer_sigs = vec![];
+            let xfer_sigs = tx.create_sigs(&mut OsRng, &xfer_secrets.signature_secrets)?;
+            let exec_sigs = tx.create_sigs(&mut OsRng, &[exec_signature_secret])?;
+            tx.signatures = vec![auth_xfer_sigs, xfer_sigs, exec_sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
+        let mut tx = tx_builder.build()?;
         let auth_xfer_sigs = vec![];
         let xfer_sigs = tx.create_sigs(&mut OsRng, &xfer_secrets.signature_secrets)?;
         let exec_sigs = tx.create_sigs(&mut OsRng, &[exec_signature_secret])?;
         tx.signatures = vec![auth_xfer_sigs, xfer_sigs, exec_sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
 
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, xfer_params, exec_params))
+        Ok((tx, xfer_params, exec_params, fee_params))
     }
 
-    pub async fn execute_dao_exec_tx(
+    /// Execute the transaction made by `dao_exec()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
+    #[allow(clippy::too_many_arguments)]
+    pub async fn excecute_dao_exec_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
+        tx: Transaction,
         xfer_params: &MoneyTransferParamsV1,
         _exec_params: &DaoExecParams,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoExec).unwrap();
-        let timer = Instant::now();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
 
-        for output in &xfer_params.outputs {
-            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+        if !append {
+            return Ok(vec![])
+        }
+
+        let mut inputs = xfer_params.inputs.to_vec();
+        let mut outputs = xfer_params.outputs.to_vec();
+
+        if let Some(ref fee_params) = fee_params {
+            inputs.push(fee_params.input.clone());
+            outputs.push(fee_params.output.clone());
+        }
+
+        for input in inputs {
+            if let Some(spent_coin) = wallet
+                .unspent_money_coins
+                .iter()
+                .find(|x| x.nullifier() == input.nullifier)
+                .cloned()
+            {
+                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                wallet.unspent_money_coins.retain(|x| x.nullifier() != input.nullifier);
+                wallet.spent_money_coins.push(spent_coin.clone());
+            }
         }
 
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+        let mut found_owncoins = vec![];
+        for output in outputs {
+            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+                continue
+            };
+
+            let owncoin = OwnCoin {
+                coin: output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            found_owncoins.push(owncoin);
+        }
 
-        Ok(())
+        Ok(found_owncoins)
     }
 }

+ 90 - 30
src/contract/test-harness/src/dao_mint.rs

@@ -16,79 +16,139 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     Result,
 };
 use darkfi_dao_contract::{
-    client,
+    client::make_mint_call,
     model::{Dao, DaoMintParams},
     DaoFunction, DAO_CONTRACT_ZKAS_DAO_MINT_NS,
 };
+use darkfi_money_contract::{
+    client::{MoneyNote, OwnCoin},
+    model::MoneyFeeParamsV1,
+};
 use darkfi_sdk::{
-    crypto::{Keypair, MerkleNode, DAO_CONTRACT_ID},
+    crypto::{contract_id::DAO_CONTRACT_ID, Keypair, MerkleNode},
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn dao_mint(
+    /// Create a `Dao::Mint` transaction with the given [`Dao`] info and a keypair.
+    /// Takes a [`Holder`] for optionally paying the transaction fee.
+    ///
+    /// Returns the [`Transaction`] and any relevant parameters.
+    pub async fn dao_mint(
         &mut self,
+        holder: &Holder,
         dao_info: &Dao,
         dao_kp: &Keypair,
-    ) -> Result<(Transaction, DaoMintParams)> {
+        block_height: u64,
+    ) -> Result<(Transaction, DaoMintParams, Option<MoneyFeeParamsV1>)> {
         let (dao_mint_pk, dao_mint_zkbin) =
             self.proving_keys.get(&DAO_CONTRACT_ZKAS_DAO_MINT_NS.to_string()).unwrap();
 
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoMint).unwrap();
-        let timer = Instant::now();
-
+        // Create the call
         let (params, proofs) =
-            client::make_mint_call(dao_info, &dao_kp.secret, dao_mint_zkbin, dao_mint_pk)?;
+            make_mint_call(dao_info, &dao_kp.secret, dao_mint_zkbin, dao_mint_pk)?;
 
+        // Encode the call
         let mut data = vec![DaoFunction::Mint as u8];
-        params.encode(&mut data)?;
+        params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
+
+        // If fees are enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&mut OsRng, &[dao_kp.secret])?;
+            tx.signatures = vec![sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[dao_kp.secret])?;
         tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, params))
+        Ok((tx, params, fee_params))
     }
 
+    /// Execute the transaction created by `dao_mint()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_dao_mint_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
+        tx: Transaction,
         params: &DaoMintParams,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoMint).unwrap();
-        let timer = Instant::now();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        if !append {
+            return Ok(vec![])
+        }
+
         wallet.dao_merkle_tree.append(MerkleNode::from(params.dao_bulla.inner()));
         let leaf_pos = wallet.dao_merkle_tree.mark().unwrap();
         wallet.dao_leafs.insert(params.dao_bulla, leaf_pos);
 
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+        if let Some(ref fee_params) = fee_params {
+            if let Some(spent_coin) = wallet
+                .unspent_money_coins
+                .iter()
+                .find(|x| x.nullifier() == fee_params.input.nullifier)
+                .cloned()
+            {
+                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                wallet.unspent_money_coins.retain(|x| x.nullifier() != fee_params.input.nullifier);
+                wallet.spent_money_coins.push(spent_coin.clone());
+            }
+
+            wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
+
+            let Ok(note) = fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+            else {
+                return Ok(vec![])
+            };
+
+            let owncoin = OwnCoin {
+                coin: fee_params.output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            return Ok(vec![owncoin])
+        }
 
-        Ok(())
+        Ok(vec![])
     }
 }

+ 94 - 35
src/contract/test-harness/src/dao_propose.rs

@@ -16,10 +16,9 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    zk::halo2::Field,
     Result,
 };
 use darkfi_dao_contract::{
@@ -28,37 +27,44 @@ use darkfi_dao_contract::{
     model::{Dao, DaoAuthCall, DaoBulla, DaoProposal, DaoProposeParams},
     DaoFunction, DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS,
 };
-use darkfi_money_contract::{client::OwnCoin, model::CoinAttributes, MoneyFunction};
+use darkfi_money_contract::{
+    client::{MoneyNote, OwnCoin},
+    model::{CoinAttributes, MoneyFeeParamsV1},
+    MoneyFunction,
+};
 use darkfi_sdk::{
-    crypto::{Blind, MerkleNode, SecretKey, DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
+    crypto::{
+        contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
+        Blind, MerkleNode, SecretKey, DAO_CONTRACT_ID, MONEY_CONTRACT_ID,
+    },
     pasta::pallas,
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn dao_propose(
+    /// Create a `Dao::Propose` transaction.
+    pub async fn dao_propose(
         &mut self,
         proposer: &Holder,
-        proposal_coinattrs: &Vec<CoinAttributes>,
+        proposal_coinattrs: &[CoinAttributes],
         user_data: pallas::Base,
         dao: &Dao,
         dao_bulla: &DaoBulla,
         block_height: u64,
-    ) -> Result<(Transaction, DaoProposeParams, DaoProposal)> {
+    ) -> Result<(Transaction, (DaoProposeParams, Option<MoneyFeeParamsV1>), DaoProposal)> {
         let wallet = self.holders.get(proposer).unwrap();
 
         let (dao_propose_burn_pk, dao_propose_burn_zkbin) =
             self.proving_keys.get(&DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS.to_string()).unwrap();
+
         let (dao_propose_main_pk, dao_propose_main_zkbin) =
             self.proving_keys.get(&DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS.to_string()).unwrap();
 
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoPropose).unwrap();
-        let timer = Instant::now();
-
         let propose_owncoin: OwnCoin = wallet
             .unspent_money_coins
             .iter()
@@ -67,6 +73,7 @@ impl TestHarness {
             .clone();
 
         let signature_secret = SecretKey::random(&mut OsRng);
+
         let input = DaoProposeStakeInput {
             secret: wallet.keypair.secret,
             note: propose_owncoin.note.clone(),
@@ -84,16 +91,17 @@ impl TestHarness {
             proposal_coins.push(coin_params.to_coin());
         }
         let mut proposal_data = vec![];
-        proposal_coins.encode(&mut proposal_data).unwrap();
+        proposal_coins.encode_async(&mut proposal_data).await?;
 
+        // Create Auth calls
         let auth_calls = vec![
             DaoAuthCall {
-                contract_id: DAO_CONTRACT_ID.inner(),
+                contract_id: *DAO_CONTRACT_ID,
                 function_code: DaoFunction::AuthMoneyTransfer as u8,
                 auth_data: proposal_data,
             },
             DaoAuthCall {
-                contract_id: MONEY_CONTRACT_ID.inner(),
+                contract_id: *MONEY_CONTRACT_ID,
                 function_code: MoneyFunction::TransferV1 as u8,
                 auth_data: vec![],
             },
@@ -128,47 +136,98 @@ impl TestHarness {
             dao_propose_main_pk,
         )?;
 
+        // Encode the call
         let mut data = vec![DaoFunction::Propose as u8];
-        params.encode(&mut data)?;
+        params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
+
+        // If fees are enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?;
+            tx.signatures = vec![sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(proposer, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?;
         tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, params, proposal))
+        Ok((tx, (params, fee_params), proposal))
     }
 
+    /// Execute the transaction created by `dao_propose()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_dao_propose_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
+        tx: Transaction,
         params: &DaoProposeParams,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoPropose).unwrap();
-        let timer = Instant::now();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
-        wallet.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        if !append {
+            return Ok(vec![])
+        }
 
+        wallet.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
         let prop_leaf_pos = wallet.dao_proposals_tree.mark().unwrap();
         let prop_money_snapshot = wallet.money_merkle_tree.clone();
-
         wallet.dao_prop_leafs.insert(params.proposal_bulla, (prop_leaf_pos, prop_money_snapshot));
 
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+        if let Some(ref fee_params) = fee_params {
+            if let Some(spent_coin) = wallet
+                .unspent_money_coins
+                .iter()
+                .find(|x| x.nullifier() == fee_params.input.nullifier)
+                .cloned()
+            {
+                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                wallet.unspent_money_coins.retain(|x| x.nullifier() != fee_params.input.nullifier);
+                wallet.spent_money_coins.push(spent_coin.clone());
+            }
+
+            wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
+
+            let Ok(note) = fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+            else {
+                return Ok(vec![])
+            };
+
+            let owncoin = OwnCoin {
+                coin: fee_params.output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}:", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            return Ok(vec![owncoin])
+        }
 
-        Ok(())
+        Ok(vec![])
     }
 }

+ 87 - 34
src/contract/test-harness/src/dao_vote.rs

@@ -2,7 +2,7 @@
  *
  * Copyright (C) 2020-2024 Dyne.org foundation
  *
- * This program is free software: you can redistribute it and/or modify
+ * This program is free software: you can redistributemoney 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.
@@ -16,8 +16,6 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     Result,
@@ -28,19 +26,24 @@ use darkfi_dao_contract::{
     model::{Dao, DaoProposal, DaoProposalBulla, DaoVoteParams},
     DaoFunction, DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS,
 };
-use darkfi_money_contract::client::OwnCoin;
+use darkfi_money_contract::{
+    client::{MoneyNote, OwnCoin},
+    model::MoneyFeeParamsV1,
+};
 use darkfi_sdk::{
-    crypto::{Keypair, SecretKey, DAO_CONTRACT_ID},
+    crypto::{contract_id::DAO_CONTRACT_ID, Keypair, MerkleNode, SecretKey},
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
+    /// Create a `Dao::Vote` transaction.
     #[allow(clippy::too_many_arguments)]
-    pub fn dao_vote(
+    pub async fn dao_vote(
         &mut self,
         voter: &Holder,
         vote_option: bool,
@@ -49,7 +52,7 @@ impl TestHarness {
         proposal: &DaoProposal,
         proposal_bulla: &DaoProposalBulla,
         block_height: u64,
-    ) -> Result<(Transaction, DaoVoteParams)> {
+    ) -> Result<(Transaction, DaoVoteParams, Option<MoneyFeeParamsV1>)> {
         let wallet = self.holders.get(voter).unwrap();
 
         let (dao_vote_burn_pk, dao_vote_burn_zkbin) =
@@ -58,10 +61,7 @@ impl TestHarness {
         let (dao_vote_main_pk, dao_vote_main_zkbin) =
             self.proving_keys.get(&DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS.to_string()).unwrap();
 
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoVote).unwrap();
-        let timer = Instant::now();
-
-        let (_proposal_leaf_pos, money_merkle_tree) =
+        let (_proposal_leaf_pos, snapshot_money_merkle_tree) =
             wallet.dao_prop_leafs.get(proposal_bulla).unwrap();
 
         let vote_owncoin: OwnCoin = wallet
@@ -72,11 +72,12 @@ impl TestHarness {
             .clone();
 
         let signature_secret = SecretKey::random(&mut OsRng);
+
         let input = DaoVoteInput {
             secret: wallet.keypair.secret,
             note: vote_owncoin.note.clone(),
             leaf_position: vote_owncoin.leaf_position,
-            merkle_path: money_merkle_tree.witness(vote_owncoin.leaf_position, 0).unwrap(),
+            merkle_path: snapshot_money_merkle_tree.witness(vote_owncoin.leaf_position, 0).unwrap(),
             signature_secret,
         };
 
@@ -97,41 +98,93 @@ impl TestHarness {
             dao_vote_main_pk,
         )?;
 
+        // Encode the call
         let mut data = vec![DaoFunction::Vote as u8];
-        params.encode(&mut data)?;
+        params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
+
+        // If fees are enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?;
+            tx.signatures = vec![sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(voter, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?;
         tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, params))
+        Ok((tx, params, fee_params))
     }
 
+    /// Execute the transaction made by `dao_vote()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_dao_vote_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
+        tx: Transaction,
         _params: &DaoVoteParams,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::DaoVote).unwrap();
-        let timer = Instant::now();
-
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
-
-        tx_action_benchmark.verify_times.push(timer.elapsed());
 
-        Ok(())
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        if !append {
+            return Ok(vec![])
+        }
+
+        if let Some(ref fee_params) = fee_params {
+            if let Some(spent_coin) = wallet
+                .unspent_money_coins
+                .iter()
+                .find(|x| x.nullifier() == fee_params.input.nullifier)
+                .cloned()
+            {
+                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                wallet.unspent_money_coins.retain(|x| x.nullifier() != fee_params.input.nullifier);
+                wallet.spent_money_coins.push(spent_coin.clone());
+            }
+
+            wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
+
+            let Ok(note) = fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+            else {
+                return Ok(vec![])
+            };
+
+            let owncoin = OwnCoin {
+                coin: fee_params.output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            return Ok(vec![owncoin])
+        }
+
+        Ok(vec![])
     }
 }

+ 101 - 257
src/contract/test-harness/src/lib.rs

@@ -16,14 +16,13 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashMap, io::Cursor, time::Instant};
+use std::{collections::HashMap, io::Cursor};
 
 use darkfi::{
     blockchain::BlockInfo,
-    tx::Transaction,
     util::{pcg::Pcg32, time::Timestamp},
     validator::{Validator, ValidatorConfig, ValidatorPtr},
-    zk::{empty_witnesses, ProvingKey, ZkCircuit},
+    zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit},
     zkas::ZkBinary,
     Result,
 };
@@ -34,33 +33,49 @@ use darkfi_money_contract::{
 };
 use darkfi_sdk::{
     bridgetree,
-    crypto::{
-        note::AeadEncryptedNote, pasta_prelude::Field, poseidon_hash, ContractId, Keypair,
-        MerkleNode, MerkleTree, Nullifier, SecretKey,
-    },
+    crypto::{Keypair, MerkleNode, MerkleTree},
     pasta::pallas,
 };
-use log::{info, warn};
+use log::debug;
 use num_bigint::BigUint;
-use rand::rngs::OsRng;
 
-mod benchmarks;
-use benchmarks::TxActionBenchmarks;
+/// Utility module for caching ZK proof PKs and VKs
 pub mod vks;
-use vks::{read_or_gen_vks_and_pks, Vks};
 
+/// `Money::PoWReward` functionality
+mod money_pow_reward;
+
+/// `Money::Fee` functionality
+mod money_fee;
+
+/// `Money::GenesisMint` functionality
+mod money_genesis_mint;
+
+/// `Money::Transfer` functionality
+mod money_transfer;
+
+/// `Money::TokenMint` functionality
+mod money_token;
+
+/// `Money::OtcSwap` functionality
+mod money_otc_swap;
+
+/// `Deployooor::Deploy` functionality
 mod contract_deploy;
-mod dao_exec;
+
+/// `Dao::Mint` functionality
 mod dao_mint;
+
+/// `Dao::Propose` functionality
 mod dao_propose;
+
+/// `Dao::Vote` functionality
 mod dao_vote;
-mod money_airdrop;
-mod money_genesis_mint;
-mod money_otc_swap;
-mod money_pow_reward;
-mod money_token;
-mod money_transfer;
 
+/// `Dao::Exec` functionality
+mod dao_exec;
+
+/// Initialize the logging mechanism
 pub fn init_logger() {
     let mut cfg = simplelog::ConfigBuilder::new();
     cfg.add_filter_ignore("sled".to_string());
@@ -78,118 +93,110 @@ pub fn init_logger() {
     )
     .is_err()
     {
-        warn!(target: "test_harness", "Logger already initialized");
+        debug!(target: "test_harness", "Logger initialized");
     }
 }
 
-/// Enum representing configured wallet holders
-#[derive(Debug, Eq, Hash, PartialEq)]
+/// Enum representing available wallet holders
+#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
 pub enum Holder {
-    Faucet,
     Alice,
     Bob,
     Charlie,
-    Rachel,
     Dao,
 }
 
-/// Enum representing transaction actions
-#[derive(Debug, Eq, Hash, PartialEq)]
-pub enum TxAction {
-    MoneyAirdrop,
-    MoneyTokenMint,
-    MoneyTokenFreeze,
-    MoneyGenesisMint,
-    MoneyTransfer,
-    MoneyOtcSwap,
-    MoneyPoWReward,
-    DaoMint,
-    DaoPropose,
-    DaoVote,
-    DaoExec,
-}
-
+/// Wallet instance for a single [`Holder`]
 pub struct Wallet {
+    /// Main holder keypair
     pub keypair: Keypair,
+    /// Keypair for arbitrary token minting
     pub token_mint_authority: Keypair,
-    pub token_blind: BaseBlind,
+    /// Keypair for arbitrary contract deployment
     pub contract_deploy_authority: Keypair,
+    /// Holder's [`Validator`] instance
     pub validator: ValidatorPtr,
+    /// Holder's instance of the Merkle tree for the `Money` contract
     pub money_merkle_tree: MerkleTree,
+    /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO bullas)
     pub dao_merkle_tree: MerkleTree,
+    /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO proposals)
     pub dao_proposals_tree: MerkleTree,
+    /// Holder's set of unspent [`OwnCoin`]s from the `Money` contract
     pub unspent_money_coins: Vec<OwnCoin>,
+    /// Holder's set of spent [`OwnCoin`]s from the `Money` contract
     pub spent_money_coins: Vec<OwnCoin>,
+    /// Witnessed leaf positions of DAO bullas in the `dao_merkle_tree`
     pub dao_leafs: HashMap<DaoBulla, bridgetree::Position>,
-    // Here the MerkleTree is the snapshotted Money tree at the time of proposal creation
+    /// Dao Proposal snapshots
     pub dao_prop_leafs: HashMap<DaoProposalBulla, (bridgetree::Position, MerkleTree)>,
 }
 
 impl Wallet {
+    /// Instantiate a new [`Wallet`] instance
     pub async fn new(
         keypair: Keypair,
-        genesis_block: &BlockInfo,
-        vks: &Vks,
+        token_mint_authority: Keypair,
+        contract_deploy_authority: Keypair,
+        genesis_block: BlockInfo,
+        vks: &vks::Vks,
         verify_fees: bool,
     ) -> Result<Self> {
+        // Create an in-memory sled db instance for this wallet
         let sled_db = sled::Config::new().temporary(true).open()?;
 
-        // Use pregenerated vks and get pregenerated pks
+        // Inject the cached VKs into the database
         vks::inject(&sled_db, vks)?;
 
-        // Generate validator
-        // NOTE: we are not using consensus constants here so we
-        // don't get circular dependencies.
-        let config = ValidatorConfig {
+        // Create the `Validator` instance
+        let validator_config = ValidatorConfig {
             finalization_threshold: 3,
             pow_target: 90,
             pow_fixed_difficulty: Some(BigUint::from(1_u8)),
-            genesis_block: genesis_block.clone(),
+            genesis_block,
             verify_fees,
         };
-        let validator = Validator::new(&sled_db, config).await?;
+        let validator = Validator::new(&sled_db, validator_config).await?;
 
-        // Create necessary Merkle trees for tracking
+        // The Merkle tree for the `Money` contract is initialized with a "null"
+        // leaf at position 0.
         let mut money_merkle_tree = MerkleTree::new(100);
         money_merkle_tree.append(MerkleNode::from(pallas::Base::ZERO));
-
-        let dao_merkle_tree = MerkleTree::new(100);
-        let dao_proposals_tree = MerkleTree::new(100);
-
-        let unspent_money_coins = vec![];
-        let spent_money_coins = vec![];
-
-        let token_mint_authority = Keypair::random(&mut OsRng);
-        let token_blind = Blind::random(&mut OsRng);
-        let contract_deploy_authority = Keypair::random(&mut OsRng);
+        money_merkle_tree.mark().unwrap();
 
         Ok(Self {
             keypair,
             token_mint_authority,
-            token_blind,
             contract_deploy_authority,
             validator,
             money_merkle_tree,
-            dao_merkle_tree,
-            dao_proposals_tree,
-            unspent_money_coins,
-            spent_money_coins,
+            dao_merkle_tree: MerkleTree::new(100),
+            dao_proposals_tree: MerkleTree::new(100),
+            unspent_money_coins: vec![],
+            spent_money_coins: vec![],
             dao_leafs: HashMap::new(),
             dao_prop_leafs: HashMap::new(),
         })
     }
 }
 
+/// Native contract test harness instance
 pub struct TestHarness {
+    /// Initialized [`Holder`]s for this instance
     pub holders: HashMap<Holder, Wallet>,
+    /// Cached [`ProvingKey`]s for native contract ZK proving
     pub proving_keys: HashMap<String, (ProvingKey, ZkBinary)>,
-    pub tx_action_benchmarks: HashMap<TxAction, TxActionBenchmarks>,
+    /// The genesis block for this harness
     pub genesis_block: BlockInfo,
+    /// Marker to know if we're supposed to include tx fees
+    pub verify_fees: bool,
 }
 
 impl TestHarness {
-    pub async fn new(_contracts: &[String], verify_fees: bool) -> Result<Self> {
-        let mut holders = HashMap::new();
+    /// Instantiate a new [`TestHarness`] given a slice of [`Holder`]s.
+    /// Additionally, a `verify_fees` boolean will enforce tx fee verification.
+    pub async fn new(holders: &[Holder], verify_fees: bool) -> Result<Self> {
+        // Create a genesis block
         let mut genesis_block = BlockInfo::default();
         genesis_block.header.timestamp = Timestamp(1689772567);
         let producer_tx = genesis_block.txs.pop().unwrap();
@@ -198,191 +205,41 @@ impl TestHarness {
         // Deterministic PRNG
         let mut rng = Pcg32::new(42);
 
-        // Build or read precompiled zk pks and vks
-        let (pks, vks) = read_or_gen_vks_and_pks()?;
-
+        // Build or read cached ZK PKs and VKs
+        let (pks, vks) = vks::get_cached_pks_and_vks()?;
         let mut proving_keys = HashMap::new();
         for (bincode, namespace, pk) in pks {
             let mut reader = Cursor::new(pk);
             let zkbin = ZkBinary::decode(&bincode)?;
             let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
-            let _pk = ProvingKey::read(&mut reader, circuit)?;
-            proving_keys.insert(namespace, (_pk, zkbin));
+            let proving_key = ProvingKey::read(&mut reader, circuit)?;
+            proving_keys.insert(namespace, (proving_key, zkbin));
         }
 
-        let faucet_kp = Keypair::random(&mut rng);
-        let faucet = Wallet::new(faucet_kp, &genesis_block, &vks, verify_fees).await?;
-        holders.insert(Holder::Faucet, faucet);
-
-        let alice_kp = Keypair::random(&mut rng);
-        let alice = Wallet::new(alice_kp, &genesis_block, &vks, verify_fees).await?;
-        holders.insert(Holder::Alice, alice);
-
-        let bob_kp = Keypair::random(&mut rng);
-        let bob = Wallet::new(bob_kp, &genesis_block, &vks, verify_fees).await?;
-        holders.insert(Holder::Bob, bob);
-
-        let charlie_kp = Keypair::random(&mut rng);
-        let charlie = Wallet::new(charlie_kp, &genesis_block, &vks, verify_fees).await?;
-        holders.insert(Holder::Charlie, charlie);
-
-        let rachel_kp = Keypair::random(&mut rng);
-        let rachel = Wallet::new(rachel_kp, &genesis_block, &vks, verify_fees).await?;
-        holders.insert(Holder::Rachel, rachel);
-
-        let dao_kp = Keypair::random(&mut rng);
-        let dao = Wallet::new(dao_kp, &genesis_block, &vks, verify_fees).await?;
-        holders.insert(Holder::Dao, dao);
-
-        // Build benchmarks map
-        let mut tx_action_benchmarks = HashMap::new();
-        tx_action_benchmarks.insert(TxAction::MoneyAirdrop, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::MoneyTokenMint, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::MoneyTokenFreeze, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::MoneyGenesisMint, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::MoneyOtcSwap, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::MoneyTransfer, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::MoneyPoWReward, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::DaoMint, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::DaoPropose, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::DaoVote, TxActionBenchmarks::default());
-        tx_action_benchmarks.insert(TxAction::DaoExec, TxActionBenchmarks::default());
-
-        Ok(Self { holders, proving_keys, tx_action_benchmarks, genesis_block })
-    }
-
-    pub async fn execute_erroneous_txs(
-        &mut self,
-        action: TxAction,
-        holder: &Holder,
-        txs: &[Transaction],
-        block_height: u64,
-        erroneous: usize,
-    ) -> Result<()> {
-        let wallet = self.holders.get(holder).unwrap();
-        let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&action).unwrap();
-        let timer = Instant::now();
-
-        let erroneous_txs = wallet
-            .validator
-            .add_transactions(txs, block_height, false)
-            .await
-            .err()
-            .unwrap()
-            .retrieve_erroneous_txs()?;
-        assert_eq!(erroneous_txs.len(), erroneous);
-        tx_action_benchmark.verify_times.push(timer.elapsed());
-
-        Ok(())
-    }
-
-    pub fn gather_owncoin(
-        &mut self,
-        holder: &Holder,
-        coin: &Coin,
-        note: &AeadEncryptedNote,
-        secret_key: Option<SecretKey>,
-    ) -> Result<OwnCoin> {
-        let wallet = self.holders.get_mut(holder).unwrap();
-        let leaf_position = wallet.money_merkle_tree.mark().unwrap();
-        let secret_key = match secret_key {
-            Some(key) => key,
-            None => wallet.keypair.secret,
-        };
-
-        let note: MoneyNote = note.decrypt(&secret_key)?;
-        let oc = OwnCoin {
-            coin: *coin,
-            note: note.clone(),
-            secret: secret_key,
-            nullifier: Nullifier::from(poseidon_hash([
-                wallet.keypair.secret.inner(),
-                coin.inner(),
-            ])),
-            leaf_position,
-        };
-
-        wallet.unspent_money_coins.push(oc.clone());
-
-        Ok(oc)
-    }
-
-    pub fn gather_owncoin_from_output(
-        &mut self,
-        holder: &Holder,
-        output: &Output,
-        secret_key: Option<SecretKey>,
-    ) -> Result<OwnCoin> {
-        self.gather_owncoin(holder, &output.coin, &output.note, secret_key)
-    }
-
-    /// This should be used after transfer call, so we can mark the merkle tree
-    /// before each output coin. Assumes using wallet secret key.
-    pub fn gather_multiple_owncoins(
-        &mut self,
-        holder: &Holder,
-        outputs: &[Output],
-    ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
-        let secret_key = wallet.keypair.secret;
-        let mut owncoins = vec![];
-        for output in outputs {
-            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
-            let leaf_position = wallet.money_merkle_tree.mark().unwrap();
-
-            let note: MoneyNote = output.note.decrypt(&secret_key)?;
-            let oc = OwnCoin {
-                coin: output.coin,
-                note: note.clone(),
-                secret: secret_key,
-                nullifier: Nullifier::from(poseidon_hash([
-                    wallet.keypair.secret.inner(),
-                    output.coin.inner(),
-                ])),
-                leaf_position,
-            };
-
-            wallet.unspent_money_coins.push(oc.clone());
-            owncoins.push(oc);
-        }
-
-        Ok(owncoins)
-    }
-
-    pub fn gather_owncoin_at_index(
-        &mut self,
-        holder: &Holder,
-        outputs: &[Output],
-        index: usize,
-    ) -> Result<OwnCoin> {
-        let wallet = self.holders.get_mut(holder).unwrap();
-        let secret_key = wallet.keypair.secret;
-        let mut owncoin = None;
-        for (i, output) in outputs.iter().enumerate() {
-            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
-            if i == index {
-                let leaf_position = wallet.money_merkle_tree.mark().unwrap();
-
-                let note: MoneyNote = output.note.decrypt(&secret_key)?;
-                let oc = OwnCoin {
-                    coin: output.coin,
-                    note: note.clone(),
-                    secret: secret_key,
-                    nullifier: Nullifier::from(poseidon_hash([
-                        wallet.keypair.secret.inner(),
-                        output.coin.inner(),
-                    ])),
-                    leaf_position,
-                };
-
-                wallet.unspent_money_coins.push(oc.clone());
-                owncoin = Some(oc);
-            }
+        // Create `Wallet` instances
+        let mut holders_map = HashMap::new();
+        for holder in holders {
+            let keypair = Keypair::random(&mut rng);
+            let token_mint_authority = Keypair::random(&mut rng);
+            let contract_deploy_authority = Keypair::random(&mut rng);
+
+            let wallet = Wallet::new(
+                keypair,
+                token_mint_authority,
+                contract_deploy_authority,
+                genesis_block.clone(),
+                &vks,
+                verify_fees,
+            )
+            .await?;
+
+            holders_map.insert(*holder, wallet);
         }
 
-        Ok(owncoin.unwrap())
+        Ok(Self { holders: holders_map, proving_keys, genesis_block, verify_fees })
     }
 
+    /// Assert that all holders' trees are the same
     pub fn assert_trees(&self, holders: &[Holder]) {
         assert!(holders.len() > 1);
         // Gather wallets
@@ -397,17 +254,4 @@ impl TestHarness {
             assert!(money_root == wallet.money_merkle_tree.root(0).unwrap());
         }
     }
-
-    pub fn contract_id(&self, holder: &Holder) -> ContractId {
-        let holder = self.holders.get(holder).unwrap();
-        ContractId::derive_public(holder.contract_deploy_authority.public)
-    }
-
-    pub fn statistics(&self) {
-        info!("==================== Statistics ====================");
-        for (action, tx_action_benchmark) in &self.tx_action_benchmarks {
-            tx_action_benchmark.statistics(action);
-        }
-        info!("====================================================");
-    }
 }

+ 317 - 0
src/contract/test-harness/src/money_fee.rs

@@ -0,0 +1,317 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 std::{collections::HashSet, hash::RandomState};
+
+use darkfi::{
+    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    zk::{halo2::Field, Proof},
+    Result,
+};
+use darkfi_money_contract::{
+    client::{
+        compute_remainder_blind,
+        fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS},
+        MoneyNote, OwnCoin,
+    },
+    model::{Input, MoneyFeeParamsV1, Output},
+    MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{
+        contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, token_id::DARK_TOKEN_ID, FuncId,
+        MerkleNode, SecretKey,
+    },
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::AsyncEncodable;
+use log::{debug, info};
+use rand::rngs::OsRng;
+
+use super::{Holder, TestHarness};
+
+impl TestHarness {
+    /// Create an empty transaction that includes a `Money::Fee` call.
+    /// This is generally used to test the actual fee call, and also to
+    /// see the gas usage of the call without other parts.
+    pub async fn create_empty_fee_call(
+        &mut self,
+        holder: &Holder,
+    ) -> Result<(Transaction, MoneyFeeParamsV1)> {
+        let wallet = self.holders.get(holder).unwrap();
+
+        // Find a compatible OwnCoin
+        let coin = wallet
+            .unspent_money_coins
+            .iter()
+            .find(|x| x.note.token_id == *DARK_TOKEN_ID && x.note.value > FEE_CALL_GAS)
+            .unwrap();
+
+        // Input and output setup
+        let input = FeeCallInput {
+            leaf_position: coin.leaf_position,
+            merkle_path: wallet.money_merkle_tree.witness(coin.leaf_position, 0).unwrap(),
+            secret: coin.secret,
+            note: coin.note.clone(),
+            user_data_blind: pallas::Base::random(&mut OsRng),
+        };
+
+        let output = FeeCallOutput {
+            public_key: wallet.keypair.public,
+            value: coin.note.value - FEE_CALL_GAS,
+            token_id: coin.note.token_id,
+            blind: pallas::Base::random(&mut OsRng),
+            spend_hook: FuncId::none(),
+            user_data: pallas::Base::ZERO,
+        };
+
+        // Generate blinding factors
+        let token_blind = pallas::Base::random(&mut OsRng);
+        let input_value_blind = pallas::Scalar::random(&mut OsRng);
+        let fee_value_blind = pallas::Scalar::random(&mut OsRng);
+        let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
+
+        // Generate an ephemeral signing key
+        let signature_secret = SecretKey::random(&mut OsRng);
+
+        info!("Creting FeeV1 ZK proof");
+        let (fee_pk, fee_zkbin) =
+            self.proving_keys.get(&MONEY_CONTRACT_ZKAS_FEE_NS_V1.to_string()).unwrap();
+
+        let (proof, public_inputs) = create_fee_proof(
+            fee_zkbin,
+            fee_pk,
+            &input,
+            input_value_blind,
+            &output,
+            output_value_blind,
+            output.spend_hook,
+            output.user_data,
+            output.blind,
+            token_blind,
+            signature_secret,
+        )?;
+
+        // Encrypted note for the output
+        let note = MoneyNote {
+            coin_blind: output.blind,
+            value: output.value,
+            token_id: output.token_id,
+            spend_hook: output.spend_hook,
+            user_data: output.user_data,
+            value_blind: output_value_blind,
+            token_blind,
+            memo: vec![],
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let params = MoneyFeeParamsV1 {
+            input: Input {
+                value_commit: public_inputs.input_value_commit,
+                token_commit: public_inputs.token_commit,
+                nullifier: public_inputs.nullifier,
+                merkle_root: public_inputs.merkle_root,
+                user_data_enc: public_inputs.input_user_data_enc,
+                signature_public: public_inputs.signature_public,
+            },
+            output: Output {
+                value_commit: public_inputs.output_value_commit,
+                token_commit: public_inputs.token_commit,
+                coin: public_inputs.output_coin,
+                note: encrypted_note,
+            },
+            fee_value_blind,
+            token_blind,
+        };
+
+        let mut data = vec![MoneyFunction::FeeV1 as u8];
+        FEE_CALL_GAS.encode_async(&mut data).await?;
+        params.encode_async(&mut data).await?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+        let mut tx_builder =
+            TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![proof] }, vec![])?;
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?;
+        tx.signatures = vec![sigs];
+
+        Ok((tx, params))
+    }
+
+    /// Execute the transaction created by `create_empty_fee_call()` for a given [`Holder`]
+    ///
+    /// Returns any found [`OwnCoin`]s.
+    pub async fn execute_empty_fee_call_tx(
+        &mut self,
+        holder: &Holder,
+        tx: Transaction,
+        params: &MoneyFeeParamsV1,
+        block_height: u64,
+    ) -> Result<Vec<OwnCoin>> {
+        let wallet = self.holders.get_mut(holder).unwrap();
+
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+        wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
+
+        // Attempt to decrypt the output note to see if this is a coin for the holder
+        let Ok(note) = params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+            return Ok(vec![])
+        };
+
+        let owncoin = OwnCoin {
+            coin: params.output.coin,
+            note: note.clone(),
+            secret: wallet.keypair.secret,
+            leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+        };
+
+        let spent_coin = wallet
+            .unspent_money_coins
+            .iter()
+            .find(|x| x.nullifier() == params.input.nullifier)
+            .unwrap()
+            .clone();
+
+        debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+        debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+
+        wallet.unspent_money_coins.retain(|x| x.nullifier() != params.input.nullifier);
+        wallet.spent_money_coins.push(spent_coin);
+        wallet.unspent_money_coins.push(owncoin.clone());
+
+        Ok(vec![owncoin])
+    }
+
+    /// Create and append a `Money::Fee` call to a given [`Transaction`] for
+    /// a given [`Holder`].
+    ///
+    /// Additionally takes a set of spent coins in order not to reuse them here.
+    ///
+    /// Returns the `Fee` call, and all necessary data and parameters related.
+    pub async fn append_fee_call(
+        &mut self,
+        holder: &Holder,
+        tx: Transaction,
+        block_height: u64,
+        spent_coins: &[OwnCoin],
+    ) -> Result<(ContractCall, Vec<Proof>, Vec<SecretKey>, Vec<OwnCoin>, MoneyFeeParamsV1)> {
+        // First we verify the fee-less transaction to see how much gas it uses for execution
+        // and verification.
+        let wallet = self.holders.get(holder).unwrap();
+        let mut gas_used = FEE_CALL_GAS;
+        gas_used += wallet.validator.add_transactions(&[tx], block_height, false, false).await?;
+
+        // Knowing the total gas, we can now find an OwnCoin of enough value
+        // so that we can create a valid Money::Fee call.
+        let spent_coins: HashSet<&OwnCoin, RandomState> = HashSet::from_iter(spent_coins);
+        let mut available_coins = wallet.unspent_money_coins.clone();
+        available_coins.retain(|x| x.note.token_id == *DARK_TOKEN_ID && x.note.value > gas_used);
+        available_coins.retain(|x| !spent_coins.contains(x));
+        assert!(!available_coins.is_empty());
+
+        let coin = &available_coins[0];
+        let change_value = coin.note.value - gas_used;
+
+        // Input and output setup
+        let input = FeeCallInput {
+            leaf_position: coin.leaf_position,
+            merkle_path: wallet.money_merkle_tree.witness(coin.leaf_position, 0).unwrap(),
+            secret: coin.secret,
+            note: coin.note.clone(),
+            user_data_blind: pallas::Base::random(&mut OsRng),
+        };
+
+        let output = FeeCallOutput {
+            public_key: wallet.keypair.public,
+            value: change_value,
+            token_id: coin.note.token_id,
+            blind: pallas::Base::random(&mut OsRng),
+            spend_hook: FuncId::none(),
+            user_data: pallas::Base::ZERO,
+        };
+
+        // Create blinding factors
+        let token_blind = pallas::Base::random(&mut OsRng);
+        let input_value_blind = pallas::Scalar::random(&mut OsRng);
+        let fee_value_blind = pallas::Scalar::random(&mut OsRng);
+        let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
+
+        // Create an ephemeral signing key
+        let signature_secret = SecretKey::random(&mut OsRng);
+
+        info!("Creating FeeV1 ZK proof");
+        let (fee_pk, fee_zkbin) =
+            self.proving_keys.get(&MONEY_CONTRACT_ZKAS_FEE_NS_V1.to_string()).unwrap();
+
+        let (proof, public_inputs) = create_fee_proof(
+            fee_zkbin,
+            fee_pk,
+            &input,
+            input_value_blind,
+            &output,
+            output_value_blind,
+            output.spend_hook,
+            output.user_data,
+            output.blind,
+            token_blind,
+            signature_secret,
+        )?;
+
+        // Encrypted note for the output
+        let note = MoneyNote {
+            coin_blind: output.blind,
+            value: output.value,
+            token_id: output.token_id,
+            spend_hook: output.spend_hook,
+            user_data: output.user_data,
+            value_blind: output_value_blind,
+            token_blind,
+            memo: vec![],
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let params = MoneyFeeParamsV1 {
+            input: Input {
+                value_commit: public_inputs.input_value_commit,
+                token_commit: public_inputs.token_commit,
+                nullifier: public_inputs.nullifier,
+                merkle_root: public_inputs.merkle_root,
+                user_data_enc: public_inputs.input_user_data_enc,
+                signature_public: public_inputs.signature_public,
+            },
+            output: Output {
+                value_commit: public_inputs.output_value_commit,
+                token_commit: public_inputs.token_commit,
+                coin: public_inputs.output_coin,
+                note: encrypted_note,
+            },
+            fee_value_blind,
+            token_blind,
+        };
+
+        // Encode the contract call
+        let mut data = vec![MoneyFunction::FeeV1 as u8];
+        gas_used.encode_async(&mut data).await?;
+        params.encode_async(&mut data).await?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        Ok((call, vec![proof], vec![signature_secret], vec![coin.clone()], params))
+    }
+}

+ 44 - 36
src/contract/test-harness/src/money_genesis_mint.rs

@@ -16,28 +16,32 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    zk::halo2::Field,
     Result,
 };
 use darkfi_money_contract::{
-    client::genesis_mint_v1::GenesisMintCallBuilder, model::MoneyGenesisMintParamsV1,
+    client::{genesis_mint_v1::GenesisMintCallBuilder, MoneyNote, OwnCoin},
+    model::MoneyGenesisMintParamsV1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
-    crypto::{FuncId, MerkleNode, MONEY_CONTRACT_ID},
+    crypto::{contract_id::MONEY_CONTRACT_ID, FuncId, MerkleNode},
     pasta::pallas,
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn genesis_mint(
+    /// Create a `Money::GenesisMint` transaction for a given [`Holder`].
+    ///
+    /// Returns the created [`Transaction`] and its parameters.
+    pub async fn genesis_mint(
         &mut self,
         holder: &Holder,
         amount: u64,
@@ -47,63 +51,67 @@ impl TestHarness {
         let (mint_pk, mint_zkbin) =
             self.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string()).unwrap();
 
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyGenesisMint).unwrap();
-
-        let timer = Instant::now();
-
-        // We're just going to be using a zero spend-hook and user-data
-        let spend_hook = FuncId::none();
-        let user_data = pallas::Base::zero();
-
+        // Build the contract call
         let builder = GenesisMintCallBuilder {
             keypair: wallet.keypair,
             amount,
-            spend_hook,
-            user_data,
+            spend_hook: FuncId::none(),
+            user_data: pallas::Base::ZERO,
             mint_zkbin: mint_zkbin.clone(),
             mint_pk: mint_pk.clone(),
         };
 
         let debris = builder.build()?;
 
+        // Encode and build the transaction
         let mut data = vec![MoneyFunction::GenesisMintV1 as u8];
-        debris.params.encode(&mut data)?;
+        debris.params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[wallet.keypair.secret])?;
         tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
 
         Ok((tx, debris.params))
     }
 
+    /// Execute the [`Transaction`] created by `genesis_mint()`.
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_genesis_mint_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
+        tx: Transaction,
         params: &MoneyGenesisMintParamsV1,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyGenesisMint).unwrap();
-        let timer = Instant::now();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        if !append {
+            return Ok(vec![])
+        }
+
         wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
-        tx_action_benchmark.verify_times.push(timer.elapsed());
 
-        Ok(())
+        let Ok(note) = params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+            return Ok(vec![])
+        };
+
+        let owncoin = OwnCoin {
+            coin: params.output.coin,
+            note: note.clone(),
+            secret: wallet.keypair.secret,
+            leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+        };
+
+        debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+        wallet.unspent_money_coins.push(owncoin.clone());
+
+        Ok(vec![owncoin])
     }
 }

+ 104 - 48
src/contract/test-harness/src/money_otc_swap.rs

@@ -16,36 +16,39 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     zk::halo2::Field,
     Result,
 };
 use darkfi_money_contract::{
-    client::{swap_v1::SwapCallBuilder, OwnCoin},
-    model::MoneyTransferParamsV1,
+    client::{swap_v1::SwapCallBuilder, MoneyNote, OwnCoin},
+    model::{MoneyFeeParamsV1, MoneyTransferParamsV1},
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
-    crypto::{Blind, FuncId, MerkleNode, MONEY_CONTRACT_ID},
+    crypto::{contract_id::MONEY_CONTRACT_ID, Blind, FuncId, MerkleNode, MONEY_CONTRACT_ID},
     pasta::pallas,
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn otc_swap(
+    /// Create a `Money::OtcSwap` transaction with two given [`Holder`]s.
+    ///
+    /// Returns the [`Transaction`], and the transaction parameters.
+    pub async fn otc_swap(
         &mut self,
         holder0: &Holder,
         owncoin0: &OwnCoin,
         holder1: &Holder,
         owncoin1: &OwnCoin,
-    ) -> Result<(Transaction, MoneyTransferParamsV1)> {
+        block_height: u64,
+    ) -> Result<(Transaction, MoneyTransferParamsV1, Option<MoneyFeeParamsV1>)> {
         let wallet0 = self.holders.get(holder0).unwrap();
         let wallet1 = self.holders.get(holder1).unwrap();
 
@@ -55,23 +58,18 @@ impl TestHarness {
         let (burn_pk, burn_zkbin) =
             self.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string()).unwrap();
 
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyOtcSwap).unwrap();
-
-        let timer = Instant::now();
-
-        // We're just going to be using a zero spend-hook and user-data
+        // Use a zero spend_hook and user_data
         let rcpt_spend_hook = FuncId::none();
         let rcpt_user_data = pallas::Base::ZERO;
         let rcpt_user_data_blind = Blind::random(&mut OsRng);
 
-        // Generating  swap blinds
+        // Create blinding factors for commitments
         let value_send_blind = Blind::random(&mut OsRng);
         let value_recv_blind = Blind::random(&mut OsRng);
-        let token_send_blind = Blind::random(&mut OsRng);
-        let token_recv_blind = Blind::random(&mut OsRng);
+        let token_send_blind = BaseBlind::random(&mut OsRng);
+        let token_recv_blind = BaseBlind::random(&mut OsRng);
 
-        // Builder first holder part
+        // Build the first half of the swap for Holder0
         let builder = SwapCallBuilder {
             pubkey: wallet0.keypair.public,
             value_send: owncoin0.note.value,
@@ -92,11 +90,10 @@ impl TestHarness {
         };
 
         let debris0 = builder.build()?;
-
         assert!(debris0.params.inputs.len() == 1);
         assert!(debris0.params.outputs.len() == 1);
 
-        // Builder second holder part
+        // Build the second half of the swap for Holder1
         let builder = SwapCallBuilder {
             pubkey: wallet1.keypair.public,
             value_send: owncoin1.note.value,
@@ -117,11 +114,10 @@ impl TestHarness {
         };
 
         let debris1 = builder.build()?;
-
         assert!(debris1.params.inputs.len() == 1);
         assert!(debris1.params.outputs.len() == 1);
 
-        // Then second holder combines the halves
+        // Holder1 then combines the halves
         let swap_full_params = MoneyTransferParamsV1 {
             inputs: vec![debris0.params.inputs[0].clone(), debris1.params.inputs[0].clone()],
             outputs: vec![debris0.params.outputs[0].clone(), debris1.params.outputs[0].clone()],
@@ -134,53 +130,113 @@ impl TestHarness {
             debris1.proofs[1].clone(),
         ];
 
-        // And signs the transaction
+        // Encode the contract call
         let mut data = vec![MoneyFunction::OtcSwapV1 as u8];
-        swap_full_params.encode(&mut data)?;
+        swap_full_params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: swap_full_proofs }, vec![])?;
+
+        // If we have tx fees enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&mut OsRng, &[debris1.signature_secret])?;
+            tx.signatures = vec![sigs];
+
+            // First holder gets the partially signed transaction and adds their signature
+            let sigs = tx.create_sigs(&mut OsRng, &[debris0.signature_secret])?;
+            tx.signatures[0].insert(0, sigs[0]);
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder0, tx, block_height, &[owncoin0.clone()]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[debris1.signature_secret])?;
         tx.signatures = vec![sigs];
-
         // First holder gets the partially signed transaction and adds their signature
         let sigs = tx.create_sigs(&mut OsRng, &[debris0.signature_secret])?;
         tx.signatures[0].insert(0, sigs[0]);
-        tx_action_benchmark.creation_times.push(timer.elapsed());
 
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, swap_full_params))
+        Ok((tx, swap_full_params, fee_params))
     }
 
+    /// Execute the transaction created by `otc_swap()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_otc_swap_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
-        params: &MoneyTransferParamsV1,
+        tx: Transaction,
+        swap_params: &MoneyTransferParamsV1,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
         append: bool,
-    ) -> Result<()> {
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyOtcSwap).unwrap();
-        let timer = Instant::now();
-
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
-        if append {
-            for output in &params.outputs {
-                wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        let mut found_owncoins = vec![];
+
+        if !append {
+            return Ok(found_owncoins)
+        }
+
+        let mut inputs = swap_params.inputs.to_vec();
+        let mut outputs = swap_params.outputs.to_vec();
+        if let Some(ref fee_params) = fee_params {
+            inputs.push(fee_params.input.clone());
+            outputs.push(fee_params.output.clone());
+        }
+
+        for input in inputs {
+            if let Some(spent_coin) = wallet
+                .unspent_money_coins
+                .iter()
+                .find(|x| x.nullifier() == input.nullifier)
+                .cloned()
+            {
+                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                wallet.unspent_money_coins.retain(|x| x.nullifier() != input.nullifier);
+                wallet.spent_money_coins.push(spent_coin.clone());
             }
         }
-        tx_action_benchmark.verify_times.push(timer.elapsed());
 
-        Ok(())
+        for output in outputs {
+            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+            // Attempt to decrypt the encrypted note
+            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+                continue
+            };
+
+            let owncoin = OwnCoin {
+                coin: output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            found_owncoins.push(owncoin);
+        }
+
+        Ok(found_owncoins)
     }
 }

+ 40 - 62
src/contract/test-harness/src/money_pow_reward.rs

@@ -16,32 +16,35 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    zk::halo2::Field,
     Result,
 };
 use darkfi_money_contract::{
-    client::pow_reward_v1::PoWRewardCallBuilder, model::MoneyPoWRewardParamsV1, MoneyFunction,
-    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+    client::{pow_reward_v1::PoWRewardCallBuilder, MoneyNote, OwnCoin},
+    model::MoneyPoWRewardParamsV1,
+    MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
-    crypto::{FuncId, MerkleNode, MONEY_CONTRACT_ID},
+    crypto::{contract_id::MONEY_CONTRACT_ID, FuncId, MerkleNode},
     pasta::pallas,
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn pow_reward(
+    /// Create a `Money::PoWReward` transaction for a given [`Holder`].
+    ///
+    /// Optionally takes a specific reward recipient and a nonstandard reward value.
+    /// Returns the created [`Transaction`] and [`MoneyPoWRewardParamsV1`].
+    pub async fn pow_reward(
         &mut self,
         holder: &Holder,
         recipient: Option<&Holder>,
-        block_height: u64,
         reward: Option<u64>,
     ) -> Result<(Transaction, MoneyPoWRewardParamsV1)> {
         let wallet = self.holders.get(holder).unwrap();
@@ -49,34 +52,26 @@ impl TestHarness {
         let (mint_pk, mint_zkbin) =
             self.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string()).unwrap();
 
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyPoWReward).unwrap();
-
-        let timer = Instant::now();
-
-        // Proposals always extend genesis block
-        let last_nonce = self.genesis_block.header.nonce;
-        let fork_previous_hash = self.genesis_block.header.previous;
-
-        // We're just going to be using a zero spend-hook and user-data
-        let spend_hook = FuncId::none();
-        let user_data = pallas::Base::zero();
+        // Reference the last block in the holder's blockchain
+        let (block_height, fork_previous_hash) = wallet.validator.blockchain.last()?;
+        let last_block = wallet.validator.blockchain.last_block()?;
 
+        // If there's a set reward recipient, use it, otherwise reward the holder
         let recipient = if let Some(holder) = recipient {
-            let holder = self.holders.get(holder).unwrap();
-            holder.keypair.public
+            self.holders.get(holder).unwrap().keypair.public
         } else {
             wallet.keypair.public
         };
 
+        // Build the transaction
         let builder = PoWRewardCallBuilder {
             secret: wallet.keypair.secret,
             recipient,
-            block_height,
-            last_nonce,
+            block_height: block_height + 1,
+            last_nonce: last_block.header.nonce,
             fork_previous_hash,
-            spend_hook,
-            user_data,
+            spend_hook: FuncId::none(),
+            user_data: pallas::Base::ZERO,
             mint_zkbin: mint_zkbin.clone(),
             mint_pk: mint_pk.clone(),
         };
@@ -86,64 +81,47 @@ impl TestHarness {
             None => builder.build()?,
         };
 
+        // Encode the transaction
         let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
-        debris.params.encode(&mut data)?;
+        debris.params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &[wallet.keypair.secret])?;
         tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
 
         Ok((tx, debris.params))
     }
 
+    /// Execute the transaction created by `pow_reward()` for a given [`Holder`].
+    ///
+    /// Returns any gathered [`OwnCoin`]s from the transaction.
     pub async fn execute_pow_reward_tx(
         &mut self,
         holder: &Holder,
         tx: &Transaction,
         params: &MoneyPoWRewardParamsV1,
         block_height: u64,
-    ) -> Result<()> {
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyPoWReward).unwrap();
-        let timer = Instant::now();
 
         wallet.validator.add_test_producer_transaction(tx, block_height, true).await?;
         wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
-        tx_action_benchmark.verify_times.push(timer.elapsed());
-
-        Ok(())
-    }
 
-    pub async fn execute_erroneous_pow_reward_tx(
-        &mut self,
-        holder: &Holder,
-        tx: &Transaction,
-        block_height: u64,
-    ) -> Result<()> {
-        let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyPoWReward).unwrap();
-        let timer = Instant::now();
+        // Attempt to decrypt the output note to see if this is a coin for the holder.
+        let Ok(note) = params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+            return Ok(vec![])
+        };
 
-        assert!(wallet
-            .validator
-            .add_test_producer_transaction(tx, block_height, true)
-            .await
-            .is_err());
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+        let owncoin = OwnCoin {
+            coin: params.output.coin,
+            note: note.clone(),
+            secret: wallet.keypair.secret,
+            leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+        };
 
-        Ok(())
+        wallet.unspent_money_coins.push(owncoin.clone());
+        Ok(vec![owncoin])
     }
 }

+ 237 - 101
src/contract/test-harness/src/money_token.rs

@@ -16,8 +16,6 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     zk::halo2::Field,
@@ -26,68 +24,72 @@ use darkfi::{
 use darkfi_money_contract::{
     client::{
         auth_token_mint_v1::AuthTokenMintCallBuilder, token_freeze_v1::TokenFreezeCallBuilder,
-        token_mint_v1::TokenMintCallBuilder,
+        token_mint_v1::TokenMintCallBuilder, MoneyNote, OwnCoin,
     },
     model::{
-        CoinAttributes, MoneyAuthTokenMintParamsV1, MoneyTokenFreezeParamsV1,
+        CoinAttributes, MoneyAuthTokenMintParamsV1, MoneyFeeParamsV1, MoneyTokenFreezeParamsV1,
         MoneyTokenMintParamsV1, TokenAttributes,
     },
     MoneyFunction, MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1,
     MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
 };
 use darkfi_sdk::{
-    crypto::{poseidon_hash, Blind, FuncId, FuncRef, MerkleNode, MONEY_CONTRACT_ID},
-    dark_tree::DarkLeaf,
+    crypto::{
+        contract_id::MONEY_CONTRACT_ID, poseidon_hash, Blind, FuncId, FuncRef, MerkleNode,
+        MONEY_CONTRACT_ID,
+    },
+    dark_tree::{DarkLeaf, DarkTree},
     pasta::pallas,
     ContractCall,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn token_mint(
+    /// Mint an arbitrary token for a given recipient using `Money::TokenMint`
+    pub async fn token_mint(
         &mut self,
         amount: u64,
         holder: &Holder,
         recipient: &Holder,
         spend_hook: Option<FuncId>,
         user_data: Option<pallas::Base>,
-    ) -> Result<(Transaction, MoneyTokenMintParamsV1, MoneyAuthTokenMintParamsV1)> {
+        block_height: u64,
+    ) -> Result<(
+        Transaction,
+        MoneyTokenMintParamsV1,
+        MoneyAuthTokenMintParamsV1,
+        Option<MoneyFeeParamsV1>,
+    )> {
         let wallet = self.holders.get(holder).unwrap();
         let mint_authority = wallet.token_mint_authority;
-        let token_blind = wallet.token_blind;
-
         let rcpt = self.holders.get(recipient).unwrap().keypair.public;
 
-        let (mint_pk, mint_zkbin) = self
-            .proving_keys
-            .get(&MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1.to_string())
-            .unwrap()
-            .clone();
-        let (auth_mint_pk, auth_mint_zkbin) = self
-            .proving_keys
-            .get(&MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1.to_string())
-            .unwrap()
-            .clone();
+        let (token_mint_pk, token_mint_zkbin) =
+            self.proving_keys.get(&MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1.to_string()).unwrap();
 
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTokenMint).unwrap();
-
-        let timer = Instant::now();
+        let (auth_mint_pk, auth_mint_zkbin) =
+            self.proving_keys.get(&MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1.to_string()).unwrap();
 
+        // Create the Auth FuncID
         let auth_func_id = FuncRef {
             contract_id: *MONEY_CONTRACT_ID,
             func_code: MoneyFunction::AuthTokenMintV1 as u8,
         }
         .to_func_id();
 
+        let (mint_auth_x, mint_auth_y) = mint_authority.public.xy();
+        let token_blind = pallas::Base::random(&mut OsRng);
+
         let token_attrs = TokenAttributes {
             auth_parent: auth_func_id,
-            user_data: poseidon_hash([mint_authority.public.x(), mint_authority.public.y()]),
+            user_data: poseidon_hash([mint_auth_x, mint_auth_y]),
             blind: token_blind,
         };
+
         let token_id = token_attrs.to_token_id();
 
         let coin_attrs = CoinAttributes {
@@ -99,146 +101,280 @@ impl TestHarness {
             blind: Blind::random(&mut OsRng),
         };
 
+        // Create the minting call
         let builder = TokenMintCallBuilder {
             coin_attrs: coin_attrs.clone(),
             token_attrs: token_attrs.clone(),
-            mint_zkbin,
-            mint_pk,
+            mint_zkbin: token_mint_zkbin.clone(),
+            mint_pk: token_mint_pk.clone(),
         };
         let mint_debris = builder.build()?;
         let mut data = vec![MoneyFunction::TokenMintV1 as u8];
-        mint_debris.params.encode(&mut data)?;
+        mint_debris.params.encode_async(&mut data).await?;
         let mint_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
+        // Create the auth call
         let builder = AuthTokenMintCallBuilder {
             coin_attrs,
             token_attrs,
             mint_keypair: mint_authority,
-            auth_mint_zkbin,
-            auth_mint_pk,
+            auth_mint_zkbin: auth_mint_zkbin.clone(),
+            auth_mint_pk: auth_mint_pk.clone(),
         };
         let auth_debris = builder.build()?;
         let mut data = vec![MoneyFunction::AuthTokenMintV1 as u8];
-        auth_debris.params.encode(&mut data)?;
+        auth_debris.params.encode_async(&mut data).await?;
         let auth_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
-        let mut tx = Transaction {
-            calls: vec![
-                DarkLeaf { data: mint_call, parent_index: Some(1), children_indexes: vec![] },
-                DarkLeaf { data: auth_call, parent_index: None, children_indexes: vec![0] },
-            ],
-            proofs: vec![mint_debris.proofs, auth_debris.proofs],
-            signatures: vec![],
-        };
+        // Create the TransactionBuilder containing above calls
+        let mut tx_builder = TransactionBuilder::new(
+            ContractCallLeaf { call: auth_call, proofs: auth_debris.proofs },
+            vec![DarkTree::new(
+                ContractCallLeaf { call: mint_call, proofs: mint_debris.proofs },
+                vec![],
+                None,
+                None,
+            )],
+        )?;
+
+        // If we have tx fees enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let mint_sigs = tx.create_sigs(&mut OsRng, &[])?;
+            let auth_sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
+            tx.signatures = vec![mint_sigs, auth_sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
+        let mut tx = tx_builder.build()?;
         let mint_sigs = tx.create_sigs(&mut OsRng, &[])?;
         let auth_sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
         tx.signatures = vec![mint_sigs, auth_sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, mint_debris.params, auth_debris.params))
+        Ok((tx, mint_debris.params, auth_debris.params, fee_params))
     }
 
+    /// Execute the transaction created by `token_mint()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
+    #[allow(clippy::too_many_arguments)]
     pub async fn execute_token_mint_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
-        params: &MoneyTokenMintParamsV1,
+        tx: Transaction,
+        mint_params: &MoneyTokenMintParamsV1,
+        auth_params: &MoneyAuthTokenMintParamsV1,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTokenMint).unwrap();
-        let timer = Instant::now();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
-        wallet.money_merkle_tree.append(MerkleNode::from(params.coin.inner()));
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        // Iterate over all inputs to mark any spent coins
+        if let Some(ref fee_params) = fee_params {
+            if append {
+                if let Some(spent_coin) = wallet
+                    .unspent_money_coins
+                    .iter()
+                    .find(|x| x.nullifier() == fee_params.input.nullifier)
+                    .cloned()
+                {
+                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                    wallet
+                        .unspent_money_coins
+                        .retain(|x| x.nullifier() != fee_params.input.nullifier);
+                    wallet.spent_money_coins.push(spent_coin.clone());
+                }
+            }
+        }
 
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+        let mut found_owncoins = vec![];
+
+        if append {
+            wallet.money_merkle_tree.append(MerkleNode::from(mint_params.coin.inner()));
+
+            // Attempt to decrypt the encrypted note of the minted token
+            if let Ok(note) = auth_params.enc_note.decrypt::<MoneyNote>(&wallet.keypair.secret) {
+                let owncoin = OwnCoin {
+                    coin: mint_params.coin,
+                    note: note.clone(),
+                    secret: wallet.keypair.secret,
+                    leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+                };
+
+                debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+                wallet.unspent_money_coins.push(owncoin.clone());
+                found_owncoins.push(owncoin);
+            };
+
+            if let Some(ref fee_params) = fee_params {
+                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
+
+                // Attempt to decrypt the encrypted note in the fee output
+                if let Ok(note) =
+                    fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+                {
+                    let owncoin = OwnCoin {
+                        coin: fee_params.output.coin,
+                        note: note.clone(),
+                        secret: wallet.keypair.secret,
+                        leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+                    };
+
+                    debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+                    wallet.unspent_money_coins.push(owncoin.clone());
+                    found_owncoins.push(owncoin);
+                }
+            }
+        }
 
-        Ok(())
+        Ok(found_owncoins)
     }
 
-    pub fn token_freeze(
+    /// Freeze the supply of a minted token
+    pub async fn token_freeze(
         &mut self,
         holder: &Holder,
-    ) -> Result<(Transaction, MoneyTokenFreezeParamsV1)> {
+        block_height: u64,
+    ) -> Result<(Transaction, MoneyTokenFreezeParamsV1, Option<MoneyFeeParamsV1>)> {
         let wallet = self.holders.get(holder).unwrap();
-        let mint_keypair = wallet.token_mint_authority;
-        let token_blind = wallet.token_blind;
+        let mint_authority = wallet.token_mint_authority;
 
         let (frz_pk, frz_zkbin) =
             self.proving_keys.get(&MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1.to_string()).unwrap();
 
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTokenFreeze).unwrap();
-
-        let timer = Instant::now();
-
         let auth_func_id = FuncRef {
             contract_id: *MONEY_CONTRACT_ID,
             func_code: MoneyFunction::AuthTokenMintV1 as u8,
         }
         .to_func_id();
 
+        let (mint_auth_x, mint_auth_y) = mint_authority.public.xy();
+        let token_blind = pallas::Base::random(&mut OsRng);
+
         let token_attrs = TokenAttributes {
             auth_parent: auth_func_id,
-            user_data: poseidon_hash([mint_keypair.public.x(), mint_keypair.public.y()]),
+            user_data: poseidon_hash([mint_auth_x, mint_auth_y]),
             blind: token_blind,
         };
 
+        // Create the freeze call
         let builder = TokenFreezeCallBuilder {
-            mint_keypair,
+            mint_keypair: mint_authority,
             token_attrs,
             freeze_zkbin: frz_zkbin.clone(),
             freeze_pk: frz_pk.clone(),
         };
-
-        let debris = builder.build()?;
-
+        let freeze_debris = builder.build()?;
         let mut data = vec![MoneyFunction::TokenFreezeV1 as u8];
-        debris.params.encode(&mut data)?;
-        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
-        let mut tx_builder =
-            TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
+        freeze_debris.params.encode_async(&mut data).await?;
+        let freeze_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        // Create the TransactionBuilder containing the above call
+        let mut tx_builder = TransactionBuilder::new(
+            ContractCallLeaf { call: freeze_call, proofs: freeze_debris.proofs },
+            vec![],
+        )?;
+
+        // If we have tx fees enabled, make an offering
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let freeze_sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
+            tx.signatures = vec![freeze_sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &[]).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with necessary keys.
         let mut tx = tx_builder.build()?;
-        let sigs = tx.create_sigs(&mut OsRng, &[mint_keypair.secret])?;
-        tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
-
-        Ok((tx, debris.params))
+        let freeze_sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
+        tx.signatures = vec![freeze_sigs];
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
+
+        Ok((tx, freeze_debris.params, fee_params))
     }
 
+    /// Execute the transaction created by `token_freeze()` for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_token_freeze_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
-        _params: &MoneyTokenFreezeParamsV1,
+        tx: Transaction,
+        _freeze_params: &MoneyTokenFreezeParamsV1,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
-    ) -> Result<()> {
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTokenFreeze).unwrap();
-        let timer = Instant::now();
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
+
+        let mut found_owncoins = vec![];
+        if let Some(ref fee_params) = fee_params {
+            if append {
+                if let Some(spent_coin) = wallet
+                    .unspent_money_coins
+                    .iter()
+                    .find(|x| x.nullifier() == fee_params.input.nullifier)
+                    .cloned()
+                {
+                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                    wallet
+                        .unspent_money_coins
+                        .retain(|x| x.nullifier() != fee_params.input.nullifier);
+                    wallet.spent_money_coins.push(spent_coin.clone());
+                }
+
+                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
+
+                // Attempt to decrypt the encrypted note
+                if let Ok(note) =
+                    fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+                {
+                    let owncoin = OwnCoin {
+                        coin: fee_params.output.coin,
+                        note: note.clone(),
+                        secret: wallet.keypair.secret,
+                        leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+                    };
+
+                    debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+                    wallet.unspent_money_coins.push(owncoin.clone());
+                    found_owncoins.push(owncoin);
+                }
+            }
+        }
 
-        Ok(())
+        Ok(found_owncoins)
     }
 }

+ 102 - 73
src/contract/test-harness/src/money_transfer.rs

@@ -16,8 +16,6 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::time::Instant;
-
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     Result,
@@ -28,23 +26,29 @@ use darkfi_money_contract::{
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
-    crypto::{MerkleNode, MONEY_CONTRACT_ID},
-    ContractCall,
+    client::{transfer_v1::make_transfer_call, MoneyNote, OwnCoin},
+    crypto::{contract_id::MONEY_CONTRACT_ID, MerkleNode, TokenId, MONEY_CONTRACT_ID},
+    model::{Input, MoneyFeeParamsV1, MoneyTransferParamsV1, Output},
+    ContractCall, MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
-use darkfi_serial::{serialize, Encodable};
+use darkfi_serial::AsyncEncodable;
+use log::debug;
 use rand::rngs::OsRng;
 
-use super::{Holder, TestHarness, TxAction};
+use super::{Holder, TestHarness};
 
 impl TestHarness {
-    pub fn transfer(
+    /// Create a `Money::Transfer` transaction.
+    pub async fn transfer(
         &mut self,
         amount: u64,
         holder: &Holder,
         recipient: &Holder,
         owncoins: &[OwnCoin],
         token_id: TokenId,
-    ) -> Result<(Transaction, MoneyTransferParamsV1, Vec<OwnCoin>)> {
+        block_height: u64,
+    ) -> Result<(Transaction, (MoneyTransferParamsV1, Option<MoneyFeeParamsV1>), Vec<OwnCoin>)>
+    {
         let wallet = self.holders.get(holder).unwrap();
         let rcpt = self.holders.get(recipient).unwrap().keypair.public;
 
@@ -54,12 +58,8 @@ impl TestHarness {
         let (burn_pk, burn_zkbin) =
             self.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string()).unwrap();
 
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTransfer).unwrap();
-
-        let timer = Instant::now();
-
-        let (params, secrets, spent_coins) = make_transfer_call(
+        // Create the transfer call
+        let (params, secrets, mut spent_coins) = make_transfer_call(
             wallet.keypair,
             rcpt,
             amount,
@@ -72,91 +72,120 @@ impl TestHarness {
             burn_pk.clone(),
         )?;
 
+        // Encode the call
         let mut data = vec![MoneyFunction::TransferV1 as u8];
-        params.encode(&mut data)?;
+        params.encode_async(&mut data).await?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        // Create the TransactionBuilder containing the `Transfer` call
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
+
+        // If we have tx fees enabled, we first have to execute the fee-less tx to gather its
+        // used gas, and then we feed it into the fee-creating function.
+        // We also tell it about any spent coins so we don't accidentally reuse them in the
+        // fee call.
+        // TODO: We have to build a proper coin selection algorithm so that we can utilize
+        // the Money::Transfer to merge any coins which would give us a coin with enough
+        // value for paying the transaction fee.
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&mut OsRng, &secrets.signature_secrets)?;
+            tx.signatures = vec![sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &spent_coins).await?;
+
+            // Append the fee call to the transaction
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            spent_coins.extend_from_slice(&spent_fee_coins);
+            fee_params = Some(fee_call_params);
+        }
+
+        // Now build the actual transaction and sign it with all necessary keys.
         let mut tx = tx_builder.build()?;
         let sigs = tx.create_sigs(&mut OsRng, &secrets.signature_secrets)?;
         tx.signatures = vec![sigs];
-        tx_action_benchmark.creation_times.push(timer.elapsed());
-
-        // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&tx);
-        let size = std::mem::size_of_val(&*encoded);
-        tx_action_benchmark.sizes.push(size);
-        let base58 = bs58::encode(&encoded).into_string();
-        let size = std::mem::size_of_val(&*base58);
-        tx_action_benchmark.broadcasted_sizes.push(size);
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&mut OsRng, &fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
 
-        Ok((tx, params, spent_coins))
+        Ok((tx, (params, fee_params), spent_coins))
     }
 
+    /// Execute a `Money::Transfer` transaction for a given [`Holder`].
+    ///
+    /// Returns any found [`OwnCoin`]s.
     pub async fn execute_transfer_tx(
         &mut self,
         holder: &Holder,
-        tx: &Transaction,
-        params: &MoneyTransferParamsV1,
+        tx: Transaction,
+        call_params: &MoneyTransferParamsV1,
+        fee_params: &Option<MoneyFeeParamsV1>,
         block_height: u64,
         append: bool,
-    ) -> Result<()> {
+    ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTransfer).unwrap();
-        let timer = Instant::now();
-
-        wallet.validator.add_transactions(&[tx.clone()], block_height, true).await?;
-        if append {
-            for output in &params.outputs {
-                wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
-            }
-        }
-        tx_action_benchmark.verify_times.push(timer.elapsed());
 
-        Ok(())
-    }
+        // Execute the transaction
+        wallet.validator.add_transactions(&[tx], block_height, true, self.verify_fees).await?;
 
-    pub async fn execute_multiple_transfer_txs(
-        &mut self,
-        holder: &Holder,
-        txs: &[Transaction],
-        txs_params: &Vec<MoneyTransferParamsV1>,
-        block_height: u64,
-        append: bool,
-    ) -> Result<()> {
-        let wallet = self.holders.get_mut(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTransfer).unwrap();
-        let timer = Instant::now();
+        // Iterate over all inputs to mark any spent coins
+        let mut inputs: Vec<Input> = call_params.inputs.to_vec();
+        if let Some(ref fee_params) = fee_params {
+            inputs.push(fee_params.input.clone());
+        }
 
-        wallet.validator.add_transactions(txs, block_height, true).await?;
         if append {
-            for params in txs_params {
-                for output in &params.outputs {
-                    wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+            for input in inputs.iter() {
+                if let Some(spent_coin) = wallet
+                    .unspent_money_coins
+                    .iter()
+                    .find(|x| x.nullifier() == input.nullifier)
+                    .cloned()
+                {
+                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                    wallet.unspent_money_coins.retain(|x| x.nullifier() != input.nullifier);
+                    wallet.spent_money_coins.push(spent_coin.clone());
                 }
             }
         }
-        tx_action_benchmark.verify_times.push(timer.elapsed());
 
-        Ok(())
-    }
+        // Iterate over all outputs to find any new OwnCoins
+        let mut found_owncoins = vec![];
+        let mut outputs: Vec<Output> = call_params.outputs.to_vec();
+        if let Some(ref fee_params) = fee_params {
+            outputs.push(fee_params.output.clone());
+        }
 
-    pub async fn verify_transfer_tx(
-        &mut self,
-        holder: &Holder,
-        tx: &Transaction,
-        block_height: u64,
-    ) -> Result<()> {
-        let wallet = self.holders.get(holder).unwrap();
-        let tx_action_benchmark =
-            self.tx_action_benchmarks.get_mut(&TxAction::MoneyTransfer).unwrap();
-        let timer = Instant::now();
+        for output in outputs.iter() {
+            if !append {
+                continue
+            }
 
-        wallet.validator.add_transactions(&[tx.clone()], block_height, false).await?;
-        tx_action_benchmark.verify_times.push(timer.elapsed());
+            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+            // Attempt to decrypt the output note to see if this is a coin for the holder.
+            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+                continue
+            };
+
+            let owncoin = OwnCoin {
+                coin: output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            found_owncoins.push(owncoin);
+        }
 
-        Ok(())
+        Ok(found_owncoins)
     }
 }

+ 68 - 62
src/contract/test-harness/src/vks.rs

@@ -38,19 +38,21 @@ use darkfi_dao_contract::{
 };
 use darkfi_deployooor_contract::DEPLOY_CONTRACT_ZKAS_DERIVE_NS_V1;
 use darkfi_money_contract::{
-    MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
-    MONEY_CONTRACT_ZKAS_FEE_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+    MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
     MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
 };
 use darkfi_sdk::crypto::{contract_id::DEPLOYOOOR_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID};
 use darkfi_serial::{deserialize, serialize};
+
 use log::debug;
 
-/// Update this if any circuits are changed
-const VKS_HASH: &str = "605a72d885e6194ac346a328482504ca37f0c990c2d636ad1b548a8bfb05542b";
-const PKS_HASH: &str = "277228a59ed3cc1df8a9d9e61b3230b4417512d649b4aca1fb3e5f02514a2e96";
+/// Update these if any circuits are changed.
+/// Delete the existing cachefiles, and enable debug logging, you will see the new hashes.
+const VKS_HASH: &str = "249d56e6e49ea45a0b42521d85f703bc7bc4428090d6fed11aa6f179a1006a97";
+const PKS_HASH: &str = "85f3e2f69df9d4e11967611be6611dd7e0726b87fbcd3985f535f556be5982fb";
 
-fn pks_path(typ: &str) -> Result<PathBuf> {
+/// Build a `PathBuf` to a cachefile
+fn cache_path(typ: &str) -> Result<PathBuf> {
     let output = Command::new("git").arg("rev-parse").arg("--show-toplevel").output()?.stdout;
     let mut path = PathBuf::from(String::from_utf8(output[..output.len() - 1].to_vec())?);
     path.push("src");
@@ -62,57 +64,61 @@ fn pks_path(typ: &str) -> Result<PathBuf> {
 
 /// (Bincode, Namespace, VK)
 pub type Vks = Vec<(Vec<u8>, String, Vec<u8>)>;
+/// (Bincode, Namespace, VK)
 pub type Pks = Vec<(Vec<u8>, String, Vec<u8>)>;
 
-pub fn read_or_gen_vks_and_pks() -> Result<(Pks, Vks)> {
-    let vks_path = pks_path("vks.bin")?;
-    let pks_path = pks_path("pks.bin")?;
+/// Generate or read cached PKs and VKs
+pub fn get_cached_pks_and_vks() -> Result<(Pks, Vks)> {
+    let pks_path = cache_path("pks.bin")?;
+    let vks_path = cache_path("vks.bin")?;
 
-    let mut vks = None;
     let mut pks = None;
+    let mut vks = None;
 
-    if vks_path.exists() {
-        debug!("Found vks.bin");
-        let mut f = File::open(vks_path.clone())?;
+    if pks_path.exists() {
+        debug!("Found {:?}", pks_path);
+        let mut f = File::open(pks_path.clone())?;
         let mut data = vec![];
         f.read_to_end(&mut data)?;
 
-        let known_hash = blake3::Hash::from_hex(VKS_HASH)?;
+        let known_hash = blake3::Hash::from_hex(PKS_HASH)?;
         let found_hash = blake3::hash(&data);
 
-        debug!("Known VKS hash: {}", known_hash);
-        debug!("Found VKS hash: {}", found_hash);
+        debug!("Known PKS hash: {}", known_hash);
+        debug!("Found PKS hash: {}", found_hash);
 
         if known_hash == found_hash {
-            vks = Some(deserialize(&data)?)
+            pks = Some(deserialize(&data)?)
         }
 
         drop(f);
     }
 
-    if pks_path.exists() {
-        debug!("Found pks.bin");
-        let mut f = File::open(pks_path.clone())?;
+    if vks_path.exists() {
+        debug!("Found {:?}", vks_path);
+        let mut f = File::open(vks_path.clone())?;
         let mut data = vec![];
         f.read_to_end(&mut data)?;
 
-        let known_hash = blake3::Hash::from_hex(PKS_HASH)?;
+        let known_hash = blake3::Hash::from_hex(VKS_HASH)?;
         let found_hash = blake3::hash(&data);
 
-        debug!("Known PKS hash: {}", known_hash);
-        debug!("Found PKS hash: {}", found_hash);
+        debug!("Known VKS hash: {}", known_hash);
+        debug!("Found VKS hash: {}", found_hash);
 
         if known_hash == found_hash {
-            pks = Some(deserialize(&data)?)
+            vks = Some(deserialize(&data)?)
         }
 
         drop(f);
     }
 
+    // Cache is correct, return
     if let (Some(pks), Some(vks)) = (pks, vks) {
         return Ok((pks, vks))
     }
 
+    // Otherwise, build them
     let bins = vec![
         // Money
         &include_bytes!("../../money/proof/fee_v1.zk.bin")[..],
@@ -120,7 +126,6 @@ pub fn read_or_gen_vks_and_pks() -> Result<(Pks, Vks)> {
         &include_bytes!("../../money/proof/burn_v1.zk.bin")[..],
         &include_bytes!("../../money/proof/token_mint_v1.zk.bin")[..],
         &include_bytes!("../../money/proof/token_freeze_v1.zk.bin")[..],
-        &include_bytes!("../../money/proof/auth_token_mint_v1.zk.bin")[..],
         // DAO
         &include_bytes!("../../dao/proof/mint.zk.bin")[..],
         &include_bytes!("../../dao/proof/propose-input.zk.bin")[..],
@@ -134,76 +139,70 @@ pub fn read_or_gen_vks_and_pks() -> Result<(Pks, Vks)> {
         &include_bytes!("../../deployooor/proof/derive_contract_id.zk.bin")[..],
     ];
 
-    let mut vks = vec![];
     let mut pks = vec![];
+    let mut vks = vec![];
 
     for bincode in bins.iter() {
         let zkbin = ZkBinary::decode(bincode)?;
-        debug!("Building VK for {}", zkbin.namespace);
+        debug!("Building PK for {}", zkbin.namespace);
         let witnesses = empty_witnesses(&zkbin)?;
         let circuit = ZkCircuit::new(witnesses, &zkbin);
-        let vk = VerifyingKey::build(zkbin.k, &circuit);
-        let mut vk_buf = vec![];
-        vk.write(&mut vk_buf)?;
-        vks.push((bincode.to_vec(), zkbin.namespace.clone(), vk_buf));
 
         let pk = ProvingKey::build(zkbin.k, &circuit);
         let mut pk_buf = vec![];
         pk.write(&mut pk_buf)?;
-        pks.push((bincode.to_vec(), zkbin.namespace, pk_buf));
+        pks.push((bincode.to_vec(), zkbin.namespace.clone(), pk_buf));
+
+        debug!("Building VK for {}", zkbin.namespace);
+        let vk = VerifyingKey::build(zkbin.k, &circuit);
+        let mut vk_buf = vec![];
+        vk.write(&mut vk_buf)?;
+        vks.push((bincode.to_vec(), zkbin.namespace.clone(), vk_buf));
     }
 
-    debug!("Writing to {:?}", vks_path);
-    let mut f = File::create(vks_path)?;
-    let ser = serialize(&vks);
+    debug!("Writing PKs to {:?}", pks_path);
+    let mut f = File::create(&pks_path)?;
+    let ser = serialize(&pks);
     let hash = blake3::hash(&ser);
-    debug!("vks.bin {}", hash);
+    debug!("{:?} {}", pks_path, hash);
     f.write_all(&ser)?;
 
-    debug!("Writing to {:?}", pks_path);
-    let mut f = File::create(pks_path)?;
-    let ser = serialize(&pks);
+    debug!("Writing VKs to {:?}", vks_path);
+    let mut f = File::create(&vks_path)?;
+    let ser = serialize(&vks);
     let hash = blake3::hash(&ser);
-    debug!("pks.bin {}", hash);
+    debug!("{:?} {}", vks_path, hash);
     f.write_all(&ser)?;
 
     Ok((pks, vks))
 }
 
+/// Inject cached VKs into a given blockchain database reference
 pub fn inject(sled_db: &sled::Db, vks: &Vks) -> Result<()> {
-    // Inject vks into the db
-    let money_zkas_tree_ptr = MONEY_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
-    let money_zkas_tree = sled_db.open_tree(money_zkas_tree_ptr)?;
-
-    let dao_zkas_tree_ptr = DAO_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
-    let dao_zkas_tree = sled_db.open_tree(dao_zkas_tree_ptr)?;
+    // Derive the database names for the specific contracts
+    let money_db_name = MONEY_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
+    let dao_db_name = DAO_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
+    let deployooor_db_name = DEPLOYOOOR_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
 
-    let deployooor_zkas_tree_ptr =
-        DEPLOYOOOR_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
-    let deployooor_zkas_tree = sled_db.open_tree(deployooor_zkas_tree_ptr)?;
+    // Create the db trees
+    let money_tree = sled_db.open_tree(money_db_name)?;
+    let dao_tree = sled_db.open_tree(dao_db_name)?;
+    let deployooor_tree = sled_db.open_tree(deployooor_db_name)?;
 
     for (bincode, namespace, vk) in vks.iter() {
         match namespace.as_str() {
-            // Money circuits
+            // Money contract circuits
             MONEY_CONTRACT_ZKAS_FEE_NS_V1 |
             MONEY_CONTRACT_ZKAS_MINT_NS_V1 |
             MONEY_CONTRACT_ZKAS_BURN_NS_V1 |
             MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1 |
-            MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1 |
-            MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1 => {
-                let key = serialize(&namespace.as_str());
-                let value = serialize(&(bincode.clone(), vk.clone()));
-                money_zkas_tree.insert(key, value)?;
-            }
-
-            // Deployooor circuits
-            DEPLOY_CONTRACT_ZKAS_DERIVE_NS_V1 => {
+            MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1 => {
                 let key = serialize(&namespace.as_str());
                 let value = serialize(&(bincode.clone(), vk.clone()));
-                deployooor_zkas_tree.insert(key, value)?;
+                money_tree.insert(key, value)?;
             }
 
-            // DAO circuits
+            // DAO contract circuits
             DAO_CONTRACT_ZKAS_DAO_MINT_NS |
             DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS |
             DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS |
@@ -214,7 +213,14 @@ pub fn inject(sled_db: &sled::Db, vks: &Vks) -> Result<()> {
             DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS => {
                 let key = serialize(&namespace.as_str());
                 let value = serialize(&(bincode.clone(), vk.clone()));
-                dao_zkas_tree.insert(key, value)?;
+                dao_tree.insert(key, value)?;
+            }
+
+            // Deployooor contract circuits
+            DEPLOY_CONTRACT_ZKAS_DERIVE_NS_V1 => {
+                let key = serialize(&namespace.as_str());
+                let value = serialize(&(bincode.clone(), vk.clone()));
+                deployooor_tree.insert(key, value)?;
             }
 
             x => panic!("Found unhandled zkas namespace {}", x),

+ 12 - 6
src/validator/mod.rs

@@ -424,41 +424,47 @@ impl Validator {
     /// In case any of the transactions fail, they will be returned to the caller.
     /// The function takes a boolean called `write` which tells it to actually write
     /// the state transitions to the database.
+    ///
+    /// Returns the total gas used for the given transactions.
     pub async fn add_transactions(
         &self,
         txs: &[Transaction],
         verifying_block_height: u64,
         write: bool,
-    ) -> Result<()> {
+        verify_fees: bool,
+    ) -> Result<u64> {
         debug!(target: "validator::add_transactions", "Instantiating BlockchainOverlay");
         let overlay = BlockchainOverlay::new(&self.blockchain)?;
 
         // Verify all transactions and get erroneous ones
-        let e = verify_transactions(
+        let verify_result = verify_transactions(
             &overlay,
             verifying_block_height,
             txs,
             &mut MerkleTree::new(1),
-            self.verify_fees,
+            verify_fees,
         )
         .await;
+
         let lock = overlay.lock().unwrap();
         let mut overlay = lock.overlay.lock().unwrap();
 
-        if let Err(e) = e {
+        if let Err(e) = verify_result {
             overlay.purge_new_trees()?;
             return Err(e)
         }
 
+        let gas_used = verify_result.unwrap();
+
         if !write {
             debug!(target: "validator::add_transactions", "Skipping apply of state updates because write=false");
             overlay.purge_new_trees()?;
-            return Ok(())
+            return Ok(gas_used)
         }
 
         debug!(target: "validator::add_transactions", "Applying overlay changes");
         overlay.apply()?;
-        Ok(())
+        Ok(gas_used)
     }
 
     /// Validate a producer `Transaction` and apply it if valid.