// Copyright (c) 2016-2024 The Hush developers // Distributed under the GPLv3 software license, see the accompanying // file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html // // Proves that a BIP39 seed phrase produces the SAME transparent and shielded // addresses on the DragonX full node as in SilentDragonXLite. The proof chain: // phrase -> entropy (round-trip) -> 64-byte BIP39 seed (known-answer) // -> z/t addresses (wallet path == direct ZIP-32/BIP44 derivation). // The 64-byte seed is anchored to the well-known BIP39 value for the all-zero // "abandon...art" entropy with an EMPTY passphrase, which is exactly what // SilentDragonXLite's tiny-bip39 0.6.2 feeds into the same coin_type=141 paths. #include #include "chainparams.h" #include "key.h" #include "key_io.h" #include "util.h" #include "wallet/mnemonic.h" #include "wallet/wallet.h" #include "zcash/Address.hpp" #include "zcash/zip32.h" // The canonical 24-word phrase for 32 bytes of all-zero entropy. static const char* ABANDON_ART = "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon abandon abandon art"; // The standard BIP39 seed for that phrase with an EMPTY passphrase // (PBKDF2-HMAC-SHA512, 2048 rounds, salt "mnemonic"). Matches tiny-bip39. static const char* SEED64_HEX = "408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf70" "5489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840"; // First shielded address for a 64-byte seed: m/32'/141'/0' default address. static std::string DeriveZAddrFromSeed64(RawHDSeed seed64) { HDSeed s(seed64); auto m = libzcash::SaplingExtendedSpendingKey::Master(s); auto xsk = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT) .Derive(141 | ZIP32_HARDENED_KEY_LIMIT) .Derive(0 | ZIP32_HARDENED_KEY_LIMIT); return EncodePaymentAddress(xsk.DefaultAddress()); } // First transparent address for a BIP32 master over `seedBytes`: // m/44'/141'/0'/0/0. static std::string DeriveTAddrFromSeedBytes(RawHDSeed seedBytes) { CExtKey master, purpose, coinType, account, external, child; master.SetMaster(seedBytes.data(), seedBytes.size()); master.Derive(purpose, 44 | BIP32_HARDENED_KEY_LIMIT); purpose.Derive(coinType, 141 | BIP32_HARDENED_KEY_LIMIT); coinType.Derive(account, 0 | BIP32_HARDENED_KEY_LIMIT); account.Derive(external, 0); external.Derive(child, 0); return EncodeDestination(child.key.GetPubKey().GetID()); } // The 64-byte seed derived from the mnemonic must equal the known BIP39 value. // This is the cross-wallet anchor: SilentDragonXLite feeds the identical seed. TEST(mnemonic_compat, Bip39SeedKnownAnswer) { RawHDSeed entropy(32, 0); RawHDSeed seed64; ASSERT_TRUE(Bip39SeedFromEntropy(entropy, seed64)); ASSERT_EQ(seed64.size(), (size_t)64); EXPECT_EQ(HexStr(seed64.begin(), seed64.end()), std::string(SEED64_HEX)); } TEST(mnemonic_compat, EntropyPhraseRoundTrip) { RawHDSeed zeros(32, 0); std::string phrase; ASSERT_TRUE(EntropyToMnemonic(zeros, phrase)); EXPECT_EQ(phrase, std::string(ABANDON_ART)); EXPECT_TRUE(MnemonicIsValid(ABANDON_ART)); RawHDSeed entropy; ASSERT_TRUE(MnemonicToEntropy(ABANDON_ART, entropy)); EXPECT_EQ(entropy.size(), (size_t)32); EXPECT_EQ(HexStr(entropy.begin(), entropy.end()), std::string(64, '0')); // Bad checksum / unknown words are rejected. EXPECT_FALSE(MnemonicIsValid("abandon abandon abandon")); EXPECT_FALSE(MnemonicIsValid("clearly not valid bip39 words at all here")); RawHDSeed junk; EXPECT_FALSE(MnemonicToEntropy("clearly not valid bip39 words at all here", junk)); } // The wallet's mnemonic derivation must reproduce the exact addresses obtained // by driving ZIP-32 / BIP44 directly from the known 64-byte seed, and must be // deterministic across wallets. TEST(mnemonic_compat, WalletDerivesSdxliteAddresses) { SelectParams(CBaseChainParams::MAIN); RawHDSeed zeros(32, 0), seed64; ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64)); const std::string expZ = DeriveZAddrFromSeed64(seed64); const std::string expT = DeriveTAddrFromSeedBytes(seed64); EXPECT_EQ(expZ.substr(0, 2), "zs"); // sapling HRP for mainnet CWallet wallet; ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART)); ASSERT_TRUE(wallet.IsMnemonicSeed()); { LOCK(wallet.cs_wallet); EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()), expZ); EXPECT_EQ(EncodeDestination(wallet.GenerateNewKey().GetID()), expT); } // Same phrase, fresh wallet -> identical first addresses. CWallet wallet2; ASSERT_TRUE(wallet2.SetHDSeedFromMnemonic(ABANDON_ART)); { LOCK(wallet2.cs_wallet); EXPECT_EQ(EncodePaymentAddress(wallet2.GenerateNewSaplingZKey()), expZ); EXPECT_EQ(EncodeDestination(wallet2.GenerateNewKey().GetID()), expT); } // The phrase round-trips out of the wallet. std::string exported; ASSERT_TRUE(wallet.GetMnemonicPhrase(exported)); EXPECT_EQ(exported, std::string(ABANDON_ART)); } // Negative: feeding the 32-byte entropy DIRECTLY as the seed (the classic // interop bug) must produce a different address than the 64-byte BIP39 seed. TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed) { SelectParams(CBaseChainParams::MAIN); RawHDSeed zeros(32, 0), seed64; ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64)); const std::string seedT = DeriveTAddrFromSeedBytes(seed64); // correct (64-byte) const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte) EXPECT_NE(seedT, entropyT); }