Skip to content

Commit 2761db1

Browse files
authored
Multiparty Senders: NS1R (payjoin#434)
Let a receiver batch sends from multiple peers into one transaction.
2 parents 8777c16 + 16aadbd commit 2761db1

File tree

14 files changed

+1106
-69
lines changed

14 files changed

+1106
-69
lines changed

payjoin-test-utils/src/lib.rs

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,42 +124,88 @@ pub fn local_cert_key() -> (Vec<u8>, Vec<u8>) {
124124
(cert_der, key_der)
125125
}
126126

127-
pub fn init_bitcoind_sender_receiver(
128-
sender_address_type: Option<AddressType>,
129-
receiver_address_type: Option<AddressType>,
130-
) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> {
127+
pub fn init_bitcoind() -> Result<bitcoind::BitcoinD, BoxError> {
131128
let bitcoind_exe = env::var("BITCOIND_EXE")
132129
.ok()
133130
.or_else(|| bitcoind::downloaded_exe_path().ok())
134131
.expect("bitcoind not found");
135132
let mut conf = bitcoind::Conf::default();
136133
conf.view_stdout = log_enabled!(Level::Debug);
137134
let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?;
138-
let receiver = bitcoind.create_wallet("receiver")?;
139-
let receiver_address = receiver.get_new_address(None, receiver_address_type)?.assume_checked();
140-
let sender = bitcoind.create_wallet("sender")?;
141-
let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked();
142-
bitcoind.client.generate_to_address(1, &receiver_address)?;
143-
bitcoind.client.generate_to_address(101, &sender_address)?;
144-
145-
assert_eq!(
146-
Amount::from_btc(50.0)?,
147-
receiver.get_balances()?.mine.trusted,
148-
"receiver doesn't own bitcoin"
149-
);
150-
151-
assert_eq!(
152-
Amount::from_btc(50.0)?,
153-
sender.get_balances()?.mine.trusted,
154-
"sender doesn't own bitcoin"
155-
);
156-
Ok((bitcoind, sender, receiver))
135+
Ok(bitcoind)
136+
}
137+
138+
pub fn init_bitcoind_sender_receiver(
139+
sender_address_type: Option<AddressType>,
140+
receiver_address_type: Option<AddressType>,
141+
) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> {
142+
let bitcoind = init_bitcoind()?;
143+
let mut wallets = create_and_fund_wallets(
144+
&bitcoind,
145+
vec![("receiver", receiver_address_type), ("sender", sender_address_type)],
146+
)?;
147+
let receiver = wallets.pop().expect("receiver to exist");
148+
let sender = wallets.pop().expect("sender to exist");
149+
150+
Ok((bitcoind, receiver, sender))
151+
}
152+
153+
fn create_and_fund_wallets<W: AsRef<str>>(
154+
bitcoind: &bitcoind::BitcoinD,
155+
wallets: Vec<(W, Option<AddressType>)>,
156+
) -> Result<Vec<bitcoincore_rpc::Client>, BoxError> {
157+
let mut funded_wallets = vec![];
158+
let funding_wallet = bitcoind.create_wallet("funding_wallet")?;
159+
let funding_address = funding_wallet.get_new_address(None, None)?.assume_checked();
160+
// 100 blocks would work here, we add a extra block to cover fees between transfers
161+
bitcoind.client.generate_to_address(101 + wallets.len() as u64, &funding_address)?;
162+
for (wallet_name, address_type) in wallets {
163+
let wallet = bitcoind.create_wallet(wallet_name)?;
164+
let address = wallet.get_new_address(None, address_type)?.assume_checked();
165+
funding_wallet.send_to_address(
166+
&address,
167+
Amount::from_btc(50.0)?,
168+
None,
169+
None,
170+
None,
171+
None,
172+
None,
173+
None,
174+
)?;
175+
funded_wallets.push(wallet);
176+
}
177+
// Mine the block which funds the different wallets
178+
bitcoind.client.generate_to_address(1, &funding_address)?;
179+
180+
for wallet in funded_wallets.iter() {
181+
let balances = wallet.get_balances()?;
182+
assert_eq!(
183+
balances.mine.trusted,
184+
Amount::from_btc(50.0)?,
185+
"wallet doesn't have expected amount of bitcoin"
186+
);
187+
}
188+
189+
Ok(funded_wallets)
157190
}
158191

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

196+
pub fn init_bitcoind_multi_sender_single_reciever(
197+
number_of_senders: usize,
198+
) -> Result<(bitcoind::BitcoinD, Vec<bitcoincore_rpc::Client>, bitcoincore_rpc::Client), BoxError> {
199+
let bitcoind = init_bitcoind()?;
200+
let wallets_to_create =
201+
(0..number_of_senders + 1).map(|i| (format!("sender_{}", i), None)).collect::<Vec<_>>();
202+
let mut wallets = create_and_fund_wallets(&bitcoind, wallets_to_create)?;
203+
let receiver = wallets.pop().expect("reciever to exist");
204+
let senders = wallets;
205+
206+
Ok((bitcoind, senders, receiver))
207+
}
208+
163209
fn http_agent_builder(cert_der: Vec<u8>) -> ClientBuilder {
164210
ClientBuilder::new().use_rustls_tls().add_root_certificate(
165211
reqwest::tls::Certificate::from_der(cert_der.as_slice())

payjoin/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ default = ["v2"]
2020
#[doc = "Core features for payjoin state machines"]
2121
_core = ["bitcoin/rand", "serde_json", "url", "bitcoin_uri"]
2222
directory = []
23-
psbt-merge = []
2423
v1 = ["_core"]
2524
v2 = ["_core", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "serde", "url/serde", "directory"]
2625
#[doc = "Functions to fetch OHTTP keys via CONNECT proxy using reqwest. Enables `v2` since only `v2` uses OHTTP."]
2726
io = ["v2", "reqwest/rustls-tls"]
2827
_danger-local-https = ["reqwest/rustls-tls", "rustls"]
28+
_multiparty = ["v2"]
2929

3030
[dependencies]
3131
bitcoin = { version = "0.32.5", features = ["base64"] }

payjoin/src/psbt/merge.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Utilities for merging unique v0 PSBTs
22
use bitcoin::Psbt;
33

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

17-
Psbt::from_unsigned_tx(unsigned_tx).expect("pulling from unsigned tx above")
16+
let mut merged_psbt =
17+
Psbt::from_unsigned_tx(unsigned_tx).expect("pulling from unsigned tx above");
18+
let zip = acc.inputs.iter().chain(psbt.inputs.iter()).collect::<Vec<_>>();
19+
merged_psbt.inputs.iter_mut().enumerate().for_each(|(i, input)| {
20+
input.witness_utxo = zip[i].witness_utxo.clone();
21+
});
22+
merged_psbt
1823
}
1924

2025
#[cfg(test)]

payjoin/src/psbt/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Utilities to make work with PSBTs easier
22
3-
#[cfg(feature = "psbt-merge")]
3+
#[cfg(feature = "_multiparty")]
44
pub(crate) mod merge;
55

66
use std::collections::BTreeMap;

payjoin/src/receive/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use crate::psbt::{InternalInputPair, InternalPsbtInputError, PsbtExt};
2525
mod error;
2626
pub(crate) mod optional_parameters;
2727

28+
#[cfg(feature = "_multiparty")]
29+
pub mod multiparty;
2830
#[cfg(feature = "v1")]
2931
#[cfg_attr(docsrs, doc(cfg(feature = "v1")))]
3032
pub mod v1;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use core::fmt;
2+
use std::error;
3+
4+
#[derive(Debug)]
5+
pub struct MultipartyError(InternalMultipartyError);
6+
7+
#[derive(Debug)]
8+
pub(crate) enum InternalMultipartyError {
9+
/// Not enough proposals
10+
NotEnoughProposals,
11+
/// Proposal version not supported
12+
ProposalVersionNotSupported(usize),
13+
/// Optimistic merge not supported
14+
OptimisticMergeNotSupported,
15+
/// Bitcoin Internal Error
16+
BitcoinExtractTxError(Box<bitcoin::psbt::ExtractTxError>),
17+
/// Input in Finalized Proposal is missing witness or script_sig
18+
InputMissingWitnessOrScriptSig,
19+
/// Failed to combine psbts
20+
FailedToCombinePsbts(bitcoin::psbt::Error),
21+
}
22+
23+
impl From<InternalMultipartyError> for MultipartyError {
24+
fn from(e: InternalMultipartyError) -> Self { MultipartyError(e) }
25+
}
26+
27+
impl fmt::Display for MultipartyError {
28+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29+
match &self.0 {
30+
InternalMultipartyError::NotEnoughProposals => write!(f, "Not enough proposals"),
31+
InternalMultipartyError::ProposalVersionNotSupported(v) =>
32+
write!(f, "Proposal version not supported: {}", v),
33+
InternalMultipartyError::OptimisticMergeNotSupported =>
34+
write!(f, "Optimistic merge not supported"),
35+
InternalMultipartyError::BitcoinExtractTxError(e) =>
36+
write!(f, "Bitcoin extract tx error: {:?}", e),
37+
InternalMultipartyError::InputMissingWitnessOrScriptSig =>
38+
write!(f, "Input in Finalized Proposal is missing witness or script_sig"),
39+
InternalMultipartyError::FailedToCombinePsbts(e) =>
40+
write!(f, "Failed to combine psbts: {:?}", e),
41+
}
42+
}
43+
}
44+
45+
impl error::Error for MultipartyError {
46+
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
47+
match &self.0 {
48+
InternalMultipartyError::NotEnoughProposals => None,
49+
InternalMultipartyError::ProposalVersionNotSupported(_) => None,
50+
InternalMultipartyError::OptimisticMergeNotSupported => None,
51+
InternalMultipartyError::BitcoinExtractTxError(e) => Some(e),
52+
InternalMultipartyError::InputMissingWitnessOrScriptSig => None,
53+
InternalMultipartyError::FailedToCombinePsbts(e) => Some(e),
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)