wallet: -checknotecache, a diagnostic for the note plaintext cache

With -checknotecache=1 (debug help), GetFilteredNotes decrypts every
note it returns again. If the cached plaintext differs from the fresh
decryption, it logs the note and replaces the cache entry with the
fresh value. It costs the full decryption the cache exists to avoid, so
it is off by default; it is there for validating the cache on a real
wallet, or for diagnosing a suspected cache problem in the field.

CheckNoteCacheRepairsADifference: by default a tampered cache entry is
returned as is (the cache is trusted); with the option it is repaired
and the fresh value is returned.

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 43635b7fd7
commit 23877e1038
3 changed files with 50 additions and 3 deletions
+21
View File
@@ -866,3 +866,24 @@ TEST(WalletNoteCacheParallel, ParallelFillMatchesSerial) {
}
mapArgs.erase("-witnessbuildthreads");
}
// The cache is trusted as is; -checknotecache decrypts every hit 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
SaplingNoteData& nd = wallet.mapWallet.at(op.hash).mapSaplingNoteData.at(op);
ASSERT_TRUE(static_cast<bool>(nd.cachedPlaintext));
const SaplingNote good = nd.cachedPlaintext->note;
nd.cachedPlaintext->note = SaplingNote(good.d, good.pk_d, good.value() + 1, good.r);
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";
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());
EXPECT_EQ(7000u, nd.cachedPlaintext->note.value()) << "repaired";
}