diff --git a/payjoin/src/core/psbt/mod.rs b/payjoin/src/core/psbt/mod.rs index 0cae24500..bea17548f 100644 --- a/payjoin/src/core/psbt/mod.rs +++ b/payjoin/src/core/psbt/mod.rs @@ -105,7 +105,7 @@ pub(crate) struct InternalInputPair<'a> { } impl InternalInputPair<'_> { - /// Returns TxOut associated with the input + /// Returns the [`TxOut`] associated with the input. pub fn previous_txout(&self) -> Result<&TxOut, PrevTxOutError> { match (&self.psbtin.non_witness_utxo, &self.psbtin.witness_utxo) { (None, None) => Err(PrevTxOutError::MissingUtxoInformation), @@ -125,6 +125,7 @@ impl InternalInputPair<'_> { } } + /// Validates that [`TxIn`] and the applicable UTXO field(s) of the [`psbt::Input`] refer to the same UTXO. pub fn validate_utxo(&self) -> Result<(), InternalPsbtInputError> { match (&self.psbtin.non_witness_utxo, &self.psbtin.witness_utxo) { (None, None) => @@ -172,6 +173,7 @@ impl InternalInputPair<'_> { } } + /// Returns the scriptPubKey address type of the UTXO this input is pointing to. pub fn address_type(&self) -> Result { let txo = self.previous_txout()?; // HACK: Network doesn't matter for our use case of only getting the address type @@ -181,6 +183,7 @@ impl InternalInputPair<'_> { .ok_or(AddressTypeError::UnknownAddressType) } + /// Returns the expected weight of this input based on the address type of the UTXO it is pointing to. pub fn expected_input_weight(&self) -> Result { use bitcoin::AddressType::*; diff --git a/payjoin/src/core/receive/mod.rs b/payjoin/src/core/receive/mod.rs index f8d04bf9e..b7bc9c28c 100644 --- a/payjoin/src/core/receive/mod.rs +++ b/payjoin/src/core/receive/mod.rs @@ -43,7 +43,8 @@ pub(crate) mod v1; #[cfg_attr(docsrs, doc(cfg(feature = "v2")))] pub mod v2; -/// Helper to construct a pair of (txin, psbtin) with some built-in validation +/// A pair of ([`TxIn`], [`psbt::Input`]) with some built-in validation. +/// /// Use with [`InputPair::new`] to contribute receiver inputs. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct InputPair { @@ -53,6 +54,8 @@ pub struct InputPair { } impl InputPair { + /// Creates a new InputPair while validating that the passed [`TxIn`] and [`psbt::Input`] + /// refer to the same and the correct UTXO. pub fn new( txin: TxIn, psbtin: psbt::Input, @@ -119,7 +122,7 @@ impl InputPair { } } - /// Constructs a new ['InputPair'] for spending a legacy P2PKH output + /// Constructs a new [`InputPair`] for spending a legacy P2PKH output. pub fn new_p2pkh( non_witness_utxo: Transaction, outpoint: OutPoint, @@ -132,7 +135,7 @@ impl InputPair { Self::new_legacy_input_pair(non_witness_utxo, outpoint, sequence, None) } - /// Constructs a new ['InputPair'] for spending a legacy P2SH output + /// Constructs a new [`InputPair`] for spending a legacy P2SH output. pub fn new_p2sh( non_witness_utxo: Transaction, outpoint: OutPoint, @@ -168,7 +171,7 @@ impl InputPair { Self::new(txin, psbtin, expected_weight) } - /// Constructs a new ['InputPair'] for spending a native SegWit P2WPKH output + /// Constructs a new [`InputPair`] for spending a native SegWit P2WPKH output. pub fn new_p2wpkh( txout: TxOut, outpoint: OutPoint, @@ -181,7 +184,7 @@ impl InputPair { Self::new_segwit_input_pair(txout, outpoint, sequence, None) } - /// Constructs a new ['InputPair'] for spending a native SegWit P2WSH output + /// Constructs a new [`InputPair`] for spending a native SegWit P2WSH output. pub fn new_p2wsh( txout: TxOut, outpoint: OutPoint, @@ -195,7 +198,7 @@ impl InputPair { Self::new_segwit_input_pair(txout, outpoint, sequence, Some(expected_weight)) } - /// Constructs a new ['InputPair'] for spending a native SegWit P2TR output + /// Constructs a new [`InputPair`] for spending a native SegWit P2TR output. pub fn new_p2tr( txout: TxOut, outpoint: OutPoint, diff --git a/payjoin/src/core/receive/v1/mod.rs b/payjoin/src/core/receive/v1/mod.rs index 090b038ea..736add3e7 100644 --- a/payjoin/src/core/receive/v1/mod.rs +++ b/payjoin/src/core/receive/v1/mod.rs @@ -56,11 +56,23 @@ mod exclusive; #[cfg(feature = "v1")] pub use exclusive::*; -/// The sender's original PSBT and optional parameters +/// The original PSBT and the optional parameters received from the sender. /// -/// This type is used to process the request. It is returned by -/// [`UncheckedProposal::from_request()`] +/// This is the first typestate after the retrieval of the sender's original proposal in +/// the receiver's workflow. At this stage, the receiver can verify that the original PSBT they have +/// received from the sender is broadcastable to the network in the case of a payjoin failure. /// +/// The recommended usage of this typestate differs based on whether you are implementing an +/// interactive (where the receiver takes manual actions to respond to the +/// payjoin proposal) or a non-interactive (ex. a donation page which automatically generates a new QR code +/// for each visit) payment receiver. For the latter, you should call [`Self::check_broadcast_suitability`] to check +/// that the proposal is actually broadcastable (and, optionally, whether the fee rate is above the +/// minimum limit you have set). These mechanisms protect the receiver against probing attacks, where +/// a malicious sender can repeatedly send proposals to have the non-interactive receiver reveal the UTXOs +/// it owns with the proposals it modifies. +/// +/// If you are implementing an interactive payment receiver, then such checks are not necessary, and you +/// can go ahead with calling [`Self::assume_interactive_receiver`] to move on to the next typestate. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct UncheckedProposal { pub(crate) psbt: Psbt, @@ -68,6 +80,7 @@ pub struct UncheckedProposal { } impl UncheckedProposal { + /// Calculates the fee rate of the original proposal PSBT. fn psbt_fee_rate(&self) -> Result { let original_psbt_fee = self.psbt.fee().map_err(|e| { InternalPayloadError::ParsePsbt(bitcoin::psbt::PsbtParseError::PsbtEncoding(e)) @@ -75,21 +88,17 @@ impl UncheckedProposal { Ok(original_psbt_fee / self.psbt.clone().extract_tx_unchecked_fee_rate().weight()) } - /// Check that the Original PSBT can be broadcasted. - /// - /// Receiver MUST check that the Original PSBT from the sender - /// can be broadcast, i.e. `testmempoolaccept` bitcoind rpc returns { "allowed": true,.. }. - /// - /// Receiver can optionally set a minimum feerate that will be enforced on the Original PSBT. - /// This can be used to prevent probing attacks and make it easier to deal with - /// high feerate environments. + /// Checks that the original PSBT in the proposal can be broadcasted. /// - /// 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. + /// If the receiver is a non-interactive payment processor (ex. a donation page which generates + /// a new QR code for each visit), then it should make sure that the original PSBT is broadcastable + /// as a fallback mechanism in case the payjoin fails. This validation would be equivalent to + /// `testmempoolaccept` Bitcoin Core RPC call returning `{"allowed": true,...}`. /// - /// Call this after checking downstream. + /// Receiver can optionally set a minimum fee rate which will be enforced on the original PSBT in the proposal. + /// This can be used to further prevent probing attacks since the attacker would now need to probe the receiver + /// with transactions which are both broadcastable and pay high fee. Unrelated to the probing attack scenario, + /// this parameter also makes operating in a high fee environment easier for the receiver. pub fn check_broadcast_suitability( self, min_fee_rate: Option, @@ -114,21 +123,22 @@ impl UncheckedProposal { } } - /// Call this method if the only way to initiate a Payjoin with this receiver - /// requires manual intervention, as in most consumer wallets. + /// Moves on to the next typestate without any of the current typestate's validations. /// - /// So-called "non-interactive" receivers, like payment processors, that allow arbitrary requests are otherwise vulnerable to probing attacks. - /// Those receivers call `extract_tx_to_check_broadcast()` after making those checks downstream. + /// Use this for interactive payment receivers, where there is no risk of a probing attack since the + /// receiver needs to manually create payjoin URIs. pub fn assume_interactive_receiver(self) -> MaybeInputsOwned { MaybeInputsOwned { psbt: self.psbt, params: self.params } } } -/// Typestate to validate that the Original PSBT has no receiver-owned inputs. +/// Typestate to check that the original PSBT has no inputs owned by the receiver. +/// +/// At this point, it has been verified that the transaction is broadcastable from previous +/// typestate. The receiver can call [`Self::extract_tx_to_schedule_broadcast`] +/// to extract the signed original PSBT to schedule a fallback in case the Payjoin process fails. /// /// Call [`Self::check_inputs_not_owned`] to proceed. -/// If you are implementing an interactive payment processor, you should get extract the original -/// transaction with extract_tx_to_schedule_broadcast() and schedule #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MaybeInputsOwned { psbt: Psbt, @@ -136,14 +146,18 @@ pub struct MaybeInputsOwned { } impl MaybeInputsOwned { - /// The Sender's Original PSBT transaction + /// Extracts the original transaction received from the sender. + /// + /// Use this for scheduling the broadcast of the original transaction as a fallback + /// for the payjoin. Note that this function does not make any validation on whether + /// the transaction is broadcastable; it simply extracts it. pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction { self.psbt.clone().extract_tx_unchecked_fee_rate() } - /// Check that the Original PSBT has no receiver-owned inputs. - /// Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + + /// Check that the original PSBT has no receiver-owned inputs. /// - /// An attacker could try to spend receiver's own inputs. This check prevents that. + /// An attacker can try to spend the receiver's own inputs. This check prevents that. pub fn check_inputs_not_owned( self, is_owned: impl Fn(&Script) -> Result, @@ -173,7 +187,7 @@ impl MaybeInputsOwned { } } -/// Typestate to validate that the Original PSBT has no inputs that have been seen before. +/// Typestate to check that the original PSBT has no inputs that the receiver has seen before. /// /// Call [`Self::check_no_inputs_seen_before`] to proceed. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -182,9 +196,14 @@ pub struct MaybeInputsSeen { params: Params, } 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. + /// Check that the receiver has never seen the inputs in the original proposal before. + /// + /// This check prevents the following attacks: + /// 1. Probing attacks, where the sender can use the exact same proposal (or with minimal change) + /// to have the receiver reveal their UTXO set by contributing to all proposals with different inputs + /// and sending them back to the receiver. + /// 2. Re-entrant payjoin, where the sender uses the payjoin PSBT of a previous payjoin as the + /// original proposal PSBT of the current, new payjoin. pub fn check_no_inputs_seen_before( self, is_known: impl Fn(&OutPoint) -> Result, @@ -204,10 +223,12 @@ impl MaybeInputsSeen { } } -/// The receiver has not yet identified which outputs belong to the receiver. +/// Typestate to check that the outputs of the original PSBT actually pay to the receiver. +/// +/// The receiver should only accept the original PSBTs from the sender if it actually sends them +/// money. /// -/// Only accept PSBTs that send us money. -/// Identify those outputs with [`Self::identify_receiver_outputs`] to proceed. +/// Call [`Self::identify_receiver_outputs`] to proceed. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct OutputsUnknown { psbt: Psbt, @@ -215,7 +236,16 @@ pub struct OutputsUnknown { } impl OutputsUnknown { - /// Find which outputs belong to the receiver + /// Validates whether the original PSBT contains outputs which pay to the receiver and only + /// then proceeds to the next typestate. + /// + /// Additionally, this function also protects the receiver from accidentally subtracting fees + /// from their own outputs: when a sender is sending a proposal, + /// they can select an output which they want the receiver to subtract fees from to account for + /// the increased transaction size. If a sender specifies a receiver output for this purpose, this + /// function sets that parameter to None so that it is ignored in subsequent steps of the + /// receiver flow. This protects the receiver from accidentally subtracting fees from their own + /// outputs. pub fn identify_receiver_outputs( self, is_receiver_output: impl Fn(&Script) -> Result, @@ -247,6 +277,9 @@ impl OutputsUnknown { } } + // In case of there being multiple outputs paying to the receiver, we select the first one + // as the `change_vout`, which we will default to when making single output changes in + // future mutating typestates. Ok(WantsOutputs { original_psbt: self.psbt.clone(), payjoin_psbt: self.psbt, @@ -257,7 +290,12 @@ impl OutputsUnknown { } } -/// A checked proposal that the receiver may substitute or add outputs to +/// Typestate which the receiver may substitute or add outputs to. +/// +/// In addition to contributing new inputs to an existing PSBT, Payjoin allows the +/// receiver to substitute the original PSBT's outputs to potentially preserve privacy and batch transfers. +/// The receiver does not have to limit themselves to the address shared with the sender in the +/// original Payjoin URI, and can make substitutions of the existing outputs in the proposal. /// /// Call [`Self::commit_outputs`] to proceed. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -270,7 +308,7 @@ pub struct WantsOutputs { } impl WantsOutputs { - /// Whether the receiver is allowed to substitute original outputs or not. + /// Returns whether the receiver is allowed to substitute original outputs or not. pub fn output_substitution(&self) -> OutputSubstitution { self.params.output_substitution } /// Substitute the receiver output script with the provided script. @@ -283,11 +321,19 @@ impl WantsOutputs { self.replace_receiver_outputs(outputs, output_script) } - /// Replace **all** receiver outputs with one or more provided outputs. - /// The drain script specifies which address to *drain* coins to. An output corresponding to - /// that address must be included in `replacement_outputs`. The value of that output may be - /// increased or decreased depending on the receiver's input contributions and whether the - /// receiver needs to pay for additional miner fees (e.g. in the case of adding many outputs). + /// Replaces **all** receiver outputs with the one or more provided `replacement_outputs`, and + /// sets up the passed `drain_script` as the receiver-owned output which might have its value + /// adjusted based on the modifications the receiver makes in the subsequent typestates. + /// + /// The sender's outputs are not touched. Existing receiver outputs will be replaced with the + /// outputs in the `replacement_outputs` argument. The number of replacement outputs should + /// match or exceed the number of receiver outputs in the original proposal PSBT. + /// + /// The drain script is the receiver script which will have its value adjusted based on the + /// modifications the receiver makes on the transaction in the subsequent typestates. For + /// example, if the receiver adds their own input, then the drain script output will have its + /// value increased by the same amount. Or if an output needs to have its value reduced to + /// account for fees, the value of the output for this script will be reduced. pub fn replace_receiver_outputs( self, replacement_outputs: impl IntoIterator, @@ -354,7 +400,8 @@ impl WantsOutputs { }) } - /// Proceed to the input contribution step. + /// Commits the outputs as final, and moves on to the next typestate. + /// /// Outputs cannot be modified after this function is called. pub fn commit_outputs(self) -> WantsInputs { WantsInputs { @@ -369,6 +416,7 @@ impl WantsOutputs { /// Shuffles `new` vector, then interleaves its elements with those from `original`, /// maintaining the relative order in `original` but randomly inserting elements from `new`. +/// /// The combined result replaces the contents of `original`. fn interleave_shuffle(original: &mut Vec, new: &mut [T], rng: &mut R) { // Shuffle the substitute_outputs @@ -391,7 +439,7 @@ fn interleave_shuffle(original: &mut Vec, new: &mut [ *original = combined; } -/// A checked proposal that the receiver may contribute inputs to to make a payjoin +/// Typestate for a checked proposal which the receiver may contribute inputs to. /// /// Call [`Self::commit_inputs`] to proceed. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -404,14 +452,13 @@ pub struct WantsInputs { } impl WantsInputs { - /// Select receiver input such that the payjoin avoids surveillance. - /// Return the input chosen that has been applied to the Proposal. + /// Selects and returns an input from `candidate_inputs` which will preserve the receiver's privacy by + /// avoiding the Unnecessary Input Heuristic 2 (UIH2) outlined in [Unnecessary Input + /// Heuristics and PayJoin Transactions by Ghesmati et al. (2022)](https://eprint.iacr.org/2022/589). /// - /// Proper coin selection allows payjoin to resemble ordinary transactions. - /// To ensure the resemblance, a number of heuristics must be avoided. - /// - /// Attempt to avoid UIH (Unnecessary input heuristic) for 2-output transactions. - /// A simple consolidation is otherwise chosen if available. + /// Privacy preservation is only supported for 2-output transactions. If the PSBT has more than + /// 2 outputs or if none of the candidates are suitable for avoiding UIH2, this function + /// defaults to the first candidate in `candidate_inputs` list. pub fn try_preserving_privacy( &self, candidate_inputs: impl IntoIterator, @@ -422,14 +469,16 @@ impl WantsInputs { .or_else(|_| self.select_first_candidate(&mut candidate_inputs)) } - /// UIH "Unnecessary input heuristic" is one class of heuristics to avoid. We define - /// UIH1 and UIH2 according to the BlockSci practice - /// BlockSci UIH1 and UIH2: - /// if min(in) > min(out) then UIH1 else UIH2 - /// + /// Returns the candidate input which avoids the UIH2 defined in [Unnecessary Input + /// Heuristics and PayJoin Transactions by Ghesmati et al. (2022)](https://eprint.iacr.org/2022/589). + /// + /// Based on the paper, we are looking for the candidate input which, when added to the + /// transaction with 2 existing outputs, results in the minimum input amount to be lower than the minimum + /// output amount. Note that when calculating the minimum output amount, we consider the + /// post-contribution amounts, and expect the output which pays to the receiver to have its + /// value increased by the amount of the candidate input. /// - /// This UIH avoidance function supports only - /// many-input, 2-output transactions for now + /// Errors if the transaction does not have exactly 2 outputs. fn avoid_uih( &self, candidate_inputs: impl IntoIterator, @@ -472,6 +521,7 @@ impl WantsInputs { Err(InternalSelectionError::NotFound.into()) } + /// Returns the first candidate input in the provided list or errors if the list is empty. fn select_first_candidate( &self, candidate_inputs: impl IntoIterator, @@ -479,8 +529,9 @@ impl WantsInputs { candidate_inputs.into_iter().next().ok_or(InternalSelectionError::Empty.into()) } - /// Add the provided list of inputs to the transaction. - /// Any excess input amount is added to the change_vout output indicated previously. + /// Contributes the provided list of inputs to the transaction at random indices. If the total input + /// amount exceeds the total output amount after the contribution, adds all excess amount to + /// the receiver change output. pub fn contribute_inputs( self, inputs: impl IntoIterator, @@ -531,7 +582,7 @@ impl WantsInputs { }) } - // Compute the minimum amount that the receiver must contribute to the transaction as input + // Compute the minimum amount that the receiver must contribute to the transaction as input. fn receiver_min_input_amount(&self) -> Amount { let output_amount = self .payjoin_psbt @@ -548,7 +599,8 @@ impl WantsInputs { output_amount.checked_sub(original_output_amount).unwrap_or(Amount::ZERO) } - /// Proceed to the proposal finalization step. + /// Commits the inputs as final, and moves on to the next typestate. + /// /// Inputs cannot be modified after this function is called. pub fn commit_inputs(self) -> ProvisionalProposal { ProvisionalProposal { @@ -561,8 +613,9 @@ impl WantsInputs { } } -/// A checked proposal that the receiver may sign and finalize to make a proposal PSBT that the -/// sender will accept. +/// Typestate for a checked proposal which had both the outputs and the inputs modified +/// by the receiver. The receiver may sign and finalize the Payjoin proposal which will be sent to +/// the sender for their signature. /// /// Call [`Self::finalize_proposal`] to return a finalized [`PayjoinProposal`]. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -575,16 +628,21 @@ pub struct ProvisionalProposal { } impl ProvisionalProposal { - /// Apply additional fee contribution now that the receiver has contributed input - /// this is kind of a "build_proposal" step before we sign and finalize and extract + /// Applies additional fee contribution now that the receiver has contributed input + /// and may have added new outputs. /// - /// max_effective_fee_rate is the maximum effective fee rate that the receiver is - /// willing to pay for their own input/output contributions. A max_effective_fee_rate + /// `max_effective_fee_rate` is the maximum effective fee rate that the receiver is + /// willing to pay for their own input/output contributions. A `max_effective_fee_rate` /// of zero indicates that the receiver is not willing to pay any additional /// fees. /// - /// If not provided, min_fee_rate and max_effective_fee_rate default to the - /// minimum relay fee, as defined by [`FeeRate::BROADCAST_MIN`]. + /// If not provided, `min_fee_rate` and `max_effective_fee_rate` default to the + /// minimum possible relay fee. + /// + /// Note that this tries to pay for the fees from the sender's outputs as much as possible, + /// using the additional fee output the sender specified in the original proposal, while + /// keeping their minimum fee rate in account. When the sender contribution limit is reached, + /// it subtracts any remaining fees from the receiver change output. fn apply_fee( &mut self, min_fee_rate: Option, @@ -657,12 +715,12 @@ impl ProvisionalProposal { Ok(&self.payjoin_psbt) } - /// Calculate the additional input weight contributed by the receiver + /// Calculate the additional input weight contributed by the receiver. fn additional_input_weight(&self) -> Result { Ok(self.receiver_inputs.iter().map(|input_pair| input_pair.expected_weight).sum()) } - /// Calculate the additional output weight contributed by the receiver + /// Calculate the additional output weight contributed by the receiver. fn additional_output_weight(&self) -> Weight { let payjoin_outputs_weight = self .payjoin_psbt @@ -719,7 +777,7 @@ impl ProvisionalProposal { PayjoinProposal { payjoin_psbt: filtered_psbt } } - /// Return the indexes of the sender inputs + /// Return the indexes of the sender inputs. fn sender_input_indexes(&self) -> Vec { // iterate proposal as mutable WITH the outpoint (previous_output) available too let mut original_inputs = self.original_psbt.input_pairs().peekable(); @@ -740,12 +798,26 @@ impl ProvisionalProposal { sender_input_indexes } - /// Return a Payjoin Proposal PSBT that the sender will find acceptable. + /// Finalizes the Payjoin proposal into a PSBT which the sender will find acceptable before + /// they sign the transaction and broadcast it to the network. + /// + /// Finalization consists of multiple steps: + /// 1. Apply additional fees to pay for increased weight from any new inputs and/or outputs. + /// 2. Remove all sender signatures which were received with the original PSBT as these signatures are now invalid. + /// 3. Sign and finalize the resulting PSBT using the passed `wallet_process_psbt` signing function. + /// + /// How much the receiver ends up paying for fees depends on how much the sender stated they + /// were willing to pay in the parameters of the original proposal. For additional + /// inputs, fees will be subtracted from the sender's outputs as much as possible until we hit + /// the limit the sender specified in the Payjoin parameters. Any remaining fees for the new inputs + /// will be then subtracted from the change output of the receiver. + /// + /// Fees for additional outputs are always subtracted from the receiver's outputs. /// - /// This attempts to calculate any network fee owed by the receiver, subtract it from their output, - /// and return a PSBT that can produce a consensus-valid transaction that the sender will accept. + /// The minimum effective fee limit is the highest of the minimum limit set by the sender in + /// the original proposal parameters and the limit passed in the `min_fee_rate` parameter. /// - /// wallet_process_psbt should sign and finalize receiver inputs + /// Errors if the final effective fee rate exceeds `max_effective_fee_rate`. pub fn finalize_proposal( mut self, wallet_process_psbt: impl Fn(&Psbt) -> Result, @@ -766,7 +838,7 @@ impl ProvisionalProposal { } } -/// A finalized payjoin proposal, complete with fees and receiver signatures, that the sender +/// A finalized Payjoin proposal, complete with fees and receiver signatures, that the sender /// should find acceptable. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PayjoinProposal { @@ -774,12 +846,12 @@ pub struct PayjoinProposal { } impl PayjoinProposal { - /// The UTXOs that would be spent by this Payjoin transaction + /// The UTXOs that would be spent by this Payjoin transaction. pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator { self.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output) } - /// The Payjoin Proposal PSBT + /// The Payjoin Proposal PSBT. pub fn psbt(&self) -> &Psbt { &self.payjoin_psbt } } diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index 335f1a3cd..de2f101a0 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -1,11 +1,29 @@ //! Receive BIP 77 Payjoin v2 //! -//! OHTTP Privacy Warning +//! This module contains the typestates and helper methods to perform a Payjoin v2 receive. +//! +//! Receiving Payjoin transactions securely and privately requires the receiver to run safety +//! checks on the sender's original proposal, followed by actually making the input and output +//! contributions and modifications before sending the Payjoin proposal back to the sender. All +//! safety check and contribution/modification logic is identical between Payjoin v1 and v2. +//! +//! Additionally, this module also provides tools to manage +//! multiple Payjoin sessions which the receiver may have in progress at any given time. +//! The receiver can pause and resume Payjoin sessions when networking is available by using a +//! Payjoin directory as a store-and-forward server, and keep track of the success and failure of past sessions. +//! +//! See the typestate and function documentation on how to proceed through the receiver protocol +//! flow. +//! +//! For more information on Payjoin v2, see [BIP 77: Async Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md). +//! +//! ## OHTTP Privacy Warning //! Encapsulated requests whether GET or POST—**must not be retried or reused**. //! Retransmitting the same ciphertext (including via automatic retries) breaks the unlinkability and privacy guarantees of OHTTP, //! as it allows the relay to correlate requests by comparing ciphertexts. //! Note: Even fresh requests may be linkable via metadata (e.g. client IP, request timing), //! but request reuse makes correlation trivial for the relay. + use std::str::FromStr; use std::time::{Duration, SystemTime}; @@ -90,7 +108,7 @@ fn subdir_path_from_pubkey(pubkey: &HpkePublicKey) -> ShortId { } /// Represents the various states of a Payjoin receiver session during the protocol flow. -/// Each variant wraps a `Receiver` with a specific state type, except for [`ReceiveSession::Uninitialized`] which +/// Each variant parameterizes a `Receiver` with a specific state type, except for [`ReceiveSession::Uninitialized`] which /// has no context yet and [`ReceiveSession::TerminalFailure`] which indicates the session has ended or is invalid. /// /// This provides type erasure for the receive session state, allowing for the session to be replayed @@ -159,10 +177,30 @@ impl ReceiveSession { } } +/// Any typestate should implement this trait to be considered a part of the protocol flow. +/// +/// **IMPORTANT**: This is only meant to be implemented within the crate. It should not be used by dependencies +/// to extend the flow with new custom typestates. +/// +/// TODO: Make this sealed (). pub trait State {} +/// A higher-level receiver construct which will be taken through different states through the +/// protocol workflow. +/// +/// A Payjoin receiver is responsible for receiving the original proposal from the sender, making +/// various safety checks, contributing and/or changing inputs and outputs, and sending the Payjoin +/// proposal back to the sender before they sign off on the receiver's contributions and broadcast +/// the transaction. +/// +/// From a code/implementation perspective, Payjoin Development Kit uses a typestate pattern to +/// help receivers go through the entire Payjoin protocol flow. Each typestate has +/// various functions to accomplish the goals of the typestate, and one or more functions which +/// will commit the changes/checks in the current typestate and move to the next one. For more +/// information on the typestate pattern, see [The Typestate Pattern in Rust](https://cliffle.com/blog/rust-typestate/). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Receiver { + /// Data associated with the current state of the receiver. pub(crate) state: State, } @@ -213,17 +251,15 @@ impl State for UninitializedReceiver {} impl Receiver { /// Creates a new [`Receiver`] with the provided parameters. /// - /// # Parameters - /// - `address`: The Bitcoin address for the payjoin session. - /// - `directory`: The URL of the store-and-forward payjoin directory. - /// - `ohttp_keys`: The OHTTP keys used for encrypting and decrypting HTTP requests and responses. - /// - `expire_after`: The duration after which the session expires. + /// This is the beginning of the receiver protocol in Payjoin v2. It uses the passed address, + /// store-and-forward Payjoin directory URL, and the OHTTP keys to encrypt and decrypt HTTP + /// requests and responses to initialize a Payjoin v2 session. /// - /// # Returns - /// A new instance of [`Receiver`]. + /// Expiration time can be optionally defined to set when the session expires (due to + /// inactivity of either party, etc.) or otherwise set to a default of 24 hours. /// - /// # References - /// - [BIP 77: Payjoin Version 2: Serverless Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md) + /// See [BIP 77: Payjoin Version 2: Serverless Payjoin](https://github.com/bitcoin/bips/blob/master/bip-0077.md) + /// for more information on the purpose of each parameter for secure Payjoin v2 functionality. pub fn create_session( address: Address, directory: impl IntoUrl, @@ -412,11 +448,6 @@ impl Receiver { } } -/// The sender's original PSBT and optional parameters -/// -/// This type is used to process the request. It is returned by -/// [`Receiver::process_res()`]. -/// #[derive(Debug, Clone, PartialEq)] pub struct UncheckedProposal { pub(crate) v1: v1::UncheckedProposal, @@ -425,19 +456,35 @@ pub struct UncheckedProposal { impl State for UncheckedProposal {} +/// The original PSBT and the optional parameters received from the sender. +/// +/// This is the first typestate after the retrieval of the sender's original proposal in +/// the receiver's workflow. At this stage, the receiver can verify that the original PSBT they have +/// received from the sender is broadcastable to the network in the case of a payjoin failure. +/// +/// The recommended usage of this typestate differs based on whether you are implementing an +/// interactive (where the receiver takes manual actions to respond to the +/// payjoin proposal) or a non-interactive (ex. a donation page which automatically generates a new QR code +/// for each visit) payment receiver. For the latter, you should call [`Receiver::check_broadcast_suitability`] to check +/// that the proposal is actually broadcastable (and, optionally, whether the fee rate is above the +/// minimum limit you have set). These mechanisms protect the receiver against probing attacks, where +/// a malicious sender can repeatedly send proposals to have the non-interactive receiver reveal the UTXOs +/// it owns with the proposals it modifies. +/// +/// If you are implementing an interactive payment receiver, then such checks are not necessary, and you +/// can go ahead with calling [`Receiver::assume_interactive_receiver`] to move on to the next typestate. impl Receiver { - /// Call after checking that the Original PSBT can be broadcast. - /// - /// Receiver MUST check that the Original PSBT from the sender - /// can be broadcast, i.e. `testmempoolaccept` bitcoind rpc returns { "allowed": true,.. } - /// for `extract_tx_to_schedule_broadcast()` before calling this method. + /// Checks that the original PSBT in the proposal can be broadcasted. /// - /// 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. + /// If the receiver is a non-interactive payment processor (ex. a donation page which generates + /// a new QR code for each visit), then it should make sure that the original PSBT is broadcastable + /// as a fallback mechanism in case the payjoin fails. This validation would be equivalent to + /// `testmempoolaccept` RPC call returning `{"allowed": true,...}`. /// - /// Call this after checking downstream. + /// Receiver can optionally set a minimum fee rate which will be enforced on the original PSBT in the proposal. + /// This can be used to further prevent probing attacks since the attacker would now need to probe the receiver + /// with transactions which are both broadcastable and pay high fee. Unrelated to the probing attack scenario, + /// this parameter also makes operating in a high fee environment easier for the receiver. pub fn check_broadcast_suitability( self, min_fee_rate: Option, @@ -461,11 +508,10 @@ impl Receiver { ) } - /// Call this method if the only way to initiate a Payjoin with this receiver - /// requires manual intervention, as in most consumer wallets. + /// Moves on to the next typestate without any of the current typestate's validations. /// - /// So-called "non-interactive" receivers, like payment processors, that allow arbitrary requests are otherwise vulnerable to probing attacks. - /// Those receivers call `extract_tx_to_check_broadcast()` after making those checks downstream. + /// Use this for interactive payment receivers, where there is no risk of a probing attack since the + /// receiver needs to manually create payjoin URIs. pub fn assume_interactive_receiver( self, ) -> NextStateTransition> { @@ -483,11 +529,6 @@ impl Receiver { } } -/// Typestate to validate that the Original PSBT has no receiver-owned inputs. -/// -/// Call [`Receiver::check_inputs_not_owned`] to proceed. -/// If you are implementing an interactive payment processor, you should get extract the original -/// transaction with extract_tx_to_schedule_broadcast() and schedule #[derive(Debug, Clone, PartialEq)] pub struct MaybeInputsOwned { v1: v1::MaybeInputsOwned, @@ -496,15 +537,26 @@ pub struct MaybeInputsOwned { impl State for MaybeInputsOwned {} +/// Typestate to check that the original PSBT has no inputs owned by the receiver. +/// +/// At this point, it has been verified that the transaction is broadcastable from previous +/// typestate. The receiver can call [`Receiver::extract_tx_to_schedule_broadcast`] +/// to extract the signed original PSBT to schedule a fallback in case the Payjoin process fails. +/// +/// Call [`Receiver::check_inputs_not_owned`] to proceed. impl Receiver { - /// The Sender's Original PSBT + /// Extracts the original transaction received from the sender. + /// + /// Use this for scheduling the broadcast of the original transaction as a fallback + /// for the payjoin. Note that this function does not make any validation on whether + /// the transaction is broadcastable; it simply extracts it. pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction { self.v1.extract_tx_to_schedule_broadcast() } - /// Check that the Original PSBT has no receiver-owned inputs. - /// Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + + /// Check that the original PSBT has no receiver-owned inputs. /// - /// An attacker could try to spend receiver's own inputs. This check prevents that. + /// An attacker can try to spend the receiver's own inputs. This check prevents that. pub fn check_inputs_not_owned( self, is_owned: impl Fn(&Script) -> Result, @@ -536,9 +588,6 @@ impl Receiver { } } -/// Typestate to validate that the Original PSBT has no inputs that have been seen before. -/// -/// Call [`Receiver::check_no_inputs_seen_before`] to proceed. #[derive(Debug, Clone, PartialEq)] pub struct MaybeInputsSeen { v1: v1::MaybeInputsSeen, @@ -547,10 +596,18 @@ pub struct MaybeInputsSeen { impl State for MaybeInputsSeen {} +/// Typestate to check that the original PSBT has no inputs that the receiver has seen before. +/// +/// Call [`Receiver::check_no_inputs_seen_before`] to proceed. impl Receiver { - /// 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. + /// Check that the receiver has never seen the inputs in the original proposal before. + /// + /// This check prevents the following attacks: + /// 1. Probing attacks, where the sender can use the exact same proposal (or with minimal change) + /// to have the receiver reveal their UTXO set by contributing to all proposals with different inputs + /// and sending them back to the receiver. + /// 2. Re-entrant payjoin, where the sender uses the payjoin PSBT of a previous payjoin as the + /// original proposal PSBT of the current, new payjoin. pub fn check_no_inputs_seen_before( self, is_known: impl Fn(&OutPoint) -> Result, @@ -582,10 +639,6 @@ impl Receiver { } } -/// The receiver has not yet identified which outputs belong to the receiver. -/// -/// Only accept PSBTs that send us money. -/// Identify those outputs with [`Receiver::identify_receiver_outputs`] to proceed. #[derive(Debug, Clone, PartialEq)] pub struct OutputsUnknown { inner: v1::OutputsUnknown, @@ -594,8 +647,23 @@ pub struct OutputsUnknown { impl State for OutputsUnknown {} +/// Typestate to check that the outputs of the original PSBT actually pay to the receiver. +/// +/// The receiver should only accept the original PSBTs from the sender which actually send them +/// money. +/// +/// Call [`Receiver::identify_receiver_outputs`] to proceed. impl Receiver { - /// Find which outputs belong to the receiver + /// Validates whether the original PSBT contains outputs which pay to the receiver and only + /// then proceeds to the next typestate. + /// + /// Additionally, this function also protects the receiver from accidentally subtracting fees + /// from their own outputs: when a sender is sending a proposal, + /// they can select an output which they want the receiver to subtract fees from to account for + /// the increased transaction size. If a sender specifies a receiver output for this purpose, this + /// function sets that parameter to None so that it is ignored in subsequent steps of the + /// receiver flow. This protects the receiver from accidentally subtracting fees from their own + /// outputs. pub fn identify_receiver_outputs( self, is_receiver_output: impl Fn(&Script) -> Result, @@ -627,9 +695,6 @@ impl Receiver { } } -/// A checked proposal that the receiver may substitute or add outputs to -/// -/// Call [`Receiver::commit_outputs`] to proceed. #[derive(Debug, Clone, PartialEq)] pub struct WantsOutputs { v1: v1::WantsOutputs, @@ -638,6 +703,14 @@ pub struct WantsOutputs { impl State for WantsOutputs {} +/// Typestate which the receiver may substitute or add outputs to. +/// +/// In addition to contributing new inputs to an existing PSBT, Payjoin allows the +/// receiver to substitute the original PSBT's outputs to potentially preserve privacy and batch transfers. +/// The receiver does not have to limit themselves to the address shared with the sender in the +/// original Payjoin URI, and can make substitutions of the existing outputs in the proposal. +/// +/// Call [`Receiver::commit_outputs`] to proceed. impl Receiver { /// Whether the receiver is allowed to substitute original outputs or not. pub fn output_substitution(&self) -> OutputSubstitution { self.v1.output_substitution() } @@ -651,11 +724,19 @@ impl Receiver { Ok(Receiver { state: WantsOutputs { v1: inner, context: self.state.context } }) } - /// Replace **all** receiver outputs with one or more provided outputs. - /// The drain script specifies which address to *drain* coins to. An output corresponding to - /// that address must be included in `replacement_outputs`. The value of that output may be - /// increased or decreased depending on the receiver's input contributions and whether the - /// receiver needs to pay for additional miner fees (e.g. in the case of adding many outputs). + /// Replaces **all** receiver outputs with the one or more provided `replacement_outputs`, and + /// sets up the passed `drain_script` as the receiver-owned output which might have its value + /// adjusted based on the modifications the receiver makes in the subsequent typestates. + /// + /// Sender's outputs are not touched. Existing receiver outputs will be replaced with the + /// outputs in the `replacement_outputs` argument. The number of replacement outputs should + /// match or exceed the number of receiver outputs in the original proposal PSBT. + /// + /// The drain script is the receiver script which will have its value adjusted based on the + /// modifications the receiver makes on the transaction in the subsequent typestates. For + /// example, if the receiver adds their own input, then the drain script output will have its + /// value increased by the same amount. Or if an output needs to have its value reduced to + /// account for fees, the value of the output for this script will be reduced. pub fn replace_receiver_outputs( self, replacement_outputs: impl IntoIterator, @@ -665,7 +746,8 @@ impl Receiver { Ok(Receiver { state: WantsOutputs { v1: inner, context: self.state.context } }) } - /// Proceed to the input contribution step. + /// Commits the outputs as final, and moves on to the next typestate. + /// /// Outputs cannot be modified after this function is called. pub fn commit_outputs(self) -> NextStateTransition> { let inner = self.state.v1.clone().commit_outputs(); @@ -681,9 +763,6 @@ impl Receiver { } } -/// A checked proposal that the receiver may contribute inputs to to make a payjoin -/// -/// Call [`Receiver::commit_inputs`] to proceed. #[derive(Debug, Clone, PartialEq)] pub struct WantsInputs { v1: v1::WantsInputs, @@ -692,18 +771,17 @@ pub struct WantsInputs { impl State for WantsInputs {} +/// Typestate for a checked proposal which the receiver may contribute inputs to. +/// +/// Call [`Receiver::commit_inputs`] to proceed. impl Receiver { - /// Select receiver input such that the payjoin avoids surveillance. - /// Return the input chosen that has been applied to the Proposal. + /// Selects and returns an input from `candidate_inputs` which will preserve the receiver's privacy by + /// avoiding the Unnecessary Input Heuristic 2 (UIH2) outlined in [Unnecessary Input + /// Heuristics and PayJoin Transactions by Ghesmati et al. (2022)](https://eprint.iacr.org/2022/589). /// - /// Proper coin selection allows payjoin to resemble ordinary transactions. - /// To ensure the resemblance, a number of heuristics must be avoided. - /// - /// UIH "Unnecessary input heuristic" is one class of them to avoid. We define - /// UIH1 and UIH2 according to the BlockSci practice - /// BlockSci UIH1 and UIH2: - /// if min(in) > min(out) then UIH1 else UIH2 - /// + /// Privacy preservation is only supported for 2-output transactions. If the PSBT has more than + /// 2 outputs or if none of the candidates are suitable for avoiding UIH2, this function + /// defaults to the first candidate in `candidate_inputs` list. pub fn try_preserving_privacy( &self, candidate_inputs: impl IntoIterator, @@ -711,8 +789,9 @@ impl Receiver { self.v1.try_preserving_privacy(candidate_inputs) } - /// Add the provided list of inputs to the transaction. - /// Any excess input amount is added to the change_vout output indicated previously. + /// Contributes the provided list of inputs to the transaction at random indices. If the total input + /// amount exceeds the total output amount after the contribution, adds all excess amount to + /// the receiver change output. pub fn contribute_inputs( self, inputs: impl IntoIterator, @@ -721,7 +800,8 @@ impl Receiver { Ok(Receiver { state: WantsInputs { v1: inner, context: self.state.context } }) } - /// Proceed to the proposal finalization step. + /// Commits the inputs as final, and moves on to the next typestate. + /// /// Inputs cannot be modified after this function is called. pub fn commit_inputs(self) -> NextStateTransition> { let inner = self.state.v1.clone().commit_inputs(); @@ -740,10 +820,6 @@ impl Receiver { } } -/// A checked proposal that the receiver may sign and finalize to make a proposal PSBT that the -/// sender will accept. -/// -/// Call [`Receiver::finalize_proposal`] to return a finalized [`PayjoinProposal`]. #[derive(Debug, Clone, PartialEq)] pub struct ProvisionalProposal { v1: v1::ProvisionalProposal, @@ -752,13 +828,32 @@ pub struct ProvisionalProposal { impl State for ProvisionalProposal {} +/// Typestate for a checked proposal which had both the outputs and the inputs modified +/// by the receiver. The receiver may sign and finalize the Payjoin proposal which will be sent to +/// the sender for their signature. +/// +/// Call [`Receiver::finalize_proposal`] to return a finalized [`PayjoinProposal`]. impl Receiver { - /// Return a Payjoin Proposal PSBT that the sender will find acceptable. + /// Finalizes the Payjoin proposal into a PSBT which the sender will find acceptable before + /// they re-sign the transaction and broadcast it to the network. + /// + /// Finalization consists of multiple steps: + /// 1. Apply additional fees to pay for increased weight from any new inputs and/or outputs. + /// 2. Remove all sender signatures which were received with the original PSBT as these signatures are now invalid. + /// 3. Sign and finalize the resulting PSBT using the passed `wallet_process_psbt` signing function. + /// + /// How much the receiver ends up paying for fees depends on how much the sender stated they + /// were willing to pay in the parameters of the original proposal. For additional + /// inputs, fees will be subtracted from the sender's outputs as much as possible until we hit + /// the limit the sender specified in the Payjoin parameters. Any remaining fees for the new inputs + /// will be then subtracted from the change output of the receiver. /// - /// This attempts to calculate any network fee owed by the receiver, subtract it from their output, - /// and return a PSBT that can produce a consensus-valid transaction that the sender will accept. + /// Fees for additional outputs are always subtracted from the receiver's outputs. /// - /// wallet_process_psbt should sign and finalize receiver inputs + /// The minimum effective fee limit is the highest of the minimum limit set by the sender in + /// the original proposal parameters and the limit passed in the `min_fee_rate` parameter. + /// + /// Errors if the final effective fee rate exceeds `max_effective_fee_rate`. pub fn finalize_proposal( self, wallet_process_psbt: impl Fn(&Psbt) -> Result, @@ -790,8 +885,6 @@ impl Receiver { } } -/// A finalized payjoin proposal, complete with fees and receiver signatures, that the sender -/// should find acceptable. #[derive(Debug, Clone, PartialEq)] pub struct PayjoinProposal { v1: v1::PayjoinProposal, @@ -808,16 +901,18 @@ impl PayjoinProposal { } } +/// A finalized Payjoin proposal, complete with fees and receiver signatures, that the sender +/// should find acceptable. impl Receiver { #[cfg(feature = "_multiparty")] pub(crate) fn new(proposal: PayjoinProposal) -> Self { Receiver { state: proposal } } - /// The UTXOs that would be spent by this Payjoin transaction + /// The UTXOs that would be spent by this Payjoin transaction. pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator { self.v1.utxos_to_be_locked() } - /// The Payjoin Proposal PSBT + /// The Payjoin Proposal PSBT. pub fn psbt(&self) -> &Psbt { self.v1.psbt() } /// Extract an OHTTP Encapsulated HTTP POST request for the Proposal PSBT