gtest: notecache review fixes

- Take LOCK2(cs_main, cs_wallet) before adding keys in SetUp: both
  AddSaplingZKey and AddSaplingIncomingViewingKey assert cs_wallet is
  held, which aborts a DEBUG_LOCKORDER build.
- TIP_HEIGHT becomes a namespace-scope constexpr. As a static class
  member it was ODR-used by EXPECT_EQ without a definition, which fails
  to link as C++11 at -O0.
- TearDown restores the previous chainActive tip and erases only the
  mapBlockIndex entries the fixture inserted.
- Add a tx mined only in a stale side block. The legacy tx_height model
  returns that block's height, as a txindex can, and the tx must still
  be excluded at every minDepth.
- The encrypted variant asserts HaveSaplingSpendingKey directly, and a
  new test unlocks the keystore and checks GetSaplingSpendingKey (the
  other loop the crypter change touched).
- The legacy-loop comment now says its two LogPrintf lines are dropped.

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 21586b353a
commit bfec5c74ae
+68 -26
View File
@@ -28,9 +28,10 @@ namespace {
typedef std::function<int(const uint256&)> TxHeightFn; typedef std::function<int(const uint256&)> TxHeightFn;
// CWallet::GetFilteredNotes as of 69bb035e7, verbatim except that tx_height() is passed in. This is // CWallet::GetFilteredNotes as of 69bb035e7, verbatim except that tx_height() is passed in and its two
// the reference the optimized walk must reproduce entry for entry and in the same order (z_sendmany // LogPrintf lines are dropped. This is the reference the optimized walk must reproduce entry for
// sorts the result with an unstable sort, so the order is observable). // entry and in the same order (z_sendmany sorts the result with an unstable sort, so the order is
// observable).
void LegacyFilteredNotes( void LegacyFilteredNotes(
CWallet& wallet, CWallet& wallet,
std::vector<SaplingNoteEntry>& saplingEntries, std::vector<SaplingNoteEntry>& saplingEntries,
@@ -119,8 +120,13 @@ void LegacyFilteredNotes(
class NoteCacheWallet : public CWallet { class NoteCacheWallet : public CWallet {
public: public:
bool EncryptKeys(CKeyingMaterial& vMasterKeyIn) { return CCryptoKeyStore::EncryptKeys(vMasterKeyIn); } bool EncryptKeys(CKeyingMaterial& vMasterKeyIn) { return CCryptoKeyStore::EncryptKeys(vMasterKeyIn); }
bool Unlock(const CKeyingMaterial& vMasterKeyIn) { return CCryptoKeyStore::Unlock(vMasterKeyIn); }
}; };
// 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;
std::array<unsigned char, HUSH_MEMO_SIZE> MemoFor(uint64_t value) std::array<unsigned char, HUSH_MEMO_SIZE> MemoFor(uint64_t value)
{ {
std::array<unsigned char, HUSH_MEMO_SIZE> memo; std::array<unsigned char, HUSH_MEMO_SIZE> memo;
@@ -192,13 +198,11 @@ void ExpectSameEntries(const std::vector<SaplingNoteEntry>& want, const std::vec
} }
// A wallet with notes at several depths, spent and unspent, locked and unlocked, spendable and // A wallet with notes at several depths, spent and unspent, locked and unlocked, spendable and
// viewing-key-only, plus txs that carry no wallet notes; mined into a fake active chain. Everything it // viewing-key-only, one mined only in a stale block, plus txs that carry no wallet notes; mined into a
// touches in the process-wide chain state (chainActive, mapBlockIndex, pcoinsTip) is undone in // fake active chain. TearDown restores the process-wide chain state it changed (the chainActive tip,
// TearDown, because every gtest suite runs in the same process. // its own mapBlockIndex entries, pcoinsTip), because every gtest suite runs in the same process.
class WalletNoteCache : public ::testing::Test { class WalletNoteCache : public ::testing::Test {
protected: protected:
static constexpr int TIP_HEIGHT = 30;
NoteCacheWallet wallet; NoteCacheWallet wallet;
SaplingPaymentAddress paA, paB, paC; // A and C spendable, B viewing key only 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::map<uint256, int> depthOf; // txid -> depth to mine it at (absent: never mined)
@@ -210,13 +214,21 @@ protected:
CCoinsViewCache* savedCoinsTip = nullptr; CCoinsViewCache* savedCoinsTip = nullptr;
std::deque<CBlock> blocks; std::deque<CBlock> blocks;
std::deque<CBlockIndex> index; std::deque<CBlockIndex> index;
std::map<uint256, int> minedHeight; // txid -> height of the block holding it std::vector<uint256> insertedBlocks; // mapBlockIndex keys this fixture added
CBlockIndex* savedTip = nullptr;
std::map<uint256, int> minedHeight; // txid -> height of the block holding it (stale ones too)
uint256 staleTx; // only ever mined in the side block at STALE_HEIGHT
void SetUp() override { void SetUp() override {
// tx_height()/GetTransaction dereference pcoinsTip; an empty view answers "not found". // tx_height()/GetTransaction dereference pcoinsTip; an empty view answers "not found".
savedCoinsTip = pcoinsTip; savedCoinsTip = pcoinsTip;
pcoinsTip = &coins; pcoinsTip = &coins;
{
LOCK(cs_main);
savedTip = chainActive.Tip();
}
LOCK2(cs_main, wallet.cs_wallet);
auto skA = TestKey(0), skB = TestKey(1), skC = TestKey(2); auto skA = TestKey(0), skB = TestKey(1), skC = TestKey(2);
paA = skA.DefaultAddress(); paA = skA.DefaultAddress();
paB = skB.DefaultAddress(); paB = skB.DefaultAddress();
@@ -226,13 +238,13 @@ protected:
ASSERT_TRUE(wallet.AddSaplingZKey(skC, paC)); ASSERT_TRUE(wallet.AddSaplingZKey(skC, paC));
SaplingPaymentAddress foreign = TestKey(9).DefaultAddress(); // not in the wallet SaplingPaymentAddress foreign = TestKey(9).DefaultAddress(); // not in the wallet
LOCK2(cs_main, wallet.cs_wallet);
AddTx(MakeTx({MakeOutput(paA, 1000), MakeOutput(paB, 2000)}), 1); AddTx(MakeTx({MakeOutput(paA, 1000), MakeOutput(paB, 2000)}), 1);
uint256 tx2 = AddTx(MakeTx({MakeOutput(paA, 3000)}), 5); uint256 tx2 = AddTx(MakeTx({MakeOutput(paA, 3000)}), 5);
AddTx(MakeTx({MakeOutput(paA, 4000), MakeOutput(paC, 5000), MakeOutput(paB, 6000)}), 11); AddTx(MakeTx({MakeOutput(paA, 4000), MakeOutput(paC, 5000), MakeOutput(paB, 6000)}), 11);
AddTx(MakeTx({MakeOutput(paC, 7000)}), 21); AddTx(MakeTx({MakeOutput(paC, 7000)}), 21);
AddTx(MakeTx({MakeOutput(paA, 8000)}), -1); // never mined: depth -1 AddTx(MakeTx({MakeOutput(paA, 8000)}), -1); // never mined: depth -1
AddTx(MakeTx({MakeOutput(foreign, 50)}), 3); // no wallet notes at all AddTx(MakeTx({MakeOutput(foreign, 50)}), 3); // no wallet notes at all
staleTx = AddTx(MakeTx({MakeOutput(paA, 11000)}), -1); // mined in a stale block only
// A note spent by a mined tx, and one whose spending tx never confirmed. // A note spent by a mined tx, and one whose spending tx never confirmed.
const uint256 nfSpent = uint256S("5a1d"), nfPending = uint256S("9e9d"); const uint256 nfSpent = uint256S("5a1d"), nfPending = uint256S("9e9d");
@@ -255,11 +267,9 @@ protected:
void TearDown() override { void TearDown() override {
LOCK(cs_main); LOCK(cs_main);
chainActive.SetTip(nullptr); chainActive.SetTip(savedTip);
for (const CBlockIndex& bi : index) { for (const uint256& hash : insertedBlocks)
const uint256 hash = bi.GetBlockHash();
mapBlockIndex.erase(hash); mapBlockIndex.erase(hash);
}
pcoinsTip = savedCoinsTip; pcoinsTip = savedCoinsTip;
} }
@@ -275,7 +285,21 @@ protected:
return tx.GetHash(); return tx.GetHash();
} }
// Builds an active chain 0..TIP_HEIGHT and puts each wallet tx into the block at its depth. CBlockIndex& AddBlock(CBlock& block, int height, CBlockIndex* pprev) {
block.hashMerkleRoot = block.BuildMerkleTree();
index.emplace_back(block);
CBlockIndex& bi = index.back();
bi.SetHeight(height);
bi.pprev = pprev;
auto inserted = mapBlockIndex.insert(std::make_pair(block.GetHash(), &bi));
EXPECT_TRUE(inserted.second) << "block hash collision at height " << height;
insertedBlocks.push_back(block.GetHash());
bi.phashBlock = &inserted.first->first;
return bi;
}
// Builds an active chain 0..TIP_HEIGHT and puts each wallet tx into the block at its depth, plus a
// side block at STALE_HEIGHT (not on the active chain) holding staleTx.
void Mine() { void Mine() {
for (int h = 0; h <= TIP_HEIGHT; h++) { for (int h = 0; h <= TIP_HEIGHT; h++) {
blocks.emplace_back(); blocks.emplace_back();
@@ -288,24 +312,27 @@ protected:
if (TIP_HEIGHT - d.second + 1 == h) if (TIP_HEIGHT - d.second + 1 == h)
block.vtx.push_back(wallet.mapWallet.at(d.first)); block.vtx.push_back(wallet.mapWallet.at(d.first));
} }
block.hashMerkleRoot = block.BuildMerkleTree(); AddBlock(block, h, h > 0 ? &index[h - 1] : nullptr);
index.emplace_back(block);
CBlockIndex& bi = index.back();
bi.SetHeight(h);
bi.pprev = h > 0 ? &index[h - 1] : nullptr;
BlockMap::iterator it = mapBlockIndex.insert(std::make_pair(block.GetHash(), &bi)).first;
bi.phashBlock = &it->first;
} }
chainActive.SetTip(&index.back()); chainActive.SetTip(&index[TIP_HEIGHT]);
for (const auto& d : depthOf) { for (const auto& d : depthOf) {
int h = TIP_HEIGHT - d.second + 1; int h = TIP_HEIGHT - d.second + 1;
wallet.mapWallet.at(d.first).SetMerkleBranch(blocks[h]); wallet.mapWallet.at(d.first).SetMerkleBranch(blocks[h]);
minedHeight[d.first] = h; minedHeight[d.first] = h;
} }
blocks.emplace_back();
CBlock& side = blocks.back();
side.nTime = 1800000000;
side.hashPrevBlock = blocks[STALE_HEIGHT - 1].GetHash();
side.vtx.push_back(wallet.mapWallet.at(staleTx));
AddBlock(side, STALE_HEIGHT, &index[STALE_HEIGHT - 1]);
wallet.mapWallet.at(staleTx).SetMerkleBranch(side);
minedHeight[staleTx] = STALE_HEIGHT; // a txindex can still point at a stale block
} }
// What tx_height() returns on a -txindex node: the height of the block holding the tx, 0 if none. // What tx_height() returns on a -txindex node: the height of the block the txindex points at
// (possibly stale), 0 if none.
TxHeightFn TxIndexHeight() const { TxHeightFn TxIndexHeight() const {
return [this](const uint256& txid) { return [this](const uint256& txid) {
auto it = minedHeight.find(txid); auto it = minedHeight.find(txid);
@@ -393,7 +420,8 @@ TEST_F(WalletNoteCache, MinDepthAboveOneUsesWalletHeight) {
} }
TEST_F(WalletNoteCache, HeightFromDepth) { TEST_F(WalletNoteCache, HeightFromDepth) {
LOCK(cs_main); LOCK2(cs_main, wallet.cs_wallet);
EXPECT_EQ(-1, wallet.mapWallet.at(staleTx).GetDepthInMainChain()) << "a stale block is not the active chain";
EXPECT_EQ(TIP_HEIGHT, HeightFromDepth(1)); EXPECT_EQ(TIP_HEIGHT, HeightFromDepth(1));
EXPECT_EQ(TIP_HEIGHT - 10, HeightFromDepth(11)); EXPECT_EQ(TIP_HEIGHT - 10, HeightFromDepth(11));
EXPECT_EQ(0, HeightFromDepth(0)) << "mempool"; EXPECT_EQ(0, HeightFromDepth(0)) << "mempool";
@@ -449,6 +477,20 @@ protected:
}; };
TEST_F(WalletNoteCacheEncrypted, LockedWalletMatchesLegacy) { TEST_F(WalletNoteCacheEncrypted, LockedWalletMatchesLegacy) {
EXPECT_TRUE(wallet.HaveSaplingSpendingKey(TestKey(0).expsk.full_viewing_key()));
EXPECT_FALSE(wallet.HaveSaplingSpendingKey(TestKey(1).expsk.full_viewing_key())) << "B is viewing key only";
EXPECT_FALSE(wallet.HaveSaplingSpendingKey(TestKey(9).expsk.full_viewing_key()));
EXPECT_EQ(6u, Filtered({}, 1, INT_MAX, true, true, false).size()) << "A's and C's keys are still known"; EXPECT_EQ(6u, Filtered({}, 1, INT_MAX, true, true, false).size()) << "A's and C's keys are still known";
ExpectEquivalent({1, 11}); ExpectEquivalent({1, 11});
} }
TEST_F(WalletNoteCacheEncrypted, UnlockedWalletFindsSpendingKeys) {
libzcash::SaplingExtendedSpendingKey sk;
EXPECT_FALSE(wallet.GetSaplingSpendingKey(TestKey(0).expsk.full_viewing_key(), sk)) << "locked";
ASSERT_TRUE(wallet.Unlock(CKeyingMaterial(32, 0x42)));
ASSERT_TRUE(wallet.GetSaplingSpendingKey(TestKey(0).expsk.full_viewing_key(), sk));
EXPECT_TRUE(sk.DefaultAddress() == paA);
ASSERT_TRUE(wallet.GetSaplingSpendingKey(TestKey(2).expsk.full_viewing_key(), sk));
EXPECT_TRUE(sk.DefaultAddress() == paC);
EXPECT_FALSE(wallet.GetSaplingSpendingKey(TestKey(9).expsk.full_viewing_key(), sk));
}