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:
2026-10-09 13:31:01 -05:00
co-authored by Claude Opus 5.5
parent 106fc1ceac
commit 375b995b38
3 changed files with 232 additions and 70 deletions
+74 -40
View File
@@ -48,6 +48,7 @@
#include <boost/assign/list_of.hpp>
#include <utf8.h>
#include <univalue.h>
#include <algorithm>
#include <numeric>
#include "hush_defs.h"
#include <string.h>
@@ -2960,6 +2961,26 @@ UniValue listunspent(const UniValue& params, bool fHelp, const CPubKey& mypk)
return results;
}
// Wallet transactions ordered by nOrderPos without copying them (the old code copied every CWalletTx,
// with every note's witness list, into a map<int64_t,CWalletTx>). The pointers point into
// pwalletMain->mapWallet and are valid only while cs_wallet is held and nothing erases from mapWallet
// (std::map insertions do not invalidate them). Reproduces map<int64_t,CWalletTx>::insert exactly:
// ascending nOrderPos and, for equal nOrderPos, only the first tx in mapWallet (txid) order, because
// insert() never replaces an existing key; hence stable_sort + unique.
typedef std::pair<int64_t, const CWalletTx*> OrderedWtx;
static bool OrderedWtxLess(const OrderedWtx& a, const OrderedWtx& b) { return a.first < b.first; }
static bool OrderedWtxSamePos(const OrderedWtx& a, const OrderedWtx& b) { return a.first == b.first; }
static void BuildOrderedWalletTxs(std::vector<OrderedWtx>& out)
{
AssertLockHeld(pwalletMain->cs_wallet);
out.clear();
out.reserve(pwalletMain->mapWallet.size());
for (std::map<uint256, CWalletTx>::const_iterator it = pwalletMain->mapWallet.begin(); it != pwalletMain->mapWallet.end(); ++it)
out.push_back(OrderedWtx(it->second.nOrderPos, &it->second));
std::stable_sort(out.begin(), out.end(), OrderedWtxLess);
out.erase(std::unique(out.begin(), out.end(), OrderedWtxSamePos), out.end());
}
// Return current blockchain status, wallet balance, address balance and the last 200 transactions
UniValue getalldata(const UniValue& params, bool fHelp,const CPubKey&)
{
@@ -3031,12 +3052,14 @@ UniValue getalldata(const UniValue& params, bool fHelp,const CPubKey&)
addressBalances.insert(make_pair(addressString,txAmounts));
}
//Create Ordered List
map<int64_t,CWalletTx> orderedTxs;
// Per-call key/nullifier memo for the note loop below and for zsWalletTxJSON (rpchushwallet.h).
// Declared after the LOCK2 above, so it is destroyed before cs_wallet is released.
ZsTxJSONContext zsCtx;
//Classify every wallet tx for the balances (the ordered list for listtransactions is built below)
for (map<uint256, CWalletTx>::iterator it = pwalletMain->mapWallet.begin(); it != pwalletMain->mapWallet.end(); ++it) {
const uint256& wtxid = it->first;
const CWalletTx& wtx = (*it).second;
orderedTxs.insert(std::pair<int64_t,CWalletTx>(wtx.nOrderPos, wtx));
unsigned int txType = 0;
// 0 Unassigend
@@ -3105,18 +3128,19 @@ UniValue getalldata(const UniValue& params, bool fHelp,const CPubKey&)
for (auto & pair : wtx.mapSaplingNoteData) {
SaplingOutPoint op = pair.first;
SaplingNoteData nd = pair.second;
const SaplingNoteData& nd = pair.second;
//Skip Spent
if (nd.nullifier && pwalletMain->IsSaplingSpent(*nd.nullifier))
continue;
//Decrypt sapling incoming commitments using IVK
for (auto addr : zs_addresses) {
libzcash::SaplingExtendedSpendingKey extsk;
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, extsk)) {
const std::vector<ZsTxJSONContext::Key>& zsKeys = zsCtx.keys();
for (size_t k = 0; k < zsKeys.size(); k++) {
const libzcash::SaplingPaymentAddress& addr = zsKeys[k].addr;
{ // spending-key availability is resolved once per call, in ZsTxJSONContext::keys()
auto pt = libzcash::SaplingNotePlaintext::decrypt(
wtx.vShieldedOutput[op.n].encCiphertext,extsk.expsk.full_viewing_key().in_viewing_key(),wtx.vShieldedOutput[op.n].ephemeralKey,wtx.vShieldedOutput[op.n].cm);
wtx.vShieldedOutput[op.n].encCiphertext,zsKeys[k].ivk,wtx.vShieldedOutput[op.n].ephemeralKey,wtx.vShieldedOutput[op.n].cm);
if (txType == 0 && pwalletMain->IsLockedNote(op))
txType == 3;
@@ -3228,10 +3252,14 @@ UniValue getalldata(const UniValue& params, bool fHelp,const CPubKey&)
}
//Ordered list of pointers into mapWallet (cs_wallet is held for the whole call)
std::vector<OrderedWtx> orderedTxs;
BuildOrderedWalletTxs(orderedTxs);
uint64_t t = GetTime();
for (map<int64_t,CWalletTx>::reverse_iterator it = orderedTxs.rbegin(); it != orderedTxs.rend(); ++it)
for (std::vector<OrderedWtx>::reverse_iterator it = orderedTxs.rbegin(); it != orderedTxs.rend(); ++it)
{
const CWalletTx& wtx = (*it).second;
const CWalletTx& wtx = *(it->second);
if (!CheckFinalTx(wtx))
continue;
@@ -3248,7 +3276,7 @@ UniValue getalldata(const UniValue& params, bool fHelp,const CPubKey&)
continue;
}
zsWalletTxJSON(wtx, trans, "*", false, 0);
zsWalletTxJSON(wtx, trans, "*", false, 0, &zsCtx);
if (trans.size() >= nCount) break;
}
@@ -3514,18 +3542,16 @@ UniValue z_listreceivedaddress(const UniValue& params, bool fHelp,const CPubKey&
if (nFilter < 0)
throw runtime_error("Filter must be greater that 0.");
//Created Ordered Transaction Map
map<int64_t,CWalletTx> orderedTxs;
for (map<uint256, CWalletTx>::iterator it = pwalletMain->mapWallet.begin(); it != pwalletMain->mapWallet.end(); ++it) {
const CWalletTx& wtx = (*it).second;
orderedTxs.insert(std::pair<int64_t,CWalletTx>(wtx.nOrderPos, wtx));
}
//Ordered list of pointers into mapWallet (cs_wallet is held for the whole call)
std::vector<OrderedWtx> orderedTxs;
BuildOrderedWalletTxs(orderedTxs);
ZsTxJSONContext zsCtx; // per-call key/nullifier memo (rpchushwallet.h)
uint64_t t = GetTime();
//Reverse Iterate thru transactions
for (map<int64_t,CWalletTx>::reverse_iterator it = orderedTxs.rbegin(); it != orderedTxs.rend(); ++it) {
const CWalletTx& wtx = (*it).second;
for (std::vector<OrderedWtx>::reverse_iterator it = orderedTxs.rbegin(); it != orderedTxs.rend(); ++it) {
const CWalletTx& wtx = *(it->second);
bool includeTransaction = true;
//Excude transactions with less confirmations than required
@@ -3546,7 +3572,7 @@ UniValue z_listreceivedaddress(const UniValue& params, bool fHelp,const CPubKey&
}
if (includeTransaction) {
zsWalletTxJSON(wtx, ret, params[0].get_str() , true, 2);
zsWalletTxJSON(wtx, ret, params[0].get_str() , true, 2, &zsCtx);
}
if (ret.size() > nCount) break;
@@ -3597,7 +3623,7 @@ UniValue z_listsentbyaddress(const UniValue& params, bool fHelp,const CPubKey&)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 5 || params.size() == 3)
if (fHelp || params.size() > 6 || params.size() == 3)
throw runtime_error(
"z_listsentbyaddress\n"
"\nReturns decrypted DragonX outputs sent to a single address.\n"
@@ -3622,11 +3648,16 @@ UniValue z_listsentbyaddress(const UniValue& params, bool fHelp,const CPubKey&)
"5. \"Count:\" (numeric, optional, default=9999999) \n"
" Last n number of transactions returned\n"
"\n"
"6. \"Sends only:\" (boolean, optional, default=false) \n"
" true: omit the \"spends\" and \"received\" keys of every entry and skip the\n"
" trial decryption they need. false: the full historic output.\n"
"\n"
"Default Parameters:\n"
"2. 0 - O confimations required\n"
"3. 0 - Returns all transactions\n"
"4. 9999999 - Ignored\n"
"5. 9999999 - Return the last 9,999,999 transactions.\n"
"6. false - Include the spends and received sections.\n"
"\n"
"\nResult:\n"
" \"txid\": \"transactionid\", (string) The transaction id.\n"
@@ -3675,6 +3706,7 @@ UniValue z_listsentbyaddress(const UniValue& params, bool fHelp,const CPubKey&)
int64_t nFilterType = 0;
int64_t nFilter = 9999999;
int64_t nCount = 9999999;
bool fSendsOnly = false; // opt-in, see help: omits the "spends"/"received" keys
if (params.size() >= 2)
nMinConfirms = params[1].get_int64();
@@ -3684,10 +3716,14 @@ UniValue z_listsentbyaddress(const UniValue& params, bool fHelp,const CPubKey&)
nFilter = params[3].get_int64();
}
if (params.size() == 5) {
if (params.size() >= 5) {
nCount = params[4].get_int64();
}
if (params.size() == 6) {
fSendsOnly = params[5].get_bool();
}
if (nMinConfirms < 0)
throw runtime_error("Minimum confimations must be greater that 0");
@@ -3697,18 +3733,16 @@ UniValue z_listsentbyaddress(const UniValue& params, bool fHelp,const CPubKey&)
if (nFilter < 0)
throw runtime_error("Filter must be greater that 0.");
//Created Ordered Transaction Map
map<int64_t,CWalletTx> orderedTxs;
for (map<uint256, CWalletTx>::iterator it = pwalletMain->mapWallet.begin(); it != pwalletMain->mapWallet.end(); ++it) {
const CWalletTx& wtx = (*it).second;
orderedTxs.insert(std::pair<int64_t,CWalletTx>(wtx.nOrderPos, wtx));
}
//Ordered list of pointers into mapWallet (cs_wallet is held for the whole call)
std::vector<OrderedWtx> orderedTxs;
BuildOrderedWalletTxs(orderedTxs);
ZsTxJSONContext zsCtx; // per-call key/nullifier memo (rpchushwallet.h)
uint64_t t = GetTime();
//Reverse Iterate thru transactions
for (map<int64_t,CWalletTx>::reverse_iterator it = orderedTxs.rbegin(); it != orderedTxs.rend(); ++it) {
const CWalletTx& wtx = (*it).second;
for (std::vector<OrderedWtx>::reverse_iterator it = orderedTxs.rbegin(); it != orderedTxs.rend(); ++it) {
const CWalletTx& wtx = *(it->second);
if (!CheckFinalTx(wtx))
continue;
@@ -3731,21 +3765,21 @@ UniValue z_listsentbyaddress(const UniValue& params, bool fHelp,const CPubKey&)
if (nFilterType == 2 && wtx.GetDepthInMainChain() > nFilter)
continue;
zsWalletTxJSON(wtx, ret, "*", false, 0);
zsWalletTxJSON(wtx, ret, "*", false, fSendsOnly ? 3 : 0, &zsCtx);
if (ret.size() >= nCount) break;
}
vector<UniValue> arrTmp = ret.getValues();
std::reverse(arrTmp.begin(), arrTmp.end()); // Return oldest to newest
ret.clear();
ret.setArray();
ret.push_backV(arrTmp);
return ret;
// Return oldest to newest (ret was filled newest first): one copy of each entry instead of
// getValues() + std::reverse + push_backV
UniValue oldestFirst(UniValue::VARR);
{
const std::vector<UniValue>& newestFirst = ret.getValues();
for (std::vector<UniValue>::const_reverse_iterator rit = newestFirst.rbegin(); rit != newestFirst.rend(); ++rit)
oldestFirst.push_back(*rit);
}
return oldestFirst;
}
UniValue z_getstats(const UniValue& params, bool fHelp, const CPubKey& mypk)