diff --git a/src/key_io.cpp b/src/key_io.cpp index 78d362ab5..6cc8f2d33 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -337,6 +337,45 @@ libzcash::ViewingKey DecodeViewingKey(const std::string& str) return libzcash::InvalidEncoding(); } +std::string KeyIO::EncodeIVK(const libzcash::SaplingIncomingViewingKey& ivk) +{ + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << ivk; + // ConvertBits requires unsigned char, but CDataStream uses char + std::vector serkey(ss.begin(), ss.end()); + std::vector data; + // See calculation comment below + data.reserve((serkey.size() * 8 + 4) / 5); + ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, serkey.begin(), serkey.end()); + std::string ret = bech32::Encode(keyConstants.Bech32HRP(KeyConstants::SAPLING_INCOMING_VIEWING_KEY), data); + memory_cleanse(serkey.data(), serkey.size()); + memory_cleanse(data.data(), data.size()); + return ret; +} + +libzcash::SaplingIncomingViewingKey KeyIO::DecodeIVK(const std::string& str) +{ + std::vector data; + auto bech = bech32::Decode(str); + if (bech.first == keyConstants.Bech32HRP(KeyConstants::SAPLING_INCOMING_VIEWING_KEY) && + bech.second.size() == ConvertedSaplingIncomingViewingKeySize) { + // Bech32 decoding + data.reserve((bech.second.size() * 5) / 8); + if (ConvertBits<5, 8, false>([&](unsigned char c) { data.push_back(c); }, bech.second.begin(), bech.second.end())) { + CDataStream ss(data, SER_NETWORK, PROTOCOL_VERSION); + libzcash::SaplingIncomingViewingKey ret; + ss >> ret; + memory_cleanse(data.data(), data.size()); + return ret; + } + } + + memory_cleanse(data.data(), data.size()); + libzcash::SaplingIncomingViewingKey ret; + ret.SetNull(); + return ret; +} + std::string EncodeSpendingKey(const libzcash::SpendingKey& zkey) { return boost::apply_visitor(SpendingKeyEncoder(Params()), zkey); diff --git a/src/key_io.h b/src/key_io.h index 008a6ace6..bea804036 100644 --- a/src/key_io.h +++ b/src/key_io.h @@ -39,6 +39,9 @@ bool IsValidPaymentAddressString(const std::string& str, uint32_t consensusBranc std::string EncodeViewingKey(const libzcash::ViewingKey& vk); libzcash::ViewingKey DecodeViewingKey(const std::string& str); +std::string EncodeIVK(const libzcash::SaplingIncomingViewingKey& ivk); +libzcash::SaplingIncomingViewingKey DecodeIVK(const std::string& str); + std::string EncodeSpendingKey(const libzcash::SpendingKey& zkey); libzcash::SpendingKey DecodeSpendingKey(const std::string& str); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 563864c12..d9b6b115a 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -160,6 +160,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "kvupdate", 4 }, { "z_importkey", 2 }, { "z_importviewingkey", 2 }, + { "z_importivk", 2 }, { "z_listsentbyaddress", 1}, { "z_listsentbyaddress", 2}, { "z_listsentbyaddress", 3}, diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 39b5caf38..4e50ff28a 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -798,6 +798,140 @@ UniValue dumpwallet_impl(const UniValue& params, bool fHelp, bool fDumpZKeys) return exportfilepath.string(); } +UniValue z_getalldiversifiedaddresses(const UniValue& params, bool fHelp) { + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() > 1) + throw runtime_error( + "z_getalldiversifiedaddresses z_address\n" + "\nReturns the list of all Sapling shielded addresses that share the same spending key as this address.\nThese are all peer diversified addresses." + "\nArguments:\n" + "1. z_address (String) The z_address to lookup\n" + "\nResult:\n" + "[ (json array of string)\n" + " \"zaddr\" (string) a zaddr belonging to the wallet which shares the same spending key\n" + " ,...\n" + "]\n" + "\nExamples:\n" + + HelpExampleCli("z_getalldiversifiedaddresses", "my_z_address") + + HelpExampleRpc("z_getalldiversifiedaddresses", "my_z_address") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + KeyIO keyIO(Params()); + string strAddress = params[0].get_str(); + + auto in_address = keyIO.DecodePaymentAddress(strAddress); + if (!IsValidPaymentAddress(in_address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid zaddr"); + } + if (!IsValidSaplingAddress(in_address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Sapling zaddr"); + } + + UniValue ret(UniValue::VARR); + + // Get the incoming viewing key for the given address + libzcash::SaplingIncomingViewingKey in_ivk; + libzcash::SaplingExtendedFullViewingKey in_xfvk; + pwalletMain->GetSaplingIncomingViewingKey(std::get(in_address), in_ivk); + pwalletMain->GetSaplingFullViewingKey(in_ivk, in_xfvk); + + std::set addresses; + pwalletMain->GetSaplingPaymentAddresses(addresses); + for (auto addr : addresses) { + libzcash::SaplingIncomingViewingKey ivk; + libzcash::SaplingExtendedFullViewingKey xfvk; + + pwalletMain->GetSaplingIncomingViewingKey(addr, ivk); + pwalletMain->GetSaplingFullViewingKey(ivk, xfvk); + + if (ivk == in_ivk && xfvk == in_xfvk) { + ret.push_back(keyIO.EncodePaymentAddress(addr)); + } + } + + return ret; +} + +UniValue z_getnewdiversifiedaddress(const UniValue& params, bool fHelp) { + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() < 1 || params.size() > 3) + throw runtime_error( + "z_getnewdiversifiedaddress \"z_address\"\n" + "\nReturns a new diversified address based on the given z_address, and adds it to your wallet.\n" + "\nArguments:\n" + "1. \"z_address\" (string, required) An existing z address in the wallet.(see z_listaddresses)\n" + "\nExamples:\n" + "\nGet a new z address\n" + + HelpExampleCli("z_getnewdiversifiedaddress", "\"my_z_address\"") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("z_getnewdiversifiedaddress", "\"my_z_address\"") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + + KeyIO keyIO(Params()); + string strAddress = params[0].get_str(); + + auto in_address = keyIO.DecodePaymentAddress(strAddress); + + if (!IsValidPaymentAddress(in_address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid zaddr"); + } + + if (!IsValidSaplingAddress(in_address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Sapling zaddr"); + } + + if (!std::visit(HaveSpendingKeyForPaymentAddress(pwalletMain), in_address)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold private zkey for this zaddr"); + } + + // Get Sapling Address + auto sk = std::visit(GetSpendingKeyForPaymentAddress(pwalletMain), in_address).value(); + // Now, get a new diversified address from the private key + auto espk = std::get(sk); + arith_uint88 div; + + libzcash::PaymentAddress address; + + // Iterate over the diversified addresses + while (true) { + div++; + + // Try to obtain an address with the default diversifier + auto try_address = espk.ToXFVK().Address(ArithToUint88(div)); + + // If there is no address, that means the diversifier was incompatible (~50% chance) + if (!try_address.has_value()) { + // Increment the diversifier and try again + continue; + } + + // Update the diversifier from the one that was returned + div = UintToArith88(try_address.value().first); + + // Check if the address exists + if (std::visit(PaymentAddressBelongsToWallet(pwalletMain), libzcash::PaymentAddress(try_address.value().second))) { + continue; + } + + // If it doesn't exist, then add it. + pwalletMain->AddSaplingIncomingViewingKey(espk.expsk.full_viewing_key().in_viewing_key(), try_address.value().second); + address = try_address.value().second; + + break; + } + + return keyIO.EncodePaymentAddress(address); +} UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk) { @@ -1064,6 +1198,147 @@ UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& m return EncodeViewingKey(ivk); } +UniValue z_exportivk(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() != 1) + throw runtime_error( + "z_exportivk \"zaddr\"\n" + "\nReveals the incoming viewing key corresponding to Sapling 'zaddr'.\n" + "Then the z_importivk can be used with this output\n" + "\nArguments:\n" + "1. \"zaddr\" (string, required) The Sapling zaddr for the viewing key\n" + "\nResult:\n" + "\"vkey\" (string) The viewing key\n" + "\nExamples:\n" + + HelpExampleCli("z_exportivk", "\"myaddress\"") + + HelpExampleRpc("z_exportivk", "\"myaddress\"") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + + string strAddress = params[0].get_str(); + + KeyIO keyIO(Params()); + auto address = keyIO.DecodePaymentAddress(strAddress); + if (!IsValidPaymentAddress(address) || !IsValidSaplingAddress(address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Sapling zaddr"); + } + + auto spa = std::get(address); + libzcash::SaplingIncomingViewingKey ivk; + if (!pwalletMain->GetSaplingIncomingViewingKey(spa, ivk)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold viewing key for this zaddr"); + } else { + return keyIO.EncodeIVK(ivk); + } +} + +UniValue z_importivk(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() < 1 || params.size() > 4) + throw runtime_error( + "z_importivk \"vkey\" ( rescan startHeight )\n" + "\nAdds a viewing key (as returned by z_exportivk) to your wallet.\n" + "\nArguments:\n" + "1. \"vkey\" (string, required) The viewing key (see z_exportivk)\n" + "2. rescan (string, optional, default=\"whenkeyisnew\") Rescan the wallet for transactions - can be \"yes\", \"no\" or \"whenkeyisnew\"\n" + "3. startHeight (numeric, optional, default=0) Block height to start rescan from\n" + "4. zaddr (string, optional, default=\"\") zaddr in case of importing viewing key for Sapling\n" + "\nNote: This call can take minutes to complete if rescan is true.\n" + "\nExamples:\n" + "\nImport an incoming viewing key\n" + + HelpExampleCli("z_importivk", "\"vkey\"") + + "\nImport the incoming viewing key without rescan\n" + + HelpExampleCli("z_importivk", "\"vkey\" no") + + "\nImport the incoming viewing key with partial rescan\n" + + HelpExampleCli("z_importivk", "\"vkey\" whenkeyisnew 30000") + + "\nRe-import the viewing key with longer partial rescan\n" + + HelpExampleCli("z_importivk", "\"vkey\" yes 20000") + + "\nImport the incoming viewing key for Sapling address\n" + + HelpExampleCli("z_importivk", "\"vkey\" no 0 \"zaddr\"") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("z_importivk", "\"vkey\", \"no\"") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + + // Whether to perform rescan after import + bool fRescan = true; + bool fIgnoreExistingKey = true; + if (params.size() > 1) { + auto rescan = params[1].get_str(); + if (rescan.compare("whenkeyisnew") != 0) { + fIgnoreExistingKey = false; + if (rescan.compare("no") == 0) { + fRescan = false; + } else if (rescan.compare("yes") != 0) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "rescan must be \"yes\", \"no\" or \"whenkeyisnew\""); + } + } + } + + // Height to rescan from + int nRescanHeight = 0; + if (params.size() > 2) { + nRescanHeight = params[2].get_int(); + } + if (nRescanHeight < 0 || nRescanHeight > chainActive.Height()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range"); + } + + string strIVKey = params[0].get_str(); + KeyIO keyIO(Params()); + auto ivk = keyIO.DecodeIVK(strIVKey); + if (ivk.IsNull()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid incoming viewing key"); + } + + if (params.size() < 4) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Missing zaddr for Sapling viewing key."); + } + string strAddress = params[3].get_str(); + auto address = keyIO.DecodePaymentAddress(strAddress); + if (!IsValidSaplingAddress(address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Sapling zaddr"); + } + + auto addr = std::get(address); + + if (!(addr == ivk.address(addr.d))) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Zaddr and viewing key are not consistent."); + } + + if (pwalletMain->HaveSaplingIncomingViewingKey(addr)) { + if (fIgnoreExistingKey) { + return NullUniValue; + } + } else { + pwalletMain->MarkDirty(); + + if (!pwalletMain->AddSaplingIncomingViewingKey(ivk, addr)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Error adding viewing key to wallet"); + } + } + + // We want to scan for transactions and notes + if (fRescan) { + pwalletMain->ScanForWalletTransactions(chainActive[nRescanHeight], true); + } + return NullUniValue; +} + extern int32_t HUSH_NSPV; #ifndef HUSH_NSPV_FULLNODE #define HUSH_NSPV_FULLNODE (HUSH_NSPV <= 0) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index c5ac5f8dc..055128e26 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -6216,6 +6216,10 @@ extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& m extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue z_exportwallet(const UniValue& params, bool fHelp, const CPubKey& mypk); +extern UniValue z_getnewdiversifiedaddress(const UniValue& params, bool fHelp); +extern UniValue z_getalldiversifiedaddresses(const UniValue& params, bool fHelp); +extern UniValue z_exportivk(const UniValue& params, bool fHelp); +extern UniValue z_importivk(const UniValue& params, bool fHelp); extern UniValue z_importwallet(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue rescan(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue getrescaninfo(const UniValue& params, bool fHelp, const CPubKey& mypk); @@ -6295,6 +6299,10 @@ static const CRPCCommand commands[] = { "wallet", "z_exportviewingkey", &z_exportviewingkey, true }, { "wallet", "z_importviewingkey", &z_importviewingkey, true }, { "wallet", "z_exportwallet", &z_exportwallet, true }, + { "wallet", "z_getnewdiversifiedaddress", &z_getnewdiversifiedaddress, true}, + { "wallet", "z_getalldiversifiedaddresses", &z_getalldiversifiedaddresses, true}, + { "wallet", "z_exportivk", &z_exportivk, true }, + { "wallet", "z_importivk", &z_importivk, true }, { "wallet", "z_importwallet", &z_importwallet, true }, { "wallet", "z_viewtransaction", &z_viewtransaction, true }, { "wallet", "z_getinfo", &z_getinfo, true }, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d217c41ec..37949be74 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1407,32 +1407,36 @@ void CWallet::UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx) { if (nd.witnesses.empty()) { // If there are no witnesses, erase the nullifier and associated mapping. if (item.second.nullifier) { - mapSaplingNullifiersToNotes.erase(item.second.nullifier.get()); + mapSaplingNullifiersToNotes.erase(item.second.nullifier.value()); } - item.second.nullifier = boost::none; + item.second.nullifier = std::nullopt; } else { uint64_t position = nd.witnesses.front().position(); // Skip if we only have incoming viewing key if (mapSaplingFullViewingKeys.count(nd.ivk) != 0) { - SaplingFullViewingKey fvk = mapSaplingFullViewingKeys.at(nd.ivk); + auto extfvk = mapSaplingFullViewingKeys.at(nd.ivk); OutputDescription output = wtx.vShieldedOutput[op.n]; - auto optPlaintext = SaplingNotePlaintext::decrypt(output.encCiphertext, nd.ivk, output.ephemeralKey, output.cm); - if (!optPlaintext) { - // An item in mapSaplingNoteData must have already been successfully decrypted, - // otherwise the item would not exist in the first place. - assert(false); - } - auto optNote = optPlaintext.get().note(nd.ivk); - if (!optNote) { - assert(false); - } - auto optNullifier = optNote.get().nullifier(fvk, position); - if (!optNullifier) { - // This should not happen. If it does, maybe the position has been corrupted or miscalculated? - assert(false); - } - uint256 nullifier = optNullifier.get(); + + auto optDeserialized = SaplingNotePlaintext::attempt_sapling_enc_decryption_deserialization(output.encCiphertext, nd.ivk, output.ephemeralKey); + + // The transaction would not have entered the wallet unless + // its plaintext had been successfully decrypted previously. + assert(optDeserialized != std::nullopt); + + auto optPlaintext = SaplingNotePlaintext::plaintext_checks_without_height(*optDeserialized, nd.ivk, output.ephemeralKey, output.cmu); + + // An item in mapSaplingNoteData must have already been successfully decrypted, + // otherwise the item would not exist in the first place. + assert(optPlaintext != std::nullopt); + + auto optNote = optPlaintext.value().note(nd.ivk); + assert(optNote != std::nullopt); + + auto optNullifier = optNote.value().nullifier(extfvk.fvk, position); + // This should not happen. If it does, maybe the position has been corrupted or miscalculated? + assert(optNullifier != std::nullopt); + uint256 nullifier = optNullifier.value(); mapSaplingNullifiersToNotes[nullifier] = op; item.second.nullifier = nullifier; }