Skip to content

Commit e5d1855

Browse files
committed
Move extract_fallback_tx to MaybeInputsOwned
Per BIP-77: > At any point, either party may choose to broadcast the fallback > transaction described by the Original PSBT instead of proceeding. However, the fallback transaction available at the `UncheckedProposal` typestate may not be "broadcastable". Only after transitioning to `MaybeInputsOwned` via `check_broadcast_suitability` can we determine with high confidence that the fallback is valid for broadcast. Related PR: [payjoin#799](payjoin#799)
1 parent c384ad5 commit e5d1855

File tree

8 files changed

+36
-44
lines changed

8 files changed

+36
-44
lines changed

payjoin-cli/src/app/v1.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,14 +296,14 @@ impl App {
296296
) -> Result<PayjoinProposal, ReplyableError> {
297297
let wallet = self.wallet();
298298

299-
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
300-
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
301-
302299
// Receive Check 1: Can Broadcast
303300
let proposal =
304301
proposal.check_broadcast_suitability(None, |tx| Ok(wallet.can_broadcast(tx)?))?;
305302
log::trace!("check1");
306303

304+
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
305+
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
306+
307307
// Receive Check 2: receiver can't sign for proposal inputs
308308
let proposal = proposal.check_inputs_not_owned(|input| Ok(wallet.is_mine(input)?))?;
309309
log::trace!("check2");

payjoin-cli/src/app/v2/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,6 @@ impl App {
342342
return Err(anyhow!("Interrupted"));
343343
}
344344
}?;
345-
println!("Fallback transaction received. Consider broadcasting this to get paid if the Payjoin fails:");
346-
println!("{}", serialize_hex(&receiver.extract_tx_to_schedule_broadcast()));
347345
self.check_proposal(receiver, persister).await
348346
}
349347

@@ -357,6 +355,8 @@ impl App {
357355
.check_broadcast_suitability(None, |tx| Ok(wallet.can_broadcast(tx)?))
358356
.save(persister)?;
359357

358+
println!("Fallback transaction received. Consider broadcasting this to get paid if the Payjoin fails:");
359+
println!("{}", serialize_hex(&proposal.extract_tx_to_schedule_broadcast()));
360360
self.check_inputs_not_owned(proposal, persister).await
361361
}
362362

payjoin-ffi/src/receive/mod.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,6 @@ impl AssumeInteractiveTransition {
331331
}
332332

333333
impl UncheckedProposal {
334-
///The Sender’s Original PSBT
335-
pub fn extract_tx_to_schedule_broadcast(&self) -> Vec<u8> {
336-
payjoin::bitcoin::consensus::encode::serialize(
337-
&self.0.clone().extract_tx_to_schedule_broadcast(),
338-
)
339-
}
340-
341334
pub fn check_broadcast_suitability(
342335
&self,
343336
min_fee_rate: Option<u64>,
@@ -413,6 +406,12 @@ impl MaybeInputsOwnedTransition {
413406
}
414407

415408
impl MaybeInputsOwned {
409+
///The Sender’s Original PSBT
410+
pub fn extract_tx_to_schedule_broadcast(&self) -> Vec<u8> {
411+
payjoin::bitcoin::consensus::encode::serialize(
412+
&self.0.clone().extract_tx_to_schedule_broadcast(),
413+
)
414+
}
416415
pub fn check_inputs_not_owned(
417416
&self,
418417
is_owned: impl Fn(&Vec<u8>) -> Result<bool, ImplementationError>,

payjoin-ffi/src/receive/uni.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,6 @@ impl AssumeInteractiveTransition {
343343
}
344344
#[uniffi::export]
345345
impl UncheckedProposal {
346-
/// The Sender’s Original PSBT
347-
pub fn extract_tx_to_schedule_broadcast(&self) -> Vec<u8> {
348-
self.0.extract_tx_to_schedule_broadcast()
349-
}
350-
351346
/// Call after checking that the Original PSBT can be broadcast.
352347
///
353348
/// Receiver MUST check that the Original PSBT from the sender can be broadcast, i.e. testmempoolaccept bitcoind rpc returns { “allowed”: true,.. } for get_transaction_to_check_broadcast() before calling this method.
@@ -418,6 +413,10 @@ impl MaybeInputsOwnedTransition {
418413

419414
#[uniffi::export]
420415
impl MaybeInputsOwned {
416+
/// The Sender’s Original PSBT
417+
pub fn extract_tx_to_schedule_broadcast(&self) -> Vec<u8> {
418+
self.0.extract_tx_to_schedule_broadcast()
419+
}
421420
///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs.
422421
/// An attacker could try to spend receiver's own inputs. This check prevents that.
423422
pub fn check_inputs_not_owned(

payjoin-ffi/tests/bdk_integration_test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,15 +359,15 @@ mod v2 {
359359

360360
fn handle_directory_proposal(receiver: Wallet, proposal: UncheckedProposal) -> PayjoinProposal {
361361
let session_persister = NoopSessionPersister::default();
362-
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
363-
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
364-
365362
// Receive Check 1: Can Broadcast
366363
let proposal = proposal
367364
.assume_interactive_receiver()
368365
.save(&session_persister)
369366
.expect("Noop Persister should not fail");
370367
let receiver = Arc::new(receiver);
368+
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
369+
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
370+
371371
// Receive Check 2: receiver can't sign for proposal inputs
372372
let proposal = proposal
373373
.check_inputs_not_owned(|script| is_script_owned(&receiver, script.clone()))

payjoin/src/receive/v1/mod.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,27 +54,18 @@ pub use exclusive::*;
5454
/// This type is used to process the request. It is returned by
5555
/// [`UncheckedProposal::from_request()`]
5656
///
57-
/// If you are implementing an interactive payment processor, you should get extract the original
58-
/// transaction with extract_tx_to_schedule_broadcast() and schedule, followed by checking
59-
/// that the transaction can be broadcast with check_broadcast_suitability. Otherwise it is safe to
60-
/// call assume_interactive_receive to proceed with validation.
6157
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6258
pub struct UncheckedProposal {
6359
pub(crate) psbt: Psbt,
6460
pub(crate) params: Params,
6561
}
6662

6763
impl UncheckedProposal {
68-
/// The Sender's Original PSBT transaction
69-
pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction {
70-
self.psbt.clone().extract_tx_unchecked_fee_rate()
71-
}
72-
7364
fn psbt_fee_rate(&self) -> Result<FeeRate, InternalPayloadError> {
7465
let original_psbt_fee = self.psbt.fee().map_err(|e| {
7566
InternalPayloadError::ParsePsbt(bitcoin::psbt::PsbtParseError::PsbtEncoding(e))
7667
})?;
77-
Ok(original_psbt_fee / self.extract_tx_to_schedule_broadcast().weight())
68+
Ok(original_psbt_fee / self.psbt.clone().extract_tx_unchecked_fee_rate().weight())
7869
}
7970

8071
/// Check that the Original PSBT can be broadcasted.
@@ -129,13 +120,19 @@ impl UncheckedProposal {
129120
/// Typestate to validate that the Original PSBT has no receiver-owned inputs.
130121
///
131122
/// Call [`Self::check_inputs_not_owned`] to proceed.
123+
/// If you are implementing an interactive payment processor, you should get extract the original
124+
/// transaction with extract_tx_to_schedule_broadcast() and schedule
132125
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133126
pub struct MaybeInputsOwned {
134127
psbt: Psbt,
135128
params: Params,
136129
}
137130

138131
impl MaybeInputsOwned {
132+
/// The Sender's Original PSBT transaction
133+
pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction {
134+
self.psbt.clone().extract_tx_unchecked_fee_rate()
135+
}
139136
/// Check that the Original PSBT has no receiver-owned inputs.
140137
/// Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs.
141138
///

payjoin/src/receive/v2/mod.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,6 @@ impl Receiver<WithContext> {
426426
/// This type is used to process the request. It is returned by
427427
/// [`Receiver::process_res()`].
428428
///
429-
/// If you are implementing an interactive payment processor, you should get extract the original
430-
/// transaction with extract_tx_to_schedule_broadcast() and schedule, followed by checking
431-
/// that the transaction can be broadcast with check_broadcast_suitability. Otherwise it is safe to
432-
/// call assume_interactive_receive to proceed with validation.
433429
#[derive(Debug, Clone, PartialEq)]
434430
pub struct UncheckedProposal {
435431
pub(crate) v1: v1::UncheckedProposal,
@@ -439,11 +435,6 @@ pub struct UncheckedProposal {
439435
impl ReceiverState for UncheckedProposal {}
440436

441437
impl Receiver<UncheckedProposal> {
442-
/// The Sender's Original PSBT
443-
pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction {
444-
self.v1.extract_tx_to_schedule_broadcast()
445-
}
446-
447438
/// Call after checking that the Original PSBT can be broadcast.
448439
///
449440
/// Receiver MUST check that the Original PSBT from the sender
@@ -504,6 +495,8 @@ impl Receiver<UncheckedProposal> {
504495
/// Typestate to validate that the Original PSBT has no receiver-owned inputs.
505496
///
506497
/// Call [`Receiver<MaybeInputsOwned>::check_inputs_not_owned`] to proceed.
498+
/// If you are implementing an interactive payment processor, you should get extract the original
499+
/// transaction with extract_tx_to_schedule_broadcast() and schedule
507500
#[derive(Debug, Clone, PartialEq)]
508501
pub struct MaybeInputsOwned {
509502
v1: v1::MaybeInputsOwned,
@@ -513,6 +506,10 @@ pub struct MaybeInputsOwned {
513506
impl ReceiverState for MaybeInputsOwned {}
514507

515508
impl Receiver<MaybeInputsOwned> {
509+
/// The Sender's Original PSBT
510+
pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction {
511+
self.v1.extract_tx_to_schedule_broadcast()
512+
}
516513
/// Check that the Original PSBT has no receiver-owned inputs.
517514
/// Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs.
518515
///

payjoin/tests/integration.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -743,8 +743,6 @@ mod integration {
743743
custom_inputs: Option<Vec<InputPair>>,
744744
) -> Result<Receiver<PayjoinProposal>, BoxError> {
745745
let noop_persister = NoopSessionPersister::default();
746-
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
747-
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
748746

749747
// Receive Check 1: Can Broadcast
750748
let proposal = proposal
@@ -759,6 +757,9 @@ mod integration {
759757
})
760758
.save(&noop_persister)?;
761759

760+
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
761+
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
762+
762763
// Receive Check 2: receiver can't sign for proposal inputs
763764
let proposal = proposal
764765
.check_inputs_not_owned(|input| {
@@ -1377,9 +1378,6 @@ mod integration {
13771378
drain_script: Option<&bitcoin::Script>,
13781379
custom_inputs: Option<Vec<InputPair>>,
13791380
) -> Result<payjoin::receive::v1::PayjoinProposal, BoxError> {
1380-
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
1381-
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
1382-
13831381
// Receive Check 1: Can Broadcast
13841382
let proposal = proposal.check_broadcast_suitability(None, |tx| {
13851383
Ok(receiver
@@ -1388,6 +1386,8 @@ mod integration {
13881386
.ok_or(ImplementationError::from("testmempoolaccept should return a result"))?
13891387
.allowed)
13901388
})?;
1389+
// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
1390+
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
13911391

13921392
// Receive Check 2: receiver can't sign for proposal inputs
13931393
let proposal = proposal.check_inputs_not_owned(|input| {

0 commit comments

Comments
 (0)