Skip to content

Commit

Permalink
Add support for multiple senders and one reciever (NS1R)
Browse files Browse the repository at this point in the history
The following is an expansion of the BIP77 protocol that allows for
multiple senders to opt in to a optimisitic merge with other potential
senders at a cost of an additional round of communication. The protocol
does not introduce new message types instead it re-uses the existing v2
structs. Everything touching NS1R is feature gated behind the
`multi_party` flag.

The remaining work is two fold:
1. Sender validation. v2 Senders have thorough validation on the payjoin PSBT
   they recieved . Some of the validation invalidates valid multiparty
responses. We would need to build and write unit tests for the multi
party sender validation.

2. Multi party version of `try_preserving_privacy`. Currently when the
   v2 reciever is assembling a payjoin it calls in the v1
`try_preserving_privacy` method which invalidates for txs with > 2
outputs. We would either need to relax this constraint or write a
bespoke version of `try_preserving_privacy` that optimizes for the
multiparty case. The latter is more favorable in my opnion.
  • Loading branch information
0xBEEFCAF3 committed Jan 26, 2025
1 parent d7a84cb commit 329e0e1
Show file tree
Hide file tree
Showing 13 changed files with 787 additions and 30 deletions.
39 changes: 39 additions & 0 deletions payjoin-test-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,45 @@ pub fn init_bitcoind_sender_receiver(
Ok((bitcoind, sender, receiver))
}

pub fn init_bitcoind_multi_sender_single_reciever(
number_of_senders: usize,
) -> Result<(bitcoind::BitcoinD, Vec<bitcoincore_rpc::Client>, bitcoincore_rpc::Client), BoxError> {
let (bitcoind, _sender, receiver) = init_bitcoind_sender_receiver(None, None)?;
let mut senders = vec![];

// to give the rest of the senders a predictable balance, lets create a specialized wallet and fund all the senders with the same amount
// The rest of the test suite has 50 BTC hardcoded, so lets stick with that
let funding_wallet_name = "funding_wallet";
let funding_wallet = bitcoind.create_wallet(funding_wallet_name)?;
let funding_address = funding_wallet.get_new_address(None, None)?.assume_checked();
bitcoind.client.generate_to_address(101 + number_of_senders as u64, &funding_address)?;

// Now lets fund the senders
for i in 0..number_of_senders {
let wallet_name = format!("sender_{}", i);
let sender = bitcoind.create_wallet(wallet_name.clone())?;
let address = sender.get_new_address(Some(&wallet_name), None)?.assume_checked();
// bitcoind.client.load_wallet(&funding_wallet_name).unwrap();
funding_wallet.send_to_address(
&address,
Amount::from_btc(50.0)?,
None,
None,
None,
None,
None,
None,
)?;
bitcoind.client.generate_to_address(1, &funding_address)?;

let balances = sender.get_balances()?;
assert_eq!(balances.mine.trusted, Amount::from_btc(50.0)?, "sender doesn't own bitcoin");
senders.push(sender);
}

Ok((bitcoind, senders, receiver))
}

pub fn http_agent(cert_der: Vec<u8>) -> Result<Client, BoxError> {
Ok(http_agent_builder(cert_der)?.build()?)
}
Expand Down
1 change: 1 addition & 0 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ v2 = ["_core", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "serde", "
#[doc = "Functions to fetch OHTTP keys via CONNECT proxy using reqwest. Enables `v2` since only `v2` uses OHTTP."]
io = ["v2", "reqwest/rustls-tls"]
_danger-local-https = ["reqwest/rustls-tls", "rustls"]
multi-party = ["v2"]

[dependencies]
bitcoin = { version = "0.32.5", features = ["base64"] }
Expand Down
2 changes: 2 additions & 0 deletions payjoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ mod uri;

#[cfg(feature = "base64")]
pub use bitcoin::base64;
#[cfg(all(feature = "v2", feature = "multi-party"))]
pub use receive::multi_party;
#[cfg(feature = "_core")]
pub use uri::{PjParseError, PjUri, Uri, UriExt};
#[cfg(feature = "_core")]
Expand Down
3 changes: 3 additions & 0 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ pub use crate::psbt::PsbtInputError;
use crate::psbt::{InternalInputPair, InternalPsbtInputError};

mod error;

pub(crate) mod optional_parameters;

#[cfg(all(feature = "v2", feature = "multi-party"))]
pub mod multi_party;
#[cfg(feature = "v1")]
pub mod v1;
#[cfg(not(feature = "v1"))]
Expand Down
62 changes: 62 additions & 0 deletions payjoin/src/receive/multi_party/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use core::fmt;
use std::error;

use crate::psbt;
#[derive(Debug)]
pub struct MultiPartyError(InternalMultiPartyError);

#[derive(Debug)]
pub(crate) enum InternalMultiPartyError {
/// Failed to merge proposals
FailedToMergeProposals(Vec<psbt::merge::MergePsbtError>),
/// Not enough proposals
NotEnoughProposals,
/// Proposal version not supported
ProposalVersionNotSupported(usize),
/// Optimistic merge not supported
OptimisticMergeNotSupported,
/// Bitcoin Internal Error
BitcoinExtractTxError(bitcoin::psbt::ExtractTxError),
/// Input in Finalized Proposal is missing witness or script_sig
InputMissingWitnessOrScriptSig,
/// Failed to combine psbts
FailedToCombinePsbts(bitcoin::psbt::Error),
}

impl From<InternalMultiPartyError> for MultiPartyError {
fn from(e: InternalMultiPartyError) -> Self { MultiPartyError(e) }
}

impl fmt::Display for MultiPartyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
InternalMultiPartyError::FailedToMergeProposals(e) =>
write!(f, "Failed to merge proposals: {:?}", e),
InternalMultiPartyError::NotEnoughProposals => write!(f, "Not enough proposals"),
InternalMultiPartyError::ProposalVersionNotSupported(v) =>
write!(f, "Proposal version not supported: {}", v),
InternalMultiPartyError::OptimisticMergeNotSupported =>
write!(f, "Optimistic merge not supported"),
InternalMultiPartyError::BitcoinExtractTxError(e) =>
write!(f, "Bitcoin extract tx error: {:?}", e),
InternalMultiPartyError::InputMissingWitnessOrScriptSig =>
write!(f, "Input in Finalized Proposal is missing witness or script_sig"),
InternalMultiPartyError::FailedToCombinePsbts(e) =>
write!(f, "Failed to combine psbts: {:?}", e),
}
}
}

impl error::Error for MultiPartyError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.0 {
InternalMultiPartyError::FailedToMergeProposals(_) => None, // Vec<MergePsbtError> doesn't implement Error
InternalMultiPartyError::NotEnoughProposals => None,
InternalMultiPartyError::ProposalVersionNotSupported(_) => None,
InternalMultiPartyError::OptimisticMergeNotSupported => None,
InternalMultiPartyError::BitcoinExtractTxError(e) => Some(e),
InternalMultiPartyError::InputMissingWitnessOrScriptSig => None,
InternalMultiPartyError::FailedToCombinePsbts(e) => Some(e),
}
}
}
Loading

0 comments on commit 329e0e1

Please sign in to comment.