Explorar el Código

contract/money: Rework test harness, fix TokenMint_V1 proof.

parazyd hace 3 años
padre
commit
60755a114f

+ 3 - 0
src/contract/money/proof/token_mint_v1.zk

@@ -14,6 +14,8 @@ contract "TokenMint_V1" {
 	Base rcpt_x,
 	# Recipient's public key y coordinate
 	Base rcpt_y,
+	# Unique serial number for the minted coin
+	Base serial,
 	# Random blinding factor for the minted coin
 	Base coin_blind,
 	# Allows composing this ZK proof to invoke other contracts
@@ -45,6 +47,7 @@ circuit "TokenMint_V1" {
 		rcpt_y,
 		supply,
 		token_id,
+		serial,
 		spend_hook,
 		user_data,
 		coin_blind,

+ 1 - 0
src/contract/money/src/client/mint_v1.rs

@@ -206,6 +206,7 @@ pub(crate) fn create_token_mint_proof(
         Witness::Base(Value::known(pallas::Base::from(output.value))),
         Witness::Base(Value::known(rcpt_x)),
         Witness::Base(Value::known(rcpt_y)),
+        Witness::Base(Value::known(serial)),
         Witness::Base(Value::known(coin_blind)),
         Witness::Base(Value::known(spend_hook)),
         Witness::Base(Value::known(user_data)),

+ 1 - 1
src/contract/money/src/entrypoint/mint_v1.rs

@@ -57,7 +57,7 @@ pub(crate) fn money_mint_get_metadata_v1(
     signature_pubkeys.push(params.input.signature_public);
 
     let value_coords = params.output.value_commit.to_affine().coordinates().unwrap();
-    let token_coords = params.output.value_commit.to_affine().coordinates().unwrap();
+    let token_coords = params.output.token_commit.to_affine().coordinates().unwrap();
 
     // Since we expect a signature from the mint authority, we use those coordinates
     // as public inputs for the ZK proof:

+ 131 - 178
src/contract/money/tests/harness.rs

@@ -23,218 +23,140 @@ use darkfi::{
         TESTNET_GENESIS_TIMESTAMP, TESTNET_INITIAL_DISTRIBUTION,
     },
     tx::Transaction,
-    wallet::WalletDb,
-    zk::{empty_witnesses, ProvingKey, ZkCircuit},
+    wallet::{WalletDb, WalletPtr},
+    zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit},
     zkas::ZkBinary,
     Result,
 };
+use darkfi_money_contract::client::OwnCoin;
 use darkfi_sdk::{
-    crypto::{
-        pasta_prelude::*, ContractId, Keypair, MerkleTree, PublicKey, DARK_TOKEN_ID,
-        MONEY_CONTRACT_ID,
-    },
+    crypto::{Keypair, MerkleTree, PublicKey, DARK_TOKEN_ID, MONEY_CONTRACT_ID},
     db::SMART_CONTRACT_ZKAS_DB_NAME,
     pasta::pallas,
     ContractCall,
 };
 use darkfi_serial::{serialize, Encodable};
-use log::{info, warn};
 use rand::rngs::OsRng;
 
 use darkfi_money_contract::{
-    client::transfer_v1::TransferCallBuilder, model::MoneyTransferParamsV1 as MoneyTransferParams,
-    MoneyFunction::TransferV1 as MoneyTransfer, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
-    MONEY_CONTRACT_ZKAS_MINT_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
+    client::{mint_v1::MintCallBuilder, transfer_v1::TransferCallBuilder},
+    model::{MoneyMintParamsV1, MoneyTransferParamsV1},
+    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+    MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
 };
 
-pub fn init_logger() -> Result<()> {
+pub fn init_logger() {
     let mut cfg = simplelog::ConfigBuilder::new();
     cfg.add_filter_ignore("sled".to_string());
-    if let Err(_) = simplelog::TermLogger::init(
-        //simplelog::LevelFilter::Info,
+    cfg.add_filter_ignore("blockchain::contractstore".to_string());
+    let _ = simplelog::TermLogger::init(
         simplelog::LevelFilter::Debug,
         //simplelog::LevelFilter::Trace,
         cfg.build(),
         simplelog::TerminalMode::Mixed,
         simplelog::ColorChoice::Auto,
-    ) {
-        warn!(target: "money_harness", "Logger already initialized");
-    }
-
-    Ok(())
+    );
 }
 
-pub struct MoneyTestHarness {
-    pub faucet_kp: Keypair,
-    pub alice_kp: Keypair,
-    pub bob_kp: Keypair,
-    pub charlie_kp: Keypair,
-    pub faucet_pubkeys: Vec<PublicKey>,
-    pub faucet_state: ValidatorStatePtr,
-    pub alice_state: ValidatorStatePtr,
-    pub bob_state: ValidatorStatePtr,
-    pub charlie_state: ValidatorStatePtr,
-    pub money_contract_id: ContractId,
-    pub proving_keys: HashMap<[u8; 32], Vec<(&'static str, ProvingKey)>>,
-    pub mint_zkbin: ZkBinary,
-    pub burn_zkbin: ZkBinary,
-    pub token_mint_zkbin: ZkBinary,
-    pub mint_pk: ProvingKey,
-    pub burn_pk: ProvingKey,
-    pub token_mint_pk: ProvingKey,
-    pub faucet_merkle_tree: MerkleTree,
-    pub alice_merkle_tree: MerkleTree,
-    pub bob_merkle_tree: MerkleTree,
-    pub charlie_merkle_tree: MerkleTree,
+pub struct Wallet {
+    pub keypair: Keypair,
+    pub state: ValidatorStatePtr,
+    pub merkle_tree: MerkleTree,
+    pub wallet: WalletPtr,
+    pub coins: Vec<OwnCoin>,
+    pub spent_coins: Vec<OwnCoin>,
 }
 
-impl MoneyTestHarness {
-    pub async fn new() -> Result<Self> {
-        let faucet_kp = Keypair::random(&mut OsRng);
-        let alice_kp = Keypair::random(&mut OsRng);
-        let bob_kp = Keypair::random(&mut OsRng);
-        let charlie_kp = Keypair::random(&mut OsRng);
-        let faucet_pubkeys = vec![faucet_kp.public];
-
-        let faucet_wallet = WalletDb::new("sqlite::memory:", "foo").await?;
-        let alice_wallet = WalletDb::new("sqlite::memory:", "foo").await?;
-        let bob_wallet = WalletDb::new("sqlite::memory:", "foo").await?;
-        let charlie_wallet = WalletDb::new("sqlite::memory:", "foo").await?;
+impl Wallet {
+    async fn new(keypair: Keypair, faucet_pubkeys: &[PublicKey]) -> Result<Self> {
+        let wallet = WalletDb::new("sqlite::memory:", "foo").await?;
+        let sled_db = sled::Config::new().temporary(true).open()?;
 
-        let faucet_sled_db = sled::Config::new().temporary(true).open()?;
-        let alice_sled_db = sled::Config::new().temporary(true).open()?;
-        let bob_sled_db = sled::Config::new().temporary(true).open()?;
-        let charlie_sled_db = sled::Config::new().temporary(true).open()?;
-
-        let faucet_state = ValidatorState::new(
-            &faucet_sled_db,
+        let state = ValidatorState::new(
+            &sled_db,
             *TESTNET_BOOTSTRAP_TIMESTAMP,
             *TESTNET_GENESIS_TIMESTAMP,
             *TESTNET_GENESIS_HASH_BYTES,
             *TESTNET_INITIAL_DISTRIBUTION,
-            faucet_wallet,
-            faucet_pubkeys.clone(),
+            wallet.clone(),
+            faucet_pubkeys.to_vec(),
             false,
             false,
         )
         .await?;
 
-        let alice_state = ValidatorState::new(
-            &alice_sled_db,
-            *TESTNET_BOOTSTRAP_TIMESTAMP,
-            *TESTNET_GENESIS_TIMESTAMP,
-            *TESTNET_GENESIS_HASH_BYTES,
-            *TESTNET_INITIAL_DISTRIBUTION,
-            alice_wallet,
-            faucet_pubkeys.clone(),
-            false,
-            false,
-        )
-        .await?;
+        let merkle_tree = MerkleTree::new(100);
 
-        let bob_state = ValidatorState::new(
-            &bob_sled_db,
-            *TESTNET_BOOTSTRAP_TIMESTAMP,
-            *TESTNET_GENESIS_TIMESTAMP,
-            *TESTNET_GENESIS_HASH_BYTES,
-            *TESTNET_INITIAL_DISTRIBUTION,
-            bob_wallet,
-            faucet_pubkeys.clone(),
-            false,
-            false,
-        )
-        .await?;
+        let coins = vec![];
+        let spent_coins = vec![];
 
-        let charlie_state = ValidatorState::new(
-            &charlie_sled_db,
-            *TESTNET_BOOTSTRAP_TIMESTAMP,
-            *TESTNET_GENESIS_TIMESTAMP,
-            *TESTNET_GENESIS_HASH_BYTES,
-            *TESTNET_INITIAL_DISTRIBUTION,
-            charlie_wallet,
-            faucet_pubkeys.clone(),
-            false,
-            false,
-        )
-        .await?;
+        Ok(Self { keypair, state, merkle_tree, wallet, coins, spent_coins })
+    }
+}
+
+pub struct MoneyTestHarness {
+    pub faucet: Wallet,
+    pub alice: Wallet,
+    pub bob: Wallet,
+    pub charlie: Wallet,
+    pub proving_keys: HashMap<&'static str, (ProvingKey, ZkBinary)>,
+}
+
+impl MoneyTestHarness {
+    pub async fn new() -> Result<Self> {
+        let faucet_kp = Keypair::random(&mut OsRng);
+        let faucet_pubkeys = vec![faucet_kp.public];
+        let faucet = Wallet::new(faucet_kp, &faucet_pubkeys).await?;
 
-        let money_contract_id = *MONEY_CONTRACT_ID;
+        let alice_kp = Keypair::random(&mut OsRng);
+        let alice = Wallet::new(alice_kp, &faucet_pubkeys).await?;
 
-        let alice_sled = alice_state.read().await.blockchain.sled_db.clone();
-        let db_handle = alice_state.read().await.blockchain.contracts.lookup(
+        let bob_kp = Keypair::random(&mut OsRng);
+        let bob = Wallet::new(bob_kp, &faucet_pubkeys).await?;
+
+        let charlie_kp = Keypair::random(&mut OsRng);
+        let charlie = Wallet::new(charlie_kp, &faucet_pubkeys).await?;
+
+        // Get the zkas circuits and build proving keys
+        let mut proving_keys = HashMap::new();
+        let alice_sled = alice.state.read().await.blockchain.sled_db.clone();
+        let db_handle = alice.state.read().await.blockchain.contracts.lookup(
             &alice_sled,
-            &money_contract_id,
+            &MONEY_CONTRACT_ID,
             SMART_CONTRACT_ZKAS_DB_NAME,
         )?;
 
-        let mint_zkbin = db_handle.get(&serialize(&MONEY_CONTRACT_ZKAS_MINT_NS_V1))?.unwrap();
-        let burn_zkbin = db_handle.get(&serialize(&MONEY_CONTRACT_ZKAS_BURN_NS_V1))?.unwrap();
-        let token_mint_zkbin =
-            db_handle.get(&serialize(&MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1))?.unwrap();
-        info!(target: "money_harness", "Decoding bincode");
-        let mint_zkbin = ZkBinary::decode(&mint_zkbin)?;
-        let burn_zkbin = ZkBinary::decode(&burn_zkbin)?;
-        let token_mint_zkbin = ZkBinary::decode(&token_mint_zkbin)?;
-        let mint_witnesses = empty_witnesses(&mint_zkbin);
-        let burn_witnesses = empty_witnesses(&burn_zkbin);
-        let token_mint_witnesses = empty_witnesses(&token_mint_zkbin);
-        let mint_circuit = ZkCircuit::new(mint_witnesses, mint_zkbin.clone());
-        let burn_circuit = ZkCircuit::new(burn_witnesses, burn_zkbin.clone());
-        let token_mint_circuit = ZkCircuit::new(token_mint_witnesses, token_mint_zkbin.clone());
-
-        info!(target: "money_harness", "Creating zk proving keys");
-        let k = 13;
-        let mut proving_keys = HashMap::<[u8; 32], Vec<(&str, ProvingKey)>>::new();
-        let mint_pk = ProvingKey::build(k, &mint_circuit);
-        let burn_pk = ProvingKey::build(k, &burn_circuit);
-        let token_mint_pk = ProvingKey::build(k, &token_mint_circuit);
-        let pks = vec![
-            (MONEY_CONTRACT_ZKAS_MINT_NS_V1, mint_pk.clone()),
-            (MONEY_CONTRACT_ZKAS_BURN_NS_V1, burn_pk.clone()),
-            (MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1, mint_pk.clone()),
-        ];
-        proving_keys.insert(money_contract_id.inner().to_repr(), pks);
+        macro_rules! mkpk {
+            ($ns:expr) => {
+                let zkbin = db_handle.get(&serialize(&$ns))?.unwrap();
+                let zkbin = ZkBinary::decode(&zkbin)?;
+                let witnesses = empty_witnesses(&zkbin);
+                let circuit = ZkCircuit::new(witnesses, zkbin.clone());
+                let pk = ProvingKey::build(13, &circuit);
+                proving_keys.insert($ns, (pk, zkbin));
+            };
+        }
 
-        let faucet_merkle_tree = MerkleTree::new(100);
-        let alice_merkle_tree = MerkleTree::new(100);
-        let bob_merkle_tree = MerkleTree::new(100);
-        let charlie_merkle_tree = MerkleTree::new(100);
+        mkpk!(MONEY_CONTRACT_ZKAS_MINT_NS_V1);
+        mkpk!(MONEY_CONTRACT_ZKAS_BURN_NS_V1);
+        mkpk!(MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1);
+        mkpk!(MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1);
 
-        Ok(Self {
-            faucet_kp,
-            alice_kp,
-            bob_kp,
-            charlie_kp,
-            faucet_pubkeys,
-            faucet_state,
-            alice_state,
-            bob_state,
-            charlie_state,
-            money_contract_id,
-            proving_keys,
-            mint_pk,
-            burn_pk,
-            token_mint_pk,
-            mint_zkbin,
-            burn_zkbin,
-            token_mint_zkbin,
-            faucet_merkle_tree,
-            alice_merkle_tree,
-            bob_merkle_tree,
-            charlie_merkle_tree,
-        })
+        Ok(Self { faucet, alice, bob, charlie, proving_keys })
     }
 
-    pub fn airdrop(
+    pub fn airdrop_native(
         &self,
-        amount: u64,
-        rcpt: PublicKey,
-    ) -> Result<(Transaction, MoneyTransferParams)> {
-        // TODO: verify change usage is correct
-        let call_debris = TransferCallBuilder {
-            keypair: self.faucet_kp,
-            recipient: rcpt,
-            value: amount,
+        value: u64,
+        recipient: PublicKey,
+    ) -> Result<(Transaction, MoneyTransferParamsV1)> {
+        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 builder = TransferCallBuilder {
+            keypair: self.faucet.keypair,
+            recipient,
+            value,
             token_id: *DARK_TOKEN_ID,
             rcpt_spend_hook: pallas::Base::zero(),
             rcpt_user_data: pallas::Base::zero(),
@@ -243,25 +165,56 @@ impl MoneyTestHarness {
             change_user_data: pallas::Base::zero(),
             change_user_data_blind: pallas::Base::random(&mut OsRng),
             coins: vec![],
-            tree: self.faucet_merkle_tree.clone(),
-            mint_zkbin: self.mint_zkbin.clone(),
-            mint_pk: self.mint_pk.clone(),
-            burn_zkbin: self.burn_zkbin.clone(),
-            burn_pk: self.burn_pk.clone(),
+            tree: self.faucet.merkle_tree.clone(),
+            mint_zkbin: mint_zkbin.clone(),
+            mint_pk: mint_pk.clone(),
+            burn_zkbin: burn_zkbin.clone(),
+            burn_pk: burn_pk.clone(),
             clear_input: true,
-        }
-        .build()?;
+        };
 
-        let contract_id = *MONEY_CONTRACT_ID;
+        let debris = builder.build()?;
 
-        let mut data = vec![MoneyTransfer as u8];
-        call_debris.params.encode(&mut data)?;
-        let calls = vec![ContractCall { contract_id, data }];
-        let proofs = vec![call_debris.proofs];
+        let mut data = vec![MoneyFunction::TransferV1 as u8];
+        debris.params.encode(&mut data)?;
+        let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
+        let proofs = vec![debris.proofs];
+        let mut tx = Transaction { calls, proofs, signatures: vec![] };
+        let sigs = tx.create_sigs(&mut OsRng, &debris.signature_secrets)?;
+        tx.signatures = vec![sigs];
+
+        Ok((tx, debris.params))
+    }
+
+    pub fn mint_token(
+        &self,
+        mint_authority: Keypair,
+        amount: u64,
+        recipient: PublicKey,
+    ) -> Result<(Transaction, MoneyMintParamsV1)> {
+        let (token_mint_pk, token_mint_zkbin) =
+            self.proving_keys.get(&MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1).unwrap();
+
+        let builder = MintCallBuilder {
+            mint_authority,
+            recipient,
+            amount,
+            spend_hook: pallas::Base::zero(),
+            user_data: pallas::Base::zero(),
+            token_mint_zkbin: token_mint_zkbin.clone(),
+            token_mint_pk: token_mint_pk.clone(),
+        };
+
+        let debris = builder.build()?;
+
+        let mut data = vec![MoneyFunction::MintV1 as u8];
+        debris.params.encode(&mut data)?;
+        let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
+        let proofs = vec![debris.proofs];
         let mut tx = Transaction { calls, proofs, signatures: vec![] };
-        let sigs = tx.create_sigs(&mut OsRng, &call_debris.signature_secrets)?;
+        let sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?;
         tx.signatures = vec![sigs];
 
-        Ok((tx, call_debris.params))
+        Ok((tx, debris.params))
     }
 }

+ 167 - 0
src/contract/money/tests/integration.rs

@@ -0,0 +1,167 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//! Integration test for functionalities of the money smart contract:
+//!
+//! * Airdrops of the native token from the faucet
+//! * Arbitrary token minting
+//! * Transfers/Payments
+//! * Atomic swaps
+//! * Token mint freezing
+//!
+//! With this test we want to confirm the money contract state transitions
+//! work between multiple parties and are able to be verified.
+//!
+//! TODO: Malicious cases
+
+use darkfi::{tx::Transaction, Result};
+use darkfi_sdk::{
+    crypto::{poseidon_hash, Keypair, MerkleNode, Nullifier, MONEY_CONTRACT_ID},
+    incrementalmerkletree::Tree,
+    ContractCall,
+};
+use darkfi_serial::Encodable;
+use log::info;
+use rand::rngs::OsRng;
+
+use darkfi_money_contract::{
+    client::{freeze_v1::FreezeCallBuilder, MoneyNote, OwnCoin},
+    MoneyFunction, MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1,
+};
+
+mod harness;
+use harness::{init_logger, MoneyTestHarness};
+
+#[async_std::test]
+async fn money_integration() -> Result<()> {
+    init_logger();
+
+    let mut th = MoneyTestHarness::new().await?;
+
+    // Let's first airdrop some tokens to Alice.
+    let (alice_airdrop_tx, alice_airdrop_params) =
+        th.airdrop_native(200, th.alice.keypair.public)?;
+
+    info!("[Faucet] Executing Alice airdrop tx");
+    th.faucet.state.read().await.verify_transactions(&[alice_airdrop_tx.clone()], true).await?;
+    th.faucet.merkle_tree.append(&MerkleNode::from(alice_airdrop_params.outputs[0].coin.inner()));
+
+    info!("[Alice] Executing Alice airdrop tx");
+    th.alice.state.read().await.verify_transactions(&[alice_airdrop_tx.clone()], true).await?;
+    th.alice.merkle_tree.append(&MerkleNode::from(alice_airdrop_params.outputs[0].coin.inner()));
+    // Alice has to witness this coin because it's hers.
+    let leaf_position = th.alice.merkle_tree.witness().unwrap();
+
+    info!("[Bob] Executing Alice airdrop tx");
+    th.bob.state.read().await.verify_transactions(&[alice_airdrop_tx.clone()], true).await?;
+    th.bob.merkle_tree.append(&MerkleNode::from(alice_airdrop_params.outputs[0].coin.inner()));
+
+    info!("[Charlie] Executing Alice airdrop tx");
+    th.charlie.state.read().await.verify_transactions(&[alice_airdrop_tx.clone()], true).await?;
+    th.charlie.merkle_tree.append(&MerkleNode::from(alice_airdrop_params.outputs[0].coin.inner()));
+
+    assert_eq!(th.alice.merkle_tree.root(0).unwrap(), th.bob.merkle_tree.root(0).unwrap());
+    assert_eq!(th.bob.merkle_tree.root(0).unwrap(), th.charlie.merkle_tree.root(0).unwrap());
+    assert_eq!(th.faucet.merkle_tree.root(0).unwrap(), th.charlie.merkle_tree.root(0).unwrap());
+
+    // Alice builds an `OwnCoin` from her airdrop.
+    let note: MoneyNote = alice_airdrop_params.outputs[0].note.decrypt(&th.alice.keypair.secret)?;
+    let owncoin = OwnCoin {
+        coin: alice_airdrop_params.outputs[0].coin,
+        note: note.clone(),
+        secret: th.alice.keypair.secret,
+        nullifier: Nullifier::from(poseidon_hash([th.alice.keypair.secret.inner(), note.serial])),
+        leaf_position,
+    };
+    th.alice.coins.push(owncoin);
+
+    // Bob creates a new mint authority keypair and mints some tokens for Charlie.
+    let bob_token_authority = Keypair::random(&mut OsRng);
+    let (bob_charlie_mint_tx, bob_charlie_mint_params) =
+        th.mint_token(bob_token_authority, 500, th.charlie.keypair.public)?;
+
+    info!("[Faucet] Executing BOBTOKEN mint to Charlie");
+    th.faucet.state.read().await.verify_transactions(&[bob_charlie_mint_tx.clone()], true).await?;
+    th.faucet.merkle_tree.append(&MerkleNode::from(bob_charlie_mint_params.output.coin.inner()));
+
+    info!("[Alice] Executing BOBTOKEN mint to Charlie");
+    th.alice.state.read().await.verify_transactions(&[bob_charlie_mint_tx.clone()], true).await?;
+    th.alice.merkle_tree.append(&MerkleNode::from(bob_charlie_mint_params.output.coin.inner()));
+
+    info!("[Bob] Executing BOBTOKEN mint to Charlie");
+    th.bob.state.read().await.verify_transactions(&[bob_charlie_mint_tx.clone()], true).await?;
+    th.bob.merkle_tree.append(&MerkleNode::from(bob_charlie_mint_params.output.coin.inner()));
+
+    info!("[Charlie] Executing BOBTOKEN mint to Charlie");
+    th.charlie.state.read().await.verify_transactions(&[bob_charlie_mint_tx.clone()], true).await?;
+    th.charlie.merkle_tree.append(&MerkleNode::from(bob_charlie_mint_params.output.coin.inner()));
+    // Charlie has to witness this coin because it's his.
+    let leaf_position = th.charlie.merkle_tree.witness().unwrap();
+
+    assert_eq!(th.alice.merkle_tree.root(0).unwrap(), th.bob.merkle_tree.root(0).unwrap());
+    assert_eq!(th.bob.merkle_tree.root(0).unwrap(), th.charlie.merkle_tree.root(0).unwrap());
+    assert_eq!(th.faucet.merkle_tree.root(0).unwrap(), th.charlie.merkle_tree.root(0).unwrap());
+
+    // Charlie builds an `OwnCoin` from this mint.
+    let note: MoneyNote =
+        bob_charlie_mint_params.output.note.decrypt(&th.charlie.keypair.secret)?;
+
+    let owncoin = OwnCoin {
+        coin: bob_charlie_mint_params.output.coin,
+        note: note.clone(),
+        secret: th.charlie.keypair.secret,
+        nullifier: Nullifier::from(poseidon_hash([th.charlie.keypair.secret.inner(), note.serial])),
+        leaf_position,
+    };
+    th.charlie.coins.push(owncoin);
+
+    // Let's attempt to freeze the BOBTOKEN mint,
+    // and after that we shouldn't be able to mint anymore.
+    let (token_freeze_pk, token_freeze_zkbin) =
+        th.proving_keys.get(&MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1).unwrap();
+
+    let bob_frz_builder = FreezeCallBuilder {
+        mint_authority: bob_token_authority,
+        token_freeze_zkbin: token_freeze_zkbin.clone(),
+        token_freeze_pk: token_freeze_pk.clone(),
+    };
+
+    let debris = bob_frz_builder.build()?;
+    let mut data = vec![MoneyFunction::FreezeV1 as u8];
+    debris.params.encode(&mut data)?;
+    let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
+    let proofs = vec![debris.proofs];
+    let mut bob_frz_tx = Transaction { calls, proofs, signatures: vec![] };
+    let sigs = bob_frz_tx.create_sigs(&mut OsRng, &[bob_token_authority.secret])?;
+    bob_frz_tx.signatures = vec![sigs];
+
+    info!("[Faucet] Executing BOBTOKEN freeze");
+    th.faucet.state.read().await.verify_transactions(&[bob_frz_tx.clone()], true).await?;
+
+    info!("[Alice] Executing BOBTOKEN freeze");
+    th.alice.state.read().await.verify_transactions(&[bob_frz_tx.clone()], true).await?;
+
+    info!("[Bob] Executing BOBTOKEN freeze");
+    th.bob.state.read().await.verify_transactions(&[bob_frz_tx.clone()], true).await?;
+
+    info!("[Charlie] Executing BOBTOKEN freeze");
+    th.charlie.state.read().await.verify_transactions(&[bob_frz_tx.clone()], true).await?;
+
+    // Thanks for reading
+    Ok(())
+}