Skip to content

Commit

Permalink
Support import/export viewing keys for sapling addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomas-M committed Sep 3, 2019
1 parent f616cdb commit 590acaa
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 51 deletions.
28 changes: 27 additions & 1 deletion src/key_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ class ViewingKeyEncoder : public boost::static_visitor<std::string>
return ret;
}

std::string operator()(const libzcash::SaplingIncomingViewingKey& vk) const
{
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << vk;
std::vector<unsigned char> serkey(ss.begin(), ss.end());
std::vector<unsigned char> 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 {}; }
};

Expand Down Expand Up @@ -167,6 +180,7 @@ class SpendingKeyEncoder : public boost::static_visitor<std::string>
// 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)
Expand Down Expand Up @@ -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();
}

Expand Down
67 changes: 50 additions & 17 deletions src/wallet/rpcdump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -668,24 +668,27 @@ 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"
"\nArguments:\n"
"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\"")
);
Expand Down Expand Up @@ -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<libzcash::SproutViewingKey>(&viewingkey) == nullptr) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Currently, only Sprout viewing keys are supported");
}
auto vkey = boost::get<libzcash::SproutViewingKey>(viewingkey);
auto addr = vkey.address();

{
if (boost::get<libzcash::SaplingIncomingViewingKey>(&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<libzcash::SaplingPaymentAddress>(address);
auto ivk = boost::get<libzcash::SaplingIncomingViewingKey>(viewingkey);

if (addr != boost::get<libzcash::SaplingPaymentAddress>(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<libzcash::SproutViewingKey>(&viewingkey) != nullptr){
auto vkey = boost::get<libzcash::SproutViewingKey>(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");
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<libzcash::SproutPaymentAddress>(&address) == nullptr) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Currently, only Sprout zaddrs are supported");
auto addr = boost::get<libzcash::SaplingPaymentAddress>(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<libzcash::SproutPaymentAddress>(address);

auto addr = boost::get<libzcash::SproutPaymentAddress>(address);
libzcash::SproutViewingKey vk;
if (!pwalletMain->GetSproutViewingKey(addr, vk)) {
libzcash::SproutSpendingKey k;
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}

Expand Down
99 changes: 68 additions & 31 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ bool CWallet::AddSaplingIncomingViewingKey(
return false;
}

nTimeFirstKey = 1; // No birthday information for viewing keys.
if (!fFileBacked) {
return true;
}
Expand Down Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -1852,23 +1856,40 @@ std::pair<mapSaplingNoteData_t, SaplingIncomingViewingKeyMap> 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;
}
}

Expand Down Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,19 @@ class PaymentAddressBelongsToWallet : public boost::static_visitor<bool>
bool operator()(const libzcash::InvalidEncoding& no) const;
};


class IncomingViewingKeyBelongsToWallet : public boost::static_visitor<bool>
{
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<bool>
{
private:
Expand Down
5 changes: 4 additions & 1 deletion src/zcash/Address.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -219,7 +222,7 @@ class SaplingSpendingKey : public uint256 {
};

typedef boost::variant<InvalidEncoding, SproutPaymentAddress, SaplingPaymentAddress> PaymentAddress;
typedef boost::variant<InvalidEncoding, SproutViewingKey> ViewingKey;
typedef boost::variant<InvalidEncoding, SproutViewingKey, SaplingIncomingViewingKey> ViewingKey;

}

Expand Down

0 comments on commit 590acaa

Please sign in to comment.