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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<bool>(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<OutputDescription> 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<bool>(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<SaplingNoteEntry>& 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<SaplingNoteEntry> 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<bool>(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<bool>(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<OutputDescription> 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<PaymentAddress> 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
+41
-16
@@ -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<SaplingNoteToDecryp
|
||||
for (unsigned int f : filled)
|
||||
nFilled += f;
|
||||
nSaplingNoteCacheFills += nFilled;
|
||||
nSaplingNoteCacheFillThreads = nPar;
|
||||
LogPrint("bench", "%s: decrypted %u of %u notes using %d thread(s)\n", __func__, nFilled, (unsigned int)notes.size(), nPar);
|
||||
return nFilled;
|
||||
}
|
||||
|
||||
// wallet.dat doesn't store the plaintext, so without this the first balance call after a restart decrypts
|
||||
// the whole wallet under cs_main (~0.5ms a note). init runs it before RPC leaves warmup and before the
|
||||
// wallet notification thread starts, so nothing waits on cs_wallet meanwhile. Unlike GetFilteredNotes it
|
||||
// never asserts, so a corrupt note can't stop the node from starting (or from serving the key-export RPCs
|
||||
// allowed during warmup); GetFilteredNotes still asserts on such a note, as before.
|
||||
void CWallet::WarmSaplingNoteCache()
|
||||
// wallet notification thread starts; only the few RPCs allowed during warmup (key export, address
|
||||
// listing) can wait on cs_wallet meanwhile. Unlike GetFilteredNotes it never asserts, so a corrupt note
|
||||
// can't stop the node from starting (or from serving those RPCs); GetFilteredNotes still asserts on such
|
||||
// a note, as before.
|
||||
CWallet::SaplingNoteCacheWarmup CWallet::WarmSaplingNoteCache()
|
||||
{
|
||||
const std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||
LOCK(cs_wallet);
|
||||
SaplingNoteCacheWarmup result;
|
||||
std::vector<SaplingNoteToDecrypt> 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::milliseconds>(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<bool>(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);
|
||||
|
||||
+14
-4
@@ -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<SaplingNoteToDecrypt>& 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<SaplingNoteEntry>& saplingEntries,
|
||||
std::string address,
|
||||
|
||||
Reference in New Issue
Block a user