wallet2: reject duplicate outputs in reserve proofs
What changed, and why it matters
This change adds a safety check in Monero's wallet code when verifying a 'reserve proof'—a cryptographic receipt that proves someone owns enough funds. Before this fix, a maliciously crafted proof could include the same output or key image more than once. The patch now rejects such duplicate entries. This likely prevents a proof from being counted multiple times or from confusing the verification logic, which could otherwise make someone appear richer than they really are.
Treat as a low-to-moderate security hardening fix. Review whether reserve proof generation (get_reserve_proof) can emit duplicates and whether other proof-verification paths need equivalent deduplication. Backport to maintained release branches. No immediate emergency response is indicated, but a security advisory or release note mentioning the hardening would be appropriate.
Security signals we found
Input validation hardening: duplicate entries in parsed proof data are now rejected
Potential proof-of-funds inflation: duplicate outputs/key images could previously be counted multiple times or bypass uniqueness assumptions in downstream verification
No explicit CVE, advisory, or security disclosure referenced in commit metadata
Patch is partial/one-sided: it adds a guard but does not show whether other proof types or callers had similar issues
Evidence from the diff
The commit modifies wallet2::check_reserve_proof() in src/wallet/wallet2.cpp to deduplicate reserve proof entries before verification. It introduces std::unordered_set
Changed components
src/wallet/wallet2.cppwallet2::check_reserve_proof()Monero reserve proof verificationInspect captured patch +9 / −0
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index dc5847b..c7250f9 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -12662,6 +12662,15 @@ bool wallet2::check_reserve_proof(const cryptonote::account_public_address &addr
catch(...) {}
THROW_WALLET_EXCEPTION_IF(!loaded, error::wallet_internal_error, "Failed to parse reserve proof signature data");
+
+ std::unordered_set<crypto::key_image> seen_key_images;
+ std::set<std::pair<crypto::hash, uint64_t>> seen_outputs;
+ for (const reserve_proof_entry &proof : proofs)
+ {
+ THROW_WALLET_EXCEPTION_IF(!seen_key_images.insert(proof.key_image).second, error::wallet_internal_error, "Duplicate key image in reserve proof");
+ THROW_WALLET_EXCEPTION_IF(!seen_outputs.emplace(proof.txid, proof.index_in_tx).second, error::wallet_internal_error, "Duplicate output in reserve proof");
+ }
+
THROW_WALLET_EXCEPTION_IF(subaddr_spendkeys.count(address.m_spend_public_key) == 0, error::wallet_internal_error,
"The given address isn't found in the proof");
Why this scored 46/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.