libelectrum2descriptors/
electrum_extended_pub_key.rs

1use crate::{Descriptors, Electrum2DescriptorError, ElectrumExtendedKey};
2use bitcoin::base58;
3use bitcoin::bip32::{ChainCode, ChildNumber, Fingerprint, Xpub};
4use bitcoin::secp256k1;
5use bitcoin::{Network, NetworkKind};
6use std::convert::TryInto;
7use std::str::FromStr;
8
9pub struct ElectrumExtendedPubKey {
10    xpub: Xpub,
11    kind: String,
12}
13
14type SentinelMap = Vec<([u8; 4], Network, String)>;
15fn initialize_sentinels() -> SentinelMap {
16    // electrum testnet
17    // https://github.com/spesmilo/electrum/blob/928e43fc530ba5befa062db788e4e04d56324161/electrum/constants.py#L118-L124
18    //     XPUB_HEADERS = {
19    //         'standard':    0x043587cf,  # tpub
20    //         'p2wpkh-p2sh': 0x044a5262,  # upub
21    //         'p2wsh-p2sh':  0x024289ef,  # Upub
22    //         'p2wpkh':      0x045f1cf6,  # vpub
23    //         'p2wsh':       0x02575483,  # Vpub
24    //     }
25    // electrum mainnet
26    // https://github.com/spesmilo/electrum/blob/928e43fc530ba5befa062db788e4e04d56324161/electrum/constants.py#L82-L88
27    //     XPUB_HEADERS = {
28    //         'standard':    0x0488b21e,  # xpub
29    //         'p2wpkh-p2sh': 0x049d7cb2,  # ypub
30    //         'p2wsh-p2sh':  0x0295b43f,  # Ypub
31    //         'p2wpkh':      0x04b24746,  # zpub
32    //         'p2wsh':       0x02aa7ed3,  # Zpub
33    //     }
34
35    vec![
36        (
37            [0x04u8, 0x35, 0x87, 0xcf],
38            Network::Testnet,
39            "pkh".to_string(),
40        ), // tpub
41        (
42            [0x04u8, 0x4a, 0x52, 0x62],
43            Network::Testnet,
44            "sh(wpkh".to_string(),
45        ), // upub
46        (
47            [0x02u8, 0x42, 0x89, 0xef],
48            Network::Testnet,
49            "sh(wsh".to_string(),
50        ), // Upub
51        (
52            [0x04u8, 0x5f, 0x1c, 0xf6],
53            Network::Testnet,
54            "wpkh".to_string(),
55        ), // vpub
56        (
57            [0x02u8, 0x57, 0x54, 0x83],
58            Network::Testnet,
59            "wsh".to_string(),
60        ), // Vpub
61        (
62            [0x04u8, 0x88, 0xB2, 0x1E],
63            Network::Bitcoin,
64            "pkh".to_string(),
65        ), // xpub
66        (
67            [0x04u8, 0x9d, 0x7c, 0xb2],
68            Network::Bitcoin,
69            "sh(wpkh".to_string(),
70        ), // ypub
71        (
72            [0x02u8, 0x95, 0xb4, 0x3f],
73            Network::Bitcoin,
74            "sh(wsh".to_string(),
75        ), // Ypub
76        (
77            [0x04u8, 0xb2, 0x47, 0x46],
78            Network::Bitcoin,
79            "wpkh".to_string(),
80        ), // zpub
81        (
82            [0x02u8, 0xaa, 0x7e, 0xd3],
83            Network::Bitcoin,
84            "wsh".to_string(),
85        ), // Zpub
86    ]
87}
88
89impl FromStr for ElectrumExtendedPubKey {
90    type Err = Electrum2DescriptorError;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        let data = base58::decode_check(s)?;
94
95        if data.len() != 78 {
96            return Err(Electrum2DescriptorError::InvalidLength(data.len()));
97        }
98
99        let cn_int = u32::from_be_bytes(data[9..13].try_into().unwrap());
100        let child_number: ChildNumber = ChildNumber::from(cn_int);
101        let (network, kind) = match_electrum_xpub(&data[0..4])?;
102
103        let xpub = Xpub {
104            network: network.into(),
105            depth: data[4],
106            parent_fingerprint: Fingerprint::from(&data[5..9].try_into().unwrap()),
107            child_number,
108            chain_code: ChainCode::from(&data[13..45].try_into().unwrap()),
109            public_key: secp256k1::PublicKey::from_slice(&data[45..78])?,
110        };
111        Ok(ElectrumExtendedPubKey { xpub, kind })
112    }
113}
114
115impl ElectrumExtendedKey for ElectrumExtendedPubKey {
116    /// Returns the kind
117    fn kind(&self) -> &str {
118        &self.kind
119    }
120
121    /// Returns the xpub as String
122    fn xkey_str(&self) -> String {
123        self.xpub.to_string()
124    }
125
126    /// Returns internal and external descriptor
127    fn to_descriptors(&self) -> Descriptors {
128        let xpub = self.xpub.to_string();
129        let closing_parenthesis = if self.kind.contains('(') { ")" } else { "" };
130        let [external, change] =
131            [0, 1].map(|i| format!("{}({}/{}/*){}", self.kind, xpub, i, closing_parenthesis));
132        Descriptors { external, change }
133    }
134}
135
136impl ElectrumExtendedPubKey {
137    /// Constructs a new instance
138    pub fn new(xpub: Xpub, kind: String) -> Self {
139        ElectrumExtendedPubKey { xpub, kind }
140    }
141
142    /// Returns the xpub
143    pub fn xpub(&self) -> &Xpub {
144        &self.xpub
145    }
146
147    /// converts to electrum format
148    pub fn electrum_xpub(&self) -> Result<String, Electrum2DescriptorError> {
149        let sentinels = initialize_sentinels();
150        let sentinel = sentinels
151            .iter()
152            .find(|sent| NetworkKind::from(sent.1) == self.xpub.network && sent.2 == self.kind)
153            .ok_or_else(|| Electrum2DescriptorError::UnknownType)?;
154        let mut data = Vec::from(&sentinel.0[..]);
155        data.push(self.xpub.depth);
156        data.extend(self.xpub.parent_fingerprint.as_bytes());
157        let child_number: u32 = self.xpub.child_number.into();
158        data.extend(child_number.to_be_bytes());
159        data.extend(self.xpub.chain_code.as_bytes());
160        data.extend(&self.xpub.public_key.serialize()); // or serialize_uncompressed
161
162        if data.len() != 78 {
163            return Err(Electrum2DescriptorError::InvalidLength(data.len()));
164        }
165
166        Ok(base58::encode_check(&data))
167    }
168}
169
170fn match_electrum_xpub(version: &[u8]) -> Result<(Network, String), Electrum2DescriptorError> {
171    let sentinels = initialize_sentinels();
172    let sentinel = sentinels
173        .iter()
174        .find(|sent| sent.0 == version)
175        .ok_or_else(|| {
176            Electrum2DescriptorError::InvalidExtendedKeyVersion(version[0..4].try_into().unwrap())
177        })?;
178    Ok((sentinel.1, sentinel.2.clone()))
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use miniscript::bitcoin::secp256k1::Secp256k1;
185    use miniscript::descriptor::DescriptorPublicKey;
186    use std::str::FromStr;
187
188    #[test]
189    fn test_vpub_from_electrum() {
190        let electrum_xpub = ElectrumExtendedPubKey::from_str("vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv").unwrap();
191        assert_eq!(electrum_xpub.xpub.to_string(),"tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp");
192        assert_eq!(electrum_xpub.kind, "wpkh");
193        let descriptors = electrum_xpub.to_descriptors();
194        assert_eq!(descriptors.external, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/0/*)");
195        assert_eq!(descriptors.change, "wpkh(tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp/1/*)");
196        let xpub = electrum_xpub.xpub();
197        assert_eq!(xpub.to_string(), "tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp");
198    }
199
200    #[test]
201    fn test_vpub_to_electrum() {
202        let electrum_xpub = ElectrumExtendedPubKey::new(
203            Xpub::from_str("tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp").unwrap(),
204            "wpkh".to_string(),
205        );
206        assert_eq!(electrum_xpub.xpub.to_string(),"tpubD9ZjaMn3rbP1cAVwJy6UcEjFfTLT7W6DbfHdS3Wn48meExtVfKmiH9meWCrSmE9qXLYbGcHC5LxLcdfLZTzwme23qAJoRzRhzbd68dHeyjp");
207        assert_eq!(electrum_xpub.kind, "wpkh");
208        assert_eq!(electrum_xpub.electrum_xpub().unwrap(), "vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv");
209    }
210
211    #[test]
212    fn test_vpub_roundtrip() {
213        let elxpub = "vpub5VXaSncXqxLbdmvrC4Y8z9CszPwuEscADoetWhfrxDFzPUbL5nbVtanYDkrVEutkv9n5A5aCcvRC9swbjDKgHjCZ2tAeae8VsBuPbS8KpXv";
214        let electrum_xpub = ElectrumExtendedPubKey::from_str(elxpub).unwrap();
215        assert_eq!(electrum_xpub.electrum_xpub().unwrap(), elxpub);
216        assert_ne!(elxpub, electrum_xpub.xpub.to_string());
217    }
218
219    #[test]
220    fn test_slip121_vectors() {
221        // from https://github.com/satoshilabs/slips/blob/master/slip-0132.md
222        test_first_address("xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj","1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA");
223        test_first_address("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP","37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf");
224        test_first_address("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs","bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
225    }
226
227    fn test_first_address(electrum_xpub: &str, expected_first_address: &str) {
228        let electrum_xpub = ElectrumExtendedPubKey::from_str(electrum_xpub).unwrap();
229        assert_eq!(electrum_xpub.xpub.network, Network::Bitcoin.into());
230        let descriptors = electrum_xpub.to_descriptors();
231        let descriptor: miniscript::Descriptor<DescriptorPublicKey> =
232            descriptors.external.parse().unwrap();
233        let secp = Secp256k1::verification_only();
234        let first_address = descriptor
235            .at_derivation_index(0)
236            .unwrap()
237            .derived_descriptor(&secp)
238            .unwrap()
239            .address(miniscript::bitcoin::Network::Bitcoin)
240            .unwrap()
241            .to_string();
242        assert_eq!(expected_first_address, first_address);
243    }
244}