Skip to content

Commit

Permalink
Revise mental model
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Apr 15, 2023
1 parent 66ab3c3 commit ff712e6
Showing 1 changed file with 99 additions and 47 deletions.
146 changes: 99 additions & 47 deletions MENTAL-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ The Payjoin SDK/`rust-payjoin` is the most well-tested and flexible library for

The primary crate, `payjoin`, is runtime-agnostic. Data persistence, chain interactions, and networking may be provided by custom implementations or copy the reference `payjoin-client` + bitcoind, `nolooking` + LND integration, or `bitmask-core` + BDK integrations.

The following is a breakdown of the existing documentation and its application to the `payjoin-client` reference implementation. Errors are mostly handled with `anyhow`.
The following is a breakdown of the existing documentation and its application to the `payjoin-client` reference implementation.

## Send a PayJoin

The `sender` feature provides all of the checks and most every PSBT formatting utility necessary to send payjoin out of the box. Sending requires an HTTP client and a bitcoin wallet. The reference implementation uses `reqwest` and Bitcoin Core RPC. Only a few non-default parameters are required:
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` and Bitcoin Core RPC. Only a few non-default parameters are required:

```rs
fn send_payjoin(
Expand All @@ -18,9 +18,13 @@ fn send_payjoin(
) -> Result<()>
```

### 1. Parse BIP21 as [`payjoin::Uri`](crate::Uri)
Default modules including http and a bitcoin wallet may be useful additions to this library.

Start by parseing and checking that there is a valid BIP 21 uri with PJ parameter. Uses the bip21 crate
The `danger_accept_invalid_certs` parameter is used for testing purposes only detailed in sectino 5.

### 1. Parse BIP21 as `payjoin::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.

```rs
let link = payjoin::Uri::try_from(bip21)
Expand All @@ -31,8 +35,6 @@ let link = link
.map_err(|e| anyhow!("The provided URI doesn't support payjoin (BIP78): {}", e))?;
```

Using a builder pattern may eliminate check_pj_supported step #19 [#19](https://github.com/payjoin/rust-payjoin/issues/19)

### 2. Construct URI request parameters, a finalized "Original PSBT" paying `.amount` to `.address`

```rs
Expand Down Expand Up @@ -65,16 +67,15 @@ 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)
.with_context(|| "Failed to create payjoin request")?;
```

### 3. (optional) Spawn a thread or async task that will broadcast the transaction after one minute unless canceled

I wrote this in the original docs, but I think it should be amended.

In case the payjoin goes through but you still want to pay by default. This missing `payjoin-client`

Writing this, I think of [Signal's contributing guidelines](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.):
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
Expand All @@ -87,9 +88,9 @@ let (req, ctx) = link

### 5. Send the request and receive response

Payjoin participants construct a transaction and require authentication and should have e2ee to prevent that transaction from being modified by a malicious third party during transit and being snooped on. Only https and .onion endpoints are spec-compatible payjoin endpoints.
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.

For testing, it's nice to be able to avoid this trust requirement (but again, Signal guidelines remind me of the folly of options).
Avoiding the secure endpoint requirement is convenient for testing.

```rs
let client = reqwest::blocking::Client::builder()
Expand All @@ -106,17 +107,19 @@ let response = client

### 6. Process the response

An Ok response should include a Payjoin PSBT signed by the receiver. Check that it's not trying to steal from us or otherwise do something wrong.
An `Ok` response should include a Payjoin Proposal PSBT. Check that it's signed, following protocol, not trying to steal or otherwise error.

```rs
// 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);
```

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 to "maliciously to phish a non technical user." Errors from `process_response` may be improved by being separated into safe `ReceiverError::WellKnown` standard error types that can be displayed in the UI and `ReceiverError::DebugOnly` which "can only appear in debug logs." Separation would simplify an integration's error handling.
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 to "maliciously to phish a non technical user." Only those listed as "well known" in the spec should be displayed with preset messages to prevent phishing.

### 7. Sign and finalize the Payjoin PSBT
### 7. Sign and finalize the Payjoin Proposal PSBT

Most software can handle adding the last signatures to a PSBT without issue.

```rs
let psbt = bitcoind
Expand All @@ -130,20 +133,25 @@ let tx = bitcoind
.ok_or_else(|| anyhow!("Incomplete PSBT"))?;
```

The signature step could be combined with the Process step if a signing closure were passed to process_response. That may error if the closure is an async runtime, however.
### 8. Broadcast the Payjoin Transaction

### 8. Broadcast the resulting PSBT
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.

```rs
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.

## Receive a Payjoin

The `receiver` feature provides all of the checks and most every PSBT formatting utility necessary to send payjoin out of the box. Receiving payjoin requires a live http endpoint listening for inbound requests. The `endpoint` (displayed in the BIP 21 URI pj parameter) could be configured rather than passed as an argument, and the amount is optional. The reference implementation uses `rouille` sync http server and Bitcoin Core RPC.
The `receiver` 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 bip21 request URI.

The reference implementation uses `rouille` sync http server and Bitcoin Core RPC.

```rs
fn receive_payjoin(
Expand All @@ -155,7 +163,7 @@ fn receive_payjoin(

### 1. Generate a pj_uri BIP21 using `payjoin::Uri::from_str`

A BIP 21 URI supporting payjoin requires an address and a secure `pj` endpoint.
A BIP 21 URI supporting payjoin contains at minimum a bitcoin address and a secure `pj` endpoint.

```rs
let pj_receiver_address = bitcoind.get_new_address(None, None)?;
Expand All @@ -173,9 +181,9 @@ let _pj_uri = pj_uri
.map_err(|e| anyhow!("Constructed URI does not support payjoin: {}", e))?;
```

A URI builder may be a more ergonomic interface. The `bip21` crate might be a good candidate to start to explore bindings with.
### 2. Listen for a sender's request on the `pj` endpoint

### 2. Listen for an original PSBT on the endpoint specified in the URI
Start a server to respond to payjoin protocol POST messages.

```rs
rouille::start_server("0.0.0.0:3000", move |req| handle_web_request(&req, &bitcoind));
Expand All @@ -196,8 +204,6 @@ fn handle_web_request(req: &Request, bitcoind: &bitcoincore_rpc::Client) -> Resp
}
```

This server only responds to payjoin protocol POST messages. It would be a better experience to respond with a payjoin QuickStart page for those sending GET requests to the payjoin endpoint.

### 3. Parse an incoming request using `UncheckedProposal::from_request()`

Parse incoming HTTP request and check that it follows protocol.
Expand All @@ -212,7 +218,6 @@ let proposal = payjoin::receiver::UncheckedProposal::from_request(
```

Headers are parsed using the `payjoin::receiver::Headers` Trait so that the library can iterate through them, ideally without cloning.
I reckon bindings will run into a problem with this design.

```rs
struct Headers<'a>(rouille::HeadersIter<'a>);
Expand All @@ -226,7 +231,7 @@ impl payjoin::receiver::Headers for Headers<'_> {

### 4. Validate the proposal using the `check` methods

The receiver checks the sender's Original PSBT to prevent it from stealing, probing to fingerprint its wallet, and so that it doesn't trip over any privacy gotchas.
Check the sender's Original PSBT to prevent it from stealing, probing to fingerprint its wallet, and to avoid privacy gotchas.

```rs
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
Expand All @@ -239,44 +244,59 @@ let network = match bitcoind.get_blockchain_info()?.chain.as_str() {
"regtest" => bitcoin::Network::Regtest,
_ => return Err(ReceiveError::Other(anyhow!("Unknown network"))),
};
```

#### Check 1: Can the Original PSBT be Broadcast?

// Receive Check 1: Can Broadcast
let proposal = proposal.check_can_broadcast(|tx| {
We need to know this transaction is consensus-valid.

```rs
let checked_1 = proposal.check_can_broadcast(|tx| {
bitcoind
.test_mempool_accept(&[bitcoin::consensus::encode::serialize(&tx).to_hex()])
.unwrap()
.first()
.unwrap()
.allowed
})?;
log::trace!("check1");
```

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.

// Receive Check 2: receiver can't sign for proposal inputs
let proposal = proposal.check_inputs_not_owned(|input| {
#### Check 2: Is the sender trying to make us sign our own inputs?

```rs
let checked_2 = checked_1.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()?;
log::trace!("check3");
```

#### Check 3: Are there mixed input scripts, breaking stenographic privacy?

```rs
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.

// 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
```rs
let mut payjoin = checked_3
.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()
})?;
log::trace!("check4");
```

### 5. Assuming the request is valid, augment with the available `try_preserving_privacy` and `substitute_output_address` methods
### 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 that break common input heuristic but violate trecherous Unnecessary Input Heuristic (UIH) so that privacy preservation is destroyed are moot. Until February 2023, even BTCPay occasionally made these errors. Privacy preserving coin selection done here is precarious and may be the most sensitive and valuable part of the kit. The Original PSBT output address that pays the receiver may be substituted with a unique one even from a watch-only wallet. The substitution may also be used to direct incoming funds and consolidate funds from a hot payjoin wallet to a cold wallet.
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 that break common input heuristic but violate trecherous Unnecessary Input Heuristic (UIH) so that privacy preservation is destroyed are moot. Until February 2023, even BTCPay occasionally made these errors. Privacy preserving coin selection as implemented in `try_preserving_privacy` is precarious and may be the most sensitive and valuable part of this kit.

PSBTv0 was not designed for input/output modification so these functions are complicated. PSBTv2 would simplify this part of the code under the hood.
Output substitution is another way to improve privacy, 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 fulfil exchange orders, open lightning channels, and more.

```rs
// Select receiver payjoin inputs.
Expand All @@ -285,15 +305,47 @@ _ = try_contributing_inputs(&mut payjoin, bitcoind)

let receiver_substitute_address = bitcoind.get_new_address(None, None)?;
payjoin.substitute_output_address(receiver_substitute_address);
```

The industry may benefit if we expose the selection algorithm inside `try_preserving_privacy` as a static function to make it easier and more lightweight to bind to and avoid dangerous instances of UIH.
// ...

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(())
}
```

Serious, in-depth research has gone into proper transaction construction. [Here's a good starting point from the JoinMarket repo](https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2796539). This part is critical to do right.
Serious, in-depth research has gone into proper transaction construction. [Here's a good starting point from the JoinMarket repo](https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2796539). Using methods for coin selection not provided by this library may have dire implications for privacy.

### 6. Extract the payjoin PSBT and sign it

Fees are applied to the proposal PSBT, having been modified by the receiver, using calculation factoring the receiver's minnimum feerate and the sender's fee-related [optional parameters](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#optional-parameters) into account. The `payjoin` implementation is very simple, disregarding PSBT fee estimation and only adding fees according to the sender's budget. More accurate fee calculation could be done with an algorithm to predict a PSBT's final weight (slightly more complicated than it sounds, but solved, just unimplemented in rust-bitcoin).
Fees are applied to the augmented Payjoin Proposal PSBT using calculation factoring the receiver's own 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 (slightly more complicated than it sounds, but solved, just unimplemented in rust-bitcoin), this `apply_fee` should be improved.

```rs
let payjoin_proposal_psbt = payjoin.apply_fee(min_feerate_sat_per_vb: Some(1))?;
Expand All @@ -309,11 +361,9 @@ let payjoin_proposal_psbt =
load_psbt_from_base64(payjoin_proposal_psbt.as_bytes()).context("Failed to parse PSBT")?;
```

The signing algorithm could be passed as a closure, but runs into the same runtime issues as mentioned previously. I.e., rust does not support async closures.

### 7. Respond to the sender's http request with the signed PSBT as payload

BIP 78 defines a very specific PSBT construction that the sender will find acceptable, which prepare_psbt handles. PSBTv0 was not designed to support input/output modification, so the protocol requires this step to be carried out precisely. A PSBTv2 protocol may not.
BIP 78 defines specific PSBT validation rules that the sender accept, which prepare_psbt ensures. PSBTv0 was not designed to support input/output modification, so the protocol requires this step to be carried out precisely. A future PSBTv2 payjoin protocol may not.

It is critical to pay special care in the error response messages. Without special care, a receiver could make itself vulnerable to probing attacks which cluster its UTXOs.

Expand All @@ -322,3 +372,5 @@ 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.

0 comments on commit ff712e6

Please sign in to comment.