Эх сурвалжийг харах

contract/test-harness: Deduplicate Nullifier and Coin handling code

x 5 сар өмнө
parent
commit
c17f2a650d

+ 7 - 51
src/contract/test-harness/src/contract_deploy.rs

@@ -21,17 +21,11 @@ use darkfi::{
     Result,
     Result,
 };
 };
 use darkfi_deployooor_contract::{client::deploy_v1::DeployCallBuilder, DeployFunction};
 use darkfi_deployooor_contract::{client::deploy_v1::DeployCallBuilder, DeployFunction};
-use darkfi_money_contract::{
-    client::{MoneyNote, OwnCoin},
-    model::MoneyFeeParamsV1,
-};
+use darkfi_money_contract::{client::OwnCoin, model::MoneyFeeParamsV1};
 use darkfi_sdk::{
 use darkfi_sdk::{
-    crypto::{contract_id::DEPLOYOOOR_CONTRACT_ID, MerkleNode},
-    deploy::DeployParamsV1,
-    ContractCall,
+    crypto::contract_id::DEPLOYOOOR_CONTRACT_ID, deploy::DeployParamsV1, ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
-use tracing::debug;
+use darkfi_serial::Encodable;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -45,7 +39,7 @@ impl TestHarness {
         wasm_bincode: Vec<u8>,
         wasm_bincode: Vec<u8>,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, DeployParamsV1, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, DeployParamsV1, Option<MoneyFeeParamsV1>)> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
         let deploy_keypair = wallet.contract_deploy_authority;
         let deploy_keypair = wallet.contract_deploy_authority;
 
 
         // Build the contract call
         // Build the contract call
@@ -54,7 +48,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DeployFunction::DeployV1 as u8];
         let mut data = vec![DeployFunction::DeployV1 as u8];
-        debris.params.encode_async(&mut data).await?;
+        debris.params.encode(&mut data)?;
         let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data };
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![] }, vec![])?;
             TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![] }, vec![])?;
@@ -102,50 +96,12 @@ impl TestHarness {
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
         let wallet = self.holders.get_mut(holder).unwrap();
 
 
-        // Execute the transaction
         wallet.add_transaction("deploy::deploy", tx, block_height).await?;
         wallet.add_transaction("deploy::deploy", tx, block_height).await?;
 
 
         if !append {
         if !append {
-            return Ok(vec![])
-        }
-
-        if let Some(ref fee_params) = fee_params {
-            let nullifier = fee_params.input.nullifier.inner();
-            wallet
-                .money_null_smt
-                .insert_batch(vec![(nullifier, nullifier)])
-                .expect("smt.insert_batch()");
-
-            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])
+            return Ok(vec![]);
         }
         }
 
 
-        Ok(vec![])
+        Ok(wallet.process_fee(fee_params, holder))
     }
     }
 }
 }

+ 8 - 54
src/contract/test-harness/src/contract_lock.rs

@@ -23,17 +23,9 @@ use darkfi::{
 use darkfi_deployooor_contract::{
 use darkfi_deployooor_contract::{
     client::lock_v1::LockCallBuilder, model::LockParamsV1, DeployFunction,
     client::lock_v1::LockCallBuilder, model::LockParamsV1, DeployFunction,
 };
 };
-use darkfi_money_contract::{
-    client::{MoneyNote, OwnCoin},
-    model::MoneyFeeParamsV1,
-};
-use darkfi_sdk::{
-    crypto::{contract_id::DEPLOYOOOR_CONTRACT_ID, MerkleNode},
-    //deploy::LockParamsV1,
-    ContractCall,
-};
-use darkfi_serial::AsyncEncodable;
-use tracing::debug;
+use darkfi_money_contract::{client::OwnCoin, model::MoneyFeeParamsV1};
+use darkfi_sdk::{crypto::contract_id::DEPLOYOOOR_CONTRACT_ID, ContractCall};
+use darkfi_serial::Encodable;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -46,7 +38,7 @@ impl TestHarness {
         holder: &Holder,
         holder: &Holder,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, LockParamsV1, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, LockParamsV1, Option<MoneyFeeParamsV1>)> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
         let deploy_keypair = wallet.contract_deploy_authority;
         let deploy_keypair = wallet.contract_deploy_authority;
 
 
         // Build the contract call
         // Build the contract call
@@ -55,7 +47,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DeployFunction::LockV1 as u8];
         let mut data = vec![DeployFunction::LockV1 as u8];
-        debris.params.encode_async(&mut data).await?;
+        debris.params.encode(&mut data)?;
         let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data };
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![] }, vec![])?;
             TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![] }, vec![])?;
@@ -101,52 +93,14 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("deploy::lock", tx, block_height).await?;
         wallet.add_transaction("deploy::lock", tx, block_height).await?;
 
 
         if !append {
         if !append {
-            return Ok(vec![])
-        }
-
-        if let Some(ref fee_params) = fee_params {
-            let nullifier = fee_params.input.nullifier.inner();
-            wallet
-                .money_null_smt
-                .insert_batch(vec![(nullifier, nullifier)])
-                .expect("smt.insert_batch()");
-
-            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])
+            return Ok(vec![]);
         }
         }
 
 
-        Ok(vec![])
+        Ok(wallet.process_fee(fee_params, holder))
     }
     }
 }
 }

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

@@ -29,21 +29,20 @@ use darkfi_dao_contract::{
     DAO_CONTRACT_ZKAS_EXEC_NS,
     DAO_CONTRACT_ZKAS_EXEC_NS,
 };
 };
 use darkfi_money_contract::{
 use darkfi_money_contract::{
-    client::{transfer_v1 as xfer, MoneyNote, OwnCoin},
+    client::{transfer_v1 as xfer, OwnCoin},
     model::{CoinAttributes, MoneyFeeParamsV1, MoneyTransferParamsV1},
     model::{CoinAttributes, MoneyFeeParamsV1, MoneyTransferParamsV1},
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 };
 use darkfi_sdk::{
 use darkfi_sdk::{
     crypto::{
     crypto::{
         contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
         contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
-        pedersen_commitment_u64, Blind, FuncRef, MerkleNode, ScalarBlind, SecretKey,
+        pedersen_commitment_u64, Blind, FuncRef, ScalarBlind, SecretKey,
     },
     },
     dark_tree::DarkTree,
     dark_tree::DarkTree,
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
+use darkfi_serial::Encodable;
 use rand::rngs::OsRng;
 use rand::rngs::OsRng;
-use tracing::debug;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -64,7 +63,7 @@ impl TestHarness {
         all_vote_blind: ScalarBlind,
         all_vote_blind: ScalarBlind,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, MoneyTransferParamsV1, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, MoneyTransferParamsV1, Option<MoneyFeeParamsV1>)> {
-        let dao_wallet = self.holders.get(&Holder::Dao).unwrap();
+        let dao_wallet = self.wallet(&Holder::Dao);
 
 
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
@@ -136,7 +135,7 @@ impl TestHarness {
 
 
         let (xfer_params, xfer_secrets) = xfer_builder.build()?;
         let (xfer_params, xfer_secrets) = xfer_builder.build()?;
         let mut data = vec![MoneyFunction::TransferV1 as u8];
         let mut data = vec![MoneyFunction::TransferV1 as u8];
-        xfer_params.encode_async(&mut data).await?;
+        xfer_params.encode(&mut data)?;
         let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
 
         // We need to extract stuff from the inputs and outputs that we'll also
         // We need to extract stuff from the inputs and outputs that we'll also
@@ -172,7 +171,7 @@ impl TestHarness {
             dao_exec_pk,
             dao_exec_pk,
         )?;
         )?;
         let mut data = vec![DaoFunction::Exec as u8];
         let mut data = vec![DaoFunction::Exec as u8];
-        exec_params.encode_async(&mut data).await?;
+        exec_params.encode(&mut data)?;
         let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
 
 
         // Auth module
         // Auth module
@@ -190,7 +189,7 @@ impl TestHarness {
             dao_auth_xfer_enc_coin_pk,
             dao_auth_xfer_enc_coin_pk,
         )?;
         )?;
         let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
         let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
-        auth_xfer_params.encode_async(&mut data).await?;
+        auth_xfer_params.encode(&mut data)?;
         let auth_xfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let auth_xfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
 
 
         // We need to construct this tree, where exec is the parent:
         // We need to construct this tree, where exec is the parent:
@@ -267,7 +266,7 @@ impl TestHarness {
         all_vote_blind: ScalarBlind,
         all_vote_blind: ScalarBlind,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, Option<MoneyFeeParamsV1>)> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet(holder);
 
 
         let (dao_exec_pk, dao_exec_zkbin) = match dao_early_exec_secret_key {
         let (dao_exec_pk, dao_exec_zkbin) = match dao_early_exec_secret_key {
             Some(_) => self.proving_keys.get(DAO_CONTRACT_ZKAS_EARLY_EXEC_NS).unwrap(),
             Some(_) => self.proving_keys.get(DAO_CONTRACT_ZKAS_EARLY_EXEC_NS).unwrap(),
@@ -297,7 +296,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DaoFunction::Exec as u8];
         let mut data = vec![DaoFunction::Exec as u8];
-        exec_params.encode_async(&mut data).await?;
+        exec_params.encode(&mut data)?;
         let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
 
 
         // Create the TransactionBuilder containing the `DAO::Exec` call
         // Create the TransactionBuilder containing the `DAO::Exec` call
@@ -348,61 +347,27 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("dao::exec", tx, block_height).await?;
         wallet.add_transaction("dao::exec", tx, block_height).await?;
 
 
         if !append {
         if !append {
-            return Ok(vec![])
+            return Ok(vec![]);
         }
         }
 
 
-        let (mut inputs, mut outputs) = match xfer_params {
-            Some(params) => (params.inputs.to_vec(), params.outputs.to_vec()),
-            None => (vec![], vec![]),
-        };
-
-        if let Some(ref fee_params) = fee_params {
-            inputs.push(fee_params.input.clone());
-            outputs.push(fee_params.output.clone());
-        }
-
-        let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
-        wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()");
-
-        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());
-            }
+        // Combine transfer and fee inputs/outputs, then process uniformly
+        let mut inputs = vec![];
+        let mut outputs = vec![];
+        if let Some(params) = xfer_params {
+            inputs.extend_from_slice(&params.inputs);
+            outputs.extend_from_slice(&params.outputs);
         }
         }
-
-        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);
+        if let Some(ref fp) = fee_params {
+            inputs.push(fp.input.clone());
+            outputs.push(fp.output.clone());
         }
         }
 
 
-        Ok(found_owncoins)
+        wallet.process_inputs(&inputs, holder);
+        Ok(wallet.process_outputs(&outputs, holder))
     }
     }
 }
 }

+ 7 - 48
src/contract/test-harness/src/dao_mint.rs

@@ -25,16 +25,12 @@ use darkfi_dao_contract::{
     model::{Dao, DaoMintParams},
     model::{Dao, DaoMintParams},
     DaoFunction, DAO_CONTRACT_ZKAS_MINT_NS,
     DaoFunction, DAO_CONTRACT_ZKAS_MINT_NS,
 };
 };
-use darkfi_money_contract::{
-    client::{MoneyNote, OwnCoin},
-    model::MoneyFeeParamsV1,
-};
+use darkfi_money_contract::{client::OwnCoin, model::MoneyFeeParamsV1};
 use darkfi_sdk::{
 use darkfi_sdk::{
     crypto::{contract_id::DAO_CONTRACT_ID, MerkleNode, SecretKey},
     crypto::{contract_id::DAO_CONTRACT_ID, MerkleNode, SecretKey},
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
-use tracing::debug;
+use darkfi_serial::Encodable;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -74,7 +70,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DaoFunction::Mint as u8];
         let mut data = vec![DaoFunction::Mint as u8];
-        params.encode_async(&mut data).await?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
 
 
@@ -119,56 +115,19 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("dao::mint", tx, block_height).await?;
         wallet.add_transaction("dao::mint", tx, block_height).await?;
 
 
         if !append {
         if !append {
-            return Ok(vec![])
+            return Ok(vec![]);
         }
         }
 
 
+        // Track the DAO bulla in the DAO Merkle tree
         wallet.dao_merkle_tree.append(MerkleNode::from(params.dao_bulla.inner()));
         wallet.dao_merkle_tree.append(MerkleNode::from(params.dao_bulla.inner()));
         let leaf_pos = wallet.dao_merkle_tree.mark().unwrap();
         let leaf_pos = wallet.dao_merkle_tree.mark().unwrap();
         wallet.dao_leafs.insert(params.dao_bulla, leaf_pos);
         wallet.dao_leafs.insert(params.dao_bulla, leaf_pos);
 
 
-        if let Some(ref fee_params) = fee_params {
-            let nullifier = fee_params.input.nullifier.inner();
-            wallet
-                .money_null_smt
-                .insert_batch(vec![(nullifier, nullifier)])
-                .expect("smt.insert_batch()");
-
-            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![])
+        Ok(wallet.process_fee(fee_params, holder))
     }
     }
 }
 }

+ 11 - 63
src/contract/test-harness/src/dao_propose.rs

@@ -27,7 +27,7 @@ use darkfi_dao_contract::{
     DaoFunction, DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_PROPOSE_MAIN_NS,
     DaoFunction, DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_PROPOSE_MAIN_NS,
 };
 };
 use darkfi_money_contract::{
 use darkfi_money_contract::{
-    client::{MoneyNote, OwnCoin},
+    client::OwnCoin,
     model::{CoinAttributes, MoneyFeeParamsV1},
     model::{CoinAttributes, MoneyFeeParamsV1},
     MoneyFunction,
     MoneyFunction,
 };
 };
@@ -39,9 +39,8 @@ use darkfi_sdk::{
     pasta::pallas,
     pasta::pallas,
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
+use darkfi_serial::Encodable;
 use rand::rngs::OsRng;
 use rand::rngs::OsRng;
-use tracing::debug;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -58,7 +57,7 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         duration_blockwindows: u64,
         duration_blockwindows: u64,
     ) -> Result<(Transaction, DaoProposeParams, Option<MoneyFeeParamsV1>, DaoProposal)> {
     ) -> Result<(Transaction, DaoProposeParams, Option<MoneyFeeParamsV1>, DaoProposal)> {
-        let wallet = self.holders.get(proposer).unwrap();
+        let wallet = self.wallet(proposer);
 
 
         let (dao_propose_burn_pk, dao_propose_burn_zkbin) =
         let (dao_propose_burn_pk, dao_propose_burn_zkbin) =
             self.proving_keys.get(DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS).unwrap();
             self.proving_keys.get(DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS).unwrap();
@@ -103,7 +102,7 @@ impl TestHarness {
             proposal_coins.push(coin_params.to_coin());
             proposal_coins.push(coin_params.to_coin());
         }
         }
         let mut proposal_data = vec![];
         let mut proposal_data = vec![];
-        proposal_coins.encode_async(&mut proposal_data).await?;
+        proposal_coins.encode(&mut proposal_data)?;
 
 
         // Create Auth calls
         // Create Auth calls
         let auth_calls = vec![
         let auth_calls = vec![
@@ -155,7 +154,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DaoFunction::Propose as u8];
         let mut data = vec![DaoFunction::Propose as u8];
-        params.encode_async(&mut data).await?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
 
 
@@ -198,7 +197,7 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         duration_blockwindows: u64,
         duration_blockwindows: u64,
     ) -> Result<(Transaction, DaoProposeParams, Option<MoneyFeeParamsV1>, DaoProposal)> {
     ) -> Result<(Transaction, DaoProposeParams, Option<MoneyFeeParamsV1>, DaoProposal)> {
-        let wallet = self.holders.get(proposer).unwrap();
+        let wallet = self.wallet(proposer);
 
 
         let (dao_propose_burn_pk, dao_propose_burn_zkbin) =
         let (dao_propose_burn_pk, dao_propose_burn_zkbin) =
             self.proving_keys.get(DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS).unwrap();
             self.proving_keys.get(DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS).unwrap();
@@ -213,20 +212,6 @@ impl TestHarness {
             .unwrap()
             .unwrap()
             .clone();
             .clone();
 
 
-        // Useful code snippet to dump a sled contract DB
-        /*{
-            let blockchain = &wallet.validator.read().await.blockchain;
-            let contracts = &blockchain.contracts;
-            let tree = contracts
-                .lookup(&blockchain.sled_db, &MONEY_CONTRACT_ID, "nullifier_roots")
-                .unwrap();
-            for kv in tree.iter() {
-                let (key, value) = kv.unwrap();
-                debug!("STATE {:?}", key);
-                debug!("  => {:?}", value);
-            }
-        }*/
-
         let input = DaoProposeStakeInput {
         let input = DaoProposeStakeInput {
             secret: wallet.keypair.secret,
             secret: wallet.keypair.secret,
             note: propose_owncoin.note.clone(),
             note: propose_owncoin.note.clone(),
@@ -273,7 +258,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DaoFunction::Propose as u8];
         let mut data = vec![DaoFunction::Propose as u8];
-        params.encode_async(&mut data).await?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
 
 
@@ -318,59 +303,22 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("dao::propose", tx, block_height).await?;
         wallet.add_transaction("dao::propose", tx, block_height).await?;
 
 
         wallet.money_null_smt_snapshot = Some(wallet.money_null_smt.clone());
         wallet.money_null_smt_snapshot = Some(wallet.money_null_smt.clone());
 
 
         if !append {
         if !append {
-            return Ok(vec![])
+            return Ok(vec![]);
         }
         }
 
 
+        // Track proposal in the proposals tree
         wallet.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
         wallet.dao_proposals_tree.append(MerkleNode::from(params.proposal_bulla.inner()));
         let prop_leaf_pos = wallet.dao_proposals_tree.mark().unwrap();
         let prop_leaf_pos = wallet.dao_proposals_tree.mark().unwrap();
         let prop_money_snapshot = wallet.money_merkle_tree.clone();
         let prop_money_snapshot = wallet.money_merkle_tree.clone();
         wallet.dao_prop_leafs.insert(params.proposal_bulla, (prop_leaf_pos, prop_money_snapshot));
         wallet.dao_prop_leafs.insert(params.proposal_bulla, (prop_leaf_pos, prop_money_snapshot));
 
 
-        if let Some(ref fee_params) = fee_params {
-            let nullifier = fee_params.input.nullifier.inner();
-            wallet
-                .money_null_smt
-                .insert_batch(vec![(nullifier, nullifier)])
-                .expect("smt.insert_batch()");
-
-            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![])
+        Ok(wallet.process_fee(fee_params, holder))
     }
     }
 }
 }

+ 9 - 54
src/contract/test-harness/src/dao_vote.rs

@@ -2,7 +2,7 @@
  *
  *
  * Copyright (C) 2020-2026 Dyne.org foundation
  * Copyright (C) 2020-2026 Dyne.org foundation
  *
  *
- * This program is free software: you can redistributemoney it and/or modify
+ * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
  * it under the terms of the GNU Affero General Public License as
  * published by the Free Software Foundation, either version 3 of the
  * published by the Free Software Foundation, either version 3 of the
  * License, or (at your option) any later version.
  * License, or (at your option) any later version.
@@ -26,16 +26,9 @@ use darkfi_dao_contract::{
     model::{Dao, DaoProposal, DaoVoteParams},
     model::{Dao, DaoProposal, DaoVoteParams},
     DaoFunction, DAO_CONTRACT_ZKAS_VOTE_INPUT_NS, DAO_CONTRACT_ZKAS_VOTE_MAIN_NS,
     DaoFunction, DAO_CONTRACT_ZKAS_VOTE_INPUT_NS, DAO_CONTRACT_ZKAS_VOTE_MAIN_NS,
 };
 };
-use darkfi_money_contract::{
-    client::{MoneyNote, OwnCoin},
-    model::MoneyFeeParamsV1,
-};
-use darkfi_sdk::{
-    crypto::{contract_id::DAO_CONTRACT_ID, MerkleNode},
-    ContractCall,
-};
-use darkfi_serial::AsyncEncodable;
-use tracing::debug;
+use darkfi_money_contract::{client::OwnCoin, model::MoneyFeeParamsV1};
+use darkfi_sdk::{crypto::contract_id::DAO_CONTRACT_ID, ContractCall};
+use darkfi_serial::Encodable;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -49,7 +42,7 @@ impl TestHarness {
         proposal: &DaoProposal,
         proposal: &DaoProposal,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, DaoVoteParams, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, DaoVoteParams, Option<MoneyFeeParamsV1>)> {
-        let wallet = self.holders.get(voter).unwrap();
+        let wallet = self.wallet(voter);
 
 
         let (dao_vote_burn_pk, dao_vote_burn_zkbin) =
         let (dao_vote_burn_pk, dao_vote_burn_zkbin) =
             self.proving_keys.get(DAO_CONTRACT_ZKAS_VOTE_INPUT_NS).unwrap();
             self.proving_keys.get(DAO_CONTRACT_ZKAS_VOTE_INPUT_NS).unwrap();
@@ -94,7 +87,7 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![DaoFunction::Vote as u8];
         let mut data = vec![DaoFunction::Vote as u8];
-        params.encode_async(&mut data).await?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
         let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
 
 
@@ -138,52 +131,14 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("dao::vote", tx, block_height).await?;
         wallet.add_transaction("dao::vote", tx, block_height).await?;
 
 
         if !append {
         if !append {
-            return Ok(vec![])
-        }
-
-        if let Some(ref fee_params) = fee_params {
-            let nullifier = fee_params.input.nullifier.inner();
-            wallet
-                .money_null_smt
-                .insert_batch(vec![(nullifier, nullifier)])
-                .expect("smt.insert_batch()");
-
-            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])
+            return Ok(vec![]);
         }
         }
 
 
-        Ok(vec![])
+        Ok(wallet.process_fee(fee_params, holder))
     }
     }
 }
 }

+ 611 - 54
src/contract/test-harness/src/lib.rs

@@ -18,8 +18,10 @@
 
 
 use std::{
 use std::{
     collections::HashMap,
     collections::HashMap,
+    fs::OpenOptions,
     io::{Cursor, Write},
     io::{Cursor, Write},
     slice,
     slice,
+    time::Instant,
 };
 };
 
 
 use darkfi::{
 use darkfi::{
@@ -36,64 +38,68 @@ use darkfi::{
     zkas::ZkBinary,
     zkas::ZkBinary,
     Result,
     Result,
 };
 };
-use darkfi_dao_contract::model::{DaoBulla, DaoProposalBulla};
-use darkfi_money_contract::client::OwnCoin;
+use darkfi_dao_contract::model::{Dao, DaoBulla, DaoProposal, DaoProposalBulla, DaoVoteParams};
+use darkfi_money_contract::{
+    client::{MoneyNote, OwnCoin},
+    model::{
+        CoinAttributes, Input, MoneyFeeParamsV1, MoneyGenesisMintParamsV1, Nullifier, Output,
+        TokenAttributes, TokenId,
+    },
+    MoneyFunction,
+};
 use darkfi_sdk::{
 use darkfi_sdk::{
     bridgetree,
     bridgetree,
     crypto::{
     crypto::{
+        contract_id::MONEY_CONTRACT_ID,
+        poseidon_hash,
         smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
         smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
-        Keypair, MerkleNode, MerkleTree,
+        BaseBlind, FuncRef, Keypair, MerkleNode, MerkleTree, ScalarBlind, SecretKey,
     },
     },
     pasta::pallas,
     pasta::pallas,
 };
 };
 use darkfi_serial::{serialize, Encodable};
 use darkfi_serial::{serialize, Encodable};
 use num_bigint::BigUint;
 use num_bigint::BigUint;
+use rand::rngs::OsRng;
 use sled_overlay::sled;
 use sled_overlay::sled;
-use tracing::warn;
+use tracing::{debug, warn};
 
 
 /// Utility module for caching ZK proof PKs and VKs
 /// Utility module for caching ZK proof PKs and VKs
 pub mod vks;
 pub mod vks;
 
 
-/// `Money::PoWReward` functionality
-mod money_pow_reward;
-
 /// `Money::Fee` functionality
 /// `Money::Fee` functionality
 mod money_fee;
 mod money_fee;
-
 /// `Money::GenesisMint` functionality
 /// `Money::GenesisMint` functionality
 mod money_genesis_mint;
 mod money_genesis_mint;
-
-/// `Money::Transfer` functionality
-mod money_transfer;
-
-/// `Money::TokenMint` functionality
-mod money_token;
-
 /// `Money::OtcSwap` functionality
 /// `Money::OtcSwap` functionality
 mod money_otc_swap;
 mod money_otc_swap;
+/// `Money::PoWReward` functionality
+mod money_pow_reward;
+/// `Money::TokenMint` functionality
+mod money_token;
+/// `Money::Transfer` functionality
+mod money_transfer;
 
 
 /// `Deployooor::Deploy` functionality
 /// `Deployooor::Deploy` functionality
 mod contract_deploy;
 mod contract_deploy;
-
 /// `Deployooor::Lock` functionality
 /// `Deployooor::Lock` functionality
 mod contract_lock;
 mod contract_lock;
 
 
+/// `Dao::Exec` functionality
+mod dao_exec;
 /// `Dao::Mint` functionality
 /// `Dao::Mint` functionality
 mod dao_mint;
 mod dao_mint;
-
 /// `Dao::Propose` functionality
 /// `Dao::Propose` functionality
 mod dao_propose;
 mod dao_propose;
-
 /// `Dao::Vote` functionality
 /// `Dao::Vote` functionality
 mod dao_vote;
 mod dao_vote;
 
 
-/// `Dao::Exec` functionality
-mod dao_exec;
+/// PoW target
+const POW_TARGET: u32 = 120;
 
 
 /// Initialize the logging mechanism
 /// Initialize the logging mechanism
 pub fn init_logger() {
 pub fn init_logger() {
-    // We check this error so we can execute same file tests in parallel,
-    // otherwise second one fails to init logger here.
+    // We check this error so we can execute same-file tests in parallel.
+    // Otherwise subsequent calls fail to init the logger here.
     if setup_test_logger(
     if setup_test_logger(
         &["sled"],
         &["sled"],
         false,
         false,
@@ -104,7 +110,7 @@ pub fn init_logger() {
     )
     )
     .is_err()
     .is_err()
     {
     {
-        warn!(target: "test_harness", "Logger already initialized");
+        warn!(target: "test-harness", "Logger already initialized");
     }
     }
 }
 }
 
 
@@ -130,13 +136,16 @@ pub struct Wallet {
     pub validator: ValidatorPtr,
     pub validator: ValidatorPtr,
     /// Holder's instance of the Merkle tree for the `Money` contract
     /// Holder's instance of the Merkle tree for the `Money` contract
     pub money_merkle_tree: MerkleTree,
     pub money_merkle_tree: MerkleTree,
-    /// Holder's instance of the SMT tree for the `Money` contract
+    /// Holder's instance of the nullifiers SMT tree for the `Money` contract
     pub money_null_smt: SmtMemoryFp,
     pub money_null_smt: SmtMemoryFp,
-    /// Holder's instance of the SMT tree for the `Money` contract (snapshotted for DAO::propose())
+    /// Holder's instance of the nullifiers SMT tree for the `Money` contract
+    /// snapshotted for `DAO::Propose`
     pub money_null_smt_snapshot: Option<SmtMemoryFp>,
     pub money_null_smt_snapshot: Option<SmtMemoryFp>,
-    /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO bullas)
+    /// Holder's instance of the Merkle tree for the `DAO` contract
+    /// holding DAO bullas
     pub dao_merkle_tree: MerkleTree,
     pub dao_merkle_tree: MerkleTree,
-    /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO proposals)
+    /// Holder's instance of the Merkle tree for the `DAO` contract
+    /// holding DAO proposals
     pub dao_proposals_tree: MerkleTree,
     pub dao_proposals_tree: MerkleTree,
     /// Holder's set of unspent [`OwnCoin`]s from the `Money` contract
     /// Holder's set of unspent [`OwnCoin`]s from the `Money` contract
     pub unspent_money_coins: Vec<OwnCoin>,
     pub unspent_money_coins: Vec<OwnCoin>,
@@ -166,8 +175,8 @@ impl Wallet {
         // Inject the cached VKs into the database
         // Inject the cached VKs into the database
         let overlay = BlockchainOverlay::new(&Blockchain::new(&sled_db)?)?;
         let overlay = BlockchainOverlay::new(&Blockchain::new(&sled_db)?)?;
         vks::inject(&overlay, vks)?;
         vks::inject(&overlay, vks)?;
-        let pow_target = 120;
-        deploy_native_contracts(&overlay, pow_target).await?;
+
+        deploy_native_contracts(&overlay, POW_TARGET).await?;
         let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&[])?;
         let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&[])?;
         overlay.lock().unwrap().contracts.update_state_monotree(&diff)?;
         overlay.lock().unwrap().contracts.update_state_monotree(&diff)?;
         overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
         overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
@@ -175,15 +184,15 @@ impl Wallet {
         // Create the `Validator` instance
         // Create the `Validator` instance
         let validator_config = ValidatorConfig {
         let validator_config = ValidatorConfig {
             confirmation_threshold: 3,
             confirmation_threshold: 3,
-            pow_target,
+            pow_target: POW_TARGET,
             pow_fixed_difficulty: Some(BigUint::from(1_u8)),
             pow_fixed_difficulty: Some(BigUint::from(1_u8)),
             genesis_block,
             genesis_block,
             verify_fees,
             verify_fees,
         };
         };
         let validator = Validator::new(&sled_db, &validator_config).await?;
         let validator = Validator::new(&sled_db, &validator_config).await?;
 
 
-        // The Merkle tree for the `Money` contract is initialized with a "null"
-        // leaf at position 0.
+        // The Merkle tree for the Money contract is initialized with a
+        // "null" leaf at position 0.
         let mut money_merkle_tree = MerkleTree::new(1);
         let mut money_merkle_tree = MerkleTree::new(1);
         money_merkle_tree.append(MerkleNode::from(pallas::Base::ZERO));
         money_merkle_tree.append(MerkleNode::from(pallas::Base::ZERO));
         money_merkle_tree.mark().unwrap();
         money_merkle_tree.mark().unwrap();
@@ -217,7 +226,7 @@ impl Wallet {
         block_height: u32,
         block_height: u32,
     ) -> Result<()> {
     ) -> Result<()> {
         if self.bench_wasm {
         if self.bench_wasm {
-            let _ = benchmark_wasm_calls(callname, &self.validator, &tx, block_height).await;
+            let _ = benchmark_wasm_calls(callname, self.validator.clone(), &tx, block_height).await;
         }
         }
 
 
         let validator = self.validator.read().await;
         let validator = self.validator.read().await;
@@ -232,14 +241,76 @@ impl Wallet {
             .await?;
             .await?;
 
 
         // Write the data
         // Write the data
+        let blockchain = &validator.blockchain;
+        let txs = &blockchain.transactions;
+        txs.insert(slice::from_ref(&tx)).expect("insert tx");
+        txs.insert_location(&[tx.hash()], block_height).expect("insert loc");
+
+        Ok(())
+    }
+
+    /// Mark a single nullifier as spent in the SMT and move any matching
+    /// OwnCoin from `unspent_money_coins` to `spent_money_coins`
+    pub fn mark_spent_nullifier(&mut self, nullifier: Nullifier, holder: &Holder) {
+        let n = nullifier.inner();
+        self.money_null_smt.insert_batch(vec![(n, n)]).expect("smt.insert_batch()");
+
+        if let Some(spent_coin) =
+            self.unspent_money_coins.iter().find(|x| x.nullifier() == nullifier).cloned()
         {
         {
-            let blockchain = &validator.blockchain;
-            let txs = &blockchain.transactions;
-            txs.insert(slice::from_ref(&tx)).expect("insert tx");
-            txs.insert_location(&[tx.hash()], block_height).expect("insert loc");
+            debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+            self.unspent_money_coins.retain(|x| x.nullifier() != nullifier);
+            self.spent_money_coins.push(spent_coin);
         }
         }
+    }
 
 
-        Ok(())
+    /// Process a set of [`Input`]s.
+    /// Insert nullifiers into the SMT and move any matching OwnCoins from
+    /// unspent to spent.
+    pub fn process_inputs(&mut self, inputs: &[Input], holder: &Holder) {
+        for input in inputs {
+            self.mark_spent_nullifier(input.nullifier, holder);
+        }
+    }
+
+    /// Process a set of [`Output`]s.
+    /// Append each coin to the Merkle tree and attempt to decrypt the note.
+    /// Returns any new OwnCoins found.
+    pub fn process_outputs(&mut self, outputs: &[Output], holder: &Holder) -> Vec<OwnCoin> {
+        let mut found = vec![];
+
+        for output in outputs {
+            self.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+            let Ok(note) = output.note.decrypt::<MoneyNote>(&self.keypair.secret) else { continue };
+
+            let owncoin = OwnCoin {
+                coin: output.coin,
+                note: note.clone(),
+                secret: self.keypair.secret,
+                leaf_position: self.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            self.unspent_money_coins.push(owncoin.clone());
+            found.push(owncoin);
+        }
+
+        found
+    }
+
+    /// Process the fee component of a transaction (if present).
+    /// Handles the fee input nullifier and fee change output.
+    /// Returns any new OwnCoins found.
+    pub fn process_fee(
+        &mut self,
+        fee_params: &Option<MoneyFeeParamsV1>,
+        holder: &Holder,
+    ) -> Vec<OwnCoin> {
+        let Some(ref fp) = fee_params else { return vec![] };
+
+        self.mark_spent_nullifier(fp.input.nullifier, holder);
+        self.process_outputs(slice::from_ref(&fp.output), holder)
     }
     }
 }
 }
 
 
@@ -247,6 +318,8 @@ impl Wallet {
 pub struct TestHarness {
 pub struct TestHarness {
     /// Initialized [`Holder`]s for this instance
     /// Initialized [`Holder`]s for this instance
     pub holders: HashMap<Holder, Wallet>,
     pub holders: HashMap<Holder, Wallet>,
+    /// Ordered list of all holder keys (for broadcast operations)
+    pub holder_keys: Vec<Holder>,
     /// Cached [`ProvingKey`]s for native contract ZK proving
     /// Cached [`ProvingKey`]s for native contract ZK proving
     pub proving_keys: HashMap<String, (ProvingKey, ZkBinary)>,
     pub proving_keys: HashMap<String, (ProvingKey, ZkBinary)>,
     /// The genesis block for this harness
     /// The genesis block for this harness
@@ -283,13 +356,15 @@ impl TestHarness {
         let sled_db = sled::Config::new().temporary(true).open()?;
         let sled_db = sled::Config::new().temporary(true).open()?;
         let overlay = BlockchainOverlay::new(&Blockchain::new(&sled_db)?)?;
         let overlay = BlockchainOverlay::new(&Blockchain::new(&sled_db)?)?;
         vks::inject(&overlay, &vks)?;
         vks::inject(&overlay, &vks)?;
-        deploy_native_contracts(&overlay, 90).await?;
+        deploy_native_contracts(&overlay, POW_TARGET).await?;
         let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&[])?;
         let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&[])?;
         genesis_block.header.state_root =
         genesis_block.header.state_root =
             overlay.lock().unwrap().contracts.update_state_monotree(&diff)?;
             overlay.lock().unwrap().contracts.update_state_monotree(&diff)?;
 
 
         // Create `Wallet` instances
         // Create `Wallet` instances
         let mut holders_map = HashMap::new();
         let mut holders_map = HashMap::new();
+        let mut holder_keys = Vec::with_capacity(holders.len());
+
         for holder in holders {
         for holder in holders {
             let keypair = Keypair::random(&mut rng);
             let keypair = Keypair::random(&mut rng);
             let token_mint_authority = Keypair::random(&mut rng);
             let token_mint_authority = Keypair::random(&mut rng);
@@ -306,35 +381,517 @@ impl TestHarness {
             .await?;
             .await?;
 
 
             holders_map.insert(*holder, wallet);
             holders_map.insert(*holder, wallet);
+            holder_keys.push(*holder);
         }
         }
 
 
-        Ok(Self { holders: holders_map, proving_keys, genesis_block, verify_fees })
+        Ok(Self { holders: holders_map, holder_keys, proving_keys, genesis_block, verify_fees })
+    }
+
+    /// Get a reference to a Holder's Wallet
+    pub fn wallet(&self, holder: &Holder) -> &Wallet {
+        self.holders.get(holder).unwrap()
     }
     }
 
 
-    /// Assert that all holders' trees are the same
+    /// Get a mutable reference to a Holder's Wallet
+    pub fn wallet_mut(&mut self, holder: &Holder) -> &mut Wallet {
+        self.holders.get_mut(holder).unwrap()
+    }
+
+    /// Get a Holder's unspent OwnCoins
+    pub fn coins(&self, holder: &Holder) -> &[OwnCoin] {
+        &self.wallet(holder).unspent_money_coins
+    }
+
+    /// Get a Holder's unspent OwnCoins filtered by token ID
+    pub fn coins_by_token(&self, holder: &Holder, token_id: TokenId) -> Vec<OwnCoin> {
+        self.coins(holder).iter().filter(|c| c.note.token_id == token_id).cloned().collect()
+    }
+
+    /// Get the total balance of a Holder for a given token
+    pub fn balance(&self, holder: &Holder, token_id: TokenId) -> u64 {
+        self.coins(holder)
+            .iter()
+            .filter(|c| c.note.token_id == token_id)
+            .map(|c| c.note.value)
+            .sum()
+    }
+
+    /// Assert that all holders' Merkle trees are consistent
     pub fn assert_trees(&self, holders: &[Holder]) {
     pub fn assert_trees(&self, holders: &[Holder]) {
-        assert!(holders.len() > 1);
-        // Gather wallets
+        assert!(!holders.is_empty());
         let mut wallets = vec![];
         let mut wallets = vec![];
         for holder in holders {
         for holder in holders {
             wallets.push(self.holders.get(holder).unwrap());
             wallets.push(self.holders.get(holder).unwrap());
         }
         }
-        // Compare trees
-        let wallet = wallets[0];
-        let money_root = wallet.money_merkle_tree.root(0).unwrap();
+
+        let money_root = wallets[0].money_merkle_tree.root(0).unwrap();
         for wallet in &wallets[1..] {
         for wallet in &wallets[1..] {
-            assert!(money_root == wallet.money_merkle_tree.root(0).unwrap());
+            assert_eq!(money_root, wallet.money_merkle_tree.root(0).unwrap());
+        }
+    }
+
+    /// Assert all registered holders' Merkle trees are consistent
+    pub fn assert_all_trees(&self) {
+        if !self.holder_keys.is_empty() {
+            self.assert_trees(&self.holder_keys);
+        }
+    }
+
+    /// Mint a token for `recipient` and execute the tx on all registered
+    /// holders.
+    /// Returns the minted `TokenId`
+    pub async fn token_mint_to_all(
+        &mut self,
+        amount: u64,
+        holder: &Holder,
+        recipient: &Holder,
+        block_height: u32,
+    ) -> Result<TokenId> {
+        let token_blind = BaseBlind::random(&mut OsRng);
+        let (tx, mint_params, auth_params, fee_params) = self
+            .token_mint(amount, holder, recipient, token_blind, None, None, block_height)
+            .await?;
+
+        // Derive the Token ID
+        let token_id = self.derive_token_id(recipient, token_blind);
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_token_mint_tx(
+                h,
+                tx.clone(),
+                &mint_params,
+                &auth_params,
+                &fee_params,
+                block_height,
+                true,
+            )
+            .await?;
+        }
+
+        self.assert_all_trees();
+
+        Ok(token_id)
+    }
+
+    /// Mint a token with a specific `token_blind` and execute on all
+    /// registered holders. Returns the [`TokenId`].
+    ///
+    /// Use this instead of `token_mint_to_all` when you need the same
+    /// token blind across multiple mints (e.g. DAO governance tokens).
+    pub async fn token_mint_with_blind_to_all(
+        &mut self,
+        amount: u64,
+        holder: &Holder,
+        recipient: &Holder,
+        token_blind: BaseBlind,
+        block_height: u32,
+    ) -> Result<TokenId> {
+        let (tx, mint_params, auth_params, fee_params) = self
+            .token_mint(amount, holder, recipient, token_blind, None, None, block_height)
+            .await?;
+
+        let token_id = self.derive_token_id(recipient, token_blind);
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_token_mint_tx(
+                h,
+                tx.clone(),
+                &mint_params,
+                &auth_params,
+                &fee_params,
+                block_height,
+                true,
+            )
+            .await?;
+        }
+        self.assert_all_trees();
+
+        Ok(token_id)
+    }
+
+    /// Transfer `amount` of `token_id` from `sender` to `recipient` and
+    /// execute the tx on all registered holders.
+    pub async fn transfer_to_all(
+        &mut self,
+        amount: u64,
+        sender: &Holder,
+        recipient: &Holder,
+        token_id: TokenId,
+        block_height: u32,
+    ) -> Result<()> {
+        let owncoins = self.coins_by_token(sender, token_id);
+        let (tx, (params, fee_params), _spent) = self
+            .transfer(amount, sender, recipient, &owncoins, token_id, block_height, false)
+            .await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_transfer_tx(h, tx.clone(), &params, &fee_params, block_height, true)
+                .await?;
         }
         }
+
+        self.assert_all_trees();
+
+        Ok(())
+    }
+
+    /// Build a genesis mint for `holder` and execute on all registered holders.
+    /// Returns the found [`OwnCoin`]s.
+    pub async fn genesis_mint_to_all(
+        &mut self,
+        holder: &Holder,
+        amounts: &[u64],
+        block_height: u32,
+    ) -> Result<Vec<OwnCoin>> {
+        let (tx, params) = self.genesis_mint(holder, amounts, None, None).await?;
+        self.genesis_mint_to_all_with(tx, &params, block_height).await
+    }
+
+    /// Execute a pre-built genesis mint transaction on all registered holders.
+    /// Useful when you need to test the transaction before broadcasting
+    /// (e.g. malicious block height checks).
+    pub async fn genesis_mint_to_all_with(
+        &mut self,
+        tx: Transaction,
+        params: &MoneyGenesisMintParamsV1,
+        block_height: u32,
+    ) -> Result<Vec<OwnCoin>> {
+        let holders = self.holder_keys.clone();
+        let mut found = vec![];
+        for h in &holders {
+            found.extend(
+                self.execute_genesis_mint_tx(h, tx.clone(), params, block_height, true).await?,
+            );
+        }
+        self.assert_all_trees();
+        Ok(found)
+    }
+
+    /// Freeze a token authority for `holder` and execute on all registered
+    /// holders.
+    pub async fn token_freeze_to_all(&mut self, holder: &Holder, block_height: u32) -> Result<()> {
+        let (tx, freeze_params, fee_params) = self.token_freeze(holder, block_height).await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_token_freeze_tx(
+                h,
+                tx.clone(),
+                &freeze_params,
+                &fee_params,
+                block_height,
+                true,
+            )
+            .await?;
+        }
+        self.assert_all_trees();
+        Ok(())
+    }
+
+    /// Generate a new block mined by `miner` and broadcast to all registered
+    /// holders. Convenience wrapper around `generate_block` that uses
+    /// `holder_keys` instead of requiring the caller to pass holders.
+    pub async fn generate_block_all(&mut self, miner: &Holder) -> Result<Vec<OwnCoin>> {
+        let holders = self.holder_keys.clone();
+        self.generate_block(miner, &holders).await
+    }
+
+    /// Consolidate all coins of `token_id` owned by `holder` into a single
+    /// coin by transferring to self, then execute on all registered holders.
+    pub async fn consolidate_to_all(
+        &mut self,
+        holder: &Holder,
+        token_id: TokenId,
+        block_height: u32,
+    ) -> Result<()> {
+        let owncoins = self.coins_by_token(holder, token_id);
+        if owncoins.len() <= 1 {
+            // Nothing to consolidate
+            return Ok(())
+        }
+
+        let total: u64 = owncoins.iter().map(|c| c.note.value).sum();
+        let (tx, (params, fee_params), _spent) =
+            self.transfer(total, holder, holder, &owncoins, token_id, block_height, false).await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_transfer_tx(h, tx.clone(), &params, &fee_params, block_height, true)
+                .await?;
+        }
+
+        self.assert_all_trees();
+
+        Ok(())
+    }
+
+    /// Perform an OTC swap between two holders and execute on all registered
+    /// holders.
+    pub async fn otc_swap_to_all(
+        &mut self,
+        holder0: &Holder,
+        coin0: &OwnCoin,
+        holder1: &Holder,
+        coin1: &OwnCoin,
+        block_height: u32,
+    ) -> Result<()> {
+        let (tx, params, fee_params) =
+            self.otc_swap(holder0, coin0, holder1, coin1, block_height).await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_otc_swap_tx(h, tx.clone(), &params, &fee_params, block_height, true)
+                .await?;
+        }
+
+        self.assert_all_trees();
+
+        Ok(())
+    }
+
+    /// Build a `Dao::Mint` transaction and execute on all registered holders.
+    #[allow(clippy::too_many_arguments)]
+    pub async fn dao_mint_to_all(
+        &mut self,
+        holder: &Holder,
+        dao: &Dao,
+        dao_notes_secret_key: &SecretKey,
+        dao_proposer_secret_key: &SecretKey,
+        dao_proposals_secret_key: &SecretKey,
+        dao_votes_secret_key: &SecretKey,
+        dao_exec_secret_key: &SecretKey,
+        dao_early_exec_secret_key: &SecretKey,
+        block_height: u32,
+    ) -> Result<()> {
+        let (tx, params, fee_params) = self
+            .dao_mint(
+                holder,
+                dao,
+                dao_notes_secret_key,
+                dao_proposer_secret_key,
+                dao_proposals_secret_key,
+                dao_votes_secret_key,
+                dao_exec_secret_key,
+                dao_early_exec_secret_key,
+                block_height,
+            )
+            .await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_dao_mint_tx(h, tx.clone(), &params, &fee_params, block_height, true)
+                .await?;
+        }
+        self.assert_all_trees();
+        Ok(())
+    }
+
+    /// Build a `Dao::Propose` (transfer) transaction and execute on all
+    /// registered holders. Returns the [`DaoProposal`] for subsequent
+    /// voting/execution.
+    #[allow(clippy::too_many_arguments)]
+    pub async fn dao_propose_transfer_to_all(
+        &mut self,
+        proposer: &Holder,
+        proposal_coinattrs: &[CoinAttributes],
+        user_data: pallas::Base,
+        dao: &Dao,
+        dao_proposer_secret_key: &SecretKey,
+        block_height: u32,
+        duration_blockwindows: u64,
+    ) -> Result<DaoProposal> {
+        let (tx, params, fee_params, proposal_info) = self
+            .dao_propose_transfer(
+                proposer,
+                proposal_coinattrs,
+                user_data,
+                dao,
+                dao_proposer_secret_key,
+                block_height,
+                duration_blockwindows,
+            )
+            .await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_dao_propose_tx(h, tx.clone(), &params, &fee_params, block_height, true)
+                .await?;
+        }
+        self.assert_all_trees();
+        Ok(proposal_info)
+    }
+
+    /// Build a `Dao::Propose` (generic) transaction and execute on all
+    /// registered holders. Returns the [`DaoProposal`].
+    pub async fn dao_propose_generic_to_all(
+        &mut self,
+        proposer: &Holder,
+        user_data: pallas::Base,
+        dao: &Dao,
+        dao_proposer_secret_key: &SecretKey,
+        block_height: u32,
+        duration_blockwindows: u64,
+    ) -> Result<DaoProposal> {
+        let (tx, params, fee_params, proposal_info) = self
+            .dao_propose_generic(
+                proposer,
+                user_data,
+                dao,
+                dao_proposer_secret_key,
+                block_height,
+                duration_blockwindows,
+            )
+            .await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_dao_propose_tx(h, tx.clone(), &params, &fee_params, block_height, true)
+                .await?;
+        }
+        self.assert_all_trees();
+        Ok(proposal_info)
+    }
+
+    /// Build and broadcast a single `Dao::Vote` transaction on all registered
+    /// holders. Returns the [`DaoVoteParams`] (needed for vote counting).
+    pub async fn dao_vote_to_all(
+        &mut self,
+        voter: &Holder,
+        vote_option: bool,
+        dao: &Dao,
+        proposal: &DaoProposal,
+        block_height: u32,
+    ) -> Result<DaoVoteParams> {
+        let (tx, vote_params, fee_params) =
+            self.dao_vote(voter, vote_option, dao, proposal, block_height).await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_dao_vote_tx(h, tx.clone(), &fee_params, block_height, true).await?;
+        }
+        self.assert_all_trees();
+        Ok(vote_params)
+    }
+
+    /// Build and broadcast a `Dao::Exec` (transfer) transaction on all
+    /// registered holders.
+    #[allow(clippy::too_many_arguments)]
+    pub async fn dao_exec_transfer_to_all(
+        &mut self,
+        executor: &Holder,
+        dao: &Dao,
+        dao_exec_secret_key: &SecretKey,
+        dao_early_exec_secret_key: &Option<SecretKey>,
+        proposal_info: &DaoProposal,
+        proposal_coinattrs: Vec<CoinAttributes>,
+        total_yes_vote_value: u64,
+        total_all_vote_value: u64,
+        total_yes_vote_blind: ScalarBlind,
+        total_all_vote_blind: ScalarBlind,
+        block_height: u32,
+    ) -> Result<()> {
+        let (tx, xfer_params, fee_params) = self
+            .dao_exec_transfer(
+                executor,
+                dao,
+                dao_exec_secret_key,
+                dao_early_exec_secret_key,
+                proposal_info,
+                proposal_coinattrs,
+                total_yes_vote_value,
+                total_all_vote_value,
+                total_yes_vote_blind,
+                total_all_vote_blind,
+                block_height,
+            )
+            .await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_dao_exec_tx(
+                h,
+                tx.clone(),
+                Some(&xfer_params),
+                &fee_params,
+                block_height,
+                true,
+            )
+            .await?;
+        }
+        self.assert_all_trees();
+        Ok(())
+    }
+
+    /// Build and broadcast a `Dao::Exec` (generic) transaction on all
+    /// registered holders.
+    #[allow(clippy::too_many_arguments)]
+    pub async fn dao_exec_generic_to_all(
+        &mut self,
+        executor: &Holder,
+        dao: &Dao,
+        dao_exec_secret_key: &SecretKey,
+        dao_early_exec_secret_key: &Option<SecretKey>,
+        proposal_info: &DaoProposal,
+        total_yes_vote_value: u64,
+        total_all_vote_value: u64,
+        total_yes_vote_blind: ScalarBlind,
+        total_all_vote_blind: ScalarBlind,
+        block_height: u32,
+    ) -> Result<()> {
+        let (tx, fee_params) = self
+            .dao_exec_generic(
+                executor,
+                dao,
+                dao_exec_secret_key,
+                dao_early_exec_secret_key,
+                proposal_info,
+                total_yes_vote_value,
+                total_all_vote_value,
+                total_yes_vote_blind,
+                total_all_vote_blind,
+                block_height,
+            )
+            .await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_dao_exec_tx(h, tx.clone(), None, &fee_params, block_height, true).await?;
+        }
+        self.assert_all_trees();
+        Ok(())
+    }
+
+    /// Derive the [`TokenId`] that a given holder's `token_mint_authority`
+    /// would produce with a given blind.
+    pub fn derive_token_id(&self, holder: &Holder, token_blind: BaseBlind) -> TokenId {
+        let wallet = self.wallet(holder);
+        let mint_authority = wallet.token_mint_authority;
+
+        let auth_func_id = FuncRef {
+            contract_id: *MONEY_CONTRACT_ID,
+            func_code: MoneyFunction::AuthTokenMintV1 as u8,
+        }
+        .to_func_id();
+
+        let token_attrs = TokenAttributes {
+            auth_parent: auth_func_id,
+            user_data: poseidon_hash([mint_authority.public.x(), mint_authority.public.y()]),
+            blind: token_blind,
+        };
+
+        token_attrs.to_token_id()
     }
     }
 }
 }
 
 
 async fn benchmark_wasm_calls(
 async fn benchmark_wasm_calls(
     callname: &str,
     callname: &str,
-    validator: &ValidatorPtr,
+    validator: ValidatorPtr,
     tx: &Transaction,
     tx: &Transaction,
     block_height: u32,
     block_height: u32,
 ) -> Result<()> {
 ) -> Result<()> {
-    let mut file = std::fs::OpenOptions::new().create(true).append(true).open("bench.csv")?;
+    let mut file = OpenOptions::new().create(true).append(true).open("bench.csv")?;
 
 
     let validator = validator.read().await;
     let validator = validator.read().await;
     for (idx, call) in tx.calls.iter().enumerate() {
     for (idx, call) in tx.calls.iter().enumerate() {
@@ -356,29 +913,29 @@ async fn benchmark_wasm_calls(
         tx.calls.encode(&mut payload)?;
         tx.calls.encode(&mut payload)?;
 
 
         let mut times = [0; 3];
         let mut times = [0; 3];
-        let now = std::time::Instant::now();
+        let now = Instant::now();
         let _metadata = runtime.metadata(&payload)?;
         let _metadata = runtime.metadata(&payload)?;
         times[0] = now.elapsed().as_micros();
         times[0] = now.elapsed().as_micros();
 
 
-        let now = std::time::Instant::now();
+        let now = Instant::now();
         let mut update = vec![call.data.data[0]];
         let mut update = vec![call.data.data[0]];
         update.append(&mut runtime.exec(&payload)?);
         update.append(&mut runtime.exec(&payload)?);
         times[1] = now.elapsed().as_micros();
         times[1] = now.elapsed().as_micros();
 
 
-        let now = std::time::Instant::now();
+        let now = Instant::now();
         runtime.apply(&update)?;
         runtime.apply(&update)?;
         times[2] = now.elapsed().as_micros();
         times[2] = now.elapsed().as_micros();
 
 
         writeln!(
         writeln!(
             file,
             file,
-            "{}, {}, {}, {}, {}, {}, {}",
+            "{},{},{},{},{},{},{}",
             callname,
             callname,
             tx.hash(),
             tx.hash(),
             idx,
             idx,
             times[0],
             times[0],
             times[1],
             times[1],
             times[2],
             times[2],
-            serialize(tx).len()
+            serialize(tx).len(),
         )?;
         )?;
     }
     }
 
 

+ 17 - 52
src/contract/test-harness/src/money_fee.rs

@@ -36,14 +36,14 @@ use darkfi_money_contract::{
 use darkfi_sdk::{
 use darkfi_sdk::{
     crypto::{
     crypto::{
         contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, BaseBlind, Blind, FuncId,
         contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, BaseBlind, Blind, FuncId,
-        MerkleNode, ScalarBlind, SecretKey,
+        ScalarBlind, SecretKey,
     },
     },
     pasta::pallas,
     pasta::pallas,
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
+use darkfi_serial::Encodable;
 use rand::rngs::OsRng;
 use rand::rngs::OsRng;
-use tracing::{debug, info};
+use tracing::info;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -55,7 +55,7 @@ impl TestHarness {
         &mut self,
         &mut self,
         holder: &Holder,
         holder: &Holder,
     ) -> Result<(Transaction, MoneyFeeParamsV1)> {
     ) -> Result<(Transaction, MoneyFeeParamsV1)> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
 
 
         // Compute fee call required fee
         // Compute fee call required fee
         let required_fee = compute_fee(&FEE_CALL_GAS);
         let required_fee = compute_fee(&FEE_CALL_GAS);
@@ -92,7 +92,7 @@ impl TestHarness {
         // Generate an ephemeral signing key
         // Generate an ephemeral signing key
         let signature_secret = SecretKey::random(&mut OsRng);
         let signature_secret = SecretKey::random(&mut OsRng);
 
 
-        info!("Creting FeeV1 ZK proof");
+        info!("Creating FeeV1 ZK proof");
         let (fee_pk, fee_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_FEE_NS_V1).unwrap();
         let (fee_pk, fee_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_FEE_NS_V1).unwrap();
 
 
         let (proof, public_inputs) = create_fee_proof(
         let (proof, public_inputs) = create_fee_proof(
@@ -143,8 +143,8 @@ impl TestHarness {
         };
         };
 
 
         let mut data = vec![MoneyFunction::FeeV1 as u8];
         let mut data = vec![MoneyFunction::FeeV1 as u8];
-        required_fee.encode_async(&mut data).await?;
-        params.encode_async(&mut data).await?;
+        required_fee.encode(&mut data)?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![proof] }, vec![])?;
             TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![proof] }, vec![])?;
@@ -155,7 +155,7 @@ impl TestHarness {
         Ok((tx, params))
         Ok((tx, params))
     }
     }
 
 
-    /// Execute the transaction created by `create_empty_fee_call()` for a given [`Holder`]
+    /// Execute the transaction created by `create_empty_fee_call()` for a given [`Holder`].
     ///
     ///
     /// Returns any found [`OwnCoin`]s.
     /// Returns any found [`OwnCoin`]s.
     pub async fn execute_empty_fee_call_tx(
     pub async fn execute_empty_fee_call_tx(
@@ -165,44 +165,9 @@ impl TestHarness {
         params: &MoneyFeeParamsV1,
         params: &MoneyFeeParamsV1,
         block_height: u32,
         block_height: u32,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
-
-        let nullifier = params.input.nullifier.inner();
-        wallet
-            .money_null_smt
-            .insert_batch(vec![(nullifier, nullifier)])
-            .expect("smt.insert_batch()");
-
+        let wallet = self.wallet_mut(holder);
         wallet.add_transaction("money::fee", tx, block_height).await?;
         wallet.add_transaction("money::fee", tx, block_height).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])
+        Ok(wallet.process_fee(&Some(params.clone()), holder))
     }
     }
 
 
     /// Create and append a `Money::Fee` call to a given [`Transaction`] for
     /// Create and append a `Money::Fee` call to a given [`Transaction`] for
@@ -218,9 +183,9 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         spent_coins: &[OwnCoin],
         spent_coins: &[OwnCoin],
     ) -> Result<(ContractCall, Vec<Proof>, Vec<SecretKey>, Vec<OwnCoin>, MoneyFeeParamsV1)> {
     ) -> 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();
+        // First we verify the fee-less transaction to see how much gas it
+        // uses for execution and verification.
+        let wallet = self.wallet(holder);
         let validator = wallet.validator.read().await;
         let validator = wallet.validator.read().await;
         let gas_used = validator
         let gas_used = validator
             .add_test_transactions(
             .add_test_transactions(
@@ -236,8 +201,8 @@ impl TestHarness {
         // Compute the required fee
         // Compute the required fee
         let required_fee = compute_fee(&(gas_used + FEE_CALL_GAS));
         let required_fee = compute_fee(&(gas_used + FEE_CALL_GAS));
 
 
-        // Knowing the total gas, we can now find an OwnCoin of enough value
-        // so that we can create a valid Money::Fee call.
+        // 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 spent_coins: HashSet<&OwnCoin, RandomState> = HashSet::from_iter(spent_coins);
         let mut available_coins = wallet.unspent_money_coins.clone();
         let mut available_coins = wallet.unspent_money_coins.clone();
         available_coins
         available_coins
@@ -325,8 +290,8 @@ impl TestHarness {
 
 
         // Encode the contract call
         // Encode the contract call
         let mut data = vec![MoneyFunction::FeeV1 as u8];
         let mut data = vec![MoneyFunction::FeeV1 as u8];
-        required_fee.encode_async(&mut data).await?;
-        params.encode_async(&mut data).await?;
+        required_fee.encode(&mut data)?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
 
         Ok((call, vec![proof], vec![signature_secret], vec![coin.clone()], params))
         Ok((call, vec![proof], vec![signature_secret], vec![coin.clone()], params))

+ 7 - 31
src/contract/test-harness/src/money_genesis_mint.rs

@@ -21,17 +21,16 @@ use darkfi::{
     Result,
     Result,
 };
 };
 use darkfi_money_contract::{
 use darkfi_money_contract::{
-    client::{genesis_mint_v1::GenesisMintCallBuilder, MoneyNote, OwnCoin},
+    client::{genesis_mint_v1::GenesisMintCallBuilder, OwnCoin},
     model::MoneyGenesisMintParamsV1,
     model::MoneyGenesisMintParamsV1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 };
 use darkfi_sdk::{
 use darkfi_sdk::{
-    crypto::{contract_id::MONEY_CONTRACT_ID, FuncId, MerkleNode},
+    crypto::{contract_id::MONEY_CONTRACT_ID, FuncId},
     pasta::pallas,
     pasta::pallas,
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
-use tracing::debug;
+use darkfi_serial::Encodable;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -46,7 +45,7 @@ impl TestHarness {
         spend_hook: Option<FuncId>,
         spend_hook: Option<FuncId>,
         user_data: Option<pallas::Base>,
         user_data: Option<pallas::Base>,
     ) -> Result<(Transaction, MoneyGenesisMintParamsV1)> {
     ) -> Result<(Transaction, MoneyGenesisMintParamsV1)> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
 
 
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
 
 
@@ -65,7 +64,7 @@ impl TestHarness {
 
 
         // Encode and build the transaction
         // Encode and build the transaction
         let mut data = vec![MoneyFunction::GenesisMintV1 as u8];
         let mut data = vec![MoneyFunction::GenesisMintV1 as u8];
-        debris.params.encode_async(&mut data).await?;
+        debris.params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
@@ -89,35 +88,12 @@ impl TestHarness {
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
         let wallet = self.holders.get_mut(holder).unwrap();
         let wallet = self.holders.get_mut(holder).unwrap();
 
 
-        // Execute the transaction
         wallet.add_transaction("money::genesis_mint", tx, block_height).await?;
         wallet.add_transaction("money::genesis_mint", tx, block_height).await?;
 
 
         if !append {
         if !append {
-            return Ok(vec![])
+            return Ok(vec![]);
         }
         }
 
 
-        // Iterate over call outputs to find any new OwnCoins
-        let mut found_owncoins = vec![];
-        for output in &params.outputs {
-            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(found_owncoins)
+        Ok(wallet.process_outputs(&params.outputs, holder))
     }
     }
 }
 }

+ 14 - 52
src/contract/test-harness/src/money_otc_swap.rs

@@ -24,18 +24,17 @@ use darkfi::{
     Result,
     Result,
 };
 };
 use darkfi_money_contract::{
 use darkfi_money_contract::{
-    client::{swap_v1::SwapCallBuilder, MoneyNote, OwnCoin},
+    client::{swap_v1::SwapCallBuilder, OwnCoin},
     model::{MoneyFeeParamsV1, MoneyTransferParamsV1},
     model::{MoneyFeeParamsV1, MoneyTransferParamsV1},
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 };
 use darkfi_sdk::{
 use darkfi_sdk::{
-    crypto::{contract_id::MONEY_CONTRACT_ID, BaseBlind, Blind, FuncId, MerkleNode},
+    crypto::{contract_id::MONEY_CONTRACT_ID, BaseBlind, Blind, FuncId},
     pasta::pallas,
     pasta::pallas,
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
+use darkfi_serial::Encodable;
 use rand::rngs::OsRng;
 use rand::rngs::OsRng;
-use tracing::debug;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -51,8 +50,8 @@ impl TestHarness {
         owncoin1: &OwnCoin,
         owncoin1: &OwnCoin,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, MoneyTransferParamsV1, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, MoneyTransferParamsV1, Option<MoneyFeeParamsV1>)> {
-        let wallet0 = self.holders.get(holder0).unwrap();
-        let wallet1 = self.holders.get(holder1).unwrap();
+        let wallet0 = self.wallet(holder0);
+        let wallet1 = self.wallet(holder1);
 
 
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
@@ -131,7 +130,7 @@ impl TestHarness {
 
 
         // Encode the contract call
         // Encode the contract call
         let mut data = vec![MoneyFunction::OtcSwapV1 as u8];
         let mut data = vec![MoneyFunction::OtcSwapV1 as u8];
-        swap_full_params.encode_async(&mut data).await?;
+        swap_full_params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: swap_full_proofs }, vec![])?;
             TransactionBuilder::new(ContractCallLeaf { call, proofs: swap_full_proofs }, vec![])?;
@@ -185,60 +184,23 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("money::otc_swap", tx, block_height).await?;
         wallet.add_transaction("money::otc_swap", tx, block_height).await?;
 
 
-        let mut found_owncoins = vec![];
-
         if !append {
         if !append {
-            return Ok(found_owncoins)
+            return Ok(vec![]);
         }
         }
 
 
+        // Combine swap inputs/outputs with fee inputs/outputs, then process
         let mut inputs = swap_params.inputs.to_vec();
         let mut inputs = swap_params.inputs.to_vec();
         let mut outputs = swap_params.outputs.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());
-        }
-
-        let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
-        wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()");
-
-        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());
-            }
-        }
-
-        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);
+        if let Some(ref fp) = fee_params {
+            inputs.push(fp.input.clone());
+            outputs.push(fp.output.clone());
         }
         }
 
 
-        Ok(found_owncoins)
+        wallet.process_inputs(&inputs, holder);
+        Ok(wallet.process_outputs(&outputs, holder))
     }
     }
 }
 }

+ 16 - 31
src/contract/test-harness/src/money_pow_reward.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
+use std::slice;
+
 use darkfi::{
 use darkfi::{
     blockchain::{BlockInfo, BlockchainOverlay, Header},
     blockchain::{BlockInfo, BlockchainOverlay, Header},
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
@@ -23,15 +25,15 @@ use darkfi::{
     Result,
     Result,
 };
 };
 use darkfi_money_contract::{
 use darkfi_money_contract::{
-    client::{pow_reward_v1::PoWRewardCallBuilder, MoneyNote, OwnCoin},
+    client::{pow_reward_v1::PoWRewardCallBuilder, OwnCoin},
     model::MoneyPoWRewardParamsV1,
     model::MoneyPoWRewardParamsV1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 };
 use darkfi_sdk::{
 use darkfi_sdk::{
-    crypto::{contract_id::MONEY_CONTRACT_ID, MerkleNode, MerkleTree},
+    crypto::{contract_id::MONEY_CONTRACT_ID, MerkleTree},
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
+use darkfi_serial::Encodable;
 use tracing::info;
 use tracing::info;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
@@ -39,7 +41,8 @@ use super::{Holder, TestHarness};
 impl TestHarness {
 impl TestHarness {
     /// Create a `Money::PoWReward` transaction for a given [`Holder`].
     /// Create a `Money::PoWReward` transaction for a given [`Holder`].
     ///
     ///
-    /// Optionally takes a specific reward recipient and a nonstandard reward value.
+    /// Optionally takes a specific reward recipient and a nonstandard
+    /// reward value.
     /// Returns the created [`Transaction`] and [`MoneyPoWRewardParamsV1`].
     /// Returns the created [`Transaction`] and [`MoneyPoWRewardParamsV1`].
     async fn pow_reward(
     async fn pow_reward(
         &mut self,
         &mut self,
@@ -48,7 +51,7 @@ impl TestHarness {
         reward: Option<u64>,
         reward: Option<u64>,
         fees: Option<u64>,
         fees: Option<u64>,
     ) -> Result<(Transaction, MoneyPoWRewardParamsV1)> {
     ) -> Result<(Transaction, MoneyPoWRewardParamsV1)> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
 
 
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
 
 
@@ -56,11 +59,7 @@ impl TestHarness {
         let last_block = wallet.validator.read().await.blockchain.last_block()?;
         let last_block = wallet.validator.read().await.blockchain.last_block()?;
 
 
         // If there's a set reward recipient, use it, otherwise reward the holder
         // If there's a set reward recipient, use it, otherwise reward the holder
-        let recipient = if let Some(holder) = recipient {
-            Some(self.holders.get(holder).unwrap().keypair.public)
-        } else {
-            None
-        };
+        let recipient = recipient.map(|holder| self.wallet(holder).keypair.public);
 
 
         // If there's fees paid, use them, otherwise set to zero
         // If there's fees paid, use them, otherwise set to zero
         let fees = fees.unwrap_or_default();
         let fees = fees.unwrap_or_default();
@@ -84,7 +83,7 @@ impl TestHarness {
 
 
         // Encode the transaction
         // Encode the transaction
         let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
         let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
-        debris.params.encode_async(&mut data).await?;
+        debris.params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
             TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
@@ -95,7 +94,7 @@ impl TestHarness {
         Ok((tx, debris.params))
         Ok((tx, debris.params))
     }
     }
 
 
-    /// Generate and add an empty block to the given [`Holder`]s blockchains.
+    /// Generate and add an empty block to the given [`Holder`]'s blockchains.
     /// The `miner` holder will produce the block and receive the reward.
     /// The `miner` holder will produce the block and receive the reward.
     ///
     ///
     /// Returns any found [`OwnCoin`]s.
     /// Returns any found [`OwnCoin`]s.
@@ -109,7 +108,7 @@ impl TestHarness {
         let (tx, params) = self.pow_reward(miner, None, None, None).await?;
         let (tx, params) = self.pow_reward(miner, None, None, None).await?;
 
 
         // Fetch the last block in the blockchain
         // Fetch the last block in the blockchain
-        let wallet = self.holders.get(miner).unwrap();
+        let wallet = self.wallet(miner);
         let validator = wallet.validator.read().await;
         let validator = wallet.validator.read().await;
         let previous = validator.blockchain.last_block()?;
         let previous = validator.blockchain.last_block()?;
 
 
@@ -141,33 +140,19 @@ impl TestHarness {
         )
         )
         .await?;
         .await?;
         drop(validator);
         drop(validator);
+
         let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&[])?;
         let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&[])?;
         block.header.state_root = overlay.lock().unwrap().contracts.update_state_monotree(&diff)?;
         block.header.state_root = overlay.lock().unwrap().contracts.update_state_monotree(&diff)?;
 
 
         // Attach signature
         // Attach signature
         block.sign(&wallet.keypair.secret);
         block.sign(&wallet.keypair.secret);
 
 
-        // For all holders, append the block
+        // For all holders, append the block and process the reward output
         let mut found_owncoins = vec![];
         let mut found_owncoins = vec![];
         for holder in holders {
         for holder in holders {
-            let wallet = self.holders.get_mut(holder).unwrap();
+            let wallet = self.wallet_mut(holder);
             wallet.validator.write().await.add_test_blocks(&[block.clone()]).await?;
             wallet.validator.write().await.add_test_blocks(&[block.clone()]).await?;
-            wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
-
-            // Attempt to decrypt the note to see if this is a coin for the holder
-            let Ok(note) = params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
-                continue
-            };
-
-            let owncoin = OwnCoin {
-                coin: params.output.coin,
-                note: note.clone(),
-                secret: wallet.keypair.secret,
-                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
-            };
-
-            wallet.unspent_money_coins.push(owncoin.clone());
-            found_owncoins.push(owncoin);
+            found_owncoins.extend(wallet.process_outputs(slice::from_ref(&params.output), holder));
         }
         }
 
 
         Ok(found_owncoins)
         Ok(found_owncoins)

+ 16 - 94
src/contract/test-harness/src/money_token.rs

@@ -39,7 +39,7 @@ use darkfi_sdk::{
     pasta::pallas,
     pasta::pallas,
     ContractCall,
     ContractCall,
 };
 };
-use darkfi_serial::AsyncEncodable;
+use darkfi_serial::Encodable;
 use rand::rngs::OsRng;
 use rand::rngs::OsRng;
 use tracing::debug;
 use tracing::debug;
 
 
@@ -63,9 +63,9 @@ impl TestHarness {
         MoneyAuthTokenMintParamsV1,
         MoneyAuthTokenMintParamsV1,
         Option<MoneyFeeParamsV1>,
         Option<MoneyFeeParamsV1>,
     )> {
     )> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
         let mint_authority = wallet.token_mint_authority;
         let mint_authority = wallet.token_mint_authority;
-        let rcpt = self.holders.get(recipient).unwrap().keypair.public;
+        let rcpt = self.wallet(recipient).keypair.public;
 
 
         let (token_mint_pk, token_mint_zkbin) =
         let (token_mint_pk, token_mint_zkbin) =
             self.proving_keys.get(MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1).unwrap();
             self.proving_keys.get(MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1).unwrap();
@@ -109,7 +109,7 @@ impl TestHarness {
         };
         };
         let auth_debris = builder.build()?;
         let auth_debris = builder.build()?;
         let mut data = vec![MoneyFunction::AuthTokenMintV1 as u8];
         let mut data = vec![MoneyFunction::AuthTokenMintV1 as u8];
-        auth_debris.params.encode_async(&mut data).await?;
+        auth_debris.params.encode(&mut data)?;
         let auth_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let auth_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
 
         // Create the minting call
         // Create the minting call
@@ -121,7 +121,7 @@ impl TestHarness {
         };
         };
         let mint_debris = builder.build()?;
         let mint_debris = builder.build()?;
         let mut data = vec![MoneyFunction::TokenMintV1 as u8];
         let mut data = vec![MoneyFunction::TokenMintV1 as u8];
-        mint_debris.params.encode_async(&mut data).await?;
+        mint_debris.params.encode(&mut data)?;
         let mint_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let mint_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
 
         // Create the TransactionBuilder containing above calls
         // Create the TransactionBuilder containing above calls
@@ -180,35 +180,17 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("money::token_mint", tx, block_height).await?;
         wallet.add_transaction("money::token_mint", tx, block_height).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());
-                }
-            }
-        }
-
         let mut found_owncoins = vec![];
         let mut found_owncoins = vec![];
-
         if append {
         if append {
-            wallet.money_merkle_tree.append(MerkleNode::from(mint_params.coin.inner()));
+            // Process the fee input (mark spent)
+            found_owncoins.extend(wallet.process_fee(fee_params, holder));
 
 
-            // Attempt to decrypt the encrypted note of the minted token
+            // Process the minted coin output
+            wallet.money_merkle_tree.append(MerkleNode::from(mint_params.coin.inner()));
             if let Ok(note) = auth_params.enc_note.decrypt::<MoneyNote>(&wallet.keypair.secret) {
             if let Ok(note) = auth_params.enc_note.decrypt::<MoneyNote>(&wallet.keypair.secret) {
                 let owncoin = OwnCoin {
                 let owncoin = OwnCoin {
                     coin: mint_params.coin,
                     coin: mint_params.coin,
@@ -216,30 +198,9 @@ impl TestHarness {
                     secret: wallet.keypair.secret,
                     secret: wallet.keypair.secret,
                     leaf_position: wallet.money_merkle_tree.mark().unwrap(),
                     leaf_position: wallet.money_merkle_tree.mark().unwrap(),
                 };
                 };
-
                 debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
                 debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
                 wallet.unspent_money_coins.push(owncoin.clone());
                 wallet.unspent_money_coins.push(owncoin.clone());
                 found_owncoins.push(owncoin);
                 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);
-                }
             }
             }
         }
         }
 
 
@@ -252,7 +213,7 @@ impl TestHarness {
         holder: &Holder,
         holder: &Holder,
         block_height: u32,
         block_height: u32,
     ) -> Result<(Transaction, MoneyAuthTokenFreezeParamsV1, Option<MoneyFeeParamsV1>)> {
     ) -> Result<(Transaction, MoneyAuthTokenFreezeParamsV1, Option<MoneyFeeParamsV1>)> {
-        let wallet = self.holders.get(holder).unwrap();
+        let wallet = self.wallet(holder);
         let mint_authority = wallet.token_mint_authority;
         let mint_authority = wallet.token_mint_authority;
 
 
         let (auth_mint_pk, auth_mint_zkbin) =
         let (auth_mint_pk, auth_mint_zkbin) =
@@ -282,7 +243,7 @@ impl TestHarness {
         };
         };
         let freeze_debris = builder.build()?;
         let freeze_debris = builder.build()?;
         let mut data = vec![MoneyFunction::AuthTokenFreezeV1 as u8];
         let mut data = vec![MoneyFunction::AuthTokenFreezeV1 as u8];
-        freeze_debris.params.encode_async(&mut data).await?;
+        freeze_debris.params.encode(&mut data)?;
         let freeze_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let freeze_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
 
         // Create the TransactionBuilder containing the above call
         // Create the TransactionBuilder containing the above call
@@ -332,53 +293,14 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("money::token_freeze", tx, block_height).await?;
         wallet.add_transaction("money::token_freeze", tx, block_height).await?;
 
 
-        let mut found_owncoins = vec![];
-        if let Some(ref fee_params) = fee_params {
-            if append {
-                let nullifier = fee_params.input.nullifier.inner();
-                wallet
-                    .money_null_smt
-                    .insert_batch(vec![(nullifier, nullifier)])
-                    .expect("smt.insert_batch()");
-
-                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);
-                }
-            }
+        if !append {
+            return Ok(vec![]);
         }
         }
 
 
-        Ok(found_owncoins)
+        Ok(wallet.process_fee(fee_params, holder))
     }
     }
 }
 }

+ 20 - 101
src/contract/test-harness/src/money_transfer.rs

@@ -21,16 +21,12 @@ use darkfi::{
     Result,
     Result,
 };
 };
 use darkfi_money_contract::{
 use darkfi_money_contract::{
-    client::{transfer_v1::make_transfer_call, MoneyNote, OwnCoin},
+    client::{transfer_v1::make_transfer_call, OwnCoin},
     model::{MoneyFeeParamsV1, MoneyTransferParamsV1, TokenId},
     model::{MoneyFeeParamsV1, MoneyTransferParamsV1, TokenId},
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 };
-use darkfi_sdk::{
-    crypto::{contract_id::MONEY_CONTRACT_ID, MerkleNode},
-    ContractCall,
-};
-use darkfi_serial::AsyncEncodable;
-use tracing::debug;
+use darkfi_sdk::{crypto::contract_id::MONEY_CONTRACT_ID, ContractCall};
+use darkfi_serial::Encodable;
 
 
 use super::{Holder, TestHarness};
 use super::{Holder, TestHarness};
 
 
@@ -48,8 +44,8 @@ impl TestHarness {
         half_split: bool,
         half_split: bool,
     ) -> Result<(Transaction, (MoneyTransferParamsV1, Option<MoneyFeeParamsV1>), Vec<OwnCoin>)>
     ) -> Result<(Transaction, (MoneyTransferParamsV1, Option<MoneyFeeParamsV1>), Vec<OwnCoin>)>
     {
     {
-        let wallet = self.holders.get(holder).unwrap();
-        let rcpt = self.holders.get(recipient).unwrap().keypair.public;
+        let wallet = self.wallet(holder);
+        let rcpt = self.wallet(recipient).keypair.public;
 
 
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
@@ -73,20 +69,21 @@ impl TestHarness {
 
 
         // Encode the call
         // Encode the call
         let mut data = vec![MoneyFunction::TransferV1 as u8];
         let mut data = vec![MoneyFunction::TransferV1 as u8];
-        params.encode_async(&mut data).await?;
+        params.encode(&mut data)?;
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
         let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
 
 
         // Create the TransactionBuilder containing the `Transfer` call
         // Create the TransactionBuilder containing the `Transfer` call
         let mut tx_builder =
         let mut tx_builder =
             TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
             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.
+        // If we have tx fees enabled, we first have to execute the fee-less
+        // transaction 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_params = None;
         let mut fee_signature_secrets = None;
         let mut fee_signature_secrets = None;
         if self.verify_fees {
         if self.verify_fees {
@@ -118,7 +115,7 @@ impl TestHarness {
 
 
     /// Execute a `Money::Transfer` transaction for a given [`Holder`].
     /// Execute a `Money::Transfer` transaction for a given [`Holder`].
     ///
     ///
-    /// Returns any found [`OwnCoin`]s.
+    /// Returns any found [`OwnCoin`]s
     pub async fn execute_transfer_tx(
     pub async fn execute_transfer_tx(
         &mut self,
         &mut self,
         holder: &Holder,
         holder: &Holder,
@@ -128,95 +125,17 @@ impl TestHarness {
         block_height: u32,
         block_height: u32,
         append: bool,
         append: bool,
     ) -> Result<Vec<OwnCoin>> {
     ) -> Result<Vec<OwnCoin>> {
-        let wallet = self.holders.get_mut(holder).unwrap();
+        let wallet = self.wallet_mut(holder);
 
 
-        // Execute the transaction
         wallet.add_transaction("money::transfer", tx, block_height).await?;
         wallet.add_transaction("money::transfer", tx, block_height).await?;
 
 
-        // Iterate over call inputs to mark any spent coins
-        let nullifiers =
-            call_params.inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
-        wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()");
+        // Always insert nullifiers into SMT (needed for state consistency)
+        wallet.process_inputs(&call_params.inputs, holder);
 
 
         let mut found_owncoins = vec![];
         let mut found_owncoins = vec![];
         if append {
         if append {
-            for input in &call_params.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());
-                }
-            }
-
-            // Iterate over call outputs to find any new OwnCoins
-            for output in &call_params.outputs {
-                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);
-            }
-        }
-
-        // Handle fee call
-        if let Some(ref fee_params) = fee_params {
-            // Process call input to mark any spent coins
-            let nullifier = fee_params.input.nullifier.inner();
-            wallet
-                .money_null_smt
-                .insert_batch(vec![(nullifier, nullifier)])
-                .expect("smt.insert_batch()");
-
-            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());
-                }
-
-                // Process call output to find any new OwnCoins
-                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
-
-                // Attempt to decrypt the output note to see if this is a coin for the holder.
-                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);
-                };
-            }
+            found_owncoins.extend(wallet.process_outputs(&call_params.outputs, holder));
+            found_owncoins.extend(wallet.process_fee(fee_params, holder));
         }
         }
 
 
         Ok(found_owncoins)
         Ok(found_owncoins)