Skip to content

Commit efc726a

Browse files
committed
Fix weight estimation for P2TR inputs
Estimation can only occur if the P2TR input is a key spend, since we can't reliably predict the size of potential unlocking scripts
1 parent 08c5571 commit efc726a

File tree

1 file changed

+165
-2
lines changed

1 file changed

+165
-2
lines changed

payjoin/src/core/psbt/mod.rs

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::fmt;
99
use bitcoin::address::FromScriptError;
1010
use bitcoin::psbt::Psbt;
1111
use bitcoin::transaction::InputWeightPrediction;
12-
use bitcoin::{bip32, psbt, Address, AddressType, Network, TxIn, TxOut, Weight};
12+
use bitcoin::{bip32, psbt, Address, AddressType, Network, TapSighashType, TxIn, TxOut, Weight};
1313

1414
#[derive(Debug, PartialEq)]
1515
pub(crate) enum InconsistentPsbt {
@@ -98,6 +98,9 @@ impl PsbtExt for Psbt {
9898
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh-nested-in-bip16-p2sh
9999
const NESTED_P2WPKH_MAX: InputWeightPrediction = InputWeightPrediction::from_slice(23, &[72, 33]);
100100

101+
const SCHNORR_SIG_DEFAULT_SIZE: usize = 64;
102+
const SCHNORR_SIG_WITH_SIGHASH_SIZE: usize = 65;
103+
101104
#[derive(Clone, Debug)]
102105
pub(crate) struct InternalInputPair<'a> {
103106
pub txin: &'a TxIn,
@@ -207,7 +210,38 @@ impl InternalInputPair<'_> {
207210
}
208211
P2wpkh => Ok(InputWeightPrediction::P2WPKH_MAX),
209212
P2wsh => Err(InputWeightError::NotSupported),
210-
P2tr => Ok(InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH),
213+
P2tr => {
214+
// For unfinalized psbts, handle via tap_key_sig
215+
if let Some(sig) = self.psbtin.tap_key_sig {
216+
if !self.psbtin.tap_scripts.is_empty()
217+
|| !self.psbtin.tap_script_sigs.is_empty()
218+
|| self.psbtin.tap_merkle_root.is_some()
219+
{
220+
return Err(InputWeightError::NotSupported);
221+
}
222+
match sig.sighash_type {
223+
TapSighashType::Default =>
224+
Ok(InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH),
225+
_ => Ok(InputWeightPrediction::P2TR_KEY_NON_DEFAULT_SIGHASH),
226+
}
227+
}
228+
// For finalized psbts, check final_script_witness
229+
else if let Some(witness) = &self.psbtin.final_script_witness {
230+
if witness.len() == 1 {
231+
match witness[0].len() {
232+
SCHNORR_SIG_DEFAULT_SIZE =>
233+
Ok(InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH),
234+
SCHNORR_SIG_WITH_SIGHASH_SIZE =>
235+
Ok(InputWeightPrediction::P2TR_KEY_NON_DEFAULT_SIGHASH),
236+
_ => Err(InputWeightError::NotSupported),
237+
}
238+
} else {
239+
Err(InputWeightError::NotSupported)
240+
}
241+
} else {
242+
Err(InputWeightError::NotSupported)
243+
}
244+
}
211245
_ => Err(AddressTypeError::UnknownAddressType.into()),
212246
}?;
213247

@@ -376,3 +410,132 @@ impl std::error::Error for InputWeightError {
376410
impl From<AddressTypeError> for InputWeightError {
377411
fn from(value: AddressTypeError) -> Self { Self::AddressType(value) }
378412
}
413+
414+
#[cfg(test)]
415+
mod tests {
416+
use bitcoin::key::Secp256k1;
417+
use bitcoin::taproot::{ControlBlock, LeafVersion};
418+
use bitcoin::{psbt, secp256k1, taproot, PublicKey, ScriptBuf, TapNodeHash, XOnlyPublicKey};
419+
420+
use super::*;
421+
use crate::core::psbt::InternalInputPair;
422+
use crate::receive::InputPair;
423+
424+
/// Lengths of txid, index and sequence: (32, 4, 4)
425+
const TXID_INDEX_SEQUENCE_WEIGHT: Weight = Weight::from_non_witness_data_size(32 + 4 + 4);
426+
427+
#[test]
428+
fn expected_weight_for_p2tr() {
429+
let pubkey_string = "0347ff3dacd07a1f43805ec6808e801505a6e18245178609972a68afbc2777ff2b";
430+
let pubkey = pubkey_string.parse::<PublicKey>().expect("valid pubkey");
431+
let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
432+
let p2tr_utxo = TxOut {
433+
value: Default::default(),
434+
script_pubkey: ScriptBuf::new_p2tr(&Secp256k1::new(), xonly_pubkey, None),
435+
};
436+
let default_sighash_pair = InputPair {
437+
txin: Default::default(),
438+
psbtin: psbt::Input {
439+
tap_key_sig: Some(
440+
taproot::Signature::from_slice(
441+
&[0; secp256k1::constants::SCHNORR_SIGNATURE_SIZE],
442+
)
443+
.unwrap(),
444+
),
445+
witness_utxo: Some(p2tr_utxo.clone()),
446+
..Default::default()
447+
},
448+
};
449+
assert_eq!(
450+
InternalInputPair::from(&default_sighash_pair).expected_input_weight().unwrap(),
451+
InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH.weight() + TXID_INDEX_SEQUENCE_WEIGHT
452+
);
453+
454+
// Add a sighash byte
455+
let mut sig_bytes = [0; secp256k1::constants::SCHNORR_SIGNATURE_SIZE + 1];
456+
sig_bytes[sig_bytes.len() - 1] = 1;
457+
let non_default_sighash_pair = InputPair {
458+
txin: Default::default(),
459+
psbtin: psbt::Input {
460+
tap_key_sig: Some(taproot::Signature::from_slice(&sig_bytes).unwrap()),
461+
witness_utxo: Some(p2tr_utxo),
462+
..Default::default()
463+
},
464+
};
465+
assert_eq!(
466+
InternalInputPair::from(&non_default_sighash_pair).expected_input_weight().unwrap(),
467+
InputWeightPrediction::P2TR_KEY_NON_DEFAULT_SIGHASH.weight()
468+
+ TXID_INDEX_SEQUENCE_WEIGHT
469+
);
470+
}
471+
472+
#[test]
473+
fn not_supported_p2tr_expected_weights() {
474+
let pubkey_string = "0347ff3dacd07a1f43805ec6808e801505a6e18245178609972a68afbc2777ff2b";
475+
let pubkey = pubkey_string.parse::<PublicKey>().expect("valid pubkey");
476+
let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
477+
let p2tr_script = ScriptBuf::new_p2tr(&Secp256k1::new(), xonly_pubkey.clone(), None);
478+
let p2tr_utxo = TxOut { value: Default::default(), script_pubkey: p2tr_script.clone() };
479+
480+
let mut tap_scripts = BTreeMap::new();
481+
let leaf_version: u8 = 0xC0;
482+
let mut control_block_vec = Vec::with_capacity(33);
483+
control_block_vec.push(leaf_version);
484+
control_block_vec.extend_from_slice(&xonly_pubkey.serialize());
485+
let control_block = ControlBlock::decode(control_block_vec.as_slice()).unwrap();
486+
tap_scripts
487+
.insert(control_block.clone(), (p2tr_script.clone(), control_block.leaf_version));
488+
489+
let pair_with_tapscripts = InputPair {
490+
txin: Default::default(),
491+
psbtin: psbt::Input {
492+
tap_scripts,
493+
witness_utxo: Some(p2tr_utxo.clone()),
494+
..Default::default()
495+
},
496+
};
497+
assert_eq!(
498+
InternalInputPair::from(&pair_with_tapscripts).expected_input_weight().err().unwrap(),
499+
InputWeightError::NotSupported
500+
);
501+
502+
let mut tap_script_sigs = BTreeMap::new();
503+
tap_script_sigs.insert(
504+
(xonly_pubkey.clone(), p2tr_script.tapscript_leaf_hash()),
505+
taproot::Signature::from_slice(&[0; secp256k1::constants::SCHNORR_SIGNATURE_SIZE])
506+
.unwrap(),
507+
);
508+
let pair_with_tap_script_sigs = InputPair {
509+
txin: Default::default(),
510+
psbtin: psbt::Input {
511+
tap_script_sigs,
512+
witness_utxo: Some(p2tr_utxo.clone()),
513+
..Default::default()
514+
},
515+
};
516+
assert_eq!(
517+
InternalInputPair::from(&pair_with_tap_script_sigs)
518+
.expected_input_weight()
519+
.err()
520+
.unwrap(),
521+
InputWeightError::NotSupported
522+
);
523+
524+
let tap_merkle_root = TapNodeHash::from_script(&p2tr_script, LeafVersion::TapScript);
525+
let pair_with_tap_merkle_root = InputPair {
526+
txin: Default::default(),
527+
psbtin: psbt::Input {
528+
tap_merkle_root: Some(tap_merkle_root),
529+
witness_utxo: Some(p2tr_utxo.clone()),
530+
..Default::default()
531+
},
532+
};
533+
assert_eq!(
534+
InternalInputPair::from(&pair_with_tap_merkle_root)
535+
.expected_input_weight()
536+
.err()
537+
.unwrap(),
538+
InputWeightError::NotSupported
539+
);
540+
}
541+
}

0 commit comments

Comments
 (0)