Problem
ProofService.recoverProofsFromOutputData() reconstructs proofs from the mint restore response by copying the blinded signature point into the proof:
const proof: Proof = {
id: signature.id,
amount: signature.amount,
secret: new TextDecoder().decode(output.secret),
C: signature.C_,
};
signature.C_ is the blinded signature returned for the blinded output. A valid Proof.C must be the unblinded signature point for the output secret.
The sibling change-proof path in the same service already does this correctly via OutputData.toProof():
output.toProof(sig, { id: keyset.id, keys: keyset.keypairs as Keys })
Verification
This is not related to the cashu-ts v4 migration. The same recovery code path exists on origin/master.
I also verified the installed @cashu/cashu-ts@4.1.0 behavior directly: for the same output data and blinded signature, signature.C_ !== output.toProof(signature, keyset).C, while the recovered secret matches. So copying C_ into Proof.C persists a structurally plausible but cryptographically wrong proof.
Impact
Any path that uses recoverProofsFromOutputData() after mint-side signing but before local proof persistence can save bad proofs. Proof-state checks may still appear to pass because they are secret/Y based, but the proof can fail later when spent at the mint.
Known affected recovery surfaces include:
- mint operation recovery
- receive operation recovery
- send rollback/recovery
- P2PK send resurfacing
- melt swap recovery
Current tests do not catch this; at least one recovery test asserts that recovered proof C equals the mock C_, which encodes the broken behavior.
Suggested fix
Use OutputData.toProof() inside recoverProofsFromOutputData() when matching restored signatures back to outputs.
The method will need access to the relevant keyset for each restored signature/output, likely by signature.id or output.blindedMessage.id, similar to unblindAndSaveChangeProofs().
Add a focused regression test that fails if recovered proof C is copied directly from signature.C_ instead of produced through OutputData.toProof().
Problem
ProofService.recoverProofsFromOutputData()reconstructs proofs from the mint restore response by copying the blinded signature point into the proof:signature.C_is the blinded signature returned for the blinded output. A validProof.Cmust be the unblinded signature point for the output secret.The sibling change-proof path in the same service already does this correctly via
OutputData.toProof():Verification
This is not related to the cashu-ts v4 migration. The same recovery code path exists on
origin/master.I also verified the installed
@cashu/cashu-ts@4.1.0behavior directly: for the same output data and blinded signature,signature.C_ !== output.toProof(signature, keyset).C, while the recovered secret matches. So copyingC_intoProof.Cpersists a structurally plausible but cryptographically wrong proof.Impact
Any path that uses
recoverProofsFromOutputData()after mint-side signing but before local proof persistence can save bad proofs. Proof-state checks may still appear to pass because they are secret/Y based, but the proof can fail later when spent at the mint.Known affected recovery surfaces include:
Current tests do not catch this; at least one recovery test asserts that recovered proof
Cequals the mockC_, which encodes the broken behavior.Suggested fix
Use
OutputData.toProof()insiderecoverProofsFromOutputData()when matching restored signatures back to outputs.The method will need access to the relevant keyset for each restored signature/output, likely by
signature.idoroutput.blindedMessage.id, similar tounblindAndSaveChangeProofs().Add a focused regression test that fails if recovered proof
Cis copied directly fromsignature.C_instead of produced throughOutputData.toProof().