1use 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)]
49pub 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 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 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 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 let address = self.default_address().await?;
109
110 let tree = self.get_money_tree().await?;
112
113 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 let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
136 let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
137
138 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 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 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 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 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 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 let address = self.default_address().await?;
212
213 let tree = self.get_money_tree().await?;
215
216 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 let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
239 let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
240
241 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 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 let sigs = tx.create_sigs(&[debris.signature_secret])?;
284 tx.signatures = vec![sigs];
285
286 Ok(tx)
287 }
288
289 pub async fn inspect_swap(&self, bytes: Vec<u8>, output: &mut Vec<String>) -> Result<()> {
291 if let Ok(partial) = deserialize_async::<PartialSwapData>(&bytes).await {
293 output.push(format!("{partial}"));
295 return Ok(())
296 }
297
298 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 let insection_error = Err(Error::Custom("Inspection failed".to_string()));
307
308 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 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 ¶ms.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 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 pub async fn sign_swap(&self, tx: &mut Transaction) -> Result<()> {
440 let secret_keys = self.get_money_secrets().await?;
442 let params: MoneyTransferParamsV1 = deserialize_async(&tx.calls[0].data.data[1..]).await?;
443
444 let mut found = false;
447
448 for secret in &secret_keys {
450 let Ok(note) = ¶ms.outputs[0].note.decrypt::<MoneyNote>(secret) else { continue };
451
452 let skey: SecretKey = deserialize_async(¬e.memo).await?;
454 let sigs = tx.create_sigs(&[skey])?;
455
456 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 for secret in &secret_keys {
470 let Ok(note) = ¶ms.outputs[1].note.decrypt::<MoneyNote>(secret) else { continue };
471
472 let skey: SecretKey = deserialize_async(¬e.memo).await?;
474 let sigs = tx.create_sigs(&[skey])?;
475
476 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}