wallet: don't copy every encrypted Sapling key on each lookup

CCryptoKeyStore::HaveSaplingSpendingKey and GetSaplingSpendingKey scan
mapCryptedSaplingSpendingKeys with `for (auto entry : ...)`. That copies
each entry (an extended full viewing key plus its encrypted-secret
vector) for every key visited, on every call. GetFilteredNotes'
requireSpendingKey check makes one such call per note, and the history
RPCs make one per output per address: ~22ns per key per call, measured
(11us per note on a 1024-key wallet). Iterate by const reference.

The equivalence test gains a locked, encrypted-keystore variant,
LockedWalletMatchesLegacy, so spendability coming from the encrypted
map is covered too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 08:16:09 -05:00
co-authored by Claude Opus 5.5
parent 94967efa7b
commit 34b94e761a
3 changed files with 25 additions and 3 deletions
+23 -1
View File
@@ -116,6 +116,11 @@ void LegacyFilteredNotes(
}
}
class NoteCacheWallet : public CWallet {
public:
bool EncryptKeys(CKeyingMaterial& vMasterKeyIn) { return CCryptoKeyStore::EncryptKeys(vMasterKeyIn); }
};
std::array<unsigned char, HUSH_MEMO_SIZE> MemoFor(uint64_t value)
{
std::array<unsigned char, HUSH_MEMO_SIZE> memo;
@@ -194,7 +199,7 @@ class WalletNoteCache : public ::testing::Test {
protected:
static constexpr int TIP_HEIGHT = 30;
CWallet wallet;
NoteCacheWallet wallet;
SaplingPaymentAddress paA, paB, paC; // A and C spendable, B viewing key only
std::map<uint256, int> depthOf; // txid -> depth to mine it at (absent: never mined)
std::vector<uint256> txids;
@@ -430,3 +435,20 @@ TEST_F(WalletNoteCacheUndecryptable, SpentAndLockedNotesAreNotDecrypted) {
for (const SaplingNoteEntry& e : entries)
EXPECT_FALSE(e.op.hash == txBad);
}
// The same wallet with its keystore encrypted and locked: spendability then comes from the
// encrypted-key map (CCryptoKeyStore::HaveSaplingSpendingKey), and nothing else may change.
class WalletNoteCacheEncrypted : public WalletNoteCache {
protected:
void AddMoreTxs() override {
CKeyingMaterial masterKey(32, 0x42);
ASSERT_TRUE(wallet.EncryptKeys(masterKey));
ASSERT_TRUE(wallet.IsCrypted());
ASSERT_TRUE(wallet.IsLocked());
}
};
TEST_F(WalletNoteCacheEncrypted, LockedWalletMatchesLegacy) {
EXPECT_EQ(6u, Filtered({}, 1, INT_MAX, true, true, false).size()) << "A's and C's keys are still known";
ExpectEquivalent({1, 11});
}