Monero / Security / Wallets
Monero Developers Address Transaction Proof Verification Weaknesses
Two Monero wallet proposals check legacy transaction data supplied by remote nodes. What the proof-verification flaws affect, the fixes and release status.

When checking an old payment, a wallet can receive altered transaction data from a dishonest node. Two Monero proposals require affected proof operations to calculate the transaction ID from complete version-1 data before relying on it. The existing code can accept a matching ID supplied by the daemon without establishing that the accompanying data has that ID.
PR #11693 addresses an incorrect payment amount from check_tx_key. PR #11696 addresses a spend proof accepted against substituted transaction inputs. Both concern the handling of pruned version-1 transaction data. Pruned data omits part of the transaction to reduce the amount transferred.
At the November 8 review, both PRs remained open. Neither fix was in the latest stable release, v0.18.5.4. The reports describe misleading verification results from an untrusted data source. The cited disclosures establish neither fund theft nor exploitation against real users. The problem is in verification logic rather than the underlying cryptography.
What Monero transaction proof verification establishes
A transaction ID is a cryptographic hash, a fixed-length identifier calculated from transaction data. An ID alone does not show how much an address received. Wallets use transaction keys and signed proofs to check payment and spending claims.
Transaction keys and payment proofs
A sender's wallet creates transaction secret keys when making a payment. A transaction key helps a verifier identify outputs belonging to a supplied recipient address and determine the amount paid. The verifier needs the transaction ID, the relevant transaction key or keys, and the address.
The official guide to proving a payment describes this process using get_tx_key and check_tx_key. The latter is the operation covered by #11693. A transaction key is different from the wallet's private spend key. Sharing it can disclose payment information, but it does not let someone spend the recipient's outputs.
A signed transaction proof provides another method. The wallet's get_tx_proof and check_tx_proof operations let people generate and verify a payment-related proof without handing over the raw transaction secret key. An optional message can tie the proof to a dispute or verification request. These are separate functions from check_tx_key, although they share transaction-fetching and amount-checking code.
Spend proofs answer a different question
A spend proof concerns control of a transaction's inputs. Each input spends an earlier output and includes decoys in its ring. The wallet creates the proof with get_spend_proof and checks it with check_spend_proof. This checks the claimant's relationship to those inputs. Establishing an amount paid to a recipient requires a payment check. The wallet RPC documentation describes the generation operation.
Its signatures cover the requested transaction ID and an optional message. Verification also uses the transaction's inputs, their key images and the public keys in their rings. If the verifier substitutes the wrong inputs, a signature check can answer the wrong question even though the cryptographic calculation itself works.
The text SpendProofV1 names the spend-proof format. The blockchain transaction has its own version field. These separate version numbers appear in the verification code.
Full data and legacy transaction hashes
The wallet retrieves historical transaction data through the /gettransactions RPC endpoint. RPC means remote procedure call, the interface through which the wallet requests information from a node. These proof operations request a pruned response first.
Version-1 transactions calculate their ID from the complete serialized transaction, including its signature data. Omitting that data prevents the wallet from independently recalculating the ID in the same way. Version-2 transactions use a different construction that combines hashes of separate parts. The transaction hashing code documents that difference.
The wallet helper get_pruned_tx handles both formats. For a full transaction, it parses the data and computes the hash. For pruned version-2 data, it can calculate an ID using the remaining data and the supplied hash of the omitted part. For pruned version-1 data, it instead reads the daemon's claimed tx_hash field. That last branch is visible in the released wallet source.
The callers do compare the returned ID with the ID requested. The weakness is what that comparison means. In the legacy pruned branch, both identifiers can match while the daemon supplies altered data alongside its identifier. Matching labels are not a calculation of the data's hash.
This is a check at the wallet's RPC boundary. It does not change how a full node validates transactions for the blockchain.
PR #11693: an output key can match while its amount is wrong
check_tx_key uses the supplied transaction key and recipient address to derive expected output public keys. When an output matches, the wallet calculates how much that address received. Version-1 outputs record their amounts directly. They do not use RingCT to hide them.
A malicious daemon can preserve a real output's public key but alter its cleartext amount in the pruned response. The output still matches the address derivation. If the daemon also supplies the expected transaction ID, the old code proceeds to read the altered amount. The sender's known transaction key does not authenticate that separate amount field by itself.
The earlier regression-test revision makes the distinction concrete. Its mock daemon changes a synthetic output from 8 to 80 atomic units in the pruned reply while retaining the original ID. The test expects the corrected wallet to return 8 after fetching the complete transaction.
The test also supplies a wrong full transaction and expects rejection. A version-2 case checks that one request suffices. We inspected those assertions. We did not execute this Monero test locally.
The current #11693 patch replaces the separate fetch in check_tx_key_helper with get_tx_for_proof. The helper obtains full version-1 data when necessary and checks its calculated hash against the requested ID before the amount calculation uses it.
The returned pool status and confirmation count still depend on the daemon. A matching transaction hash establishes data identity, without independently proving its position in the current chain.
PR #11696: verify the inputs belong to the requested transaction
The spend-proof path has the same pruned version-1 hash problem, but it consumes inputs rather than just payment outputs. The old check_spend_proof compares the requested ID with the result of get_pruned_tx, then uses the returned inputs to determine which ring signatures to verify.
The earlier #11696 test constructs a synthetic version-1 transaction and a modified copy with a different input key image. The mock daemon supplies the modified pruned data under the original transaction ID. It also supplies a substituted public key through /get_outs.bin, the endpoint used to retrieve ring members.
The test creates a proof using the secret key associated with the substituted input, signing over the requested ID and message. That is not simply a replay of an unrelated proof. It tests whether the wallet checks signatures against attacker-supplied inputs without first proving that those inputs belong to the requested transaction.
With the correction, the test expects the substituted proof to fail. A valid version-1 proof must still succeed, while failure to get the full transaction must raise an error. The author reports that unpatched master accepted the forged proof and the corrected code rejected it. We inspected the historical test, but have not independently executed that reproduction. The report describes a local mocked-RPC experiment.
The current patch moves both spend-proof generation and verification to get_tx_for_proof. It preserves their RPC timeout and status-error handling. It also strengthens the shared helper to reject non-OK RPC responses, including responses containing transaction entries alongside an error status. That change also applies to get_tx_proof and check_tx_proof.
The full-transaction check comes from PR #11654
PR #11654 addressed a related failure in generating and checking payment proofs for old version-1 transactions. Those paths could reject a pruned transaction because they later needed its full hash. The change introduced get_tx_for_proof and merged into master on November 7.
The helper first requests pruned data. If the parsed transaction is version 1 and still pruned, it requests the full transaction. It parses that response, computes the ID from the full data and compares it with the requested ID. A mismatched full transaction fails. If the daemon again supplies only pruned version-1 data, the helper fails instead of retrying indefinitely. Version 2 keeps the initial request.

The hash comparison must precede the payment or spend-proof check.
The newer proposals extend this helper to other callers. Merging #11654 did not by itself fix check_tx_key or check_spend_proof, because those functions still used their own fetch paths. Nor does a full transaction hash check make every answer from a remote node trustworthy.
Review status and test evidence
The reviewed heads were 203add0 for #11693 and 3b24f26 for #11696. Their status matters separately from their authors' test reports:
- #11693: open, approved by jpk68 and selsta, and labelled
needs backport. Its current diff contains the wallet change but no added regression test. Selsta requested removal of that test, and the author removed it. - #11696: open, labelled
pending review, with no submitted formal review at the cutoff. Its current diff changeswallet2.cppandwallet2.h. The author removed the added regression test following a similar request. - #11654: merged into master as
2ae78ba. Its merged change retains a mocked-RPC test for fetching full version-1 transactions, rejecting a mismatched full response and avoiding a second fetch for version 2.
The #11693 checks, #11696 checks and #11654 checks each showed 18 successful jobs, including Ubuntu 24.04 tests, on their displayed heads. A green job on a revision without the added test does not show that CI ran that removed regression.
The #11696 description also reports a successful local unit-test run and the functional RPC proofs test. The cited discussion contains no reviewer report of a failed reproduction. We checked the source and recorded upstream results without compiling Monero or executing these proof tests ourselves.
Affected releases and stable availability
Source inspection confirms that v0.18.5.4 still has both affected caller paths and the legacy helper behavior. Its release notes do not include these changes, and neither open PR had a published stable fix at review.
The same pattern is present in the inspected tags v0.18.5.3, v0.18.5.0, v0.18.4.0, v0.18.3.4, v0.18.0.0 and v0.17.3.2. This is a source-based finding for those tags. We have not established the first affected release or tested every intervening binary.
These findings concern Monero's wallet code. They do not establish the affected versions of every third-party wallet that uses, modifies or replaces that code. Application release notes must identify which fixes a distributed version includes.
Attacker prerequisites and practical consequences
For the demonstrated mechanisms, the verifier must use an affected wallet path and accept manipulated daemon responses. The attacker needs control of the daemon's relevant replies, or another way to replace them. For the amount check, the verifier supplies a transaction key and recipient address that identify a matching output. The /gettransactions request sends the transaction ID, not that secret key. The spend-proof reproduction combines substituted transaction data, a matching output-key response and a proof constructed for that substituted input.
The weakness can mislead a person or application relying on a proof result during a payment dispute. It does not give the daemon the wallet's seed, sign a spend of the recipient's funds or alter an accepted blockchain transaction. A false report of 80 instead of 8 does not credit the difference to the recipient's wallet.
Our remote-node fee-security article covers a different way daemon responses influence wallet behavior. These proof checks deserve their own treatment because they concern evidence about an existing transaction rather than construction of a new payment.
Checking a disputed payment while the fixes are pending
For a consequential verification, use your own synchronized node or a node whose operator you trust. If a proof result looks inconsistent, repeat the check through an independently operated node and compare it with the recipient wallet's transaction record. Our wallet connection guide explains the connection choices. Agreement between independent sources provides corroboration. The wallet still needs to validate the data.
Share transaction keys and proof material only with the intended verifier. Keep your recovery seed out of payment disputes and proof checkers. The correction belongs in wallet software, so updating a remote daemon alone leaves an old wallet's checking logic unchanged. Follow official wallet release notes and verify downloaded software.
The earliest affected release, independent execution of the historical regressions, and adoption by third-party wallets remain unverified. Neither PR documents exploitation in the wild. A developer reproduction and a source-code finding establish a different level of evidence from a confirmed incident.