bpwallet/
bip43.rs

1// Modern, minimalistic & standard-compliant cold wallet library.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2020-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved.
9// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved.
10//
11// Licensed under the Apache License, Version 2.0 (the "License");
12// you may not use this file except in compliance with the License.
13// You may obtain a copy of the License at
14//
15//     http://www.apache.org/licenses/LICENSE-2.0
16//
17// Unless required by applicable law or agreed to in writing, software
18// distributed under the License is distributed on an "AS IS" BASIS,
19// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20// See the License for the specific language governing permissions and
21// limitations under the License.
22
23use std::str::FromStr;
24
25use bpstd::{DerivationIndex, DerivationPath, HardenedIndex, Idx, IdxBase, NormalIndex};
26
27/// Errors in parsing derivation scheme string representation
28#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Error, Display)]
29#[display(doc_comments)]
30pub enum ParseBip43Error {
31    /// invalid blockchain name {0}; it must be either `bitcoin`, `testnet` or
32    /// hardened index number
33    InvalidBlockchainName(String),
34
35    /// LNPBP-43 blockchain index {0} must be hardened
36    UnhardenedBlockchainIndex(u32),
37
38    /// invalid LNPBP-43 identity representation {0}
39    InvalidIdentityIndex(String),
40
41    /// invalid BIP-43 purpose {0}
42    InvalidPurposeIndex(String),
43
44    /// BIP-{0} support is not implemented (of BIP with this number does not
45    /// exist)
46    UnimplementedBip(u16),
47
48    /// derivation path can't be recognized as one of BIP-43-based standards
49    UnrecognizedBipScheme,
50
51    /// BIP-43 scheme must have form of `bip43/<purpose>h`
52    InvalidBip43Scheme,
53
54    /// BIP-48 scheme must have form of `bip48-native` or `bip48-nested`
55    InvalidBip48Scheme,
56
57    /// invalid derivation path `{0}`
58    InvalidDerivationPath(String),
59}
60
61/// Specific derivation scheme after BIP-43 standards
62#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)]
63#[cfg_attr(feature = "clap", derive(ValueEnum))]
64#[non_exhaustive]
65pub enum Bip43 {
66    /// Account-based P2PKH derivation.
67    ///
68    /// `m / 44' / coin_type' / account'`
69    #[display("bip44", alt = "m/44h")]
70    Bip44,
71
72    /// Account-based native P2WPKH derivation.
73    ///
74    /// `m / 84' / coin_type' / account'`
75    #[display("bip84", alt = "m/84h")]
76    Bip84,
77
78    /// Account-based legacy P2WPH-in-P2SH derivation.
79    ///
80    /// `m / 49' / coin_type' / account'`
81    #[display("bip49", alt = "m/49h")]
82    Bip49,
83
84    /// Account-based single-key P2TR derivation.
85    ///
86    /// `m / 86' / coin_type' / account'`
87    #[display("bip86", alt = "m/86h")]
88    Bip86,
89
90    /// Cosigner-index-based multisig derivation.
91    ///
92    /// `m / 45' / cosigner_index
93    #[display("bip45", alt = "m/45h")]
94    Bip45,
95
96    /// Account-based multisig derivation with sorted keys & P2WSH nested.
97    /// scripts
98    ///
99    /// `m / 48' / coin_type' / account' / 1'`
100    #[display("bip48-nested", alt = "m/48h//1h")]
101    Bip48Nested,
102
103    /// Account-based multisig derivation with sorted keys & P2WSH native.
104    /// scripts
105    ///
106    /// `m / 48' / coin_type' / account' / 2'`
107    #[display("bip48-native", alt = "m/48h//2h")]
108    Bip48Native,
109
110    /// Account- & descriptor-based derivation for multi-sig wallets.
111    ///
112    /// `m / 87' / coin_type' / account'`
113    #[display("bip87", alt = "m/87h")]
114    Bip87,
115
116    /// Generic BIP43 derivation with custom (non-standard) purpose value.
117    ///
118    /// `m / purpose'`
119    #[display("bip43/{purpose}", alt = "m/{purpose}")]
120    #[cfg_attr(feature = "clap", clap(skip))]
121    Bip43 {
122        /// Purpose value
123        purpose: HardenedIndex,
124    },
125}
126
127impl FromStr for Bip43 {
128    type Err = ParseBip43Error;
129
130    fn from_str(s: &str) -> Result<Self, Self::Err> {
131        let s = s.to_lowercase();
132        let bip = s.strip_prefix("bip").or_else(|| s.strip_prefix("m/"));
133        Ok(match bip {
134            Some("44") => Bip43::Bip44,
135            Some("84") => Bip43::Bip84,
136            Some("49") => Bip43::Bip49,
137            Some("86") => Bip43::Bip86,
138            Some("45") => Bip43::Bip45,
139            Some(bip48) if bip48.starts_with("48//") => match bip48
140                .strip_prefix("48//")
141                .and_then(|index| HardenedIndex::from_str(index).ok())
142            {
143                Some(script_type) if script_type == 1u8 => Bip43::Bip48Nested,
144                Some(script_type) if script_type == 2u8 => Bip43::Bip48Native,
145                _ => {
146                    return Err(ParseBip43Error::InvalidBip48Scheme);
147                }
148            },
149            Some("48-nested") => Bip43::Bip48Nested,
150            Some("48-native") => Bip43::Bip48Native,
151            Some("87") => Bip43::Bip87,
152            Some(bip43) if bip43.starts_with("43/") => match bip43.strip_prefix("43/") {
153                Some(purpose) => {
154                    let purpose = HardenedIndex::from_str(purpose)
155                        .map_err(|_| ParseBip43Error::InvalidPurposeIndex(purpose.to_owned()))?;
156                    Bip43::Bip43 { purpose }
157                }
158                None => return Err(ParseBip43Error::InvalidBip43Scheme),
159            },
160            Some(_) | None => return Err(ParseBip43Error::UnrecognizedBipScheme),
161        })
162    }
163}
164
165impl Bip43 {
166    /// Constructs derivation standard corresponding to a single-sig P2PKH.
167    pub const PKH: Bip43 = Bip43::Bip44;
168    /// Constructs derivation standard corresponding to a single-sig
169    /// P2WPKH-in-P2SH.
170    pub const WPKH_SH: Bip43 = Bip43::Bip49;
171    /// Constructs derivation standard corresponding to a single-sig P2WPKH.
172    pub const WPKH: Bip43 = Bip43::Bip84;
173    /// Constructs derivation standard corresponding to a single-sig P2TR.
174    pub const TR_SINGLE: Bip43 = Bip43::Bip86;
175    /// Constructs derivation standard corresponding to a multi-sig P2SH BIP45.
176    pub const MULTI_SH_SORTED: Bip43 = Bip43::Bip45;
177    /// Constructs derivation standard corresponding to a multi-sig sorted
178    /// P2WSH-in-P2SH.
179    pub const MULTI_WSH_SH: Bip43 = Bip43::Bip48Nested;
180    /// Constructs derivation standard corresponding to a multi-sig sorted
181    /// P2WSH.
182    pub const MULTI_WSH: Bip43 = Bip43::Bip48Native;
183    /// Constructs derivation standard corresponding to a multi-sig BIP87.
184    pub const DESCRIPTOR: Bip43 = Bip43::Bip87;
185}
186
187/// Methods for derivation standard enumeration types.
188pub trait DerivationStandard: Eq + Clone {
189    /// Deduces derivation standard used by the provided derivation path, if
190    /// possible.
191    fn deduce(derivation: &DerivationPath) -> Option<Self>
192    where Self: Sized;
193
194    /// Get hardened index matching BIP-43 purpose value, if any.
195    fn purpose(&self) -> Option<HardenedIndex>;
196
197    /// Depth of the account extended public key according to the given
198    /// standard.
199    ///
200    /// Returns `None` if the standard does not provide information on
201    /// account-level xpubs.
202    fn account_depth(&self) -> Option<u8>;
203
204    /// Depth of the derivation path defining `coin_type` key, i.e. the used
205    /// blockchain.
206    ///
207    /// Returns `None` if the standard does not provide information on
208    /// blockchain/coin type.
209    fn coin_type_depth(&self) -> Option<u8>;
210
211    /// Returns information whether the account xpub in this standard is the
212    /// last hardened derivation path step, or there might be more hardened
213    /// steps (like `script_type` in BIP-48).
214    ///
215    /// Returns `None` if the standard does not provide information on
216    /// account-level xpubs.
217    fn is_account_last_hardened(&self) -> Option<bool>;
218
219    /// Checks which bitcoin network corresponds to a given derivation path
220    /// according to the used standard requirements.
221    fn is_testnet(&self, path: &DerivationPath) -> Result<bool, Option<DerivationIndex>>;
222
223    /// Extracts hardened index from a derivation path position defining coin
224    /// type information (used blockchain), if present.
225    ///
226    /// # Returns
227    ///
228    /// - `Err(None)` error if the path doesn't contain any coin index information;
229    /// - `Err(`[`NormalIndex`]`)` error if the coin type in the derivation path was an unhardened
230    ///   index.
231    /// - `Ok(`[`HardenedIndex`]`)` with the coin type index otherwise.
232    fn extract_coin_type(
233        &self,
234        path: &DerivationPath,
235    ) -> Result<HardenedIndex, Option<NormalIndex>> {
236        let coin = self.coin_type_depth().and_then(|i| path.get(i as usize)).ok_or(None)?;
237        match coin {
238            DerivationIndex::Normal(idx) => Err(Some(*idx)),
239            DerivationIndex::Hardened(idx) => Ok(*idx),
240        }
241    }
242
243    /// Extracts hardened index from a derivation path position defining account
244    /// number, if present.
245    ///
246    /// # Returns
247    ///
248    /// - `Err(None)` error if the path doesn't contain any account number information;
249    /// - `Err(`[`NormalIndex`]`)` error if the account number in the derivation path was an
250    ///   unhardened index.
251    /// - `Ok(`[`HardenedIndex`]`)` with the account number otherwise.
252    fn extract_account_index(
253        &self,
254        path: &DerivationPath,
255    ) -> Result<HardenedIndex, Option<NormalIndex>> {
256        let coin = self.account_depth().and_then(|i| path.get(i as usize)).ok_or(None)?;
257        match coin {
258            DerivationIndex::Normal(idx) => Err(Some(*idx)),
259            DerivationIndex::Hardened(idx) => Ok(*idx),
260        }
261    }
262
263    /// Returns string representation of the template derivation path for an
264    /// account-level keys. Account key is represented by `*` wildcard fragment.
265    fn account_template_string(&self, testnet: bool) -> String;
266
267    /// Construct derivation path for the account xpub.
268    fn to_origin_derivation(&self, testnet: bool) -> DerivationPath<HardenedIndex>;
269
270    /// Construct derivation path up to the provided account index segment.
271    fn to_account_derivation(
272        &self,
273        account_index: HardenedIndex,
274        testnet: bool,
275    ) -> DerivationPath<HardenedIndex>;
276
277    /// Construct full derivation path including address index and case
278    /// (main, change etc).
279    fn to_key_derivation(
280        &self,
281        account_index: HardenedIndex,
282        testnet: bool,
283        keychain: NormalIndex,
284        index: NormalIndex,
285    ) -> DerivationPath;
286}
287
288impl DerivationStandard for Bip43 {
289    fn deduce(derivation: &DerivationPath) -> Option<Bip43> {
290        let mut iter = derivation.into_iter();
291        let first = iter.next().map(HardenedIndex::try_from).transpose().ok()??;
292        let fourth = iter.nth(3).map(HardenedIndex::try_from);
293        Some(match (first.child_number(), fourth) {
294            (44, ..) => Bip43::Bip44,
295            (84, ..) => Bip43::Bip84,
296            (49, ..) => Bip43::Bip49,
297            (86, ..) => Bip43::Bip86,
298            (45, ..) => Bip43::Bip45,
299            (87, ..) => Bip43::Bip87,
300            (48, Some(Ok(script_type))) if script_type == 1u8 => Bip43::Bip48Nested,
301            (48, Some(Ok(script_type))) if script_type == 2u8 => Bip43::Bip48Native,
302            (48, _) => return None,
303            (purpose, ..) if derivation.len() > 2 && purpose > 2 => Bip43::Bip43 {
304                purpose: HardenedIndex::hardened(purpose as u16),
305            },
306            _ => return None,
307        })
308    }
309
310    fn purpose(&self) -> Option<HardenedIndex> {
311        Some(match self {
312            Bip43::Bip44 => HardenedIndex::hardened(44),
313            Bip43::Bip84 => HardenedIndex::hardened(84),
314            Bip43::Bip49 => HardenedIndex::hardened(49),
315            Bip43::Bip86 => HardenedIndex::hardened(86),
316            Bip43::Bip45 => HardenedIndex::hardened(45),
317            Bip43::Bip48Nested | Bip43::Bip48Native => HardenedIndex::hardened(48),
318            Bip43::Bip87 => HardenedIndex::hardened(87),
319            Bip43::Bip43 { purpose } => *purpose,
320        })
321    }
322
323    fn account_depth(&self) -> Option<u8> {
324        Some(match self {
325            Bip43::Bip45 => return None,
326            Bip43::Bip44
327            | Bip43::Bip84
328            | Bip43::Bip49
329            | Bip43::Bip86
330            | Bip43::Bip87
331            | Bip43::Bip48Nested
332            | Bip43::Bip48Native
333            | Bip43::Bip43 { .. } => 3,
334        })
335    }
336
337    fn coin_type_depth(&self) -> Option<u8> {
338        Some(match self {
339            Bip43::Bip45 => return None,
340            Bip43::Bip44
341            | Bip43::Bip84
342            | Bip43::Bip49
343            | Bip43::Bip86
344            | Bip43::Bip87
345            | Bip43::Bip48Nested
346            | Bip43::Bip48Native
347            | Bip43::Bip43 { .. } => 2,
348        })
349    }
350
351    fn is_account_last_hardened(&self) -> Option<bool> {
352        Some(match self {
353            Bip43::Bip45 => false,
354            Bip43::Bip44
355            | Bip43::Bip84
356            | Bip43::Bip49
357            | Bip43::Bip86
358            | Bip43::Bip87
359            | Bip43::Bip43 { .. } => true,
360            Bip43::Bip48Nested | Bip43::Bip48Native => false,
361        })
362    }
363
364    fn is_testnet(&self, path: &DerivationPath) -> Result<bool, Option<DerivationIndex>> {
365        match self.extract_coin_type(path) {
366            Err(None) => Err(None),
367            Err(Some(idx)) => Err(Some(idx.into())),
368            Ok(HardenedIndex::ZERO) => Ok(false),
369            Ok(HardenedIndex::ONE) => Ok(true),
370            Ok(idx) => Err(Some(idx.into())),
371        }
372    }
373
374    fn account_template_string(&self, testnet: bool) -> String {
375        let coin_type = if testnet { HardenedIndex::ONE } else { HardenedIndex::ZERO };
376        match self {
377            Bip43::Bip45
378            | Bip43::Bip44
379            | Bip43::Bip84
380            | Bip43::Bip49
381            | Bip43::Bip86
382            | Bip43::Bip87
383            | Bip43::Bip43 { .. } => format!("{:#}/{}/*h", self, coin_type),
384            Bip43::Bip48Nested => {
385                format!("{:#}", self).replace("//", &format!("/{}/*h/", coin_type))
386            }
387            Bip43::Bip48Native => {
388                format!("{:#}", self).replace("//", &format!("/{}/*h/", coin_type))
389            }
390        }
391    }
392
393    fn to_origin_derivation(&self, testnet: bool) -> DerivationPath<HardenedIndex> {
394        let mut path = Vec::with_capacity(2);
395        if let Some(purpose) = self.purpose() {
396            path.push(purpose)
397        }
398        path.push(if testnet { HardenedIndex::ONE } else { HardenedIndex::ZERO });
399        path.into()
400    }
401
402    fn to_account_derivation(
403        &self,
404        account_index: HardenedIndex,
405        testnet: bool,
406    ) -> DerivationPath<HardenedIndex> {
407        let mut path = Vec::with_capacity(4);
408        path.push(account_index);
409        if self == &Bip43::Bip48Native {
410            path.push(HardenedIndex::from(2u8));
411        } else if self == &Bip43::Bip48Nested {
412            path.push(HardenedIndex::ONE);
413        }
414        let mut derivation = self.to_origin_derivation(testnet);
415        derivation.extend(&path);
416        derivation
417    }
418
419    fn to_key_derivation(
420        &self,
421        account_index: HardenedIndex,
422        testnet: bool,
423        keychain: NormalIndex,
424        index: NormalIndex,
425    ) -> DerivationPath {
426        let mut derivation = self
427            .to_account_derivation(account_index, testnet)
428            .into_iter()
429            .map(DerivationIndex::from)
430            .collect::<DerivationPath>();
431        derivation.push(keychain.into());
432        derivation.push(index.into());
433        derivation
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_bip43_str_round_trip() {
443        fn assert_from_str_to_str(bip43: Bip43) {
444            let str = bip43.to_string();
445            let from_str = Bip43::from_str(&str).unwrap();
446
447            assert_eq!(bip43, from_str);
448        }
449
450        assert_from_str_to_str(Bip43::Bip44);
451        assert_from_str_to_str(Bip43::Bip84);
452        assert_from_str_to_str(Bip43::Bip49);
453        assert_from_str_to_str(Bip43::Bip86);
454        assert_from_str_to_str(Bip43::Bip45);
455        assert_from_str_to_str(Bip43::Bip48Nested);
456        assert_from_str_to_str(Bip43::Bip48Native);
457        assert_from_str_to_str(Bip43::Bip87);
458        assert_from_str_to_str(Bip43::Bip43 {
459            purpose: HardenedIndex::hardened(1),
460        });
461    }
462}