Skip to content

fix(tx): make NoteConsumptionChecker bundle-aware - #3801

Draft
Fumuran wants to merge 1 commit into
nextfrom
fumuran-claude/bundle-aware-note-checker
Draft

fix(tx): make NoteConsumptionChecker bundle-aware#3801
Fumuran wants to merge 1 commit into
nextfrom
fumuran-claude/bundle-aware-note-checker

Conversation

@Fumuran

@Fumuran Fumuran commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #3710.

A feature note and the FEE_SPONSORSHIP notes bound to it are consumable only together. An
uncovered feature note fails in the epilogue, which lands the batch in
find_largest_executable_combination;
that search grew its candidate set one note at a time, and neither half of a bound pair executes
alone. Intact pairs were therefore dropped alongside the note that actually failed - with F1
uncovered, [F0, S0, F1] reported nothing successful rather than {F0, S0}.

The fix makes the bundle, not the note, the unit of the search.

Changes

  • NoteBundle::group groups the inputs by the binding that already exists on chain, since a
    FEE_SPONSORSHIP note names its feature note by NoteId. No caller has to supply the grouping,
    so the ntx builder and clients are fixed together. A sponsorship whose feature note is absent
    forms its own bundle and so fails alone rather than taking others down with it.
  • find_largest_executable_combination appends whole bundles, and its for size in 1..=n loop
    gives way to a round loop that stops when a pass adds nothing - which also fixes the loop bound
    having been computed before remaining_notes started shrinking.
  • FailedNote::error becomes Arc<TransactionExecutorError> so one error can be shared across a
    rejected bundle; new and error keep their signatures, so this stays source-compatible. New
    bundled_with() marks the notes dropped as collateral, which callers need in order to tell those
    apart from genuine failures.
  • New FeeSponsorshipNote::sponsored_feature_note_id puts the binding rule in one public place the
    node can call for its own checks.

The elimination path is untouched: it is only reached on note-script failures, where it already
converges on bundles, and leaving it alone avoids inventing an error for collaterally-dropped notes
there. No MASM, commitment or account ID changes.

Open questions

  1. Two further asks accumulated on the issue that this PR leaves alone: domain-based ordering of
    order-sensitive config notes, and a NoteConsumptionStatus variant for "consumable had the fee
    been covered". Both look like separate issues to me rather than v0.17 blockers on this PR, but
    worth confirming before this is marked ready.

A feature note and the FEE_SPONSORSHIP notes bound to it are consumable
only together: fee collection rejects a sponsorship whose feature note is
absent, and a feature note whose fee is uncovered. Because fee collection
runs in the auth procedure, an uncovered note fails in the epilogue, which
routed the batch into find_largest_executable_combination.

That search grew its candidate set one note at a time from the empty set,
so it only ever reached sets containing a consumable subset with one note
fewer. A bound pair has none, so intact pairs were dropped alongside the
note that actually failed.

Group input notes into bundles that must be consumed together, derived
from the note ID a sponsorship names in its storage, and make the bundle
the unit of the search. Notes of a rejected bundle that were not blamed
for the failure are reported with FailedNote::bundled_with set, so callers
can tell a collateral drop from a genuine failure.

Closes #3710

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GSDwt99bX6hr5TEfaff1Uf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make NoteConsumptionChecker bundle-aware

2 participants