diff --git a/src/silentpayments.rs b/src/silentpayments.rs index ff9301f..750c91c 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -5,88 +5,43 @@ use std::str::FromStr; use anyhow::{Error, Result}; use rand::{thread_rng, Rng}; +use sp_client::bitcoin::consensus::deserialize; use sp_client::bitcoin::hashes::{sha256, Hash}; use sp_client::bitcoin::hex::FromHex; -use sp_client::bitcoin::psbt::raw; -use sp_client::bitcoin::{Psbt, Transaction, Txid}; -use sp_client::bitcoin::{Amount, OutPoint}; -use sp_client::bitcoin::consensus::deserialize; +use sp_client::bitcoin::key::{Keypair, Secp256k1, TapTweak}; +use sp_client::bitcoin::psbt::{raw, Output}; +use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::{Address, Psbt, ScriptBuf, Transaction, Txid}; +use sp_client::bitcoin::{Amount, OutPoint, TxOut}; +use sp_client::constants::{ + self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, +}; +use sp_client::silentpayments::utils::sending::calculate_ecdh_shared_secret; use sp_client::silentpayments::utils::SilentPaymentAddress; use sp_client::spclient::{OwnedOutput, Recipient, SpClient, SpWallet}; -use sp_client::constants; -pub fn create_transaction(sp_address: SilentPaymentAddress, sp_wallet: &SpWallet, fee_rate: Amount) -> Result { - let available_outpoints = sp_wallet.get_outputs().to_spendable_list(); +use crate::crypto::AnkSharedSecret; - // Here we need to add more heuristics about which outpoint we spend - // For now let's keep it simple - - let mut inputs: HashMap = HashMap::new(); - - let mut total_available = Amount::from_sat(0); - for (outpoint, output) in available_outpoints { - total_available += output.amount; - inputs.insert(outpoint, output); - if total_available > Amount::from_sat(1000) { - break; - } - } - - if total_available < Amount::from_sat(1000) { - return Err(Error::msg("Not enough available funds")); - } - - let recipient = Recipient { - address: sp_address.into(), - amount: Amount::from_sat(1000), - nb_outputs: 1, - }; - - let mut new_psbt = sp_wallet.get_client().create_new_psbt( - inputs, - vec![recipient], - None, - )?; - - let change_addr = sp_wallet.get_client().sp_receiver.get_change_address(); - SpClient::set_fees(&mut new_psbt, fee_rate, change_addr)?; - - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&new_psbt)?; - - // This wouldn't work with many recipients in the same transaction - // each address (or more precisely each scan public key) would have its own point - // let shared_point = shared_secret_point(&sp_address.get_scan_key(), &partial_secret); - - sp_wallet - .get_client() - .fill_sp_outputs(&mut new_psbt, partial_secret)?; - let mut aux_rand = [0u8; 32]; - rand::thread_rng().fill(&mut aux_rand); - let mut signed = sp_wallet.get_client().sign_psbt(new_psbt, &aux_rand)?; - SpClient::finalize_psbt(&mut signed)?; - - let final_tx = signed.extract_tx()?; - - Ok(final_tx) -} - -pub fn create_transaction_spend_outpoint( - outpoint: &OutPoint, +pub fn create_transaction( + mandatory_inputs: &[&OutPoint], sp_wallet: &SpWallet, mut recipient: Recipient, - commited_in_txid: &Txid, payload: Option>, - fee_rate: Amount + fee_rate: Amount, + fee_payer: Option, // None means sender pays everything ) -> Result { let available_outpoints = sp_wallet.get_outputs().to_spendable_list(); + let recipient_address = SilentPaymentAddress::try_from(recipient.address.as_str())?; let mut inputs: HashMap = HashMap::new(); let mut total_available = Amount::from_sat(0); - let (must_outpoint, must_output) = available_outpoints.get_key_value(outpoint).ok_or_else(|| Error::msg("Mandatory outpoint unknown"))?; - total_available += must_output.amount; - inputs.insert(*must_outpoint, must_output.clone()); + for outpoint in mandatory_inputs { + let (must_outpoint, must_output) = available_outpoints + .get_key_value(outpoint) + .ok_or_else(|| Error::msg("Mandatory outpoint unknown"))?; + total_available += must_output.amount; + inputs.insert(*must_outpoint, must_output.clone()); + } for (outpoint, output) in available_outpoints { if total_available > Amount::from_sat(1000) { @@ -100,44 +55,99 @@ pub fn create_transaction_spend_outpoint( return Err(Error::msg("Not enough available funds")); } - // update the amount for the recipient - recipient.amount = total_available; - - // Take the recipient address - let address = recipient.address.clone(); - - let mut commitment = [0u8;32]; - if let Some(p) = payload { - let mut engine = sha256::HashEngine::default(); - engine.write_all(&p)?; - let hash = sha256::Hash::from_engine(engine); - - commitment.copy_from_slice(hash.as_byte_array()); - } else { - // create a dummy commitment that is H(b_scan | commited_in txid) - let mut buf = [0u8;64]; - buf[..32].copy_from_slice(commited_in_txid.as_raw_hash().as_byte_array()); - buf[32..].copy_from_slice(&sp_wallet.get_client().get_scan_key().secret_bytes()); - - let mut engine = sha256::HashEngine::default(); - engine.write_all(&buf)?; - let hash = sha256::Hash::from_engine(engine); - - commitment.copy_from_slice(hash.as_byte_array()); + if recipient.amount == Amount::from_sat(0) { + // update the amount for the recipient + recipient.amount = total_available; } - let mut new_psbt = sp_wallet.get_client().create_new_psbt( - inputs, - vec![recipient], - Some(&commitment), - )?; + let mut commitment = [0u8; 32]; + if let Some(ref p) = payload { + commitment.copy_from_slice(&p); + } else { + thread_rng().fill(&mut commitment); + } - SpClient::set_fees(&mut new_psbt, fee_rate, address)?; + let mut new_psbt = + sp_wallet + .get_client() + .create_new_psbt(inputs, vec![recipient], Some(&commitment))?; + + let sender_address = sp_wallet.get_client().get_receiving_address(); + let change_address = sp_wallet.get_client().sp_receiver.get_change_address(); + if let Some(address) = fee_payer { + SpClient::set_fees(&mut new_psbt, fee_rate, address)?; + } else { + let candidates: Vec> = new_psbt.outputs + .iter() + .map(|o| { + if let Some(value) = o.proprietary.get(&raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), + }) { + let candidate: String = + SilentPaymentAddress::try_from(deserialize::(value).unwrap()) + .unwrap() + .into(); + return Some(candidate); + } else { + return None; + } + }) + .collect(); + + let mut fee_set = false; + for candidate in candidates { + if let Some(c) = candidate { + if c == change_address { + SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?; + fee_set = true; + break; + } else if c == sender_address { + SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?; + fee_set = true; + break; + } + } + } + + if !fee_set { + return Err(Error::msg("Must specify payer for fee")); + } + }; let partial_secret = sp_wallet .get_client() .get_partial_secret_from_psbt(&new_psbt)?; + // if we have a payload, it means we are notifying, so let's add a revokation output + if payload.is_some() { + let shared_point = + calculate_ecdh_shared_secret(&recipient_address.get_scan_key(), &partial_secret); + + let shared_secret = AnkSharedSecret::new(shared_point); + + // add the revokation output + let revokation_key = + Keypair::from_seckey_slice(&Secp256k1::signing_only(), &shared_secret.to_byte_array())?; + let spk = ScriptBuf::new_p2tr_tweaked( + revokation_key + .x_only_public_key() + .0 + .dangerous_assume_tweaked(), + ); + + let txout = TxOut { + value: Amount::from_sat(0), + script_pubkey: spk, + }; + + // For now let's just push it to the last output + new_psbt.unsigned_tx.output.push(txout); + + new_psbt.outputs.push(Output::default()); + } + sp_wallet .get_client() .fill_sp_outputs(&mut new_psbt, partial_secret)?; @@ -149,58 +159,6 @@ pub fn create_transaction_spend_outpoint( Ok(signed) } -pub fn create_transaction_for_address_with_shared_secret( - recipient: Recipient, - sp_wallet: &SpWallet, - message: Option<&str>, - fee_rate: Amount, -) -> Result { - let available_outpoints = sp_wallet.get_outputs().to_spendable_list(); - - // Here we need to add more heuristics about which outpoint we spend - // For now let's keep it simple - - let mut inputs: HashMap = HashMap::new(); - - let mut total_available = Amount::from_sat(0); - for (outpoint, output) in available_outpoints { - total_available += output.amount; - inputs.insert(outpoint, output); - if total_available > Amount::from_sat(1000) { - break; - } - } - - if total_available < Amount::from_sat(1000) { - return Err(Error::msg("Not enough available funds")); - } - - let message_bin = if message.is_some() { Vec::from_hex(message.unwrap())? } else { vec![] }; - - let mut new_psbt = sp_wallet.get_client().create_new_psbt( - inputs, - vec![recipient], - if !message_bin.is_empty() { Some(&message_bin) } else { None }, - )?; - - let change_addr = sp_wallet.get_client().sp_receiver.get_change_address(); - SpClient::set_fees(&mut new_psbt, fee_rate, change_addr)?; - - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&new_psbt)?; - - sp_wallet - .get_client() - .fill_sp_outputs(&mut new_psbt, partial_secret)?; - let mut aux_rand = [0u8; 32]; - rand::thread_rng().fill(&mut aux_rand); - let mut signed = sp_wallet.get_client().sign_psbt(new_psbt, &aux_rand)?; - SpClient::finalize_psbt(&mut signed)?; - - Ok(signed.to_string()) -} - pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result>> { let psbt = Psbt::from_str(&psbt_str)?; @@ -225,3 +183,198 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result PublicKey { + let prevout = tx.input.get(0).unwrap().to_owned(); + let outpoint_data = ( + prevout.previous_output.txid.to_string(), + prevout.previous_output.vout, + ); + let input_pubkey = + get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap(); + let tweak_data = + calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap(); + tweak_data + } + + fn helper_create_commitment(payload_to_hash: String) -> String { + let mut engine = sha256::HashEngine::default(); + engine.write_all(&payload_to_hash.as_bytes()); + let hash = sha256::Hash::from_engine(engine); + hash.to_byte_array().to_lower_hex_string() + } + + #[test] + fn it_creates_notification_transaction() { + let recipient = Recipient { + address: BOB_ADDRESS.to_owned(), + amount: Amount::from_sat(1200), + nb_outputs: 1, + }; + let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap(); + let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap(); + let message: CipherMessage = CipherMessage::new(ALICE_ADDRESS.to_owned(), "TEST".to_owned()); + let commitment = helper_create_commitment(serde_json::to_string(&message).unwrap()); + + assert!(commitment == "d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99"); + + let psbt = create_transaction( + &vec![], + &alice_wallet, + recipient, + Some(Vec::from_hex(COMMITMENT).unwrap()), + FEE_RATE, + None, + ) + .unwrap(); + + let final_tx = psbt.extract_tx().unwrap(); + let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1"; + + let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); + + // Check that Alice and Bob are both able to find that transaction + let alice_update = alice_wallet + .update_wallet_with_transaction(&final_tx, 0, tweak_data) + .unwrap(); + assert!(alice_update.len() > 0); + let bob_update = bob_wallet + .update_wallet_with_transaction(&final_tx, 0, tweak_data) + .unwrap(); + assert!(bob_update.len() > 0); + println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); + println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); + assert!(false); + } + + #[test] + fn it_creates_confirmation_transaction() { + let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap(); + let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap(); + + // Bob must spend notification output + let (confirmation_outpoint, _) = bob_wallet + .get_outputs() + .get_outpoint( + OutPoint::from_str( + "148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0", + ) + .unwrap(), + ) + .unwrap(); + + let recipient = Recipient { + address: ALICE_ADDRESS.to_owned(), + amount: Amount::from_sat(0), + nb_outputs: 1, + }; + + let psbt = create_transaction( + &vec![&confirmation_outpoint], + &bob_wallet, + recipient, + None, + FEE_RATE, + Some(ALICE_ADDRESS.to_owned()), + ) + .unwrap(); + + let final_tx = psbt.extract_tx().unwrap(); + // println!( + // "{}", + // serialize::(&final_tx).to_lower_hex_string() + // ); + let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c"; + + let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); + + // Check that Alice and Bob are both able to find that transaction + let alice_update = alice_wallet + .update_wallet_with_transaction(&final_tx, 0, tweak_data) + .unwrap(); + assert!(alice_update.len() > 0); + let bob_update = bob_wallet + .update_wallet_with_transaction(&final_tx, 0, tweak_data) + .unwrap(); + assert!(bob_update.len() > 0); + println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); + println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); + assert!(false); + } + + #[test] + fn it_creates_answer_transaction() { + let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap(); + let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap(); + + // Bob must spend notification output + let (confirmation_outpoint, _) = alice_wallet + .get_outputs() + .get_outpoint( + OutPoint::from_str( + "bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0", + ) + .unwrap(), + ) + .unwrap(); + + let recipient = Recipient { + address: BOB_ADDRESS.to_owned(), + amount: Amount::from_sat(0), + nb_outputs: 1, + }; + + let psbt = create_transaction( + &vec![&confirmation_outpoint], + &alice_wallet, + recipient, + None, + FEE_RATE, + Some(BOB_ADDRESS.to_owned()), + ) + .unwrap(); + + let final_tx = psbt.extract_tx().unwrap(); + // println!("{}", serialize::(&final_tx).to_lower_hex_string()); + let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa"; + + let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); + + // Check that Alice and Bob are both able to find that transaction + let alice_update = alice_wallet + .update_wallet_with_transaction(&final_tx, 0, tweak_data) + .unwrap(); + assert!(alice_update.len() > 0); + let bob_update = bob_wallet + .update_wallet_with_transaction(&final_tx, 0, tweak_data) + .unwrap(); + assert!(bob_update.len() > 0); + println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); + println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); + assert!(false); + } +}