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>) -> 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            println!("{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            eprintln!(
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        println!("Parameters:\n{:#?}", params);
319
320        if params.inputs.len() != 2 {
321            eprintln!("Found {} inputs, there should be 2", params.inputs.len());
322            return insection_error
323        }
324
325        if params.outputs.len() != 2 {
326            eprintln!("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 output_idx = 0;
335
336        for output in &params.outputs {
337            println!("Trying to decrypt note in output {output_idx}");
338
339            for secret in &secret_keys {
340                if let Ok(d_note) = 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                    println!("Successfully decrypted and found an ephemeral secret");
345                    break
346                }
347            }
348
349            if note.is_some() {
350                break
351            }
352
353            output_idx += 1;
354        }
355
356        let Some(note) = note else {
357            eprintln!("Error: Could not decrypt notes of either output");
358            return insection_error
359        };
360
361        println!(
362            "Output[{output_idx}] value: {} ({})",
363            note.value,
364            encode_base10(note.value, BALANCE_BASE10_DECIMALS)
365        );
366        println!("Output[{output_idx}] token ID: {}", note.token_id);
367
368        let skey = skey.unwrap();
369        let (pub_x, pub_y) = PublicKey::from_secret(skey).xy();
370        let coin = Coin::from(poseidon_hash([
371            pub_x,
372            pub_y,
373            pallas::Base::from(note.value),
374            note.token_id.inner(),
375            note.coin_blind.inner(),
376        ]));
377
378        if coin == params.outputs[output_idx].coin {
379            println!("Output[{output_idx}] coin matches decrypted note metadata");
380        } else {
381            eprintln!("Error: Output[{output_idx}] coin does not match note metadata");
382            return insection_error
383        }
384
385        let valcom = pedersen_commitment_u64(note.value, note.value_blind);
386        let tokcom = poseidon_hash([note.token_id.inner(), note.token_blind.inner()]);
387
388        if valcom != params.outputs[output_idx].value_commit {
389            eprintln!("Error: Output[{output_idx}] value commitment does not match note metadata");
390            return insection_error
391        }
392
393        if tokcom != params.outputs[output_idx].token_commit {
394            eprintln!("Error: Output[{output_idx}] token commitment does not match note metadata");
395            return insection_error
396        }
397
398        println!("Value and token commitments match decrypted note metadata");
399
400        // Verify that the output commitments match the other input commitments
401        match output_idx {
402            0 => {
403                if valcom != params.inputs[1].value_commit ||
404                    tokcom != params.inputs[1].token_commit
405                {
406                    eprintln!("Error: Value/Token commits of output[0] do not match input[1]");
407                    return insection_error
408                }
409            }
410            1 => {
411                if valcom != params.inputs[0].value_commit ||
412                    tokcom != params.inputs[0].token_commit
413                {
414                    eprintln!("Error: Value/Token commits of output[1] do not match input[0]");
415                    return insection_error
416                }
417            }
418            _ => unreachable!(),
419        }
420
421        println!("Found matching pedersen commitments for outputs and inputs");
422
423        Ok(())
424    }
425
426    /// Sign given swap transaction by retrieving the secret key from the encrypted
427    /// note and prepending it to the transaction's signatures.
428    pub async fn sign_swap(&self, tx: &mut Transaction) -> Result<()> {
429        // We need our secret keys to try and decrypt the notes
430        let secret_keys = self.get_money_secrets().await?;
431        let params: MoneyTransferParamsV1 = deserialize_async(&tx.calls[0].data.data[1..]).await?;
432
433        // We wil try to decrypt each note separately,
434        // since we might us the same key in both of them.
435        let mut found = false;
436
437        // Try to decrypt the first note
438        for secret in &secret_keys {
439            let Ok(note) = &params.outputs[0].note.decrypt::<MoneyNote>(secret) else { continue };
440
441            // Sign the swap transaction
442            let skey: SecretKey = deserialize_async(&note.memo).await?;
443            let sigs = tx.create_sigs(&[skey])?;
444
445            // If transaction contains both signatures, replace the first one,
446            // otherwise insert signature on first position.
447            if tx.signatures[0].len() == 2 {
448                tx.signatures[0][0] = sigs[0];
449            } else {
450                tx.signatures[0].insert(0, sigs[0]);
451            }
452
453            found = true;
454            break
455        }
456
457        // Try to decrypt the second note
458        for secret in &secret_keys {
459            let Ok(note) = &params.outputs[1].note.decrypt::<MoneyNote>(secret) else { continue };
460
461            // Sign the swap transaction
462            let skey: SecretKey = deserialize_async(&note.memo).await?;
463            let sigs = tx.create_sigs(&[skey])?;
464
465            // If transaction contains both signatures, replace the second one,
466            // otherwise replace the first one.
467            if tx.signatures[0].len() == 2 {
468                tx.signatures[0][1] = sigs[0];
469            } else {
470                tx.signatures[0][0] = sigs[0];
471            }
472
473            found = true;
474            break
475        }
476
477        if !found {
478            eprintln!("Error: Failed to decrypt note with any of our secret keys");
479            return Err(Error::Custom(
480                "Failed to decrypt note with any of our secret keys".to_string(),
481            ))
482        };
483
484        Ok(())
485    }
486}