I think that this check runs at the start of an input, but checks the state left by earlier inputs. So when one input leaks, the crash is reported on the next input, which then does not reproduce on its own.
To test this, I made one input (A) leak 100 change keys, and ran it before a normal input (B):
A then B -> crash reported on B
B alone -> no crash
A alone -> no crash (with -detect_leaks=0)
Maybe we could check at the end of each input instead , so the crash lands on the right input..?..That would also seem to allow for a much tighter bound: on my corpus side with 4640 inputs, no input moved the range by more than 2 (0: 4490, 1: 147, 2: 4).
<details>
<summary><strong>A suggestion of a potential fix diff </strong></summary>
--- a/src/test/fuzz/util/wallet.h
+++ b/src/test/fuzz/util/wallet.h
@@ void Reset()
assert(wallet->GetTXOs().empty());
- // Deriving a change address advances one descriptor's next_index and
- // extends its range. A single CreateTransaction() can do this more than
- // once: the avoid-partial-spends retry reserves a second change key on
- // the same descriptor when the first attempt was changeless. Rather
- // than track the exact count, bound the total drift loosely at
- // m_iterations * m_spkms.size() as a tripwire: growing faster than that
- // means an iteration is leaving behind state this class does not know
- // about.
- const int64_t drift{RangeTotal() - m_baseline_range_total};
- assert(drift >= 0);
- assert(static_cast<uint64_t>(drift) <= m_iterations * m_spkms.size());
}
@@
CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) {
return GetScriptForDestination(GetDestination(fuzzed_data_provider));
}
+ int64_t RangeTotal() const
+ {
+ int64_t total{0};
+ for (const DescriptorScriptPubKeyMan* spkm : m_spkms) total += spkm->GetEndRange();
+ return total;
+ }
+
private:
@@
std::vector<DescriptorScriptPubKeyMan*> m_spkms;
- int64_t m_baseline_range_total{0};
uint64_t m_iterations{0};
@@ void Build()
PrecomputeDestinations();
- m_baseline_range_total = RangeTotal();
m_iterations = 0;
}
@@
-
- int64_t RangeTotal() const
- {
- int64_t total{0};
- for (const DescriptorScriptPubKeyMan* spkm : m_spkms) total += spkm->GetEndRange();
- return total;
- }
-
};
--- a/src/wallet/test/fuzz/spend.cpp
+++ b/src/wallet/test/fuzz/spend.cpp
@@
FUZZ_TARGET(wallet_create_transaction, .init = initialize_setup)
FuzzedWallet& fuzzed_wallet{*g_wallet};
fuzzed_wallet.Reset();
+ // Checked when this input ends, so a failure is reported on the input that caused it.
+ // At most 2: one change key, plus one more if the avoid-partial-spends retry runs.
+ struct DriftCheck {
+ FuzzedWallet& w;
+ int64_t before;
+ ~DriftCheck() { assert(w.RangeTotal() - before <= 2); }
+ } drift_check{fuzzed_wallet, fuzzed_wallet.RangeTotal()};
</details>