wallet: take note heights from the wallet's depth, not tx_height()

For minDepth > 1, GetFilteredNotes located each wallet tx's block with
tx_height(), which calls GetTransaction and re-reads the tx from disk
through the txindex. It did this once per wallet tx, transparent ones
included, while holding LOCK2. z_listunspent and z_listreceivedbyaddress
did it once more per returned note. Without -txindex, GetTransaction can
only find a tx that still has unspent transparent outputs. tx_height()
then returned 0 for every fully shielded tx, and the tx was skipped. So
on a -txindex=0 node, sweep and consolidation (minDepth 11) and any
minconf >= 2 call never saw those notes.

The wallet already has the depth, and for an active-chain tx
GetDepthInMainChain is chainActive.Height() - height + 1. The new
HeightFromDepth() inverts that (0 when unconfirmed or off the active
chain) and replaces tx_height() at all three sites. The height only
feeds the == 0 skip and hush_dpowconfs. Both sides hold cs_main for the
whole call, so the tip can't move between the walk and the RPC tail.

On a -txindex node the results are identical: the txindex block and
wtx.hashBlock are the same block. The extended equivalence test now
also covers minDepth 2/11/12/22. On -txindex=0 nodes, fully shielded
notes now show up for minDepth > 1. With -zsweep or -consolidation
enabled, those notes will now be swept and consolidated, so init logs
that once. MinDepthAboveOneUsesWalletHeight pins the new behaviour and
shows that the old path returned nothing without a txindex.

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 bfc1334565
commit 94967efa7b
5 changed files with 60 additions and 8 deletions
+34 -4
View File
@@ -20,6 +20,7 @@
#include "zcash/zip32.h"
int32_t hush_dpowconfs(int32_t height, int32_t numconfs);
int tx_height(const uint256 &hash);
using namespace libzcash;
@@ -191,7 +192,7 @@ void ExpectSameEntries(const std::vector<SaplingNoteEntry>& want, const std::vec
// TearDown, because every gtest suite runs in the same process.
class WalletNoteCache : public ::testing::Test {
protected:
static const int TIP_HEIGHT = 30;
static constexpr int TIP_HEIGHT = 30;
CWallet wallet;
SaplingPaymentAddress paA, paB, paC; // A and C spendable, B viewing key only
@@ -361,10 +362,39 @@ TEST_F(WalletNoteCache, FixtureExercisesEveryFilter) {
}
}
// The walk returns exactly what the 69bb035e7 loop returned, in the same order, for every filter
// combination.
// The walk returns exactly what the 69bb035e7 loop returned on a -txindex node, in the same order,
// for every filter combination. minDepth 11/12/22 sit on the fixture's depth-11 and depth-21 txs.
TEST_F(WalletNoteCache, FilteredNotesMatchLegacy) {
ExpectEquivalent({0, 1});
ExpectEquivalent({0, 1, 2, 11, 12, 22});
}
// minDepth > 1 (sweep and consolidation use 11) locates each tx's block from its depth. The old
// tx_height() re-read the tx through the txindex, and without one (as here, and on a -txindex=0
// node) it never found a fully shielded tx, so nothing was returned at all.
TEST_F(WalletNoteCache, MinDepthAboveOneUsesWalletHeight) {
// Unspent notes at depth >= 11: tx3's 3 (depth 11) and tx4's 1 (depth 21); tx6 (12) is spent.
EXPECT_EQ(4u, Filtered({}, 11, INT_MAX, true, false, false).size());
EXPECT_EQ(5u, Filtered({}, 11, INT_MAX, false, false, false).size());
EXPECT_EQ(1u, Filtered({}, 12, INT_MAX, true, false, false).size()) << "depth 11 is below minDepth 12";
EXPECT_EQ(3u, Filtered({}, 11, 11, true, false, false).size()) << "depth 21 is above maxDepth 11";
EXPECT_EQ(0u, Filtered({}, 22, INT_MAX, false, false, false).size());
for (const SaplingNoteEntry& e : Filtered({}, 2, INT_MAX, false, false, false))
EXPECT_GE(e.confirmations, 2) << "unmined and shallower txs stay out";
std::set<PaymentAddress> none;
std::vector<SaplingNoteEntry> legacy;
LegacyFilteredNotes(wallet, legacy, none, 11, INT_MAX, true, false, false, tx_height);
EXPECT_EQ(0u, legacy.size()) << "the old code found nothing here without a txindex";
}
TEST_F(WalletNoteCache, HeightFromDepth) {
LOCK(cs_main);
EXPECT_EQ(TIP_HEIGHT, HeightFromDepth(1));
EXPECT_EQ(TIP_HEIGHT - 10, HeightFromDepth(11));
EXPECT_EQ(0, HeightFromDepth(0)) << "mempool";
EXPECT_EQ(0, HeightFromDepth(-1)) << "not in the chain or mempool";
for (const auto& d : depthOf)
EXPECT_EQ(minedHeight.at(d.first), HeightFromDepth(wallet.mapWallet.at(d.first).GetDepthInMainChain()));
}
// Two notes recorded under A's viewing key that it can't actually decrypt (real outputs to someone