Skip to content

Commit b6a3322

Browse files
committed
Fix descriptor derivation from psbt input
1 parent dc49891 commit b6a3322

File tree

3 files changed

+110
-49
lines changed

3 files changed

+110
-49
lines changed

src/descriptor/mod.rs

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
//! This module contains generic utilities to work with descriptors, plus some re-exported types
1515
//! from [`miniscript`].
1616

17-
use std::collections::{BTreeMap, HashMap, HashSet};
17+
use std::collections::{BTreeMap, HashSet};
1818
use std::ops::Deref;
1919

20-
use bitcoin::secp256k1;
20+
use bitcoin::{secp256k1, XOnlyPublicKey, PublicKey};
2121
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
2222
use bitcoin::util::{psbt, taproot};
2323
use bitcoin::{Network, Script, TxOut};
2424

25-
use miniscript::descriptor::{DescriptorType, InnerXKey};
25+
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
2626
pub use miniscript::{
2727
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
2828
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
@@ -327,6 +327,7 @@ pub(crate) trait DescriptorMeta {
327327
tap_key_origins: &TapKeyOrigins,
328328
secp: &'s SecpCtx,
329329
) -> Option<DerivedDescriptor<'s>>;
330+
fn derive_from_psbt_key_origins<'s>(&self, key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>, secp: &'s SecpCtx) -> Option<DerivedDescriptor<'s>>;
330331
fn derive_from_psbt_input<'s>(
331332
&self,
332333
psbt_input: &psbt::Input,
@@ -395,78 +396,101 @@ impl DescriptorMeta for ExtendedDescriptor {
395396
Ok(answer)
396397
}
397398

398-
fn derive_from_hd_keypaths<'s>(
399-
&self,
400-
hd_keypaths: &HdKeyPaths,
401-
secp: &'s SecpCtx,
402-
) -> Option<DerivedDescriptor<'s>> {
403-
let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect();
399+
fn derive_from_psbt_key_origins<'s>(&self, key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>, secp: &'s SecpCtx) -> Option<DerivedDescriptor<'s>> {
400+
// Ensure that deriving `xpub` with `path` yields `expected`
401+
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>, path: &DerivationPath, expected: &SinglePubKey| {
402+
let derived = xpub.xkey.derive_pub(secp, path).expect("The path should never contain hardened derivation steps").public_key;
404403

405-
let mut path_found = None;
406-
self.for_each_key(|key| {
407-
if path_found.is_some() {
408-
// already found a matching path, we are done
409-
return true;
404+
match expected {
405+
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
406+
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
407+
_ => false,
410408
}
409+
};
411410

411+
let mut path_found = None;
412+
413+
// using `for_any_key` should make this stop as soon as we return `true`
414+
self.for_any_key(|key| {
412415
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
413-
// Check if the key matches one entry in our `index`. If it does, `matches()` will
416+
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
414417
// return the "prefix" that matched, so we remove that prefix from the full path
415-
// found in `index` and save it in `derive_path`. We expect this to be a derivation
418+
// found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
416419
// path of length 1 if the key is `wildcard` and an empty path otherwise.
417420
let root_fingerprint = xpub.root_fingerprint(secp);
418-
let derivation_path: Option<Vec<ChildNumber>> = index
421+
let derive_path = key_origins
419422
.get_key_value(&root_fingerprint)
420-
.and_then(|(fingerprint, path)| {
421-
xpub.matches(&(**fingerprint, (*path).clone()), secp)
423+
.and_then(|(fingerprint, (path, expected))| {
424+
xpub.matches(&(*fingerprint, (*path).clone()), secp).zip(Some((path, expected)))
422425
})
423-
.map(|prefix| {
424-
index
425-
.get(&xpub.root_fingerprint(secp))
426-
.unwrap()
427-
.into_iter()
426+
.and_then(|(prefix, (full_path, expected))| {
427+
let derive_path = full_path.into_iter()
428428
.skip(prefix.into_iter().count())
429429
.cloned()
430-
.collect()
430+
.collect::<DerivationPath>();
431+
432+
// `derive_path` only contains the replacement index for the wildcard, if present, or
433+
// an empty path for fixed descriptors. To verify the key we also need the normal steps
434+
// that come before the wildcard, so we take them directly from `xpub` and then append
435+
// the final index
436+
if verify_key(xpub, &xpub.derivation_path.extend(derive_path.clone()), expected) {
437+
Some(derive_path)
438+
} else {
439+
log::debug!("Key `{}` derived with {} yields an unexpected key", root_fingerprint, derive_path);
440+
None
441+
}
431442
});
432443

433-
match derivation_path {
444+
match derive_path {
434445
Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => {
435446
// Ignore hardened wildcards
436447
if let ChildNumber::Normal { index } = path[0] {
437-
path_found = Some(index)
448+
path_found = Some(index);
449+
return true;
438450
}
439451
}
440452
Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => {
441-
path_found = Some(0)
453+
path_found = Some(0);
454+
return true;
442455
}
443456
_ => {}
444457
}
445458
}
446459

447-
true
460+
false
448461
});
449462

450463
path_found.map(|path| self.as_derived(path, secp))
451464
}
452465

466+
fn derive_from_hd_keypaths<'s>(
467+
&self,
468+
hd_keypaths: &HdKeyPaths,
469+
secp: &'s SecpCtx,
470+
) -> Option<DerivedDescriptor<'s>> {
471+
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
472+
let key_origins = hd_keypaths
473+
.iter()
474+
.map(|(pk, (fingerprint, path))| {
475+
(*fingerprint, (path, SinglePubKey::FullKey(PublicKey::new(*pk))))
476+
})
477+
.collect();
478+
self.derive_from_psbt_key_origins(key_origins, secp)
479+
}
480+
453481
fn derive_from_tap_key_origins<'s>(
454482
&self,
455483
tap_key_origins: &TapKeyOrigins,
456484
secp: &'s SecpCtx,
457485
) -> Option<DerivedDescriptor<'s>> {
458-
// "Convert" a tap_key_origins map to an hd_keypaths map, by dropping the leaf hash vector and
459-
// converting the public key type
460-
let hd_keypaths = tap_key_origins
486+
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
487+
let key_origins = tap_key_origins
461488
.iter()
462-
.map(|(pk, (_, keysource))| {
463-
let mut data: Vec<u8> = vec![0x02];
464-
data.extend(pk.serialize().iter());
465-
let pk = secp256k1::PublicKey::from_slice(&data).expect("Invalid XOnlyPublicKey");
466-
(pk, keysource.clone())
489+
.map(|(pk, (_, (fingerprint, path)))| {
490+
(*fingerprint, (path, SinglePubKey::XOnly(*pk)))
467491
})
468492
.collect();
469-
self.derive_from_hd_keypaths(&hd_keypaths, secp)
493+
self.derive_from_psbt_key_origins(key_origins, secp)
470494
}
471495

472496
fn derive_from_psbt_input<'s>(

src/wallet/mod.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use bitcoin::secp256k1::Secp256k1;
2626
use bitcoin::consensus::encode::serialize;
2727
use bitcoin::util::{psbt, taproot};
2828
use bitcoin::{
29-
Address, EcdsaSighashType, Network, OutPoint, Script, Transaction, TxOut, Txid, Witness,
29+
Address, EcdsaSighashType, SchnorrSighashType, Network, OutPoint, Script, Transaction, TxOut, Txid, Witness,
3030
};
3131

3232
use miniscript::descriptor::DescriptorTrait;
@@ -1009,23 +1009,24 @@ where
10091009
// this helps us doing our job later
10101010
self.add_input_hd_keypaths(psbt)?;
10111011

1012-
// If we aren't allowed to use `witness_utxo`, ensure that every input but finalized one
1013-
// has the `non_witness_utxo`
1012+
// If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
1013+
// have the `non_witness_utxo`
10141014
if !sign_options.trust_witness_utxo
10151015
&& psbt
10161016
.inputs
10171017
.iter()
10181018
.filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none())
1019+
.filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none())
10191020
.any(|i| i.non_witness_utxo.is_none())
10201021
{
10211022
return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo));
10221023
}
10231024

10241025
// If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
1025-
// is using `SIGHASH_ALL`
1026+
// is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot
10261027
if !sign_options.allow_all_sighashes
10271028
&& !psbt.inputs.iter().all(|i| {
1028-
i.sighash_type.is_none() || i.sighash_type == Some(EcdsaSighashType::All.into())
1029+
i.sighash_type.is_none() || i.sighash_type == Some(EcdsaSighashType::All.into()) || i.sighash_type == Some(SchnorrSighashType::All.into()) || i.sighash_type == Some(SchnorrSighashType::Default.into())
10291030
})
10301031
{
10311032
return Err(Error::Signer(signer::SignerError::NonStandardSighash));
@@ -1818,7 +1819,7 @@ pub(crate) mod test {
18181819
}
18191820

18201821
pub(crate) fn get_test_tr_with_taptree() -> &'static str {
1821-
"tr(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh,{pk(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
1822+
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
18221823
}
18231824

18241825
macro_rules! assert_fee_rate {
@@ -4163,6 +4164,33 @@ pub(crate) mod test {
41634164
);
41644165
assert_eq!(psbt.inputs[0].tap_scripts.len(), 2, "Empty tap_scripts");
41654166
}
4167+
4168+
#[test]
4169+
fn test_taproot_key_spend() {
4170+
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig());
4171+
let addr = wallet.get_address(AddressIndex::New).unwrap();
4172+
4173+
let mut builder = wallet.build_tx();
4174+
builder.add_recipient(addr.script_pubkey(), 25_000);
4175+
let (mut psbt, _) = builder.finish().unwrap();
4176+
4177+
// assert!(wallet.sign(&mut psbt, Default::default()).unwrap(), "Unable to finalize taproot key spend");
4178+
wallet.sign(&mut psbt, Default::default()).unwrap();
4179+
4180+
dbg!(&psbt);
4181+
}
4182+
4183+
#[test]
4184+
fn test_taproot_script_spend() {
4185+
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree());
4186+
let addr = wallet.get_address(AddressIndex::New).unwrap();
4187+
4188+
let mut builder = wallet.build_tx();
4189+
builder.add_recipient(addr.script_pubkey(), 25_000);
4190+
let (mut psbt, _) = builder.finish().unwrap();
4191+
4192+
assert!(wallet.sign(&mut psbt, Default::default()).unwrap(), "Unable to finalize taproot script spend");
4193+
}
41664194
}
41674195

41684196
/// Deterministically generate a unique name given the descriptors defining the wallet

src/wallet/signer.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot};
9494
use bitcoin::{secp256k1, XOnlyPublicKey};
9595
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
9696

97-
use miniscript::descriptor::{DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, KeyMap};
97+
use miniscript::descriptor::{DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, SinglePubKey, KeyMap};
9898
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
9999

100100
use super::utils::SecpCtx;
@@ -258,11 +258,14 @@ impl InputSigner for DescriptorXKey<ExtendedPrivKey> {
258258
return Ok(());
259259
}
260260

261+
let tap_key_origins = psbt.inputs[input_index].tap_key_origins.iter().map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)).collect::<Vec<_>>();
261262
let (public_key, full_path) = match psbt.inputs[input_index]
262263
.bip32_derivation
263264
.iter()
264-
.filter_map(|(pk, &(fingerprint, ref path))| {
265-
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
265+
.map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource))
266+
.chain(tap_key_origins.into_iter())
267+
.filter_map(|(pk, keysource @ (_, path))| {
268+
if self.matches(&keysource, secp).is_some() {
266269
Some((pk, path))
267270
} else {
268271
None
@@ -285,7 +288,13 @@ impl InputSigner for DescriptorXKey<ExtendedPrivKey> {
285288
None => self.xkey.derive_priv(secp, &full_path).unwrap(),
286289
};
287290

288-
if &secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key) != public_key {
291+
let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key);
292+
let valid_key = match public_key {
293+
SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true,
294+
SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true,
295+
_ => false,
296+
};
297+
if !valid_key {
289298
Err(SignerError::InvalidKey)
290299
} else {
291300
// HD wallets imply compressed keys
@@ -440,7 +449,7 @@ fn produce_schnorr_sig(
440449
.tap_script_sigs
441450
.insert((pubkey, lh), final_signature);
442451
} else {
443-
psbt_input.tap_key_sig = Some(final_signature)
452+
psbt_input.tap_key_sig = Some(final_signature);
444453
}
445454
}
446455

0 commit comments

Comments
 (0)