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 Feb 8, 2025
1 parent cef3d52 commit ccc4ba9
Show file tree
Hide file tree
Showing 13 changed files with 807 additions and 52 deletions.
94 changes: 71 additions & 23 deletions payjoin-test-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,42 +124,90 @@ pub fn local_cert_key() -> (Vec<u8>, Vec<u8>) {
(cert_der, key_der)
}

pub fn init_bitcoind_sender_receiver(
sender_address_type: Option<AddressType>,
receiver_address_type: Option<AddressType>,
) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> {
pub fn init_bitcoind() -> Result<bitcoind::BitcoinD, BoxError> {
let bitcoind_exe = env::var("BITCOIND_EXE")
.ok()
.or_else(|| bitcoind::downloaded_exe_path().ok())
.expect("bitcoind not found");
let mut conf = bitcoind::Conf::default();
conf.view_stdout = log_enabled!(Level::Debug);
let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?;
let receiver = bitcoind.create_wallet("receiver")?;
let receiver_address = receiver.get_new_address(None, receiver_address_type)?.assume_checked();
let sender = bitcoind.create_wallet("sender")?;
let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked();
bitcoind.client.generate_to_address(1, &receiver_address)?;
bitcoind.client.generate_to_address(101, &sender_address)?;

assert_eq!(
Amount::from_btc(50.0)?,
receiver.get_balances()?.mine.trusted,
"receiver doesn't own bitcoin"
);

assert_eq!(
Amount::from_btc(50.0)?,
sender.get_balances()?.mine.trusted,
"sender doesn't own bitcoin"
);
Ok((bitcoind, sender, receiver))
Ok(bitcoind)
}

pub fn init_bitcoind_sender_receiver(
sender_address_type: Option<AddressType>,
receiver_address_type: Option<AddressType>,
) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> {
let bitcoind = init_bitcoind()?;
// let receiver = bitcoind.create_wallet("receiver")?;
// let receiver_address = receiver.get_new_address(None, receiver_address_type)?.assume_checked();
// let sender = bitcoind.create_wallet("sender")?;
// let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked();
// bitcoind.client.generate_to_address(1, &receiver_address)?;
let wallets = create_and_wallets(
&bitcoind,
vec![
("receiver".to_string(), receiver_address_type),
("sender".to_string(), sender_address_type),
],
)?;
let mut wallets = wallets;
let receiver = wallets.remove(0);
let sender = wallets.remove(0);

Ok((bitcoind, receiver, sender))
}

fn create_and_wallets(
bitcoind: &bitcoind::BitcoinD,
wallets: Vec<(String, Option<AddressType>)>,
) -> Result<Vec<bitcoincore_rpc::Client>, BoxError> {
let mut funded_wallets = vec![];
let funding_wallet = bitcoind.create_wallet("funding_wallet")?;
let funding_address = funding_wallet.get_new_address(None, None)?.assume_checked();
bitcoind.client.generate_to_address(101 + wallets.len() as u64, &funding_address)?;
for (wallet_name, address_type) in wallets {
let wallet = bitcoind.create_wallet(wallet_name)?;
let address = wallet.get_new_address(None, address_type)?.assume_checked();
funding_wallet.send_to_address(
&address,
Amount::from_btc(50.0)?,
None,
None,
None,
None,
None,
None,
)?;
funded_wallets.push(wallet);
}
bitcoind.client.generate_to_address(1, &funding_address)?;
for wallet in funded_wallets.iter() {
let balances = wallet.get_balances()?;
assert_eq!(balances.mine.trusted, Amount::from_btc(50.0)?, "wallet doesn't own bitcoin");
}

Ok(funded_wallets)
}

pub fn http_agent(cert_der: Vec<u8>) -> Result<Client, BoxSendSyncError> {
Ok(http_agent_builder(cert_der).build()?)
}

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 = init_bitcoind()?;
let wallets_to_create =
(0..number_of_senders + 1).map(|i| (format!("sender_{}", i), None)).collect::<Vec<_>>();
let mut wallets = create_and_wallets(&bitcoind, wallets_to_create)?;
let receiver = wallets.pop().expect("reciever to exist");
let senders = wallets;

Ok((bitcoind, senders, receiver))
}

fn http_agent_builder(cert_der: Vec<u8>) -> ClientBuilder {
ClientBuilder::new().danger_accept_invalid_certs(true).use_rustls_tls().add_root_certificate(
reqwest::tls::Certificate::from_der(cert_der.as_slice())
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
9 changes: 7 additions & 2 deletions payjoin/src/psbt/merge.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Utilities for merging unique v0 PSBTs
use bitcoin::Psbt;

#[allow(dead_code)]
/// Try to merge two PSBTs
/// PSBTs here should not have the same unsigned tx
/// if you do have the same unsigned tx, use `combine` instead
Expand All @@ -14,7 +13,13 @@ pub(crate) fn merge_unsigned_tx(acc: Psbt, psbt: Psbt) -> Psbt {
unsigned_tx.input.dedup_by_key(|input| input.previous_output);
unsigned_tx.output.extend(psbt.unsigned_tx.output);

Psbt::from_unsigned_tx(unsigned_tx).expect("pulling from unsigned tx above")
let mut merged_psbt =
Psbt::from_unsigned_tx(unsigned_tx).expect("pulling from unsigned tx above");
let zip = acc.inputs.iter().chain(psbt.inputs.iter()).collect::<Vec<_>>();
merged_psbt.inputs.iter_mut().enumerate().for_each(|(i, input)| {
input.witness_utxo = zip[i].witness_utxo.clone();
});
merged_psbt
}

#[cfg(test)]
Expand Down
2 changes: 2 additions & 0 deletions payjoin/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use crate::psbt::{InternalInputPair, InternalPsbtInputError};
mod error;
pub(crate) mod optional_parameters;

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

#[derive(Debug)]
pub struct MultiPartyError(InternalMultiPartyError);

#[derive(Debug)]
pub(crate) enum InternalMultiPartyError {
/// 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::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::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 ccc4ba9

Please sign in to comment.