Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
cargo update -p log --precise 0.4.18
cargo update -p tempfile --precise 3.6.0
cargo update -p flate2 --precise 1.0.26
cargo update -p minreq --precise 2.8.0
cargo update -p rustls --precise 0.20.8
- name: test
run: cargo test --verbose --all-features --lib

Expand Down Expand Up @@ -74,4 +74,4 @@ jobs:
- name: fmt check
run: |
cd ${{ matrix.package }}
cargo fmt --all -- --check
cargo fmt --all -- --check
2 changes: 1 addition & 1 deletion payjoin-cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ impl App {
)?;

// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast();
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();

// The network is used for checks later
let network =
Expand Down
7 changes: 7 additions & 0 deletions payjoin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Payjoin Changelog

## 0.10.0

- Export `base64` with feature by @jbesraa in #102
- Improve `receive` api with `ProvisionalProposal`by @jbesraa in #90
- Document `create_pj_request` by @jbesraa in #87
- Add BIP 78 reccommended fee `Configuration` by @DanGould in #86

## 0.9.0

Bumping `bitcoin` and other crates was a breaking api change. This is a 0.8.1 semver re-release.
Expand Down
2 changes: 1 addition & 1 deletion payjoin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "payjoin"
version = "0.9.0"
version = "0.10.0"
authors = ["Dan Gould <dan@chaincase.app>"]
description = "Payjoin Library for the BIP78 Pay to Endpoint protocol."
repository = "https://github.com/payjoin/rust-payjoin"
Expand Down
113 changes: 54 additions & 59 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
//!
//! ```
//! // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
//! let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast();
//! let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();
//!
//! // The network is used for checks later
//! let network = match bitcoind.get_blockchain_info()?.chain.as_str() {
Expand Down Expand Up @@ -268,7 +268,6 @@

use std::cmp::{max, min};
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;

use bitcoin::psbt::Psbt;
use bitcoin::{Amount, FeeRate, OutPoint, Script, TxOut};
Expand All @@ -295,38 +294,14 @@ pub trait Headers {
/// [`UncheckedProposal::from_request()`](crate::receive::UncheckedProposal::from_request()).
///
/// If you are implementing an interactive payment processor, you should get extract the original
/// transaction with get_transaction_to_schedule_broadcast() and schedule, followed by checking
/// transaction with extract_tx_to_schedule_broadcast() and schedule, followed by checking
/// that the transaction can be broadcast with check_can_broadcast. Otherwise it is safe to
/// call assume_interactive_receive to proceed with validation.
pub struct UncheckedProposal {
psbt: Psbt,
params: Params,
}

/// Typestate to validate that the Original PSBT has no receiver-owned inputs.
///
/// Call [`check_no_receiver_owned_inputs()`](struct.UncheckedProposal.html#method.check_no_receiver_owned_inputs) to proceed.
pub struct MaybeInputsOwned {
psbt: Psbt,
params: Params,
}

/// Typestate to validate that the Original PSBT has no mixed input types.
///
/// Call [`check_no_mixed_input_types`](struct.UncheckedProposal.html#method.check_no_mixed_input_scripts) to proceed.
pub struct MaybeMixedInputScripts {
psbt: Psbt,
params: Params,
}

/// Typestate to validate that the Original PSBT has no inputs that have been seen before.
///
/// Call [`check_no_inputs_seen`](struct.MaybeInputsSeen.html#method.check_no_inputs_seen_before) to proceed.
pub struct MaybeInputsSeen {
psbt: Psbt,
params: Params,
}

impl UncheckedProposal {
pub fn from_request(
mut body: impl std::io::Read,
Expand Down Expand Up @@ -368,15 +343,15 @@ impl UncheckedProposal {
}

/// The Sender's Original PSBT
pub fn get_transaction_to_schedule_broadcast(&self) -> bitcoin::Transaction {
pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction {
self.psbt.clone().extract_tx()
}

/// 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 `get_transaction_to_check_broadcast()` before calling this method.
/// for `extract_tx_to_sheculed_broadcast()` before calling this method.
///
/// 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.
Expand All @@ -399,12 +374,20 @@ impl UncheckedProposal {
/// requires manual intervention, as in most consumer wallets.
///
/// So-called "non-interactive" receivers, like payment processors, that allow arbitrary requests are otherwise vulnerable to probing attacks.
/// Those receivers call `get_transaction_to_check_broadcast()` and `attest_tested_and_scheduled_broadcast()` after making those checks downstream.
/// Those receivers call `extract_tx_to_check_broadcast()` and `attest_tested_and_scheduled_broadcast()` after making those checks downstream.
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.
///
/// Call [`check_no_receiver_owned_inputs()`](struct.UncheckedProposal.html#method.check_no_receiver_owned_inputs) to proceed.
pub struct MaybeInputsOwned {
psbt: Psbt,
params: Params,
}

impl MaybeInputsOwned {
/// Check that the Original PSBT has no receiver-owned inputs.
/// Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs.
Expand Down Expand Up @@ -440,6 +423,14 @@ impl MaybeInputsOwned {
}
}

/// Typestate to validate that the Original PSBT has no mixed input types.
///
/// Call [`check_no_mixed_input_types`](struct.UncheckedProposal.html#method.check_no_mixed_input_scripts) to proceed.
pub struct MaybeMixedInputScripts {
psbt: Psbt,
params: Params,
}

impl MaybeMixedInputScripts {
/// Verify the original transaction did not have mixed input types
/// Call this after checking downstream.
Expand Down Expand Up @@ -484,6 +475,13 @@ impl MaybeMixedInputScripts {
}
}

/// Typestate to validate that the Original PSBT has no inputs that have been seen before.
///
/// Call [`check_no_inputs_seen`](struct.MaybeInputsSeen.html#method.check_no_inputs_seen_before) to proceed.
pub struct MaybeInputsSeen {
psbt: Psbt,
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
Expand Down Expand Up @@ -550,27 +548,6 @@ impl OutputsUnknown {
}
}

/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin.
pub struct PayjoinProposal {
payjoin_psbt: Psbt,
params: Params,
owned_vouts: Vec<usize>,
}

impl PayjoinProposal {
pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> {
self.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
}

pub fn is_output_substitution_disabled(&self) -> bool {
self.params.disable_output_substitution
}

pub fn get_owned_vouts(&self) -> &Vec<usize> { &self.owned_vouts }

pub fn psbt(&self) -> &Psbt { &self.payjoin_psbt }
}

/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin.
pub struct ProvisionalProposal {
original_psbt: Psbt,
Expand Down Expand Up @@ -814,15 +791,33 @@ impl ProvisionalProposal {
min_feerate_sat_per_vb: Option<FeeRate>,
) -> Result<PayjoinProposal, Error> {
let psbt = self.apply_fee(min_feerate_sat_per_vb)?;
let psbt = wallet_process_psbt(psbt).map_err(|e| {
log::error!("wallet_process_psbt error");
Error::from(e)
})?;
let payjoin_proposal = self.prepare_psbt(psbt).map_err(RequestError::from)?;
let psbt = wallet_process_psbt(psbt)?;
let payjoin_proposal = self.prepare_psbt(psbt)?;
Ok(payjoin_proposal)
}
}

/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin.
pub struct PayjoinProposal {
payjoin_psbt: Psbt,
params: Params,
owned_vouts: Vec<usize>,
}

impl PayjoinProposal {
pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> {
self.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
}

pub fn is_output_substitution_disabled(&self) -> bool {
self.params.disable_output_substitution
}

pub fn owned_vouts(&self) -> &Vec<usize> { &self.owned_vouts }

pub fn psbt(&self) -> &Psbt { &self.payjoin_psbt }
}

#[cfg(test)]
mod test {
use super::*;
Expand All @@ -846,7 +841,7 @@ mod test {
}
}

fn get_proposal_from_test_vector() -> Result<UncheckedProposal, RequestError> {
fn proposal_from_test_vector() -> Result<UncheckedProposal, RequestError> {
// OriginalPSBT Test Vector from BIP
// | InputScriptType | Orginal PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex|
// |-----------------|-----------------------|------------------------------|-------------------------|
Expand All @@ -864,7 +859,7 @@ mod test {

#[test]
fn can_get_proposal_from_request() {
let proposal = get_proposal_from_test_vector();
let proposal = proposal_from_test_vector();
assert!(proposal.is_ok(), "OriginalPSBT should be a valid request");
}

Expand All @@ -874,7 +869,7 @@ mod test {

use bitcoin::{Address, Network};

let proposal = get_proposal_from_test_vector().unwrap();
let proposal = proposal_from_test_vector().unwrap();
let mut payjoin = proposal
.assume_interactive_receiver()
.check_inputs_not_owned(|_| Ok(false))
Expand Down
2 changes: 1 addition & 1 deletion payjoin/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ mod integration {
.unwrap();

// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
let _to_broadcast_in_failure_case = proposal.get_transaction_to_schedule_broadcast();
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();

// Receive Check 1: Can Broadcast
let proposal = proposal
Expand Down