diff --git a/src/key_io.cpp b/src/key_io.cpp index f118e3625b7..28da592fe40 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -120,6 +120,19 @@ class ViewingKeyEncoder : public boost::static_visitor return ret; } + std::string operator()(const libzcash::SaplingIncomingViewingKey& vk) const + { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << vk; + std::vector serkey(ss.begin(), ss.end()); + std::vector data; + ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, serkey.begin(), serkey.end()); + std::string ret = bech32::Encode(m_params.Bech32HRP(CChainParams::SAPLING_INCOMING_VIEWING_KEY), data); + memory_cleanse(serkey.data(), serkey.size()); + memory_cleanse(data.data(), data.size()); + return ret; + } + std::string operator()(const libzcash::InvalidEncoding& no) const { return {}; } }; @@ -167,6 +180,7 @@ class SpendingKeyEncoder : public boost::static_visitor // perform ceiling division to get the number of 5-bit clusters. const size_t ConvertedSaplingPaymentAddressSize = ((32 + 11) * 8 + 4) / 5; const size_t ConvertedSaplingExtendedSpendingKeySize = (ZIP32_XSK_SIZE * 8 + 4) / 5; +const size_t ConvertedSaplingIncomingViewingKeySize = (32 * 8 + 4) / 5; } // namespace CKey DecodeSecret(const std::string& str) @@ -325,7 +339,19 @@ libzcash::ViewingKey DecodeViewingKey(const std::string& str) return ret; } } - memory_cleanse(data.data(), data.size()); + data.clear(); + auto bech = bech32::Decode(str); + if (bech.first == Params().Bech32HRP(CChainParams::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; + return ret; + } + } return libzcash::InvalidEncoding(); } diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 3268cbd800b..4e10668b9c4 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -668,7 +668,7 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 1 || params.size() > 3) + if (fHelp || params.size() < 1 || params.size() > 4) throw runtime_error( "z_importviewingkey \"vkey\" ( rescan startHeight )\n" "\nAdds a viewing key (as returned by z_exportviewingkey) to your wallet.\n" @@ -676,16 +676,19 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp) "1. \"vkey\" (string, required) The viewing key (see z_exportviewingkey)\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 a viewing key\n" + HelpExampleCli("z_importviewingkey", "\"vkey\"") + "\nImport the viewing key without rescan\n" - + HelpExampleCli("z_importviewingkey", "\"vkey\", no") + + + HelpExampleCli("z_importviewingkey", "\"vkey\" no") + "\nImport the viewing key with partial rescan\n" + HelpExampleCli("z_importviewingkey", "\"vkey\" whenkeyisnew 30000") + "\nRe-import the viewing key with longer partial rescan\n" + HelpExampleCli("z_importviewingkey", "\"vkey\" yes 20000") + + "\nImport the viewing key for Sapling address\n" + + HelpExampleCli("z_importviewingkey", "\"vkey\" no 0 \"zaddr\"") + "\nAs a JSON-RPC call\n" + HelpExampleRpc("z_importviewingkey", "\"vkey\", \"no\"") ); @@ -725,14 +728,38 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp) if (!IsValidViewingKey(viewingkey)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid viewing key"); } - // TODO: Add Sapling support. For now, return an error to the user. - if (boost::get(&viewingkey) == nullptr) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Currently, only Sprout viewing keys are supported"); - } - auto vkey = boost::get(viewingkey); - auto addr = vkey.address(); - { + if (boost::get(&viewingkey) != nullptr) { + 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 = DecodePaymentAddress(strAddress); + if (!IsValidPaymentAddress(address)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid zaddr"); + } + + auto addr = boost::get(address); + auto ivk = boost::get(viewingkey); + + if (addr != boost::get(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"); + } + } + } else if (boost::get(&viewingkey) != nullptr){ + auto vkey = boost::get(viewingkey); + auto addr = vkey.address(); if (pwalletMain->HaveSproutSpendingKey(addr)) { throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already contains the private key for this viewing key"); } @@ -749,13 +776,14 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp) 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); - } + } else { + throw JSONRPCError(RPC_WALLET_ERROR, "Currently, only Sprout and Sapling zaddrs are supported."); } + // We want to scan for transactions and notes + if (fRescan) { + pwalletMain->ScanForWalletTransactions(chainActive[nRescanHeight], true); + } return NullUniValue; } @@ -827,12 +855,17 @@ UniValue z_exportviewingkey(const UniValue& params, bool fHelp) if (!IsValidPaymentAddress(address)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid zaddr"); } - // TODO: Add Sapling support. For now, return an error to the user. + if (boost::get(&address) == nullptr) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Currently, only Sprout zaddrs are supported"); + auto addr = boost::get(address); + libzcash::SaplingIncomingViewingKey ivk; + if (!pwalletMain->GetSaplingIncomingViewingKey(addr, ivk)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold viewing key for this zaddr"); + } + return EncodeViewingKey(ivk); } - auto addr = boost::get(address); + auto addr = boost::get(address); libzcash::SproutViewingKey vk; if (!pwalletMain->GetSproutViewingKey(addr, vk)) { libzcash::SproutSpendingKey k; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e0d26336356..e2b20da8efd 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3349,7 +3349,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) } // Visitor to support Sprout and Sapling addrs - if (!boost::apply_visitor(PaymentAddressBelongsToWallet(pwalletMain), zaddr)) { + if (!boost::apply_visitor(IncomingViewingKeyBelongsToWallet(pwalletMain), zaddr)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key or viewing key not found."); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 56569f7e0ce..19e7d77b431 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -189,6 +189,7 @@ bool CWallet::AddSaplingIncomingViewingKey( return false; } + nTimeFirstKey = 1; // No birthday information for viewing keys. if (!fFileBacked) { return true; } @@ -1453,26 +1454,29 @@ void CWallet::UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx) { } else { uint64_t position = nd.witnesses.front().position(); - SaplingFullViewingKey fvk = 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); + // Skip if we only have incoming viewing key + if (mapSaplingFullViewingKeys.count(nd.ivk) != 0) { + SaplingFullViewingKey fvk = 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(); + mapSaplingNullifiersToNotes[nullifier] = op; + item.second.nullifier = nullifier; } - uint256 nullifier = optNullifier.get(); - mapSaplingNullifiersToNotes[nullifier] = op; - item.second.nullifier = nullifier; } } } @@ -1852,23 +1856,40 @@ std::pair CWallet::FindMySap // Protocol Spec: 4.19 Block Chain Scanning (Sapling) for (uint32_t i = 0; i < tx.vShieldedOutput.size(); ++i) { const OutputDescription output = tx.vShieldedOutput[i]; + bool found = false; for (auto it = mapSaplingFullViewingKeys.begin(); it != mapSaplingFullViewingKeys.end(); ++it) { SaplingIncomingViewingKey ivk = it->first; auto result = SaplingNotePlaintext::decrypt(output.encCiphertext, ivk, output.ephemeralKey, output.cm); - if (!result) { - continue; + if (result) { + auto address = ivk.address(result.get().d); + if (address && mapSaplingIncomingViewingKeys.count(address.get()) == 0) { + viewingKeysToAdd[address.get()] = ivk; + } + // We don't cache the nullifier here as computing it requires knowledge of the note position + // in the commitment tree, which can only be determined when the transaction has been mined. + SaplingOutPoint op {hash, i}; + SaplingNoteData nd; + nd.ivk = ivk; + noteData.insert(std::make_pair(op, nd)); + found = true; + break; } - auto address = ivk.address(result.get().d); - if (address && mapSaplingIncomingViewingKeys.count(address.get()) == 0) { - viewingKeysToAdd[address.get()] = ivk; + } + if (!found) { + for (auto it = mapSaplingIncomingViewingKeys.begin(); it != mapSaplingIncomingViewingKeys.end(); ++it) { + SaplingIncomingViewingKey ivk = it-> second; + auto result = SaplingNotePlaintext::decrypt(output.encCiphertext, ivk, output.ephemeralKey, output.cm); + if (!result) { + continue; + } + // We don't cache the nullifier here as computing it requires knowledge of the note position + // in the commitment tree, which can only be determined when the transaction has been mined. + SaplingOutPoint op {hash, i}; + SaplingNoteData nd; + nd.ivk = ivk; + noteData.insert(std::make_pair(op, nd)); + break; } - // We don't cache the nullifier here as computing it requires knowledge of the note position - // in the commitment tree, which can only be determined when the transaction has been mined. - SaplingOutPoint op {hash, i}; - SaplingNoteData nd; - nd.ivk = ivk; - noteData.insert(std::make_pair(op, nd)); - break; } } @@ -4635,6 +4656,22 @@ void CWallet::GetFilteredNotes( // Shielded key and address generalizations // +bool IncomingViewingKeyBelongsToWallet::operator()(const libzcash::SproutPaymentAddress &zaddr) const +{ + return m_wallet->HaveSproutViewingKey(zaddr); +} + +bool IncomingViewingKeyBelongsToWallet::operator()(const libzcash::SaplingPaymentAddress &zaddr) const +{ + libzcash::SaplingIncomingViewingKey ivk; + return m_wallet->GetSaplingIncomingViewingKey(zaddr, ivk); +} + +bool IncomingViewingKeyBelongsToWallet::operator()(const libzcash::InvalidEncoding& no) const +{ + return false; +} + bool PaymentAddressBelongsToWallet::operator()(const libzcash::SproutPaymentAddress &zaddr) const { return m_wallet->HaveSproutSpendingKey(zaddr) || m_wallet->HaveSproutViewingKey(zaddr); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 2ee1074613c..1943b753cff 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1393,6 +1393,19 @@ class PaymentAddressBelongsToWallet : public boost::static_visitor bool operator()(const libzcash::InvalidEncoding& no) const; }; + +class IncomingViewingKeyBelongsToWallet : public boost::static_visitor +{ +private: + CWallet *m_wallet; +public: + IncomingViewingKeyBelongsToWallet(CWallet *wallet) : m_wallet(wallet) {} + + bool operator()(const libzcash::SproutPaymentAddress &zaddr) const; + bool operator()(const libzcash::SaplingPaymentAddress &zaddr) const; + bool operator()(const libzcash::InvalidEncoding& no) const; +}; + class HaveSpendingKeyForPaymentAddress : public boost::static_visitor { private: diff --git a/src/zcash/Address.hpp b/src/zcash/Address.hpp index dd2a75cffbc..9a2169570d5 100644 --- a/src/zcash/Address.hpp +++ b/src/zcash/Address.hpp @@ -124,6 +124,9 @@ class SaplingPaymentAddress { friend inline bool operator==(const SaplingPaymentAddress& a, const SaplingPaymentAddress& b) { return a.d == b.d && a.pk_d == b.pk_d; } + friend inline bool operator!=(const SaplingPaymentAddress& a, const SaplingPaymentAddress& b) { + return a.d != b.d || a.pk_d != b.pk_d; + } friend inline bool operator<(const SaplingPaymentAddress& a, const SaplingPaymentAddress& b) { return (a.d < b.d || (a.d == b.d && a.pk_d < b.pk_d)); @@ -219,7 +222,7 @@ class SaplingSpendingKey : public uint256 { }; typedef boost::variant PaymentAddress; -typedef boost::variant ViewingKey; +typedef boost::variant ViewingKey; }