Skip to main content

bhwi_async/signing/
ledger.rs

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/// A script family used to order Ledger signing passes.
22#[derive(Debug, Clone, Copy, Eq, PartialEq)]
23pub enum LedgerAddressType {
24    /// Taproot key-path signing.
25    Tap,
26    /// Native SegWit signing.
27    Wit,
28    /// P2SH-wrapped SegWit signing.
29    ShWit,
30    /// Legacy signing.
31    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/// A wallet policy inferred from PSBT metadata for a Ledger signing pass.
55#[derive(Debug, Clone, Eq, PartialEq)]
56pub enum LedgerSigningPlan {
57    /// A standard single-key account policy that needs no registration.
58    Default {
59        /// The account's script family.
60        address_type: LedgerAddressType,
61        /// The account-level key derivation path.
62        account_path: DerivationPath,
63    },
64    /// A multisig policy that must be registered before signing.
65    Registered {
66        /// The policy's script family.
67        address_type: LedgerAddressType,
68        /// The generated registration name.
69        name: String,
70        /// The wallet-policy descriptor text.
71        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
85/// Infers distinct standard-account and supported multisig signing policies from a PSBT.
86///
87/// Uses `fingerprint` to identify owned inputs and `network` for account paths.
88/// Skips inputs with neither UTXO field before checking ownership. For inputs
89/// with UTXO data, returns an error if an owned input's policy cannot be inferred.
90/// Plans are ordered Taproot, native SegWit, wrapped SegWit, then legacy; no I/O occurs.
91pub 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    // sortedmulti semantics do not depend on the key-info order. Canonicalize it so
360    // inputs at different indexes reconstruct one stable registered wallet.
361    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
478/// Appends the receive branch and first address index (`/0/0`) to an account path.
479pub 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/// A multisig policy and concrete derivation prepared for Ledger address display.
615#[derive(Debug)]
616pub struct LedgerMultisigDisplayPlan {
617    /// The generated wallet-registration name.
618    pub name: String,
619    /// The ranged wallet-policy descriptor text.
620    pub policy_text: String,
621    /// The parsed ranged wallet policy.
622    pub policy: WalletPolicy,
623    /// Whether the concrete address uses the change branch.
624    pub change: bool,
625    /// The concrete address's unhardened child index.
626    pub address_index: u32,
627}
628
629/// Converts a concrete multisig request into a ranged Ledger policy and display coordinates.
630///
631/// Requires 1–16 origin-bearing xpubs with the same unhardened `/0/index` or
632/// `/1/index` suffix and no wildcard. Origin paths may contain at most four
633/// components. This prepares data without registering or displaying anything.
634pub 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
716/// Creates Ledger context with a version-2 policy and an optional registration HMAC.
717///
718/// An absent HMAC is appropriate for a supported default policy; this helper
719/// does not verify that the supplied policy is a default policy.
720pub 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/// A Ledger policy-inference or registration failure.
732#[derive(Debug, thiserror::Error)]
733pub enum LedgerSigningError {
734    /// PSBT or descriptor data could not be converted into a supported policy.
735    #[error("{0}")]
736    BadArgument(
737        /// The policy-conversion failure message.
738        String,
739    ),
740
741    /// A wallet operation failed.
742    #[error(transparent)]
743    Device(
744        /// The wallet-operation error.
745        #[from]
746        HWIDeviceError,
747    ),
748
749    /// Registration returned no HMAC, including pending confirmation.
750    #[error("Ledger wallet registration returned no HMAC")]
751    MissingHmac,
752
753    /// Registration returned a HMAC with an unexpected byte length.
754    #[error("Ledger wallet registration returned a {0}-byte HMAC instead of 32 bytes")]
755    HmacLength(
756        /// The returned length in bytes.
757        usize,
758    ),
759}
760
761/// The script family and backend context for one Ledger signing pass.
762pub struct LedgerSigningContext {
763    /// The script family used to order signing passes.
764    pub address_type: LedgerAddressType,
765    /// The Ledger wallet policy and optional registration HMAC.
766    pub context: DeviceContext,
767}
768
769/// Prepares signing contexts, querying keys and registering inferred multisig policies.
770///
771/// Uses [`ledger_signing_plans`], including its omission of inputs without UTXO
772/// data. Registration may prompt the user. This function prepares contexts rather
773/// than signing; a missing registration HMAC is an error, even if confirmation
774/// is pending.
775pub 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
835/// Registers a multisig policy and prepares its address-display request and context.
836///
837/// Registration may prompt the user, but this function does not display the
838/// address. A missing HMAC, including pending confirmation, is an error.
839pub 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}