drk/
swap.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 */
18use std::fmt;
19
20use rand::rngs::OsRng;
21
22use darkfi::{
23    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
24    util::parse::encode_base10,
25    zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof},
26    zkas::ZkBinary,
27    Error, Result,
28};
29use darkfi_money_contract::{
30    client::{swap_v1::SwapCallBuilder, MoneyNote},
31    model::{Coin, MoneyTransferParamsV1, TokenId},
32    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
33};
34use darkfi_sdk::{
35    crypto::{
36        contract_id::MONEY_CONTRACT_ID, pedersen::pedersen_commitment_u64, poseidon_hash,
37        BaseBlind, Blind, FuncId, PublicKey, ScalarBlind, SecretKey,
38    },
39    pasta::pallas,
40    tx::ContractCall,
41};
42use darkfi_serial::{
43    async_trait, deserialize_async, AsyncEncodable, SerialDecodable, SerialEncodable,
44};
45
46use super::{money::BALANCE_BASE10_DECIMALS, Drk};
47
48#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
49/// Half of the swap data, includes the coin that is supposed to be sent,
50/// and the coin that is supposed to be received.
51pub struct PartialSwapData {
52    params: MoneyTransferParamsV1,
53    proofs: Vec<Proof>,
54    value_pair: (u64, u64),
55    token_pair: (TokenId, TokenId),
56    value_blinds: Vec<ScalarBlind>,
57    token_blinds: Vec<BaseBlind>,
58}
59
60impl fmt::Display for PartialSwapData {
61    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
62        let s =
63            format!(
64            "{:#?}\nValue pair: {}:{}\nToken pair: {}:{}\nValue blinds: {:?}\nToken blinds: {:?}\n",
65            self.params, self.value_pair.0, self.value_pair.1, self.token_pair.0, self.token_pair.1,
66            self.value_blinds, self.token_blinds,
67        );
68
69        write!(f, "{s}")
70    }
71}
72
73impl Drk {
74    /// Initialize the first half of an atomic swap
75    pub async fn init_swap(
76        &self,
77        value_pair: (u64, u64),
78        token_pair: (TokenId, TokenId),
79        user_data_blind_send: Option<BaseBlind>,
80        spend_hook_recv: Option<FuncId>,
81        user_data_recv: Option<pallas::Base>,
82    ) -> Result<PartialSwapData> {
83        // First get all unspent OwnCoins to see what our balance is
84        let owncoins = self.get_token_coins(&token_pair.0).await?;
85        if owncoins.is_empty() {
86            return Err(Error::Custom(format!(
87                "Did not find any unspent coins with token ID: {}",
88                token_pair.0
89            )))
90        }
91
92        // Find one with the correct value
93        let mut burn_coin = None;
94        for coin in owncoins {
95            if coin.note.value == value_pair.0 {
96                burn_coin = Some(coin);
97                break
98            }
99        }
100        let Some(burn_coin) = burn_coin else {
101            return Err(Error::Custom(format!(
102                "Did not find any unspent coins of value {} and token_id {}",
103                value_pair.0, token_pair.0,
104            )))
105        };
106
107        // Fetch our default address
108        let address = self.default_address().await?;
109
110        // We'll also need our Merkle tree
111        let tree = self.get_money_tree().await?;
112
113        // Now we need to do a lookup for the zkas proof bincodes, and create
114        // the circuit objects and proving keys so we can build the transaction.
115        // We also do this through the RPC.
116        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
117
118        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
119        else {
120            return Err(Error::Custom("Mint circuit not found".to_string()))
121        };
122
123        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
124        else {
125            return Err(Error::Custom("Burn circuit not found".to_string()))
126        };
127
128        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
129        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
130
131        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
132        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
133
134        // Creating Mint and Burn circuits proving keys
135        let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
136        let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
137
138        // Since we're creating the first half, we generate the blinds.
139        let value_blinds = [Blind::random(&mut OsRng), Blind::random(&mut OsRng)];
140        let token_blinds = [Blind::random(&mut OsRng), Blind::random(&mut OsRng)];
141
142        // Now we should have everything we need to build the swap half
143        let builder = SwapCallBuilder {
144            pubkey: address,
145            value_send: value_pair.0,
146            token_id_send: token_pair.0,
147            value_recv: value_pair.1,
148            token_id_recv: token_pair.1,
149            user_data_blind_send: user_data_blind_send.unwrap_or(Blind::random(&mut OsRng)),
150            spend_hook_recv: spend_hook_recv.unwrap_or(FuncId::none()),
151            user_data_recv: user_data_recv.unwrap_or(pallas::Base::ZERO),
152            value_blinds,
153            token_blinds,
154            coin: burn_coin,
155            tree,
156            mint_zkbin,
157            mint_pk,
158            burn_zkbin,
159            burn_pk,
160        };
161        let debris = builder.build()?;
162
163        // Now we have the half, so we can build `PartialSwapData` and return it.
164        let ret = PartialSwapData {
165            params: debris.params,
166            proofs: debris.proofs,
167            value_pair,
168            token_pair,
169            value_blinds: value_blinds.to_vec(),
170            token_blinds: token_blinds.to_vec(),
171        };
172
173        Ok(ret)
174    }
175
176    /// Create a full transaction by inspecting and verifying given partial swap data,
177    /// making the other half, and joining all this into a `Transaction` object.
178    pub async fn join_swap(
179        &self,
180        partial: PartialSwapData,
181        user_data_blind_send: Option<BaseBlind>,
182        spend_hook_recv: Option<FuncId>,
183        user_data_recv: Option<pallas::Base>,
184    ) -> Result<Transaction> {
185        // Our side of the tx in the pairs is the second half, so we try to find
186        // an unspent coin like that in our wallet.
187        let owncoins = self.get_token_coins(&partial.token_pair.1).await?;
188        if owncoins.is_empty() {
189            return Err(Error::Custom(format!(
190                "Did not find any unspent coins with token ID: {}",
191                partial.token_pair.1
192            )))
193        }
194
195        // Find one with the correct value
196        let mut burn_coin = None;
197        for coin in owncoins {
198            if coin.note.value == partial.value_pair.1 {
199                burn_coin = Some(coin);
200                break
201            }
202        }
203        let Some(burn_coin) = burn_coin else {
204            return Err(Error::Custom(format!(
205                "Did not find any unspent coins of value {} and token_id {}",
206                partial.value_pair.1, partial.token_pair.1,
207            )))
208        };
209
210        // Fetch our default address
211        let address = self.default_address().await?;
212
213        // We'll also need our Merkle tree
214        let tree = self.get_money_tree().await?;
215
216        // Now we need to do a lookup for the zkas proof bincodes, and create
217        // the circuit objects and proving keys so we can build the transaction.
218        // We also do this through the RPC.
219        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
220
221        let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
222        else {
223            return Err(Error::Custom("Mint circuit not found".to_string()))
224        };
225
226        let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
227        else {
228            return Err(Error::Custom("Burn circuit not found".to_string()))
229        };
230
231        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
232        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
233
234        let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
235        let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
236
237        // Creating Mint and Burn circuits proving keys
238        let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
239        let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
240
241        // Now we should have everything we need to build the swap half
242        let builder = SwapCallBuilder {
243            pubkey: address,
244            value_send: partial.value_pair.1,
245            token_id_send: partial.token_pair.1,
246            value_recv: partial.value_pair.0,
247            token_id_recv: partial.token_pair.0,
248            user_data_blind_send: user_data_blind_send.unwrap_or(Blind::random(&mut OsRng)),
249            spend_hook_recv: spend_hook_recv.unwrap_or(FuncId::none()),
250            user_data_recv: user_data_recv.unwrap_or(pallas::Base::ZERO),
251            value_blinds: [partial.value_blinds[1], partial.value_blinds[0]],
252            token_blinds: [partial.token_blinds[1], partial.token_blinds[0]],
253            coin: burn_coin,
254            tree,
255            mint_zkbin,
256            mint_pk,
257            burn_zkbin,
258            burn_pk,
259        };
260        let debris = builder.build()?;
261
262        // Build the full transaction
263        let full_params = MoneyTransferParamsV1 {
264            inputs: vec![partial.params.inputs[0].clone(), debris.params.inputs[0].clone()],
265            outputs: vec![partial.params.outputs[0].clone(), debris.params.outputs[0].clone()],
266        };
267
268        let full_proofs = vec![
269            partial.proofs[0].clone(),
270            debris.proofs[0].clone(),
271            partial.proofs[1].clone(),
272            debris.proofs[1].clone(),
273        ];
274
275        let mut data = vec![MoneyFunction::OtcSwapV1 as u8];
276        full_params.encode_async(&mut data).await?;
277        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
278        let mut tx_builder =
279            TransactionBuilder::new(ContractCallLeaf { call, proofs: full_proofs }, vec![])?;
280        let mut tx = tx_builder.build()?;
281
282        // Sign the transaction and return it
283        let sigs = tx.create_sigs(&[debris.signature_secret])?;
284        tx.signatures = vec![sigs];
285
286        Ok(tx)
287    }
288
289    /// Inspect and verify a given swap (half or full) transaction
290    pub async fn inspect_swap(&self, bytes: Vec<u8>, output: &mut Vec<String>) -> Result<()> {
291        // First we check if its a partial swap
292        if let Ok(partial) = deserialize_async::<PartialSwapData>(&bytes).await {
293            // Inspect the PartialSwapData
294            output.push(format!("{partial}"));
295            return Ok(())
296        }
297
298        // Try to deserialize a full swap transaction
299        let Ok(tx) = deserialize_async::<Transaction>(&bytes).await else {
300            return Err(Error::Custom(
301                "Failed to deserialize to Transaction or PartialSwapData".to_string(),
302            ))
303        };
304
305        // Default error to return in case insection fails
306        let insection_error = Err(Error::Custom("Inspection failed".to_string()));
307
308        // We're inspecting a full transaction
309        if tx.calls.len() != 1 {
310            output.push(format!(
311                "Found {} contract calls in the transaction, there should be 1",
312                tx.calls.len()
313            ));
314            return insection_error
315        }
316
317        let params: MoneyTransferParamsV1 = deserialize_async(&tx.calls[0].data.data[1..]).await?;
318        output.push(format!("Parameters:\n{params:#?}"));
319
320        if params.inputs.len() != 2 {
321            output.push(format!("Found {} inputs, there should be 2", params.inputs.len()));
322            return insection_error
323        }
324
325        if params.outputs.len() != 2 {
326            output.push(format!("Found {} outputs, there should be 2", params.outputs.len()));
327            return insection_error
328        }
329
330        // Try to decrypt one of the outputs.
331        let secret_keys = self.get_money_secrets().await?;
332        let mut skey: Option<SecretKey> = None;
333        let mut note: Option<MoneyNote> = None;
334        let mut param_output_idx = 0;
335
336        for param_output in &params.outputs {
337            output.push(format!("Trying to decrypt note in output {param_output_idx}"));
338
339            for secret in &secret_keys {
340                if let Ok(d_note) = param_output.note.decrypt::<MoneyNote>(secret) {
341                    let s: SecretKey = deserialize_async(&d_note.memo).await?;
342                    skey = Some(s);
343                    note = Some(d_note);
344                    output
345                        .push(String::from("Successfully decrypted and found an ephemeral secret"));
346                    break
347                }
348            }
349
350            if note.is_some() {
351                break
352            }
353
354            param_output_idx += 1;
355        }
356
357        let Some(note) = note else {
358            output.push(String::from("Error: Could not decrypt notes of either output"));
359            return insection_error
360        };
361
362        output.push(format!(
363            "Output[{param_output_idx}] value: {} ({})",
364            note.value,
365            encode_base10(note.value, BALANCE_BASE10_DECIMALS)
366        ));
367        output.push(format!("Output[{param_output_idx}] token ID: {}", note.token_id));
368
369        let skey = skey.unwrap();
370        let (pub_x, pub_y) = PublicKey::from_secret(skey).xy();
371        let coin = Coin::from(poseidon_hash([
372            pub_x,
373            pub_y,
374            pallas::Base::from(note.value),
375            note.token_id.inner(),
376            note.coin_blind.inner(),
377        ]));
378
379        if coin == params.outputs[param_output_idx].coin {
380            output.push(format!("Output[{param_output_idx}] coin matches decrypted note metadata"));
381        } else {
382            output.push(format!(
383                "Error: Output[{param_output_idx}] coin does not match note metadata"
384            ));
385            return insection_error
386        }
387
388        let valcom = pedersen_commitment_u64(note.value, note.value_blind);
389        let tokcom = poseidon_hash([note.token_id.inner(), note.token_blind.inner()]);
390
391        if valcom != params.outputs[param_output_idx].value_commit {
392            output.push(format!(
393                "Error: Output[{param_output_idx}] value commitment does not match note metadata"
394            ));
395            return insection_error
396        }
397
398        if tokcom != params.outputs[param_output_idx].token_commit {
399            output.push(format!(
400                "Error: Output[{param_output_idx}] token commitment does not match note metadata"
401            ));
402            return insection_error
403        }
404
405        output.push(String::from("Value and token commitments match decrypted note metadata"));
406
407        // Verify that the param output commitments match the other input commitments
408        match param_output_idx {
409            0 => {
410                if valcom != params.inputs[1].value_commit ||
411                    tokcom != params.inputs[1].token_commit
412                {
413                    output.push(String::from(
414                        "Error: Value/Token commits of output[0] do not match input[1]",
415                    ));
416                    return insection_error
417                }
418            }
419            1 => {
420                if valcom != params.inputs[0].value_commit ||
421                    tokcom != params.inputs[0].token_commit
422                {
423                    output.push(String::from(
424                        "Error: Value/Token commits of output[1] do not match input[0]",
425                    ));
426                    return insection_error
427                }
428            }
429            _ => unreachable!(),
430        }
431
432        output.push(String::from("Found matching pedersen commitments for outputs and inputs"));
433
434        Ok(())
435    }
436
437    /// Sign given swap transaction by retrieving the secret key from the encrypted
438    /// note and prepending it to the transaction's signatures.
439    pub async fn sign_swap(&self, tx: &mut Transaction) -> Result<()> {
440        // We need our secret keys to try and decrypt the notes
441        let secret_keys = self.get_money_secrets().await?;
442        let params: MoneyTransferParamsV1 = deserialize_async(&tx.calls[0].data.data[1..]).await?;
443
444        // We wil try to decrypt each note separately,
445        // since we might us the same key in both of them.
446        let mut found = false;
447
448        // Try to decrypt the first note
449        for secret in &secret_keys {
450            let Ok(note) = &params.outputs[0].note.decrypt::<MoneyNote>(secret) else { continue };
451
452            // Sign the swap transaction
453            let skey: SecretKey = deserialize_async(&note.memo).await?;
454            let sigs = tx.create_sigs(&[skey])?;
455
456            // If transaction contains both signatures, replace the first one,
457            // otherwise insert signature on first position.
458            if tx.signatures[0].len() == 2 {
459                tx.signatures[0][0] = sigs[0];
460            } else {
461                tx.signatures[0].insert(0, sigs[0]);
462            }
463
464            found = true;
465            break
466        }
467
468        // Try to decrypt the second note
469        for secret in &secret_keys {
470            let Ok(note) = &params.outputs[1].note.decrypt::<MoneyNote>(secret) else { continue };
471
472            // Sign the swap transaction
473            let skey: SecretKey = deserialize_async(&note.memo).await?;
474            let sigs = tx.create_sigs(&[skey])?;
475
476            // If transaction contains both signatures, replace the second one,
477            // otherwise replace the first one.
478            if tx.signatures[0].len() == 2 {
479                tx.signatures[0][1] = sigs[0];
480            } else {
481                tx.signatures[0][0] = sigs[0];
482            }
483
484            found = true;
485            break
486        }
487
488        if !found {
489            return Err(Error::Custom(
490                "Failed to decrypt note with any of our secret keys".to_string(),
491            ))
492        };
493
494        Ok(())
495    }
496}