From 190023f85fe7cc05a48139c6920d4b9d1ad97397 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 15 Oct 2026 22:56:23 -0500 Subject: [PATCH] wallet: note-cache review follow-ups - UpdatedNoteData now keeps the incoming plaintext when it keeps the wallet's own entries. When it returns "unchanged" (equal note data, or SaplingNoteDataIdentical), any entry lacking a plaintext takes the incoming one, for the same outpoint and ivk only. The startup rescan replays loaded txs with FindMySaplingNotes' freshly decrypted data; that plaintext was thrown away, so the warm-up decrypted those notes a second time. The cache is outside every comparison, so the function still returns false and nothing is rewritten. - -checknotecache is read once at init into fCheckNoteCache, instead of a mapArgs lookup on every GetFilteredNotes call. Its help text and comment now say what it checks: every note the walk considers, before the address filter, so a corrupt cached address can't drop a note unchecked. - GetFilteredNotes pre-sizes its result only when there's no address filter, so a one-address call no longer reserves room for the whole wallet. - WarmSaplingNoteCache returns its counts (notes, filled, no such output, undecryptable), and FillSaplingNoteCache records the threads it used, so tests can tell the cases apart. - WarmSaplingNoteCache's comment no longer claims nothing can wait on cs_wallet while it runs: the RPCs allowed during warmup can. Tests: - ReplayWithPlaintextIsNoUpdate pins the rule that the plaintext cache stays out of the "did this tx change?" decision. A rescan replays a witnessed note loaded without plaintext, using FindMySaplingNotes' data that carries one. That must be no update: no rewrite, and the witness and nullifier are kept. The stored entry then gains the plaintext. Checked by mutation: with cache presence added to SaplingNoteDataIdentical, this test fails and every other test still passes. - ParallelFillMatchesSerial uses 460 notes and asserts the thread count. So the 7-thread case really runs 7 threads, with uneven shards (460 % 7 != 0). Before, it was capped at 4 and repeated the 4-thread case. - ColdParallelFillMatchesLegacy: a cold GetFilteredNotes over ~270 candidates fills on 4 threads and matches the 69bb035e7 loop entry for entry. - WarmUpSkipsBadNotes asserts the warm-up's separate counts (1 no such output, 2 undecryptable), so removing the out-of-range guard is now caught. - CachedPlaintextMatchesFreshDecrypt checks exact counts. CheckNoteCacheRepairsADifference also tampers with a memo, and looks entries up by outpoint. The meaningless GetHash check is dropped. - A ScopedArg / ScopedCheckNoteCache RAII helper restores -args and fCheckNoteCache even when an assertion returns early. Co-Authored-By: Claude Opus 5.5 --- src/gtest/test_wallet_notecache.cpp | 165 ++++++++++++++++++++++++---- src/init.cpp | 3 +- src/wallet/wallet.cpp | 57 +++++++--- src/wallet/wallet.h | 18 ++- 4 files changed, 200 insertions(+), 43 deletions(-) diff --git a/src/gtest/test_wallet_notecache.cpp b/src/gtest/test_wallet_notecache.cpp index 1c7cc944a..62638c35f 100644 --- a/src/gtest/test_wallet_notecache.cpp +++ b/src/gtest/test_wallet_notecache.cpp @@ -153,6 +153,29 @@ void ExpectCacheMatchesFreshDecrypt(const SaplingNoteData& nd, const OutputDescr EXPECT_TRUE(nd.cachedPlaintext->memo == fresh->first.memo()); } +// Sets a -arg for the scope and restores its previous value (or absence), even when an assertion returns early. +class ScopedArg { + std::string name; + bool had; + std::string old; +public: + ScopedArg(const std::string& nameIn, const std::string& value) : name(nameIn), had(mapArgs.count(nameIn) > 0) { + if (had) old = mapArgs[name]; + mapArgs[name] = value; + } + ~ScopedArg() { + if (had) mapArgs[name] = old; + else mapArgs.erase(name); + } +}; + +class ScopedCheckNoteCache { + bool old; +public: + ScopedCheckNoteCache() : old(fCheckNoteCache) { fCheckNoteCache = true; } + ~ScopedCheckNoteCache() { fCheckNoteCache = old; } +}; + // The fake active chain is 0..TIP_HEIGHT; STALE_HEIGHT is a side block forked off it. constexpr int TIP_HEIGHT = 30; constexpr int STALE_HEIGHT = 25; @@ -653,6 +676,7 @@ TEST_F(WalletNoteCache, CachedPlaintextMatchesFreshDecrypt) { LOCK2(cs_main, wallet.cs_wallet); for (int pass = 0; pass < 2; pass++) { if (pass == 1) { + // The default filters skip spent and locked notes, so ask for everything. wallet.ClearPlaintextCache(); Filtered({}, 0, INT_MAX, false, false, false); // lazy fill of every confirmed note } @@ -665,7 +689,8 @@ TEST_F(WalletNoteCache, CachedPlaintextMatchesFreshDecrypt) { checked++; } } - EXPECT_GE(checked, 9u) << "pass " << pass; + // Eagerly: all 11 notes. Lazily: the 9 confirmed ones (not the never-mined or stale-block note). + EXPECT_EQ(pass ? 9u : 11u, checked) << "pass " << pass; } } @@ -714,7 +739,6 @@ TEST_F(WalletNoteCache, CacheNeverSerialized) { d << empty.mapSaplingNoteData.begin()->second; EXPECT_TRUE(c.str() == d.str()) << "SaplingNoteData, type " << type; } - EXPECT_EQ(txid, filled.GetHash()); CDataStream ss(SER_DISK, CLIENT_VERSION); ss << filled; @@ -725,15 +749,15 @@ TEST_F(WalletNoteCache, CacheNeverSerialized) { EXPECT_FALSE(static_cast(item.second.cachedPlaintext)); } -// UpdatedNoteData: a difference in the cache alone is no update (no rewrite to disk); the replace branch -// keeps a plaintext the wallet already had; and nothing is carried across a different ivk. +// UpdatedNoteData: a replay arriving without plaintext keeps the wallet's cache; the replace branch keeps a +// plaintext the wallet already had; and nothing is carried across a different ivk. TEST_F(WalletNoteCache, UpdatedNoteDataAndTheCache) { LOCK2(cs_main, wallet.cs_wallet); const uint256 txid = txids[2]; CWalletTx& wtx = wallet.mapWallet.at(txid); const SaplingOutPoint op0(txid, 0), op1(txid, 1); - // Same note data, but arriving without plaintext: unchanged, and the wallet keeps its cache. + // Same note data arriving without plaintext: no update, and the wallet keeps its cache. CWalletTx replay = wtx; for (auto& item : replay.mapSaplingNoteData) item.second.cachedPlaintext = boost::none; @@ -800,7 +824,10 @@ TEST_F(WalletNoteCache, WarmUpFillsEveryNote) { TEST_F(WalletNoteCacheUndecryptable, WarmUpSkipsBadNotes) { LOCK2(cs_main, wallet.cs_wallet); wallet.ClearPlaintextCache(); - wallet.WarmSaplingNoteCache(); + auto warmup = wallet.WarmSaplingNoteCache(); + EXPECT_EQ(1u, warmup.noSuchOutput); + EXPECT_EQ(2u, warmup.undecryptable); + EXPECT_EQ(warmup.notes - 3, warmup.filled); const auto& bad = wallet.mapWallet.at(txBad).mapSaplingNoteData; ASSERT_EQ(3u, bad.size()); for (const auto& item : bad) @@ -825,7 +852,7 @@ TEST(WalletNoteCacheParallel, ParallelFillMatchesSerial) { ASSERT_TRUE(wallet.AddSaplingZKey(skA, addrsA[0])); ASSERT_TRUE(wallet.AddSaplingIncomingViewingKey(skB.expsk.full_viewing_key().in_viewing_key(), skB.DefaultAddress())); uint64_t value = 0; - for (int t = 0; t < 30; t++) { + for (int t = 0; t < 46; t++) { std::vector outs; for (int i = 0; i < 10; i++) outs.push_back(MakeOutput(i % 3 == 2 ? skB.DefaultAddress() : addrsA[i % addrsA.size()], ++value)); @@ -846,14 +873,16 @@ TEST(WalletNoteCacheParallel, ParallelFillMatchesSerial) { return out; }; auto eager = snapshot(); - ASSERT_EQ(300u, eager.size()); + ASSERT_EQ(460u, eager.size()); - for (const char* threads : {"1", "4", "7"}) { - mapArgs["-witnessbuildthreads"] = threads; + // 460 notes allow up to 7 threads (at least 64 notes each); 460 % 7 != 0, so the shards are uneven. + for (int threads : {1, 4, 7}) { + ScopedArg arg("-witnessbuildthreads", std::to_string(threads)); wallet.ClearPlaintextCache(); const uint64_t base = wallet.nSaplingNoteCacheFills; wallet.WarmSaplingNoteCache(); - EXPECT_EQ(base + 300, wallet.nSaplingNoteCacheFills) << threads << " thread(s)"; + EXPECT_EQ(threads, wallet.nSaplingNoteCacheFillThreads); + EXPECT_EQ(base + 460, wallet.nSaplingNoteCacheFills) << threads << " thread(s)"; auto filled = snapshot(); ASSERT_EQ(eager.size(), filled.size()) << threads << " thread(s)"; for (size_t i = 0; i < eager.size(); i++) { @@ -864,26 +893,118 @@ TEST(WalletNoteCacheParallel, ParallelFillMatchesSerial) { EXPECT_TRUE(eager[i].second.memo == filled[i].second.memo); } } - mapArgs.erase("-witnessbuildthreads"); } -// The cache is trusted as is; -checknotecache decrypts every hit again, and logs and repairs a difference. +// The cache is trusted as is; -checknotecache decrypts each note a walk considers again, and logs and +// repairs a difference. TEST_F(WalletNoteCache, CheckNoteCacheRepairsADifference) { LOCK2(cs_main, wallet.cs_wallet); const SaplingOutPoint op(txids[3], 0); // tx4: one note to C, value 7000 + const SaplingOutPoint memoOp(txids[2], 1); // tx3 output 1: to C, value 5000 SaplingNoteData& nd = wallet.mapWallet.at(op.hash).mapSaplingNoteData.at(op); - ASSERT_TRUE(static_cast(nd.cachedPlaintext)); + SaplingNoteData& memoNd = wallet.mapWallet.at(memoOp.hash).mapSaplingNoteData.at(memoOp); + ASSERT_TRUE(nd.cachedPlaintext && memoNd.cachedPlaintext); const SaplingNote good = nd.cachedPlaintext->note; nd.cachedPlaintext->note = SaplingNote(good.d, good.pk_d, good.value() + 1, good.r); + memoNd.cachedPlaintext->memo[0] ^= 0xff; - auto trusted = Filtered({paC}, 21, INT_MAX, true, false, false); - ASSERT_EQ(1u, trusted.size()); - EXPECT_EQ(7001u, trusted[0].note.value()) << "without the check, the cache is what gets returned"; + // Entries come back in wallet (txid) order, so find them by outpoint. + auto entry = [](const std::vector& entries, const SaplingOutPoint& want) { + for (const SaplingNoteEntry& e : entries) + if (e.op == want) + return e; + ADD_FAILURE() << "no entry for " << want.hash.ToString() << ":" << want.n; + return SaplingNoteEntry(); + }; + auto trusted = Filtered({paC}, 1, INT_MAX, true, false, false); + ASSERT_EQ(3u, trusted.size()); + EXPECT_FALSE(entry(trusted, memoOp).memo == MemoFor(5000)) << "without the check, the cache is what gets returned"; + EXPECT_EQ(7001u, entry(trusted, op).note.value()); - mapArgs["-checknotecache"] = "1"; - auto checked = Filtered({paC}, 21, INT_MAX, true, false, false); - mapArgs.erase("-checknotecache"); - ASSERT_EQ(1u, checked.size()); - EXPECT_EQ(7000u, checked[0].note.value()); + std::vector checked; + { + ScopedCheckNoteCache on; + checked = Filtered({paC}, 1, INT_MAX, true, false, false); + } + ASSERT_EQ(3u, checked.size()); + EXPECT_TRUE(entry(checked, memoOp).memo == MemoFor(5000)); + EXPECT_EQ(7000u, entry(checked, op).note.value()); EXPECT_EQ(7000u, nd.cachedPlaintext->note.value()) << "repaired"; + EXPECT_TRUE(memoNd.cachedPlaintext->memo == MemoFor(5000)) << "repaired"; +} + +// The cache must stay out of the "did this tx change?" decision. A rescan replays a witnessed note that was +// loaded from wallet.dat (so it has no plaintext) with FindMySaplingNotes' fresh, plaintext-carrying data: +// that is no update (no rewrite to disk; counting the cache as a difference brings back the rewrite storm +// 69bb035e7 removed), the stored witness and nullifier stay, and the stored entry gains the plaintext. +TEST_F(WalletNoteCache, ReplayWithPlaintextIsNoUpdate) { + LOCK2(cs_main, wallet.cs_wallet); + const uint256 txid = txids[2]; // tx3: three notes + CWalletTx& stored = wallet.mapWallet.at(txid); + const SaplingOutPoint op0(txid, 0); + SaplingMerkleTree tree; + tree.append(stored.vShieldedOutput[0].cm); + SaplingNoteData& nd0 = stored.mapSaplingNoteData.at(op0); + nd0.witnesses.push_front(tree.witness()); + nd0.witnessHeight = 20; + nd0.nullifier = uint256S("abcd1234"); + wallet.ClearPlaintextCache(); + + CWalletTx replay(&wallet, stored); + mapSaplingNoteData_t fresh = wallet.FindMySaplingNotes(stored).first; + replay.SetSaplingNoteData(fresh); + for (const auto& item : replay.mapSaplingNoteData) + ASSERT_TRUE(static_cast(item.second.cachedPlaintext)); + ASSERT_FALSE(replay.mapSaplingNoteData == stored.mapSaplingNoteData) << "op0 differs in nullifier/height"; + + EXPECT_FALSE(wallet.UpdatedNoteData(replay, stored)); + const SaplingNoteData& after = stored.mapSaplingNoteData.at(op0); // re-read: a replace would rebuild the map + EXPECT_EQ(1u, after.witnesses.size()); + EXPECT_EQ(20, after.witnessHeight); + ASSERT_TRUE(static_cast(after.nullifier)) << "a replace dropped the witnessed note's nullifier"; + EXPECT_EQ(uint256S("abcd1234"), *after.nullifier); + for (const auto& item : stored.mapSaplingNoteData) + ExpectCacheMatchesFreshDecrypt(item.second, stored.vShieldedOutput[item.first.n]); + + // The note data equal outright (operator== ignores the cache): no update, plaintext kept as well. + wallet.ClearPlaintextCache(); + CWalletTx same(&wallet, stored); + mapSaplingNoteData_t sameData = stored.mapSaplingNoteData; + for (auto& item : sameData) + item.second.cachedPlaintext = fresh.at(item.first).cachedPlaintext; + same.SetSaplingNoteData(sameData); + ASSERT_TRUE(same.mapSaplingNoteData == stored.mapSaplingNoteData); + EXPECT_FALSE(wallet.UpdatedNoteData(same, stored)); + EXPECT_EQ(3u, wallet.CachedNotes()) << "only tx3's entries were refilled"; +} + +// A cold walk big enough for the parallel fill returns exactly what the 69bb035e7 loop returns. +class WalletNoteCacheMany : public WalletNoteCache { +protected: + void AddMoreTxs() override { + uint64_t value = 20000; + for (int t = 0; t < 26; t++) { + std::vector outs; + for (int i = 0; i < 10; i++) + outs.push_back(MakeOutput(i % 2 ? paA : paC, ++value)); + AddTx(MakeTx(outs), 2 + t % 9); + } + } +}; + +TEST_F(WalletNoteCacheMany, ColdParallelFillMatchesLegacy) { + ScopedArg threads("-witnessbuildthreads", "4"); + const std::set none, onlyC = {paC}; + for (int minDepth : {0, 2}) { + for (bool requireSpendingKey : {false, true}) { + { + LOCK(wallet.cs_wallet); + wallet.ClearPlaintextCache(); + } + auto got = Filtered(minDepth ? onlyC : none, minDepth, INT_MAX, true, requireSpendingKey, true); + EXPECT_EQ(4, wallet.nSaplingNoteCacheFillThreads) << "the cold fill ran on 4 threads"; + ExpectSameEntries(Legacy(minDepth ? onlyC : none, minDepth, INT_MAX, true, requireSpendingKey, true), got, + Describe(minDepth, INT_MAX, true, requireSpendingKey, true, minDepth ? "C" : "none")); + } + } } diff --git a/src/init.cpp b/src/init.cpp index ea04c9025..7e296bebd 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -516,7 +516,7 @@ std::string HelpMessage(HelpMessageMode mode) if (showDebug) strUsage += HelpMessageOpt("-notecachewarmup", "Decrypt all wallet notes into memory at startup, so the first balance call after a restart doesn't (default: 1)"); if (showDebug) - strUsage += HelpMessageOpt("-checknotecache", "Decrypt every cached wallet note again on each balance call and log any difference from the cache (slow; default: 0)"); + strUsage += HelpMessageOpt("-checknotecache", "On every balance or note-listing call, decrypt each cached note it considers again and log and repair any difference from the cache (slow; default: 0)"); strUsage += HelpMessageOpt("-rescan", _("Rescan the block chain for missing wallet transactions") + " " + _("on startup")); strUsage += HelpMessageOpt("-rescanheight", _("Rescan from specified height when rescan=1 on startup")); strUsage += HelpMessageOpt("-salvagewallet", _("Attempt to recover private keys from a corrupt wallet.dat") + " " + _("on startup")); @@ -2713,6 +2713,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // Decrypt the wallet's notes into the in-memory plaintext cache now, while RPC is still in warmup // and before the wallet notification thread (started below) can want cs_wallet. Otherwise the // first balance call after a restart decrypts them all under cs_main. + fCheckNoteCache = GetBoolArg("-checknotecache", false); if (GetBoolArg("-notecachewarmup", true) && !ShutdownRequested()) { uiInterface.InitMessage(_("Decrypting wallet notes...")); pwalletMain->WarmSaplingNoteCache(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 7497f419c..f3f098589 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -78,6 +78,7 @@ CBlockIndex *hush_chainactive(int32_t height); extern int32_t HUSH_LOADINGBLOCKS; int32_t hush_dpowconfs(int32_t height,int32_t numconfs); bool fTxDeleteEnabled = false; +bool fCheckNoteCache = false; bool fTxConflictDeleteEnabled = false; int fDeleteInterval = DEFAULT_TX_DELETE_INTERVAL; unsigned int fDeleteTransactionsAfterNBlocks = DEFAULT_TX_RETENTION_BLOCKS; @@ -2034,6 +2035,21 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletD // True if both maps hold the same notes with identical persisted state (every field SaplingNoteData // serializes). SaplingNoteData::operator== ignores the witnesses, so it cannot tell a real change // from a redundant update. +// Copies the decrypted plaintext of each incoming note into the matching (same outpoint and ivk) entry that +// lacks one. UpdatedNoteData uses it when it keeps the wallet's entries: a replay or rescan has just +// decrypted the notes (FindMySaplingNotes), and entries loaded from wallet.dat have no plaintext. The cache +// is in memory only and outside every comparison, so this never makes a tx look changed. +static void KeepNotePlaintexts(const mapSaplingNoteData_t& incoming, mapSaplingNoteData_t& wallet) +{ + for (const mapSaplingNoteData_t::value_type& in : incoming) { + if (!in.second.cachedPlaintext) + continue; + mapSaplingNoteData_t::iterator it = wallet.find(in.first); + if (it != wallet.end() && !it->second.cachedPlaintext && it->second.ivk == in.second.ivk) + it->second.cachedPlaintext = in.second.cachedPlaintext; + } +} + static bool SaplingNoteDataIdentical(const mapSaplingNoteData_t& a, const mapSaplingNoteData_t& b) { if (a.size() != b.size()) @@ -2053,8 +2069,10 @@ static bool SaplingNoteDataIdentical(const mapSaplingNoteData_t& a, const mapSap bool CWallet::UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx, bool fSameBlock) { bool unchangedSaplingFlag = (wtxIn.mapSaplingNoteData.empty() || wtxIn.mapSaplingNoteData == wtx.mapSaplingNoteData); - if (unchangedSaplingFlag) + if (unchangedSaplingFlag) { + KeepNotePlaintexts(wtxIn.mapSaplingNoteData, wtx.mapSaplingNoteData); return false; + } auto tmp = wtxIn.mapSaplingNoteData; // Ensure we keep any cached witnesses we may already have @@ -2093,6 +2111,7 @@ bool CWallet::UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx, bool fSame if (SaplingNoteDataIdentical(tmp, wtx.mapSaplingNoteData)) { for (mapSaplingNoteData_t::value_type& item : wtx.mapSaplingNoteData) item.second.witnessRootValidated = false; + KeepNotePlaintexts(tmp, wtx.mapSaplingNoteData); return false; } @@ -5782,49 +5801,55 @@ unsigned int CWallet::FillSaplingNoteCache(const std::vector uncached; - unsigned int nNotes = 0, nBad = 0; for (auto& p : mapWallet) { CWalletTx& wtx = p.second; for (auto& item : wtx.mapSaplingNoteData) { - nNotes++; + result.notes++; if (item.second.cachedPlaintext) continue; if (item.first.n >= wtx.vShieldedOutput.size()) { LogPrint("zrpcunsafe", "%s: note %s:%u has no such output\n", __func__, item.first.hash.ToString(), item.first.n); - nBad++; + result.noSuchOutput++; continue; } uncached.push_back(SaplingNoteToDecrypt { &item.second, &wtx.vShieldedOutput[item.first.n] }); } } - const unsigned int nFilled = uncached.empty() ? 0 : FillSaplingNoteCache(uncached); + result.filled = uncached.empty() ? 0 : FillSaplingNoteCache(uncached); for (const SaplingNoteToDecrypt& n : uncached) { if (!n.nd->cachedPlaintext) { LogPrint("zrpcunsafe", "%s: a note of output %s does not decrypt under its ivk\n", __func__, n.output->cm.ToString()); - nBad++; + result.undecryptable++; } } + const unsigned int nBad = result.noSuchOutput + result.undecryptable; const int64_t nMs = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); - LogPrintf("%s: decrypted %u of %u Sapling notes in %dms%s\n", __func__, nFilled, nNotes, nMs, + LogPrintf("%s: decrypted %u of %u Sapling notes in %dms%s\n", __func__, result.filled, result.notes, nMs, nBad ? strprintf("; %u could not be decrypted (-debug=zrpcunsafe lists them)", nBad) : ""); + return result; } -// -checknotecache: decrypts a cached note again, and logs and repairs any difference. A diagnostic for -// the cache; it costs the full decryption it exists to avoid. +// -checknotecache: decrypts a cached note again, and logs and repairs any difference. GetFilteredNotes runs +// it on every note that passes its tx, spent and locked filters, before the address filter, so a corrupt +// cached address can't wrongly drop a note unchecked. A diagnostic for the cache: it costs the full +// decryption the cache exists to avoid. static void CheckSaplingNoteCache(const SaplingOutPoint& op, SaplingNoteData& nd, const OutputDescription& output) { auto fresh = SaplingNotePlaintext::decrypt_with_note(output.encCiphertext, nd.ivk, output.ephemeralKey, output.cm); @@ -5966,12 +5991,12 @@ void CWallet::GetFilteredNotes( const unsigned int nFilled = uncached.empty() ? 0 : FillSaplingNoteCache(uncached); // Pass 2: the filters that need the note's address. - const bool fCheckCache = GetBoolArg("-checknotecache", false); - saplingEntries.reserve(saplingEntries.size() + candidates.size()); + if (filterAddresses.empty()) + saplingEntries.reserve(saplingEntries.size() + candidates.size()); for (const Candidate& c : candidates) { // A wallet note that doesn't decrypt under its own ivk is corrupt; as before, that's fatal here. assert(static_cast(c.nd->cachedPlaintext)); - if (fCheckCache) + if (fCheckNoteCache) CheckSaplingNoteCache(*c.op, *c.nd, *c.output); const SaplingNoteCache& plaintext = *c.nd->cachedPlaintext; const libzcash::SaplingPaymentAddress pa(plaintext.note.d, plaintext.note.pk_d); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 47e39bc75..96bd4b631 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -62,6 +62,8 @@ extern bool bSpendZeroConfChange; extern bool fSendFreeTransactions; extern bool fPayAtLeastCustomFee; extern bool fTxDeleteEnabled; +//! -checknotecache: re-decrypt cached Sapling notes in GetFilteredNotes and repair any difference +extern bool fCheckNoteCache; extern bool fTxConflictDeleteEnabled; extern int fDeleteInterval; extern unsigned int fDeleteTransactionsAfterNBlocks; @@ -1494,12 +1496,20 @@ public: /** Decrypts each note into nd->cachedPlaintext and returns how many it filled. A note that doesn't * decrypt under its ivk is left unfilled. Caller holds cs_wallet. */ unsigned int FillSaplingNoteCache(const std::vector& notes); - /** Notes FillSaplingNoteCache has decrypted since startup (FindMySaplingNotes' fills not counted). - * Guarded by cs_wallet. */ + /** Notes FillSaplingNoteCache has decrypted since startup (FindMySaplingNotes' fills not counted), and + * the threads its last call used. Guarded by cs_wallet. */ uint64_t nSaplingNoteCacheFills = 0; + int nSaplingNoteCacheFillThreads = 0; + + struct SaplingNoteCacheWarmup { + unsigned int notes = 0; //!< notes in the wallet + unsigned int filled = 0; //!< decrypted by this call + unsigned int noSuchOutput = 0; //!< naming an output their tx doesn't have + unsigned int undecryptable = 0; //!< not decrypting under their own ivk + }; /** Decrypts every wallet note whose plaintext isn't cached yet (all of them after wallet.dat is loaded). - * Never asserts: a note that won't decrypt is logged and left unfilled. Run at startup. */ - void WarmSaplingNoteCache(); + * Never asserts: a note that can't be decrypted is counted, logged and left unfilled. Run at startup. */ + SaplingNoteCacheWarmup WarmSaplingNoteCache(); void GetFilteredNotes(std::vector& saplingEntries, std::string address,