Skip to content

Commit 2183b5d

Browse files
Revise v1 receiver typestate doc comments
Apply the same doc comment rework to the v1 receive module: state-level comments are concise and point to the method that advances to the next typestate, while per-method comments align with the wording used in the FFI interface. Unlike v2, v1 methods return the next state directly, so the docs describe the returned state without transition or persistence framing.
1 parent 315fcfe commit 2183b5d

1 file changed

Lines changed: 72 additions & 78 deletions

File tree

  • payjoin/src/core/receive/v1

payjoin/src/core/receive/v1/mod.rs

Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
9185
pub struct UncheckedOriginalPayload {
9286
original: OriginalPayload,
9387
}
9488

9589
impl 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)]
133123
pub struct MaybeInputsOwned {
134124
pub(crate) original: OriginalPayload,
135125
}
136126

137127
impl 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)]
163161
pub struct MaybeInputsSeen {
164162
original: OriginalPayload,
165163
}
166164
impl 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)]
191185
pub struct OutputsUnknown {
192186
original: OriginalPayload,
193187
}
194188

195189
impl 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)]
280269
pub struct ProvisionalProposal {
281270
psbt_context: PsbtContext,
282271
}
283272

284273
impl 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)]
311304
pub struct PayjoinProposal {
312305
payjoin_psbt: Psbt,
313306
}
314307

315308
impl 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

Comments
 (0)