Skip to content

refactor(pswap): follow-ups for #3000 (AssetAmount + NonZeroU32, parsing, MASM asserts, file split, zero-amount fix)#4

Open
VAIBHAVJINDAL3012 wants to merge 14 commits into
nextfrom
vaibhav/pswap-refactor
Open

refactor(pswap): follow-ups for #3000 (AssetAmount + NonZeroU32, parsing, MASM asserts, file split, zero-amount fix)#4
VAIBHAVJINDAL3012 wants to merge 14 commits into
nextfrom
vaibhav/pswap-refactor

Conversation

@VAIBHAVJINDAL3012

@VAIBHAVJINDAL3012 VAIBHAVJINDAL3012 commented May 29, 2026

Copy link
Copy Markdown

Follow-ups to the PSWAP note refactor, addressing the four review groups from upstream issue 0xMiden/protocol#3000, plus one fix surfaced by the review pass on this branch.

Commits

  1. b8d78bf6 refactor(pswap): switch public APIs to AssetAmount and NonZeroU32 (Group A)

    • PswapNoteStorage::requested_asset_amount() returns AssetAmount.
    • PswapNote::create_args takes AssetAmount on both args and is infallible.
    • PswapNoteAttachment::new takes depth: NonZeroU32.
    • parent_depth returns u32, next_depth() yields NonZeroU32 with overflow check.
    • Drops runtime depth == 0 checks from payback_note / remainder_note (now type-enforced).
  2. cf8fee4e refactor(pswap): harden PswapNoteAttachment parsing (Group B)

    • New TryFrom<&NoteAttachment> validates scheme, num_words == 1, AssetAmount range, and non-zero u32 depth.
    • Reroutes parent_depth through the typed parse so malformed attachments degrade to 0 in one place.
    • Integration-test helper migrated to exercise the new TryFrom on every output (9 sites).
  3. a29e336b feat(pswap.masm): assert num_words == 1 and depth fits in u32 in get_current_depth (Group C)

    • Gates write_attachment_to_memory's num_words against PSWAP_ATTACHMENT_NUM_WORDS = 1.
    • Splits the loaded parent_depth into (lo, hi) and assertz the high u32 == 0. Uses manual hi-check over u32assert.err=... so the named error surfaces verbatim at the test boundary (the VM wraps u32assert messages at runtime).
    • Sharpens the procedure docstring: the initial (depth-0) PSWAP carries no PSWAP_ATTACHMENT_SCHEME attachment.
    • New integration tests stamp malformed attachments via NoteAttachment::{with_words,with_word} and exercise both asserts.
  4. 54d07fa4 refactor(pswap): split into submodule, introduce OrderId, expose AssetId view (Group D)

    • Splits crates/miden-standards/src/note/pswap.rs (1430 lines) into pswap/{mod,storage,attachment,tests}.rs. git mv preserves history on the pswap.rs → pswap/mod.rs path.
    • Introduces OrderId(Felt) newtype used by PswapNote::order_id and PswapNoteAttachment{::new,::order_id}.
    • Adds PswapNoteStorage::requested_asset_id() returning AssetId (on-chain layout unchanged).
    • Elevates PARENT_ATTACHMENT_DEPTH_OFFSET to pub(super) so attachment.rs's TryFrom can consume it.
    • Re-exports OrderId from crates/miden-standards/src/note/mod.rs.
  5. 2b17b5fd fix(pswap): reject zero offered/requested amounts in PswapNote::build

    • Surfaced by the principal-engineer review on this branch: calculate_offered_for_requested would panic with a u128 div-by-zero on a PswapNote whose requested_amount == 0.
    • Fixes the root cause by rejecting zero on both sides in PswapNote::build. Covers every constructor including TryFrom<&Note>, which funnels through the builder.
    • The MASM side already rejects such a note via u128::div; this just moves the rejection to the Rust layer so callers get a typed NoteError instead of a panic.

Verification

  • cargo check --workspace --tests clean.
  • cargo test -p miden-standards --lib pswap27 unit tests pass (21 baseline + 6 new for OrderId round-trip, order_id == serial[1], requested_asset_id packing, wrong-num-words TryFrom rejection, and the two zero-amount builder guards).
  • cargo test -p miden-testing --test lib -- pswap72 integration tests pass (70 baseline + 2 new for the MASM num_words and depth asserts).
  • Coverage on the net-new code paths (pswap/attachment.rs, OrderId, requested_asset_id, hardened TryFrom, zero-amount guards) is at or above 95%.

Review

  • Principal-engineer reviewer flagged the zero-amount panic that commit 5 fixes; three other findings (MASM write-before-check ordering, parent_depth == u32::MAX producing unspendable outputs, depth == 0 MASM-vs-Rust leniency gap) were classified as DoS / economic edge cases that are atomically reverted at the VM level and tracked separately, not blockers for this PR.
  • Security review found no exploitable vulnerabilities meeting the >80% confidence threshold (all candidates were DoS-class, excluded by policy).

Notes

  • This is a follow-up branch to a prior PSWAP refactor PR and targets next on inicio's fork.
  • Branch is based on inicio next at a1be171b, which currently matches 0xMiden/protocol next.

Summary by CodeRabbit

  • New Features

    • Typed PSWAP attachments (order IDs, single-word attachments) and a canonical PSWAP storage type
  • Improvements

    • Stronger attachment depth validation and explicit u32 checks; safer remainder/payback note construction and non-zero depth handling
    • Standard-note script resolution now surfaces load errors instead of silently treating them as absent
  • Bug Fixes

    • Reject malformed/oversized attachments and prevent depth/overflow mismatches
  • Tests

    • Expanded PSWAP tests covering parsing, fills, overflow regressions and round-trips

Review Change Stack

- PswapNoteStorage::requested_asset_amount() returns AssetAmount.
- PswapNote::create_args takes AssetAmount on both args and is infallible.
- PswapNoteAttachment::new takes depth: NonZeroU32 (new is infallible).
- parent_depth returns u32; next_depth() yields NonZeroU32 with overflow check.
- payback_note/remainder_note drop runtime depth==0 checks (now type-enforced).
- Split fill_sum u64-overflow test to hand-built note_args (create_args now
  rejects > AssetAmount::MAX client-side; MASM check still exercised).

Closes group A of issue 0xMiden#3000.
- Add TryFrom<&NoteAttachment> for PswapNoteAttachment validating scheme,
  num_words == 1, AssetAmount range, and non-zero u32 depth.
- Reroute PswapNote::parent_depth through the typed TryFrom so any malformed
  attachment degrades to 0 in a single place.
- Inline the deleted pswap_output_attachment helper at the two callers using
  PswapNoteAttachment::new(...).into(); construction stays infallible apart
  from the AssetAmount::new check that was already there.
- Migrate the integration tests' first_attachment_word helper to
  first_pswap_attachment, exercising the new TryFrom on every output note
  (9 sites total).

Closes group B of issue 0xMiden#3000.
…rrent_depth

- New constant PSWAP_ATTACHMENT_NUM_WORDS = 1 and error code
  ERR_PSWAP_ATTACHMENT_WRONG_NUM_WORDS gate the write_attachment_to_memory
  result before any loc_load, ruling out multi-word attachments overwriting
  adjacent local memory.
- After loc_load.PARENT_ATTACHMENT_DEPTH_OFFSET, split into (lo, hi) and
  assertz hi == 0 via ERR_PSWAP_ATTACHMENT_DEPTH_NOT_U32. Prefer manual hi
  check over u32assert.err=... so the named error surfaces verbatim at the
  test boundary (the VM's u32assert wraps its message at runtime).
- Sharpened the procedure docstring: the initial (depth-0) PSWAP carries no
  PSWAP_ATTACHMENT_SCHEME attachment; only payback / remainder notes do.
- Integration tests pswap_assert_attachment_wrong_num_words and
  pswap_assert_attachment_depth_not_u32 stamp malformed attachments via
  NoteAttachment::{with_words,with_word} and exercise both asserts.

Closes group C of issue 0xMiden#3000.
…tId view

- Split crates/miden-standards/src/note/pswap.rs (1430 lines) into pswap/{mod,
  storage,attachment,tests}.rs. mod.rs now hosts the PswapNote struct and the
  new OrderId newtype; storage.rs and attachment.rs own their respective types
  and impls; tests.rs holds the unit tests. git mv preserves history on the old
  pswap.rs -> pswap/mod.rs path.
- Introduce OrderId(Felt) - a transparent newtype that PswapNote::order_id and
  PswapNoteAttachment{::new,::order_id} now use. From<Felt>/From<OrderId> keep
  the MASM bridge cheap.
- Add PswapNoteStorage::requested_asset_id() returning the existing AssetId
  type (suffix, prefix). On-chain layout is unchanged - the typed accessor is
  a Rust-side convenience.
- PARENT_ATTACHMENT_DEPTH_OFFSET elevated to pub(super) so the new
  attachment.rs TryFrom can consume it without duplicating the literal.
- Re-export OrderId from crates/miden-standards/src/note/mod.rs.
- New unit tests cover OrderId round-trips, the order_id == serial[1]
  invariant on PswapNote, and the requested_asset_id() packing.

Closes group D of issue 0xMiden#3000.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c84ce4f5-fcc8-4bb5-ab58-185acf41803e

📥 Commits

Reviewing files that changed from the base of the PR and between 157eef5 and c816401.

📒 Files selected for processing (2)
  • crates/miden-standards/src/note/pswap/mod.rs
  • crates/miden-standards/src/note/pswap/storage.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/miden-standards/src/note/pswap/storage.rs
  • crates/miden-standards/src/note/pswap/mod.rs

📝 Walkthrough

Walkthrough

Refactors PSWAP into typed Rust submodules (OrderId, PswapNoteAttachment, PswapNoteStorage), strengthens MASM attachment validation (single-word + u32-fit), updates PswapNote APIs and execution (payout, payback, remainder), makes StandardNote script lookups fallible, and adds extensive Rust and MASM tests plus callsite/executor adjustments.

Changes

PSWAP Note Refactor and Validation

Layer / File(s) Summary
MASM attachment word count & depth validation
crates/miden-standards/asm/standards/notes/pswap.masm
Adds PSWAP_ATTACHMENT_NUM_WORDS = 1, attachment validation error codes, documents depth-0 behavior, enforces single-word attachments and explicit u32-fit checks in get_current_depth.
PswapNoteAttachment typed encoding/decoding
crates/miden-standards/src/note/pswap/attachment.rs
Introduces PswapNoteAttachment(amount, OrderId, NonZeroU32) with FromNoteAttachment serialization to a single word and TryFrom<&NoteAttachment> with scheme, word-count, and field validation returning NoteError.
Module re-exports & StandardNote fallible APIs
crates/miden-standards/src/note/mod.rs
Re-exports OrderId; makes StandardNote::from_script, from_script_root, script, and script_root return Result<..., NoteError> and propagate PSWAP script/script_root load errors.
PswapNote core: OrderId, builder, depth helpers
crates/miden-standards/src/note/pswap/mod.rs
Adds OrderId(Felt) newtype; tightens builder validation (no matching faucets, no zero amounts); create_args now typed and infallible; order_id(), parent_depth() (safe fallback), and next_depth() (overflow→error) added.
Execute, payout calc, payback & remainder
crates/miden-standards/src/note/pswap/mod.rs
Implements execute_full_fill and execute with input validation, separate account/note payout computation (floor division using u128), payback note creation, and optional remainder note with attachment; adds payout and tag helpers.
Protocol conversions
crates/miden-standards/src/note/pswap/mod.rs
Adds TryFrom<PswapNote> for Note and TryFrom<&Note> for PswapNote: typed asset/tag mapping, optional attachment handling, and PSWAP script/root validation.
PswapNoteStorage: 7-slot layout & (de)serialization
crates/miden-standards/src/note/pswap/storage.rs
Adds PswapNoteStorage with canonical 7-felt mapping, typed accessors, From<PswapNoteStorage> for NoteStorage with compile-time fit assert, and TryFrom<&[Felt]> strict decoding to typed storage.
Rust unit tests
crates/miden-standards/src/note/pswap/tests.rs
Adds deterministic helpers and comprehensive tests covering builder invariants, payout math, storage round-trips, execute partial/exact fills, callback flag preservation, attachment validation cases and round-trips, depth fallback/overflow, OrderId conversions, and asset-id packing.
MASM integration & PSWAP tests
crates/miden-testing/tests/scripts/pswap.rs
Refactors MASM tests to use first_pswap_attachment typed parsing, typed AssetAmount/NonZeroU32 inputs, Note::try_from/try_into conversions, and adds MASM regressions for forged attachments (wrong word count, depth > u32::MAX) and a hand-forged overflow case; updates output sorting to derive order_id from typed attachments.
Callsite & executor updates
bin/bench-transaction/..., crates/miden-testing/..., crates/miden-tx/src/executor/...
Updates call sites to propagate fallible StandardNote::script/script_root (adds ?/unwraps where used), maps script-load failures to explicit TransactionKernelError in exec_host, and treats script-load errors as non-standard in notes_checker ordering/fast-paths.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibble bits and pack the fields,
Depths fit into u32 shields,
Typed attachments snug and tight,
Payout math floors through the night,
Tests hop in—PSWAP sings just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objective: implementing follow-ups to a prior PSWAP refactor with specific technical improvements (AssetAmount, NonZeroU32, parsing hardening, MASM asserts, code reorganization, and zero-amount fix).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vaibhav/pswap-refactor

Comment @coderabbitai help to get the list of available commands and usage tips.

@VAIBHAVJINDAL3012

Copy link
Copy Markdown
Author

@CodeRabbit help

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai emit path instructions to emit generated path-specific review instructions for this repository.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
crates/miden-testing/tests/scripts/pswap.rs (1)

226-230: ⚡ Quick win

Feed the parsed attachment through the reconstruction path.

These branches only trust amount() from first_pswap_attachment; order_id and depth are re-derived from local state. That weakens the new typed-attachment coverage and can miss a decode/accessor regression while the reconstruction tests still pass. Reusing the parsed PswapNoteAttachment here keeps the test end-to-end.

♻️ Suggested change
-    let payback_attachment = PswapNoteAttachment::new(
-        AssetAmount::new(fill_amount_from_aux)?,
-        pswap.order_id(),
-        NonZeroU32::new(1).unwrap(),
-    );
-    let reconstructed_payback =
-        pswap.payback_note(output_p2id.metadata().sender(), &payback_attachment)?;
+    let reconstructed_payback =
+        pswap.payback_note(output_p2id.metadata().sender(), &on_chain_pswap_att)?;
...
-        let remainder_attachment = PswapNoteAttachment::new(
-            AssetAmount::new(amt_payout_from_attachment)?,
-            pswap.order_id(),
-            NonZeroU32::new(1).unwrap(),
-        );
         let reconstructed_remainder = pswap.remainder_note(
             output_remainder.metadata().sender(),
-            &remainder_attachment,
+            &remainder_pswap_att,
             remaining_offered,
             remaining_requested,
         )?;
...
-        let payback_attachment = PswapNoteAttachment::new(
-            on_chain_payback_att.amount(),
-            original_pswap.order_id(),
-            depth,
-        );
         let reconstructed_payback = original_pswap
-            .payback_note(on_chain_payback.metadata().sender(), &payback_attachment)?;
+            .payback_note(on_chain_payback.metadata().sender(), &on_chain_payback_att)?;
...
-            let remainder_attachment = PswapNoteAttachment::new(
-                on_chain_remainder_att.amount(),
-                original_pswap.order_id(),
-                depth,
-            );
             let reconstructed_remainder = original_pswap.remainder_note(
                 on_chain_remainder.metadata().sender(),
-                &remainder_attachment,
+                &on_chain_remainder_att,
                 AssetAmount::new(remaining_offered)?,
                 AssetAmount::new(remaining_requested)?,
             )?;

Also applies to: 258-262, 1769-1773, 1788-1792

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/miden-testing/tests/scripts/pswap.rs` around lines 226 - 230, The test
is reconstructing order_id and depth from local state instead of reusing the
parsed PswapNoteAttachment, which weakens end-to-end coverage; update the
payback_attachment creation (and the similar instances at the other locations)
to derive the attachment from the parsed first_pswap_attachment (e.g., clone or
otherwise reuse first_pswap_attachment and only adjust the amount) rather than
calling PswapNoteAttachment::new with pswap.order_id() and
NonZeroU32::new(1).unwrap(), so the reconstruction path exercises the parsed
attachment's order_id(), depth(), and accessors directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/miden-standards/asm/standards/notes/pswap.masm`:
- Around line 522-528: The current guard (eq.PSWAP_ATTACHMENT_NUM_WORDS /
assert.err=ERR_PSWAP_ATTACHMENT_WRONG_NUM_WORDS) runs after
exec.active_note::write_attachment_to_memory has already copied into the 4-word
`@locals`(4) buffer, so a forged multi-word attachment can clobber locals before
failing; fix by performing the attachment-length check before any write (i.e.,
check num_words == 1 immediately in exec.active_note::write_attachment_to_memory
or its caller) or else allocate/use a scratch region sized for the maximum
allowed attachment length and validate bounds prior to writing; reference
exec.active_note::write_attachment_to_memory, `@locals`(4),
eq.PSWAP_ATTACHMENT_NUM_WORDS and ERR_PSWAP_ATTACHMENT_WRONG_NUM_WORDS when
making the change.

In `@crates/miden-standards/src/note/pswap/attachment.rs`:
- Around line 62-87: The TryFrom<&NoteAttachment> for PswapNoteAttachment
implementation currently ignores the reserved trailing limb (word[3]) and must
reject non-zero values to preserve canonical encoding; update the function (in
the PswapNoteAttachment conversion block) to inspect words[0..] and if word[3]
is not zero return Err(NoteError::other("PSWAP attachment reserved trailing limb
must be zero")) before constructing Self { amount, order_id:
OrderId::from(word[1]), depth }, so malformed attachments that set the reserved
slot are rejected rather than silently accepted.

In `@crates/miden-standards/src/note/pswap/mod.rs`:
- Around line 430-437: The reconstructed child serials are being advanced using
the absolute attachment.depth() (parent_depth) which over-increments when called
on a remainder parent; update the p2id_serial construction (and the analogous
block used later around create_payback_note/create_remainder_pswap_note) to
advance the serial relative to the direct parent by adding ONE to
self.serial_number[3] (instead of adding parent_depth/attachment.depth()), so
use self.serial_number[3] + ONE for the fourth word component when building
p2id_serial.
- Around line 311-324: The match arm that handles a single fill source (the
(Some(asset), None) | (None, Some(asset)) branch) must validate that that asset
matches the requested faucet asset before proceeding to create_payback_note();
currently it bypasses add() and allows minting with a non-requested asset.
Update the code around account_fill_asset and note_fill_asset to compare the
lone asset to self.storage.requested_asset() and return a NoteError (e.g.,
NoteError::other with a clear message) if they differ, otherwise continue using
that asset as payback_asset; keep the existing add() path unchanged for the
(Some, Some) case.

In `@crates/miden-standards/src/note/pswap/tests.rs`:
- Around line 131-134: Update the test for PswapNote::calculate_output_amount to
assert the exact floored result for the non-integer ratio case: replace the
loose assert!("> 0") with an equality check expecting 9 (since 100 * 7 / 73
floors to 9). Locate the call to PswapNote::calculate_output_amount(100, 73, 7)
in the tests and change the assertion to assert_eq!(result, 9) so incorrect
implementations that return any positive value will fail.

---

Nitpick comments:
In `@crates/miden-testing/tests/scripts/pswap.rs`:
- Around line 226-230: The test is reconstructing order_id and depth from local
state instead of reusing the parsed PswapNoteAttachment, which weakens
end-to-end coverage; update the payback_attachment creation (and the similar
instances at the other locations) to derive the attachment from the parsed
first_pswap_attachment (e.g., clone or otherwise reuse first_pswap_attachment
and only adjust the amount) rather than calling PswapNoteAttachment::new with
pswap.order_id() and NonZeroU32::new(1).unwrap(), so the reconstruction path
exercises the parsed attachment's order_id(), depth(), and accessors directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c954cdf8-03a9-4262-907f-2849d73494a9

📥 Commits

Reviewing files that changed from the base of the PR and between a1be171 and 2b17b5f.

📒 Files selected for processing (8)
  • crates/miden-standards/asm/standards/notes/pswap.masm
  • crates/miden-standards/src/note/mod.rs
  • crates/miden-standards/src/note/pswap.rs
  • crates/miden-standards/src/note/pswap/attachment.rs
  • crates/miden-standards/src/note/pswap/mod.rs
  • crates/miden-standards/src/note/pswap/storage.rs
  • crates/miden-standards/src/note/pswap/tests.rs
  • crates/miden-testing/tests/scripts/pswap.rs
💤 Files with no reviewable changes (1)
  • crates/miden-standards/src/note/pswap.rs

Comment on lines 522 to +528
exec.active_note::write_attachment_to_memory
# => [num_words]

drop
# A PswapAttachment is exactly one word - assert before reading back from @locals(4) to
# rule out an attacker-crafted multi-word attachment overwriting adjacent local memory.
eq.PSWAP_ATTACHMENT_NUM_WORDS
assert.err=ERR_PSWAP_ATTACHMENT_WRONG_NUM_WORDS

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The attachment-length guard runs after the unsafe write.

write_attachment_to_memory has already copied the payload into a 4-local buffer before num_words == 1 is asserted, so a forged multi-word PSWAP attachment can still clobber adjacent local memory and only then fail. If there is no length-only probe, this needs a scratch region sized for the maximum attachment length or another pre-write bound check; the current check does not provide the protection described in the comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/miden-standards/asm/standards/notes/pswap.masm` around lines 522 -
528, The current guard (eq.PSWAP_ATTACHMENT_NUM_WORDS /
assert.err=ERR_PSWAP_ATTACHMENT_WRONG_NUM_WORDS) runs after
exec.active_note::write_attachment_to_memory has already copied into the 4-word
`@locals`(4) buffer, so a forged multi-word attachment can clobber locals before
failing; fix by performing the attachment-length check before any write (i.e.,
check num_words == 1 immediately in exec.active_note::write_attachment_to_memory
or its caller) or else allocate/use a scratch region sized for the maximum
allowed attachment length and validate bounds prior to writing; reference
exec.active_note::write_attachment_to_memory, `@locals`(4),
eq.PSWAP_ATTACHMENT_NUM_WORDS and ERR_PSWAP_ATTACHMENT_WRONG_NUM_WORDS when
making the change.

Comment thread crates/miden-standards/src/note/pswap/attachment.rs Outdated
Comment thread crates/miden-standards/src/note/pswap/mod.rs Outdated
Comment thread crates/miden-standards/src/note/pswap/mod.rs
Comment thread crates/miden-standards/src/note/pswap/tests.rs Outdated
VAIBHAVJINDAL3012 pushed a commit that referenced this pull request May 29, 2026
…ction to original

Address review feedback on PR #4:

- execute(): when only one fill source is provided, the path bypassed
  FungibleAsset::add, which is the only place a faucet mismatch would have
  been caught. A caller could mint a payback note whose asset disagrees with
  the storage's requested_faucet_id. The MASM rejects this on-chain but the
  Rust API now catches it client-side via a per-source faucet check at the
  top of execute(). New unit test exercises both single-source arms.

- payback_note() and remainder_note() advance the serial using the absolute
  lineage depth (depth - 1). Calling them on a remainder mid-lineage
  over-counts that delta and reconstructs the wrong note. Document the
  precondition (must be called on the original PSWAP) at the top of each
  doc comment so the contract is loud, without adding a runtime guard that
  would change the existing contract surface.
# write the word at PARENT_ATTACHMENT_PTR, read back only the depth at DEPTH_OFFSET.
const PARENT_ATTACHMENT_PTR = 0
const PARENT_ATTACHMENT_DEPTH_OFFSET = 2
# PswapAttachment is always exactly one word - see PSWAP_ATTACHMENT_SCHEME above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this see line - see PSWAP_ATTACHMENT_SCHEME above.


drop
# A PswapAttachment is exactly one word - assert before reading back from @locals(4) to
# rule out an attacker-crafted multi-word attachment overwriting adjacent local memory.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment not required.....self understandable

drop
# => [hi, parent_depth]
assertz.err=ERR_PSWAP_ATTACHMENT_DEPTH_NOT_U32
# => [parent_depth]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this long comment
And see if their direct way to check if value is u32 or not

let depth = NonZeroU32::new(depth_u32)
.ok_or_else(|| NoteError::other("PSWAP attachment depth must be non-zero"))?;

Ok(Self { amount, order_id: OrderId::from(word[1]), depth })

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the check for word[3] as 0 too.

Comment on lines +24 to +25
/// Slots `[1, 2]` together form a 2-element [`AssetId`] view of the faucet (see
/// [`Self::requested_asset_id`]). The on-chain layout is unchanged - the typed accessor is a

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The on-chain layout is unchanged - the typed accessor is a

remvoe this comment...

self.serial_number[3] + ONE,
]);

let current_depth = self.next_depth()?;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use depth

consumer_account_id: AccountId,
remaining_offered_asset: FungibleAsset,
remaining_requested_asset: FungibleAsset,
offered_amount_for_fill: u64,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be AssetAmount,.....not u64

let recipient = pswap.storage.into_recipient(pswap.serial_number);

let assets = NoteAssets::new(vec![pswap.offered_asset.into()])
.expect("single fungible asset should be valid");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't use expect, as it would panic.

Asset::NonFungible(_) => {
return Err(NoteError::other("PSWAP note asset must be fungible"));
},
};

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note onmly has one asset, so no need to iter it......

.build();
let expected = AssetId::new(faucet_id.suffix(), faucet_id.prefix().as_felt());
assert_eq!(storage.requested_asset_id(), expected);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check in the test if we can use rtest

VAIBHAVJINDAL3012 pushed a commit that referenced this pull request May 29, 2026
Addresses the 26 inline review comments on #4 in one pass.

MASM (pswap.masm):
- Drop the redundant 'see PSWAP_ATTACHMENT_SCHEME above' cross-reference and
  the self-explanatory comment on the num_words == 1 assert.
- Shorten the depth-check comment to a one-line note on why u32assert.err is
  avoided (VM wraps the message at runtime).

attachment.rs:
- TryFrom now asserts word[3] == 0 (the reserved slot). Matches the encoder's
  invariant and stops malformed attachments from round-tripping to a different
  value. New unit test try_from_rejects_non_zero_reserved_slot.

storage.rs:
- Trim three doc blurbs (the on-chain-layout note, the payback_note_type
  rationale, the AssetId description) to focus on what the type is, not why.
- Replace the .expect on NoteStorage::new with const _: () = assert! +
  unwrap_or_else(|_| unreachable!()), giving compile-time proof that the
  serialization path is unreachable on its error branch.

mod.rs:
- Drop 'lineage' from OrderId / order_id / payback_note / remainder_note docs;
  drop the network-account paragraphs from PswapNote / attachments() docs;
  shorten the zero-amount guard comment to one line.
- Rename current_depth -> depth in code and docs throughout.
- Migrate calculate_output_amount, calculate_offered_for_requested, execute,
  and create_remainder_pswap_note to take/return AssetAmount end-to-end
  (callers stop bouncing through u64).
- Remove the .iter().flatten() on the 2-element fill array; use a small
  is_some_and closure instead.
- Replace note.assets().iter().next().unwrap() + match Asset variant with
  note.assets().iter_fungible().next().ok_or_else(...) on the TryFrom<&Note>
  reconstruction path.
- Replace the num_attachments() match + .expect on get(0) with a >1 guard
  followed by attachments.get(0).cloned() (no panic path).
- Drop the now-redundant fill_amount param from create_payback_note;
  payback_asset.amount() carries it.
- Drop the duplicated 'matching MASM add.1' / 'matching MASM movup.3 add.1
  movdn.3' inline comments on serial derivations.

tests.rs:
- Update calculate_output_amount unit test to AssetAmount and assert the
  exact floor result for the non-integer ratio case (CodeRabbit #5).
- Add try_from_rejects_non_zero_reserved_slot covering the new word[3]
  guard.

miden-testing/tests/scripts/pswap.rs:
- Update six calculate_offered_for_requested call sites to pass AssetAmount
  and unwrap the AssetAmount result back to u64 where downstream code needs
  it for vault/amount arithmetic.

Verified:
- cargo +nightly fmt --all clean.
- cargo clippy -p miden-standards -p miden-testing --all-targets -- -D
  warnings clean on both crates.
- 29 unit tests + 72 integration tests pass.
Without this guard, calling calculate_offered_for_requested on a PswapNote
with requested_amount == 0 panics with an unchecked integer division by zero
at u128 / 0 in calculate_output_amount. The Rust panic is reachable only
through direct API misuse (execute short-circuits via fill > requested),
but it is still a Rust panic rather than a NoteError, which is the wrong
failure mode for a typed builder API.

Fix the root cause by rejecting zero on both sides in PswapNote::build:

- offered_amount == 0 means the note pays out nothing on any fill (useless).
- requested_amount == 0 is the div-by-zero path.

Placing the check in build covers every constructor: the typed builder
itself, and TryFrom<&Note> which funnels through it on the reconstruction
path. Storage- and attachment-level reconstruction stays permissive because
the on-chain MASM's u128::div would have rejected such a note anyway -
the new check just moves that rejection to the Rust layer so callers get
a typed NoteError instead of a panic.

Two new unit tests cover both rejection paths.
…ction to original

Address review feedback on PR #4:

- execute(): when only one fill source is provided, the path bypassed
  FungibleAsset::add, which is the only place a faucet mismatch would have
  been caught. A caller could mint a payback note whose asset disagrees with
  the storage's requested_faucet_id. The MASM rejects this on-chain but the
  Rust API now catches it client-side via a per-source faucet check at the
  top of execute(). New unit test exercises both single-source arms.

- payback_note() and remainder_note() advance the serial using the absolute
  lineage depth (depth - 1). Calling them on a remainder mid-lineage
  over-counts that delta and reconstructs the wrong note. Document the
  precondition (must be called on the original PSWAP) at the top of each
  doc comment so the contract is loud, without adding a runtime guard that
  would change the existing contract surface.
Re-flows imports, multi-line argument lists, and a few long expressions
through cargo +nightly fmt with the repo's rustfmt.toml. No behavior
change. Confirmed cargo clippy -p miden-standards --all-targets -D
warnings and -p miden-testing --all-targets -D warnings are clean.
Addresses the 26 inline review comments on #4 in one pass.

MASM (pswap.masm):
- Drop the redundant 'see PSWAP_ATTACHMENT_SCHEME above' cross-reference and
  the self-explanatory comment on the num_words == 1 assert.
- Shorten the depth-check comment to a one-line note on why u32assert.err is
  avoided (VM wraps the message at runtime).

attachment.rs:
- TryFrom now asserts word[3] == 0 (the reserved slot). Matches the encoder's
  invariant and stops malformed attachments from round-tripping to a different
  value. New unit test try_from_rejects_non_zero_reserved_slot.

storage.rs:
- Trim three doc blurbs (the on-chain-layout note, the payback_note_type
  rationale, the AssetId description) to focus on what the type is, not why.
- Replace the .expect on NoteStorage::new with const _: () = assert! +
  unwrap_or_else(|_| unreachable!()), giving compile-time proof that the
  serialization path is unreachable on its error branch.

mod.rs:
- Drop 'lineage' from OrderId / order_id / payback_note / remainder_note docs;
  drop the network-account paragraphs from PswapNote / attachments() docs;
  shorten the zero-amount guard comment to one line.
- Rename current_depth -> depth in code and docs throughout.
- Migrate calculate_output_amount, calculate_offered_for_requested, execute,
  and create_remainder_pswap_note to take/return AssetAmount end-to-end
  (callers stop bouncing through u64).
- Remove the .iter().flatten() on the 2-element fill array; use a small
  is_some_and closure instead.
- Replace note.assets().iter().next().unwrap() + match Asset variant with
  note.assets().iter_fungible().next().ok_or_else(...) on the TryFrom<&Note>
  reconstruction path.
- Replace the num_attachments() match + .expect on get(0) with a >1 guard
  followed by attachments.get(0).cloned() (no panic path).
- Drop the now-redundant fill_amount param from create_payback_note;
  payback_asset.amount() carries it.
- Drop the duplicated 'matching MASM add.1' / 'matching MASM movup.3 add.1
  movdn.3' inline comments on serial derivations.

tests.rs:
- Update calculate_output_amount unit test to AssetAmount and assert the
  exact floor result for the non-integer ratio case (CodeRabbit #5).
- Add try_from_rejects_non_zero_reserved_slot covering the new word[3]
  guard.

miden-testing/tests/scripts/pswap.rs:
- Update six calculate_offered_for_requested call sites to pass AssetAmount
  and unwrap the AssetAmount result back to u64 where downstream code needs
  it for vault/amount arithmetic.

Verified:
- cargo +nightly fmt --all clean.
- cargo clippy -p miden-standards -p miden-testing --all-targets -- -D
  warnings clean on both crates.
- 29 unit tests + 72 integration tests pass.
@VAIBHAVJINDAL3012 VAIBHAVJINDAL3012 force-pushed the vaibhav/pswap-refactor branch from f24004b to 0565c0f Compare May 29, 2026 13:37
loc_load.PARENT_ATTACHMENT_DEPTH_OFFSET
# => [parent_depth]

# Manual hi==0 check rather than u32assert.err=... because the VM wraps u32assert's

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment....

# => [parent_depth]

# Manual hi==0 check rather than u32assert.err=... because the VM wraps u32assert's
# message at runtime, which would defeat exact-match assertions on the named error.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this above line

# message at runtime, which would defeat exact-match assertions on the named error.
dup u32split drop
# => [hi, parent_depth]
assertz.err=ERR_PSWAP_ATTACHMENT_DEPTH_NOT_U32

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line after # =>

&self,
consumer_account_id: AccountId,
account_fill_asset: Option<FungibleAsset>,
note_fill_asset: Option<FungibleAsset>,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Option rahter than asset.....

};
if wrong_faucet(&account_fill_asset) || wrong_faucet(&note_fill_asset) {
return Err(NoteError::other("fill asset faucet does not match the requested faucet"));
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wrong_Facuet will go once we do the get the asseAmount input

)));
}

// Mirror the MASM, which computes payouts separately for the account and note halves

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont say Mirror the Masm, it not required..


// Mirror the MASM, which computes payouts separately for the account and note halves
// (the account half flows to the consumer, the sum drives the remainder's offered).
let account_fill_amount = account_fill_asset.map_or(AssetAmount::ZERO, |a| a.amount());

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would direclty get the amount now..

Comment on lines +572 to +573
let product = (offered_total.as_u64() as u128) * (fill_amount.as_u64() as u128);
let quotient = product / (requested_total.as_u64() as u128);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use better vaiable name

@VAIBHAVJINDAL3012 VAIBHAVJINDAL3012 left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make a change

…Note

Mirrors the storage.rs fix from the previous commit. The expect was provably
unreachable but still a panic-capable expression. Replace with a compile-time
assertion (1 <= NoteAssets::MAX_NUM_ASSETS) plus unwrap_or_else(|_|
unreachable!()) so the path is unreachable by construction.
Addresses the second review batch on #4.

API: PswapNote::execute now takes Option<AssetAmount> on both fill slots
instead of Option<FungibleAsset>. The faucet is implicit (it must be the
PSWAP's requested faucet), so passing a typed amount removes the only
caller-visible mismatch surface. Callers no longer construct FungibleAsset
client-side to invoke execute.

Knock-on cleanups:
- Drop the wrong-faucet guard added in 9365d38 and its unit test
  (unreachable now that callers can't pass a mismatched faucet).
- Drop the 'Mirror the MASM' comment in execute.
- Collapse map_or(AssetAmount::ZERO, |a| a.amount()) to a direct
  unwrap_or(AssetAmount::ZERO) per the AssetAmount signature.
- Rename calculate_output_amount's  intermediate to .

MASM: drop the 2-line u32assert-rationale comment + blank line above the
depth check; add a blank line after the # => [hi, parent_depth] annotation
so the assertz lines visually separate.

Tests: all 24 .execute() call sites in tests/scripts/pswap.rs and
src/note/pswap/tests.rs updated to pass AssetAmount (or Asset::amount())
on the fill slots. 28 unit + 72 integration tests pass; cargo +nightly fmt
and cargo clippy -p miden-standards -p miden-testing --all-targets -- -D
warnings clean.
The doc still referenced `account_fill_asset` / `note_fill_asset` — both
renamed to `account_fill` / `note_fill` when execute() switched to
Option<AssetAmount>.
…expect

Drop the last .expect on the PSWAP_SCRIPT LazyLock. The init can only fail if
the standards library is built without the PSWAP procedure (a build-time
invariant), but we now surface the failure as a NoteError so callers control
the failure mode rather than getting a panic.

Storage of the load result:
- LazyLock<Result<NoteScript, String>> (NoteError is not Clone, so the error
  side is held as a String and re-wrapped into a fresh NoteError on each
  access via pswap_script_error).

API ripple (all build-time-invariant, never expected to fire at runtime):
- PswapNote::script() / ::script_root() / ::create_tag() now return Result.
- PswapNoteStorage::into_recipient() now returns Result.
- From<PswapNote> for Note becomes TryFrom<PswapNote> for Note.
- StandardNote::from_script(), ::from_script_root(), ::script(),
  ::script_root() now return Result (the PSWAP arm propagates; other arms
  are wrapped in Ok). from_script* now return Result<Option<Self>>.
- TryFrom<&Note> for PswapNote uses script_root()? .

Call-site updates:
- Tests: 24 Note::from(pswap) / pswap.into() sites → Note::try_from(pswap)? .
- crates/miden-tx executor + notes_checker: thread the Result through
  exec_host's StandardNote::from_script_root + .script() path, and treat the
  build-time failure as 'not a known standard note' in the checker's
  ordering / consumption-status paths so existing behavior is preserved.
- bin/bench-transaction, miden-testing/tests/agglayer/bridge_out,
  miden-testing/tests/scripts/faucet, miden-testing/src/kernel_tests: minor
  ? / .unwrap() additions on infallible-by-construction call sites.

Verified: cargo +nightly fmt --all clean; cargo clippy on
miden-standards, miden-testing, miden-tx --all-targets -- -D warnings clean;
28 unit + 72 integration tests pass.
- storage.rs: explain that NoteStorage::new only fails on > MAX_NOTE_STORAGE_ITEMS,
  we always pass exactly NUM_STORAGE_ITEMS = 7, and the const _: () = assert!
  above proves the inequality at compile time.
- mod.rs: explain that NoteAssets::new only fails on > MAX_NUM_ASSETS or on a
  duplicate, we always pass exactly one asset (covered by the const _: () =
  assert!), and the duplicate-detection loop skips index 0 so it never runs
  for a single-element vec.

The old comments said 'unreachable per the const assert above' without
spelling out WHY both failure conditions are ruled out. Future readers
shouldn't have to dig into NoteStorage::new / NoteAssets::new to convince
themselves.
…able!())

The SAFETY comment above each call already proves the path is unreachable;
.unwrap() carries the same semantics with less ceremony and gives a more
informative panic message (the actual NoteError) if the impossible ever
happens.
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.

1 participant