|
| 1 | +/* |
| 2 | + * |
| 3 | + * |
| 4 | + * HTTPS SERVER |
| 5 | + */ |
| 6 | + |
| 7 | +use axum::Router; |
| 8 | + |
| 9 | +pub struct HttpServer; |
| 10 | + |
| 11 | +impl HttpServer { |
| 12 | + pub async fn new(port: u16, router: Router) { |
| 13 | + let url = format!("0.0.0.0:{}", port); |
| 14 | + let listener = tokio::net::TcpListener::bind(url).await.unwrap(); |
| 15 | + axum::serve(listener, router).await.unwrap(); |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +/* |
| 20 | + * |
| 21 | + * |
| 22 | + * PAYJOIN RECEIVER |
| 23 | + */ |
| 24 | + |
| 25 | +pub mod payjoin_receiver { |
| 26 | + use axum::extract::State; |
| 27 | + use axum::http::HeaderMap; |
| 28 | + use axum::response::IntoResponse; |
| 29 | + use axum::routing::post; |
| 30 | + use axum::{extract::Request, Router}; |
| 31 | + use bitcoin::address::NetworkChecked; |
| 32 | + use bitcoin::psbt::Psbt; |
| 33 | + use bitcoin::{base64, Address}; |
| 34 | + use bitcoincore_rpc::RpcApi; |
| 35 | + use http_body_util::BodyExt; |
| 36 | + use payjoin::bitcoin::{self, Amount}; |
| 37 | + use payjoin::receive::{PayjoinProposal, ProvisionalProposal}; |
| 38 | + use payjoin::Uri; |
| 39 | + use std::sync::Arc; |
| 40 | + use std::{collections::HashMap, str::FromStr}; |
| 41 | + |
| 42 | + use crate::types::Wallet; |
| 43 | + |
| 44 | + use super::HttpServer; |
| 45 | + |
| 46 | + struct Headers(HeaderMap); |
| 47 | + |
| 48 | + impl payjoin::receive::Headers for Headers { |
| 49 | + fn get_header(&self, key: &str) -> Option<&str> { |
| 50 | + self.0.get(key).and_then(|v| v.to_str().ok()) |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + fn build_pj_uri( |
| 55 | + address: bitcoin::Address, amount: Amount, pj: &'static str, |
| 56 | + ) -> Uri<'static, NetworkChecked> { |
| 57 | + let pj_uri_string = format!("{}?amount={}&pj={}", address.to_qr_uri(), amount.to_btc(), pj); |
| 58 | + let pj_uri = Uri::from_str(&pj_uri_string).unwrap(); |
| 59 | + pj_uri.assume_checked() |
| 60 | + } |
| 61 | + |
| 62 | + // Payjoin receiver |
| 63 | + // |
| 64 | + // This is the code that receives a Payjoin request from a sender. |
| 65 | + // |
| 66 | + // The receiver flow is: |
| 67 | + // 1. Extracting request data |
| 68 | + // 2 Check if the Original PSBT can be broadcast |
| 69 | + // 3. Check if the sender is trying to make us sign our own inputs |
| 70 | + // 4. Check if there are mixed input scripts, breaking stenographic privacy |
| 71 | + // 5. Check if we have seen this input before |
| 72 | + // 6. Augment a valid proposal to preserve privacy |
| 73 | + // 7. Extract the payjoin PSBT and sign it |
| 74 | + // 8. Respond to the sender's http request with the signed PSBT as payload |
| 75 | + pub struct Receiver { |
| 76 | + wallet: Arc<Wallet>, |
| 77 | + } |
| 78 | + |
| 79 | + impl Receiver { |
| 80 | + pub async fn handle_pj_request( |
| 81 | + State(wallet): State<Arc<Wallet>>, request: Request, |
| 82 | + ) -> impl IntoResponse { |
| 83 | + // let receiver_wallet = unimplemented!(); |
| 84 | + // Step 0: extract request data |
| 85 | + let (parts, body) = request.into_parts(); |
| 86 | + let bytes = body.collect().await.unwrap().to_bytes(); |
| 87 | + let headers = Headers(parts.headers.clone()); |
| 88 | + let proposal = |
| 89 | + payjoin::receive::UncheckedProposal::from_request(&bytes[..], "", headers).unwrap(); |
| 90 | + |
| 91 | + let min_fee_rate = None; |
| 92 | + // Step 1: Can the Original PSBT be Broadcast? |
| 93 | + // We need to know this transaction is consensus-valid. |
| 94 | + let checked_1 = |
| 95 | + proposal.check_broadcast_suitability(min_fee_rate, |tx| Ok(true)).unwrap(); |
| 96 | + // Step 2: Is the sender trying to make us sign our own inputs? |
| 97 | + let checked_2 = checked_1.check_inputs_not_owned(|input| Ok(true)).unwrap(); |
| 98 | + // Step 3: Are there mixed input scripts, breaking stenographic privacy? |
| 99 | + let checked_3 = checked_2.check_no_mixed_input_scripts().unwrap(); |
| 100 | + // Step 4: Have we seen this input before? |
| 101 | + // |
| 102 | + // Non-interactive i.e. payment processors should be careful to keep track |
| 103 | + // of request inputs or else a malicious sender may try and probe |
| 104 | + // multiple responses containing the receiver utxos, clustering their wallet. |
| 105 | + let checked_4 = checked_3.check_no_inputs_seen_before(|_outpoint| Ok(false)).unwrap(); |
| 106 | + // Step 5. Augment a valid proposal to preserve privacy |
| 107 | + // |
| 108 | + // Here's where the PSBT is modified. |
| 109 | + // Inputs may be added to break common input ownership heurstic. |
| 110 | + // There are a number of ways to select coins and break common input heuristic but |
| 111 | + // fail to preserve privacy because of Unnecessary Input Heuristic (UIH). |
| 112 | + // Until February 2023, even BTCPay occasionally made these errors. |
| 113 | + // Privacy preserving coin selection as implemented in `try_preserving_privacy` |
| 114 | + // is precarious to implement yourself may be the most sensitive and valuable part of this kit. |
| 115 | + // |
| 116 | + // Output substitution is another way to improve privacy and increase functionality. |
| 117 | + // For example, if the Original PSBT output address paying the receiver is coming from a static URI, |
| 118 | + // a new address may be generated on the fly to avoid address reuse. |
| 119 | + // This can even be done from a watch-only wallet. |
| 120 | + // Output substitution may also be used to consolidate incoming funds to a remote cold wallet, |
| 121 | + // break an output into smaller UTXOs to fulfill exchange orders, open lightning channels, and more. |
| 122 | + // |
| 123 | + // |
| 124 | + // Using methods for coin selection not provided by this library may have dire implications for privacy. |
| 125 | + // Significant in-depth research and careful implementation iteration has |
| 126 | + // gone into privacy preserving transaction construction. |
| 127 | + let mut prov_proposal = |
| 128 | + checked_4.identify_receiver_outputs(|output_script| Ok(true)).unwrap(); |
| 129 | + let unspent = wallet.list_unspent().unwrap(); |
| 130 | + let _ = Self::try_contributing_inputs(&mut prov_proposal, unspent); |
| 131 | + // Select receiver payjoin inputs. |
| 132 | + let receiver_substitute_address = wallet.get_new_address().unwrap(); |
| 133 | + prov_proposal.substitute_output_address(receiver_substitute_address); |
| 134 | + // Step 6. Extract the payjoin PSBT and sign it |
| 135 | + // |
| 136 | + // Fees are applied to the augmented Payjoin Proposal PSBT using calculation factoring both receiver's |
| 137 | + // preferred feerate and the sender's fee-related [optional parameters] |
| 138 | + // (https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#optional-parameters). |
| 139 | + let payjoin_proposal: PayjoinProposal = prov_proposal |
| 140 | + .finalize_proposal( |
| 141 | + |psbt: &Psbt| Ok(wallet.wallet_process_psbt(psbt).unwrap()), |
| 142 | + Some(payjoin::bitcoin::FeeRate::MIN), |
| 143 | + ) |
| 144 | + .unwrap(); |
| 145 | + // Step 7. Respond to the sender's http request with the signed PSBT as payload |
| 146 | + // |
| 147 | + // BIP 78 senders require specific PSBT validation constraints regulated by prepare_psbt. |
| 148 | + // PSBTv0 was not designed to support input/output modification, |
| 149 | + // so the protocol requires this precise preparation step. A future PSBTv2 payjoin protocol may not. |
| 150 | + // |
| 151 | + // It is critical to pay special care when returning error response messages. |
| 152 | + // Responding with internal errors can make a receiver vulnerable to sender probing attacks which cluster UTXOs. |
| 153 | + let payjoin_proposal_psbt = payjoin_proposal.psbt(); |
| 154 | + payjoin_proposal_psbt.to_string() |
| 155 | + } |
| 156 | + |
| 157 | + fn try_contributing_inputs( |
| 158 | + provisional_proposal: &mut ProvisionalProposal, unspent: Vec<bdk::LocalUtxo>, |
| 159 | + ) -> Result<(), ()> { |
| 160 | + use payjoin::bitcoin::OutPoint; |
| 161 | + |
| 162 | + let available_inputs = unspent; |
| 163 | + let candidate_inputs: HashMap<payjoin::bitcoin::Amount, OutPoint> = available_inputs |
| 164 | + .iter() |
| 165 | + .map(|i| { |
| 166 | + ( |
| 167 | + payjoin::bitcoin::Amount::from_sat(i.txout.value), |
| 168 | + OutPoint { txid: i.outpoint.txid, vout: i.outpoint.vout }, |
| 169 | + ) |
| 170 | + }) |
| 171 | + .collect(); |
| 172 | + |
| 173 | + let selected_outpoint = |
| 174 | + provisional_proposal.try_preserving_privacy(candidate_inputs).unwrap(); |
| 175 | + let selected_utxo = available_inputs |
| 176 | + .iter() |
| 177 | + .find(|i| { |
| 178 | + i.outpoint.txid == selected_outpoint.txid |
| 179 | + && i.outpoint.vout == selected_outpoint.vout |
| 180 | + }) |
| 181 | + .unwrap(); |
| 182 | + |
| 183 | + // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt |
| 184 | + let txo_to_contribute = payjoin::bitcoin::TxOut { |
| 185 | + value: selected_utxo.txout.value, |
| 186 | + script_pubkey: selected_utxo.txout.script_pubkey.clone(), |
| 187 | + }; |
| 188 | + let outpoint_to_contribute = payjoin::bitcoin::OutPoint { |
| 189 | + txid: selected_utxo.outpoint.txid, |
| 190 | + vout: selected_utxo.outpoint.vout, |
| 191 | + }; |
| 192 | + provisional_proposal |
| 193 | + .contribute_witness_input(txo_to_contribute, outpoint_to_contribute); |
| 194 | + Ok(()) |
| 195 | + } |
| 196 | + } |
| 197 | +} |
0 commit comments