darkfi_money_contract/client/transfer_v1/
builder.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    zk::{Proof, ProvingKey},
21    zkas::ZkBinary,
22    ClientFailed, Result,
23};
24use darkfi_sdk::{
25    crypto::{
26        note::AeadEncryptedNote, pasta_prelude::*, BaseBlind, Blind, MerkleNode, ScalarBlind,
27        SecretKey,
28    },
29    pasta::pallas,
30};
31use log::debug;
32use rand::rngs::OsRng;
33
34use super::proof::{create_transfer_burn_proof, create_transfer_mint_proof};
35use crate::{
36    client::{compute_remainder_blind, MoneyNote, OwnCoin, TokenId},
37    error::MoneyError,
38    model::{CoinAttributes, Input, MoneyTransferParamsV1, Output},
39};
40
41/// Struct holding necessary information to build a `Money::TransferV1` contract call.
42pub struct TransferCallBuilder {
43    /// Clear inputs
44    pub clear_inputs: Vec<TransferCallClearInput>,
45    /// Anonymous inputs
46    pub inputs: Vec<TransferCallInput>,
47    /// Anonymous outputs
48    pub outputs: Vec<TransferCallOutput>,
49    /// `Mint_V1` zkas circuit ZkBinary
50    pub mint_zkbin: ZkBinary,
51    /// Proving key for the `Mint_V1` zk circuit
52    pub mint_pk: ProvingKey,
53    /// `Burn_V1` zkas circuit ZkBinary
54    pub burn_zkbin: ZkBinary,
55    /// Proving key for the `Burn_V1` zk circuit
56    pub burn_pk: ProvingKey,
57}
58
59pub struct TransferCallClearInput {
60    pub value: u64,
61    pub token_id: TokenId,
62    pub signature_secret: SecretKey,
63}
64
65pub struct TransferCallInput {
66    /// The [`OwnCoin`] containing necessary metadata to create an input
67    pub coin: OwnCoin,
68    /// Merkle path in the Money Merkle tree for `coin`
69    pub merkle_path: Vec<MerkleNode>,
70    // In the DAO all inputs must have the same user_data_enc and use the same blind
71    // So support allowing the user to set their own blind.
72    pub user_data_blind: BaseBlind,
73}
74
75pub type TransferCallOutput = CoinAttributes;
76
77impl TransferCallBuilder {
78    pub fn build(self) -> Result<(MoneyTransferParamsV1, TransferCallSecrets)> {
79        debug!(target: "contract::money::client::transfer::build", "Building Money::TransferV1 contract call");
80        if self.clear_inputs.is_empty() && self.inputs.is_empty() {
81            return Err(
82                ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into()
83            )
84        }
85
86        let mut params = MoneyTransferParamsV1 { inputs: vec![], outputs: vec![] };
87        let mut signature_secrets = vec![];
88        let mut proofs = vec![];
89
90        let token_blind = BaseBlind::random(&mut OsRng);
91        let mut input_blinds = vec![];
92        let mut output_blinds = vec![];
93
94        debug!(target: "contract::money::client::transfer::build", "Building anonymous inputs");
95        for (i, input) in self.inputs.iter().enumerate() {
96            let value_blind = Blind::random(&mut OsRng);
97            input_blinds.push(value_blind);
98
99            let signature_secret = SecretKey::random(&mut OsRng);
100            signature_secrets.push(signature_secret);
101
102            debug!(target: "contract::money::client::transfer::build", "Creating transfer burn proof for input {}", i);
103            let (proof, public_inputs) = create_transfer_burn_proof(
104                &self.burn_zkbin,
105                &self.burn_pk,
106                input,
107                value_blind,
108                token_blind,
109                signature_secret,
110            )?;
111
112            params.inputs.push(Input {
113                value_commit: public_inputs.value_commit,
114                token_commit: public_inputs.token_commit,
115                nullifier: public_inputs.nullifier,
116                merkle_root: public_inputs.merkle_root,
117                user_data_enc: public_inputs.user_data_enc,
118                signature_public: public_inputs.signature_public,
119            });
120
121            proofs.push(proof);
122        }
123
124        // This value_blind calc assumes there will always be at least a single output
125        if self.outputs.is_empty() {
126            return Err(
127                ClientFailed::VerifyError(MoneyError::TransferMissingOutputs.to_string()).into()
128            )
129        }
130
131        let mut output_notes = vec![];
132
133        for (i, output) in self.outputs.iter().enumerate() {
134            let value_blind = if i == self.outputs.len() - 1 {
135                compute_remainder_blind(&input_blinds, &output_blinds)
136            } else {
137                Blind::random(&mut OsRng)
138            };
139
140            output_blinds.push(value_blind);
141
142            debug!(target: "contract::money::client::transfer::build", "Creating transfer mint proof for output {}", i);
143            let (proof, public_inputs) = create_transfer_mint_proof(
144                &self.mint_zkbin,
145                &self.mint_pk,
146                output,
147                value_blind,
148                token_blind,
149                output.spend_hook,
150                output.user_data,
151                output.blind,
152            )?;
153
154            proofs.push(proof);
155
156            // Encrypted note
157            let note = MoneyNote {
158                value: output.value,
159                token_id: output.token_id,
160                spend_hook: output.spend_hook,
161                user_data: output.user_data,
162                coin_blind: output.blind,
163                value_blind,
164                token_blind,
165                memo: vec![],
166            };
167
168            let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
169            output_notes.push(note);
170
171            params.outputs.push(Output {
172                value_commit: public_inputs.value_commit,
173                token_commit: public_inputs.token_commit,
174                coin: public_inputs.coin,
175                note: encrypted_note,
176            });
177        }
178
179        // Now we should have all the params, zk proofs, and signature secrets.
180        // We return it all and let the caller deal with it.
181        let secrets = TransferCallSecrets {
182            proofs,
183            signature_secrets,
184            output_notes,
185            input_value_blinds: input_blinds,
186            output_value_blinds: output_blinds,
187        };
188        Ok((params, secrets))
189    }
190}
191
192pub struct TransferCallSecrets {
193    /// The ZK proofs created in this builder
194    pub proofs: Vec<Proof>,
195    /// The ephemeral secret keys created for signing
196    pub signature_secrets: Vec<SecretKey>,
197
198    /// Decrypted notes associated with each output
199    pub output_notes: Vec<MoneyNote>,
200
201    /// The value blinds created for the inputs
202    pub input_value_blinds: Vec<ScalarBlind>,
203    /// The value blinds created for the outputs
204    pub output_value_blinds: Vec<ScalarBlind>,
205}
206
207impl TransferCallSecrets {
208    pub fn minted_coins(&self, params: &MoneyTransferParamsV1) -> Vec<OwnCoin> {
209        let mut minted_coins = vec![];
210        for (output, output_note) in params.outputs.iter().zip(self.output_notes.iter()) {
211            minted_coins.push(OwnCoin {
212                coin: output.coin,
213                note: output_note.clone(),
214                secret: SecretKey::from(pallas::Base::ZERO),
215                leaf_position: 0.into(),
216            });
217        }
218        minted_coins
219    }
220}