From 83dee7e8868a5fe773a35702d99bdd050c2ee7d6 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 21 Sep 2021 11:17:33 -0600 Subject: [PATCH] Adds basic unified spending key derivation. Also, begin pruning Sapling key derivation down to the minimal amount required to support the legacy Sapling key derivation process. --- src/gtest/test_keys.cpp | 2 +- src/gtest/test_keystore.cpp | 16 +-- src/key.cpp | 24 ++-- src/key.h | 13 ++- src/test/bip32_tests.cpp | 16 ++- src/test/key_tests.cpp | 2 +- src/utiltest.cpp | 2 +- .../asyncrpcoperation_saplingmigration.cpp | 11 +- src/wallet/gtest/test_wallet.cpp | 18 +-- src/wallet/gtest/test_wallet_zkeys.cpp | 12 +- src/wallet/test/rpc_wallet_tests.cpp | 2 +- src/wallet/wallet.cpp | 108 +++++++++--------- src/wallet/wallet.h | 7 ++ src/wallet/walletdb.h | 43 ------- src/zcash/Address.cpp | 6 +- src/zcash/address/zip32.cpp | 79 ++++++++++--- src/zcash/address/zip32.h | 74 +++++++++++- src/zcbenchmarks.cpp | 2 +- 18 files changed, 271 insertions(+), 166 deletions(-) diff --git a/src/gtest/test_keys.cpp b/src/gtest/test_keys.cpp index b7aef27bb..4ff275128 100644 --- a/src/gtest/test_keys.cpp +++ b/src/gtest/test_keys.cpp @@ -48,7 +48,7 @@ TEST(Keys, EncodeAndDecodeSapling) EXPECT_EQ(extfvk, extfvk2); } { - auto addr = sk.DefaultAddress(); + auto addr = sk.ToXFVK().DefaultAddress(); std::string addr_string = keyIO.EncodePaymentAddress(addr); EXPECT_EQ( diff --git a/src/gtest/test_keystore.cpp b/src/gtest/test_keystore.cpp index f856877fa..03a9f082f 100644 --- a/src/gtest/test_keystore.cpp +++ b/src/gtest/test_keystore.cpp @@ -14,6 +14,8 @@ #define MAKE_STRING(x) std::string((x), (x)+sizeof(x)) +const uint32_t BIP44_TESTNET_TYPE = 1; + TEST(KeystoreTests, StoreAndRetrieveHDSeed) { CBasicKeyStore keyStore; @@ -23,7 +25,7 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeed) { EXPECT_FALSE(seedOut.has_value()); // Generate a random seed - auto seed = MnemonicSeed::Random(); + auto seed = MnemonicSeed::Random(BIP44_TESTNET_TYPE); // We should be able to set and retrieve the seed ASSERT_TRUE(keyStore.SetMnemonicSeed(seed)); @@ -33,7 +35,7 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeed) { EXPECT_EQ(seed, seedOut.value()); // Generate another random seed - auto seed2 = MnemonicSeed::Random(); + auto seed2 = MnemonicSeed::Random(BIP44_TESTNET_TYPE); EXPECT_NE(seed, seed2); // We should not be able to set and retrieve a different seed @@ -203,7 +205,7 @@ TEST(KeystoreTests, StoreAndRetrieveSaplingSpendingKey) { auto sk = GetTestMasterSaplingSpendingKey(); auto extfvk = sk.ToXFVK(); auto ivk = extfvk.fvk.in_viewing_key(); - auto addr = sk.DefaultAddress(); + auto addr = sk.ToXFVK().DefaultAddress(); // Sanity-check: we can't get a key we haven't added EXPECT_FALSE(keyStore.HaveSaplingSpendingKey(extfvk)); @@ -237,7 +239,7 @@ TEST(KeystoreTests, StoreAndRetrieveSaplingFullViewingKey) { auto sk = GetTestMasterSaplingSpendingKey(); auto extfvk = sk.ToXFVK(); auto ivk = extfvk.fvk.in_viewing_key(); - auto addr = sk.DefaultAddress(); + auto addr = sk.ToXFVK().DefaultAddress(); // Sanity-check: we can't get a full viewing key we haven't added EXPECT_FALSE(keyStore.HaveSaplingFullViewingKey(ivk)); @@ -289,7 +291,7 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeedInEncryptedStore) { GetRandBytes(vMasterKey.data(), 32); // 1) Test adding a seed to an unencrypted key store, then encrypting it - auto seed = MnemonicSeed::Random(); + auto seed = MnemonicSeed::Random(BIP44_TESTNET_TYPE); EXPECT_FALSE(keyStore.HaveMnemonicSeed()); auto seedOut = keyStore.GetMnemonicSeed(); EXPECT_FALSE(seedOut.has_value()); @@ -321,7 +323,7 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeedInEncryptedStore) { EXPECT_EQ(seed, seedOut.value()); // 2) Test replacing the seed in an already-encrypted key store fails - auto seed2 = MnemonicSeed::Random(); + auto seed2 = MnemonicSeed::Random(BIP44_TESTNET_TYPE); EXPECT_FALSE(keyStore.SetMnemonicSeed(seed2)); EXPECT_TRUE(keyStore.HaveMnemonicSeed()); seedOut = keyStore.GetMnemonicSeed(); @@ -341,7 +343,7 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeedInEncryptedStore) { seedOut = keyStore2.GetMnemonicSeed(); EXPECT_FALSE(seedOut.has_value()); - auto seed3 = MnemonicSeed::Random(); + auto seed3 = MnemonicSeed::Random(BIP44_TESTNET_TYPE); ASSERT_TRUE(keyStore2.SetMnemonicSeed(seed3)); EXPECT_TRUE(keyStore2.HaveMnemonicSeed()); seedOut = keyStore2.GetMnemonicSeed(); diff --git a/src/key.cpp b/src/key.cpp index 9f03cf0c2..ff52e5eea 100644 --- a/src/key.cpp +++ b/src/key.cpp @@ -274,23 +274,31 @@ bool CKey::Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const return ret; } -bool CExtKey::Derive(CExtKey &out, unsigned int _nChild) const { +std::optional CExtKey::Derive(unsigned int _nChild) const { + CExtKey out; out.nDepth = nDepth + 1; CKeyID id = key.GetPubKey().GetID(); memcpy(&out.vchFingerprint[0], &id, 4); out.nChild = _nChild; - return key.Derive(out.key, out.chaincode, _nChild, chaincode); + if (key.Derive(out.key, out.chaincode, _nChild, chaincode)) { + return out; + } else { + return std::nullopt; + } } -void CExtKey::SetMaster(const unsigned char *seed, unsigned int nSeedLen) { +CExtKey CExtKey::Master(const unsigned char *seed, unsigned int nSeedLen) { + CExtKey xk; static const unsigned char hashkey[] = {'B','i','t','c','o','i','n',' ','s','e','e','d'}; std::vector> vout(64); CHMAC_SHA512(hashkey, sizeof(hashkey)).Write(seed, nSeedLen).Finalize(vout.data()); - key.Set(vout.data(), vout.data() + 32, true); - memcpy(chaincode.begin(), vout.data() + 32, 32); - nDepth = 0; - nChild = 0; - memset(vchFingerprint, 0, sizeof(vchFingerprint)); + xk.key.Set(vout.data(), vout.data() + 32, true); + memcpy(xk.chaincode.begin(), vout.data() + 32, 32); + xk.nDepth = 0; + xk.nChild = 0; + memset(xk.vchFingerprint, 0, sizeof(xk.vchFingerprint)); + + return xk; } CExtPubKey CExtKey::Neuter() const { diff --git a/src/key.h b/src/key.h index c61f1dd02..2a9ee32c5 100644 --- a/src/key.h +++ b/src/key.h @@ -15,6 +15,12 @@ #include #include +/** + * The account identifier used for HD derivation of the transparent + * p2pkh public key from which all child transparent addresses are + * derived in accordance with ZIP-316. + */ +const uint32_t ZCASH_LEGACY_TRANSPARENT_ACCOUNT = 0x7FFFFFFE; /** * secure_allocator is defined in allocators.h @@ -62,6 +68,8 @@ public: keydata.resize(32); } + static std::optional FromEntropy(std::vector> keydata); + friend bool operator==(const CKey& a, const CKey& b) { return a.fCompressed == b.fCompressed && @@ -160,11 +168,12 @@ struct CExtKey { a.key == b.key; } + static CExtKey Master(const unsigned char* seed, unsigned int nSeedLen); + void Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const; void Decode(const unsigned char code[BIP32_EXTKEY_SIZE]); - bool Derive(CExtKey& out, unsigned int nChild) const; + std::optional Derive(unsigned int nChild) const; CExtPubKey Neuter() const; - void SetMaster(const unsigned char* seed, unsigned int nSeedLen); template void Serialize(Stream& s) const { diff --git a/src/test/bip32_tests.cpp b/src/test/bip32_tests.cpp index f83ed78c8..64e9b87ab 100644 --- a/src/test/bip32_tests.cpp +++ b/src/test/bip32_tests.cpp @@ -80,10 +80,8 @@ TestVector test2 = void RunTest(const TestVector &test) { std::vector seed = ParseHex(test.strHexMaster); - CExtKey key; - CExtPubKey pubkey; - key.SetMaster(&seed[0], seed.size()); - pubkey = key.Neuter(); + CExtKey key = CExtKey::Master(&seed[0], seed.size()); + CExtPubKey pubkey = key.Neuter(); KeyIO keyIO(Params()); for (const TestDerivation &derive : test.vDerive) { unsigned char data[74]; @@ -99,16 +97,16 @@ void RunTest(const TestVector &test) { BOOST_CHECK(keyIO.DecodeExtPubKey(derive.pub) == pubkey); //ensure a base58 decoded pubkey also matches // Derive new keys - CExtKey keyNew; - BOOST_CHECK(key.Derive(keyNew, derive.nChild)); - CExtPubKey pubkeyNew = keyNew.Neuter(); + auto keyNew = key.Derive(derive.nChild); + BOOST_CHECK(keyNew.has_value()); + CExtPubKey pubkeyNew = keyNew.value().Neuter(); if (!(derive.nChild & 0x80000000)) { // Compare with public derivation CExtPubKey pubkeyNew2; BOOST_CHECK(pubkey.Derive(pubkeyNew2, derive.nChild)); BOOST_CHECK(pubkeyNew == pubkeyNew2); } - key = keyNew; + key = keyNew.value(); pubkey = pubkeyNew; CDataStream ssPub(SER_DISK, CLIENT_VERSION); @@ -116,7 +114,7 @@ void RunTest(const TestVector &test) { BOOST_CHECK(ssPub.size() == 75); CDataStream ssPriv(SER_DISK, CLIENT_VERSION); - ssPriv << keyNew; + ssPriv << keyNew.value(); BOOST_CHECK(ssPriv.size() == 75); CExtPubKey pubCheck; diff --git a/src/test/key_tests.cpp b/src/test/key_tests.cpp index 959bb25da..ae87834a6 100644 --- a/src/test/key_tests.cpp +++ b/src/test/key_tests.cpp @@ -247,7 +247,7 @@ BOOST_AUTO_TEST_CASE(zs_address_test) BOOST_CHECK(sk == sk2); } { - auto addr = sk.DefaultAddress(); + auto addr = sk.ToXFVK().DefaultAddress(); std::string addr_string = keyIO.EncodePaymentAddress(addr); BOOST_CHECK(addr_string.compare(0, 15, Params().Bech32HRP(CChainParams::SAPLING_PAYMENT_ADDRESS)) == 0); diff --git a/src/utiltest.cpp b/src/utiltest.cpp index ac404ed79..be0e5ecf2 100644 --- a/src/utiltest.cpp +++ b/src/utiltest.cpp @@ -346,7 +346,7 @@ CWalletTx GetValidSaplingReceive(const Consensus::Params& consensusParams, auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); // To zaddr auto fvk = sk.expsk.full_viewing_key(); - auto pa = sk.DefaultAddress(); + auto pa = sk.ToXFVK().DefaultAddress(); auto builder = TransactionBuilder(consensusParams, 1, &keyStore); builder.SetFee(0); diff --git a/src/wallet/asyncrpcoperation_saplingmigration.cpp b/src/wallet/asyncrpcoperation_saplingmigration.cpp index 11fc89aa4..5bfdb759a 100644 --- a/src/wallet/asyncrpcoperation_saplingmigration.cpp +++ b/src/wallet/asyncrpcoperation_saplingmigration.cpp @@ -203,12 +203,15 @@ libzcash::SaplingPaymentAddress AsyncRPCOperation_saplingmigration::getMigration } // TODO: use UVK-based derivation here instead. - auto xsk = pwalletMain->GenerateNewSaplingZKey(seed, 0); + auto usk = pwalletMain->GetUnifiedSpendingKeyForAccount(0); + assert(usk.has_value()); // mnemonic seeds are currently always generated to have valid USKs at account 0 + auto xsk = usk.value().GetSaplingExtendedSpendingKey(); if (xsk.has_value()) { - return xsk.value().DefaultAddress(); + return xsk.value().ToXFVK().DefaultAddress(); } else { - // the wallet already has a key at account 0; what is the - // correct behavior here? + // This error will only occur if Sapling address generation has been disbled for USKs from this + // wallet. + throw std::runtime_error(std::string(__func__) + ": No Sapling address generated for account 0."); } } diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index edc060585..8308a2029 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -393,7 +393,7 @@ TEST(WalletTests, SetSaplingNoteAddrsInCWalletTx) { auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto ivk = fvk.in_viewing_key(); - auto pk = sk.DefaultAddress(); + auto pk = sk.ToXFVK().DefaultAddress(); libzcash::SaplingNote note(pk, 50000, zip_212_enabled[ver]); auto cm = note.cmu().value(); @@ -535,7 +535,7 @@ TEST(WalletTests, FindMySaplingNotes) { auto sk = GetTestMasterSaplingSpendingKey(); auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); - auto pa = sk.DefaultAddress(); + auto pa = extfvk.DefaultAddress(); auto testNote = GetTestSaplingNote(pa, 50000); @@ -669,7 +669,7 @@ TEST(WalletTests, GetConflictedSaplingNotes) { auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); auto ivk = extfvk.fvk.in_viewing_key(); - auto pk = sk.DefaultAddress(); + auto pk = extfvk.DefaultAddress(); ASSERT_TRUE(wallet.AddSaplingZKey(sk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(extfvk)); @@ -829,7 +829,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) { auto sk = GetTestMasterSaplingSpendingKey(); auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); - auto pa = sk.DefaultAddress(); + auto pa = extfvk.DefaultAddress(); auto testNote = GetTestSaplingNote(pa, 50000); @@ -914,7 +914,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) { auto sk = GetTestMasterSaplingSpendingKey(); auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); - auto pa = sk.DefaultAddress(); + auto pa = extfvk.DefaultAddress(); auto testNote = GetTestSaplingNote(pa, 50000); @@ -1045,7 +1045,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) { auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); auto ivk = extfvk.fvk.in_viewing_key(); - auto pk = sk.DefaultAddress(); + auto pk = extfvk.DefaultAddress(); // Generate Sapling note A libzcash::SaplingNote note(pk, 50000, zip_212_enabled[ver]); @@ -1841,13 +1841,13 @@ TEST(WalletTests, UpdatedSaplingNoteData) { auto sk = m.Derive(0); auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); - auto pa = sk.DefaultAddress(); + auto pa = extfvk.DefaultAddress(); // Generate dummy recipient Sapling address auto sk2 = m.Derive(1); auto expsk2 = sk2.expsk; auto extfvk2 = sk2.ToXFVK(); - auto pa2 = sk2.DefaultAddress(); + auto pa2 = extfvk2.DefaultAddress(); auto testNote = GetTestSaplingNote(pa, 50000); @@ -1983,7 +1983,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) { auto expsk = sk.expsk; auto extfvk = sk.ToXFVK(); auto ivk = extfvk.fvk.in_viewing_key(); - auto pk = sk.DefaultAddress(); + auto pk = extfvk.DefaultAddress(); ASSERT_TRUE(wallet.AddSaplingZKey(sk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(extfvk)); diff --git a/src/wallet/gtest/test_wallet_zkeys.cpp b/src/wallet/gtest/test_wallet_zkeys.cpp index 0622932af..514fb9b35 100644 --- a/src/wallet/gtest/test_wallet_zkeys.cpp +++ b/src/wallet/gtest/test_wallet_zkeys.cpp @@ -64,7 +64,7 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) { wallet.GetSaplingPaymentAddresses(addrs); EXPECT_EQ(2, addrs.size()); EXPECT_EQ(1, addrs.count(address)); - EXPECT_EQ(1, addrs.count(sk.DefaultAddress())); + EXPECT_EQ(1, addrs.count(sk.ToXFVK().DefaultAddress())); // Generate a diversified address different to the default // If we can't get an early diversified address, we are very unlucky @@ -73,7 +73,7 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) { auto dpa = sk.ToXFVK().Address(diversifier).value(); // verify wallet only has the default address - EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(sk.DefaultAddress())); + EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(sk.ToXFVK().DefaultAddress())); EXPECT_FALSE(wallet.HaveSaplingIncomingViewingKey(dpa)); // manually add a diversified address @@ -81,7 +81,7 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) { EXPECT_TRUE(wallet.AddSaplingIncomingViewingKey(ivk, dpa)); // verify wallet did add it - EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(sk.DefaultAddress())); + EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(sk.ToXFVK().DefaultAddress())); EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(dpa)); // Load a third key into the wallet @@ -99,7 +99,7 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) { // Load a diversified address for the third key into the wallet auto dpa2 = sk2.ToXFVK().Address(diversifier).value(); - EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(sk2.DefaultAddress())); + EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(sk2.ToXFVK().DefaultAddress())); EXPECT_FALSE(wallet.HaveSaplingIncomingViewingKey(dpa2)); EXPECT_TRUE(wallet.LoadSaplingPaymentAddress(dpa2, ivk2)); EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(dpa2)); @@ -501,8 +501,8 @@ TEST(wallet_zkeys_tests, WriteCryptedSaplingZkeyDirectToDb) { wallet2.Unlock(strWalletPass); EXPECT_TRUE(wallet2.GetSaplingExtendedSpendingKey(address, keyOut)); - ASSERT_EQ(address, keyOut.DefaultAddress()); + ASSERT_EQ(address, keyOut.ToXFVK().DefaultAddress()); EXPECT_TRUE(wallet2.GetSaplingExtendedSpendingKey(address2, keyOut)); - ASSERT_EQ(address2, keyOut.DefaultAddress()); + ASSERT_EQ(address2, keyOut.ToXFVK().DefaultAddress()); } diff --git a/src/wallet/test/rpc_wallet_tests.cpp b/src/wallet/test/rpc_wallet_tests.cpp index 134b36979..771e558f1 100644 --- a/src/wallet/test/rpc_wallet_tests.cpp +++ b/src/wallet/test/rpc_wallet_tests.cpp @@ -665,7 +665,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_z_importexport) // create a random Sapling key locally; split between IVKs and spending keys. auto testSaplingSpendingKey = m.Derive(i); - auto testSaplingPaymentAddress = testSaplingSpendingKey.DefaultAddress(); + auto testSaplingPaymentAddress = testSaplingSpendingKey.ToXFVK().DefaultAddress(); if (i % 2 == 0) { std::string testSaplingAddr = keyIO.EncodePaymentAddress(testSaplingPaymentAddress); std::string testSaplingKey = keyIO.EncodeSpendingKey(testSaplingSpendingKey); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e41e21967..bdf98e4da 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -118,9 +118,9 @@ libzcash::SproutPaymentAddress CWallet::GenerateNewSproutZKey() // is not present. When using legacy HD seeds, the account index is determined // by trial of legacyHDChain.GetAccountCounter(); for unified addresses this must use // valued derived from legacyHDChain.unifiedAccountCounter -std::optional CWallet::GenerateNewLegacySaplingZKey() -{ - // Try to get the seed +std::optional CWallet::GenerateNewLegacySaplingZKey() { + AssertLockHeld(cs_wallet); + auto seedOpt = GetLegacyHDSeed(); if (seedOpt.has_value()) { auto seed = seedOpt.value(); @@ -128,18 +128,33 @@ std::optional CWallet::GenerateNewLegacySaplingZKey() legacyHDChain = CHDChain(seed.Fingerprint(), GetTime()); } CHDChain& hdChain = legacyHDChain.value(); + + // loop until we find an unused account id while (true) { - auto xsk = GenerateNewSaplingZKey(seed, hdChain.GetAccountCounter()); - if (!xsk.has_value()) { - hdChain.IncrementAccountCounter(); + auto xsk = libzcash::SaplingExtendedSpendingKey::ForAccount( + seed, + BIP44CoinType(), + hdChain.GetAccountCounter()); + // advance the account counter so that the next time we need to generate + // a key we're pointing at a free index. + hdChain.IncrementAccountCounter(); + if (HaveSaplingSpendingKey(xsk.first.ToXFVK())) { + // try the next account ID continue; } else { - // Update the chain model in the database + // Update the persisted chain information if (fFileBacked && !CWalletDB(strWalletFile).WriteLegacyHDChain(hdChain)) { throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): Writing HD chain model failed"); } - return xsk.value().ToXFVK().DefaultAddress(); + auto ivk = xsk.first.expsk.full_viewing_key().in_viewing_key(); + mapSaplingZKeyMetadata[ivk] = xsk.second; + + if (!AddSaplingZKey(xsk.first)) { + throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): AddSaplingZKey failed"); + } + + return xsk.first.ToXFVK().DefaultAddress(); } } } else { @@ -147,48 +162,6 @@ std::optional CWallet::GenerateNewLegacySaplingZKey() } } -// Generate a new Sapling spending key and return its public payment address -// -// NU5: This must take both the account number and the HD seed to be used. -// Generation of unified accounts must allow the user to specify a set of protocol -// types that they wish to include in their unified addresses. -std::optional CWallet::GenerateNewSaplingZKey( - const HDSeed& seed, - uint32_t accountId) { - AssertLockHeld(cs_wallet); - - auto m = libzcash::SaplingExtendedSpendingKey::Master(seed); - uint32_t bip44CoinType = BIP44CoinType(); - - // We use a fixed keypath scheme of m/32'/coin_type'/account' - // Derive m/32' - auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT); - // Derive m/32'/coin_type' - auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT); - - // Derive account key at next index, skip keys already known to the wallet - libzcash::SaplingExtendedSpendingKey xsk = m_32h_cth.Derive(accountId | ZIP32_HARDENED_KEY_LIMIT); - if (HaveSaplingSpendingKey(xsk.ToXFVK())) { - return std::nullopt; - } else { - // Create new metadata - int64_t nCreationTime = GetTime(); - CKeyMetadata metadata(nCreationTime); - - metadata.hdKeypath = "m/32'/" + std::to_string(bip44CoinType) + "'/" + std::to_string(accountId) + "'"; - metadata.seedFp = seed.Fingerprint(); - - auto ivk = xsk.expsk.full_viewing_key().in_viewing_key(); - mapSaplingZKeyMetadata[ivk] = metadata; - - if (!AddSaplingZKey(xsk)) { - throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): AddSaplingZKey failed"); - } - - return xsk; - } -} - // Add spending key to keystore bool CWallet::AddSaplingZKey(const libzcash::SaplingExtendedSpendingKey &sk) { @@ -394,6 +367,37 @@ bool CWallet::AddCryptedSaplingSpendingKey(const libzcash::SaplingExtendedFullVi return false; } +UnifiedSpendingKey CWallet::GenerateNewUnifiedSpendingKey() { + AssertLockHeld(cs_wallet); + + auto seed = GetMnemonicSeed(); + if (!seed.has_value()) { + throw std::runtime_error(std::string(__func__) + ": Wallet has no mnemonic HD seed. Please upgrade this wallet."); + } + + auto hdChain = GetMnemonicHDChain().value(); + while (true) { + auto usk = UnifiedSpendingKey::Derive(seed.value(), BIP44CoinType(), hdChain.GetAccountCounter()); + hdChain.IncrementAccountCounter(); + + if (usk.has_value()) { + // Update the persisted chain information + if (fFileBacked && !CWalletDB(strWalletFile).WriteMnemonicHDChain(hdChain)) { + throw std::runtime_error("CWallet::GenerateNewUnifiedSpendingKey(): Writing HD chain model failed"); + } + + // TODO: Save the unified full viewing key to the wallet metadata + + return usk.value().first; + } + } +} + +std::optional CWallet::GetUnifiedSpendingKeyForAccount(uint32_t accountId) { + //TODO + return std::nullopt; +} + void CWallet::LoadKeyMetadata(const CPubKey &pubkey, const CKeyMetadata &meta) { AssertLockHeld(cs_wallet); // mapKeyMetadata @@ -2252,7 +2256,7 @@ void CWallet::GenerateNewSeed(Language language) { LOCK(cs_wallet); - auto seed = MnemonicSeed::Random(language, HD_WALLET_SEED_LENGTH); + auto seed = MnemonicSeed::Random(BIP44CoinType(), language, HD_WALLET_SEED_LENGTH); int64_t nCreationTime = GetTime(); @@ -5468,7 +5472,7 @@ KeyAddResult AddSpendingKeyToWallet::operator()(const libzcash::SaplingExtendedS KeyIO keyIO(Params()); { if (log){ - LogPrint("zrpc", "Importing zaddr %s...\n", keyIO.EncodePaymentAddress(sk.DefaultAddress())); + LogPrint("zrpc", "Importing zaddr %s...\n", keyIO.EncodePaymentAddress(sk.ToXFVK().DefaultAddress())); } // Don't throw error in case a key is already there if (m_wallet->HaveSaplingSpendingKey(extfvk)) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 677d23a35..f173dfd92 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -829,6 +829,7 @@ public: std::map mapKeyMetadata; std::map mapSproutZKeyMetadata; std::map mapSaplingZKeyMetadata; + //std::map mapUnifiedKeyMetadata; typedef std::map MasterKeyMap; MasterKeyMap mapMasterKeys; @@ -1100,6 +1101,12 @@ public: bool LoadCryptedSaplingZKey(const libzcash::SaplingExtendedFullViewingKey &extfvk, const std::vector &vchCryptedSecret); + /** + * Unified keys & addresses + */ + libzcash::UnifiedSpendingKey GenerateNewUnifiedSpendingKey(); + std::optional GetUnifiedSpendingKeyForAccount(uint32_t accountId); + /** * Increment the next transaction order id * @return next transaction order id diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index b2f297e2c..a3abf37bc 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -96,49 +96,6 @@ public: } }; -class CKeyMetadata -{ -public: - static const int VERSION_BASIC=1; - static const int VERSION_WITH_HDDATA=10; - static const int CURRENT_VERSION=VERSION_WITH_HDDATA; - int nVersion; - int64_t nCreateTime; // 0 means unknown - std::string hdKeypath; //optional HD/zip32 keypath - uint256 seedFp; - - CKeyMetadata() - { - SetNull(); - } - CKeyMetadata(int64_t nCreateTime_) - { - SetNull(); - nCreateTime = nCreateTime_; - } - - ADD_SERIALIZE_METHODS; - - template - inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(this->nVersion); - READWRITE(nCreateTime); - if (this->nVersion >= VERSION_WITH_HDDATA) - { - READWRITE(hdKeypath); - READWRITE(seedFp); - } - } - - void SetNull() - { - nVersion = CKeyMetadata::CURRENT_VERSION; - nCreateTime = 0; - hdKeypath.clear(); - seedFp.SetNull(); - } -}; - /** Access to the wallet database */ class CWalletDB : public CDB { diff --git a/src/zcash/Address.cpp b/src/zcash/Address.cpp index d827a395a..9b5340cb1 100644 --- a/src/zcash/Address.cpp +++ b/src/zcash/Address.cpp @@ -39,7 +39,7 @@ std::pair AddressInfoFromSpendingKey::operator()(co return std::make_pair("sprout", sk.address()); } std::pair AddressInfoFromSpendingKey::operator()(const SaplingExtendedSpendingKey &sk) const { - return std::make_pair("sapling", sk.DefaultAddress()); + return std::make_pair("sapling", sk.ToXFVK().DefaultAddress()); } std::pair AddressInfoFromSpendingKey::operator()(const InvalidEncoding&) const { throw std::invalid_argument("Cannot derive default address from invalid spending key"); @@ -48,8 +48,8 @@ std::pair AddressInfoFromSpendingKey::operator()(co std::pair AddressInfoFromViewingKey::operator()(const SproutViewingKey &sk) const { return std::make_pair("sprout", sk.address()); } -std::pair AddressInfoFromViewingKey::operator()(const SaplingExtendedFullViewingKey &sk) const { - return std::make_pair("sapling", sk.DefaultAddress()); +std::pair AddressInfoFromViewingKey::operator()(const SaplingExtendedFullViewingKey &xfvk) const { + return std::make_pair("sapling", xfvk.DefaultAddress()); } std::pair AddressInfoFromViewingKey::operator()(const InvalidEncoding&) const { throw std::invalid_argument("Cannot derive default address from invalid viewing key"); diff --git a/src/zcash/address/zip32.cpp b/src/zcash/address/zip32.cpp index 8fad4d136..12f5a8548 100644 --- a/src/zcash/address/zip32.cpp +++ b/src/zcash/address/zip32.cpp @@ -19,20 +19,23 @@ const unsigned char ZCASH_HD_SEED_FP_PERSONAL[BLAKE2bPersonalBytes] = const unsigned char ZCASH_TADDR_OVK_PERSONAL[BLAKE2bPersonalBytes] = {'Z', 'c', 'T', 'a', 'd', 'd', 'r', 'T', 'o', 'S', 'a', 'p', 'l', 'i', 'n', 'g'}; -MnemonicSeed MnemonicSeed::Random(Language language, size_t len) +MnemonicSeed MnemonicSeed::Random(uint32_t bip44CoinType, Language language, size_t entropyLen) { - assert(len >= 32); + assert(entropyLen >= 32); while (true) { - std::vector entropy(len, 0); - GetRandBytes(entropy.data(), len); - const char* phrase = zip339_entropy_to_phrase(language, entropy.data(), len); + std::vector entropy(entropyLen, 0); + GetRandBytes(entropy.data(), entropyLen); + const char* phrase = zip339_entropy_to_phrase(language, entropy.data(), entropyLen); std::string mnemonic(phrase); zip339_free_phrase(phrase); MnemonicSeed seed(language, mnemonic); - // TODO: check for the validity of the Sapling spending key at account 0 for this seed. - - return seed; + // Verify that the seed data is valid entropy for unified spending keys at + // account 0 and account 0x7FFFFFFE + if (libzcash::UnifiedSpendingKey::Derive(seed, bip44CoinType, 0).has_value() && + libzcash::DeriveZip32TransparentSpendingKey(seed, bip44CoinType, ZCASH_LEGACY_TRANSPARENT_ACCOUNT).has_value()) { + return seed; + } } } @@ -113,12 +116,12 @@ libzcash::SaplingPaymentAddress SaplingExtendedFullViewingKey::DefaultAddress() diversifier_index_t j_default; diversifier_index_t j_ret; - CSerializeData addr_bytes(libzcash::SerializedSaplingPaymentAddressSize); + CSerializeData addr_bytes_ret(libzcash::SerializedSaplingPaymentAddressSize); if (librustzcash_zip32_find_xfvk_address( reinterpret_cast(xfvk_bytes.data()), j_default.begin(), j_ret.begin(), - reinterpret_cast(addr_bytes.data()))) { - CDataStream ss_addr(addr_bytes, SER_NETWORK, PROTOCOL_VERSION); + reinterpret_cast(addr_bytes_ret.data()))) { + CDataStream ss_addr(addr_bytes_ret, SER_NETWORK, PROTOCOL_VERSION); libzcash::SaplingPaymentAddress addr; ss_addr >> addr; return addr; @@ -161,6 +164,27 @@ SaplingExtendedSpendingKey SaplingExtendedSpendingKey::Derive(uint32_t i) const return xsk_i; } +std::pair SaplingExtendedSpendingKey::ForAccount(const HDSeed& seed, uint32_t bip44CoinType, uint32_t accountId) { + auto m = Master(seed); + + // We use a fixed keypath scheme of m/32'/coin_type'/account' + // Derive m/32' + auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT); + // Derive m/32'/coin_type' + auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT); + + // Derive account key at next index, skip keys already known to the wallet + auto xsk = m_32h_cth.Derive(accountId | ZIP32_HARDENED_KEY_LIMIT); + + // Create new metadata + int64_t nCreationTime = GetTime(); + CKeyMetadata metadata(nCreationTime); + metadata.hdKeypath = "m/32'/" + std::to_string(bip44CoinType) + "'/" + std::to_string(accountId) + "'"; + metadata.seedFp = seed.Fingerprint(); + + return std::make_pair(xsk, metadata); +} + SaplingExtendedFullViewingKey SaplingExtendedSpendingKey::ToXFVK() const { SaplingExtendedFullViewingKey ret; @@ -173,9 +197,34 @@ SaplingExtendedFullViewingKey SaplingExtendedSpendingKey::ToXFVK() const return ret; } -libzcash::SaplingPaymentAddress SaplingExtendedSpendingKey::DefaultAddress() const -{ - return ToXFVK().DefaultAddress(); +std::optional DeriveZip32TransparentSpendingKey(const HDSeed& seed, uint32_t bip44CoinType, uint32_t accountId) { + auto rawSeed = seed.RawSeed(); + auto m = CExtKey::Master(rawSeed.data(), rawSeed.size()); + + // We use a fixed keypath scheme of m/32'/coin_type'/account' + // Derive m/32' + auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT); + if (!m_32h.has_value()) return std::nullopt; + + // Derive m/32'/coin_type' + auto m_32h_cth = m_32h.value().Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT); + if (!m_32h_cth.has_value()) return std::nullopt; + + // Derive m/32'/coin_type'/account_id' + return m_32h_cth.value().Derive(accountId | ZIP32_HARDENED_KEY_LIMIT); +} + +std::optional> UnifiedSpendingKey::Derive(const HDSeed& seed, uint32_t bip44CoinType, uint32_t accountId) { + UnifiedSpendingKey usk; + + auto transparentKey = DeriveZip32TransparentSpendingKey(seed, bip44CoinType, accountId); + if (!transparentKey.has_value()) return std::nullopt; + usk.p2pkhKey = transparentKey.value(); + + auto saplingKey = SaplingExtendedSpendingKey::ForAccount(seed, bip44CoinType, accountId); + usk.saplingKey = saplingKey.first; + + return std::make_pair(usk, saplingKey.second); } std::optional ParseZip32KeypathAccount(const std::string& keyPath) { @@ -188,4 +237,4 @@ std::optional ParseZip32KeypathAccount(const std::string& keyPath } } -} +}; diff --git a/src/zcash/address/zip32.h b/src/zcash/address/zip32.h index 6c4a36068..478f67c13 100644 --- a/src/zcash/address/zip32.h +++ b/src/zcash/address/zip32.h @@ -6,8 +6,10 @@ #define ZCASH_ZCASH_ADDRESS_ZIP32_H #include "serialize.h" +#include "key.h" #include "support/allocators/secure.h" #include "uint256.h" +#include "utiltime.h" #include "zcash/address/sapling.hpp" #include "rust/zip339.h" @@ -65,7 +67,12 @@ public: seed.assign(buf, std::end(buf)); } - static MnemonicSeed Random(Language language = English, size_t len = 32); + /** + * Randomly generate a new mnemonic seed. A SLIP-44 coin type is required to make it possible + * to check that the generated seed can produce valid transparent and unified addresses at account + * numbers 0x7FFFFFFE and 0x0 respectively. + */ + static MnemonicSeed Random(uint32_t bip44CoinType, Language language = English, size_t entropyLen = 32); static std::string LanguageName(Language language) { switch (language) { @@ -142,6 +149,51 @@ public: // This is not part of ZIP 32, but is here because it's linked to the HD seed. uint256 ovkForShieldingFromTaddr(HDSeed& seed); +// Key derivation metadata +class CKeyMetadata +{ +public: + static const int VERSION_BASIC=1; + static const int VERSION_WITH_HDDATA=10; + static const int CURRENT_VERSION=VERSION_WITH_HDDATA; + int nVersion; + int64_t nCreateTime; // 0 means unknown + std::string hdKeypath; //optional HD/zip32 keypath + uint256 seedFp; + + CKeyMetadata() + { + SetNull(); + } + CKeyMetadata(int64_t nCreateTime_) + { + SetNull(); + nCreateTime = nCreateTime_; + } + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(this->nVersion); + READWRITE(nCreateTime); + if (this->nVersion >= VERSION_WITH_HDDATA) + { + READWRITE(hdKeypath); + READWRITE(seedFp); + } + } + + void SetNull() + { + nVersion = CKeyMetadata::CURRENT_VERSION; + nCreateTime = 0; + hdKeypath.clear(); + seedFp.SetNull(); + } +}; + + namespace libzcash { typedef blob88 diversifier_index_t; @@ -212,13 +264,12 @@ struct SaplingExtendedSpendingKey { } static SaplingExtendedSpendingKey Master(const HDSeed& seed); + static std::pair ForAccount(const HDSeed& seed, uint32_t bip44CoinType, uint32_t accountId); SaplingExtendedSpendingKey Derive(uint32_t i) const; SaplingExtendedFullViewingKey ToXFVK() const; - libzcash::SaplingPaymentAddress DefaultAddress() const; - friend bool operator==(const SaplingExtendedSpendingKey& a, const SaplingExtendedSpendingKey& b) { return a.depth == b.depth && @@ -230,8 +281,25 @@ struct SaplingExtendedSpendingKey { } }; +class UnifiedSpendingKey { +private: + uint32_t accountId; + std::optional p2pkhKey; + std::optional saplingKey; + + UnifiedSpendingKey() {} +public: + static std::optional> Derive(const HDSeed& seed, uint32_t bip44CoinType, uint32_t accountId); + + const std::optional& GetSaplingExtendedSpendingKey() { + return saplingKey; + } +}; + std::optional ParseZip32KeypathAccount(const std::string& keyPath); +std::optional DeriveZip32TransparentSpendingKey(const HDSeed& seed, uint32_t bip44CoinType, uint32_t accountId); + } #endif // ZCASH_ZCASH_ADDRESS_ZIP32_H diff --git a/src/zcbenchmarks.cpp b/src/zcbenchmarks.cpp index 7a864af7e..b5299fc5e 100644 --- a/src/zcbenchmarks.cpp +++ b/src/zcbenchmarks.cpp @@ -375,7 +375,7 @@ CWalletTx CreateSaplingTxWithNoteData(const Consensus::Params& consensusParams, CBasicKeyStore& keyStore, const libzcash::SaplingExtendedSpendingKey &sk) { auto wtx = GetValidSaplingReceive(consensusParams, keyStore, sk, 10); - auto testNote = GetTestSaplingNote(sk.DefaultAddress(), 10); + auto testNote = GetTestSaplingNote(sk.ToXFVK().DefaultAddress(), 10); auto fvk = sk.expsk.full_viewing_key(); auto nullifier = testNote.note.nullifier(fvk, testNote.tree.witness().position()).value();