darkfi_contract_test_harness/
dao_exec.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 darkfi::{
20    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
21    Result,
22};
23use darkfi_dao_contract::{
24    blockwindow,
25    client::{DaoAuthMoneyTransferCall, DaoExecCall},
26    model::{Dao, DaoProposal},
27    DaoFunction, DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS,
28    DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS, DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS,
29    DAO_CONTRACT_ZKAS_DAO_EXEC_NS,
30};
31use darkfi_money_contract::{
32    client::{transfer_v1 as xfer, MoneyNote, OwnCoin},
33    model::{CoinAttributes, MoneyFeeParamsV1, MoneyTransferParamsV1},
34    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
35};
36use darkfi_sdk::{
37    crypto::{
38        contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
39        pedersen_commitment_u64, Blind, FuncRef, MerkleNode, ScalarBlind, SecretKey,
40    },
41    dark_tree::DarkTree,
42    ContractCall,
43};
44use darkfi_serial::AsyncEncodable;
45use log::debug;
46use rand::rngs::OsRng;
47
48use super::{Holder, TestHarness};
49
50impl TestHarness {
51    /// Create a transfer `Dao::Exec` transaction.
52    #[allow(clippy::too_many_arguments)]
53    pub async fn dao_exec_transfer(
54        &mut self,
55        holder: &Holder,
56        dao: &Dao,
57        dao_exec_secret_key: &SecretKey,
58        dao_early_exec_secret_key: &Option<SecretKey>,
59        proposal: &DaoProposal,
60        proposal_coinattrs: Vec<CoinAttributes>,
61        yes_vote_value: u64,
62        all_vote_value: u64,
63        yes_vote_blind: ScalarBlind,
64        all_vote_blind: ScalarBlind,
65        block_height: u32,
66    ) -> Result<(Transaction, MoneyTransferParamsV1, Option<MoneyFeeParamsV1>)> {
67        let dao_wallet = self.holders.get(&Holder::Dao).unwrap();
68
69        let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
70        let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
71
72        let (dao_exec_pk, dao_exec_zkbin) = match dao_early_exec_secret_key {
73            Some(_) => self.proving_keys.get(DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS).unwrap(),
74            None => self.proving_keys.get(DAO_CONTRACT_ZKAS_DAO_EXEC_NS).unwrap(),
75        };
76        let (dao_auth_xfer_pk, dao_auth_xfer_zkbin) =
77            self.proving_keys.get(DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS).unwrap();
78        let (dao_auth_xfer_enc_coin_pk, dao_auth_xfer_enc_coin_zkbin) =
79            self.proving_keys.get(DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS).unwrap();
80
81        let input_user_data_blind = Blind::random(&mut OsRng);
82        let exec_signature_secret = SecretKey::random(&mut OsRng);
83
84        assert!(!proposal_coinattrs.is_empty());
85        let proposal_token_id = proposal_coinattrs[0].token_id;
86        assert!(proposal_coinattrs.iter().all(|c| c.token_id == proposal_token_id));
87        let proposal_amount = proposal_coinattrs.iter().map(|c| c.value).sum();
88
89        let dao_coins = dao_wallet
90            .unspent_money_coins
91            .iter()
92            .filter(|x| x.note.token_id == proposal_token_id)
93            .cloned()
94            .collect();
95        let (spent_coins, change_value) = xfer::select_coins(dao_coins, proposal_amount)?;
96        let tree = dao_wallet.money_merkle_tree.clone();
97
98        let mut inputs = vec![];
99        for coin in &spent_coins {
100            inputs.push(xfer::TransferCallInput {
101                coin: coin.clone(),
102                merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
103                user_data_blind: input_user_data_blind,
104            });
105        }
106
107        let mut outputs = vec![];
108        for coin_attr in proposal_coinattrs.clone() {
109            assert_eq!(proposal_token_id, coin_attr.token_id);
110            outputs.push(coin_attr);
111        }
112
113        let spend_hook =
114            FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
115                .to_func_id();
116
117        let dao_coin_attrs = CoinAttributes {
118            public_key: dao_wallet.keypair.public,
119            value: change_value,
120            token_id: proposal_token_id,
121            spend_hook,
122            user_data: dao.to_bulla().inner(),
123            blind: Blind::random(&mut OsRng),
124        };
125        outputs.push(dao_coin_attrs.clone());
126
127        let xfer_builder = xfer::TransferCallBuilder {
128            clear_inputs: vec![],
129            inputs,
130            outputs,
131            mint_zkbin: mint_zkbin.clone(),
132            mint_pk: mint_pk.clone(),
133            burn_zkbin: burn_zkbin.clone(),
134            burn_pk: burn_pk.clone(),
135        };
136
137        let (xfer_params, xfer_secrets) = xfer_builder.build()?;
138        let mut data = vec![MoneyFunction::TransferV1 as u8];
139        xfer_params.encode_async(&mut data).await?;
140        let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
141
142        // We need to extract stuff from the inputs and outputs that we'll also
143        // use in the DAO::Exec call. This DAO API needs to be better.
144        let mut input_value = 0;
145        let mut input_value_blind = Blind::ZERO;
146        for (input, blind) in spent_coins.iter().zip(xfer_secrets.input_value_blinds.iter()) {
147            input_value += input.note.value;
148            input_value_blind += *blind;
149        }
150        assert_eq!(
151            pedersen_commitment_u64(input_value, input_value_blind),
152            xfer_params.inputs.iter().map(|input| input.value_commit).sum()
153        );
154
155        let block_target = dao_wallet.validator.consensus.module.read().await.target;
156        let current_blockwindow = blockwindow(block_height, block_target);
157        let exec_builder = DaoExecCall {
158            proposal: proposal.clone(),
159            dao: dao.clone(),
160            yes_vote_value,
161            all_vote_value,
162            yes_vote_blind,
163            all_vote_blind,
164            signature_secret: exec_signature_secret,
165            current_blockwindow,
166        };
167
168        let (exec_params, exec_proofs) = exec_builder.make(
169            dao_exec_secret_key,
170            dao_early_exec_secret_key,
171            dao_exec_zkbin,
172            dao_exec_pk,
173        )?;
174        let mut data = vec![DaoFunction::Exec as u8];
175        exec_params.encode_async(&mut data).await?;
176        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
177
178        // Auth module
179        let auth_xfer_builder = DaoAuthMoneyTransferCall {
180            proposal: proposal.clone(),
181            proposal_coinattrs,
182            dao: dao.clone(),
183            input_user_data_blind,
184            dao_coin_attrs,
185        };
186        let (auth_xfer_params, auth_xfer_proofs) = auth_xfer_builder.make(
187            dao_auth_xfer_zkbin,
188            dao_auth_xfer_pk,
189            dao_auth_xfer_enc_coin_zkbin,
190            dao_auth_xfer_enc_coin_pk,
191        )?;
192        let mut data = vec![DaoFunction::AuthMoneyTransfer as u8];
193        auth_xfer_params.encode_async(&mut data).await?;
194        let auth_xfer_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
195
196        // We need to construct this tree, where exec is the parent:
197        //
198        //   exec ->
199        //       auth_xfer
200        //       xfer
201        //
202
203        let mut tx_builder = TransactionBuilder::new(
204            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
205            vec![
206                DarkTree::new(
207                    ContractCallLeaf { call: auth_xfer_call, proofs: auth_xfer_proofs },
208                    vec![],
209                    None,
210                    None,
211                ),
212                DarkTree::new(
213                    ContractCallLeaf { call: xfer_call, proofs: xfer_secrets.proofs },
214                    vec![],
215                    None,
216                    None,
217                ),
218            ],
219        )?;
220
221        // If fees are enabled, make an offering
222        let mut fee_params = None;
223        let mut fee_signature_secrets = None;
224        if self.verify_fees {
225            let mut tx = tx_builder.build()?;
226            let auth_xfer_sigs = vec![];
227            let xfer_sigs = tx.create_sigs(&xfer_secrets.signature_secrets)?;
228            let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
229            tx.signatures = vec![auth_xfer_sigs, xfer_sigs, exec_sigs];
230
231            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
232                self.append_fee_call(holder, tx, block_height, &[]).await?;
233
234            // Append the fee call to the transaction
235            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
236            fee_signature_secrets = Some(fee_secrets);
237            fee_params = Some(fee_call_params);
238        }
239
240        // Now build the actual transaction and sign it with necessary keys.
241        let mut tx = tx_builder.build()?;
242        let auth_xfer_sigs = vec![];
243        let xfer_sigs = tx.create_sigs(&xfer_secrets.signature_secrets)?;
244        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
245        tx.signatures = vec![auth_xfer_sigs, xfer_sigs, exec_sigs];
246
247        if let Some(fee_signature_secrets) = fee_signature_secrets {
248            let sigs = tx.create_sigs(&fee_signature_secrets)?;
249            tx.signatures.push(sigs);
250        }
251
252        Ok((tx, xfer_params, fee_params))
253    }
254
255    /// Create a generic `Dao::Exec` transaction.
256    #[allow(clippy::too_many_arguments)]
257    pub async fn dao_exec_generic(
258        &mut self,
259        holder: &Holder,
260        dao: &Dao,
261        dao_exec_secret_key: &SecretKey,
262        dao_early_exec_secret_key: &Option<SecretKey>,
263        proposal: &DaoProposal,
264        yes_vote_value: u64,
265        all_vote_value: u64,
266        yes_vote_blind: ScalarBlind,
267        all_vote_blind: ScalarBlind,
268        block_height: u32,
269    ) -> Result<(Transaction, Option<MoneyFeeParamsV1>)> {
270        let wallet = self.holders.get_mut(holder).unwrap();
271
272        let (dao_exec_pk, dao_exec_zkbin) = match dao_early_exec_secret_key {
273            Some(_) => self.proving_keys.get(DAO_CONTRACT_ZKAS_DAO_EARLY_EXEC_NS).unwrap(),
274            None => self.proving_keys.get(DAO_CONTRACT_ZKAS_DAO_EXEC_NS).unwrap(),
275        };
276
277        // Create the exec call
278        let exec_signature_secret = SecretKey::random(&mut OsRng);
279        let block_target = wallet.validator.consensus.module.read().await.target;
280        let current_blockwindow = blockwindow(block_height, block_target);
281        let exec_builder = DaoExecCall {
282            proposal: proposal.clone(),
283            dao: dao.clone(),
284            yes_vote_value,
285            all_vote_value,
286            yes_vote_blind,
287            all_vote_blind,
288            signature_secret: exec_signature_secret,
289            current_blockwindow,
290        };
291        let (exec_params, exec_proofs) = exec_builder.make(
292            dao_exec_secret_key,
293            dao_early_exec_secret_key,
294            dao_exec_zkbin,
295            dao_exec_pk,
296        )?;
297
298        // Encode the call
299        let mut data = vec![DaoFunction::Exec as u8];
300        exec_params.encode_async(&mut data).await?;
301        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
302
303        // Create the TransactionBuilder containing the `DAO::Exec` call
304        let mut tx_builder = TransactionBuilder::new(
305            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
306            vec![],
307        )?;
308
309        // If fees are enabled, make an offering
310        let mut fee_params = None;
311        let mut fee_signature_secrets = None;
312        if self.verify_fees {
313            let mut tx = tx_builder.build()?;
314            let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
315            tx.signatures = vec![exec_sigs];
316
317            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
318                self.append_fee_call(holder, tx, block_height, &[]).await?;
319
320            // Append the fee call to the transaction
321            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
322            fee_signature_secrets = Some(fee_secrets);
323            fee_params = Some(fee_call_params);
324        }
325
326        // Now build the actual transaction and sign it with necessary keys.
327        let mut tx = tx_builder.build()?;
328        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
329        tx.signatures = vec![exec_sigs];
330
331        if let Some(fee_signature_secrets) = fee_signature_secrets {
332            let sigs = tx.create_sigs(&fee_signature_secrets)?;
333            tx.signatures.push(sigs);
334        }
335
336        Ok((tx, fee_params))
337    }
338
339    /// Execute the transaction made by `dao_exec_*()` for a given [`Holder`].
340    ///
341    /// Returns any found [`OwnCoin`]s.
342    pub async fn execute_dao_exec_tx(
343        &mut self,
344        holder: &Holder,
345        tx: Transaction,
346        xfer_params: Option<&MoneyTransferParamsV1>,
347        fee_params: &Option<MoneyFeeParamsV1>,
348        block_height: u32,
349        append: bool,
350    ) -> Result<Vec<OwnCoin>> {
351        let wallet = self.holders.get_mut(holder).unwrap();
352
353        // Execute the transaction
354        wallet.add_transaction("dao::exec", tx, block_height).await?;
355
356        if !append {
357            return Ok(vec![])
358        }
359
360        let (mut inputs, mut outputs) = match xfer_params {
361            Some(params) => (params.inputs.to_vec(), params.outputs.to_vec()),
362            None => (vec![], vec![]),
363        };
364
365        if let Some(ref fee_params) = fee_params {
366            inputs.push(fee_params.input.clone());
367            outputs.push(fee_params.output.clone());
368        }
369
370        let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
371        wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()");
372
373        for input in inputs {
374            if let Some(spent_coin) = wallet
375                .unspent_money_coins
376                .iter()
377                .find(|x| x.nullifier() == input.nullifier)
378                .cloned()
379            {
380                debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
381                wallet.unspent_money_coins.retain(|x| x.nullifier() != input.nullifier);
382                wallet.spent_money_coins.push(spent_coin.clone());
383            }
384        }
385
386        let mut found_owncoins = vec![];
387        for output in outputs {
388            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
389
390            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
391                continue
392            };
393
394            let owncoin = OwnCoin {
395                coin: output.coin,
396                note: note.clone(),
397                secret: wallet.keypair.secret,
398                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
399            };
400
401            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
402            wallet.unspent_money_coins.push(owncoin.clone());
403            found_owncoins.push(owncoin);
404        }
405
406        Ok(found_owncoins)
407    }
408}