Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiparty Senders: NS1R #434

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
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
234 changes: 234 additions & 0 deletions payjoin/src/psbt/merge/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
use std::fmt;

use bitcoin::{OutPoint, Psbt};

/// Error type for merging two unique unsigned PSBTs
#[derive(Debug, PartialEq)]
pub(crate) enum MergePsbtError {
/// Input from other PSBT already exists in this PSBT
InputAlreadyExists(OutPoint),
/// Input is already signed
MyInputIsSigned(OutPoint),
/// Other PSBT's input is already signed
OtherInputIsSigned(OutPoint),
}

impl fmt::Display for MergePsbtError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InputAlreadyExists(outpoint) =>
write!(f, "input already exists with outpoint: {}", outpoint),
Self::MyInputIsSigned(outpoint) =>
write!(f, "my input is already signed with outpoint: {}", outpoint),
Self::OtherInputIsSigned(outpoint) =>
write!(f, "other input is already signed with outpoint: {}", outpoint),
}
}
}

impl std::error::Error for MergePsbtError {}

pub(crate) trait MergePsbtExt: Sized {
fn dangerous_clear_signatures(&mut self);
fn merge_unsigned_tx(&mut self, other: Self) -> Result<(), Vec<MergePsbtError>>;
}

impl MergePsbtExt for Psbt {
/// Clear all script sig and witness fields from this PSBT
fn dangerous_clear_signatures(&mut self) {
for input in self.inputs.iter_mut() {
input.final_script_sig = None;
input.final_script_witness = None;
}
}

/// Try to merge two PSBTs
/// PSBTs here are assumed to not have the same unsigned tx
/// if you do have the same unsigned tx, use `combine` instead
/// Note this method does not merge non inputs or outputs
fn merge_unsigned_tx(&mut self, other: Self) -> Result<(), Vec<MergePsbtError>> {
let mut errors = Vec::new();
for (input, txin) in self.inputs.iter().zip(self.unsigned_tx.input.iter()) {
if input.final_script_sig.is_some() || input.final_script_witness.is_some() {
errors.push(MergePsbtError::MyInputIsSigned(txin.previous_output));
}
}

// Do the same for the other PSBT down below
let mut inputs_to_add = Vec::with_capacity(other.inputs.len());
let mut txins_to_add = Vec::with_capacity(other.inputs.len());
for (other_input, other_txin) in other.inputs.iter().zip(other.unsigned_tx.input.iter()) {
if self.unsigned_tx.input.contains(other_txin) {
errors.push(MergePsbtError::InputAlreadyExists(other_txin.previous_output));
}

if other_input.final_script_sig.is_some() || other_input.final_script_witness.is_some()
{
errors.push(MergePsbtError::OtherInputIsSigned(other_txin.previous_output));
continue;
}

inputs_to_add.push(other_input.clone());
txins_to_add.push(other_txin.clone());
}

let mut outputs_to_add = Vec::with_capacity(other.outputs.len());
let mut txouts_to_add = Vec::with_capacity(other.outputs.len());
for (other_output, other_txout) in other.outputs.iter().zip(other.unsigned_tx.output.iter())
{
// TODO(armins) if we recognize the exact same output this is a not neccecarily an error but an indication for an improved tx structure
outputs_to_add.push(other_output.clone());
txouts_to_add.push(other_txout.clone());
}

if !errors.is_empty() {
return Err(errors);
}

self.inputs.extend(inputs_to_add);
self.unsigned_tx.input.extend(txins_to_add);
self.outputs.extend(outputs_to_add);
self.unsigned_tx.output.extend(txouts_to_add);

Ok(())
}
}

// Tests
#[cfg(test)]
mod tests {
use bitcoin::absolute::LockTime;
use bitcoin::hashes::Hash;
use bitcoin::key::rand::Rng;
use bitcoin::secp256k1::rand::thread_rng;
use bitcoin::secp256k1::SECP256K1;
use bitcoin::{
Amount, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
Witness,
};

use crate::psbt::merge::{MergePsbtError, MergePsbtExt};

/// Util function to create a random p2wpkh script
pub fn random_p2wpkh_script() -> ScriptBuf {
let sk = bitcoin::PrivateKey::generate(Network::Bitcoin);
let pk = sk.public_key(SECP256K1);

pk.p2wpkh_script_code().unwrap()
}

/// Util function to create a random txid
pub fn random_txid() -> Txid {
let mut rng = thread_rng();
let mut txid = [0u8; 32];
rng.try_fill(&mut txid).expect("should fill");
Txid::from_slice(&txid).unwrap()
}

// Util function to create a btc tx with random inputs and outputs as defined by fn params
fn create_tx(num_inputs: usize, num_outputs: usize) -> Transaction {
let txid = random_txid();

let mut inputs = vec![];
for i in 0..num_inputs {
let op = OutPoint::new(txid, i as u32);
inputs.push(TxIn {
previous_output: op,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Default::default(),
});
}

let mut outputs = vec![];
for _ in 0..num_outputs {
outputs.push(TxOut {
value: Amount::from_sat(1000),
script_pubkey: random_p2wpkh_script(),
});
}

Transaction {
version: bitcoin::transaction::Version(2),
lock_time: LockTime::ZERO,
input: inputs,
output: outputs,
}
}

#[test]
pub fn test_clear_signatures() {
let mut psbt = Psbt::from_unsigned_tx(create_tx(1, 1)).unwrap();
psbt.inputs[0].final_script_sig = Some(ScriptBuf::new());
psbt.inputs[0].final_script_witness = Some(Witness::new());

psbt.dangerous_clear_signatures();
assert_eq!(psbt.inputs[0].final_script_sig, None);
assert_eq!(psbt.inputs[0].final_script_witness, None);
}
#[test]
fn test_merge_unsigned_tx() {
let tx_1 = create_tx(1, 1);
let tx_2 = create_tx(1, 1);
let original_psbt = Psbt::from_unsigned_tx(tx_1).unwrap();
let mut merged_psbt = original_psbt.clone();
let other = Psbt::from_unsigned_tx(tx_2).unwrap();
merged_psbt.merge_unsigned_tx(other.clone()).expect("should merge two unique psbts");

assert_eq!(merged_psbt.inputs[0], original_psbt.inputs[0]);
assert_eq!(merged_psbt.inputs[1], other.inputs[0]);
assert_eq!(merged_psbt.outputs[0], original_psbt.outputs[0]);
assert_eq!(merged_psbt.outputs[1], other.outputs[0]);

// Assert unsigned tx is also updated
let merged_tx = merged_psbt.unsigned_tx.clone();
assert_eq!(merged_tx.input[0], original_psbt.unsigned_tx.input[0]);
assert_eq!(merged_tx.input[1], other.unsigned_tx.input[0]);
assert_eq!(merged_tx.output[0], original_psbt.unsigned_tx.output[0]);
assert_eq!(merged_tx.output[1], other.unsigned_tx.output[0]);
}

#[test]
fn should_not_merge_if_psbt_share_inputs() {
let tx_1 = create_tx(1, 1);
let original_psbt = Psbt::from_unsigned_tx(tx_1.clone()).unwrap();
let mut merged_psbt = original_psbt.clone();
let other = original_psbt.clone();

let res = merged_psbt.merge_unsigned_tx(other.clone());
assert!(res.is_err());
assert_eq!(
res.err().unwrap()[0],
MergePsbtError::InputAlreadyExists(tx_1.input[0].previous_output)
);
// ensure the psbt has not been modified
assert_eq!(merged_psbt, original_psbt);
}

#[test]
fn should_not_merge_signed_psbt() {
let tx_1 = create_tx(1, 1);
let tx_2 = create_tx(1, 1);
let mut original_psbt = Psbt::from_unsigned_tx(tx_1.clone()).unwrap();
let mut other = Psbt::from_unsigned_tx(tx_2.clone()).unwrap();

// Lets add some witness data
original_psbt.inputs[0].final_script_witness = Some(Witness::new());
other.inputs[0].final_script_witness = Some(Witness::new());
let mut merged_psbt = original_psbt.clone();
let res = merged_psbt.merge_unsigned_tx(other.clone());
assert!(res.is_err());
assert_eq!(
res.err().unwrap()[0],
MergePsbtError::MyInputIsSigned(tx_1.input[0].previous_output)
);
// ensure the psbt has not been modified
assert_eq!(merged_psbt, original_psbt);
// Lets try the same thing with the second psbt
let err = merged_psbt.merge_unsigned_tx(other.clone()).err().unwrap();
assert!(err.contains(&MergePsbtError::OtherInputIsSigned(tx_2.input[0].previous_output)));
assert!(err.contains(&MergePsbtError::MyInputIsSigned(tx_1.input[0].previous_output)));
// ensure the psbt has not been modified
assert_eq!(merged_psbt, original_psbt);
}
}
2 changes: 1 addition & 1 deletion payjoin/src/psbt.rs → payjoin/src/psbt/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Utilities to make work with PSBTs easier
pub mod merge;

use std::collections::BTreeMap;
use std::fmt;
Expand All @@ -24,7 +25,6 @@ impl fmt::Display for InconsistentPsbt {
}

impl std::error::Error for InconsistentPsbt {}

/// Our Psbt type for validation and utilities
pub(crate) trait PsbtExt: Sized {
fn inputs_mut(&mut self) -> &mut [psbt::Input];
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
Loading