1use std::str::FromStr;
2
3use bhwi::bitcoin::{
4 self, Address, CompressedPublicKey, Network, PublicKey, ScriptBuf, TxOut,
5 bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource},
6 blockdata::{
7 opcodes::all::{OP_CHECKMULTISIG, OP_PUSHNUM_1, OP_PUSHNUM_16},
8 script::{Instruction, PushBytes},
9 },
10 psbt::{Input, Psbt},
11 secp256k1::Secp256k1,
12};
13use bhwi::common::{DeviceContext, DisplayAddress, MultisigAddressType, MultisigDisplayAddress};
14use bhwi::ledger::{LedgerWalletPolicy, Version, singlesig_wallet_policy};
15use bhwi::miniscript::descriptor::{DescriptorPublicKey, WalletPolicy, Wildcard};
16
17use crate::HWIDeviceError;
18use crate::device::Device;
19use crate::display_address::multisig_display_address_from_descriptor;
20
21#[derive(Debug, Clone, Copy, Eq, PartialEq)]
23pub enum LedgerAddressType {
24 Tap,
26 Wit,
28 ShWit,
30 Legacy,
32}
33
34impl LedgerAddressType {
35 fn priority(self) -> u8 {
36 match self {
37 Self::Tap => 0,
38 Self::Wit => 1,
39 Self::ShWit => 2,
40 Self::Legacy => 3,
41 }
42 }
43
44 fn purpose(self) -> u32 {
45 match self {
46 Self::Legacy => 44,
47 Self::ShWit => 49,
48 Self::Wit => 84,
49 Self::Tap => 86,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Eq, PartialEq)]
56pub enum LedgerSigningPlan {
57 Default {
59 address_type: LedgerAddressType,
61 account_path: DerivationPath,
63 },
64 Registered {
66 address_type: LedgerAddressType,
68 name: String,
70 policy: String,
72 },
73}
74
75impl LedgerSigningPlan {
76 fn priority(&self) -> u8 {
77 match self {
78 Self::Default { address_type, .. } | Self::Registered { address_type, .. } => {
79 address_type.priority()
80 }
81 }
82 }
83}
84
85pub fn ledger_signing_plans(
92 psbt: &Psbt,
93 fingerprint: Fingerprint,
94 network: Network,
95) -> Result<Vec<LedgerSigningPlan>, String> {
96 let mut plans = Vec::new();
97
98 for (input_index, input) in psbt.inputs.iter().enumerate() {
99 let Some(utxo) = input_utxo(psbt, input_index)? else {
100 continue;
101 };
102 let owns_input = input_has_fingerprint(input, fingerprint);
103 let envelope = match multisig_script(input, &utxo, input_index) {
104 Ok(envelope) => envelope,
105 Err(err) if owns_input => return Err(err),
106 Err(_) => None,
107 };
108
109 if let Some((address_type, script)) = envelope.as_ref()
110 && let Some((threshold, pubkeys)) = parse_multisig_script(script)?
111 {
112 let owns_multisig = pubkeys.iter().any(|pubkey| {
113 input
114 .bip32_derivation
115 .get(&pubkey.inner)
116 .is_some_and(|(key_fingerprint, _)| *key_fingerprint == fingerprint)
117 });
118 if owns_multisig {
119 let plan = ledger_multisig_plan(
120 psbt,
121 input,
122 input_index,
123 *address_type,
124 threshold,
125 &pubkeys,
126 )?;
127 if !plans.contains(&plan) {
128 plans.push(plan);
129 }
130 continue;
131 }
132 }
133
134 if let Some(plan) = ledger_singlesig_plan(input, &utxo, input_index, fingerprint, network)?
135 {
136 if !plans.contains(&plan) {
137 plans.push(plan);
138 }
139 continue;
140 }
141
142 if owns_input {
143 let policy = if utxo.script_pubkey.is_p2tr() {
144 "taproot script-path"
145 } else if envelope.is_some() {
146 "non-sorted-multisig or miniscript"
147 } else {
148 "non-default"
149 };
150 return Err(format!(
151 "input {input_index}: Ledger HWI signtx cannot infer {policy} wallet policy; use explicit descriptor and HMAC signing"
152 ));
153 }
154 }
155
156 plans.sort_by_key(LedgerSigningPlan::priority);
157 Ok(plans)
158}
159
160fn ledger_singlesig_plan(
161 input: &Input,
162 utxo: &TxOut,
163 input_index: usize,
164 fingerprint: Fingerprint,
165 network: Network,
166) -> Result<Option<LedgerSigningPlan>, String> {
167 let Some(address_type) = singlesig_address_type(input, utxo) else {
168 return Ok(None);
169 };
170
171 if address_type == LedgerAddressType::Tap {
172 let owned: Vec<_> = input
173 .tap_key_origins
174 .iter()
175 .filter(|(_, (_, (key_fingerprint, _)))| *key_fingerprint == fingerprint)
176 .collect();
177 if owned.is_empty() {
178 return Ok(None);
179 }
180 let Some(internal_key) = input.tap_internal_key else {
181 return Err(format!(
182 "input {input_index}: Ledger BIP86 input is missing tap_internal_key; use explicit descriptor and HMAC signing for non-default taproot policies"
183 ));
184 };
185 let candidates: Vec<_> = owned
186 .into_iter()
187 .filter(|(key, (leaf_hashes, _))| **key == internal_key && leaf_hashes.is_empty())
188 .collect();
189 if candidates.len() != 1 || input.tap_merkle_root.is_some() {
190 return Err(format!(
191 "input {input_index}: Ledger HWI signtx supports only unambiguous BIP86 key-path inputs; use explicit descriptor and HMAC signing for taproot script paths"
192 ));
193 }
194 let (_, (_, (_, path))) = candidates[0];
195 let account_path =
196 validate_standard_singlesig_path(path, address_type, network, input_index)?;
197 let secp = Secp256k1::verification_only();
198 let expected = Address::p2tr(&secp, internal_key, None, network).script_pubkey();
199 if expected != utxo.script_pubkey {
200 return Err(format!(
201 "input {input_index}: BIP86 internal key does not match the prevout script; use explicit descriptor and HMAC signing for non-default taproot policies"
202 ));
203 }
204 return Ok(Some(LedgerSigningPlan::Default {
205 address_type,
206 account_path,
207 }));
208 }
209
210 let owned: Vec<_> = input
211 .bip32_derivation
212 .iter()
213 .filter(|(_, (key_fingerprint, _))| *key_fingerprint == fingerprint)
214 .collect();
215 if owned.is_empty() {
216 return Ok(None);
217 }
218 let candidates: Vec<_> = owned
219 .into_iter()
220 .filter(|(key, _)| singlesig_key_matches(**key, address_type, input, utxo, network))
221 .collect();
222 if candidates.len() != 1 {
223 return Err(format!(
224 "input {input_index}: Ledger single-sig key metadata is missing or ambiguous"
225 ));
226 }
227 let (_, (_, path)) = candidates[0];
228 let account_path = validate_standard_singlesig_path(path, address_type, network, input_index)?;
229 Ok(Some(LedgerSigningPlan::Default {
230 address_type,
231 account_path,
232 }))
233}
234
235fn singlesig_address_type(input: &Input, utxo: &TxOut) -> Option<LedgerAddressType> {
236 if utxo.script_pubkey.is_p2pkh() {
237 Some(LedgerAddressType::Legacy)
238 } else if utxo.script_pubkey.is_p2wpkh() {
239 Some(LedgerAddressType::Wit)
240 } else if utxo.script_pubkey.is_p2tr() {
241 Some(LedgerAddressType::Tap)
242 } else if utxo.script_pubkey.is_p2sh()
243 && input
244 .redeem_script
245 .as_ref()
246 .is_some_and(|script| script.is_p2wpkh() && script.to_p2sh() == utxo.script_pubkey)
247 {
248 Some(LedgerAddressType::ShWit)
249 } else {
250 None
251 }
252}
253
254fn singlesig_key_matches(
255 key: bitcoin::secp256k1::PublicKey,
256 address_type: LedgerAddressType,
257 input: &Input,
258 utxo: &TxOut,
259 network: Network,
260) -> bool {
261 let key = PublicKey::new(key);
262 match address_type {
263 LedgerAddressType::Legacy => {
264 Address::p2pkh(key, network).script_pubkey() == utxo.script_pubkey
265 }
266 LedgerAddressType::Wit => CompressedPublicKey::try_from(key)
267 .is_ok_and(|key| Address::p2wpkh(&key, network).script_pubkey() == utxo.script_pubkey),
268 LedgerAddressType::ShWit => CompressedPublicKey::try_from(key).is_ok_and(|key| {
269 input.redeem_script.as_ref().is_some_and(|script| {
270 Address::p2wpkh(&key, network).script_pubkey() == *script
271 && script.to_p2sh() == utxo.script_pubkey
272 })
273 }),
274 LedgerAddressType::Tap => false,
275 }
276}
277
278fn validate_standard_singlesig_path(
279 path: &DerivationPath,
280 address_type: LedgerAddressType,
281 network: Network,
282 input_index: usize,
283) -> Result<DerivationPath, String> {
284 let children = path.as_ref();
285 if children.len() != 5 {
286 return Err(format!(
287 "input {input_index}: Ledger default wallet requires an exact five-level derivation path"
288 ));
289 }
290 let purpose = hardened_index(children[0]);
291 let coin_type = hardened_index(children[1]);
292 let account = hardened_index(children[2]);
293 let branch = normal_index(children[3]);
294 let index = normal_index(children[4]);
295 let expected_coin_type = if network == Network::Bitcoin { 0 } else { 1 };
296 if purpose != Some(address_type.purpose())
297 || coin_type != Some(expected_coin_type)
298 || account.is_none()
299 || !matches!(branch, Some(0 | 1))
300 || index.is_none()
301 {
302 return Err(format!(
303 "input {input_index}: derivation path {path} is not a standard Ledger {:?} wallet path",
304 address_type
305 ));
306 }
307 Ok(DerivationPath::from(children[..3].to_vec()))
308}
309
310fn hardened_index(child: ChildNumber) -> Option<u32> {
311 match child {
312 ChildNumber::Hardened { index } => Some(index),
313 ChildNumber::Normal { .. } => None,
314 }
315}
316
317fn normal_index(child: ChildNumber) -> Option<u32> {
318 match child {
319 ChildNumber::Normal { index } => Some(index),
320 ChildNumber::Hardened { .. } => None,
321 }
322}
323
324fn ledger_multisig_plan(
325 psbt: &Psbt,
326 input: &Input,
327 input_index: usize,
328 address_type: LedgerAddressType,
329 threshold: usize,
330 pubkeys: &[PublicKey],
331) -> Result<LedgerSigningPlan, String> {
332 if !pubkeys
333 .windows(2)
334 .all(|keys| keys[0].inner.serialize() < keys[1].inner.serialize())
335 {
336 return Err(format!(
337 "input {input_index}: Ledger HWI signtx supports only sorted multisig scripts"
338 ));
339 }
340
341 let mut keys = Vec::with_capacity(pubkeys.len());
342 let mut expected_suffix = None;
343 for pubkey in pubkeys {
344 let key_source = input.bip32_derivation.get(&pubkey.inner).ok_or_else(|| {
345 format!("input {input_index}: multisig public key is missing BIP32 derivation metadata")
346 })?;
347 let resolved = global_xpub_key_expression(psbt, key_source, pubkey, input_index)?;
348 match expected_suffix {
349 Some(suffix) if suffix != resolved.suffix => {
350 return Err(format!(
351 "input {input_index}: multisig keys do not share one receive/change derivation"
352 ));
353 }
354 None => expected_suffix = Some(resolved.suffix),
355 Some(_) => {}
356 }
357 keys.push(format!("{}/<0;1>/*", resolved.expression));
358 }
359 keys.sort();
362 let policy = multisig_policy_descriptor(address_type, threshold, &keys, true);
363 Ok(LedgerSigningPlan::Registered {
364 address_type,
365 name: format!("{threshold} of {} Multisig", pubkeys.len()),
366 policy,
367 })
368}
369
370struct ResolvedMultisigKey {
371 expression: String,
372 suffix: (u32, u32),
373}
374
375fn global_xpub_key_expression(
376 psbt: &Psbt,
377 key_source: &KeySource,
378 pubkey: &PublicKey,
379 input_index: usize,
380) -> Result<ResolvedMultisigKey, String> {
381 let (fingerprint, key_path) = key_source;
382 let children = key_path.as_ref();
383 if children.len() < 2 {
384 return Err(format!(
385 "input {input_index}: multisig derivation path is too short"
386 ));
387 }
388 let branch = normal_index(children[children.len() - 2]);
389 let index = normal_index(children[children.len() - 1]);
390 if !matches!(branch, Some(0 | 1)) || index.is_none() {
391 return Err(format!(
392 "input {input_index}: Ledger multisig derivation must end in /0/index or /1/index"
393 ));
394 }
395 let suffix = (
396 branch.expect("branch checked"),
397 index.expect("index checked"),
398 );
399 let xpub_path = DerivationPath::from(children[..children.len() - 2].to_vec());
400 let matches: Vec<_> = psbt
401 .xpub
402 .iter()
403 .filter(|(_, (xpub_fingerprint, path))| {
404 *xpub_fingerprint == *fingerprint && *path == xpub_path
405 })
406 .collect();
407 if matches.len() != 1 {
408 return Err(format!(
409 "input {input_index}: expected one account-level global xpub for {fingerprint}, found {}",
410 matches.len()
411 ));
412 }
413 let (xpub, (_, origin_path)) = matches[0];
414 let secp = Secp256k1::verification_only();
415 let derived = xpub
416 .derive_pub(
417 &secp,
418 &[
419 ChildNumber::from_normal_idx(suffix.0).expect("branch checked"),
420 ChildNumber::from_normal_idx(suffix.1).expect("index checked"),
421 ],
422 )
423 .map_err(|err| format!("input {input_index}: failed to derive global xpub: {err}"))?;
424 if derived.public_key != pubkey.inner {
425 return Err(format!(
426 "input {input_index}: global xpub derivation does not match multisig public key"
427 ));
428 }
429 let origin = origin_path.to_string();
430 let origin = origin.trim_start_matches('m').trim_start_matches('/');
431 let expression = if origin.is_empty() {
432 format!("[{fingerprint}]{xpub}")
433 } else {
434 format!("[{fingerprint}/{origin}]{xpub}")
435 };
436 Ok(ResolvedMultisigKey { expression, suffix })
437}
438
439fn input_has_fingerprint(input: &Input, fingerprint: Fingerprint) -> bool {
440 input
441 .bip32_derivation
442 .values()
443 .any(|(key_fingerprint, _)| *key_fingerprint == fingerprint)
444 || input
445 .tap_key_origins
446 .values()
447 .any(|(_, (key_fingerprint, _))| *key_fingerprint == fingerprint)
448}
449
450fn input_utxo(psbt: &Psbt, input_index: usize) -> Result<Option<TxOut>, String> {
451 let input = &psbt.inputs[input_index];
452 let txin = &psbt.unsigned_tx.input[input_index];
453 let non_witness = if let Some(tx) = &input.non_witness_utxo {
454 if tx.compute_txid() != txin.previous_output.txid {
455 return Err(format!(
456 "input {input_index}: non_witness_utxo transaction id does not match prevout"
457 ));
458 }
459 Some(
460 tx.output
461 .get(txin.previous_output.vout as usize)
462 .cloned()
463 .ok_or_else(|| format!("input {input_index}: prevout index is out of range"))?,
464 )
465 } else {
466 None
467 };
468 if let (Some(witness), Some(non_witness)) = (&input.witness_utxo, &non_witness)
469 && witness != non_witness
470 {
471 return Err(format!(
472 "input {input_index}: witness_utxo and non_witness_utxo disagree"
473 ));
474 }
475 Ok(input.witness_utxo.clone().or(non_witness))
476}
477
478pub fn extend_account_path_for_policy(path: &DerivationPath) -> DerivationPath {
480 let mut children = path.as_ref().to_vec();
481 children.push(ChildNumber::from_normal_idx(0).expect("valid receive branch"));
482 children.push(ChildNumber::from_normal_idx(0).expect("valid address index"));
483 DerivationPath::from(children)
484}
485
486fn multisig_script(
487 input: &Input,
488 utxo: &TxOut,
489 input_index: usize,
490) -> Result<Option<(LedgerAddressType, ScriptBuf)>, String> {
491 if utxo.script_pubkey.is_p2wsh() {
492 let witness_script = input
493 .witness_script
494 .as_ref()
495 .ok_or_else(|| format!("input {input_index}: P2WSH input is missing witness_script"))?;
496 if witness_script.to_p2wsh() != utxo.script_pubkey {
497 return Err(format!(
498 "input {input_index}: witness_script does not match P2WSH prevout"
499 ));
500 }
501 return Ok(Some((LedgerAddressType::Wit, witness_script.clone())));
502 }
503 if !utxo.script_pubkey.is_p2sh() {
504 return Ok(None);
505 }
506 let redeem_script = input
507 .redeem_script
508 .as_ref()
509 .ok_or_else(|| format!("input {input_index}: P2SH input is missing redeem_script"))?;
510 if redeem_script.to_p2sh() != utxo.script_pubkey {
511 return Err(format!(
512 "input {input_index}: redeem_script does not match P2SH prevout"
513 ));
514 }
515 if redeem_script.is_p2wsh() {
516 let witness_script = input.witness_script.as_ref().ok_or_else(|| {
517 format!("input {input_index}: nested P2WSH input is missing witness_script")
518 })?;
519 if witness_script.to_p2wsh() != *redeem_script {
520 return Err(format!(
521 "input {input_index}: witness_script does not match nested P2WSH redeem_script"
522 ));
523 }
524 Ok(Some((LedgerAddressType::ShWit, witness_script.clone())))
525 } else {
526 Ok(Some((LedgerAddressType::Legacy, redeem_script.clone())))
527 }
528}
529
530fn parse_multisig_script(script: &ScriptBuf) -> Result<Option<(usize, Vec<PublicKey>)>, String> {
531 let mut instructions = script.instructions();
532 let Some(first) = instructions.next() else {
533 return Ok(None);
534 };
535 let threshold = match first.map_err(|err| err.to_string())? {
536 Instruction::Op(op) => pushnum(op).filter(|n| *n <= 16),
537 Instruction::PushBytes(_) => None,
538 };
539 let Some(threshold) = threshold else {
540 return Ok(None);
541 };
542
543 let mut pubkeys = Vec::new();
544 let signer_count = loop {
545 let Some(instruction) = instructions.next() else {
546 return Ok(None);
547 };
548 match instruction.map_err(|err| err.to_string())? {
549 Instruction::PushBytes(bytes) if bytes.len() == 33 => {
550 let public_key = PublicKey::from_slice(push_bytes_as_bytes(bytes))
551 .map_err(|err| err.to_string())?;
552 pubkeys.push(public_key);
553 }
554 Instruction::Op(op) => {
555 break pushnum(op);
556 }
557 Instruction::PushBytes(_) => return Ok(None),
558 }
559 };
560
561 let Some(signer_count) = signer_count else {
562 return Ok(None);
563 };
564 let Some(last) = instructions.next() else {
565 return Ok(None);
566 };
567 if last.map_err(|err| err.to_string())? != Instruction::Op(OP_CHECKMULTISIG)
568 || instructions.next().is_some()
569 || signer_count != pubkeys.len()
570 || threshold == 0
571 || threshold > signer_count
572 {
573 return Ok(None);
574 }
575 Ok(Some((threshold, pubkeys)))
576}
577
578fn multisig_policy_descriptor(
579 address_type: LedgerAddressType,
580 threshold: usize,
581 keys: &[String],
582 sorted: bool,
583) -> String {
584 let operator = if sorted { "sortedmulti" } else { "multi" };
585 let body = format!("{operator}({threshold},{})", keys.join(","));
586 match address_type {
587 LedgerAddressType::Legacy => format!("sh({body})"),
588 LedgerAddressType::ShWit => format!("sh(wsh({body}))"),
589 LedgerAddressType::Wit => format!("wsh({body})"),
590 LedgerAddressType::Tap => unreachable!("taproot is not classic multisig"),
591 }
592}
593
594fn pushnum(op: bitcoin::blockdata::opcodes::Opcode) -> Option<usize> {
595 if op == OP_PUSHNUM_1 {
596 return Some(1);
597 }
598 if op.to_u8() >= OP_PUSHNUM_1.to_u8() && op.to_u8() <= OP_PUSHNUM_16.to_u8() {
599 return Some((op.to_u8() - OP_PUSHNUM_1.to_u8() + 1) as usize);
600 }
601 None
602}
603
604fn push_bytes_as_bytes(bytes: &PushBytes) -> &[u8] {
605 bytes.as_bytes()
606}
607
608const MULTISIG_BRANCH_RULE: &str =
609 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig";
610const MULTISIG_KEY_COUNT_RULE: &str = "Invalid threshold or number of keys";
611const MULTISIG_ORIGIN_RULE: &str =
612 "Ledger multisig display requires extended public keys with origin information";
613
614#[derive(Debug)]
616pub struct LedgerMultisigDisplayPlan {
617 pub name: String,
619 pub policy_text: String,
621 pub policy: WalletPolicy,
623 pub change: bool,
625 pub address_index: u32,
627}
628
629pub fn ledger_multisig_display_plan(
635 multisig: MultisigDisplayAddress,
636) -> Result<LedgerMultisigDisplayPlan, String> {
637 let key_count = multisig.keys.len();
638 if multisig.threshold == 0
639 || key_count == 0
640 || key_count > 16
641 || usize::from(multisig.threshold) > key_count
642 {
643 return Err(MULTISIG_KEY_COUNT_RULE.to_owned());
644 }
645
646 let mut suffixes = Vec::with_capacity(key_count);
647 let mut keys = Vec::with_capacity(key_count);
648 let mut origin_too_long = false;
649 for key in &multisig.keys {
650 let xpub = match key {
651 DescriptorPublicKey::XPub(xpub) => xpub,
652 DescriptorPublicKey::Single(_) => return Err(MULTISIG_ORIGIN_RULE.to_owned()),
653 DescriptorPublicKey::MultiXPub(_) => return Err(MULTISIG_BRANCH_RULE.to_owned()),
654 };
655 let Some((_, origin_path)) = &xpub.origin else {
656 return Err(MULTISIG_ORIGIN_RULE.to_owned());
657 };
658 let suffix = xpub.derivation_path.as_ref();
659 if xpub.wildcard != Wildcard::None || suffix.len() != 2 {
660 return Err(MULTISIG_BRANCH_RULE.to_owned());
661 }
662 let Some(branch) = normal_index(suffix[0]) else {
663 return Err(MULTISIG_BRANCH_RULE.to_owned());
664 };
665 let Some(index) = normal_index(suffix[1]) else {
666 return Err(MULTISIG_BRANCH_RULE.to_owned());
667 };
668 if branch > 1 || index > 0x7fff_ffff {
669 return Err(MULTISIG_BRANCH_RULE.to_owned());
670 }
671 suffixes.push((branch, index));
672 origin_too_long |= origin_path.as_ref().len() > 4;
673 keys.push(format!("{}/<0;1>/*", bhwi::policy::format_key_info(key)));
674 }
675 let Some((branch, address_index)) = suffixes.first().copied() else {
676 return Err(MULTISIG_KEY_COUNT_RULE.to_owned());
677 };
678 if suffixes
679 .iter()
680 .any(|suffix| *suffix != (branch, address_index))
681 {
682 return Err(
683 "Ledger Bitcoin app requires all derivation paths to end with /0/*, or all with /1/* for multisig"
684 .to_owned(),
685 );
686 }
687 if origin_too_long {
688 return Err(
689 "Ledger Bitcoin app requires extended keys with derivation length at most 4".to_owned(),
690 );
691 }
692
693 let address_type = match multisig.address_type {
694 MultisigAddressType::Legacy => LedgerAddressType::Legacy,
695 MultisigAddressType::ShWit => LedgerAddressType::ShWit,
696 MultisigAddressType::Wit => LedgerAddressType::Wit,
697 };
698
699 let policy_text = multisig_policy_descriptor(
700 address_type,
701 usize::from(multisig.threshold),
702 &keys,
703 multisig.sorted,
704 );
705 let policy = WalletPolicy::from_str(&policy_text).map_err(|err| err.to_string())?;
706
707 Ok(LedgerMultisigDisplayPlan {
708 name: format!("{} of {key_count} Multisig", multisig.threshold),
709 policy_text,
710 policy,
711 change: branch == 1,
712 address_index,
713 })
714}
715
716pub fn registered_context(
721 name: String,
722 policy: WalletPolicy,
723 hmac: Option<[u8; 32]>,
724) -> DeviceContext {
725 DeviceContext::Ledger {
726 wallet_policy: LedgerWalletPolicy::new(name, Version::V2, policy),
727 wallet_hmac: hmac,
728 }
729}
730
731#[derive(Debug, thiserror::Error)]
733pub enum LedgerSigningError {
734 #[error("{0}")]
736 BadArgument(
737 String,
739 ),
740
741 #[error(transparent)]
743 Device(
744 #[from]
746 HWIDeviceError,
747 ),
748
749 #[error("Ledger wallet registration returned no HMAC")]
751 MissingHmac,
752
753 #[error("Ledger wallet registration returned a {0}-byte HMAC instead of 32 bytes")]
755 HmacLength(
756 usize,
758 ),
759}
760
761pub struct LedgerSigningContext {
763 pub address_type: LedgerAddressType,
765 pub context: DeviceContext,
767}
768
769pub async fn ledger_signing_contexts(
776 device: &mut Device,
777 psbt: &Psbt,
778 network: Network,
779) -> Result<Vec<LedgerSigningContext>, LedgerSigningError> {
780 let fingerprint = device.fingerprint().await?;
781 let plans = ledger_signing_plans(psbt, fingerprint, network)
782 .map_err(LedgerSigningError::BadArgument)?;
783 let mut contexts = Vec::with_capacity(plans.len());
784
785 for plan in plans {
786 match plan {
787 LedgerSigningPlan::Default {
788 address_type,
789 account_path,
790 } => {
791 let xpub = device
792 .device()
793 .get_extended_pubkey(account_path.clone(), false)
794 .await?;
795 let policy = singlesig_wallet_policy(
796 &extend_account_path_for_policy(&account_path),
797 fingerprint,
798 xpub,
799 )
800 .map_err(|err| LedgerSigningError::BadArgument(err.to_string()))?;
801 contexts.push(LedgerSigningContext {
802 address_type,
803 context: DeviceContext::Ledger {
804 wallet_policy: LedgerWalletPolicy::new(String::new(), Version::V2, policy),
805 wallet_hmac: None,
806 },
807 });
808 }
809 LedgerSigningPlan::Registered {
810 address_type,
811 name,
812 policy,
813 } => {
814 let registration = device.device().register_wallet(&name, &policy).await?;
815 let hmac = registration.hmac().ok_or(LedgerSigningError::MissingHmac)?;
816 if hmac.len() != 32 {
817 return Err(LedgerSigningError::HmacLength(hmac.len()));
818 }
819 let wallet_policy = WalletPolicy::from_str(&policy)
820 .map_err(|err| LedgerSigningError::BadArgument(err.to_string()))?;
821 contexts.push(LedgerSigningContext {
822 address_type,
823 context: DeviceContext::Ledger {
824 wallet_policy: LedgerWalletPolicy::new(name, Version::V2, wallet_policy),
825 wallet_hmac: Some(hmac),
826 },
827 });
828 }
829 }
830 }
831
832 Ok(contexts)
833}
834
835pub async fn ledger_multisig_display_address(
840 device: &mut Device,
841 descriptor: &str,
842) -> Result<(DisplayAddress, DeviceContext), LedgerSigningError> {
843 let multisig = multisig_display_address_from_descriptor(descriptor)
844 .map_err(|err| LedgerSigningError::BadArgument(err.to_string()))?;
845 let plan = ledger_multisig_display_plan(multisig).map_err(LedgerSigningError::BadArgument)?;
846 let registration = device
847 .device()
848 .register_wallet(&plan.name, &plan.policy_text)
849 .await?;
850 let hmac = registration.hmac().ok_or(LedgerSigningError::MissingHmac)?;
851 if hmac.len() != 32 {
852 return Err(LedgerSigningError::HmacLength(hmac.len()));
853 }
854
855 Ok((
856 DisplayAddress::ByDescriptor {
857 index: plan.address_index,
858 change: plan.change,
859 display: true,
860 descriptor_name: String::new(),
861 },
862 DeviceContext::Ledger {
863 wallet_policy: LedgerWalletPolicy::new(plan.name, Version::V2, plan.policy),
864 wallet_hmac: Some(hmac),
865 },
866 ))
867}
868
869#[cfg(test)]
870mod tests {
871 use std::str::FromStr;
872
873 use bhwi::bitcoin::{
874 Amount, NetworkKind, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
875 absolute::LockTime,
876 bip32::{Xpriv, Xpub},
877 blockdata::script::Builder,
878 transaction::Version as TxVersion,
879 };
880
881 use super::*;
882 use crate::display_address::multisig_display_address_from_descriptor;
883
884 #[test]
885 fn ledger_signing_plans_cover_all_default_wallets() {
886 let fingerprint = Fingerprint::from([0xf5, 0xac, 0xc2, 0xfd]);
887 let pubkey = sample_child_pubkey(0);
888 for (address_type, path, input) in [
889 (
890 LedgerAddressType::Legacy,
891 "m/44'/1'/0'/0/0",
892 Input {
893 witness_utxo: Some(TxOut {
894 value: Amount::from_sat(50_000),
895 script_pubkey: Address::p2pkh(pubkey, Network::Testnet).script_pubkey(),
896 }),
897 ..Default::default()
898 },
899 ),
900 (LedgerAddressType::ShWit, "m/49'/1'/0'/0/0", {
901 let redeem_script = Address::p2wpkh(
902 &CompressedPublicKey::try_from(pubkey).unwrap(),
903 Network::Testnet,
904 )
905 .script_pubkey();
906 Input {
907 witness_utxo: Some(TxOut {
908 value: Amount::from_sat(50_000),
909 script_pubkey: redeem_script.to_p2sh(),
910 }),
911 redeem_script: Some(redeem_script),
912 ..Default::default()
913 }
914 }),
915 (
916 LedgerAddressType::Wit,
917 "m/84'/1'/0'/0/0",
918 Input {
919 witness_utxo: Some(TxOut {
920 value: Amount::from_sat(50_000),
921 script_pubkey: Address::p2wpkh(
922 &CompressedPublicKey::try_from(pubkey).unwrap(),
923 Network::Testnet,
924 )
925 .script_pubkey(),
926 }),
927 ..Default::default()
928 },
929 ),
930 ] {
931 let path = DerivationPath::from_str(path).unwrap();
932 let mut input = input;
933 input
934 .bip32_derivation
935 .insert(pubkey.inner, (fingerprint, path));
936 assert_eq!(
937 ledger_signing_plans(&psbt_with_input(input), fingerprint, Network::Testnet)
938 .unwrap(),
939 vec![LedgerSigningPlan::Default {
940 address_type,
941 account_path: DerivationPath::from_str(&format!(
942 "m/{}'/1'/0'",
943 address_type.purpose()
944 ))
945 .unwrap(),
946 }]
947 );
948 }
949
950 let internal_key = pubkey.inner.x_only_public_key().0;
951 let path = DerivationPath::from_str("m/86'/1'/0'/0/0").unwrap();
952 let psbt = psbt_with_input(Input {
953 witness_utxo: Some(TxOut {
954 value: Amount::from_sat(50_000),
955 script_pubkey: Address::p2tr(
956 &Secp256k1::verification_only(),
957 internal_key,
958 None,
959 Network::Testnet,
960 )
961 .script_pubkey(),
962 }),
963 tap_internal_key: Some(internal_key),
964 tap_key_origins: [(internal_key, (Vec::new(), (fingerprint, path)))].into(),
965 ..Default::default()
966 });
967 assert_eq!(
968 ledger_signing_plans(&psbt, fingerprint, Network::Testnet).unwrap(),
969 vec![LedgerSigningPlan::Default {
970 address_type: LedgerAddressType::Tap,
971 account_path: DerivationPath::from_str("m/86'/1'/0'").unwrap(),
972 }]
973 );
974 }
975
976 #[test]
977 fn ledger_signing_plans_support_multiple_singlesig_accounts() {
978 let fingerprint = Fingerprint::from([0xf5, 0xac, 0xc2, 0xfd]);
979 let pubkey_a = sample_child_pubkey(0);
980 let pubkey_b = sample_child_pubkey(1);
981 let psbt = psbt_with_inputs(vec![
982 Input {
983 witness_utxo: Some(TxOut {
984 value: Amount::from_sat(50_000),
985 script_pubkey: Address::p2wpkh(
986 &CompressedPublicKey::try_from(pubkey_a).unwrap(),
987 Network::Testnet,
988 )
989 .script_pubkey(),
990 }),
991 bip32_derivation: [(
992 pubkey_a.inner,
993 (
994 fingerprint,
995 DerivationPath::from_str("m/84'/1'/0'/0/0").unwrap(),
996 ),
997 )]
998 .into(),
999 ..Default::default()
1000 },
1001 Input {
1002 witness_utxo: Some(TxOut {
1003 value: Amount::from_sat(50_000),
1004 script_pubkey: Address::p2wpkh(
1005 &CompressedPublicKey::try_from(pubkey_b).unwrap(),
1006 Network::Testnet,
1007 )
1008 .script_pubkey(),
1009 }),
1010 bip32_derivation: [(
1011 pubkey_b.inner,
1012 (
1013 fingerprint,
1014 DerivationPath::from_str("m/84'/1'/1'/0/0").unwrap(),
1015 ),
1016 )]
1017 .into(),
1018 ..Default::default()
1019 },
1020 ]);
1021
1022 let plans = ledger_signing_plans(&psbt, fingerprint, Network::Testnet).unwrap();
1023 assert_eq!(plans.len(), 2);
1024 assert!(plans.iter().all(|plan| matches!(
1025 plan,
1026 LedgerSigningPlan::Default {
1027 address_type: LedgerAddressType::Wit,
1028 ..
1029 }
1030 )));
1031 }
1032
1033 #[test]
1034 fn ledger_signing_plans_support_mixed_default_and_registered_policies() {
1035 let (multisig, fingerprint) = sample_multisig_psbt(LedgerAddressType::Wit, true);
1036 let pubkey = sample_child_pubkey(0);
1037 let singlesig = Input {
1038 witness_utxo: Some(TxOut {
1039 value: Amount::from_sat(50_000),
1040 script_pubkey: Address::p2wpkh(
1041 &CompressedPublicKey::try_from(pubkey).unwrap(),
1042 Network::Testnet,
1043 )
1044 .script_pubkey(),
1045 }),
1046 bip32_derivation: [(
1047 pubkey.inner,
1048 (
1049 fingerprint,
1050 DerivationPath::from_str("m/84'/1'/0'/0/0").unwrap(),
1051 ),
1052 )]
1053 .into(),
1054 ..Default::default()
1055 };
1056 let mut psbt = psbt_with_inputs(vec![singlesig, multisig.inputs[0].clone()]);
1057 psbt.xpub = multisig.xpub;
1058
1059 let plans = ledger_signing_plans(&psbt, fingerprint, Network::Testnet).unwrap();
1060 assert_eq!(plans.len(), 2);
1061 assert!(matches!(plans[0], LedgerSigningPlan::Default { .. }));
1062 assert!(matches!(plans[1], LedgerSigningPlan::Registered { .. }));
1063 }
1064
1065 #[test]
1066 fn ledger_multisig_plans_cover_all_hwi_wrappers() {
1067 for address_type in [
1068 LedgerAddressType::Legacy,
1069 LedgerAddressType::ShWit,
1070 LedgerAddressType::Wit,
1071 ] {
1072 let (psbt, fingerprint) = sample_multisig_psbt(address_type, true);
1073 let plans = ledger_signing_plans(&psbt, fingerprint, Network::Testnet).unwrap();
1074 let LedgerSigningPlan::Registered { policy, name, .. } = &plans[0] else {
1075 panic!("registered policy");
1076 };
1077 assert_eq!(name, "2 of 2 Multisig");
1078 assert_eq!(policy.matches("/<0;1>/*").count(), 2);
1079 match address_type {
1080 LedgerAddressType::Legacy => assert!(policy.starts_with("sh(sortedmulti(2,")),
1081 LedgerAddressType::ShWit => {
1082 assert!(policy.starts_with("sh(wsh(sortedmulti(2,"))
1083 }
1084 LedgerAddressType::Wit => assert!(policy.starts_with("wsh(sortedmulti(2,")),
1085 LedgerAddressType::Tap => unreachable!(),
1086 }
1087 }
1088 }
1089
1090 #[test]
1091 fn ledger_multisig_plan_rejects_missing_global_xpub() {
1092 let (mut psbt, fingerprint) = sample_multisig_psbt(LedgerAddressType::Wit, true);
1093 psbt.xpub.clear();
1094
1095 let err =
1096 ledger_signing_plans(&psbt, fingerprint, Network::Testnet).expect_err("missing xpub");
1097 assert!(err.contains("expected one account-level global xpub"));
1098 }
1099
1100 #[test]
1101 fn ledger_multisig_plan_rejects_unsorted_script() {
1102 let (psbt, fingerprint) = sample_multisig_psbt(LedgerAddressType::Wit, false);
1103
1104 let err = ledger_signing_plans(&psbt, fingerprint, Network::Testnet)
1105 .expect_err("unsorted multisig");
1106 assert!(err.contains("supports only sorted multisig"));
1107 }
1108
1109 fn sample_child_pubkey(index: u32) -> PublicKey {
1110 let secp = Secp256k1::verification_only();
1111 let xpub = sample_xpub()
1112 .derive_pub(
1113 &secp,
1114 &[
1115 ChildNumber::from_normal_idx(0).unwrap(),
1116 ChildNumber::from_normal_idx(index).unwrap(),
1117 ],
1118 )
1119 .expect("derive pubkey");
1120 PublicKey::new(xpub.public_key)
1121 }
1122
1123 fn multisig_script_buf(threshold: i64, pubkeys: &[PublicKey]) -> ScriptBuf {
1124 let mut builder = Builder::new().push_int(threshold);
1125 for pubkey in pubkeys {
1126 builder = builder.push_slice(pubkey.inner.serialize());
1127 }
1128 builder
1129 .push_int(pubkeys.len() as i64)
1130 .push_opcode(OP_CHECKMULTISIG)
1131 .into_script()
1132 }
1133
1134 fn sample_multisig_psbt(address_type: LedgerAddressType, sorted: bool) -> (Psbt, Fingerprint) {
1135 let secp = Secp256k1::new();
1136 let account_path = DerivationPath::from_str("m/48'/1'/0'/2'").unwrap();
1137 let suffix = [
1138 ChildNumber::from_normal_idx(0).unwrap(),
1139 ChildNumber::from_normal_idx(0).unwrap(),
1140 ];
1141 let mut sources = Vec::new();
1142 for seed in [1_u8, 2] {
1143 let master = Xpriv::new_master(NetworkKind::Test, &[seed; 32]).unwrap();
1144 let fingerprint = master.fingerprint(&secp);
1145 let account = master.derive_priv(&secp, &account_path).unwrap();
1146 let xpub = Xpub::from_priv(&secp, &account);
1147 let child = xpub.derive_pub(&secp, &suffix).unwrap();
1148 sources.push((fingerprint, xpub, PublicKey::new(child.public_key)));
1149 }
1150 sources.sort_by_key(|(_, _, pubkey)| pubkey.inner.serialize());
1151 if !sorted {
1152 sources.reverse();
1153 }
1154 let pubkeys: Vec<_> = sources.iter().map(|(_, _, pubkey)| *pubkey).collect();
1155 let script = multisig_script_buf(2, &pubkeys);
1156 let mut input = Input {
1157 bip32_derivation: sources
1158 .iter()
1159 .map(|(fingerprint, _, pubkey)| {
1160 let mut path = account_path.as_ref().to_vec();
1161 path.extend_from_slice(&suffix);
1162 (pubkey.inner, (*fingerprint, DerivationPath::from(path)))
1163 })
1164 .collect(),
1165 ..Default::default()
1166 };
1167 let script_pubkey = match address_type {
1168 LedgerAddressType::Legacy => {
1169 input.redeem_script = Some(script.clone());
1170 script.to_p2sh()
1171 }
1172 LedgerAddressType::ShWit => {
1173 let redeem_script = script.to_p2wsh();
1174 input.redeem_script = Some(redeem_script.clone());
1175 input.witness_script = Some(script);
1176 redeem_script.to_p2sh()
1177 }
1178 LedgerAddressType::Wit => {
1179 input.witness_script = Some(script.clone());
1180 script.to_p2wsh()
1181 }
1182 LedgerAddressType::Tap => unreachable!(),
1183 };
1184 input.witness_utxo = Some(TxOut {
1185 value: Amount::from_sat(50_000),
1186 script_pubkey,
1187 });
1188 let mut psbt = psbt_with_input(input);
1189 for (fingerprint, xpub, _) in &sources {
1190 psbt.xpub
1191 .insert(*xpub, (*fingerprint, account_path.clone()));
1192 }
1193 (psbt, sources[0].0)
1194 }
1195
1196 fn psbt_with_input(input: Input) -> Psbt {
1197 psbt_with_inputs(vec![input])
1198 }
1199
1200 fn psbt_with_inputs(inputs: Vec<Input>) -> Psbt {
1201 let unsigned_tx = Transaction {
1202 version: TxVersion::TWO,
1203 lock_time: LockTime::ZERO,
1204 input: inputs
1205 .iter()
1206 .map(|_| TxIn {
1207 previous_output: OutPoint::null(),
1208 script_sig: ScriptBuf::new(),
1209 sequence: Sequence::MAX,
1210 witness: Witness::new(),
1211 })
1212 .collect(),
1213 output: vec![TxOut {
1214 value: Amount::from_sat(0),
1215 script_pubkey: ScriptBuf::new(),
1216 }],
1217 };
1218 let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).expect("psbt");
1219 psbt.inputs = inputs;
1220 psbt
1221 }
1222
1223 fn sample_xpub() -> Xpub {
1224 Xpub::from_str("tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT")
1225 .expect("sample xpub")
1226 }
1227
1228 fn ledger_display_test_key_info(seed: u8, origin: &str) -> String {
1229 let secp = Secp256k1::new();
1230 let master = Xpriv::new_master(NetworkKind::Test, &[seed; 32]).unwrap();
1231 let path = DerivationPath::from_str(&format!("m/{origin}")).unwrap();
1232 let xpub = Xpub::from_priv(&secp, &master.derive_priv(&secp, &path).unwrap());
1233 format!("[{}/{origin}]{xpub}", master.fingerprint(&secp))
1234 }
1235
1236 fn wrap_multisig_descriptor(address_type: MultisigAddressType, body: &str) -> String {
1237 match address_type {
1238 MultisigAddressType::Legacy => format!("sh({body})"),
1239 MultisigAddressType::ShWit => format!("sh(wsh({body}))"),
1240 MultisigAddressType::Wit => format!("wsh({body})"),
1241 }
1242 }
1243
1244 fn ledger_display_plan_from_descriptor(
1245 descriptor: &str,
1246 ) -> Result<LedgerMultisigDisplayPlan, String> {
1247 let multisig =
1248 multisig_display_address_from_descriptor(descriptor).map_err(|err| err.to_string())?;
1249 ledger_multisig_display_plan(multisig)
1250 }
1251
1252 #[test]
1253 fn ledger_multisig_display_plans_cover_wrappers_operators_and_suffixes() {
1254 let key_infos = [
1255 ledger_display_test_key_info(1, "48'/1'/0'/2'"),
1256 ledger_display_test_key_info(2, "48'/1'/0'/2'"),
1257 ];
1258 let cases = [
1259 (MultisigAddressType::Legacy, true, 0, 0),
1260 (MultisigAddressType::Legacy, false, 1, 1),
1261 (MultisigAddressType::ShWit, true, 0, 7),
1262 (MultisigAddressType::ShWit, false, 1, 8),
1263 (MultisigAddressType::Wit, true, 0, 2_147_483_647),
1264 (MultisigAddressType::Wit, false, 1, 42),
1265 ];
1266
1267 for (address_type, sorted, branch, index) in cases {
1268 let operator = if sorted { "sortedmulti" } else { "multi" };
1269 let ordered_key_infos = if !sorted && matches!(address_type, MultisigAddressType::Wit) {
1270 key_infos.iter().rev().collect::<Vec<_>>()
1271 } else {
1272 key_infos.iter().collect::<Vec<_>>()
1273 };
1274 let descriptor_keys = ordered_key_infos
1275 .iter()
1276 .map(|key| format!("{key}/{branch}/{index}"))
1277 .collect::<Vec<_>>();
1278 let descriptor = wrap_multisig_descriptor(
1279 address_type,
1280 &format!("{operator}(2,{})", descriptor_keys.join(",")),
1281 );
1282 let policy_keys = ordered_key_infos
1283 .iter()
1284 .map(|key| format!("{key}/<0;1>/*"))
1285 .collect::<Vec<_>>();
1286 let expected_policy = wrap_multisig_descriptor(
1287 address_type,
1288 &format!("{operator}(2,{})", policy_keys.join(",")),
1289 );
1290
1291 let plan =
1292 ledger_display_plan_from_descriptor(&descriptor).expect("Ledger display plan");
1293
1294 assert_eq!(plan.name, "2 of 2 Multisig");
1295 assert_eq!(plan.policy_text, expected_policy);
1296 assert_eq!(plan.change, branch == 1);
1297 assert_eq!(plan.address_index, index);
1298 }
1299 }
1300
1301 #[test]
1302 fn ledger_multisig_display_rejects_invalid_thresholds_and_key_count() {
1303 let key = ledger_display_test_key_info(1, "48'/1'/0'/2'");
1304 let descriptor = wrap_multisig_descriptor(
1305 MultisigAddressType::Wit,
1306 &format!("sortedmulti(1,{key}/0/0)"),
1307 );
1308 let valid = multisig_display_address_from_descriptor(&descriptor).unwrap();
1309
1310 for threshold in [0, 2] {
1311 let mut multisig = valid.clone();
1312 multisig.threshold = threshold;
1313 let error =
1314 ledger_multisig_display_plan(multisig).expect_err("invalid multisig threshold");
1315 assert_eq!(error, "Invalid threshold or number of keys");
1316 }
1317
1318 let mut empty = valid.clone();
1319 empty.keys.clear();
1320 let error = ledger_multisig_display_plan(empty).expect_err("empty multisig key set");
1321 assert_eq!(error, "Invalid threshold or number of keys");
1322
1323 let mut too_many = valid;
1324 too_many.keys = vec![too_many.keys[0].clone(); 17];
1325 let error = ledger_multisig_display_plan(too_many).expect_err("too many multisig keys");
1326 assert_eq!(error, "Invalid threshold or number of keys");
1327 }
1328
1329 #[test]
1330 fn ledger_multisig_display_rejects_unsupported_key_shapes() {
1331 let key_a = ledger_display_test_key_info(1, "48'/1'/0'/2'");
1332 let key_b = ledger_display_test_key_info(2, "48'/1'/0'/2'");
1333 let long_origin = ledger_display_test_key_info(3, "48'/1'/0'/2'/3'");
1334 let originless_xpub = sample_xpub();
1335 let cases = [
1336 (
1337 "raw key",
1338 "wsh(sortedmulti(1,[f5acc2fd/48h/1h/0h/2h]0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798))".to_owned(),
1339 "Ledger multisig display requires extended public keys with origin information",
1340 ),
1341 (
1342 "originless xpub",
1343 format!("wsh(sortedmulti(1,{originless_xpub}/0/0))"),
1344 "Ledger multisig display requires extended public keys with origin information",
1345 ),
1346 (
1347 "wildcard",
1348 format!("wsh(sortedmulti(1,{key_a}/0/0/*))"),
1349 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig",
1350 ),
1351 (
1352 "multipath",
1353 format!("wsh(sortedmulti(1,{key_a}/<0;1>/0))"),
1354 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig",
1355 ),
1356 (
1357 "hardened suffix",
1358 format!("wsh(sortedmulti(1,{key_a}/0/1'))"),
1359 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig",
1360 ),
1361 (
1362 "short suffix",
1363 format!("wsh(sortedmulti(1,{key_a}/0))"),
1364 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig",
1365 ),
1366 (
1367 "long suffix",
1368 format!("wsh(sortedmulti(1,{key_a}/0/0/1))"),
1369 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig",
1370 ),
1371 (
1372 "invalid branch",
1373 format!("wsh(sortedmulti(1,{key_a}/2/0))"),
1374 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig",
1375 ),
1376 (
1377 "mismatched suffixes",
1378 format!("wsh(sortedmulti(2,{key_a}/0/0,{key_b}/1/0))"),
1379 "Ledger Bitcoin app requires all derivation paths to end with /0/*, or all with /1/* for multisig",
1380 ),
1381 (
1382 "overlong origin",
1383 format!("wsh(sortedmulti(1,{long_origin}/0/0))"),
1384 "Ledger Bitcoin app requires extended keys with derivation length at most 4",
1385 ),
1386 ];
1387
1388 for (case, descriptor, expected) in cases {
1389 let error = ledger_display_plan_from_descriptor(&descriptor)
1390 .expect_err("unsupported Ledger multisig key");
1391 assert_eq!(error, expected, "{case}");
1392 }
1393
1394 let valid_descriptor = format!("wsh(sortedmulti(1,{key_a}/0/0))");
1395 let mut multisig = multisig_display_address_from_descriptor(&valid_descriptor).unwrap();
1396 let DescriptorPublicKey::XPub(xpub) = &mut multisig.keys[0] else {
1397 panic!("xpub descriptor key");
1398 };
1399 xpub.derivation_path = DerivationPath::from(vec![
1400 ChildNumber::Normal { index: 0 },
1401 ChildNumber::Normal { index: 0x8000_0000 },
1402 ]);
1403 let error = ledger_multisig_display_plan(multisig).expect_err("out-of-range address index");
1404 assert_eq!(
1405 error,
1406 "Ledger Bitcoin app requires derivation paths ending with /0/* or /1/* for multisig"
1407 );
1408 }
1409}