darkfi_contract_test_harness/
lib.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    collections::HashMap,
21    io::{Cursor, Write},
22};
23
24use darkfi::{
25    blockchain::{BlockInfo, BlockchainOverlay},
26    runtime::vm_runtime::Runtime,
27    tx::Transaction,
28    util::{pcg::Pcg32, time::Timestamp},
29    validator::{Validator, ValidatorConfig, ValidatorPtr},
30    zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit},
31    zkas::ZkBinary,
32    Result,
33};
34use darkfi_dao_contract::model::{DaoBulla, DaoProposalBulla};
35use darkfi_money_contract::client::OwnCoin;
36use darkfi_sdk::{
37    bridgetree,
38    crypto::{
39        smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
40        Keypair, MerkleNode, MerkleTree,
41    },
42    pasta::pallas,
43};
44use darkfi_serial::Encodable;
45use log::debug;
46use num_bigint::BigUint;
47use sled_overlay::sled;
48
49/// Utility module for caching ZK proof PKs and VKs
50pub mod vks;
51
52/// `Money::PoWReward` functionality
53mod money_pow_reward;
54
55/// `Money::Fee` functionality
56mod money_fee;
57
58/// `Money::GenesisMint` functionality
59mod money_genesis_mint;
60
61/// `Money::Transfer` functionality
62mod money_transfer;
63
64/// `Money::TokenMint` functionality
65mod money_token;
66
67/// `Money::OtcSwap` functionality
68mod money_otc_swap;
69
70/// `Deployooor::Deploy` functionality
71mod contract_deploy;
72
73/// `Dao::Mint` functionality
74mod dao_mint;
75
76/// `Dao::Propose` functionality
77mod dao_propose;
78
79/// `Dao::Vote` functionality
80mod dao_vote;
81
82/// `Dao::Exec` functionality
83mod dao_exec;
84
85/// Initialize the logging mechanism
86pub fn init_logger() {
87    let mut cfg = simplelog::ConfigBuilder::new();
88    cfg.add_filter_ignore("sled".to_string());
89    //cfg.set_target_level(simplelog::LevelFilter::Error);
90
91    // We check this error so we can execute same file tests in parallel,
92    // otherwise second one fails to init logger here.
93    if simplelog::TermLogger::init(
94        simplelog::LevelFilter::Info,
95        //simplelog::LevelFilter::Debug,
96        //simplelog::LevelFilter::Trace,
97        cfg.build(),
98        simplelog::TerminalMode::Mixed,
99        simplelog::ColorChoice::Auto,
100    )
101    .is_err()
102    {
103        debug!(target: "test_harness", "Logger initialized");
104    }
105}
106
107/// Enum representing available wallet holders
108#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
109pub enum Holder {
110    Alice,
111    Bob,
112    Charlie,
113    Dao,
114    Rachel,
115}
116
117/// Wallet instance for a single [`Holder`]
118pub struct Wallet {
119    /// Main holder keypair
120    pub keypair: Keypair,
121    /// Keypair for arbitrary token minting
122    pub token_mint_authority: Keypair,
123    /// Keypair for arbitrary contract deployment
124    pub contract_deploy_authority: Keypair,
125    /// Holder's [`Validator`] instance
126    pub validator: ValidatorPtr,
127    /// Holder's instance of the Merkle tree for the `Money` contract
128    pub money_merkle_tree: MerkleTree,
129    /// Holder's instance of the SMT tree for the `Money` contract
130    pub money_null_smt: SmtMemoryFp,
131    /// Holder's instance of the SMT tree for the `Money` contract (snapshotted for DAO::propose())
132    pub money_null_smt_snapshot: Option<SmtMemoryFp>,
133    /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO bullas)
134    pub dao_merkle_tree: MerkleTree,
135    /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO proposals)
136    pub dao_proposals_tree: MerkleTree,
137    /// Holder's set of unspent [`OwnCoin`]s from the `Money` contract
138    pub unspent_money_coins: Vec<OwnCoin>,
139    /// Holder's set of spent [`OwnCoin`]s from the `Money` contract
140    pub spent_money_coins: Vec<OwnCoin>,
141    /// Witnessed leaf positions of DAO bullas in the `dao_merkle_tree`
142    pub dao_leafs: HashMap<DaoBulla, bridgetree::Position>,
143    /// Dao Proposal snapshots
144    pub dao_prop_leafs: HashMap<DaoProposalBulla, (bridgetree::Position, MerkleTree)>,
145    /// Create bench.csv file
146    pub bench_wasm: bool,
147}
148
149impl Wallet {
150    /// Instantiate a new [`Wallet`] instance
151    pub async fn new(
152        keypair: Keypair,
153        token_mint_authority: Keypair,
154        contract_deploy_authority: Keypair,
155        genesis_block: BlockInfo,
156        vks: &vks::Vks,
157        verify_fees: bool,
158    ) -> Result<Self> {
159        // Create an in-memory sled db instance for this wallet
160        let sled_db = sled::Config::new().temporary(true).open()?;
161
162        // Inject the cached VKs into the database
163        vks::inject(&sled_db, vks)?;
164
165        // Create the `Validator` instance
166        let validator_config = ValidatorConfig {
167            confirmation_threshold: 3,
168            pow_target: 90,
169            pow_fixed_difficulty: Some(BigUint::from(1_u8)),
170            genesis_block,
171            verify_fees,
172        };
173        let validator = Validator::new(&sled_db, &validator_config).await?;
174
175        // The Merkle tree for the `Money` contract is initialized with a "null"
176        // leaf at position 0.
177        let mut money_merkle_tree = MerkleTree::new(1);
178        money_merkle_tree.append(MerkleNode::from(pallas::Base::ZERO));
179        money_merkle_tree.mark().unwrap();
180
181        let hasher = PoseidonFp::new();
182        let store = MemoryStorageFp::new();
183        let money_null_smt = SmtMemoryFp::new(store, hasher, &EMPTY_NODES_FP);
184
185        Ok(Self {
186            keypair,
187            token_mint_authority,
188            contract_deploy_authority,
189            validator,
190            money_merkle_tree,
191            money_null_smt,
192            money_null_smt_snapshot: None,
193            dao_merkle_tree: MerkleTree::new(1),
194            dao_proposals_tree: MerkleTree::new(1),
195            unspent_money_coins: vec![],
196            spent_money_coins: vec![],
197            dao_leafs: HashMap::new(),
198            dao_prop_leafs: HashMap::new(),
199            bench_wasm: false,
200        })
201    }
202
203    pub async fn add_transaction(
204        &mut self,
205        callname: &str,
206        tx: Transaction,
207        block_height: u32,
208    ) -> Result<()> {
209        if self.bench_wasm {
210            benchmark_wasm_calls(callname, &self.validator, &tx, block_height).await;
211        }
212
213        self.validator
214            .add_test_transactions(
215                &[tx.clone()],
216                block_height,
217                self.validator.consensus.module.read().await.target,
218                true,
219                self.validator.verify_fees,
220            )
221            .await?;
222
223        // Write the data
224        {
225            let blockchain = &self.validator.blockchain;
226            let txs = &blockchain.transactions;
227            txs.insert(&[tx.clone()]).expect("insert tx");
228            txs.insert_location(&[tx.hash()], block_height).expect("insert loc");
229        }
230
231        Ok(())
232    }
233}
234
235/// Native contract test harness instance
236pub struct TestHarness {
237    /// Initialized [`Holder`]s for this instance
238    pub holders: HashMap<Holder, Wallet>,
239    /// Cached [`ProvingKey`]s for native contract ZK proving
240    pub proving_keys: HashMap<String, (ProvingKey, ZkBinary)>,
241    /// The genesis block for this harness
242    pub genesis_block: BlockInfo,
243    /// Marker to know if we're supposed to include tx fees
244    pub verify_fees: bool,
245}
246
247impl TestHarness {
248    /// Instantiate a new [`TestHarness`] given a slice of [`Holder`]s.
249    /// Additionally, a `verify_fees` boolean will enforce tx fee verification.
250    pub async fn new(holders: &[Holder], verify_fees: bool) -> Result<Self> {
251        // Create a genesis block
252        let mut genesis_block = BlockInfo::default();
253        genesis_block.header.timestamp = Timestamp::from_u64(1689772567);
254        let producer_tx = genesis_block.txs.pop().unwrap();
255        genesis_block.append_txs(vec![producer_tx]);
256
257        // Deterministic PRNG
258        let mut rng = Pcg32::new(42);
259
260        // Build or read cached ZK PKs and VKs
261        let (pks, vks) = vks::get_cached_pks_and_vks()?;
262        let mut proving_keys = HashMap::new();
263        for (bincode, namespace, pk) in pks {
264            let mut reader = Cursor::new(pk);
265            let zkbin = ZkBinary::decode(&bincode)?;
266            let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
267            let proving_key = ProvingKey::read(&mut reader, circuit)?;
268            proving_keys.insert(namespace, (proving_key, zkbin));
269        }
270
271        // Create `Wallet` instances
272        let mut holders_map = HashMap::new();
273        for holder in holders {
274            let keypair = Keypair::random(&mut rng);
275            let token_mint_authority = Keypair::random(&mut rng);
276            let contract_deploy_authority = Keypair::random(&mut rng);
277
278            let wallet = Wallet::new(
279                keypair,
280                token_mint_authority,
281                contract_deploy_authority,
282                genesis_block.clone(),
283                &vks,
284                verify_fees,
285            )
286            .await?;
287
288            holders_map.insert(*holder, wallet);
289        }
290
291        Ok(Self { holders: holders_map, proving_keys, genesis_block, verify_fees })
292    }
293
294    /// Assert that all holders' trees are the same
295    pub fn assert_trees(&self, holders: &[Holder]) {
296        assert!(holders.len() > 1);
297        // Gather wallets
298        let mut wallets = vec![];
299        for holder in holders {
300            wallets.push(self.holders.get(holder).unwrap());
301        }
302        // Compare trees
303        let wallet = wallets[0];
304        let money_root = wallet.money_merkle_tree.root(0).unwrap();
305        for wallet in &wallets[1..] {
306            assert!(money_root == wallet.money_merkle_tree.root(0).unwrap());
307        }
308    }
309}
310
311async fn benchmark_wasm_calls(
312    callname: &str,
313    validator: &Validator,
314    tx: &Transaction,
315    block_height: u32,
316) {
317    let mut file = std::fs::OpenOptions::new().create(true).append(true).open("bench.csv").unwrap();
318
319    for (idx, call) in tx.calls.iter().enumerate() {
320        let overlay = BlockchainOverlay::new(&validator.blockchain).expect("blockchain overlay");
321        let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id).unwrap();
322        let mut runtime = Runtime::new(
323            &wasm,
324            overlay.clone(),
325            call.data.contract_id,
326            block_height,
327            validator.consensus.module.read().await.target,
328            tx.hash(),
329            idx as u8,
330        )
331        .expect("runtime");
332
333        // Write call data
334        let mut payload = vec![];
335        tx.calls.encode(&mut payload).unwrap();
336
337        let mut times = [0; 3];
338        let now = std::time::Instant::now();
339        let _metadata = runtime.metadata(&payload).expect("metadata");
340        times[0] = now.elapsed().as_micros();
341
342        let now = std::time::Instant::now();
343        let update = runtime.exec(&payload).expect("exec");
344        times[1] = now.elapsed().as_micros();
345
346        let now = std::time::Instant::now();
347        runtime.apply(&update).expect("update");
348        times[2] = now.elapsed().as_micros();
349
350        writeln!(
351            file,
352            "{}, {}, {}, {}, {}, {}",
353            callname,
354            tx.hash(),
355            idx,
356            times[0],
357            times[1],
358            times[2]
359        )
360        .unwrap();
361    }
362}