Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotate the reference implementation for Audit #52

Merged
merged 5 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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-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
Expand Down
2 changes: 1 addition & 1 deletion payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion payjoin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
2 changes: 1 addition & 1 deletion payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "payjoin"
version = "0.8.0"
authors = ["Dan Gould <[email protected]>"]
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"
Expand Down
8 changes: 5 additions & 3 deletions payjoin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand Down
266 changes: 260 additions & 6 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Amount, OutPoint> = 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};
Expand Down Expand Up @@ -125,7 +379,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.
Expand All @@ -142,7 +396,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.
Expand Down Expand Up @@ -233,8 +487,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<bool, Error>,
Expand Down
Loading