@@ -70,40 +70,27 @@ impl UncheckedOriginalPayload {
7070 }
7171}
7272
73- /// The original PSBT and the optional parameters received from the sender.
73+ /// This is the first typestate after retrieving the sender's proposal. Here the
74+ /// receiver verifies the Original PSBT is broadcastable so it can serve as a
75+ /// fallback if the payjoin fails.
7476///
75- /// This is the first typestate after the retrieval of the sender's original proposal in
76- /// the receiver's workflow. At this stage, the receiver can verify that the original PSBT they have
77- /// received from the sender is broadcastable to the network in the case of a payjoin failure.
78- ///
79- /// The recommended usage of this typestate differs based on whether you are implementing an
80- /// interactive (where the receiver takes manual actions to respond to the
81- /// payjoin proposal) or a non-interactive (ex. a donation page which automatically generates a new QR code
82- /// for each visit) payment receiver. For the latter, you should call [`Self::check_broadcast_suitability`] to check
83- /// that the proposal is actually broadcastable (and, optionally, whether the fee rate is above the
84- /// minimum limit you have set). These mechanisms protect the receiver against probing attacks, where
85- /// a malicious sender can repeatedly send proposals to have the non-interactive receiver reveal the UTXOs
86- /// it owns with the proposals it modifies.
87- ///
88- /// If you are implementing an interactive payment receiver, then such checks are not necessary, and you
89- /// can go ahead with calling [`Self::assume_interactive_receiver`] to move on to the next typestate.
77+ /// Non-interactive receivers (e.g. a donation page that generates a fresh QR code
78+ /// per visit) should call [`Self::check_broadcast_suitability`] to confirm the
79+ /// proposal is broadcastable (and optionally above a minimum fee rate), guarding
80+ /// against probing attacks that trick the receiver into revealing its UTXOs.
81+ /// Interactive receivers can skip that check and call
82+ /// [`Self::assume_interactive_receiver`] instead. Either path advances to
83+ /// [`MaybeInputsOwned`].
9084#[ derive( Debug , Clone ) ]
9185pub struct UncheckedOriginalPayload {
9286 original : OriginalPayload ,
9387}
9488
9589impl UncheckedOriginalPayload {
96- /// Checks that the original PSBT in the proposal can be broadcasted.
97- ///
98- /// If the receiver is a non-interactive payment processor (ex. a donation page which generates
99- /// a new QR code for each visit), then it should make sure that the original PSBT is broadcastable
100- /// as a fallback mechanism in case the payjoin fails. This validation would be equivalent to
101- /// `testmempoolaccept` Bitcoin Core RPC call returning `{"allowed": true,...}`.
90+ /// Check that the sender's Original PSBT is suitable for broadcast, ensuring
91+ /// it can be used as a fallback if the payjoin does not complete.
10292 ///
103- /// Receiver can optionally set a minimum fee rate which will be enforced on the original PSBT in the proposal.
104- /// This can be used to further prevent probing attacks since the attacker would now need to probe the receiver
105- /// with transactions which are both broadcastable and pay high fee. Unrelated to the probing attack scenario,
106- /// this parameter also makes operating in a high fee environment easier for the receiver.
93+ /// Returns a [`MaybeInputsOwned`] to continue validation.
10794 pub fn check_broadcast_suitability (
10895 self ,
10996 min_fee_rate : Option < FeeRate > ,
@@ -113,40 +100,43 @@ impl UncheckedOriginalPayload {
113100 Ok ( MaybeInputsOwned { original : self . original } )
114101 }
115102
116- /// Moves on to the next typestate without any of the current typestate's validations.
103+ /// Skip the current typestate's validations.
117104 ///
118- /// Use this for interactive payment receivers, where there is no risk of a probing attack since the
119- /// receiver needs to manually create payjoin URIs.
105+ /// Use this for interactive receivers, which manually create Payjoin URIs and so
106+ /// are not exposed to the probing attacks the checks guard against.
107+ ///
108+ /// Returns a [`MaybeInputsOwned`].
120109 pub fn assume_interactive_receiver ( self ) -> MaybeInputsOwned {
121110 MaybeInputsOwned { original : self . original }
122111 }
123112}
124113
125- /// Typestate to check that the original PSBT has no inputs owned by the receiver.
114+ /// Typestate to check that the Original PSBT has no inputs owned by the receiver.
126115///
127- /// At this point, it has been verified that the transaction is broadcastable from previous
128- /// typestate. The receiver can call [`Self::extract_tx_to_schedule_broadcast`]
129- /// to extract the signed original PSBT to schedule a fallback in case the Payjoin process fails.
116+ /// At this point, the Original PSBT has been verified as broadcastable; the receiver
117+ /// can call [`Self::extract_tx_to_schedule_broadcast`] to schedule a fallback broadcast
118+ /// in case the payjoin fails.
130119///
131- /// Call [`Self::check_inputs_not_owned`] to proceed.
120+ /// Call [`Self::check_inputs_not_owned`] to advance to [`MaybeInputsSeen`] to continue
121+ /// validation.
132122#[ derive( Debug , Clone ) ]
133123pub struct MaybeInputsOwned {
134124 pub ( crate ) original : OriginalPayload ,
135125}
136126
137127impl MaybeInputsOwned {
138- /// Extracts the original transaction received from the sender.
128+ /// Extract the transaction from the Original PSBT for scheduling broadcast as a
129+ /// fallback in case the payjoin does not complete.
139130 ///
140- /// Use this for scheduling the broadcast of the original transaction as a fallback
141- /// for the payjoin. Note that this function does not make any validation on whether
142- /// the transaction is broadcastable; it simply extracts it.
131+ /// Returns the extracted [`bitcoin::Transaction`].
143132 pub fn extract_tx_to_schedule_broadcast ( & self ) -> bitcoin:: Transaction {
144133 self . original . psbt . clone ( ) . extract_tx_unchecked_fee_rate ( )
145134 }
146135
147- /// Check that the original PSBT has no receiver-owned inputs.
136+ /// Check that none of the Original PSBT's inputs belong to the receiver,
137+ /// preventing an attacker from spending the receiver's own inputs.
148138 ///
149- /// An attacker can try to spend the receiver's own inputs. This check prevents that .
139+ /// Returns a [`MaybeInputsSeen`] to continue validation .
150140 pub fn check_inputs_not_owned (
151141 self ,
152142 is_owned : & mut impl FnMut ( & Script ) -> Result < bool , ImplementationError > ,
@@ -156,22 +146,27 @@ impl MaybeInputsOwned {
156146 }
157147}
158148
159- /// Typestate to check that the original PSBT has no inputs that the receiver has seen before.
149+ /// Typestate to check that the Original PSBT has no inputs the receiver has seen before.
150+ ///
151+ /// This check prevents the following attacks:
152+ /// 1. Probing attacks, where the sender uses the exact same proposal (or with
153+ /// minimal change) to have the receiver reveal their UTXO set by contributing
154+ /// to all proposals with different inputs and sending them back to the receiver.
155+ /// 2. Re-entrant payjoin, where the sender uses the payjoin PSBT of a previous
156+ /// payjoin as the Original PSBT of the current, new payjoin.
160157///
161- /// Call [`Self::check_no_inputs_seen_before`] to proceed.
158+ /// Call [`Self::check_no_inputs_seen_before`] to advance to [`OutputsUnknown`] to
159+ /// continue validation.
162160#[ derive( Debug , Clone ) ]
163161pub struct MaybeInputsSeen {
164162 original : OriginalPayload ,
165163}
166164impl MaybeInputsSeen {
167- /// Check that the receiver has never seen the inputs in the original proposal before.
165+ /// Check that none of the inputs have been seen before, preventing input
166+ /// probing and replay attacks (where inputs have been used in a previous
167+ /// payjoin attempt).
168168 ///
169- /// This check prevents the following attacks:
170- /// 1. Probing attacks, where the sender can use the exact same proposal (or with minimal change)
171- /// to have the receiver reveal their UTXO set by contributing to all proposals with different inputs
172- /// and sending them back to the receiver.
173- /// 2. Re-entrant payjoin, where the sender uses the payjoin PSBT of a previous payjoin as the
174- /// original proposal PSBT of the current, new payjoin.
169+ /// Returns an [`OutputsUnknown`] to continue validation.
175170 pub fn check_no_inputs_seen_before (
176171 self ,
177172 is_known : & mut impl FnMut ( & OutPoint ) -> Result < bool , ImplementationError > ,
@@ -181,28 +176,21 @@ impl MaybeInputsSeen {
181176 }
182177}
183178
184- /// Typestate to check that the outputs of the original PSBT actually pay to the receiver.
179+ /// Typestate to check that the outputs of the Original PSBT actually pay the receiver.
185180///
186- /// The receiver should only accept the original PSBTs from the sender if it actually sends them
187- /// money.
188- ///
189- /// Call [`Self::identify_receiver_outputs`] to proceed.
181+ /// The receiver should only accept Original PSBTs from the sender that actually send
182+ /// them money. Call [`Self::identify_receiver_outputs`] to advance to [`WantsOutputs`]
183+ /// to continue the proposal.
190184#[ derive( Debug , Clone ) ]
191185pub struct OutputsUnknown {
192186 original : OriginalPayload ,
193187}
194188
195189impl OutputsUnknown {
196- /// Validates whether the original PSBT contains outputs which pay to the receiver and only
197- /// then proceeds to the next typestate .
190+ /// Identify which outputs in the original transaction belong to the receiver
191+ /// and ensure at least one output pays the receiver .
198192 ///
199- /// Additionally, this function also protects the receiver from accidentally subtracting fees
200- /// from their own outputs: when a sender is sending a proposal,
201- /// they can select an output which they want the receiver to subtract fees from to account for
202- /// the increased transaction size. If a sender specifies a receiver output for this purpose, this
203- /// function sets that parameter to None so that it is ignored in subsequent steps of the
204- /// receiver flow. This protects the receiver from accidentally subtracting fees from their own
205- /// outputs.
193+ /// Returns a [`WantsOutputs`] to continue the proposal.
206194 #[ cfg_attr( not( feature = "v1" ) , allow( dead_code) ) ]
207195 pub fn identify_receiver_outputs (
208196 self ,
@@ -260,6 +248,8 @@ impl crate::receive::common::WantsFeeRange {
260248 ///
261249 /// The minimum effective fee limit is the highest of the minimum limit set by the sender in
262250 /// the original proposal parameters and the limit passed in the `min_fee_rate` parameter.
251+ ///
252+ /// Returns a [`ProvisionalProposal`].
263253 pub fn apply_fee_range (
264254 self ,
265255 min_fee_rate : Option < FeeRate > ,
@@ -271,21 +261,19 @@ impl crate::receive::common::WantsFeeRange {
271261 }
272262}
273263
274- /// Typestate for a checked proposal which had both the outputs and the inputs modified
275- /// by the receiver. The receiver may sign and finalize the Payjoin proposal which will be sent to
276- /// the sender for their signature.
264+ /// Typestate for a checked proposal that the receiver has modified the outputs and
265+ /// inputs of, and is ready to be signed and finalized.
277266///
278- /// Call [`Self::finalize_proposal`] to return a finalized [`PayjoinProposal`].
267+ /// Call [`Self::finalize_proposal`] to advance to [`PayjoinProposal`].
279268#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
280269pub struct ProvisionalProposal {
281270 psbt_context : PsbtContext ,
282271}
283272
284273impl ProvisionalProposal {
285- /// Finalizes the Payjoin proposal into a PSBT which the sender will find acceptable before
286- /// they sign the transaction and broadcast it to the network.
274+ /// Finalize the proposal by signing the PSBT via the `wallet_process_psbt` callback.
287275 ///
288- /// Finalization consists of signing and finalizing the PSBT using the passed `wallet_process_psbt` signing function .
276+ /// Returns the final [`PayjoinProposal`] .
289277 pub fn finalize_proposal (
290278 self ,
291279 wallet_process_psbt : impl Fn ( & Psbt ) -> Result < Psbt , ImplementationError > ,
@@ -297,28 +285,34 @@ impl ProvisionalProposal {
297285 Ok ( PayjoinProposal { payjoin_psbt : finalized_psbt } )
298286 }
299287
300- /// The Payjoin proposal PSBT that the receiver needs to sign
288+ /// Extract the PSBT that needs to be signed by the receiver's wallet.
301289 ///
302- /// In some applications the entity that progresses the typestate
303- /// is different from the entity that has access to the private keys,
304- /// so the PSBT to sign must be accessible to such implementers.
290+ /// In some applications the entity that progresses the typestate is different from the
291+ /// entity that has access to the private keys, so the PSBT to sign must be accessible to
292+ /// such implementers.
293+ ///
294+ /// Returns the Payjoin proposal [`Psbt`] to be signed.
305295 pub fn psbt_to_sign ( & self ) -> Psbt { self . psbt_context . psbt_to_sign ( ) }
306296}
307297
308- /// A finalized Payjoin proposal, complete with fees and receiver signatures, that the sender
309- /// should find acceptable.
298+ /// Typestate for a signed and finalized Payjoin proposal that is to be sent to the
299+ /// sender for them to sign and broadcast.
300+ ///
301+ /// Extract the proposal PSBT with [`Self::psbt`] and respond to the sender's original
302+ /// request with it.
310303#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
311304pub struct PayjoinProposal {
312305 payjoin_psbt : Psbt ,
313306}
314307
315308impl PayjoinProposal {
316- /// The UTXOs that would be spent by this Payjoin transaction.
309+ /// Returns the outpoints of UTXOs that should be locked to
310+ /// prevent double-spending while the payjoin is in progress.
317311 pub fn utxos_to_be_locked ( & self ) -> impl ' _ + Iterator < Item = & bitcoin:: OutPoint > {
318312 self . payjoin_psbt . unsigned_tx . input . iter ( ) . map ( |input| & input. previous_output )
319313 }
320314
321- /// The Payjoin Proposal PSBT.
315+ /// Returns the finalized payjoin proposal PSBT.
322316 pub fn psbt ( & self ) -> & Psbt { & self . payjoin_psbt }
323317}
324318
0 commit comments