wallet: stop z_listsentbyaddress re-deriving keys and copying the wallet
z_listsentbyaddress, getalldata, z_getbalances and z_listreceivedaddress deep-copied every CWalletTx (witness lists included) into an ordered map, and the JSON helpers re-derived keys and scanned the whole wallet for every output: zsTxReceivedToJSON trial-decrypted each output with every spending key, and each change check called GetNullifiersForAddresses, a full mapWallet note scan, once per decrypted output. - ZsTxJSONContext memoises per call the spending keys with their ivk/ovk, the HD-seed ovk and the wallet's nullifier set. - Wallet txs are ordered by pointer. stable_sort + unique on nOrderPos keeps the first entry, exactly like the old map::insert. - A received output is first decrypted with the ivk the wallet recorded for it when that ivk belongs to one of the usable keys, then with the remaining keys in the old order. - "size" is measured without a slicing copy; the result is reversed in one pass; getalldata's note loop reads note data by reference and reuses the memoised keys. Output is unchanged. New opt-in 6th argument sendsOnly (default false): true omits each entry's "spends" and "received" sections and the trial decryption they need. Older daemons reject a 6th argument with the help text, so a client can fall back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+112
-27
@@ -11,11 +11,72 @@
|
||||
#include "utilmoneystr.h"
|
||||
#include <utf8.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
using namespace libzcash;
|
||||
|
||||
bool EnsureWalletIsAvailable(bool avoidException);
|
||||
|
||||
const std::vector<ZsTxJSONContext::Key>& ZsTxJSONContext::keys()
|
||||
{
|
||||
if (!keysBuilt) {
|
||||
// The calls, order and filter of the per-output key loops this replaces.
|
||||
std::set<libzcash::SaplingPaymentAddress> addresses;
|
||||
pwalletMain->GetSaplingPaymentAddresses(addresses);
|
||||
for (std::set<libzcash::SaplingPaymentAddress>::const_iterator it = addresses.begin(); it != addresses.end(); ++it) {
|
||||
libzcash::SaplingExtendedSpendingKey extsk;
|
||||
if (!pwalletMain->GetSaplingExtendedSpendingKey(*it, extsk))
|
||||
continue; // viewing-key-only, or encrypted and locked: skipped, as before
|
||||
libzcash::SaplingFullViewingKey fvk = extsk.expsk.full_viewing_key();
|
||||
Key key;
|
||||
key.addr = *it;
|
||||
key.ivk = fvk.in_viewing_key();
|
||||
key.ovk = fvk.ovk;
|
||||
vKeys.push_back(key);
|
||||
}
|
||||
keysBuilt = true;
|
||||
}
|
||||
return vKeys;
|
||||
}
|
||||
|
||||
bool ZsTxJSONContext::hasNullifier(const libzcash::SaplingPaymentAddress& addr, const uint256& nullifier)
|
||||
{
|
||||
if (!nullifiersBuilt) {
|
||||
// The population GetNullifiersForAddresses() scans: every mapWallet note that has a nullifier.
|
||||
for (std::map<uint256, CWalletTx>::const_iterator it = pwalletMain->mapWallet.begin(); it != pwalletMain->mapWallet.end(); ++it) {
|
||||
for (mapSaplingNoteData_t::const_iterator nit = it->second.mapSaplingNoteData.begin(); nit != it->second.mapSaplingNoteData.end(); ++nit) {
|
||||
if (nit->second.nullifier)
|
||||
nfByIvk.insert(std::make_pair(nit->second.ivk, nit->second.nullifier.get()));
|
||||
}
|
||||
}
|
||||
nullifiersBuilt = true;
|
||||
}
|
||||
// GetNullifiersForAddresses() ignores the result of this lookup, so an address the wallet does not
|
||||
// know is matched against the default (null) ivk. Keep that.
|
||||
libzcash::SaplingIncomingViewingKey ivk;
|
||||
pwalletMain->GetSaplingIncomingViewingKey(addr, ivk);
|
||||
return nfByIvk.count(std::make_pair(ivk, nullifier)) > 0;
|
||||
}
|
||||
|
||||
bool ZsTxJSONContext::seedOvk(uint256& ovkOut)
|
||||
{
|
||||
if (!seedOvkTried) {
|
||||
HDSeed seed;
|
||||
if (pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
seedOvkValue = ovkForShieldingFromTaddr(seed);
|
||||
seedOvkOk = true;
|
||||
}
|
||||
seedOvkTried = true;
|
||||
}
|
||||
if (seedOvkOk)
|
||||
ovkOut = seedOvkValue;
|
||||
return seedOvkOk;
|
||||
}
|
||||
|
||||
void zsTxSpendsToJSON(const CWalletTx& wtx, UniValue& spends, CAmount& totalSpends, CAmount& filteredSpends, const std::string& strAddress, bool filterByAddress) {
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
@@ -132,13 +193,16 @@ void zsTxSpendsToJSON(const CWalletTx& wtx, UniValue& spends, CAmount& totalSpen
|
||||
}
|
||||
|
||||
|
||||
void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& totalReceived, const std::string& strAddress, bool filterByAddress) {
|
||||
void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& totalReceived, const std::string& strAddress, bool filterByAddress, ZsTxJSONContext* ctx) {
|
||||
|
||||
if(fZdebug)
|
||||
fprintf(stderr,"%s: txid=%s\n", __func__, wtx.GetHash().ToString().c_str() );
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
ZsTxJSONContext localCtx; // used only when the caller passed no per-call memo
|
||||
ZsTxJSONContext& zctx = ctx ? *ctx : localCtx;
|
||||
|
||||
//Check address
|
||||
bool isTAddress = false;
|
||||
bool isZsAddress = false;
|
||||
@@ -192,13 +256,36 @@ void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& total
|
||||
UniValue obj(UniValue::VOBJ);
|
||||
bool changeTx = false;
|
||||
//Decrypt sapling incoming commitments using IVK
|
||||
std::set<libzcash::SaplingPaymentAddress> addresses;
|
||||
pwalletMain->GetSaplingPaymentAddresses(addresses);
|
||||
for (auto addr : addresses) {
|
||||
libzcash::SaplingExtendedSpendingKey extsk;
|
||||
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, extsk)) {
|
||||
const std::vector<ZsTxJSONContext::Key>& keys = zctx.keys();
|
||||
// An output opens under at most one ivk (decrypt() checks the note commitment, which binds
|
||||
// pk_d = [ivk]g_d), and FindMySaplingNotes records that ivk in mapSaplingNoteData. When the recorded
|
||||
// ivk is one of keys(), pass 0 tries only the keys with that ivk: every other key would fail, as in
|
||||
// the old single loop. If pass 0 opens nothing (stale note data), pass 1 tries the remaining keys in
|
||||
// the same order. Without a usable record only pass 1 runs, over every key, exactly as before.
|
||||
libzcash::SaplingIncomingViewingKey recordedIvk;
|
||||
bool narrow = false;
|
||||
{
|
||||
mapSaplingNoteData_t::const_iterator ndIt = wtx.mapSaplingNoteData.find(SaplingOutPoint(wtx.GetHash(), i));
|
||||
if (ndIt != wtx.mapSaplingNoteData.end()) {
|
||||
for (size_t k = 0; k < keys.size() && !narrow; k++) {
|
||||
if (keys[k].ivk == ndIt->second.ivk) {
|
||||
recordedIvk = ndIt->second.ivk;
|
||||
narrow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bool openedByRecordedIvk = false;
|
||||
for (int pass = narrow ? 0 : 1; pass < 2 && !openedByRecordedIvk; pass++) {
|
||||
for (size_t k = 0; k < keys.size(); k++) {
|
||||
if (narrow && ((pass == 0) != (keys[k].ivk == recordedIvk)))
|
||||
continue; // pass 0: only the recorded ivk; pass 1: only the others
|
||||
const libzcash::SaplingPaymentAddress& addr = keys[k].addr;
|
||||
{ // spending-key availability is resolved once per call, in ZsTxJSONContext::keys()
|
||||
auto pt = libzcash::SaplingNotePlaintext::decrypt(
|
||||
outputDesc.encCiphertext, extsk.expsk.full_viewing_key().in_viewing_key(), outputDesc.ephemeralKey, outputDesc.cm);
|
||||
outputDesc.encCiphertext, keys[k].ivk, outputDesc.ephemeralKey, outputDesc.cm);
|
||||
if (pt && pass == 0)
|
||||
openedByRecordedIvk = true;
|
||||
|
||||
if (pt) {
|
||||
auto note = pt.get();
|
||||
@@ -219,10 +306,8 @@ void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& total
|
||||
|
||||
//Check Change Status
|
||||
if (wtx.vShieldedSpend.size()!=0) {
|
||||
std::set<std::pair<PaymentAddress, uint256>> nullifierSet;
|
||||
nullifierSet = pwalletMain->GetNullifiersForAddresses({addr});
|
||||
BOOST_FOREACH(const SpendDescription& spendDesc, wtx.vShieldedSpend) {
|
||||
if (nullifierSet.count(std::make_pair(addr, spendDesc.nullifier))) {
|
||||
if (zctx.hasNullifier(addr, spendDesc.nullifier)) {
|
||||
changeTx = true;
|
||||
}
|
||||
}
|
||||
@@ -235,6 +320,7 @@ void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& total
|
||||
}
|
||||
}
|
||||
}
|
||||
} // pass
|
||||
}
|
||||
}
|
||||
received.push_back(Pair("saplingReceived",zsReceived));
|
||||
@@ -243,10 +329,13 @@ void zsTxReceivedToJSON(const CWalletTx& wtx, UniValue& received, CAmount& total
|
||||
}
|
||||
|
||||
|
||||
void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends, const std::string& strAddress, bool filterByAddress) {
|
||||
void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends, const std::string& strAddress, bool filterByAddress, ZsTxJSONContext* ctx) {
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
ZsTxJSONContext localCtx; // used only when the caller passed no per-call memo
|
||||
ZsTxJSONContext& zctx = ctx ? *ctx : localCtx;
|
||||
|
||||
//Used to identify incomplete key sets
|
||||
int shieldedOutputCount = 0;
|
||||
int shieldedOutputDecryptedCount = 0;
|
||||
@@ -304,10 +393,10 @@ void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends,
|
||||
|
||||
//Decrypt sapling outgoing t to z transaction using HDseed
|
||||
if (wtx.vShieldedSpend.size()==0) {
|
||||
HDSeed seed;
|
||||
if (pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
uint256 taddrOvk;
|
||||
if (zctx.seedOvk(taddrOvk)) {
|
||||
auto opt = libzcash::SaplingOutgoingPlaintext::decrypt(
|
||||
outputDesc.outCiphertext,ovkForShieldingFromTaddr(seed),outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey);
|
||||
outputDesc.outCiphertext,taddrOvk,outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey);
|
||||
|
||||
if (opt) {
|
||||
auto opt_unwrapped = opt.get();
|
||||
@@ -332,13 +421,11 @@ void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends,
|
||||
|
||||
//attempt Decryption of Outgoing Sapling using wallet extended spending keys
|
||||
} else {
|
||||
std::set<libzcash::SaplingPaymentAddress> addresses;
|
||||
pwalletMain->GetSaplingPaymentAddresses(addresses);
|
||||
for (auto addr : addresses) {
|
||||
libzcash::SaplingExtendedSpendingKey extsk;
|
||||
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, extsk)) {
|
||||
const std::vector<ZsTxJSONContext::Key>& keys = zctx.keys();
|
||||
for (size_t k = 0; k < keys.size(); k++) {
|
||||
{ // spending-key availability is resolved once per call, in ZsTxJSONContext::keys()
|
||||
auto opt = libzcash::SaplingOutgoingPlaintext::decrypt(
|
||||
outputDesc.outCiphertext,extsk.expsk.full_viewing_key().ovk,outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey);
|
||||
outputDesc.outCiphertext,keys[k].ovk,outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey);
|
||||
|
||||
if (opt) {
|
||||
auto opt_unwrapped = opt.get();
|
||||
@@ -365,10 +452,8 @@ void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends,
|
||||
|
||||
//Check Change Status
|
||||
if (wtx.vShieldedSpend.size()!=0) {
|
||||
std::set<std::pair<PaymentAddress, uint256>> nullifierSet;
|
||||
nullifierSet = pwalletMain->GetNullifiersForAddresses({sentAddr});
|
||||
BOOST_FOREACH(const SpendDescription& spendDesc, wtx.vShieldedSpend) {
|
||||
if (nullifierSet.count(std::make_pair(sentAddr, spendDesc.nullifier))) {
|
||||
if (zctx.hasNullifier(sentAddr, spendDesc.nullifier)) {
|
||||
changeTx = true;
|
||||
}
|
||||
}
|
||||
@@ -397,7 +482,7 @@ void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends,
|
||||
}
|
||||
|
||||
|
||||
void zsWalletTxJSON(const CWalletTx& wtx, UniValue& ret, const std::string strAddress, bool fBool, const int returnType) {
|
||||
void zsWalletTxJSON(const CWalletTx& wtx, UniValue& ret, const std::string strAddress, bool fBool, const int returnType, ZsTxJSONContext* ctx) {
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
@@ -445,7 +530,7 @@ void zsWalletTxJSON(const CWalletTx& wtx, UniValue& ret, const std::string strAd
|
||||
tx.push_back(Pair("expiryheight", (int64_t)wtx.nExpiryHeight));
|
||||
tx.push_back(Pair("confirmations", confirms));
|
||||
tx.push_back(Pair("time", wtx.GetTxTime()));
|
||||
tx.push_back(Pair("size", static_cast<uint64_t>(GetSerializeSize(static_cast<CTransaction>(wtx), SER_NETWORK, PROTOCOL_VERSION))));
|
||||
tx.push_back(Pair("size", static_cast<uint64_t>(GetSerializeSize(static_cast<const CTransaction&>(wtx), SER_NETWORK, PROTOCOL_VERSION))));
|
||||
|
||||
//Wallet Conflicts
|
||||
UniValue conflicts(UniValue::VARR);
|
||||
@@ -469,7 +554,7 @@ void zsWalletTxJSON(const CWalletTx& wtx, UniValue& ret, const std::string strAd
|
||||
}
|
||||
// Get Received
|
||||
if (returnType == 0 || returnType == 2) {
|
||||
zsTxReceivedToJSON(wtx, received, totalReceived, strAddress, fBool);
|
||||
zsTxReceivedToJSON(wtx, received, totalReceived, strAddress, fBool, ctx);
|
||||
if (!fBool || totalReceived != 0) {
|
||||
tx.push_back(Pair("received",received));
|
||||
}
|
||||
@@ -479,7 +564,7 @@ void zsWalletTxJSON(const CWalletTx& wtx, UniValue& ret, const std::string strAd
|
||||
if (returnType == 0 || returnType == 3) {
|
||||
//Only include sends if there are spends that belong to the wallet.
|
||||
if (totalSpends != 0 || fBool) {
|
||||
zsTxSendsToJSON(wtx, sends, totalSends, strAddress, fBool);
|
||||
zsTxSendsToJSON(wtx, sends, totalSends, strAddress, fBool, ctx);
|
||||
}
|
||||
if (!fBool || totalSends != 0) {
|
||||
tx.push_back(Pair("sends",sends));
|
||||
|
||||
Reference in New Issue
Block a user