use std::fmt;
use rand::rngs::OsRng;
use darkfi::{
tx::{ContractCallLeaf, Transaction, TransactionBuilder},
util::parse::encode_base10,
zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof},
zkas::ZkBinary,
Error, Result,
};
use darkfi_money_contract::{
client::{swap_v1::SwapCallBuilder, MoneyNote},
model::{Coin, MoneyTransferParamsV1, TokenId},
MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
};
use darkfi_sdk::{
crypto::{
contract_id::MONEY_CONTRACT_ID, pedersen::pedersen_commitment_u64, poseidon_hash,
BaseBlind, Blind, FuncId, PublicKey, ScalarBlind, SecretKey,
},
pasta::pallas,
tx::ContractCall,
};
use darkfi_serial::{
async_trait, deserialize_async, AsyncEncodable, SerialDecodable, SerialEncodable,
};
use super::{money::BALANCE_BASE10_DECIMALS, Drk};
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct PartialSwapData {
params: MoneyTransferParamsV1,
proofs: Vec<Proof>,
value_pair: (u64, u64),
token_pair: (TokenId, TokenId),
value_blinds: Vec<ScalarBlind>,
token_blinds: Vec<BaseBlind>,
}
impl fmt::Display for PartialSwapData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s =
format!(
"{:#?}\nValue pair: {}:{}\nToken pair: {}:{}\nValue blinds: {:?}\nToken blinds: {:?}\n",
self.params, self.value_pair.0, self.value_pair.1, self.token_pair.0, self.token_pair.1,
self.value_blinds, self.token_blinds,
);
write!(f, "{}", s)
}
}
impl Drk {
pub async fn init_swap(
&self,
value_pair: (u64, u64),
token_pair: (TokenId, TokenId),
user_data_blind_send: Option<BaseBlind>,
spend_hook_recv: Option<FuncId>,
user_data_recv: Option<pallas::Base>,
) -> Result<PartialSwapData> {
let owncoins = self.get_token_coins(&token_pair.0).await?;
if owncoins.is_empty() {
return Err(Error::Custom(format!(
"Did not find any unspent coins with token ID: {}",
token_pair.0
)))
}
let mut burn_coin = None;
for coin in owncoins {
if coin.note.value == value_pair.0 {
burn_coin = Some(coin);
break
}
}
let Some(burn_coin) = burn_coin else {
return Err(Error::Custom(format!(
"Did not find any unspent coins of value {} and token_id {}",
value_pair.0, token_pair.0,
)))
};
let address = self.default_address().await?;
let tree = self.get_money_tree().await?;
let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
else {
return Err(Error::Custom("Mint circuit not found".to_string()))
};
let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
else {
return Err(Error::Custom("Burn circuit not found".to_string()))
};
let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
let value_blinds = [Blind::random(&mut OsRng), Blind::random(&mut OsRng)];
let token_blinds = [Blind::random(&mut OsRng), Blind::random(&mut OsRng)];
let builder = SwapCallBuilder {
pubkey: address,
value_send: value_pair.0,
token_id_send: token_pair.0,
value_recv: value_pair.1,
token_id_recv: token_pair.1,
user_data_blind_send: user_data_blind_send.unwrap_or(Blind::random(&mut OsRng)),
spend_hook_recv: spend_hook_recv.unwrap_or(FuncId::none()),
user_data_recv: user_data_recv.unwrap_or(pallas::Base::ZERO),
value_blinds,
token_blinds,
coin: burn_coin,
tree,
mint_zkbin,
mint_pk,
burn_zkbin,
burn_pk,
};
let debris = builder.build()?;
let ret = PartialSwapData {
params: debris.params,
proofs: debris.proofs,
value_pair,
token_pair,
value_blinds: value_blinds.to_vec(),
token_blinds: token_blinds.to_vec(),
};
Ok(ret)
}
pub async fn join_swap(
&self,
partial: PartialSwapData,
user_data_blind_send: Option<BaseBlind>,
spend_hook_recv: Option<FuncId>,
user_data_recv: Option<pallas::Base>,
) -> Result<Transaction> {
let owncoins = self.get_token_coins(&partial.token_pair.1).await?;
if owncoins.is_empty() {
return Err(Error::Custom(format!(
"Did not find any unspent coins with token ID: {}",
partial.token_pair.1
)))
}
let mut burn_coin = None;
for coin in owncoins {
if coin.note.value == partial.value_pair.1 {
burn_coin = Some(coin);
break
}
}
let Some(burn_coin) = burn_coin else {
return Err(Error::Custom(format!(
"Did not find any unspent coins of value {} and token_id {}",
partial.value_pair.1, partial.token_pair.1,
)))
};
let address = self.default_address().await?;
let tree = self.get_money_tree().await?;
let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
else {
return Err(Error::Custom("Mint circuit not found".to_string()))
};
let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1)
else {
return Err(Error::Custom("Burn circuit not found".to_string()))
};
let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
let builder = SwapCallBuilder {
pubkey: address,
value_send: partial.value_pair.1,
token_id_send: partial.token_pair.1,
value_recv: partial.value_pair.0,
token_id_recv: partial.token_pair.0,
user_data_blind_send: user_data_blind_send.unwrap_or(Blind::random(&mut OsRng)),
spend_hook_recv: spend_hook_recv.unwrap_or(FuncId::none()),
user_data_recv: user_data_recv.unwrap_or(pallas::Base::ZERO),
value_blinds: [partial.value_blinds[1], partial.value_blinds[0]],
token_blinds: [partial.token_blinds[1], partial.token_blinds[0]],
coin: burn_coin,
tree,
mint_zkbin,
mint_pk,
burn_zkbin,
burn_pk,
};
let debris = builder.build()?;
let full_params = MoneyTransferParamsV1 {
inputs: vec![partial.params.inputs[0].clone(), debris.params.inputs[0].clone()],
outputs: vec![partial.params.outputs[0].clone(), debris.params.outputs[0].clone()],
};
let full_proofs = vec![
partial.proofs[0].clone(),
debris.proofs[0].clone(),
partial.proofs[1].clone(),
debris.proofs[1].clone(),
];
let mut data = vec![MoneyFunction::OtcSwapV1 as u8];
full_params.encode_async(&mut data).await?;
let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
let mut tx_builder =
TransactionBuilder::new(ContractCallLeaf { call, proofs: full_proofs }, vec![])?;
let mut tx = tx_builder.build()?;
let sigs = tx.create_sigs(&[debris.signature_secret])?;
tx.signatures = vec![sigs];
Ok(tx)
}
pub async fn inspect_swap(&self, bytes: Vec<u8>) -> Result<()> {
if let Ok(partial) = deserialize_async::<PartialSwapData>(&bytes).await {
println!("{partial}");
return Ok(())
}
let Ok(tx) = deserialize_async::<Transaction>(&bytes).await else {
return Err(Error::Custom(
"Failed to deserialize to Transaction or PartialSwapData".to_string(),
))
};
let insection_error = Err(Error::Custom("Inspection failed".to_string()));
if tx.calls.len() != 1 {
eprintln!(
"Found {} contract calls in the transaction, there should be 1",
tx.calls.len()
);
return insection_error
}
let params: MoneyTransferParamsV1 = deserialize_async(&tx.calls[0].data.data[1..]).await?;
println!("Parameters:\n{:#?}", params);
if params.inputs.len() != 2 {
eprintln!("Found {} inputs, there should be 2", params.inputs.len());
return insection_error
}
if params.outputs.len() != 2 {
eprintln!("Found {} outputs, there should be 2", params.outputs.len());
return insection_error
}
let secret_keys = self.get_money_secrets().await?;
let mut skey: Option<SecretKey> = None;
let mut note: Option<MoneyNote> = None;
let mut output_idx = 0;
for output in ¶ms.outputs {
println!("Trying to decrypt note in output {output_idx}");
for secret in &secret_keys {
if let Ok(d_note) = output.note.decrypt::<MoneyNote>(secret) {
let s: SecretKey = deserialize_async(&d_note.memo).await?;
skey = Some(s);
note = Some(d_note);
println!("Successfully decrypted and found an ephemeral secret");
break
}
}
if note.is_some() {
break
}
output_idx += 1;
}
let Some(note) = note else {
eprintln!("Error: Could not decrypt notes of either output");
return insection_error
};
println!(
"Output[{output_idx}] value: {} ({})",
note.value,
encode_base10(note.value, BALANCE_BASE10_DECIMALS)
);
println!("Output[{output_idx}] token ID: {}", note.token_id);
let skey = skey.unwrap();
let (pub_x, pub_y) = PublicKey::from_secret(skey).xy();
let coin = Coin::from(poseidon_hash([
pub_x,
pub_y,
pallas::Base::from(note.value),
note.token_id.inner(),
note.coin_blind.inner(),
]));
if coin == params.outputs[output_idx].coin {
println!("Output[{output_idx}] coin matches decrypted note metadata");
} else {
eprintln!("Error: Output[{output_idx}] coin does not match note metadata");
return insection_error
}
let valcom = pedersen_commitment_u64(note.value, note.value_blind);
let tokcom = poseidon_hash([note.token_id.inner(), note.token_blind.inner()]);
if valcom != params.outputs[output_idx].value_commit {
eprintln!("Error: Output[{output_idx}] value commitment does not match note metadata");
return insection_error
}
if tokcom != params.outputs[output_idx].token_commit {
eprintln!("Error: Output[{output_idx}] token commitment does not match note metadata");
return insection_error
}
println!("Value and token commitments match decrypted note metadata");
match output_idx {
0 => {
if valcom != params.inputs[1].value_commit ||
tokcom != params.inputs[1].token_commit
{
eprintln!("Error: Value/Token commits of output[0] do not match input[1]");
return insection_error
}
}
1 => {
if valcom != params.inputs[0].value_commit ||
tokcom != params.inputs[0].token_commit
{
eprintln!("Error: Value/Token commits of output[1] do not match input[0]");
return insection_error
}
}
_ => unreachable!(),
}
println!("Found matching pedersen commitments for outputs and inputs");
Ok(())
}
pub async fn sign_swap(&self, tx: &mut Transaction) -> Result<()> {
let secret_keys = self.get_money_secrets().await?;
let params: MoneyTransferParamsV1 = deserialize_async(&tx.calls[0].data.data[1..]).await?;
let mut found = false;
for secret in &secret_keys {
let Ok(note) = ¶ms.outputs[0].note.decrypt::<MoneyNote>(secret) else { continue };
let skey: SecretKey = deserialize_async(¬e.memo).await?;
let sigs = tx.create_sigs(&[skey])?;
if tx.signatures[0].len() == 2 {
tx.signatures[0][0] = sigs[0];
} else {
tx.signatures[0].insert(0, sigs[0]);
}
found = true;
break
}
for secret in &secret_keys {
let Ok(note) = ¶ms.outputs[1].note.decrypt::<MoneyNote>(secret) else { continue };
let skey: SecretKey = deserialize_async(¬e.memo).await?;
let sigs = tx.create_sigs(&[skey])?;
if tx.signatures[0].len() == 2 {
tx.signatures[0][1] = sigs[0];
} else {
tx.signatures[0][0] = sigs[0];
}
found = true;
break
}
if !found {
eprintln!("Error: Failed to decrypt note with any of our secret keys");
return Err(Error::Custom(
"Failed to decrypt note with any of our secret keys".to_string(),
))
};
Ok(())
}
}