From aa08e528d33728782303219fa9f9c9f48272a3b1 Mon Sep 17 00:00:00 2001 From: DanGould Date: Sun, 21 May 2023 16:59:21 -0400 Subject: [PATCH 1/5] Move payjoin-client payjoin-cli --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f437e8b..630a5ea9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ It doesn't care whether you use `async`, blocking, `tokio`, `sync-std` `hyper`, There are already too many frameworks in Rust so it's best avoiding directly introducing them into library code. The library currently only contains sender implementation and a partial receiver. -The payjoin-client binary performs no-frills PayJoin using Bitcoin Core wallet. +The payjoin-cli binary performs no-frills PayJoin using Bitcoin Core wallet. The payjoin crate also supports other wallet software [like LND](https://github.com/chaincase-app/nolooking). ### Disclaimer ⚠️ WIP From 82fdc35eb9fd271c0f4f15de4d051d562739be38 Mon Sep 17 00:00:00 2001 From: DanGould Date: Sun, 21 May 2023 17:10:42 -0400 Subject: [PATCH 2/5] Move PayJoin Payjoin --- README.md | 8 ++++---- payjoin-cli/src/app.rs | 2 +- payjoin/CHANGELOG.md | 3 ++- payjoin/Cargo.toml | 2 +- payjoin/src/lib.rs | 6 +++--- payjoin/src/receive/mod.rs | 8 ++++---- payjoin/src/send/mod.rs | 6 +++--- payjoin/src/uri.rs | 32 ++++++++++++++++---------------- payjoin/tests/integration.rs | 4 ++-- 9 files changed, 36 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 630a5ea9..a35d2967 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# PayJoin implementation in Rust +# Payjoin implementation in Rust ## About -This is a library and a client binary for bitcoind implementing BIP78 PayJoin. +This is a library and a client binary for bitcoind implementing BIP78 Payjoin. The library is perfectly IO-agnostic—in fact, it does no IO. The primary goal of such design is to be easy to unit test. -While not there yet, it already has infinitely more tests than the [PayJoin PR against Electrum](https://github.com/spesmilo/electrum/pull/6804). :P +While not there yet, it already has infinitely more tests than the [Payjoin PR against Electrum](https://github.com/spesmilo/electrum/pull/6804). :P It doesn't care whether you use `async`, blocking, `tokio`, `sync-std` `hyper`, `actix` or whatever. There are already too many frameworks in Rust so it's best avoiding directly introducing them into library code. The library currently only contains sender implementation and a partial receiver. -The payjoin-cli binary performs no-frills PayJoin using Bitcoin Core wallet. +The payjoin-cli binary performs no-frills Payjoin using Bitcoin Core wallet. The payjoin crate also supports other wallet software [like LND](https://github.com/chaincase-app/nolooking). ### Disclaimer ⚠️ WIP diff --git a/payjoin-cli/src/app.rs b/payjoin-cli/src/app.rs index 73fd2dcd..f9a05612 100644 --- a/payjoin-cli/src/app.rs +++ b/payjoin-cli/src/app.rs @@ -303,7 +303,7 @@ impl App { .context("Failed to parse PSBT") .map_err(|e| Error::Server(e.into()))?; let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt)?; - log::debug!("Receiver's PayJoin proposal PSBT Rsponse: {:#?}", 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"); diff --git a/payjoin/CHANGELOG.md b/payjoin/CHANGELOG.md index 8a27e638..3b72cc66 100644 --- a/payjoin/CHANGELOG.md +++ b/payjoin/CHANGELOG.md @@ -1,9 +1,10 @@ -# PayJoin Changelog +# Payjoin Changelog ## 0.8.0 - Test receiver compatibility with BlueWallet - Rename `sender`, `receiver` features `send`, `receive` +- Rename `PayJoin` `Payjoin` - introduce `receive::Error` for fallable checklist items [#59](https://github.com/payjoin/rust-payjoin/pull/59) - Display receiver errors, RequestErrors with JSON (https://github.com/payjoin/rust-payjoin/pull/49) diff --git a/payjoin/Cargo.toml b/payjoin/Cargo.toml index f6b24245..924f3975 100644 --- a/payjoin/Cargo.toml +++ b/payjoin/Cargo.toml @@ -2,7 +2,7 @@ name = "payjoin" version = "0.8.0" authors = ["Dan Gould "] -description = "PayJoin Library for the BIP78 Pay to Endpoint protocol." +description = "Payjoin Library for the BIP78 Pay to Endpoint protocol." homepage = "https://github.com/chaincase-app/payjoin" repository = "https://github.com/chaincase-app/payjoin" readme = "../README.md" diff --git a/payjoin/src/lib.rs b/payjoin/src/lib.rs index c06727f6..155b24b8 100644 --- a/payjoin/src/lib.rs +++ b/payjoin/src/lib.rs @@ -1,14 +1,14 @@ -//! # PayJoin implementation in Rust +//! # Payjoin implementation in Rust //! //! **Important: this crate is WIP!** //! //! While I don't think there's a huge risk running it, don't rely on its security for now! //! Please at least review the code that verifies there's no overpayment and let me know you did. //! -//! This is a library and an example binary implementing BIP78 PayJoin. +//! This is a library and an example binary implementing BIP78 Payjoin. //! The library is perfectly IO-agnostic - in fact, it does no IO. //! The primary goal of such design is to make it easy to unit test. -//! While we're not there yet, it already has infinitely more tests than the PayJoin PR against Electrum. :P +//! While we're not there yet, it already has infinitely more tests than the Payjoin PR against Electrum. :P //! //! Additional advantage is it doesn't care whether you use `async`, blocking, `tokio`, `sync-std` `hyper`, `actix` or whatever. //! There are already too many frameworks in Rust so it's best avoiding directly introducing them into library code. diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index f794a819..143a106f 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -125,7 +125,7 @@ impl UncheckedProposal { /// can be broadcast, i.e. `testmempoolaccept` bitcoind rpc returns { "allowed": true,.. } /// for `get_transaction_to_check_broadcast()` before calling this method. /// - /// Do this check if you generate bitcoin uri to receive PayJoin on sender request without manual human approval, like a payment processor. + /// Do this check if you generate bitcoin uri to receive Payjoin on sender request without manual human approval, like a payment processor. /// Such so called "non-interactive" receivers are otherwise vulnerable to probing attacks. /// If a sender can make requests at will, they can learn which bitcoin the receiver owns at no cost. /// Broadcasting the Original PSBT after some time in the failure case makes incurs sender cost and prevents probing. @@ -142,7 +142,7 @@ impl UncheckedProposal { } } - /// Call this method if the only way to initiate a PayJoin with this receiver + /// Call this method if the only way to initiate a Payjoin with this receiver /// requires manual intervention, as in most consumer wallets. /// /// So-called "non-interactive" receivers, like payment processors, that allow arbitrary requests are otherwise vulnerable to probing attacks. @@ -233,8 +233,8 @@ impl MaybeMixedInputScripts { impl MaybeInputsSeen { /// Make sure that the original transaction inputs have never been seen before. - /// This prevents probing attacks. This prevents reentrant PayJoin, where a sender - /// proposes a PayJoin PSBT as a new Original PSBT for a new PayJoin. + /// This prevents probing attacks. This prevents reentrant Payjoin, where a sender + /// proposes a Payjoin PSBT as a new Original PSBT for a new Payjoin. pub fn check_no_inputs_seen_before( self, is_known: impl Fn(&OutPoint) -> Result, diff --git a/payjoin/src/send/mod.rs b/payjoin/src/send/mod.rs index 04d8bbaa..385880b8 100644 --- a/payjoin/src/send/mod.rs +++ b/payjoin/src/send/mod.rs @@ -1,4 +1,4 @@ -//! Send a PayJoin +//! Send a Payjoin //! //! This module contains types and methods used to implement sending via BIP78. //! Usage is pretty simple: @@ -35,7 +35,7 @@ type InternalResult = Result; /// Builder for sender-side payjoin parameters /// -/// These parameters define how client wants to handle PayJoin. +/// These parameters define how client wants to handle Payjoin. pub struct Configuration { disable_output_substitution: bool, fee_contribution: Option<(bitcoin::Amount, Option)>, @@ -63,7 +63,7 @@ impl Configuration { } } - /// Perform PayJoin without incentivizing the payee to cooperate. + /// Perform Payjoin without incentivizing the payee to cooperate. /// /// While it's generally better to offer some contribution some users may wish not to. /// This function disables contribution. diff --git a/payjoin/src/uri.rs b/payjoin/src/uri.rs index 43cf6e83..a1efa1e4 100644 --- a/payjoin/src/uri.rs +++ b/payjoin/src/uri.rs @@ -7,28 +7,28 @@ use url::Url; use crate::send; #[derive(Debug, Clone)] -pub enum PayJoin { - Supported(PayJoinParams), +pub enum Payjoin { + Supported(PayjoinParams), Unsupported, } -impl PayJoin { +impl Payjoin { pub fn pj_is_supported(&self) -> bool { match self { - PayJoin::Supported(_) => true, - PayJoin::Unsupported => false, + Payjoin::Supported(_) => true, + Payjoin::Unsupported => false, } } } #[derive(Debug, Clone)] -pub struct PayJoinParams { +pub struct PayjoinParams { pub(crate) endpoint: Url, pub(crate) disable_output_substitution: bool, } -pub type Uri<'a> = bip21::Uri<'a, PayJoin>; -pub type PjUri<'a> = bip21::Uri<'a, PayJoinParams>; +pub type Uri<'a> = bip21::Uri<'a, Payjoin>; +pub type PjUri<'a> = bip21::Uri<'a, PayjoinParams>; mod sealed { pub trait UriExt: Sized {} @@ -68,7 +68,7 @@ impl<'a> PjUriExt for PjUri<'a> { impl<'a> UriExt<'a> for Uri<'a> { fn check_pj_supported(self) -> Result, bip21::Uri<'a>> { match self.extras { - PayJoin::Supported(payjoin) => { + Payjoin::Supported(payjoin) => { let mut uri = bip21::Uri::with_extras(self.address, payjoin); uri.amount = self.amount; uri.label = self.label; @@ -76,7 +76,7 @@ impl<'a> UriExt<'a> for Uri<'a> { Ok(uri) } - PayJoin::Unsupported => { + Payjoin::Unsupported => { let mut uri = bip21::Uri::new(self.address); uri.amount = self.amount; uri.label = self.label; @@ -88,15 +88,15 @@ impl<'a> UriExt<'a> for Uri<'a> { } } -impl PayJoinParams { +impl PayjoinParams { pub fn is_output_substitution_disabled(&self) -> bool { self.disable_output_substitution } } -impl bip21::de::DeserializationError for PayJoin { +impl bip21::de::DeserializationError for Payjoin { type Error = PjParseError; } -impl<'a> bip21::de::DeserializeParams<'a> for PayJoin { +impl<'a> bip21::de::DeserializeParams<'a> for Payjoin { type DeserializationState = DeserializationState; } @@ -114,7 +114,7 @@ impl From for PjParseError { } impl<'a> bip21::de::DeserializationState<'a> for DeserializationState { - type Value = PayJoin; + type Value = Payjoin; fn is_param_known(&self, param: &str) -> bool { matches!(param, "pj" | "pjos") } @@ -152,14 +152,14 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState { self, ) -> std::result::Result::Error> { match (self.pj, self.pjos) { - (None, None) => Ok(PayJoin::Unsupported), + (None, None) => Ok(Payjoin::Unsupported), (None, Some(_)) => Err(PjParseError(InternalPjParseError::MissingEndpoint)), (Some(endpoint), pjos) => { if endpoint.scheme() == "https" || endpoint.scheme() == "http" && endpoint.domain().unwrap_or_default().ends_with(".onion") { - Ok(PayJoin::Supported(PayJoinParams { + Ok(Payjoin::Supported(PayjoinParams { endpoint, disable_output_substitution: pjos.unwrap_or(false), })) diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index 3cf70de0..258b6386 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -99,7 +99,7 @@ mod integration { sender.wallet_process_psbt(&payjoin_base64_string, None, None, None).unwrap().psbt; let payjoin_psbt = sender.finalize_psbt(&payjoin_psbt, Some(false)).unwrap().psbt.unwrap(); let payjoin_psbt = load_psbt_from_base64(payjoin_psbt.as_bytes()).unwrap(); - debug!("Sender's PayJoin PSBT: {:#?}", payjoin_psbt); + debug!("Sender's Payjoin PSBT: {:#?}", payjoin_psbt); let payjoin_tx = payjoin_psbt.extract_tx(); bitcoind.client.send_raw_transaction(&payjoin_tx).unwrap().first().unwrap(); @@ -211,7 +211,7 @@ mod integration { load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).unwrap(); let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt).unwrap(); - debug!("Receiver's PayJoin proposal PSBT: {:#?}", payjoin_proposal_psbt); + debug!("Receiver's Payjoin proposal PSBT: {:#?}", payjoin_proposal_psbt); base64::encode(consensus::serialize(&payjoin_proposal_psbt)) } From af73a5d16a14ff30118fbed2745abab64a1ecbf7 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 22 May 2023 22:01:15 -0400 Subject: [PATCH 3/5] Document send feature walkthrough --- payjoin/src/send/mod.rs | 145 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 9 deletions(-) diff --git a/payjoin/src/send/mod.rs b/payjoin/src/send/mod.rs index 385880b8..89ca9bc4 100644 --- a/payjoin/src/send/mod.rs +++ b/payjoin/src/send/mod.rs @@ -1,18 +1,145 @@ //! Send a Payjoin //! -//! This module contains types and methods used to implement sending via BIP78. +//! This module contains types and methods used to implement sending via [BIP 78 Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki). //! Usage is pretty simple: //! //! 1. Parse BIP21 as [`payjoin::Uri`](crate::Uri) -//! 2. Create a finalized PSBT paying `.amount` to `.address` -//! 3. Spawn a thread or async task that will broadcast the transaction after one minute unless -//! canceled -//! 4. Call [`PjUriExt::create_pj_request()`](crate::PjUriExt::create_pj_request()) with the PSBT and your parameters +//! 2. Construct URI request parameters, a finalized “Original PSBT” paying .amount to .address +//! 3. (optional) Spawn a thread or async task that will broadcast the original PSBT fallback after delay (e.g. 1 minute) unless canceled +//! 4. Construct the request [`PjUriExt::create_pj_request()`](crate::PjUriExt::create_pj_request()) with the PSBT and your parameters //! 5. Send the request and receive response -//! 6. Feed the response to [`Context::process_response()`](crate::send::Context::process_response()) -//! 7. Sign resulting PSBT -//! 8. Cancel the one-minute deadline and broadcast the resulting PSBT +//! 6. Process the response with [`Context::process_response()`](crate::send::Context::process_response()) +//! 7. Sign and finalize the Payjoin Proposal PSBT +//! 8. Broadcast the Payjoin Transaction (and cancel the optional fallback broadcast) //! +//! This crate is runtime-agnostic. Data persistence, chain interactions, and networking may be provided by custom implementations or copy the reference [`payjoin-cli`](https://github.com/payjoin/rust-payjoin/tree/master/payjoin-cli) for bitcoind, [`nolooking`](https://github.com/chaincase-app/nolooking) for LND, or [`bitmask-core`](https://github.com/diba-io/bitmask-core) BDK integration. Bring your own wallet and http client. +//! +//! ## Send a Payjoin +//! +//! The `sender` feature provides the check methods and PSBT data manipulation necessary to send payjoins. Just connect your wallet and an HTTP client. The reference implementation uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) and Bitcoin Core RPC. +//! +//! ### 1. Parse BIP21 as [`payjoin::Uri`](crate::Uri) +//! +//! Start by parsing a valid BIP 21 uri having the `pj` parameter. This is the [`bip21`](https://crates.io/crates/bip21) crate under the hood. +//! +//! ``` +//! let link = payjoin::Uri::try_from(bip21) +//! .map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?; +//! +//! let link = link +//! .check_pj_supported() +//! .map_err(|e| anyhow!("The provided URI doesn't support payjoin (BIP78): {}", e))?; +//! ``` +//! +//! ### 2. Construct URI request parameters, a finalized "Original PSBT" paying `.amount` to `.address` +//! +//! ``` +//! let mut outputs = HashMap::with_capacity(1); +//! outputs.insert(link.address.to_string(), amount); +//! +//! let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { +//! lock_unspent: Some(true), +//! fee_rate: Some(Amount::from_sat(2000)), // SPECIFY YOUR USER'S FEE RATE +//! ..Default::default() +//! }; +//! // in payjoin-cli, bitcoind is set up as a client from the config file +//! let psbt = bitcoind +//! .wallet_create_funded_psbt( +//! &[], // inputs +//! &outputs, +//! None, // locktime +//! Some(options), +//! None, +//! ) +//! .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 = load_psbt_from_base64(psbt.as_bytes()) // SHOULD BE PROVIDED BY CRATE AS HELPER USING rust-bitcoin base64 feature +//! .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, +//! ); +//! ``` +//! +//! ### 3. (optional) Spawn a thread or async task that will broadcast the transaction after delay (e.g. 1 minute) unless canceled +//! +//! This was written in the original docs, but it should be clarified: In case the payjoin goes through but you still want to pay by default. This is missing from the `payjoin-cli`. +//! +//! Writing this, I think of [Signal's contributing guidelines](https://github.com/signalapp/Signal-iOS/blob/main/CONTRIBUTING.md#development-ideology): +//! > "The answer is not more options. If you feel compelled to add a preference that's exposed to the user, it's very possible you've made a wrong turn somewhere." +//! +//! ### 4. Construct the request with the PSBT and parameters +//! +//! ``` +//! let (req, ctx) = link +//! .create_pj_request(psbt, pj_params) +//! .with_context(|| "Failed to create payjoin request")?; +//! ``` +//! +//! ### 5. Send the request and receive response +//! +//! Senders request a payjoin from the receiver with a payload containing the Original PSBT and optional parameters. They require a secure endpoint for authentication and message secrecy to prevent that transaction from being modified by a malicious third party during transit or being snooped on. Only https and .onion endpoints are spec-compatible payjoin endpoints. +//! +//! Avoiding the secure endpoint requirement is convenient for testing. +//! +//! ``` +//! let client = reqwest::blocking::Client::builder() +//! .danger_accept_invalid_certs(danger_accept_invalid_certs) +//! .build() +//! .with_context(|| "Failed to build reqwest http client")?; +//! let response = client +//! .post(req.url) +//! .body(req.body) +//! .header("Content-Type", "text/plain") +//! .send() +//! .with_context(|| "HTTP request failed")?; +//! ``` +//! +//! ### 6. Process the response +//! +//! An `Ok` response should include a Payjoin Proposal PSBT. Check that it's signed, following protocol, not trying to steal or otherwise error. +//! +//! ``` +//! // TODO display well-known errors and log::debug the rest +//! // ctx is the context returned from create_pj_request in step 4. +//! let psbt = ctx.process_response(response).with_context(|| "Failed to process response")?; +//! log::debug!("Proposed psbt: {:#?}", psbt); +//! ``` +//! +//! Payjoin response errors (called [receiver's errors in spec](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#receivers-well-known-errors)) come from a remote server and can be used "maliciously to phish a non technical user." Only those "well-known" errors according to spec should be displayed with preset messages to prevent phishing. +//! +//! ### 7. Sign and finalize the Payjoin Proposal PSBT +//! +//! Most software can handle adding the last signatures to a PSBT without issue. +//! +//! ``` +//! 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"))?; +//! ``` +//! +//! ### 8. Broadcast the Payjoin Transaction +//! +//! In order to preserve privacy between the transaction and the IP address from which it originates, transaction broadcasting should be done using Tor, a VPN, or proxy. +//! +//! ``` +//! let txid = +//! bitcoind.send_raw_transaction(&tx).with_context(|| "Failed to send raw transaction")?; +//! log::info!("Transaction sent: {}", txid); +//! ``` +//! +//! 📤 Sending payjoin is just that simple. use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; use bitcoin::{Script, Sequence, TxOut}; @@ -49,7 +176,7 @@ impl Configuration { /// These parameters will allow the receiver to take `max_fee_contribution` from given change /// output to pay for additional inputs. The recommended fee is `size_of_one_input * fee_rate`. /// - /// `change_index` specifies which output can be used to pay fee. I `None` is provided, then + /// `change_index` specifies which output can be used to pay fee. If `None` is provided, then /// the output is auto-detected unless the supplied transaction has more than two outputs. pub fn with_fee_contribution( max_fee_contribution: bitcoin::Amount, From 64b1d3bef390c3639a6ed571929ec71d26c391b0 Mon Sep 17 00:00:00 2001 From: DanGould Date: Sun, 21 May 2023 18:07:13 -0400 Subject: [PATCH 4/5] Address clippy lints --- payjoin/src/lib.rs | 2 ++ payjoin/src/send/mod.rs | 2 +- payjoin/src/uri.rs | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/payjoin/src/lib.rs b/payjoin/src/lib.rs index 155b24b8..dedca885 100644 --- a/payjoin/src/lib.rs +++ b/payjoin/src/lib.rs @@ -21,7 +21,9 @@ pub extern crate bitcoin; #[cfg(feature = "receive")] pub mod receive; +#[cfg(feature = "receive")] pub use crate::receive::Error; + #[cfg(feature = "send")] pub mod send; diff --git a/payjoin/src/send/mod.rs b/payjoin/src/send/mod.rs index 89ca9bc4..3e38ab64 100644 --- a/payjoin/src/send/mod.rs +++ b/payjoin/src/send/mod.rs @@ -704,7 +704,7 @@ pub(crate) fn from_psbt_and_uri( let txout = zeroth_input.previous_txout().expect("We already checked this above"); let input_type = InputType::from_spent_input(txout, zeroth_input.psbtin).unwrap(); let url = serialize_url( - uri.extras.endpoint.into(), + uri.extras._endpoint.into(), disable_output_substitution, fee_contribution, params.min_fee_rate, diff --git a/payjoin/src/uri.rs b/payjoin/src/uri.rs index a1efa1e4..26a6100d 100644 --- a/payjoin/src/uri.rs +++ b/payjoin/src/uri.rs @@ -23,7 +23,7 @@ impl Payjoin { #[derive(Debug, Clone)] pub struct PayjoinParams { - pub(crate) endpoint: Url, + pub(crate) _endpoint: Url, pub(crate) disable_output_substitution: bool, } @@ -160,7 +160,7 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState { && endpoint.domain().unwrap_or_default().ends_with(".onion") { Ok(Payjoin::Supported(PayjoinParams { - endpoint, + _endpoint: endpoint, disable_output_substitution: pjos.unwrap_or(false), })) } else { From 580d4afa97dc868a34ac11ce67f9d8050f26e1e7 Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 22 May 2023 22:01:27 -0400 Subject: [PATCH 5/5] Document receive feature walkthrough --- payjoin/src/receive/mod.rs | 258 ++++++++++++++++++++++++++++++++++++- 1 file changed, 256 insertions(+), 2 deletions(-) diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index 143a106f..d89bf592 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -3,14 +3,268 @@ //! This module contains types and methods used to receive payjoin via BIP78. //! Usage is pretty simple: //! -//! 1. Generate a pj_uri BIP21 using [`payjoin::Uri`](crate::Uri)::from_str -//! 2. Listen for an original PSBT on the endpoint specified in the URI +//! 1. Generate a pj_uri [BIP 21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) using [`payjoin::Uri`](crate::Uri)::from_str +//! 2. Listen for a sender's request on the `pj` endpoint //! 3. Parse the request using [`UncheckedProposal::from_request()`](crate::receive::UncheckedProposal::from_request()) //! 4. Validate the proposal using the `check` methods to guide you. //! 5. Assuming the proposal is valid, augment it into a payjoin with the available `try_preserving_privacy` and `contribute` methods //! 6. Extract the payjoin PSBT and sign it //! 7. Respond to the sender's http request with the signed PSBT as payload. //! +//! ## Receive a Payjoin +//! +//! The `receive` feature provides all of the check methods, PSBT data manipulation, coin selection, and transport structures to receive payjoin and handle errors in a privacy preserving way. +//! +//! Receiving payjoin entails listening to a secure http endpoint for inbound requests. The endpoint is displayed in the `pj` parameter of a [bip 21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) request URI. +//! +//! The [reference implementation annotated below](https://github.com/payjoin/rust-payjoin/tree/master/payjoin-cli) uses `rouille` sync http server and Bitcoin Core RPC. +//! +//! ``` +//! fn receive_payjoin( +//! bitcoind: bitcoincore_rpc::Client, +//! amount_arg: &str, +//! endpoint_arg: &str, +//! ) -> Result<()> +//! ``` +//! +//! ### 1. Generate a pj_uri BIP21 using `payjoin::Uri::from_str` +//! +//! A BIP 21 URI supporting payjoin contains at minimum a bitcoin address and a secure `pj` endpoint. +//! +//! ``` +//! 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) +//! .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))?; +//! ``` +//! +//! ### 2. Listen for a sender's request on the `pj` endpoint +//! +//! Start a webserver of your choice to respond to payjoin protocol POST messages. The reference `payjoin-cli` implementation uses `rouille` sync http server. +//! +//! ``` +//! rouille::start_server(self.config.pj_host.clone(), move |req| self.handle_web_request(req)); +//! // ... +//! fn handle_web_request(&self, req: &Request) -> Response { +//! log::debug!("Received request: {:?}", req); +//! match (req.method(), req.url().as_ref()) { +//! // ... +//! ("POST", _) => self +//! .handle_payjoin_post(req) +//! .map_err(|e| match e { +//! Error::BadRequest(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), +//! _ => Response::empty_404(), +//! } +//! } +//! ``` +//! +//! ### 3. Parse an incoming request using [`UncheckedProposal::from_request()`](crate::receive::UncheckedProposal::from_request()) +//! +//! Parse the incoming HTTP request and check that it follows protocol. +//! +//! ``` +//! let headers = Headers(req.headers()); +//! let proposal = payjoin::receive::UncheckedProposal::from_request( +//! req.data().context("Failed to read request body")?, +//! req.raw_query_string(), +//! headers, +//! )?; +//! ``` +//! +//! Headers are parsed using the [`payjoin::receiver::Headers`] Trait so that the library can iterate through them, ideally without cloning. +//! +//! ``` +//! struct Headers<'a>(rouille::HeadersIter<'a>); +//! impl payjoin::receive::Headers for Headers<'_> { +//! fn get_header(&self, key: &str) -> Option<&str> { +//! let mut copy = self.0.clone(); //! lol +//! copy.find(|(k, _)| k.eq_ignore_ascii_case(key)).map(|(_, v)| v) +//! } +//! } +//! ``` +//! +//! ### 4. Validate the proposal using the `check` methods +//! +//! Check the sender's Original PSBT to prevent it from stealing, probing to fingerprint its wallet, and to avoid privacy gotchas. +//! +//! ``` +//! // 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()?.chain.as_str() { +//! "main" => bitcoin::Network::Bitcoin, +//! "test" => bitcoin::Network::Testnet, +//! "regtest" => bitcoin::Network::Regtest, +//! _ => return Err(ReceiveError::Other(anyhow!("Unknown network"))), +//! }; +//! ``` +//! +//! #### Check 1: Can the Original PSBT be Broadcast? +//! +//! We need to know this transaction is consensus-valid. +//! +//! ``` +//! let checked_1 = proposal.check_can_broadcast(|tx| { +//! let raw_tx = bitcoin::consensus::encode::serialize(&tx).to_hex(); +//! let mempool_results = self +//! .bitcoind +//! .test_mempool_accept(&[raw_tx]) +//! .map_err(|e| Error::Server(e.into()))?; +//! match mempool_results.first() { +//! Some(result) => Ok(result.allowed), +//! None => Err(Error::Server( +//! anyhow!("No mempool results returned on broadcast check").into(), +//! )), +//! } +//! })?; +//! ``` +//! +//! If writing a payment processor, schedule that this transaction is broadcast as fallback if the payjoin fails after a timeout. BTCPay broadcasts fallback after two minutes. +//! +//! #### Check 2: Is the sender trying to make us sign our own inputs? +//! +//! ``` +//! let checked_2 = checked_1.check_inputs_not_owned(|input| { +//! if let Ok(address) = bitcoin::Address::from_script(input, network) { +//! self.bitcoind +//! .get_address_info(&address) +//! .map(|info| info.is_mine.unwrap_or(false)) +//! .map_err(|e| Error::Server(e.into())) +//! } else { +//! Ok(false) +//! } +//! })?; +//! ``` +//! +//! #### Check 3: Are there mixed input scripts, breaking stenographic privacy? +//! +//! ``` +//! let checked_3 = checked_2.check_no_mixed_input_scripts()?; +//! ``` +//! +//! #### Check 4: Have we seen this input before? +//! +//! Non-interactive i.e. payment processors should be careful to keep track of request inputs or else a malicious sender may try and probe multiple responses containing the receiver utxos, clustering their wallet. +//! +//! ``` +//! let mut checked_4 = checked_3.check_no_inputs_seen_before(|input| { +//! Ok(!self.insert_input_seen_before(*input).map_err(|e| Error::Server(e.into()))?) +//! })?; +//! ``` +//! +//! ### 5. Augment a valid proposal to preserve privacy +//! +//! Here's where the PSBT is modified. Inputs may be added to break common input ownership heurstic. There are a number of ways to select coins and break common input heuristic but fail to preserve privacy because of Unnecessary Input Heuristic (UIH). Until February 2023, even BTCPay occasionally made these errors. Privacy preserving coin selection as implemented in `try_preserving_privacy` is precarious to implement yourself may be the most sensitive and valuable part of this kit. +//! +//! Output substitution is another way to improve privacy and increase functionality. For example, if the Original PSBT output address paying the receiver is coming from a static URI, a new address may be generated on the fly to avoid address reuse. This can even be done from a watch-only wallet. Output substitution may also be used to consolidate incoming funds to a remote cold wallet, break an output into smaller UTXOs to fulfill exchange orders, open lightning channels, and more. +//! +//! ``` +//! // Distinguish our outputs to augment with input amount +//! let mut payjoin = checked_4.identify_receiver_outputs(|output_script| { +//! if let Ok(address) = bitcoin::Address::from_script(output_script, network) { +//! self.bitcoind +//! .get_address_info(&address) +//! .map(|info| info.is_mine.unwrap_or(false)) +//! .map_err(|e| Error::Server(e.into())) +//! } else { +//! Ok(false) +//! } +//! })?; +//! // Select receiver payjoin inputs. +//! _ = 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); +//! +//! // ... +//! +//! 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(()) +//! } +//! ``` +//! +//! Using methods for coin selection not provided by this library may have dire implications for privacy. Significant in-depth research and careful implementation iteration has gone into privacy preserving transaction construction. [Here's a good starting point from the JoinMarket repo to being a deep dive of your own](https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2796539). +//! +//! ### 6. Extract the payjoin PSBT and sign it +//! +//! Fees are applied to the augmented Payjoin Proposal PSBT using calculation factoring both receiver's preferred feerate and the sender's fee-related [optional parameters](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#optional-parameters). The current `apply_fee` method is primitive, disregarding PSBT fee estimation and only adding fees coming from the sender's budget. When more accurate tools are available to calculate a PSBT's fee-dependent weight (solved, more complicated than it sounds, but unimplemented in rust-bitcoin), this `apply_fee` should be improved. +//! +//! ``` +//! let min_feerate_sat_per_vb = 1; +//! let payjoin_proposal_psbt = payjoin.apply_fee(Some(min_feerate_sat_per_vb))?; +//! +//! 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, sign: None, sighash_type: None, bip32derivs: Some(false))?.psbt; +//! let payjoin_proposal_psbt = +//! load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).context("Failed to parse PSBT")?; +//! ``` +//! +//! ### 7. Respond to the sender's http request with the signed PSBT as payload +//! +//! BIP 78 senders require specific PSBT validation constraints regulated by prepare_psbt. PSBTv0 was not designed to support input/output modification, so the protocol requires this precise preparation step. A future PSBTv2 payjoin protocol may not. +//! +//! It is critical to pay special care when returning error response messages. Responding with internal errors can make a receiver vulnerable to sender probing attacks which cluster UTXOs. +//! +//! ``` +//! let payjoin_proposal_psbt = payjoin.prepare_psbt(payjoin_proposal_psbt)?; +//! let payload = base64::encode(bitcoin::consensus::serialize(&payjoin_proposal_psbt)); +//! Ok(Response::text(payload)) +//! ``` + +//! 📥 That's how one receives a payjoin. use std::cmp::{max, min}; use std::collections::{BTreeMap, HashMap};