wagyu_ethereum/
extended_public_key.rs

1use crate::address::EthereumAddress;
2use crate::derivation_path::EthereumDerivationPath;
3use crate::extended_private_key::EthereumExtendedPrivateKey;
4use crate::format::EthereumFormat;
5use crate::network::EthereumNetwork;
6use crate::public_key::EthereumPublicKey;
7use wagyu_model::{
8    crypto::{checksum, hash160},
9    AddressError, ChildIndex, DerivationPath, ExtendedPrivateKey, ExtendedPublicKey, ExtendedPublicKeyError, PublicKey,
10};
11
12use base58::{FromBase58, ToBase58};
13use hex;
14use hmac::{Hmac, Mac};
15use secp256k1::{PublicKey as Secp256k1_PublicKey, Secp256k1, SecretKey};
16use sha2::Sha512;
17use std::{convert::TryFrom, fmt, marker::PhantomData, str::FromStr};
18
19type HmacSha512 = Hmac<Sha512>;
20
21/// Represents a Ethereum extended public key
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
23pub struct EthereumExtendedPublicKey<N: EthereumNetwork> {
24    /// The depth of key derivation, e.g. 0x00 for master nodes, 0x01 for level-1 derived keys, ...
25    depth: u8,
26    /// The first 32 bits of the key identifier (hash160(ECDSA_public_key))
27    parent_fingerprint: [u8; 4],
28    /// The child index of the key (0 for master key)
29    child_index: ChildIndex,
30    /// The chain code from the extended private key
31    chain_code: [u8; 32],
32    /// The Ethereum public key
33    public_key: EthereumPublicKey,
34    /// PhantomData
35    _network: PhantomData<N>,
36}
37
38impl<N: EthereumNetwork> ExtendedPublicKey for EthereumExtendedPublicKey<N> {
39    type Address = EthereumAddress;
40    type DerivationPath = EthereumDerivationPath<N>;
41    type ExtendedPrivateKey = EthereumExtendedPrivateKey<N>;
42    type Format = EthereumFormat;
43    type PublicKey = EthereumPublicKey;
44
45    /// Returns the extended public key of the corresponding extended private key.
46    fn from_extended_private_key(extended_private_key: &Self::ExtendedPrivateKey) -> Self {
47        Self {
48            depth: extended_private_key.depth,
49            parent_fingerprint: extended_private_key.parent_fingerprint,
50            child_index: extended_private_key.child_index,
51            chain_code: extended_private_key.chain_code,
52            public_key: extended_private_key.to_public_key(),
53            _network: PhantomData,
54        }
55    }
56
57    /// Returns the extended public key for the given derivation path.
58    fn derive(&self, path: &Self::DerivationPath) -> Result<Self, ExtendedPublicKeyError> {
59        if self.depth == 255 {
60            return Err(ExtendedPublicKeyError::MaximumChildDepthReached(self.depth));
61        }
62
63        let mut extended_public_key = self.clone();
64
65        for index in path.to_vec()?.into_iter() {
66            let public_key_serialized = &self.public_key.to_secp256k1_public_key().serialize()[..];
67
68            let mut mac = HmacSha512::new_varkey(&self.chain_code)?;
69            match index {
70                // HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i))
71                ChildIndex::Normal(_) => mac.input(public_key_serialized),
72                // Return failure
73                ChildIndex::Hardened(_) => {
74                    return Err(ExtendedPublicKeyError::InvalidChildNumber(1 << 31, u32::from(index)))
75                }
76            }
77            // Append the child index in big-endian format
78            mac.input(&u32::from(index).to_be_bytes());
79            let hmac = mac.result().code();
80
81            let mut chain_code = [0u8; 32];
82            chain_code[0..32].copy_from_slice(&hmac[32..]);
83
84            let mut public_key = self.public_key.to_secp256k1_public_key();
85            public_key.add_exp_assign(&Secp256k1::new(), &SecretKey::from_slice(&hmac[..32])?[..])?;
86            let public_key = Self::PublicKey::from_secp256k1_public_key(public_key);
87
88            let mut parent_fingerprint = [0u8; 4];
89            parent_fingerprint.copy_from_slice(&hash160(public_key_serialized)[0..4]);
90
91            extended_public_key = Self {
92                depth: extended_public_key.depth + 1,
93                parent_fingerprint,
94                child_index: index,
95                chain_code,
96                public_key,
97                _network: PhantomData,
98            };
99        }
100
101        Ok(extended_public_key)
102    }
103
104    /// Returns the public key of the corresponding extended public key.
105    fn to_public_key(&self) -> Self::PublicKey {
106        self.public_key
107    }
108
109    /// Returns the address of the corresponding extended public key.
110    fn to_address(&self, _format: &Self::Format) -> Result<Self::Address, AddressError> {
111        self.public_key.to_address(_format)
112    }
113}
114
115impl<N: EthereumNetwork> FromStr for EthereumExtendedPublicKey<N> {
116    type Err = ExtendedPublicKeyError;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        let data = s.from_base58()?;
120        if data.len() != 82 {
121            return Err(ExtendedPublicKeyError::InvalidByteLength(data.len()));
122        }
123
124        if &data[0..4] != [0x04u8, 0x88, 0xB2, 0x1E] {
125            return Err(ExtendedPublicKeyError::InvalidVersionBytes(data[0..4].to_vec()));
126        };
127
128        let depth = data[4] as u8;
129
130        let mut parent_fingerprint = [0u8; 4];
131        parent_fingerprint.copy_from_slice(&data[5..9]);
132
133        let child_index = ChildIndex::from(u32::from_be_bytes(<[u8; 4]>::try_from(&data[9..13])?));
134
135        let mut chain_code = [0u8; 32];
136        chain_code.copy_from_slice(&data[13..45]);
137
138        let public_key = EthereumPublicKey::from_str(&hex::encode(
139            &Secp256k1_PublicKey::from_slice(&data[45..78])?.serialize_uncompressed()[1..],
140        ))?;
141
142        let expected = &data[78..82];
143        let checksum = &checksum(&data[0..78])[0..4];
144        if *expected != *checksum {
145            let expected = expected.to_base58();
146            let found = checksum.to_base58();
147            return Err(ExtendedPublicKeyError::InvalidChecksum(expected, found));
148        }
149
150        Ok(Self {
151            depth,
152            parent_fingerprint,
153            child_index,
154            chain_code,
155            public_key,
156            _network: PhantomData,
157        })
158    }
159}
160
161impl<N: EthereumNetwork> fmt::Display for EthereumExtendedPublicKey<N> {
162    /// BIP32 serialization format
163    /// https://github.com/ethereum/bips/blob/master/bip-0032.mediawiki#serialization-format
164    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
165        let mut result = [0u8; 82];
166        result[0..4].copy_from_slice(&[0x04u8, 0x88, 0xB2, 0x1E][..]);
167        result[4] = self.depth as u8;
168        result[5..9].copy_from_slice(&self.parent_fingerprint[..]);
169        result[9..13].copy_from_slice(&u32::from(self.child_index).to_be_bytes());
170        result[13..45].copy_from_slice(&self.chain_code[..]);
171        result[45..78].copy_from_slice(&self.public_key.to_secp256k1_public_key().serialize()[..]);
172
173        let sum = &checksum(&result[0..78])[0..4];
174        result[78..82].copy_from_slice(sum);
175
176        fmt.write_str(&result.to_base58())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::network::*;
184    use wagyu_model::extended_private_key::ExtendedPrivateKey;
185
186    use hex;
187    use std::convert::TryInto;
188
189    fn test_from_extended_private_key<N: EthereumNetwork>(
190        expected_extended_public_key: &str,
191        expected_public_key: &str,
192        expected_child_index: u32,
193        expected_chain_code: &str,
194        expected_parent_fingerprint: &str,
195        extended_private_key: &str,
196    ) {
197        let extended_private_key = EthereumExtendedPrivateKey::<N>::from_str(extended_private_key).unwrap();
198        let extended_public_key = EthereumExtendedPublicKey::<N>::from_extended_private_key(&extended_private_key);
199        assert_eq!(expected_extended_public_key, extended_public_key.to_string());
200        assert_eq!(
201            expected_public_key,
202            extended_public_key.public_key.to_secp256k1_public_key().to_string()
203        );
204        assert_eq!(expected_child_index, u32::from(extended_public_key.child_index));
205        assert_eq!(expected_chain_code, hex::encode(extended_public_key.chain_code));
206        assert_eq!(
207            expected_parent_fingerprint,
208            hex::encode(extended_public_key.parent_fingerprint)
209        );
210    }
211
212    // Check: (extended_private_key1 -> extended_private_key2 -> extended_public_key2) == (expected_extended_public_key2)
213    fn test_derive<N: EthereumNetwork>(
214        expected_extended_private_key1: &str,
215        expected_extended_public_key2: &str,
216        expected_child_index2: u32,
217    ) {
218        let path = vec![ChildIndex::from(expected_child_index2)].try_into().unwrap();
219
220        let extended_private_key1 = EthereumExtendedPrivateKey::<N>::from_str(expected_extended_private_key1).unwrap();
221        let extended_private_key2 = extended_private_key1.derive(&path).unwrap();
222        let extended_public_key2 = extended_private_key2.to_extended_public_key();
223
224        let expected_extended_public_key2 =
225            EthereumExtendedPublicKey::<N>::from_str(&expected_extended_public_key2).unwrap();
226
227        assert_eq!(expected_extended_public_key2, extended_public_key2);
228        assert_eq!(
229            expected_extended_public_key2.public_key,
230            extended_public_key2.public_key
231        );
232        assert_eq!(expected_extended_public_key2.depth, extended_public_key2.depth);
233        assert_eq!(
234            expected_extended_public_key2.child_index,
235            extended_public_key2.child_index
236        );
237        assert_eq!(
238            expected_extended_public_key2.chain_code,
239            extended_public_key2.chain_code
240        );
241        assert_eq!(
242            expected_extended_public_key2.parent_fingerprint,
243            extended_public_key2.parent_fingerprint
244        );
245    }
246
247    fn test_from_str<N: EthereumNetwork>(
248        expected_public_key: &str,
249        expected_child_index: u32,
250        expected_chain_code: &str,
251        expected_parent_fingerprint: &str,
252        extended_public_key: &str,
253    ) {
254        let extended_public_key = EthereumExtendedPublicKey::<N>::from_str(&extended_public_key).unwrap();
255        assert_eq!(
256            expected_public_key,
257            extended_public_key.public_key.to_secp256k1_public_key().to_string()
258        );
259        assert_eq!(expected_child_index, u32::from(extended_public_key.child_index));
260        assert_eq!(expected_chain_code, hex::encode(extended_public_key.chain_code));
261        assert_eq!(
262            expected_parent_fingerprint,
263            hex::encode(extended_public_key.parent_fingerprint)
264        );
265    }
266
267    fn test_to_string<N: EthereumNetwork>(expected_extended_public_key: &str) {
268        let extended_public_key = EthereumExtendedPublicKey::<N>::from_str(&expected_extended_public_key).unwrap();
269        assert_eq!(expected_extended_public_key, extended_public_key.to_string());
270    }
271
272    mod bip32_mainnet {
273        use super::*;
274
275        type N = Mainnet;
276
277        // (path, seed, child_index, public_key, chain_code, parent_fingerprint, extended_private_key, extended_public_key)
278        const KEYPAIRS: [(&str, &str, &str, &str, &str, &str, &str, &str); 12] = [
279            (
280                "m",
281                "000102030405060708090a0b0c0d0e0f",
282                "0",
283                "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2",
284                "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508",
285                "00000000",
286                "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
287                "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
288            ),
289            (
290                "m/0'",
291                "000102030405060708090a0b0c0d0e0f",
292                "2147483648",
293                "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56",
294                "47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141",
295                "3442193e",
296                "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7",
297                "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
298            ),
299            (
300                "m/0'/1",
301                "000102030405060708090a0b0c0d0e0f",
302                "1",
303                "03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c",
304                "2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19",
305                "5c1bd648",
306                "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs",
307                "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
308            ),
309            (
310                "m/0'/1/2'",
311                "000102030405060708090a0b0c0d0e0f",
312                "2147483650",
313                "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2",
314                "04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f",
315                "bef5a2f9",
316                "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM",
317                "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"
318            ),
319            (
320                "m/0'/1/2'/2",
321                "000102030405060708090a0b0c0d0e0f",
322                "2",
323                "02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29",
324                "cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd",
325                "ee7ab90c",
326                "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334",
327                "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
328            ),
329            (
330                "m/0'/1/2'/2/1000000000",
331                "000102030405060708090a0b0c0d0e0f",
332                "1000000000",
333                "022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011",
334                "c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e",
335                "d880d7d8",
336                "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76",
337                "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"
338            ),
339            (
340                "m",
341                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
342                "0",
343                "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7",
344                "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689",
345                "00000000",
346                "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U",
347                "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
348            ),
349            (
350                "m/0",
351                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
352                "0",
353                "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea",
354                "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c",
355                "bd16bee5",
356                "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt",
357                "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
358            ),
359            (
360                "m/0/2147483647'",
361                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
362                "4294967295",
363                "03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b",
364                "be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9",
365                "5a61ff8e",
366                "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9",
367                "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"
368            ),
369            (
370                "m/0/2147483647'/1",
371                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
372                "1",
373                "03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9",
374                "f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb",
375                "d8ab4937",
376                "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef",
377                "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
378            ),
379            (
380                "m/0/2147483647'/1/2147483646'",
381                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
382                "4294967294",
383                "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0",
384                "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29",
385                "78412e3a",
386                "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc",
387                "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
388            ),
389            (
390                "m/0/2147483647'/1/2147483646'/2",
391                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
392                "2",
393                "024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c",
394                "9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271",
395                "31a507b8",
396                "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j",
397                "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"
398            ),
399        ];
400
401        #[test]
402        fn from_extended_private_key() {
403            KEYPAIRS.iter().for_each(
404                |(
405                    _,
406                    _,
407                    child_index,
408                    public_key,
409                    chain_code,
410                    parent_fingerprint,
411                    extended_private_key,
412                    extended_public_key,
413                )| {
414                    test_from_extended_private_key::<N>(
415                        extended_public_key,
416                        public_key,
417                        child_index.parse().unwrap(),
418                        chain_code,
419                        parent_fingerprint,
420                        extended_private_key,
421                    );
422                },
423            );
424        }
425
426        #[test]
427        fn derive() {
428            KEYPAIRS.chunks(2).for_each(|pair| {
429                let (_, _, _, _, _, _, expected_extended_private_key1, _) = pair[0];
430                let (_, _, expected_child_index2, _, _, _, _, expected_extended_public_key2) = pair[1];
431                test_derive::<N>(
432                    expected_extended_private_key1,
433                    expected_extended_public_key2,
434                    expected_child_index2.parse().unwrap(),
435                );
436            });
437        }
438
439        #[test]
440        fn from_str() {
441            KEYPAIRS.iter().for_each(
442                |(_, _, child_index, public_key, chain_code, parent_fingerprint, _, extended_public_key)| {
443                    test_from_str::<N>(
444                        public_key,
445                        child_index.parse().unwrap(),
446                        chain_code,
447                        parent_fingerprint,
448                        extended_public_key,
449                    );
450                },
451            );
452        }
453
454        #[test]
455        fn to_string() {
456            KEYPAIRS.iter().for_each(|(_, _, _, _, _, _, _, extended_public_key)| {
457                test_to_string::<N>(extended_public_key);
458            });
459        }
460    }
461
462    mod test_invalid {
463        use super::*;
464
465        type N = Mainnet;
466
467        const INVALID_EXTENDED_PUBLIC_KEY__SECP256K1_PUBLIC_KEY: &str = "xpub661MyMwAqRbcftXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
468        const INVALID_EXTENDED_PUBLIC_KEY_NETWORK: &str = "xpub561MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
469        const INVALID_EXTENDED_PUBLIC_KEY_CHECKSUM: &str = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet7";
470        const VALID_EXTENDED_PUBLIC_KEY: &str = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
471
472        #[test]
473        #[should_panic(expected = "Crate(\"secp256k1\", \"InvalidPublicKey\")")]
474        fn from_str_invalid_secret_key() {
475            let _result =
476                EthereumExtendedPublicKey::<N>::from_str(INVALID_EXTENDED_PUBLIC_KEY__SECP256K1_PUBLIC_KEY).unwrap();
477        }
478
479        #[test]
480        #[should_panic(expected = "InvalidVersionBytes([4, 136, 178, 29])")]
481        fn from_str_invalid_version() {
482            let _result = EthereumExtendedPublicKey::<N>::from_str(INVALID_EXTENDED_PUBLIC_KEY_NETWORK).unwrap();
483        }
484
485        #[test]
486        #[should_panic(expected = "InvalidChecksum(\"5Nvot3\", \"5Nvot4\")")]
487        fn from_str_invalid_checksum() {
488            let _result = EthereumExtendedPublicKey::<N>::from_str(INVALID_EXTENDED_PUBLIC_KEY_CHECKSUM).unwrap();
489        }
490
491        #[test]
492        #[should_panic(expected = "InvalidByteLength(81)")]
493        fn from_str_short() {
494            let _result = EthereumExtendedPublicKey::<N>::from_str(&VALID_EXTENDED_PUBLIC_KEY[1..]).unwrap();
495        }
496
497        #[test]
498        #[should_panic(expected = "InvalidByteLength(83)")]
499        fn from_str_long() {
500            let mut string = String::from(VALID_EXTENDED_PUBLIC_KEY);
501            string.push('a');
502            let _result = EthereumExtendedPublicKey::<N>::from_str(&string).unwrap();
503        }
504    }
505}