wallet2: validate scan_tx daemon responses
What changed, and why it matters
This commit adds a safety check in the Monero wallet software. When the wallet asks a remote node (daemon) for specific transaction data, it now verifies that the daemon actually returned the exact transactions that were requested, in the correct order. Before this change, a malicious or buggy daemon could potentially return the wrong transactions, which the wallet might then process as if they were the right ones. This could lead to incorrect balance calculations, failed transactions, or other wallet confusion.
Treat as a hardening fix with possible security relevance. Users running wallets against untrusted remote nodes should upgrade. Further review should assess whether other daemon response paths in wallet2 lack similar validation, and whether this issue was exploitable to induce false balance or transaction metadata.
Security signals we found
Daemon response validation added for transaction hash matching
Trust boundary crossing between wallet client and remote daemon
Potential wrong-transaction processing mitigated
No explicit security framing in commit message or diff
Evidence from the diff
In wallet2::get_tx_entries(), the wallet builds a request of transaction hashes (req.txs_hashes) sent to the daemon via scan_tx / get_transactions. The patch records an iterator to the beginning of the requested hash slice (expected_txid_begin), then after receiving res.txs, iterates the daemon’s responses and checks that each decoded transaction hash matches the corresponding expected txid. If not, it throws a wallet_internal_error. This closes a trust gap where the wallet assumed the daemon returned the requested transactions in the same order and count.
Changed components
src/wallet/wallet2.cppwallet2::get_tx_entries()daemon transaction fetch pathInspect captured patch +4 / −0
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 86eba93..a77b76d 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -1704,6 +1704,7 @@ wallet2::tx_entry_data wallet2::get_tx_entries(const std::unordered_set<crypto::
req.prune = true;
size_t ntxes = slice + SLICE_SIZE > txids.size() ? txids.size() - slice : SLICE_SIZE;
+ const auto expected_txid_begin = it;
for (size_t i = slice; i < slice + ntxes; ++i)
{
req.txs_hashes.push_back(epee::string_tools::pod_to_hex(*it));
@@ -1717,6 +1718,7 @@ wallet2::tx_entry_data wallet2::get_tx_entries(const std::unordered_set<crypto::
THROW_WALLET_EXCEPTION_IF(res.txs.size() != req.txs_hashes.size(), error::wallet_internal_error, "Failed to get transaction from daemon");
}
+ auto expected_txid = expected_txid_begin;
for (auto& tx_info : res.txs)
{
if (!tx_info.in_pool)
@@ -1728,6 +1730,8 @@ wallet2::tx_entry_data wallet2::get_tx_entries(const std::unordered_set<crypto::
cryptonote::transaction tx;
crypto::hash tx_hash;
THROW_WALLET_EXCEPTION_IF(!get_pruned_tx(tx_info, tx, tx_hash), error::wallet_internal_error, "Failed to get transaction from daemon");
+ THROW_WALLET_EXCEPTION_IF(tx_hash != *expected_txid, error::wallet_internal_error, "Failed to get the right transaction from daemon");
+ ++expected_txid;
tx_entries.tx_entries.emplace_back(process_tx_entry_t{ std::move(tx_info), std::move(tx), std::move(tx_hash) });
}
}
Why this scored 59/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.