From 34076ed33fcbf4a7869b55c29a0fa5fee633b937 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 10:32:54 -0400 Subject: [PATCH 1/8] Error cli with anyhow context --- Cargo.lock | 7 +++++++ payjoin-client/Cargo.toml | 1 + payjoin-client/src/main.rs | 25 +++++++++++++++++-------- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e237978..4fca8bdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + [[package]] name = "ascii" version = "1.1.0" @@ -1144,6 +1150,7 @@ dependencies = [ name = "payjoin-client" version = "0.1.15" dependencies = [ + "anyhow", "base64", "bitcoin", "bitcoincore-rpc", diff --git a/payjoin-client/Cargo.toml b/payjoin-client/Cargo.toml index d34a6b6a..79753bc4 100644 --- a/payjoin-client/Cargo.toml +++ b/payjoin-client/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" native-tls-vendored = ["reqwest/native-tls-vendored"] [dependencies] +anyhow = "1.0.70" bitcoin = "0.29.2" payjoin = { path = "../payjoin", features = ["sender", "receiver"] } bitcoincore-rpc = "0.16.0" diff --git a/payjoin-client/src/main.rs b/payjoin-client/src/main.rs index cde1551b..b239862b 100644 --- a/payjoin-client/src/main.rs +++ b/payjoin-client/src/main.rs @@ -2,26 +2,31 @@ use std::collections::HashMap; use std::convert::TryFrom; use std::str::FromStr; +use anyhow::{Context, Result}; use bitcoincore_rpc::bitcoin::Amount; use bitcoincore_rpc::RpcApi; use clap::{arg, Arg, Command}; use payjoin::bitcoin::util::psbt::PartiallySignedTransaction as Psbt; use payjoin::{PjUriExt, UriExt}; -fn main() { +fn main() -> Result<()> { env_logger::init(); let matches = cli().get_matches(); - let port = matches.get_one::("PORT").unwrap(); - let cookie_file = matches.get_one::("COOKIE_FILE").unwrap(); + let port = matches.get_one::("PORT").context("Missing PORT argument")?; + let cookie_file = + matches.get_one::("COOKIE_FILE").context("Missing COOKIE_FILE argument")?; let bitcoind = bitcoincore_rpc::Client::new( - &format!("http://127.0.0.1:{}", port.parse::().unwrap()), + &format!( + "http://127.0.0.1:{}", + port.parse::().context("Failed to parse PORT argument")? + ), bitcoincore_rpc::Auth::CookieFile(cookie_file.into()), ) - .unwrap(); + .context("Failed to connect to bitcoind")?; match matches.subcommand() { Some(("send", sub_matches)) => { - let bip21 = sub_matches.get_one::("BIP21").unwrap(); + let bip21 = sub_matches.get_one::("BIP21").context("Missing BIP21 argument")?; let danger_accept_invalid_certs = match { sub_matches.get_one::("DANGER_ACCEPT_INVALID_CERTS") } { Some(danger_accept_invalid_certs) => @@ -31,12 +36,16 @@ fn main() { send_payjoin(bitcoind, bip21, danger_accept_invalid_certs); } Some(("receive", sub_matches)) => { - let amount = sub_matches.get_one::("AMOUNT").unwrap(); - let endpoint = sub_matches.get_one::("ENDPOINT").unwrap(); + let amount = + sub_matches.get_one::("AMOUNT").context("Missing AMOUNT argument")?; + let endpoint = + sub_matches.get_one::("ENDPOINT").context("Missing ENDPOINT argument")?; receive_payjoin(bitcoind, amount, endpoint); } _ => unreachable!(), // If all subcommands are defined above, anything else is unreachabe!() } + + Ok(()) } fn send_payjoin(bitcoind: bitcoincore_rpc::Client, bip21: &str, danger_accept_invalid_certs: bool) { From 40f81225099723c7ec407a010db85ca08b2f3900 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 11:03:41 -0400 Subject: [PATCH 2/8] Display PjParseError messages --- payjoin/src/uri.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/payjoin/src/uri.rs b/payjoin/src/uri.rs index 46573b3f..76a9dc37 100644 --- a/payjoin/src/uri.rs +++ b/payjoin/src/uri.rs @@ -172,6 +172,23 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState { } } +impl std::fmt::Display for PjParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + InternalPjParseError::BadPjOs => write!(f, "Bad pjos parameter"), + InternalPjParseError::MultipleParams(param) => { + write!(f, "Multiple instances of parameter '{}'", param) + } + InternalPjParseError::MissingEndpoint => write!(f, "Missing payjoin endpoint"), + InternalPjParseError::NotUtf8(_) => write!(f, "Endpoint is not valid UTF-8"), + InternalPjParseError::BadEndpoint(_) => write!(f, "Endpoint is not valid"), + InternalPjParseError::UnsecureEndpoint => { + write!(f, "Endpoint scheme is not secure (https or onion)") + } + } + } +} + #[derive(Debug)] enum InternalPjParseError { BadPjOs, From 17dbedb890a5b1603fd42cd90c7597a0b0c0f0c1 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 10:52:21 -0400 Subject: [PATCH 3/8] Handle send_payjoin errors with anyhow --- payjoin-client/src/main.rs | 61 ++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/payjoin-client/src/main.rs b/payjoin-client/src/main.rs index b239862b..228aacf0 100644 --- a/payjoin-client/src/main.rs +++ b/payjoin-client/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::convert::TryFrom; use std::str::FromStr; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use bitcoincore_rpc::bitcoin::Amount; use bitcoincore_rpc::RpcApi; use clap::{arg, Arg, Command}; @@ -33,7 +33,7 @@ fn main() -> Result<()> { bool::from_str(danger_accept_invalid_certs).unwrap_or(false), None => false, }; - send_payjoin(bitcoind, bip21, danger_accept_invalid_certs); + send_payjoin(bitcoind, bip21, danger_accept_invalid_certs)?; } Some(("receive", sub_matches)) => { let amount = @@ -48,18 +48,22 @@ fn main() -> Result<()> { Ok(()) } -fn send_payjoin(bitcoind: bitcoincore_rpc::Client, bip21: &str, danger_accept_invalid_certs: bool) { - let link = payjoin::Uri::try_from(bip21).unwrap(); +fn send_payjoin( + bitcoind: bitcoincore_rpc::Client, + bip21: &str, + danger_accept_invalid_certs: bool, +) -> Result<()> { + let link = payjoin::Uri::try_from(bip21) + .map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?; let link = link .check_pj_supported() - .unwrap_or_else(|_| panic!("The provided URI doesn't support payjoin (BIP78)")); + .map_err(|e| anyhow!("The provided URI doesn't support payjoin (BIP78): {}", e))?; - if link.amount.is_none() { - panic!("please specify the amount in the Uri"); - } - - let amount = Amount::from_sat(link.amount.unwrap().to_sat()); + let amount = link + .amount + .ok_or(anyhow!("please specify the amount in the Uri")) + .map(|amt| Amount::from_sat(amt.to_sat()))?; let mut outputs = HashMap::with_capacity(1); outputs.insert(link.address.to_string(), amount); @@ -76,33 +80,46 @@ fn send_payjoin(bitcoind: bitcoincore_rpc::Client, bip21: &str, danger_accept_in Some(options), None, ) - .expect("failed to create PSBT") + .context("Failed to create PSBT")? + .psbt; + let psbt = bitcoind + .wallet_process_psbt(&psbt, None, None, None) + .with_context(|| "Failed to process PSBT")? .psbt; - let psbt = bitcoind.wallet_process_psbt(&psbt, None, None, None).unwrap().psbt; - let psbt = load_psbt_from_base64(psbt.as_bytes()).unwrap(); + let psbt = load_psbt_from_base64(psbt.as_bytes()) + .with_context(|| "Failed to load PSBT from base64")?; log::debug!("Original psbt: {:#?}", psbt); let pj_params = payjoin::sender::Configuration::with_fee_contribution( payjoin::bitcoin::Amount::from_sat(10000), None, ); - let (req, ctx) = link.create_pj_request(psbt, pj_params).unwrap(); + let (req, ctx) = link + .create_pj_request(psbt, pj_params) + .with_context(|| "Failed to create payjoin request")?; let client = reqwest::blocking::Client::builder() .danger_accept_invalid_certs(danger_accept_invalid_certs) .build() - .unwrap(); + .with_context(|| "Failed to build reqwest http client")?; let response = client .post(req.url) .body(req.body) .header("Content-Type", "text/plain") .send() - .expect("failed to communicate"); - //.error_for_status() - //.unwrap(); - let psbt = ctx.process_response(response).unwrap(); + .with_context(|| "HTTP request failed")?; + // TODO display well-known errors and log::debug the rest + let psbt = ctx.process_response(response).with_context(|| "Failed to process response")?; log::debug!("Proposed psbt: {:#?}", psbt); - let psbt = bitcoind.wallet_process_psbt(&serialize_psbt(&psbt), None, None, None).unwrap().psbt; - let tx = bitcoind.finalize_psbt(&psbt, Some(true)).unwrap().hex.expect("incomplete psbt"); - bitcoind.send_raw_transaction(&tx).unwrap(); + let psbt = bitcoind + .wallet_process_psbt(&serialize_psbt(&psbt), None, None, None) + .with_context(|| "Failed to process PSBT")? + .psbt; + let tx = bitcoind + .finalize_psbt(&psbt, Some(true)) + .with_context(|| "Failed to finalize PSBT")? + .hex + .ok_or_else(|| anyhow!("Incomplete PSBT"))?; + bitcoind.send_raw_transaction(&tx).with_context(|| "Failed to send raw transaction")?; + Ok(()) } fn receive_payjoin(bitcoind: bitcoincore_rpc::Client, amount_arg: &str, endpoint_arg: &str) { From 593713898714bd3d37ab2a6c489dba72586e01e0 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 11:31:18 -0400 Subject: [PATCH 4/8] Return anyhow::Result for receiver cli setup --- payjoin-client/src/main.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/payjoin-client/src/main.rs b/payjoin-client/src/main.rs index 228aacf0..66adc6a2 100644 --- a/payjoin-client/src/main.rs +++ b/payjoin-client/src/main.rs @@ -122,22 +122,29 @@ fn send_payjoin( Ok(()) } -fn receive_payjoin(bitcoind: bitcoincore_rpc::Client, amount_arg: &str, endpoint_arg: &str) { +fn receive_payjoin( + bitcoind: bitcoincore_rpc::Client, + amount_arg: &str, + endpoint_arg: &str, +) -> Result<()> { use bitcoin::hashes::hex::ToHex; use bitcoin::OutPoint; use payjoin::Uri; use rouille::Response; - let pj_receiver_address = bitcoind.get_new_address(None, None).unwrap(); - let amount = Amount::from_sat(amount_arg.parse().unwrap()); + let pj_receiver_address = bitcoind.get_new_address(None, None)?; + let amount = Amount::from_sat(amount_arg.parse()?); let pj_uri_string = format!( "{}?amount={}&pj={}", pj_receiver_address.to_qr_uri(), amount.to_btc(), endpoint_arg ); - let pj_uri = Uri::from_str(&pj_uri_string).unwrap(); - let _pj_uri = pj_uri.check_pj_supported().expect("Bad Uri"); + let pj_uri = Uri::from_str(&pj_uri_string) + .map_err(|e| anyhow!("Constructed a bad URI string from args: {}", e))?; + let _pj_uri = pj_uri + .check_pj_supported() + .map_err(|e| anyhow!("Constructed URI does not support payjoin: {}", e))?; println!("Awaiting payjoin at BIP 21 Payjoin Uri:"); println!("{}", pj_uri_string); From 47844923a63a575d75c00b90fd5f39d2dd662702 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 13:47:15 -0400 Subject: [PATCH 5/8] Separate handle_payjoin_request function By cratingo a separate function we can wrap errors in an HTTP response --- payjoin-client/src/main.rs | 214 +++++++++++++++++++------------------ 1 file changed, 108 insertions(+), 106 deletions(-) diff --git a/payjoin-client/src/main.rs b/payjoin-client/src/main.rs index 66adc6a2..5a5ff86c 100644 --- a/payjoin-client/src/main.rs +++ b/payjoin-client/src/main.rs @@ -8,6 +8,7 @@ use bitcoincore_rpc::RpcApi; use clap::{arg, Arg, Command}; use payjoin::bitcoin::util::psbt::PartiallySignedTransaction as Psbt; use payjoin::{PjUriExt, UriExt}; +use rouille::{Request, Response}; fn main() -> Result<()> { env_logger::init(); @@ -40,7 +41,7 @@ fn main() -> Result<()> { sub_matches.get_one::("AMOUNT").context("Missing AMOUNT argument")?; let endpoint = sub_matches.get_one::("ENDPOINT").context("Missing ENDPOINT argument")?; - receive_payjoin(bitcoind, amount, endpoint); + receive_payjoin(bitcoind, amount, endpoint)?; } _ => unreachable!(), // If all subcommands are defined above, anything else is unreachabe!() } @@ -118,7 +119,9 @@ fn send_payjoin( .with_context(|| "Failed to finalize PSBT")? .hex .ok_or_else(|| anyhow!("Incomplete PSBT"))?; - bitcoind.send_raw_transaction(&tx).with_context(|| "Failed to send raw transaction")?; + let txid = + bitcoind.send_raw_transaction(&tx).with_context(|| "Failed to send raw transaction")?; + log::info!("Transaction sent: {}", txid); Ok(()) } @@ -127,10 +130,7 @@ fn receive_payjoin( amount_arg: &str, endpoint_arg: &str, ) -> Result<()> { - use bitcoin::hashes::hex::ToHex; - use bitcoin::OutPoint; use payjoin::Uri; - use rouille::Response; let pj_receiver_address = bitcoind.get_new_address(None, None)?; let amount = Amount::from_sat(amount_arg.parse()?); @@ -149,109 +149,111 @@ fn receive_payjoin( println!("Awaiting payjoin at BIP 21 Payjoin Uri:"); println!("{}", pj_uri_string); - rouille::start_server("0.0.0.0:3000", move |req| { - let headers = Headers(req.headers()); - let proposal = payjoin::receiver::UncheckedProposal::from_request( - req.data().unwrap(), - req.raw_query_string(), - headers, - ) + rouille::start_server("0.0.0.0:3000", move |req| handle_payjoin_request(&req, &bitcoind)); +} + +fn handle_payjoin_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> Response { + use bitcoin::hashes::hex::ToHex; + use bitcoin::OutPoint; + + let headers = Headers(req.headers()); + let proposal = payjoin::receiver::UncheckedProposal::from_request( + req.data().unwrap(), + req.raw_query_string(), + headers, + ) + .unwrap(); + + // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx + let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast(); + + // The network is used for checks later + let network = match bitcoind.get_blockchain_info().unwrap().chain.as_str() { + "main" => bitcoin::Network::Bitcoin, + "test" => bitcoin::Network::Testnet, + "regtest" => bitcoin::Network::Regtest, + _ => panic!("Unknown network"), + }; + + // Receive Check 1: Can Broadcast + let proposal = proposal + .check_can_broadcast(|tx| { + bitcoind + .test_mempool_accept(&[bitcoin::consensus::encode::serialize(&tx).to_hex()]) + .unwrap() + .first() + .unwrap() + .allowed + }) + .expect("Payjoin proposal should be broadcastable"); + log::trace!("check1"); + + // Receive Check 2: receiver can't sign for proposal inputs + let proposal = proposal + .check_inputs_not_owned(|input| { + let address = bitcoin::Address::from_script(&input, network).unwrap(); + bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() + }) + .expect("Receiver should not own any of the inputs"); + log::trace!("check2"); + // Receive Check 3: receiver can't sign for proposal inputs + let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + log::trace!("check3"); + + // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + let mut payjoin = proposal + .check_no_inputs_seen_before(|_| false) + .unwrap() + .identify_receiver_outputs(|output_script| { + let address = bitcoin::Address::from_script(&output_script, network).unwrap(); + bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() + }) + .expect("Receiver should have at least one output"); + log::trace!("check4"); + + // Select receiver payjoin inputs. + let available_inputs = bitcoind.list_unspent(None, None, None, None, None).unwrap(); + let candidate_inputs: HashMap = available_inputs + .iter() + .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) + .collect(); + + let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); + let selected_utxo = available_inputs + .iter() + .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) .unwrap(); + log::debug!("selected utxo: {:#?}", selected_utxo); + + // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, + let txo_to_contribute = bitcoin::TxOut { + value: selected_utxo.amount.to_sat(), + script_pubkey: selected_utxo.script_pub_key.clone(), + }; + let outpoint_to_contribute = + bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout }; + payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + + let receiver_substitute_address = bitcoind.get_new_address(None, None).unwrap(); + payjoin.substitute_output_address(receiver_substitute_address); + + let payjoin_proposal_psbt = payjoin.apply_fee(Some(1)).expect("failed to apply fees"); + + log::debug!("Extracted PSBT: {:#?}", payjoin_proposal_psbt); + // Sign payjoin psbt + let payjoin_base64_string = + base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); + // `wallet_process_psbt` adds available utxo data and finalizes + let payjoin_proposal_psbt = + bitcoind.wallet_process_psbt(&payjoin_base64_string, None, None, Some(false)).unwrap().psbt; + let payjoin_proposal_psbt = load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).unwrap(); + let payjoin_proposal_psbt = + payjoin.prepare_psbt(payjoin_proposal_psbt).expect("failed to prepare psbt"); + log::debug!("Receiver's PayJoin proposal PSBT Rsponse: {:#?}", payjoin_proposal_psbt); - // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx - let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast(); - - // The network is used for checks later - let network = match bitcoind.get_blockchain_info().unwrap().chain.as_str() { - "main" => bitcoin::Network::Bitcoin, - "test" => bitcoin::Network::Testnet, - "regtest" => bitcoin::Network::Regtest, - _ => panic!("Unknown network"), - }; - - // Receive Check 1: Can Broadcast - let proposal = proposal - .check_can_broadcast(|tx| { - bitcoind - .test_mempool_accept(&[bitcoin::consensus::encode::serialize(&tx).to_hex()]) - .unwrap() - .first() - .unwrap() - .allowed - }) - .expect("Payjoin proposal should be broadcastable"); - log::trace!("check1"); - - // Receive Check 2: receiver can't sign for proposal inputs - let proposal = proposal - .check_inputs_not_owned(|input| { - let address = bitcoin::Address::from_script(&input, network).unwrap(); - bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() - }) - .expect("Receiver should not own any of the inputs"); - log::trace!("check2"); - // Receive Check 3: receiver can't sign for proposal inputs - let proposal = proposal.check_no_mixed_input_scripts().unwrap(); - log::trace!("check3"); - - // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. - let mut payjoin = proposal - .check_no_inputs_seen_before(|_| false) - .unwrap() - .identify_receiver_outputs(|output_script| { - let address = bitcoin::Address::from_script(&output_script, network).unwrap(); - bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() - }) - .expect("Receiver should have at least one output"); - log::trace!("check4"); - - // Select receiver payjoin inputs. - let available_inputs = bitcoind.list_unspent(None, None, None, None, None).unwrap(); - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); - let selected_utxo = available_inputs - .iter() - .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) - .unwrap(); - log::debug!("selected utxo: {:#?}", selected_utxo); - - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - let txo_to_contribute = bitcoin::TxOut { - value: selected_utxo.amount.to_sat(), - script_pubkey: selected_utxo.script_pub_key.clone(), - }; - let outpoint_to_contribute = - bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout }; - payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); - - let receiver_substitute_address = bitcoind.get_new_address(None, None).unwrap(); - payjoin.substitute_output_address(receiver_substitute_address); - - let payjoin_proposal_psbt = payjoin.apply_fee(Some(1)).expect("failed to apply fees"); - - log::debug!("Extracted PSBT: {:#?}", payjoin_proposal_psbt); - // Sign payjoin psbt - let payjoin_base64_string = - base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); - // `wallet_process_psbt` adds available utxo data and finalizes - let payjoin_proposal_psbt = bitcoind - .wallet_process_psbt(&payjoin_base64_string, None, None, Some(false)) - .unwrap() - .psbt; - let payjoin_proposal_psbt = - load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).unwrap(); - let payjoin_proposal_psbt = - payjoin.prepare_psbt(payjoin_proposal_psbt).expect("failed to prepare psbt"); - log::debug!("Receiver's PayJoin proposal PSBT Rsponse: {:#?}", payjoin_proposal_psbt); - - let payload = base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); - log::info!("successful response"); - Response::text(payload) - }); + let payload = base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); + log::info!("successful response"); + Response::text(payload) } struct Headers<'a>(rouille::HeadersIter<'a>); From 3e1f79f98197a9d46eacfdd0256aa15c57186973 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 14:31:50 -0400 Subject: [PATCH 6/8] Display receiver RequestError --- payjoin/src/input_type.rs | 24 ++++++++++++++- payjoin/src/receiver/error.rs | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/payjoin/src/input_type.rs b/payjoin/src/input_type.rs index 89f80e5e..eb732554 100644 --- a/payjoin/src/input_type.rs +++ b/payjoin/src/input_type.rs @@ -1,5 +1,5 @@ use std::convert::{TryFrom, TryInto}; -use std::fmt; +use std::fmt::{self, Display}; use bitcoin::blockdata::script::{Instruction, Instructions, Script}; use bitcoin::blockdata::transaction::TxOut; @@ -84,6 +84,19 @@ impl InputType { } } +impl fmt::Display for InputType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InputType::P2Pk => write!(f, "P2PK"), + InputType::P2Pkh => write!(f, "P2PKH"), + InputType::P2Sh => write!(f, "P2SH"), + InputType::SegWitV0 { ty, nested } => + write!(f, "SegWitV0: type={}, nested={}", ty, nested), + InputType::Taproot => write!(f, "Taproot"), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum SegWitV0Type { Pubkey, @@ -111,6 +124,15 @@ impl TryFrom> for SegWitV0Type { } } +impl fmt::Display for SegWitV0Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SegWitV0Type::Pubkey => write!(f, "pubkey"), + SegWitV0Type::Script => write!(f, "script"), + } + } +} + #[derive(Debug)] pub(crate) enum InputTypeError { UnknownInputType, diff --git a/payjoin/src/receiver/error.rs b/payjoin/src/receiver/error.rs index 466c07b9..33408cef 100644 --- a/payjoin/src/receiver/error.rs +++ b/payjoin/src/receiver/error.rs @@ -1,3 +1,5 @@ +use std::fmt::{self, Display}; + /// Error that may occur when the request from sender is malformed. /// /// This is currently opaque type because we aren't sure which variants will stay. @@ -35,6 +37,61 @@ impl From for RequestError { fn from(value: InternalRequestError) -> Self { RequestError(value) } } +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn write_error(f: &mut fmt::Formatter, code: &str, message: impl Display) -> fmt::Result { + write!(f, r#"{{ "errorCode": "{}", "message": "{}" }}"#, code, message) + } + + match &self.0 { + InternalRequestError::Decode(e) => write_error(f, "decode-error", e), + InternalRequestError::MissingHeader(header) => + write_error(f, "missing-header", &format!("Missing header: {}", header)), + InternalRequestError::InvalidContentType(content_type) => write_error( + f, + "invalid-content-type", + &format!("Invalid content type: {}", content_type), + ), + InternalRequestError::InvalidContentLength(e) => + write_error(f, "invalid-content-length", e), + InternalRequestError::ContentLengthTooLarge(length) => write_error( + f, + "content-length-too-large", + &format!("Content length too large: {}.", length), + ), + InternalRequestError::SenderParams(e) => match e { + super::optional_parameters::Error::UnknownVersion => write_error( + f, + "version-unsupported", + "This version of payjoin is not supported.", + ), + _ => write_error(f, "sender-params-error", e), + }, + InternalRequestError::Psbt(e) => write_error(f, "original-psbt-rejected", e), + InternalRequestError::PrevTxOut(e) => + write_error(f, "original-psbt-rejected", &format!("PrevTxOut Error: {}", e)), + InternalRequestError::MissingPayment => + write_error(f, "original-psbt-rejected", "Missing payment."), + InternalRequestError::OriginalPsbtNotBroadcastable => write_error( + f, + "original-psbt-rejected", + "Can't broadcast. PSBT rejected by mempool.", + ), + InternalRequestError::InputOwned(_) => + write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."), + InternalRequestError::MixedInputScripts(type_a, type_b) => write_error( + f, + "original-psbt-rejected", + &format!("Mixed input scripts: {}; {}.", type_a, type_b), + ), + InternalRequestError::InputType(e) => + write_error(f, "original-psbt-rejected", &format!("Input Type Error: {}.", e)), + InternalRequestError::InputSeen(_) => + write_error(f, "original-psbt-rejected", "The receiver rejected the original PSBT."), + } + } +} + /// Error that may occur when coin selection fails. /// /// This is currently opaque type because we aren't sure which variants will stay. From 72e1deac7bce2fd50315f6bc68cbdb7f5b50290d Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 17:57:47 -0400 Subject: [PATCH 7/8] Respond with appropriate http response on error --- payjoin-client/src/main.rs | 105 ++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/payjoin-client/src/main.rs b/payjoin-client/src/main.rs index 5a5ff86c..23864288 100644 --- a/payjoin-client/src/main.rs +++ b/payjoin-client/src/main.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::convert::TryFrom; +use std::fmt; use std::str::FromStr; use anyhow::{anyhow, Context, Result}; @@ -149,55 +150,68 @@ fn receive_payjoin( println!("Awaiting payjoin at BIP 21 Payjoin Uri:"); println!("{}", pj_uri_string); - rouille::start_server("0.0.0.0:3000", move |req| handle_payjoin_request(&req, &bitcoind)); + rouille::start_server("0.0.0.0:3000", move |req| handle_web_request(&req, &bitcoind)); } -fn handle_payjoin_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> Response { +fn handle_web_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> Response { + handle_payjoin_request(req, bitcoind) + .map_err(|e| match e { + ReceiveError::RequestError(e) => { + log::error!("Error handling request: {}", e); + Response::text(e.to_string()).with_status_code(400) + } + e => { + log::error!("Error handling request: {}", e); + Response::text(e.to_string()).with_status_code(500) + } + }) + .unwrap_or_else(|err_resp| err_resp) +} + +fn handle_payjoin_request( + req: &Request, + bitcoind: &bitcoincore_rpc::Client, +) -> Result { use bitcoin::hashes::hex::ToHex; use bitcoin::OutPoint; let headers = Headers(req.headers()); let proposal = payjoin::receiver::UncheckedProposal::from_request( - req.data().unwrap(), + req.data().context("Failed to read request body")?, req.raw_query_string(), headers, - ) - .unwrap(); + )?; // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast(); // The network is used for checks later - let network = match bitcoind.get_blockchain_info().unwrap().chain.as_str() { + let network = match bitcoind.get_blockchain_info()?.chain.as_str() { "main" => bitcoin::Network::Bitcoin, "test" => bitcoin::Network::Testnet, "regtest" => bitcoin::Network::Regtest, - _ => panic!("Unknown network"), + _ => return Err(ReceiveError::Other(anyhow!("Unknown network"))), }; // Receive Check 1: Can Broadcast - let proposal = proposal - .check_can_broadcast(|tx| { - bitcoind - .test_mempool_accept(&[bitcoin::consensus::encode::serialize(&tx).to_hex()]) - .unwrap() - .first() - .unwrap() - .allowed - }) - .expect("Payjoin proposal should be broadcastable"); + let proposal = proposal.check_can_broadcast(|tx| { + bitcoind + .test_mempool_accept(&[bitcoin::consensus::encode::serialize(&tx).to_hex()]) + .unwrap() + .first() + .unwrap() + .allowed + })?; log::trace!("check1"); // Receive Check 2: receiver can't sign for proposal inputs - let proposal = proposal - .check_inputs_not_owned(|input| { - let address = bitcoin::Address::from_script(&input, network).unwrap(); - bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() - }) - .expect("Receiver should not own any of the inputs"); + let proposal = proposal.check_inputs_not_owned(|input| { + let address = bitcoin::Address::from_script(&input, network).unwrap(); + bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() + })?; log::trace!("check2"); // Receive Check 3: receiver can't sign for proposal inputs - let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + let proposal = proposal.check_no_mixed_input_scripts()?; log::trace!("check3"); // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. @@ -207,8 +221,7 @@ fn handle_payjoin_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> .identify_receiver_outputs(|output_script| { let address = bitcoin::Address::from_script(&output_script, network).unwrap(); bitcoind.get_address_info(&address).unwrap().is_mine.unwrap() - }) - .expect("Receiver should have at least one output"); + })?; log::trace!("check4"); // Select receiver payjoin inputs. @@ -234,10 +247,10 @@ fn handle_payjoin_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout }; payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); - let receiver_substitute_address = bitcoind.get_new_address(None, None).unwrap(); + let receiver_substitute_address = bitcoind.get_new_address(None, None)?; payjoin.substitute_output_address(receiver_substitute_address); - let payjoin_proposal_psbt = payjoin.apply_fee(Some(1)).expect("failed to apply fees"); + let payjoin_proposal_psbt = payjoin.apply_fee(Some(1))?; log::debug!("Extracted PSBT: {:#?}", payjoin_proposal_psbt); // Sign payjoin psbt @@ -245,15 +258,43 @@ fn handle_payjoin_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); // `wallet_process_psbt` adds available utxo data and finalizes let payjoin_proposal_psbt = - bitcoind.wallet_process_psbt(&payjoin_base64_string, None, None, Some(false)).unwrap().psbt; - let payjoin_proposal_psbt = load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).unwrap(); + bitcoind.wallet_process_psbt(&payjoin_base64_string, None, None, Some(false))?.psbt; let payjoin_proposal_psbt = - payjoin.prepare_psbt(payjoin_proposal_psbt).expect("failed to prepare psbt"); + load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).context("Failed to parse PSBT")?; + let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt)?; log::debug!("Receiver's PayJoin proposal PSBT Rsponse: {:#?}", payjoin_proposal_psbt); let payload = base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); log::info!("successful response"); - Response::text(payload) + Ok(Response::text(payload)) +} + +enum ReceiveError { + RequestError(payjoin::receiver::RequestError), + BitcoinRpc(bitcoincore_rpc::Error), + Other(anyhow::Error), +} + +impl fmt::Display for ReceiveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ReceiveError::RequestError(e) => write!(f, "RequestError: {}", e), + ReceiveError::BitcoinRpc(e) => write!(f, "BitcoinRpc: {}", e), + ReceiveError::Other(e) => write!(f, "Other: {}", e), + } + } +} + +impl From for ReceiveError { + fn from(e: payjoin::receiver::RequestError) -> Self { ReceiveError::RequestError(e) } +} + +impl From for ReceiveError { + fn from(e: bitcoincore_rpc::Error) -> Self { ReceiveError::BitcoinRpc(e) } +} + +impl From for ReceiveError { + fn from(e: anyhow::Error) -> Self { ReceiveError::Other(e) } } struct Headers<'a>(rouille::HeadersIter<'a>); From f37d154699970f4f4a79c3ca6b3ac90b8a093d04 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 3 Apr 2023 18:29:02 -0400 Subject: [PATCH 8/8] Continue if input contribution fails --- payjoin-client/src/main.rs | 57 +++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/payjoin-client/src/main.rs b/payjoin-client/src/main.rs index 23864288..a59704a6 100644 --- a/payjoin-client/src/main.rs +++ b/payjoin-client/src/main.rs @@ -8,6 +8,7 @@ use bitcoincore_rpc::bitcoin::Amount; use bitcoincore_rpc::RpcApi; use clap::{arg, Arg, Command}; use payjoin::bitcoin::util::psbt::PartiallySignedTransaction as Psbt; +use payjoin::receiver::PayjoinProposal; use payjoin::{PjUriExt, UriExt}; use rouille::{Request, Response}; @@ -173,7 +174,6 @@ fn handle_payjoin_request( bitcoind: &bitcoincore_rpc::Client, ) -> Result { use bitcoin::hashes::hex::ToHex; - use bitcoin::OutPoint; let headers = Headers(req.headers()); let proposal = payjoin::receiver::UncheckedProposal::from_request( @@ -225,27 +225,8 @@ fn handle_payjoin_request( log::trace!("check4"); // Select receiver payjoin inputs. - let available_inputs = bitcoind.list_unspent(None, None, None, None, None).unwrap(); - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) - .collect(); - - let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); - let selected_utxo = available_inputs - .iter() - .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) - .unwrap(); - log::debug!("selected utxo: {:#?}", selected_utxo); - - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - let txo_to_contribute = bitcoin::TxOut { - value: selected_utxo.amount.to_sat(), - script_pubkey: selected_utxo.script_pub_key.clone(), - }; - let outpoint_to_contribute = - bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout }; - payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + _ = try_contributing_inputs(&mut payjoin, bitcoind) + .map_err(|e| log::warn!("Failed to contribute inputs: {}", e)); let receiver_substitute_address = bitcoind.get_new_address(None, None)?; payjoin.substitute_output_address(receiver_substitute_address); @@ -269,6 +250,38 @@ fn handle_payjoin_request( Ok(Response::text(payload)) } +fn try_contributing_inputs( + payjoin: &mut PayjoinProposal, + bitcoind: &bitcoincore_rpc::Client, +) -> Result<()> { + use bitcoin::OutPoint; + + let available_inputs = bitcoind + .list_unspent(None, None, None, None, None) + .context("Failed to list unspent from bitcoind")?; + let candidate_inputs: HashMap = available_inputs + .iter() + .map(|i| (i.amount, OutPoint { txid: i.txid, vout: i.vout })) + .collect(); + + let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); + let selected_utxo = available_inputs + .iter() + .find(|i| i.txid == selected_outpoint.txid && i.vout == selected_outpoint.vout) + .context("This shouldn't happen. Failed to retrieve the privacy preserving utxo from those we provided to the seclector.")?; + log::debug!("selected utxo: {:#?}", selected_utxo); + + // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, + let txo_to_contribute = bitcoin::TxOut { + value: selected_utxo.amount.to_sat(), + script_pubkey: selected_utxo.script_pub_key.clone(), + }; + let outpoint_to_contribute = + bitcoin::OutPoint { txid: selected_utxo.txid, vout: selected_utxo.vout }; + payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + Ok(()) +} + enum ReceiveError { RequestError(payjoin::receiver::RequestError), BitcoinRpc(bitcoincore_rpc::Error),