Disable signing devices that are not related to the actual spending path

test

test

test

rebase i

rebase i
This commit is contained in:
Thomas Ballivet 2025-05-06 21:11:38 +02:00
parent 41747538ed
commit 86c4896534
7 changed files with 232 additions and 34 deletions

View File

@ -187,6 +187,7 @@ impl PsbtState {
cache.datadir_path.clone(), cache.datadir_path.clone(),
cache.network, cache.network,
self.saved, self.saved,
self.tx.recovery_timelock(),
); );
let cmd = modal.load(daemon); let cmd = modal.load(daemon);
self.modal = Some(PsbtModal::Sign(modal)); self.modal = Some(PsbtModal::Sign(modal));
@ -438,6 +439,7 @@ pub struct SignModal {
signed: HashSet<Fingerprint>, signed: HashSet<Fingerprint>,
is_saved: bool, is_saved: bool,
display_modal: bool, display_modal: bool,
recovery_timelock: Option<u16>,
} }
impl SignModal { impl SignModal {
@ -447,6 +449,7 @@ impl SignModal {
datadir_path: LianaDirectory, datadir_path: LianaDirectory,
network: Network, network: Network,
is_saved: bool, is_saved: bool,
recovery_timelock: Option<u16>,
) -> Self { ) -> Self {
Self { Self {
signing: HashSet::new(), signing: HashSet::new(),
@ -456,6 +459,7 @@ impl SignModal {
signed, signed,
is_saved, is_saved,
display_modal: true, display_modal: true,
recovery_timelock,
} }
} }
} }
@ -565,6 +569,7 @@ impl Modal for SignModal {
view::psbt::sign_action( view::psbt::sign_action(
self.error.as_ref(), self.error.as_ref(),
&self.hws.list, &self.hws.list,
&self.wallet.main_descriptor,
self.wallet.signer.as_ref().map(|s| s.fingerprint()), self.wallet.signer.as_ref().map(|s| s.fingerprint()),
self.wallet self.wallet
.signer .signer
@ -572,6 +577,7 @@ impl Modal for SignModal {
.and_then(|signer| self.wallet.keys_aliases.get(&signer.fingerprint)), .and_then(|signer| self.wallet.keys_aliases.get(&signer.fingerprint)),
&self.signed, &self.signed,
&self.signing, &self.signing,
self.recovery_timelock,
), ),
) )
.on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel))) .on_blur(Some(view::Message::Spend(view::SpendTxMessage::Cancel)))

View File

@ -13,6 +13,7 @@ pub fn hw_list_view(
hw: &HardwareWallet, hw: &HardwareWallet,
signed: bool, signed: bool,
signing: bool, signing: bool,
can_sign: bool,
) -> Element<Message> { ) -> Element<Message> {
let mut bttn = Button::new(match hw { let mut bttn = Button::new(match hw {
HardwareWallet::Supported { HardwareWallet::Supported {
@ -40,6 +41,8 @@ pub fn hw_list_view(
alias.as_ref(), alias.as_ref(),
"The wallet descriptor is not registered on the device.\n You can register it in the settings.", "The wallet descriptor is not registered on the device.\n You can register it in the settings.",
) )
} else if !can_sign {
hw::disabled_hardware_wallet(kind, version.as_ref(), fingerprint, "This signing device is not part of this spending path.")
} else { } else {
hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
} }
@ -71,7 +74,7 @@ pub fn hw_list_view(
}) })
.style(theme::button::secondary) .style(theme::button::secondary)
.width(Length::Fill); .width(Length::Fill);
if !signing { if can_sign && !signing {
if let HardwareWallet::Supported { registered, .. } = hw { if let HardwareWallet::Supported { registered, .. } = hw {
if *registered != Some(false) { if *registered != Some(false) {
bttn = bttn.on_press(Message::SelectHardwareWallet(i)); bttn = bttn.on_press(Message::SelectHardwareWallet(i));

View File

@ -5,6 +5,7 @@ use iced::{
Alignment, Length, Alignment, Length,
}; };
use liana::descriptors::LianaDescriptor;
use liana::{ use liana::{
descriptors::{LianaPolicy, PathInfo, PathSpendInfo}, descriptors::{LianaPolicy, PathInfo, PathSpendInfo},
miniscript::bitcoin::{ miniscript::bitcoin::{
@ -1014,13 +1015,16 @@ fn change_view(output: &TxOut, network: Network) -> Element<Message> {
.into() .into()
} }
#[allow(clippy::too_many_arguments)]
pub fn sign_action<'a>( pub fn sign_action<'a>(
warning: Option<&Error>, warning: Option<&Error>,
hws: &'a [HardwareWallet], hws: &'a [HardwareWallet],
descriptor: &LianaDescriptor,
signer: Option<Fingerprint>, signer: Option<Fingerprint>,
signer_alias: Option<&'a String>, signer_alias: Option<&'a String>,
signed: &HashSet<Fingerprint>, signed: &HashSet<Fingerprint>,
signing: &HashSet<Fingerprint>, signing: &HashSet<Fingerprint>,
recovery_timelock: Option<u16>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
Column::new() Column::new()
.push_maybe(warning.map(|w| warn(Some(w)))) .push_maybe(warning.map(|w| warn(Some(w))))
@ -1037,6 +1041,9 @@ pub fn sign_action<'a>(
.push(hws.iter().enumerate().fold( .push(hws.iter().enumerate().fold(
Column::new().spacing(10), Column::new().spacing(10),
|col, (i, hw)| { |col, (i, hw)| {
let can_sign = hw.fingerprint().map_or(false, |f| {
descriptor.contains_fingerprint_in_path(f, recovery_timelock)
});
col.push(hw_list_view( col.push(hw_list_view(
i, i,
hw, hw,
@ -1046,20 +1053,29 @@ pub fn sign_action<'a>(
hw.fingerprint() hw.fingerprint()
.map(|f| signing.contains(&f)) .map(|f| signing.contains(&f))
.unwrap_or(false), .unwrap_or(false),
can_sign,
)) ))
}, },
)) ))
.push_maybe(signer.map(|fingerprint| { .push_maybe({
Button::new(if signed.contains(&fingerprint) { signer.map(|fingerprint| {
let can_sign = descriptor
.contains_fingerprint_in_path(fingerprint, recovery_timelock);
let btn = Button::new(if signed.contains(&fingerprint) {
hw::sign_success_hot_signer(fingerprint, signer_alias) hw::sign_success_hot_signer(fingerprint, signer_alias)
} else { } else {
hw::hot_signer(fingerprint, signer_alias) hw::hot_signer(fingerprint, signer_alias, can_sign)
}) })
.on_press(Message::Spend(SpendTxMessage::SelectHotSigner))
.padding(10) .padding(10)
.style(theme::button::secondary) .style(theme::button::secondary)
.width(Length::Fill) .width(Length::Fill);
})) if can_sign {
btn.on_press(Message::Spend(SpendTxMessage::SelectHotSigner))
} else {
btn
}
})
})
.width(Length::Fill), .width(Length::Fill),
) )
.spacing(20) .spacing(20)

View File

@ -204,6 +204,10 @@ impl SpendTx {
.find(|&path| path.sigs_count >= path.threshold) .find(|&path| path.sigs_count >= path.threshold)
} }
pub fn recovery_timelock(&self) -> Option<u16> {
self.sigs.recovery_paths().keys().max().cloned()
}
pub fn signers(&self) -> HashSet<Fingerprint> { pub fn signers(&self) -> HashSet<Fingerprint> {
let mut signers = HashSet::new(); let mut signers = HashSet::new();
for fg in self.sigs.primary_path().signed_pubkeys.keys() { for fg in self.sigs.primary_path().signed_pubkeys.keys() {

View File

@ -125,10 +125,11 @@ pub fn unimplemented_method_hardware_wallet<'a, T: 'a, K: Display, V: Display, F
.width(Length::Fill) .width(Length::Fill)
} }
pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>( pub fn disabled_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
kind: K, kind: K,
version: Option<V>, version: Option<V>,
fingerprint: F, fingerprint: F,
label: &'static str,
) -> Container<'a, T> { ) -> Container<'a, T> {
let key = column(vec![ let key = column(vec![
text::p1_regular(format!("#{}", fingerprint)).into(), text::p1_regular(format!("#{}", fingerprint)).into(),
@ -144,9 +145,7 @@ pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
.push(key) .push(key)
.push(Space::with_width(15)) .push(Space::with_width(15))
.push(Space::with_width(Length::Fill)) .push(Space::with_width(Length::Fill))
.push(text::text( .push(text::text(label))
"This signing device is not related to this Liana wallet.",
))
.push(Space::with_width(Length::Fill)) .push(Space::with_width(Length::Fill))
.align_y(Vertical::Center), .align_y(Vertical::Center),
) )
@ -157,6 +156,19 @@ pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
.width(Length::Fill) .width(Length::Fill)
} }
pub fn unrelated_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
kind: K,
version: Option<V>,
fingerprint: F,
) -> Container<'a, T> {
disabled_hardware_wallet(
kind,
version,
fingerprint,
"This signing device is not related to this Liana wallet.",
)
}
pub fn processing_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>( pub fn processing_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>(
kind: K, kind: K,
version: Option<V>, version: Option<V>,
@ -484,9 +496,11 @@ pub fn unselected_hot_signer<'a, T: 'a, F: Display>(
pub fn hot_signer<'a, T: 'a, F: Display>( pub fn hot_signer<'a, T: 'a, F: Display>(
fingerprint: F, fingerprint: F,
alias: Option<impl Into<Cow<'a, str>> + Display>, alias: Option<impl Into<Cow<'a, str>> + Display>,
can_sign: bool,
) -> Container<'a, T> { ) -> Container<'a, T> {
Container::new( Container::new(
column(vec![ Row::new()
.push(column(vec![
Row::new() Row::new()
.spacing(5) .spacing(5)
.push_maybe(alias.map(|a| text::p1_bold(a))) .push_maybe(alias.map(|a| text::p1_bold(a)))
@ -496,8 +510,17 @@ pub fn hot_signer<'a, T: 'a, F: Display>(
.spacing(5) .spacing(5)
.push(text::caption("This computer")) .push(text::caption("This computer"))
.into(), .into(),
]) ]))
.width(Length::Fill), .push(Space::with_width(Length::Fixed(20.0)))
.push_maybe(if !can_sign {
Some(text::text(
"This hot signer is not part of this spending path.",
))
} else {
None
})
.push(Space::with_width(Length::Fill))
.align_y(Vertical::Center),
) )
.padding(10) .padding(10)
} }

View File

@ -9,6 +9,7 @@ use miniscript::{
RelLockTime, ScriptContext, Threshold, RelLockTime, ScriptContext, Threshold,
}; };
use miniscript::bitcoin::bip32::Fingerprint;
use std::{ use std::{
collections::{BTreeMap, HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
convert::TryFrom, convert::TryFrom,
@ -368,6 +369,11 @@ impl PathInfo {
), ),
}) })
} }
/// Determine whether the fingerprint is part of this path.
pub fn contains_fingerprint(&self, fingerprint: Fingerprint) -> bool {
self.thresh_origins().1.contains_key(&fingerprint)
}
} }
// See // See

View File

@ -217,6 +217,39 @@ impl LianaDescriptor {
.for_any_key(|k| k.master_fingerprint() == fg) .for_any_key(|k| k.master_fingerprint() == fg)
} }
/// Determine whether the fingerprint is part of a specific path of this descriptor.
/// If recovery_timelock is None, checks in the primary path.
/// If recovery_timelock is Some(timelock), checks in the recovery path with specified timelock.
pub fn contains_fingerprint_in_path(
&self,
fingerprint: Fingerprint,
recovery_timelock: Option<u16>,
) -> bool {
match recovery_timelock {
None => self.contains_fingerprint_in_primary_path(fingerprint),
Some(timelock) => self.contains_fingerprint_in_recovery_path(fingerprint, timelock),
}
}
/// Determine whether the fingerprint is part of the primary path of this descriptor.
fn contains_fingerprint_in_primary_path(&self, fingerprint: Fingerprint) -> bool {
self.policy().primary_path.contains_fingerprint(fingerprint)
}
/// Determine whether the fingerprint is part of the recovery path of this descriptor for the
/// specified timelock.
fn contains_fingerprint_in_recovery_path(
&self,
fingerprint: Fingerprint,
recovery_timelock: u16,
) -> bool {
self.policy()
.recovery_paths
.get(&recovery_timelock)
.map(|path_info| path_info.contains_fingerprint(fingerprint))
.unwrap_or(false)
}
/// Get the descriptor for receiving addresses. /// Get the descriptor for receiving addresses.
pub fn receive_descriptor(&self) -> &SinglePathLianaDesc { pub fn receive_descriptor(&self) -> &SinglePathLianaDesc {
&self.receive_desc &self.receive_desc
@ -751,9 +784,9 @@ impl DerivedSinglePathLianaDesc {
mod tests { mod tests {
use super::*; use super::*;
use bitcoin::{hashes::Hash, Sequence};
use crate::signer::HotSigner; use crate::signer::HotSigner;
use bitcoin::{hashes::Hash, Sequence};
use miniscript::bitcoin::bip32::Fingerprint;
fn random_desc_key( fn random_desc_key(
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>, secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
@ -2210,13 +2243,120 @@ mod tests {
} }
#[test] #[test]
fn descriptor_contains_fingerprint() { fn descriptor_contains_fingerprint_in_primary_path_multi() {
let descr = LianaDescriptor::from_str("wsh(or_d(multi(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*),and_v(v:thresh(2,pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*),a:pkh([aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*),a:pkh([aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*)),older(26352))))").unwrap(); let descr = LianaDescriptor::from_str("wsh(or_d(multi(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*),and_v(v:thresh(2,pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*),a:pkh([aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*),a:pkh([aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*)),older(26352))))").unwrap();
assert!(descr.contains_fingerprint(Fingerprint::from_str("aabb0011").unwrap())); assert!(
assert!(descr.contains_fingerprint(Fingerprint::from_str("aabb0012").unwrap())); descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0011").unwrap())
assert!(descr.contains_fingerprint(Fingerprint::from_str("aabb0013").unwrap())); );
assert!(!descr.contains_fingerprint(Fingerprint::from_str("aabb0014").unwrap())); assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0012").unwrap())
);
assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0013").unwrap())
);
assert!(
!descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0014").unwrap())
);
}
#[test]
fn descriptor_contains_fingerprint_in_primary_path_single_key() {
let descr = LianaDescriptor::from_str("wsh(or_d(pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*),and_v(v:thresh(1,pkh([bbcc2233/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*)),older(26352))))").unwrap();
assert!(
descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("aabb0011").unwrap())
);
assert!(
!descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("bbcc2233").unwrap())
);
assert!(
!descr.contains_fingerprint_in_primary_path(Fingerprint::from_str("ddeeff00").unwrap())
);
}
#[test]
fn descriptor_contains_fingerprint_in_recovery_path_multi() {
let descr = LianaDescriptor::from_str("wsh(or_d(multi(3,[aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*,[aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/0/<0;1>/*,[aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/0/<0;1>/*),and_v(v:thresh(2,pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/1/<0;1>/*),a:pkh([aabb0012/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*),a:pkh([aabb0013/48'/0'/0'/2']xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/1/<0;1>/*)),older(26352))))").unwrap();
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0011").unwrap(),
26352
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0012").unwrap(),
26352
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0013").unwrap(),
26352
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0013").unwrap(),
1000
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0014").unwrap(),
26352
));
}
#[test]
fn descriptor_contains_fingerprint_in_recovery_path_single_key() {
let descr = LianaDescriptor::from_str("wsh(or_d(pkh([aabb0011/48'/0'/0'/2']xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/0/<0;1>/*),and_v(v:thresh(1,pkh([bbcc2233/48'/0'/0'/2']xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/1/<0;1>/*)),older(26352))))").unwrap();
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("aabb0011").unwrap(),
26352
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("bbcc2233").unwrap(),
26352
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("ddeeff00").unwrap(),
26352
));
}
#[test]
fn descriptor_contains_fingerprint_in_recovery_path_multiple_keys() {
let descr = LianaDescriptor::from_str("wsh(or_d(c:or_i(and_v(v:older(38305),and_v(v:pkh([d6bba22a/84'/1'/0']tpubDCwMfgJWBGfJuZFmgTAP9qdSwJeC4fEKaYXQx7CNiTzsB5WrpVSmySdPFnKDu8ChWZweYNh7MoAoFsCNY7gTRFSGtDYbG9s6vAKNKzT1Hii/<0;1>/*),pk_h([e9e6c583/48'/1'/0'/2']tpubDEWn2LRKdyREaweKHxj7XzSjcxXGTVbFkL5Qi5AWsJzGvN28cKQwGqCND9TP6EPtPaE13eK9SnyuiQ4qsfy5UuGD3p32Ew36mWfKmYCJRcz/<0;1>/*))),pk_k([de8abde2/48'/1'/0'/2']tpubDES5ZQEwEuj7Fpe6d6wkwD8SdequEa2cqq57QHQ43pb1x2HxbLp6anHwutDNzrMhDAbx1YgxCFAbRi6EhWwQLaGMSSmxJRaAzCUgn6VwpVD/<0;1>/*)),and_v(v:thresh(1,pkh([d6bba22a/84'/1'/0']tpubDCwMfgJWBGfJuZFmgTAP9qdSwJeC4fEKaYXQx7CNiTzsB5WrpVSmySdPFnKDu8ChWZweYNh7MoAoFsCNY7gTRFSGtDYbG9s6vAKNKzT1Hii/<2;3>/*),a:pkh([e9e6c583/48'/1'/0'/2']tpubDEWn2LRKdyREaweKHxj7XzSjcxXGTVbFkL5Qi5AWsJzGvN28cKQwGqCND9TP6EPtPaE13eK9SnyuiQ4qsfy5UuGD3p32Ew36mWfKmYCJRcz/<2;3>/*)),older(52596))))").unwrap();
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("d6bba22a").unwrap(),
38305
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("e9e6c583").unwrap(),
38305
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("d6bba22a").unwrap(),
52596
));
assert!(descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("e9e6c583").unwrap(),
52596
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("d6bba22a").unwrap(),
12345
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("e9e6c583").unwrap(),
12345
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("ffffffff").unwrap(),
38305
));
assert!(!descr.contains_fingerprint_in_recovery_path(
Fingerprint::from_str("ffffffff").unwrap(),
52596
));
} }
// TODO: test error conditions of deserialization. // TODO: test error conditions of deserialization.