Skip to content

Commit 76f4e5f

Browse files
committed
Introduce constructor for Segwit input pairs
1 parent 16f0c20 commit 76f4e5f

File tree

3 files changed

+194
-5
lines changed

3 files changed

+194
-5
lines changed

payjoin-test-utils/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,18 @@ pub static PARSED_PAYJOIN_PROPOSAL_WITH_SENDER_INFO: Lazy<Psbt> = Lazy::new(|| {
302302
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==";
303303

304304
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==";
305+
306+
/// sha256d of the empty string
307+
#[rustfmt::skip]
308+
pub const DUMMY32: [u8; 32] = [
309+
0x5d, 0xf6, 0xe0, 0xe2, 0x76, 0x13, 0x59, 0xd3,
310+
0x0a, 0x82, 0x75, 0x05, 0x8e, 0x29, 0x9f, 0xcc,
311+
0x03, 0x81, 0x53, 0x45, 0x45, 0xf5, 0x5c, 0xf4,
312+
0x3e, 0x41, 0x98, 0x3f, 0x5d, 0x4c, 0x94, 0x56,
313+
];
314+
/// hash160 of the empty string
315+
#[rustfmt::skip]
316+
pub const DUMMY20: [u8; 20] = [
317+
0xb4, 0x72, 0xa2, 0x66, 0xd0, 0xbd, 0x89, 0xc1, 0x37, 0x06,
318+
0xa4, 0x13, 0x2c, 0xcf, 0xb1, 0x6f, 0x7c, 0x3b, 0x9f, 0xcb,
319+
];

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: 166 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, ScriptBuf, Sequence, TxIn, TxOut};
1515
pub(crate) use error::InternalPayloadError;
1616
pub use error::{
1717
Error, InputContributionError, JsonReply, OutputSubstitutionError, PayloadError,
@@ -58,6 +58,72 @@ 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+
witness_script: Option<ScriptBuf>,
67+
) -> Result<Self, PsbtInputError> {
68+
let txin = TxIn {
69+
previous_output: OutPoint { txid: outpoint.txid, vout: outpoint.vout },
70+
script_sig: Default::default(),
71+
sequence: sequence.unwrap_or_default(),
72+
witness: Default::default(),
73+
};
74+
75+
let psbtin = psbt::Input {
76+
witness_utxo: Some(TxOut { value: txout.value, script_pubkey: txout.script_pubkey }),
77+
witness_script,
78+
..psbt::Input::default()
79+
};
80+
let input_pair = Self { txin, psbtin };
81+
let raw = InternalInputPair::from(&input_pair);
82+
raw.validate_utxo()?;
83+
84+
Ok(input_pair)
85+
}
86+
87+
/// Constructs a new ['InputPair'] for spending a native SegWit P2WPKH output
88+
pub fn new_p2wpkh(
89+
txout: TxOut,
90+
outpoint: OutPoint,
91+
sequence: Option<Sequence>,
92+
) -> Result<Self, PsbtInputError> {
93+
if !txout.script_pubkey.is_p2wpkh() {
94+
return Err(InternalPsbtInputError::InvalidP2wpkhScriptPubkey.into());
95+
}
96+
97+
Self::new_segwit_input_pair(txout, outpoint, sequence, None)
98+
}
99+
100+
/// Constructs a new ['InputPair'] for spending a native SegWit P2WSH output
101+
pub fn new_p2wsh(
102+
txout: TxOut,
103+
outpoint: OutPoint,
104+
witness_script: ScriptBuf,
105+
sequence: Option<Sequence>,
106+
) -> Result<Self, PsbtInputError> {
107+
if !txout.script_pubkey.is_p2wsh() {
108+
return Err(InternalPsbtInputError::InvalidP2wshScriptPubkey.into());
109+
}
110+
111+
Self::new_segwit_input_pair(txout, outpoint, sequence, Some(witness_script))
112+
}
113+
114+
/// Constructs a new ['InputPair'] for spending a native SegWit P2TR output
115+
pub fn new_p2tr(
116+
txout: TxOut,
117+
outpoint: OutPoint,
118+
sequence: Option<Sequence>,
119+
) -> Result<Self, PsbtInputError> {
120+
if !txout.script_pubkey.is_p2tr() {
121+
return Err(InternalPsbtInputError::InvalidP2trScriptPubkey.into());
122+
}
123+
124+
Self::new_segwit_input_pair(txout, outpoint, sequence, None)
125+
}
126+
61127
pub(crate) fn previous_txout(&self) -> TxOut {
62128
InternalInputPair::from(self)
63129
.previous_txout()
@@ -88,3 +154,102 @@ pub(crate) fn parse_payload(
88154

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

0 commit comments

Comments
 (0)