darkfi_contract_test_harness/
money_fee.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::{collections::HashSet, hash::RandomState};
20
21use darkfi::{
22    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
23    validator::fees::compute_fee,
24    zk::{halo2::Field, Proof},
25    Result,
26};
27use darkfi_money_contract::{
28    client::{
29        compute_remainder_blind,
30        fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS},
31        MoneyNote, OwnCoin,
32    },
33    model::{token_id::DARK_TOKEN_ID, Input, MoneyFeeParamsV1, Output},
34    MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
35};
36use darkfi_sdk::{
37    crypto::{
38        contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, BaseBlind, Blind, FuncId,
39        MerkleNode, ScalarBlind, SecretKey,
40    },
41    pasta::pallas,
42    ContractCall,
43};
44use darkfi_serial::AsyncEncodable;
45use log::{debug, info};
46use rand::rngs::OsRng;
47
48use super::{Holder, TestHarness};
49
50impl TestHarness {
51    /// Create an empty transaction that includes a `Money::Fee` call.
52    /// This is generally used to test the actual fee call, and also to
53    /// see the gas usage of the call without other parts.
54    pub async fn create_empty_fee_call(
55        &mut self,
56        holder: &Holder,
57    ) -> Result<(Transaction, MoneyFeeParamsV1)> {
58        let wallet = self.holders.get(holder).unwrap();
59
60        // Compute fee call required fee
61        let required_fee = compute_fee(&FEE_CALL_GAS);
62
63        // Find a compatible OwnCoin
64        let coin = wallet
65            .unspent_money_coins
66            .iter()
67            .find(|x| x.note.token_id == *DARK_TOKEN_ID && x.note.value > required_fee)
68            .unwrap();
69
70        // Input and output setup
71        let input = FeeCallInput {
72            coin: coin.clone(),
73            merkle_path: wallet.money_merkle_tree.witness(coin.leaf_position, 0).unwrap(),
74            user_data_blind: Blind::random(&mut OsRng),
75        };
76
77        let output = FeeCallOutput {
78            public_key: wallet.keypair.public,
79            value: coin.note.value - required_fee,
80            token_id: coin.note.token_id,
81            blind: Blind::random(&mut OsRng),
82            spend_hook: FuncId::none(),
83            user_data: pallas::Base::ZERO,
84        };
85
86        // Generate blinding factors
87        let token_blind = BaseBlind::random(&mut OsRng);
88        let input_value_blind = ScalarBlind::random(&mut OsRng);
89        let fee_value_blind = ScalarBlind::random(&mut OsRng);
90        let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
91
92        // Generate an ephemeral signing key
93        let signature_secret = SecretKey::random(&mut OsRng);
94
95        info!("Creting FeeV1 ZK proof");
96        let (fee_pk, fee_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_FEE_NS_V1).unwrap();
97
98        let (proof, public_inputs) = create_fee_proof(
99            fee_zkbin,
100            fee_pk,
101            &input,
102            input_value_blind,
103            &output,
104            output_value_blind,
105            output.spend_hook,
106            output.user_data,
107            output.blind,
108            token_blind,
109            signature_secret,
110        )?;
111
112        // Encrypted note for the output
113        let note = MoneyNote {
114            coin_blind: output.blind,
115            value: output.value,
116            token_id: output.token_id,
117            spend_hook: output.spend_hook,
118            user_data: output.user_data,
119            value_blind: output_value_blind,
120            token_blind,
121            memo: vec![],
122        };
123
124        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
125
126        let params = MoneyFeeParamsV1 {
127            input: Input {
128                value_commit: public_inputs.input_value_commit,
129                token_commit: public_inputs.token_commit,
130                nullifier: public_inputs.nullifier,
131                merkle_root: public_inputs.merkle_root,
132                user_data_enc: public_inputs.input_user_data_enc,
133                signature_public: public_inputs.signature_public,
134            },
135            output: Output {
136                value_commit: public_inputs.output_value_commit,
137                token_commit: public_inputs.token_commit,
138                coin: public_inputs.output_coin,
139                note: encrypted_note,
140            },
141            fee_value_blind,
142            token_blind,
143        };
144
145        let mut data = vec![MoneyFunction::FeeV1 as u8];
146        required_fee.encode_async(&mut data).await?;
147        params.encode_async(&mut data).await?;
148        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
149        let mut tx_builder =
150            TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![proof] }, vec![])?;
151        let mut tx = tx_builder.build()?;
152        let sigs = tx.create_sigs(&[signature_secret])?;
153        tx.signatures = vec![sigs];
154
155        Ok((tx, params))
156    }
157
158    /// Execute the transaction created by `create_empty_fee_call()` for a given [`Holder`]
159    ///
160    /// Returns any found [`OwnCoin`]s.
161    pub async fn execute_empty_fee_call_tx(
162        &mut self,
163        holder: &Holder,
164        tx: Transaction,
165        params: &MoneyFeeParamsV1,
166        block_height: u32,
167    ) -> Result<Vec<OwnCoin>> {
168        let wallet = self.holders.get_mut(holder).unwrap();
169
170        let nullifier = params.input.nullifier.inner();
171        wallet
172            .money_null_smt
173            .insert_batch(vec![(nullifier, nullifier)])
174            .expect("smt.insert_batch()");
175
176        wallet.add_transaction("money::fee", tx, block_height).await?;
177        wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
178
179        // Attempt to decrypt the output note to see if this is a coin for the holder
180        let Ok(note) = params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
181            return Ok(vec![])
182        };
183
184        let owncoin = OwnCoin {
185            coin: params.output.coin,
186            note: note.clone(),
187            secret: wallet.keypair.secret,
188            leaf_position: wallet.money_merkle_tree.mark().unwrap(),
189        };
190
191        let spent_coin = wallet
192            .unspent_money_coins
193            .iter()
194            .find(|x| x.nullifier() == params.input.nullifier)
195            .unwrap()
196            .clone();
197
198        debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
199        debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
200
201        wallet.unspent_money_coins.retain(|x| x.nullifier() != params.input.nullifier);
202        wallet.spent_money_coins.push(spent_coin);
203        wallet.unspent_money_coins.push(owncoin.clone());
204
205        Ok(vec![owncoin])
206    }
207
208    /// Create and append a `Money::Fee` call to a given [`Transaction`] for
209    /// a given [`Holder`].
210    ///
211    /// Additionally takes a set of spent coins in order not to reuse them here.
212    ///
213    /// Returns the `Fee` call, and all necessary data and parameters related.
214    pub async fn append_fee_call(
215        &mut self,
216        holder: &Holder,
217        tx: Transaction,
218        block_height: u32,
219        spent_coins: &[OwnCoin],
220    ) -> Result<(ContractCall, Vec<Proof>, Vec<SecretKey>, Vec<OwnCoin>, MoneyFeeParamsV1)> {
221        // First we verify the fee-less transaction to see how much gas it uses for execution
222        // and verification.
223        let wallet = self.holders.get(holder).unwrap();
224        let gas_used = wallet
225            .validator
226            .add_test_transactions(
227                &[tx],
228                block_height,
229                wallet.validator.consensus.module.read().await.target,
230                false,
231                false,
232            )
233            .await?
234            .0;
235
236        // Compute the required fee
237        let required_fee = compute_fee(&(gas_used + FEE_CALL_GAS));
238
239        // Knowing the total gas, we can now find an OwnCoin of enough value
240        // so that we can create a valid Money::Fee call.
241        let spent_coins: HashSet<&OwnCoin, RandomState> = HashSet::from_iter(spent_coins);
242        let mut available_coins = wallet.unspent_money_coins.clone();
243        available_coins
244            .retain(|x| x.note.token_id == *DARK_TOKEN_ID && x.note.value > required_fee);
245        available_coins.retain(|x| !spent_coins.contains(x));
246        assert!(!available_coins.is_empty());
247
248        let coin = &available_coins[0];
249        let change_value = coin.note.value - required_fee;
250
251        // Input and output setup
252        let input = FeeCallInput {
253            coin: coin.clone(),
254            merkle_path: wallet.money_merkle_tree.witness(coin.leaf_position, 0).unwrap(),
255            user_data_blind: BaseBlind::random(&mut OsRng),
256        };
257
258        let output = FeeCallOutput {
259            public_key: wallet.keypair.public,
260            value: change_value,
261            token_id: coin.note.token_id,
262            blind: BaseBlind::random(&mut OsRng),
263            spend_hook: FuncId::none(),
264            user_data: pallas::Base::ZERO,
265        };
266
267        // Create blinding factors
268        let token_blind = BaseBlind::random(&mut OsRng);
269        let input_value_blind = ScalarBlind::random(&mut OsRng);
270        let fee_value_blind = ScalarBlind::random(&mut OsRng);
271        let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
272
273        // Create an ephemeral signing key
274        let signature_secret = SecretKey::random(&mut OsRng);
275
276        info!("Creating FeeV1 ZK proof");
277        let (fee_pk, fee_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_FEE_NS_V1).unwrap();
278
279        let (proof, public_inputs) = create_fee_proof(
280            fee_zkbin,
281            fee_pk,
282            &input,
283            input_value_blind,
284            &output,
285            output_value_blind,
286            output.spend_hook,
287            output.user_data,
288            output.blind,
289            token_blind,
290            signature_secret,
291        )?;
292
293        // Encrypted note for the output
294        let note = MoneyNote {
295            coin_blind: output.blind,
296            value: output.value,
297            token_id: output.token_id,
298            spend_hook: output.spend_hook,
299            user_data: output.user_data,
300            value_blind: output_value_blind,
301            token_blind,
302            memo: vec![],
303        };
304
305        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
306
307        let params = MoneyFeeParamsV1 {
308            input: Input {
309                value_commit: public_inputs.input_value_commit,
310                token_commit: public_inputs.token_commit,
311                nullifier: public_inputs.nullifier,
312                merkle_root: public_inputs.merkle_root,
313                user_data_enc: public_inputs.input_user_data_enc,
314                signature_public: public_inputs.signature_public,
315            },
316            output: Output {
317                value_commit: public_inputs.output_value_commit,
318                token_commit: public_inputs.token_commit,
319                coin: public_inputs.output_coin,
320                note: encrypted_note,
321            },
322            fee_value_blind,
323            token_blind,
324        };
325
326        // Encode the contract call
327        let mut data = vec![MoneyFunction::FeeV1 as u8];
328        required_fee.encode_async(&mut data).await?;
329        params.encode_async(&mut data).await?;
330        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
331
332        Ok((call, vec![proof], vec![signature_secret], vec![coin.clone()], params))
333    }
334}