diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index ba2ec0d4a..32f07a017 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -935,10 +935,10 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { OtcSubcmd::Init { value_pair, token_pair } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; - let (vp_send, vp_recv) = parse_value_pair(&value_pair)?; - let (tp_send, tp_recv) = parse_token_pair(&drk, &token_pair).await?; + let value_pair = parse_value_pair(&value_pair)?; + let token_pair = parse_token_pair(&drk, &token_pair).await?; - let half = match drk.init_swap(vp_send, tp_send, vp_recv, tp_recv).await { + let half = match drk.init_swap(value_pair, token_pair, None, None, None).await { Ok(h) => h, Err(e) => { eprintln!("Failed to create swap transaction half: {e:?}"); @@ -962,7 +962,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; - let tx = match drk.join_swap(partial).await { + let tx = match drk.join_swap(partial, None, None, None).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create a join swap transaction: {e:?}"); diff --git a/bin/drk/src/swap.rs b/bin/drk/src/swap.rs index 03767d831..8e61fc25e 100644 --- a/bin/drk/src/swap.rs +++ b/bin/drk/src/swap.rs @@ -74,28 +74,35 @@ impl Drk { /// Initialize the first half of an atomic swap pub async fn init_swap( &self, - value_send: u64, - token_send: TokenId, - value_recv: u64, - token_recv: TokenId, + value_pair: (u64, u64), + token_pair: (TokenId, TokenId), + user_data_blind_send: Option, + spend_hook_recv: Option, + user_data_recv: Option, ) -> Result { - // First we'll fetch all of our unspent coins from the wallet. - let mut owncoins = self.get_coins(false).await?; - // Then we see if we have one that we can send. - owncoins.retain(|x| { - x.0.note.value == value_send && - x.0.note.token_id == token_send && - x.0.note.spend_hook == FuncId::none() - }); - + // First get all unspent OwnCoins to see what our balance is + 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 of value {value_send} and token_id {token_send}", + "Did not find any unspent coins with token ID: {}", + token_pair.0 ))) } - // If there are any, we'll just spend the first one we see. - let burn_coin = owncoins[0].0.clone(); + // Find one with the correct value + 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, + ))) + }; // Fetch our default address let address = self.default_address().await?; @@ -103,12 +110,10 @@ impl Drk { // We'll also need our Merkle tree let tree = self.get_money_tree().await?; - let contract_id = *MONEY_CONTRACT_ID; - // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&contract_id).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 { @@ -126,23 +131,24 @@ impl Drk { let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); + // Creating Mint and Burn circuits proving keys + let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); + let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); + // Since we're creating the first half, we generate the blinds. let value_blinds = [Blind::random(&mut OsRng), Blind::random(&mut OsRng)]; let token_blinds = [Blind::random(&mut OsRng), Blind::random(&mut OsRng)]; // Now we should have everything we need to build the swap half - println!("Creating Mint and Burn circuit proving keys"); - 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, - token_id_send: token_send, - value_recv, - token_id_recv: token_recv, - user_data_blind_send: Blind::random(&mut OsRng), // <-- FIXME: Perhaps should be passed in - spend_hook_recv: FuncId::none(), // <-- FIXME: Should be passed in - user_data_recv: pallas::Base::ZERO, // <-- FIXME: Should be passed in + 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, @@ -152,16 +158,14 @@ impl Drk { burn_zkbin, burn_pk, }; - - println!("Building first half of the swap transaction"); let debris = builder.build()?; // Now we have the half, so we can build `PartialSwapData` and return it. let ret = PartialSwapData { params: debris.params, proofs: debris.proofs, - value_pair: (value_send, value_recv), - token_pair: (token_send, token_recv), + value_pair, + token_pair, value_blinds: value_blinds.to_vec(), token_blinds: token_blinds.to_vec(), }; @@ -171,23 +175,37 @@ impl Drk { /// Create a full transaction by inspecting and verifying given partial swap data, /// making the other half, and joining all this into a `Transaction` object. - pub async fn join_swap(&self, partial: PartialSwapData) -> Result { + pub async fn join_swap( + &self, + partial: PartialSwapData, + user_data_blind_send: Option, + spend_hook_recv: Option, + user_data_recv: Option, + ) -> Result { // Our side of the tx in the pairs is the second half, so we try to find // an unspent coin like that in our wallet. - let mut owncoins = self.get_coins(false).await?; - owncoins.retain(|x| { - x.0.note.value == partial.value_pair.1 && x.0.note.token_id == partial.token_pair.1 - }); - + 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 of value {} and token_id {}", - partial.value_pair.1, partial.token_pair.1 + "Did not find any unspent coins with token ID: {}", + partial.token_pair.1 ))) } - // If there are any, we'll just spend the first one we see. - let burn_coin = owncoins[0].0.clone(); + // Find one with the correct value + 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, + ))) + }; // Fetch our default address let address = self.default_address().await?; @@ -195,12 +213,10 @@ impl Drk { // We'll also need our Merkle tree let tree = self.get_money_tree().await?; - let contract_id = *MONEY_CONTRACT_ID; - // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&contract_id).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 { @@ -218,21 +234,20 @@ impl Drk { let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); - // TODO: Maybe some kind of verification at this point - - // Now we should have everything we need to build the swap half - println!("Creating Mint and Burn circuit proving keys"); + // Creating Mint and Burn circuits proving keys let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); + + // Now we should have everything we need to build the swap half 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: Blind::random(&mut OsRng), // <-- FIXME: Perhaps should be passed in - spend_hook_recv: FuncId::none(), // <-- FIXME: Should be passed in - user_data_recv: pallas::Base::ZERO, // <-- FIXME: Should be passed in + 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, @@ -242,10 +257,9 @@ impl Drk { burn_zkbin, burn_pk, }; - - println!("Building second half of the swap transaction"); let debris = builder.build()?; + // Build the full transaction 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()], @@ -264,7 +278,8 @@ impl Drk { let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs: full_proofs }, vec![])?; let mut tx = tx_builder.build()?; - println!("Signing swap transaction"); + + // Sign the transaction and return it let sigs = tx.create_sigs(&[debris.signature_secret])?; tx.signatures = vec![sigs]; @@ -273,156 +288,137 @@ impl Drk { /// Inspect and verify a given swap (half or full) transaction pub async fn inspect_swap(&self, bytes: Vec) -> Result<()> { - // Default error to return in case insection fails - let insection_error = Err(Error::Custom("Inspection failed".to_string())); - - let mut full: Option = None; - let mut half: Option = None; - - if let Ok(v) = deserialize_async(&bytes).await { - full = Some(v) - }; - - match deserialize_async(&bytes).await { - Ok(v) => half = Some(v), - Err(_) => { - if full.is_none() { - return Err(Error::Custom( - "Failed to deserialize to Transaction or PartialSwapData".to_string(), - )) - } - } - } - - if let Some(tx) = full { - // We're inspecting a full transaction - 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 - } - - // Try to decrypt one of the outputs. - let secret_keys = self.get_money_secrets().await?; - let mut skey: Option = None; - let mut note: Option = 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::(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"); - - // Verify that the output commitments match the other input commitments - 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"); - - // TODO: Verify signature - // TODO: Verify ZK proofs + // First we check if its a partial swap + if let Ok(partial) = deserialize_async::(&bytes).await { + // Inspect the PartialSwapData + println!("{partial}"); return Ok(()) } - // Inspect PartialSwapData - let partial = half.unwrap(); - println!("{partial}"); + // Try to deserialize a full swap transaction + let Ok(tx) = deserialize_async::(&bytes).await else { + return Err(Error::Custom( + "Failed to deserialize to Transaction or PartialSwapData".to_string(), + )) + }; + + // Default error to return in case insection fails + let insection_error = Err(Error::Custom("Inspection failed".to_string())); + + // We're inspecting a full transaction + 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 + } + + // Try to decrypt one of the outputs. + let secret_keys = self.get_money_secrets().await?; + let mut skey: Option = None; + let mut note: Option = 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::(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"); + + // Verify that the output commitments match the other input commitments + 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(()) } diff --git a/bin/drk/src/transfer.rs b/bin/drk/src/transfer.rs index 51045c590..b8cb9d24e 100644 --- a/bin/drk/src/transfer.rs +++ b/bin/drk/src/transfer.rs @@ -46,7 +46,9 @@ impl Drk { // First get all unspent OwnCoins to see what our balance is let owncoins = self.get_token_coins(&token_id).await?; if owncoins.is_empty() { - return Err(Error::Custom(format!("Did not find any coins with token ID: {token_id}"))) + return Err(Error::Custom(format!( + "Did not find any unspent coins with token ID: {token_id}" + ))) } let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?; @@ -62,12 +64,13 @@ impl Drk { ))) } - // We'll also need our Merkle tree - let tree = self.get_money_tree().await?; - + // Fetch our default secret let secret = self.default_secret().await?; let keypair = Keypair::new(secret); + // We'll also need our Merkle tree + let tree = self.get_money_tree().await?; + // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. diff --git a/src/contract/money/src/entrypoint/swap_v1.rs b/src/contract/money/src/entrypoint/swap_v1.rs index b1822d0da..a46e6bec1 100644 --- a/src/contract/money/src/entrypoint/swap_v1.rs +++ b/src/contract/money/src/entrypoint/swap_v1.rs @@ -115,14 +115,6 @@ pub(crate) fn money_otcswap_process_instruction_v1( msg!("[OtcSwapV1] Iterating over anonymous inputs"); for (i, input) in params.inputs.iter().enumerate() { - // For now, make sure that the inputs' spend hooks are zero. - // This should however be allowed to some extent, e.g. if we - // want a DAO to be able to do an atomic swap. - if calls[call_idx as usize].parent_index.is_some() { - msg!("[OtcSwapV1] Error: Unable to swap coins with spend_hook != 0 (input {})", i); - return Err(MoneyError::SpendHookNonZero.into()) - } - // The Merkle root is used to know whether this coin // has existed in a previous state. if !wasm::db::db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {