fix: Reject circular note dependencies at transaction, batch and block level#2993
Conversation
4d690a6 to
3fcc765
Compare
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left a few small comments inline.
| // Skip inserting the erased note into the input notes set. | ||
| continue 'input_note_iter; |
There was a problem hiding this comment.
Question: is the loop label really needed here? Seems like we are applying the continue to the inner-most loop anyways.
There was a problem hiding this comment.
No, it's not, I mainly added it for extra clarity. No strong opinion on keeping or removing it. If other reviewers have a preference, please comment.
| if output_notes.iter().any(|output_note| output_note.id() == input_note.id()) { | ||
| return Err(ProvenTransactionError::NoteConsumedAndCreated(input_note.id())); | ||
| } | ||
| } |
There was a problem hiding this comment.
just to confirm: this is the same condition that is enforced by erase_notes_inner (and will be later encoded in the batch kernel).
So this is just a sanity check, rather than enforcement of anything?
There was a problem hiding this comment.
Not just a sanity check, it is important logic that the batch relies on. Note that the note tracker only asserts that a given container doesn't have the same note in its input and output note set, it doesn't return an error. So in other words, it assumes this condition is upheld.
Previously, I returned an error for this, but this felt odd: A transaction can and should itself ensure that this condition is upheld, so checking it at the batch level felt wrong. Hence, only an assertion ala "this should never happen". I think we should even move this check to the tx kernel, because right now this is only enforced at the Rust API level, but really the tx kernel should ensure this condition.
Mirko-von-Leipzig
left a comment
There was a problem hiding this comment.
Code is probably good; I would personally prefer it be easier to read. So treat this as an approve.
Background
Issue #2796 describes an asset-fabrication path that works because unauthenticated input notes are credited to the consuming account's vault without any provenance check. A user can wrap a forged asset in a self-directed note and consume that note as an unauthenticated input; the kernel adds the asset to the vault, and the transaction's asset-preservation rules are satisfied as long as the note is balanced on the output side.
The structural enabler is a circular dependency between input and output notes that gets erased before the fabricated asset ever has to match any real on-chain state. This PR refactors
InputOutputNoteTrackerto reject those note cycles.Cycles at three levels
The cycle that lets a forged asset round-trip in and out of an account's vault can be set up at the transaction, batch, or block layer. Each level uses the same trick (an unauthenticated input note matches an output note, so both sides get erased). Consequently, we need to fix all of them.
Changes
InputOutputNoteTrackerand replaces it with functions since everything happens in a single function now.ProvenTransactionlevel that input and output notes sets are disjoint. We should probably add this check in the transaction kernel, but I didn't want to go quite that far in this PR, since that isn't straightforward.Unrelated Changes
NoteCommitmentMismatcherror variant. No longer needed after UpdateNoteIdandNoteCommitmentcomputation #2861.closes #2796
closes #1768