1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
//! SLIP-0010 and BIP-0032 compatible Secp256k1 cryptography that allows child key derivation.

mod bip32;
mod bip44;
mod ext_pk;
mod ext_sk;
mod id;
mod networks;
mod pk;
mod sig;
mod sk;

use super::*;

use digest::generic_array::{typenum::U20, GenericArray};
use digest::FixedOutput;
use ripemd160::Ripemd160;
use sha2::{Digest, Sha256};

fn hash160<B: AsRef<[u8]>>(input: B) -> GenericArray<u8, U20> {
    let mut inner_hasher = Sha256::default();
    inner_hasher.update(input);
    let mut outer_hasher = Ripemd160::default();
    outer_hasher.update(inner_hasher.finalize_fixed());
    outer_hasher.finalize_fixed()
}

const CHECKSUM_LEN: usize = 4;

/// Encoding binary data with BASE58 after adding a 4-byte checksum pops up in the Bitcoin
/// ecosystem on several places. Addresses, wallet-import-format, extended public and private
/// key serialization formats. So this transformation is pulled up here as a free function.
pub fn to_base58check<D: AsRef<[u8]>>(data: D) -> String {
    let data = data.as_ref();
    let mut inner_hasher = Sha256::default();
    inner_hasher.update(data);
    let mut outer_hasher = Sha256::default();
    outer_hasher.update(inner_hasher.finalize_fixed());
    let hash = outer_hasher.finalize_fixed();
    let checksum = &hash[..CHECKSUM_LEN];
    let mut bytes = Vec::with_capacity(data.len() + checksum.len());
    bytes.extend_from_slice(data);
    bytes.extend_from_slice(checksum);

    // we do not need the multibase prefix, but want to conform to multibase otherwise.
    // Prefix is always a single character.
    let prefixed_enc = multibase::encode(multibase::Base::Base58Btc, &bytes);
    prefixed_enc[1..].to_owned()
}

/// Decoding string with BASE58 into binary data and verify if the 4-byte checksum at the end
/// matches the rest of the data. Only the decoded data without checksum will be returned.
pub fn from_base58check<S: AsRef<str>>(s: S) -> Result<Vec<u8>> {
    let mut to_decode = String::new();
    to_decode.push(multibase::Base::Base58Btc.code());
    to_decode += s.as_ref();
    let (_base, checked_data) = multibase::decode(&to_decode)?;
    let (data, actual_checksum) = checked_data.split_at(checked_data.len() - CHECKSUM_LEN);

    let mut inner_hasher = Sha256::default();
    inner_hasher.update(data);
    let mut outer_hasher = Sha256::default();
    outer_hasher.update(inner_hasher.finalize_fixed());
    let hash = outer_hasher.finalize_fixed();
    let expected_checksum = &hash[..CHECKSUM_LEN];

    ensure!(expected_checksum == actual_checksum, "Incorrect checksum");

    Ok(data.to_vec())
}

/// This elliptic curve cryptography implements both the [AsymmetricCrypto](AsymmetricCrypto) and
/// [KeyDerivationCrypto](KeyDerivationCrypto) traits so for BTC, ETH and IOP as examples.
#[derive(Clone, Debug)]
pub struct Secp256k1;

impl Secp256k1 {
    fn hash_message<D: AsRef<[u8]>>(data: D) -> secp::Message {
        let mut hasher = Sha256::default();
        hasher.update(data.as_ref());
        let mut hash = [0u8; secp::util::MESSAGE_SIZE];
        hash.copy_from_slice(hasher.finalize_fixed().as_slice());
        secp::Message::parse(&hash)
    }
}

pub use bip32::*;
pub use bip44::*;
pub use cc::{ChainCode, CHAIN_CODE_SIZE};
pub use ext_pk::SecpExtPublicKey;
pub use ext_sk::SecpExtPrivateKey;
pub use id::{SecpKeyId, KEY_ID_SIZE, KEY_ID_VERSION1};
pub use networks::{ark, btc, hyd, iop};
pub use pk::{SecpPublicKey, PUBLIC_KEY_SIZE, PUBLIC_KEY_UNCOMPRESSED_SIZE};
pub use sig::{SecpSignature, SIGNATURE_SIZE, SIGNATURE_VERSION1};
pub use sk::{SecpPrivateKey, PRIVATE_KEY_SIZE};

impl AsymmetricCrypto for Secp256k1 {
    type KeyId = SecpKeyId;
    type PublicKey = SecpPublicKey;
    type PrivateKey = SecpPrivateKey;
    type Signature = SecpSignature;
}

impl KeyDerivationCrypto for Secp256k1 {
    type ExtendedPrivateKey = SecpExtPrivateKey;
    type ExtendedPublicKey = SecpExtPublicKey;

    fn master(seed: &Seed) -> SecpExtPrivateKey {
        SecpExtPrivateKey::from_seed(seed.as_bytes())
    }
}

/// Since Wigy could not find any constant expression for the length of `u8` in bytes
/// (`std::u8::LEN` could be a good place), this is some manual trickery to define our
/// "standard version byte length in bytes"
pub const VERSION_SIZE: usize = 1;

/// SLIP-0010 defines keyed hashing for master key derivation. This does domain separation
/// for different cryptographic algorithms. This is the standard key for BIP-0032
pub const SLIP10_SEED_HASH_SALT: &[u8] = b"Bitcoin seed";

/// [BIP-0178](https://github.com/bitcoin/bips/blob/master/bip-0178.mediawiki) is an extension
/// to the de-facto WIF to encode how the private key was used to generate receiving addresses.
/// If in doubt, just use Compressed, which is compatible with most wallets.
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Bip178 {
    /// Addresses generated with uncompressed public keys were mostly used before bitcoin core 0.6
    /// and are not economical since then. Still, if you have a WIF that does not start with L or K
    /// on the BTC mainnet, [you have to use uncompressed keys](https://www.reddit.com/r/Electrum/comments/bec22p/potential_loss_of_funds_if_import_uncompressed/).
    Uncompressed,
    /// The most common format as of 2019Q2 which only promises that the wallet did not generate the receiving
    /// addresses using an uncompressed public key
    Compressed,
    /// Not that popular format as of 2019Q2, but it promises that the wallet generated only P2PKH
    /// receiving addresses with this private key
    P2PKH_Only,
    /// Not that popular format as of 2019Q2, but it promises that the wallet generated only P2WPKH
    /// (bech32 segwit native) receiving addresses with this private key
    P2WPKH,
    /// Not that popular format as of 2019Q2, but it promises that the wallet generated only P2WPKH_P2SH
    /// (segwit wrapped in a legacy script hash address) receiving addresses with this private key
    P2WPKH_P2SH,
}

impl Bip178 {
    /// Provides WIF suffix bytes for different usages of a private key
    pub fn to_wif_suffix(self) -> &'static [u8] {
        use Bip178::*;
        match self {
            Uncompressed => b"",
            Compressed => b"\x01",
            P2PKH_Only => b"\x10",
            P2WPKH => b"\x11",
            P2WPKH_P2SH => b"\x12",
        }
    }

    /// Parses usage type from WIF suffix bytes
    pub fn from_wif_suffix(data: &[u8]) -> Result<Self> {
        use Bip178::*;
        match data {
            b"" => Ok(Uncompressed),
            b"\x01" => Ok(Compressed),
            b"\x10" => Ok(P2PKH_Only),
            b"\x11" => Ok(P2WPKH),
            b"\x12" => Ok(P2WPKH_P2SH),
            _ => Err(anyhow!("Unknown wif suffix {}", hex::encode(data))),
        }
    }
}

#[cfg(test)]
mod test {

    #[test]
    fn invalid_private_key() {
        use super::SecpPrivateKey;
        let sk_bytes =
            hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
                .unwrap();
        let err = SecpPrivateKey::from_bytes(sk_bytes).unwrap_err();
        assert!(err.to_string().contains("Invalid secret key"))
    }

    mod sign_verify {
        use crate::secp256k1::{SecpPrivateKey, SecpSignature};
        use crate::{PrivateKey, PublicKey};

        fn test(sk_hex: &str, msg: &[u8], sig_hex: &str) {
            let sk_bytes = hex::decode(sk_hex).unwrap();
            let sk = SecpPrivateKey::from_bytes(sk_bytes).unwrap();

            let sig = sk.sign(msg);
            let sig_bytes = sig.to_bytes();
            assert_eq!(hex::encode(&sig_bytes), sig_hex);

            let sig2 = SecpSignature::from_bytes(&sig_bytes).unwrap();
            let pk = sk.public_key();
            assert!(pk.verify(msg, &sig2));
        }

        #[test]
        fn test_1() {
            test(
                "0000000000000000000000000000000000000000000000000000000000000001",
                b"Satoshi Nakamoto",
                "01934b1ea10a4b3c1757e2b0c017d0b6143ce3c9a7e6a4a49860d7a6ab210ee3d82442ce9d2b916064108014783e923ec36b49743e2ffa1c4496f01a512aafd9e5",
            );
        }
    }

    // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-2
    // https://raw.githubusercontent.com/satoshilabs/slips/master/slip-0010/testvectors.py
    // Also, you can use
    mod slip10_test_vectors {
        use crate::{
            secp256k1::{btc, Network, Secp256k1, SecpExtPrivateKey},
            ExtendedPrivateKey, KeyDerivationCrypto, Seed,
        };
        struct TestDerivation {
            xprv: SecpExtPrivateKey,
        }

        impl TestDerivation {
            fn new(seed_hex: &str) -> Self {
                let seed_bytes = hex::decode(seed_hex).unwrap();
                let seed = Seed::from_bytes(&seed_bytes).unwrap();
                let master = Secp256k1::master(&seed);
                Self { xprv: master }
            }

            fn assert_state(&self, xpub_str: &str, xprv_str: &str) {
                let xpub = self.xprv.neuter();

                assert_eq!(xpub.to_xpub(&btc::Mainnet.bip32_xpub()), xpub_str);
                assert_eq!(self.xprv.to_xprv(&btc::Mainnet.bip32_xprv()), xprv_str);
            }

            fn derive_hardened(&mut self, idx: i32) {
                let xprv = self.xprv.derive_hardened_child(idx).unwrap();
                self.xprv = xprv;
            }

            fn derive_normal(&mut self, idx: i32) {
                let xprv = self.xprv.derive_normal_child(idx).unwrap();
                self.xprv = xprv;
            }
        }

        #[test]
        fn test_vector_2() {
            let mut t = TestDerivation::new("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542");
            t.assert_state("xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U");
            t.derive_normal(0);
            t.assert_state("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt");
            t.derive_hardened(2_147_483_647);
            t.assert_state("xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9");
            t.derive_normal(1);
            t.assert_state("xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef");
            t.derive_hardened(2_147_483_646);
            t.assert_state("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc");
            t.derive_normal(2);
            t.assert_state("xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j");
        }

        #[test]
        fn test_vector_3() {
            let mut t = TestDerivation::new("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be");
            t.assert_state("xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13", "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6");
            t.derive_hardened(0);
            t.assert_state("xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L");
        }
    }

    // https://gobittest.appspot.com/Address is pre bitcoin-core 0.6 and uses the uncompressed public key, so
    // avoid using it now.
    // In https://gobittest.appspot.com/PrivateKey just ignore the 01 suffix added to WIFs only used
    // for post-0.6 compressed public keys.
    // You can check that these tests are valid using the following
    // ./bitcointool -c pubfrompriv -p <WIF>
    // ./bitcointool -c addrfrompub -k <pub>
    mod btc_key_conversions {
        use crate::secp256k1::{btc, Bip178, Network, SecpPrivateKey};
        use crate::{PrivateKey, PublicKey};

        fn test(sk_hex: &str, wif: &str, pk_hex: &str, id_hex: &str, address: &str) {
            let sk_bytes = hex::decode(sk_hex).unwrap();
            let sk = SecpPrivateKey::from_bytes(sk_bytes).unwrap();

            let sk_wif = sk.to_wif(&btc::Mainnet.wif(), Bip178::Compressed);
            assert_eq!(sk_wif, wif);

            let pk = sk.public_key();
            let pk_bytes = pk.to_bytes();
            assert_eq!(hex::encode(&pk_bytes), pk_hex);

            let id = pk.key_id();
            let id_bytes = id.to_bytes();
            assert_eq!(hex::encode(&id_bytes), id_hex);

            let act_address = id.to_p2pkh_addr(&btc::Mainnet.p2pkh_addr());
            assert_eq!(act_address, address);
        }

        #[test]
        fn test_1() {
            test(
                "0000000000000000000000000000000000000000000000000000000000000001",
                "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn",
                "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
                "01751e76e8199196d454941c45d1b3a323f1433bd6",
                "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH",
            );
        }
        #[test]
        fn test_2() {
            test(
                "0000000000000000000000000000000000000000000000000000000000000002",
                "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74NMTptX4",
                "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
                "0106afd46bcdfd22ef94ac122aa11f241244a37ecc",
                "1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP",
            );
        }
        #[test]
        fn test_3() {
            test(
                "0000000000000000000000000000000000000000000000000000000000000003",
                "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74sHUHy8S",
                "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
                "017dd65592d0ab2fe0d0257d571abf032cd9db93dc",
                "1CUNEBjYrCn2y1SdiUMohaKUi4wpP326Lb",
            );
        }
        #[test]
        fn test_4() {
            test(
                "aa5e28d6a97a2479a65527f7290311a3624d4cc0fa1578598ee3c2613bf99522",
                "L2vtCpubwLeqLNYywTUqLLmN6LiijyYWUArxvyw5DyFD8TaxqJyu",
                "0234f9460f0e4f08393d192b3c5133a6ba099aa0ad9fd54ebccfacdfa239ff49c6",
                "01db820065e5bd79e976f0dc09f2257e35243879cf",
                "1M1eigHFbhtWLnc37qXQt1ao2taLhE49yg",
            );
        }
        #[test]
        fn test_5() {
            test(
                "7e2b897b8cebc6361663ad410835639826d590f393d90a9538881735256dfae3",
                "L1Sy9ysFzZDXh5gXYrgJmbyhnhbJVyptuTypUnD9ofZoV3V2SpUi",
                "03d74bf844b0862475103d96a611cf2d898447e288d34b360bc885cb8ce7c00575",
                "015716c6c9146a548ce31092f72ab24b44d8580914",
                "18wV5EG3Hqocod1RLm9STvbUnSqb1NMo44",
            );
        }
        #[test]
        fn test_6() {
            test(
                "6461e6df0fe7dfd05329f41bf771b86578143d4dd1f7866fb4ca7e97c5fa945d",
                "Kzaqk53898thvqucDWi4MqC3ogC2s2QmtZL31qjS9MRMvgHFKpDZ",
                "03e8aecc370aedd953483719a116711963ce201ac3eb21d3f3257bb48668c6a72f",
                "01e3281990058f008a4b6c658cb735ae2b7327daa5",
                "1Mi6RjU7ASvudQMZkeobQ1WoiZWAtVhkd6",
            );
        }
    }

    mod blockchain_com_derivation {
        use crate::secp256k1::*;

        #[test]
        fn btc_wallet() {
            let phrase =
                "hint replace increase neglect egg wood ill alert beef rich install potato";
            let xpub = "xpub6DVu7eWDWJkczyrDQdo2i99MKdns8idTeVfzwuHCaA3rKfSWaSCnpKigU21vJ2TdCT5MiLgbKWzpxZUx4gFx6zpNWukDzX8yRGU3UKyE9fC";
            let xpub2 = "xpub6DVu7eWDWJkd4MbGacHiWGFKT2amHnJwQ7SmxWpD2YuacVtaQz7XPsRYxR5PqsGa2TiLYWPi3jsimnruvJ5LvMBxjc96jgZniP7peLHcEdM";
            let old_addr0 = "17kxMsME7f8CVVWqPadgQNsDEMaHDBpCbv";
            let bip44_addr0 = "17Y82siUGdY8KEWGpUTW7uF5kj6cEMRYfo";
            let net = &btc::Mainnet;

            let seed = Bip39::new().short_phrase(phrase).unwrap().password("");
            let coin = Bip44.network(&seed, net).unwrap();
            let account: Bip44Account<Secp256k1> = coin.account(0).unwrap();

            assert_eq!(account.neuter().to_xpub(), xpub);

            let old_pk0 = account.node().derive_normal(0).unwrap().neuter();

            assert_eq!(old_pk0.to_p2pkh_addr(net), old_addr0);

            let pk0 = account.key(0).unwrap().neuter();

            assert_eq!(pk0.to_p2pkh_addr(), bip44_addr0);

            let account2 = coin.account(1).unwrap();

            assert_eq!(account2.neuter().to_xpub(), xpub2);
        }
    }

    mod coinomi_derivation {
        use crate::{
            secp256k1::{btc, hyd},
            *,
        };

        #[test]
        fn hyd_derive() {
            let mnemonic = "blast cargo razor option vote shoe stock cruel mansion boy spot never album crop reflect kangaroo blouse slam empty shoot cable vital crane manual";
            let pk0_hex = "02f946d10106f55c755c1f836b63bef35fb0015603e1870c8dbdcacf62f178587e";
            let addr0 = "hWNN8ymcsLdJivbwbBaPS8X1vekxB2pdwV";

            let seed = Bip39::new().phrase(mnemonic).unwrap().password(Seed::PASSWORD);
            let account = Bip44.network(&seed, &hyd::Mainnet).unwrap().account(0).unwrap();
            let key0 = account.key(0).unwrap().neuter();

            assert_eq!(hex::encode(key0.to_public_key().to_bytes()), pk0_hex);
            assert_eq!(key0.to_p2pkh_addr(), addr0);
        }

        #[test]
        fn tbtc_derive() {
            let mnemonic = "blast cargo razor option vote shoe stock cruel mansion boy spot never album crop reflect kangaroo blouse slam empty shoot cable vital crane manual";
            let xpub = "tpubDDfA4LQYyG71KmPW65gktxjvzxFAFcdhDAzj4zc6y5hpeX3rZu3nPh1GuvgWCyj4VWKfuFbnnCvFXyTuDLD6mmFA5yVTe2UUcSoNy7kgcYm";
            let xpub_coinomi_bug = "xpub6DHXY6asFz9Z3yCedthfh3QZfq9WGE8dfYQRiSYxdAwvuEP9VgChFFLSftbigUmphbnmHg5vF76CqnyHpUMrtKns2Nk9xVpF2VCyD7Uej6C";
            let addr0 = "mgH9VjC6uGTt1cWDDZEXisASAtCE8D6Xar";
            let net = &btc::Testnet;

            let seed = Bip39::new().phrase(mnemonic).unwrap().password(Seed::PASSWORD);
            let account = Bip44.network(&seed, net).unwrap().account(0).unwrap().neuter();

            assert_eq!(account.to_xpub(), xpub);
            assert_eq!(account.node().to_xpub(&btc::Mainnet), xpub_coinomi_bug);

            let key0 = account.key(0).unwrap();

            assert_eq!(key0.to_p2pkh_addr(), addr0);
        }
    }

    mod ark_desktop_hyd_address {
        use super::super::*;

        #[test]
        fn test() {
            let phrase = "boss slice draft close detail mix nation casino judge cigar melody catch";
            let pk_hex = "03fdd041ed3e51d8909c44ef6b9d1268a412161d8e6544c5cd4d87ef78bb49e2f7";
            let addr = "hFxvDfqQfXHzhvEebTJGkds8DBBGBCKpY9";
            let addr_dev = "dEatNarXZifEXaqnMFy5uP9Fv8bq7aB3Vn";
            let addr_test = "tXRoniBUYaGXc495HCdCL9V9qJPgBMywaH";

            let sk = SecpPrivateKey::from_ark_passphrase(phrase).unwrap();
            let pk = sk.public_key();

            assert_eq!(hex::encode(pk.to_bytes()), pk_hex);

            let key_id = pk.ark_key_id();

            assert_eq!(key_id.to_p2pkh_addr(&hyd::Mainnet.p2pkh_addr()), addr);
            assert_eq!(key_id.to_p2pkh_addr(&hyd::Devnet.p2pkh_addr()), addr_dev);
            assert_eq!(key_id.to_p2pkh_addr(&hyd::Testnet.p2pkh_addr()), addr_test);
        }
    }

    // ARK is special 😉 These tests are cross-checked with their JavaScript crpyto implementation and use private keys
    // of their testnet genesis delegates.
    mod ark_key_conversions {
        use super::super::*;

        fn test(passphrase: &str, pk_hex: &str, main_addr: &str, dev_addr: &str) {
            let sk = SecpPrivateKey::from_ark_passphrase(passphrase).unwrap();
            let pk = sk.public_key();
            assert_eq!(hex::encode(pk.to_bytes()), pk_hex);

            let key_id = pk.ark_key_id();

            assert_eq!(key_id.to_p2pkh_addr(&ark::Mainnet.p2pkh_addr()), main_addr);
            assert_eq!(key_id.to_p2pkh_addr(&ark::Devnet.p2pkh_addr()), dev_addr);
        }

        #[test]
        fn test_delegate_1() {
            test(
                "clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire",
                "03287bfebba4c7881a0509717e71b34b63f31e40021c321f89ae04f84be6d6ac37",
                "ANBkoGqWeTSiaEVgVzSKZd3jS7UWzv9PSo",
                "DBYyh2vXcigrJGUHfvmYxVxEqeH7vomw6x",
            );
        }

        #[test]
        fn test_delegate_2() {
            test(
                "venue below waste gather spin cruise title still boost mother flash tuna",
                "02def27da9336e7fbf63131b8d7e5c9f45b296235db035f1f4242c507398f0f21d",
                "AbfQq8iRSf9TFQRzQWo33dHYU7HFMS17Zd",
                "DR2ditoSQvPaySQbaT8GSWC3se5rLyfq4T",
            );
        }
    }
}