Skip to content

Commit 0531747

Browse files
committed
Introduce constructor for Segwit input pairs
1 parent 3f0b6cc commit 0531747

File tree

3 files changed

+180
-5
lines changed

3 files changed

+180
-5
lines changed

payjoin-test-utils/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,18 @@ pub static PARSED_PAYJOIN_PROPOSAL_WITH_SENDER_INFO: Lazy<Psbt> = Lazy::new(|| {
303303
pub const MULTIPARTY_ORIGINAL_PSBT_ONE: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAQCaAgAAAAKa/zFrWjicRDNVCehGj/2jdXGKuZLMzLNUo+uj7PBW8wAAAAAA/v///4HkRlmU9FA3sHckkCQMjGrabNpgAw37XaqUItNFjR5rAAAAAAD+////AgDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpLg6QUqAQAAABYAFH6MyygysrAmk/nunEFuGGDLf244aAAAAAEBHwDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpIBCGsCRzBEAiBLJUhV7tja6FdELXDcB6Q3Gd+BMBpkjdHpj3DLnbL6AAIgJmFl3kpWBzkO8yDDl73BMMalSlAOQvfY+hqFRos5DhQBIQLfeq7CCftNEUHcRLZniJOqcgKZEqPc1A40BnEiZHj3FQABAJoCAAAAAp3/WZGMnqydV72jQR8wzxsgTF2XB9qKDWP8mdS6mohbAQAAAAD+////1xNypiNKcl215SfdF7Xa8W5VvnE/KliF4fN7vhYFbwQBAAAAAP7///8CoNkFKgEAAAAWABQchAe9uxzqT1EjLx4jgx9u1Mn6QADyBSoBAAAAFgAUye8yXWX0MvouXYhAFb06xX5kADpoAAAAAQEfAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOgEIawJHMEQCIF7ihY/YtUPUTOaEdJbg0/HiwKunK398BI67/LknPGqMAiBHBXmL6gTP8PxEGeWswk6T0tCI2Gvwq1zh+wd7h8QCWAEhApM0w2WFlw0eg64Zp3PeyRmxl/7WGHUED8Ul/aX1FiTBAAAAAA==";
304304

305305
pub const MULTIPARTY_ORIGINAL_PSBT_TWO: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAAEAmgIAAAACnf9ZkYyerJ1XvaNBHzDPGyBMXZcH2ooNY/yZ1LqaiFsBAAAAAP7////XE3KmI0pyXbXlJ90XtdrxblW+cT8qWIXh83u+FgVvBAEAAAAA/v///wKg2QUqAQAAABYAFByEB727HOpPUSMvHiODH27UyfpAAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOmgAAAABAR8A8gUqAQAAABYAFMnvMl1l9DL6Ll2IQBW9OsV+ZAA6AQhrAkcwRAIgXuKFj9i1Q9RM5oR0luDT8eLAq6crf3wEjrv8uSc8aowCIEcFeYvqBM/w/EQZ5azCTpPS0IjYa/CrXOH7B3uHxAJYASECkzTDZYWXDR6Drhmnc97JGbGX/tYYdQQPxSX9pfUWJMEAAQCaAgAAAAIKOB8lY4eoEupDnxviz0/nAuR2biNFKbdkvckiW5ioPAAAAAAA/v///w0g/mj67592vy29xhnZMGeVtEXN1jD4lU/SMZM8oStqAAAAAAD+////AgDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R36/g6QUqAQAAABYAFMGlw3hwcx1b+KQGWIfOUxzwrwWkaAAAAAEBHwDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R368BCGsCRzBEAiA//JH+jonzbzqnKI0Uti16iJcdsXI+6Zu0IAZKlOq6AwIgP0XawCCH73uXKilFqSXQQQrBvmi/Sx44D/A+/MQ/mJYBIQIekOyEpJKpFQd7eHuY6Vt4Qlf0+00Wp529I23hl/EpcQAAAA==";
306+
307+
/// sha256d of the empty string
308+
#[rustfmt::skip]
309+
pub const DUMMY32: [u8; 32] = [
310+
0x5d, 0xf6, 0xe0, 0xe2, 0x76, 0x13, 0x59, 0xd3,
311+
0x0a, 0x82, 0x75, 0x05, 0x8e, 0x29, 0x9f, 0xcc,
312+
0x03, 0x81, 0x53, 0x45, 0x45, 0xf5, 0x5c, 0xf4,
313+
0x3e, 0x41, 0x98, 0x3f, 0x5d, 0x4c, 0x94, 0x56,
314+
];
315+
/// hash160 of the empty string
316+
#[rustfmt::skip]
317+
pub const DUMMY20: [u8; 20] = [
318+
0xb4, 0x72, 0xa2, 0x66, 0xd0, 0xbd, 0x89, 0xc1, 0x37, 0x06,
319+
0xa4, 0x13, 0x2c, 0xcf, 0xb1, 0x6f, 0x7c, 0x3b, 0x9f, 0xcb,
320+
];

payjoin/src/psbt/mod.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ impl InternalInputPair<'_> {
217217
}
218218
}
219219

220-
#[derive(Debug, PartialEq)]
220+
#[derive(Debug, PartialEq, Eq)]
221221
pub(crate) enum PrevTxOutError {
222222
MissingUtxoInformation,
223223
IndexOutOfBounds { output_count: usize, index: u32 },
@@ -236,14 +236,17 @@ impl fmt::Display for PrevTxOutError {
236236

237237
impl std::error::Error for PrevTxOutError {}
238238

239-
#[derive(Debug, PartialEq)]
239+
#[derive(Debug, PartialEq, Eq)]
240240
pub(crate) enum InternalPsbtInputError {
241241
PrevTxOut(PrevTxOutError),
242242
UnequalTxid,
243243
/// TxOut provided in `segwit_utxo` doesn't match the one in `non_segwit_utxo`
244244
SegWitTxOutMismatch,
245245
AddressType(AddressTypeError),
246246
NoRedeemScript,
247+
InvalidP2wpkhScriptPubkey,
248+
InvalidP2wshScriptPubkey,
249+
InvalidP2trScriptPubkey,
247250
}
248251

249252
impl fmt::Display for InternalPsbtInputError {
@@ -254,6 +257,9 @@ impl fmt::Display for InternalPsbtInputError {
254257
Self::SegWitTxOutMismatch => write!(f, "transaction output provided in SegWit UTXO field doesn't match the one in non-SegWit UTXO field"),
255258
Self::AddressType(_) => write!(f, "invalid address type"),
256259
Self::NoRedeemScript => write!(f, "provided p2sh PSBT input is missing a redeem_script"),
260+
Self::InvalidP2wpkhScriptPubkey => write!(f, "provided script pubkey is invalid for P2WPKH"),
261+
Self::InvalidP2wshScriptPubkey => write!(f, "provided script pubkey is invalid for P2WSH"),
262+
Self::InvalidP2trScriptPubkey => write!(f, "provided script pubkey is invalid for P2TR"),
257263
}
258264
}
259265
}
@@ -266,6 +272,9 @@ impl std::error::Error for InternalPsbtInputError {
266272
Self::SegWitTxOutMismatch => None,
267273
Self::AddressType(error) => Some(error),
268274
Self::NoRedeemScript => None,
275+
Self::InvalidP2wpkhScriptPubkey => None,
276+
Self::InvalidP2wshScriptPubkey => None,
277+
Self::InvalidP2trScriptPubkey => None,
269278
}
270279
}
271280
}
@@ -278,7 +287,7 @@ impl From<AddressTypeError> for InternalPsbtInputError {
278287
fn from(value: AddressTypeError) -> Self { Self::AddressType(value) }
279288
}
280289

281-
#[derive(Debug)]
290+
#[derive(Debug, PartialEq, Eq)]
282291
pub struct PsbtInputError(InternalPsbtInputError);
283292

284293
impl From<InternalPsbtInputError> for PsbtInputError {
@@ -309,7 +318,7 @@ impl std::error::Error for PsbtInputsError {
309318
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.error) }
310319
}
311320

312-
#[derive(Debug, PartialEq)]
321+
#[derive(Debug, PartialEq, Eq)]
313322
pub(crate) enum AddressTypeError {
314323
PrevTxOut(PrevTxOutError),
315324
InvalidScript(FromScriptError),

payjoin/src/receive/mod.rs

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
use std::str::FromStr;
1313

14-
use bitcoin::{psbt, AddressType, Psbt, TxIn, TxOut};
14+
use bitcoin::{psbt, AddressType, OutPoint, Psbt, Sequence, TxIn, TxOut};
1515
pub(crate) use error::InternalPayloadError;
1616
pub use error::{
1717
Error, InputContributionError, JsonReply, OutputSubstitutionError, PayloadError,
@@ -58,6 +58,69 @@ impl InputPair {
5858
Ok(input_pair)
5959
}
6060

61+
/// Helper function for creating SegWit input pairs
62+
fn new_segwit_input_pair(
63+
txout: TxOut,
64+
outpoint: OutPoint,
65+
sequence: Option<Sequence>,
66+
) -> Result<Self, PsbtInputError> {
67+
let txin = TxIn {
68+
previous_output: OutPoint { txid: outpoint.txid, vout: outpoint.vout },
69+
script_sig: Default::default(),
70+
sequence: sequence.unwrap_or_default(),
71+
witness: Default::default(),
72+
};
73+
74+
let psbtin = psbt::Input {
75+
witness_utxo: Some(TxOut { value: txout.value, script_pubkey: txout.script_pubkey }),
76+
..psbt::Input::default()
77+
};
78+
let input_pair = Self { txin, psbtin };
79+
let raw = InternalInputPair::from(&input_pair);
80+
raw.validate_utxo()?;
81+
82+
Ok(input_pair)
83+
}
84+
85+
/// Constructs a new ['InputPair'] for spending a native SegWit P2WPKH output
86+
pub fn new_p2wpkh(
87+
txout: TxOut,
88+
outpoint: OutPoint,
89+
sequence: Option<Sequence>,
90+
) -> Result<Self, PsbtInputError> {
91+
if !txout.script_pubkey.is_p2wpkh() {
92+
return Err(InternalPsbtInputError::InvalidP2wpkhScriptPubkey.into());
93+
}
94+
95+
Self::new_segwit_input_pair(txout, outpoint, sequence)
96+
}
97+
98+
/// Constructs a new ['InputPair'] for spending a native SegWit P2WSH output
99+
pub fn new_p2wsh(
100+
txout: TxOut,
101+
outpoint: OutPoint,
102+
sequence: Option<Sequence>,
103+
) -> Result<Self, PsbtInputError> {
104+
if !txout.script_pubkey.is_p2wsh() {
105+
return Err(InternalPsbtInputError::InvalidP2wshScriptPubkey.into());
106+
}
107+
108+
Self::new_segwit_input_pair(txout, outpoint, sequence)
109+
}
110+
111+
/// Constructs a new ['InputPair'] for spending a native SegWit P2TR output
112+
pub fn new_p2tr(
113+
txout: TxOut,
114+
outpoint: OutPoint,
115+
sequence: Option<Sequence>,
116+
) -> Result<Self, PsbtInputError> {
117+
if !txout.script_pubkey.is_p2tr() {
118+
return Err(InternalPsbtInputError::InvalidP2trScriptPubkey.into());
119+
}
120+
121+
Self::new_segwit_input_pair(txout, outpoint, sequence)
122+
}
123+
61124
pub(crate) fn previous_txout(&self) -> TxOut {
62125
InternalInputPair::from(self)
63126
.previous_txout()
@@ -88,3 +151,91 @@ pub(crate) fn parse_payload(
88151

89152
Ok((psbt, params))
90153
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use bitcoin::hashes::Hash;
158+
use bitcoin::key::{PublicKey, WPubkeyHash};
159+
use bitcoin::secp256k1::Secp256k1;
160+
use bitcoin::{Amount, ScriptBuf, ScriptHash, Txid, WScriptHash, XOnlyPublicKey};
161+
use payjoin_test_utils::{DUMMY20, DUMMY32};
162+
163+
use super::*;
164+
use crate::psbt::InternalPsbtInputError::{
165+
InvalidP2trScriptPubkey, InvalidP2wpkhScriptPubkey, InvalidP2wshScriptPubkey,
166+
};
167+
168+
#[test]
169+
fn create_p2wpkh_input_pair() {
170+
let outpoint = OutPoint { txid: Txid::from_byte_array(DUMMY32), vout: 31 };
171+
let sequence = Sequence::from_512_second_intervals(123);
172+
let p2wpkh_txout = TxOut {
173+
value: Amount::from_sat(12345),
174+
script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array(DUMMY20)),
175+
};
176+
let p2wpkh_pair =
177+
InputPair::new_p2wpkh(p2wpkh_txout.clone(), outpoint, Some(sequence)).unwrap();
178+
assert_eq!(p2wpkh_pair.txin.previous_output, outpoint);
179+
assert_eq!(p2wpkh_pair.txin.sequence, sequence);
180+
assert_eq!(p2wpkh_pair.psbtin.witness_utxo.unwrap(), p2wpkh_txout);
181+
182+
let p2sh_txout = TxOut {
183+
value: Default::default(),
184+
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
185+
};
186+
assert_eq!(
187+
InputPair::new_p2wpkh(p2sh_txout, outpoint, Some(sequence)).err().unwrap(),
188+
PsbtInputError::from(InvalidP2wpkhScriptPubkey)
189+
)
190+
}
191+
192+
#[test]
193+
fn create_p2wsh_input_pair() {
194+
let outpoint = OutPoint { txid: Txid::from_byte_array(DUMMY32), vout: 31 };
195+
let sequence = Sequence::from_512_second_intervals(123);
196+
let p2wsh_txout = TxOut {
197+
value: Amount::from_sat(12345),
198+
script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::from_byte_array(DUMMY32)),
199+
};
200+
let p2wsh_pair =
201+
InputPair::new_p2wsh(p2wsh_txout.clone(), outpoint, Some(sequence)).unwrap();
202+
assert_eq!(p2wsh_pair.txin.previous_output, outpoint);
203+
assert_eq!(p2wsh_pair.txin.sequence, sequence);
204+
assert_eq!(p2wsh_pair.psbtin.witness_utxo.unwrap(), p2wsh_txout);
205+
206+
let p2sh_txout = TxOut {
207+
value: Default::default(),
208+
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
209+
};
210+
assert_eq!(
211+
InputPair::new_p2wsh(p2sh_txout, outpoint, Some(sequence)).err().unwrap(),
212+
PsbtInputError::from(InvalidP2wshScriptPubkey)
213+
)
214+
}
215+
216+
#[test]
217+
fn create_p2tr_input_pair() {
218+
let outpoint = OutPoint { txid: Txid::from_byte_array(DUMMY32), vout: 31 };
219+
let sequence = Sequence::from_512_second_intervals(123);
220+
let pubkey_string = "0347ff3dacd07a1f43805ec6808e801505a6e18245178609972a68afbc2777ff2b";
221+
let pubkey = pubkey_string.parse::<PublicKey>().expect("valid pubkey");
222+
let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
223+
let p2tr_txout = TxOut {
224+
value: Amount::from_sat(12345),
225+
script_pubkey: ScriptBuf::new_p2tr(&Secp256k1::new(), xonly_pubkey, None),
226+
};
227+
let p2tr_pair = InputPair::new_p2tr(p2tr_txout.clone(), outpoint, Some(sequence)).unwrap();
228+
assert_eq!(p2tr_pair.txin.previous_output, outpoint);
229+
assert_eq!(p2tr_pair.txin.sequence, sequence);
230+
assert_eq!(p2tr_pair.psbtin.witness_utxo.unwrap(), p2tr_txout);
231+
232+
let p2sh_txout = TxOut {
233+
value: Default::default(),
234+
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
235+
};
236+
assert_eq!(
237+
InputPair::new_p2tr(p2sh_txout, outpoint, Some(sequence)).err().unwrap(),
238+
PsbtInputError::from(InvalidP2trScriptPubkey)
239+
)
240+
}
241+
}

0 commit comments

Comments
 (0)