Skip to content

chore: port agglayer lock/unlock onto next#2955

Merged
mmagician merged 7 commits into
nextfrom
ajl-claude/agglayer-to-next-sync
May 20, 2026
Merged

chore: port agglayer lock/unlock onto next#2955
mmagician merged 7 commits into
nextfrom
ajl-claude/agglayer-to-next-sync

Conversation

@partylikeits1983
Copy link
Copy Markdown
Contributor

Purpose

The agglayer and next branches diverged on 2026-03-30 (merge-base 954a37bc). With the release approaching, this PR brings the agglayer-only commits onto next so the release branch is complete. It is the inverse direction of #2951 (which backported nextagglayer).

I reviewed every PR merged to agglayer in the past ~4 weeks and cross-referenced each against next's history by PR number. Most had already flowed to next under different hashes (git cherry over-reported them due to patch-id drift). Only three commits were genuinely missing — and they are exactly the lock/unlock cluster.

Commits

Cherry-picked (with -x), oldest first:

  • #2752 — perf(agglayer): selective frontier load/save in B2AGG processing
  • #2860 — fix(AggLayer): key faucet registry by (origin_network, origin_token_address)
  • #2771 — feat: lock/unlock for native Miden assets (the main feature, ~20 files)

#2752 and #2860 cherry-picked cleanly modulo next's Felt::newFelt::new_unchecked change and the AssetAmount / AccountStorageMode / faucet-flag renames.

Note on #2771: this is an adapted port, not a verbatim cherry-pick

next and agglayer made incompatible faucet design choices after they forked, so #2771 could not be cherry-picked verbatim — it was re-implemented on next's architecture:

The port keeps #2771's design (faucet holds only token metadata; the bridge holds conversion data and locks native assets in its own vault) but expresses it on next's current API:

  • AggLayerFaucet is now a thin wrapper over next's FungibleFaucet with no bridge-specific storage slots.
  • The new bridge_in_output.masm keeps #2771's genuinely new native-token unlock_and_send procedure, but its wrapped-token MINT path uses next's existing, proven 14-felt MINT-note machinery (note::compute_and_store_recipient, output_note::add_word_attachment) rather than agglayer's 18-felt attachment-in-storage layout — next's MINT note script expects the 14-felt layout.
  • MASM procedures referenced across modules were marked pub as next's assembler (miden-vm v0.23) requires.
  • CONFIG_AGG_BRIDGE note creation uses next's PartialNoteMetadata + NoteAttachments API.
  • Test call sites were updated to next's signatures (ConfigAggBridgeNote::create(ConversionMetadata, …), 6-arg create_existing_agglayer_faucet, ClaimNote::create, AccountIdVersion::Version1, AccountStorageMode::Public).

Intentionally excluded (already on next)

Checked by PR number and confirmed present on next, so not cherry-picked: #2730 (87dcd642), #2745 (5b871b7c), #2746 (f2c66258), #2759 (588ca081), and #2848 (redundant — it is itself a cherry-pick of #2759).

Verification

  • cargo build --workspace — clean.
  • cargo clippy --workspace --all-targets — clean.
  • cargo test -p miden-testing --test lib -- agglayer56 passed, 0 failed, including the lock/unlock tests bridge_in_unlock_native_token, bridge_in_unlock_native_duplicate_rejected, and bridge_out_lock_native_token, plus #2752's bridge_out_at_high_num_leaves and #2860's test_config_agg_bridge_distinguishes_origin_network / test_claim_fails_when_origin_network_unregistered regression tests.

🤖 Generated with Claude Code

mmagician and others added 5 commits May 19, 2026 10:59
…2752)

* perf(agglayer): selective frontier load/save in bridge-out

Replace unconditional load/save of all 32 LET frontier entries with
selective operations based on the bit pattern of num_leaves:

- load_let_frontier_selective: only loads entries where bit h=1
  (entries that will be READ by append_and_update_frontier)
- save_let_frontier_selective: only saves entries where bit h=0
  (entries that were WRITTEN by append_and_update_frontier)

This halves the number of storage map syscalls from 128 to 64 per
B2AGG note consumption. Cycle savings vary by frontier occupancy:
- Empty tree (0 leaves): 145k -> 116k (-20%)
- Populated tree (2^31-1 leaves): 140k -> 60k (-57%)

Also adds benchmarks with pre-populated frontier states (2^31 and
2^31-1 leaves) to measure the impact across different access patterns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review nits

- Add v0.15.0 CHANGELOG entry for selective frontier optimization
- Add newline after stack comment to separate logic blocks
- Move add.1 to same line as swap u32shr.1 swap
- Use if.false instead of eq.0 if.true for bit check
- Add stack comment after double_word_array::set

https://claude.ai/code/session_01GjjUe9KUggLJMAdiuBw8vk

* Apply suggestions from code review

Co-authored-by: Marti <marcin.gorny.94@protonmail.com>

* perf(agglayer): avoid num_leaves memory round-trip in load_let_frontier_selective

Keep num_leaves on the stack across the storew of [num_leaves, 0, 0, 0] to
LET_FRONTIER_MEM_PTR, instead of dropping the word and reloading from memory.

* chore: clarify load and save docs

* docs: read/write frontier with explanations

* chore: test large `num_leaves` for selective frontier loading (#2853)

* test(agglayer): exercise selective frontier load/save at high num_leaves

Adds bridge_out_at_high_num_leaves with two parameterised cases that pre-populate
the bridge LET storage and consume one B2AGG note each, verifying the resulting
Local Exit Root against an in-process MerkleTreeFrontier32 reference (already
Solidity-compatible via test_solidity_mtf_compatibility):

- peak_read  (num_leaves = 2^31 - 1, binary 0111...1): every bit 0-30 is 1, bit 31
  is 0. The masm READs frontier[0..30] from storage and WRITEs frontier[31] back.
- peak_write (num_leaves = 2^31, binary 1000...0): every bit 0-30 is 0, bit 31
  is 1. The masm READs frontier[31] and WRITEs frontier[0..30].

Together these two single-leaf inserts exercise every height in both READ and
WRITE roles. The existing bridge_out_consecutive only walks num_leaves through
0..32, exercising bits 0-4; this fills in bits 5-31.

To support pre-populating the frontier, MerkleTreeFrontier32 gains a from_state
constructor and is made pub(super). The populate_let_state helper sets storage
map keys as Word [0, 0, 0, h] (lo) / [0, 0, 1, h] (hi) — matching what the masm
builds via push.0.0.0 / push.1.0.0 with the index at the bottom of the 4-felt
key window.

* chore: simplify docs; remove silly assertion

* test(agglayer): seed initial frontier with a random byte stream

Replaces the hand-rolled byte pattern with a seeded StdRng-driven fill, matching
the StdRng-from-u64 pattern already used in asset_conversion.rs. Keeps the test
deterministic across runs.

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
(cherry picked from commit 4e4a496)
…ddress) (#2860)

* fix(AggLayer): key faucet registry by (origin_network, origin_token_address)

* chore: add CHANGELOG entry for #2860

* test(agglayer): pass origin_network to ConfigAggBridgeNote::create in bridge_out_at_high_num_leaves

The new test added on agglayer (#2752) used the old 5-arg ConfigAggBridgeNote::create
signature; this PR's signature change (adding origin_network) needed to be threaded
through after the merge.

* fix(testing): align agglayer-imported tests with new ConfigAggBridgeNote::create signature

The merged agglayer cherry-pick introduced tests that called the 5-argument
ConfigAggBridgeNote::create and referenced ClaimDataSource::SimulatedL1ToMiden,
neither of which exist on this branch. Pass the origin_network argument and use
the renamed L1ToMiden variant so these tests compile.

* fix: address review comments on bridge_config.masm

- register_faucet: correct pad(8) to pad(10) after exec.hash_token_address;
  callable procs auto-pad the stack to 16, so the annotation must reflect the
  two zero felts that come up from overflow when hash_token_address consumes 6
  felts and produces 4.
- lookup_faucet_by_token_address: trim the docblock to drop the contextual
  "keying on the address alone..." sentence that only made sense in the
  context of this PR.
- hash_token_address: collapse the per-store inline comments back to a single
  umbrella comment, matching the original's terser style.

(cherry picked from commit 0003d0a)
…n assets (#2771)

* feat: add scale data to bridge storage

* feat: update config_agg_bridge note & fix register_faucet proc

* feat: extend faucet registration with full metadata, is_native flag, and metadata hash

* feat: replace FPI with bridge-local reads in bridge_out, add lock path for native tokens

* feat: add unlock path for native tokens in bridge_in, plus lock/unlock tests

* chore: add changelog entry & bump rand to fix cargo-deny

Bumps rand 0.9.2 → 0.9.4 and rand 0.10.0 → 0.10.1 to resolve
RUSTSEC-2026-0097 flagged by `cargo deny check` in CI.

* refactor: slim faucet & update SPEC section

* style: rustfmt import grouping

* refactor: tighten MASM stack ops, normalize stack-comment style

* refactor: cleanup masm & stack comments

* refactor: pack ConfigAggBridgeNote::create args into ConversionMetadata

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: split bridge_in output-note emission into bridge_in_output.masm

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: fix stack-comment depths in CONFIG_AGG_BRIDGE.masm

The inter-call dropw sequences were removed, so the input pad(12) now
persists across both call boundaries. Update every # => [...] comment
to reflect the true sdepth (verified with sdepth debug.stack drop):
pad(16) through the setup, pad(32) after call 1, pad(48) after call 2,
pad(16) after the final repeat.8 cleanup.

* refactor: clarify docstrings and add native-path duplicate-claim test

- bridge_config.masm: reword the register_faucet local-offset comment and
  move an implementation detail out of get_faucet_metadata_hash's docstring
  into an inline comment.
- bridge_in_output.masm: document that unlock_and_send's replay safety comes
  from the nullifier check in bridge_in::claim, not serial-number uniqueness.
- bridge_in.rs: add bridge_in_unlock_native_duplicate_rejected. Seeds the
  bridge vault with 2x the claim amount so the nullifier is the only thing
  stopping a second unlock, then asserts the replay fails with
  ERR_CLAIM_ALREADY_SPENT.

* refactor: dedupe shared CLAIM_* consts and colocate UNLOCK_*_LOC

Make CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT and CLAIM_PROOF_DATA_KEY_MEM_ADDR
public in bridge_in.masm and import them from bridge_in_output.masm,
removing the duplicate definitions and the "keep in sync" comments.
Move UNLOCK_*_LOC consts to sit immediately above unlock_and_send so the
local-memory layout lives next to the procedure that uses it.

* refactor: apply PR suggestions

Co-authored-by: Marti <marcin.gorny.94@protonmail.com>

* refactor(agglayer): drop redundant memory layout description in CONFIG_AGG_BRIDGE.masm

The 18-felt note storage layout was documented in three places:
the top-of-file comment block, the constant definitions immediately
below, and the docblock above 'begin'. The constants are self-documenting
and the docblock covers it for callers, so drop the top-of-file block.

* refactor(agglayer): add mem_load_double_word_unaligned helper

Mirror mem_store_double_word_unaligned in asm/agglayer/common/utils.masm
and use it in CONFIG_AGG_BRIDGE.masm to load the metadata-hash double word
from offset 10..17, replacing eight individual mem_load.METADATA_HASH_*
ops with a single push.METADATA_HASH_LO_0 exec.utils::mem_load_double_word_unaligned.

* refactor(agglayer): pre-pad CONFIG_AGG_BRIDGE call frames to drop the movdn dance

Push the call's 6 trailing pad zeros first, then build the args on top, instead
of pushing args first and then doing 'movdn.15 movdn.15 movdn.15 movdn.15 movdn.15
movdn.15' to move 6 pads to the bottom. The end-state stack is identical; this
just removes a 6-instruction shuffle from each of the two call sites.

* refactor(agglayer): introduce sub-key constants for faucet_metadata_map

Replace literal push.0.0 / push.0.1 / push.0.2 / push.0.3 sub-key
prefixes with named constants FAUCET_METADATA_SUBKEY_{ADDR_LO,ADDR_HI,
HASH_LO,HASH_HI}. Only the metadata-map sites are touched; push.0.0 in
faucet_registry / token_registry contexts is a different map and stays
literal.

* refactor(agglayer): cheaper register_faucet local stash via repeated movup.5

Replace movup.9/.8/.7/.6/.5 with five repeated movup.5 starting from
the now-deepest non-address element. Each movup always lifts from depth
5 (instead of climbing 9→5), and the post-iteration stack is identical.

* refactor(agglayer): use repeat.5 dup.4 to duplicate origin token address

Replace 'dup.4 dup.4 dup.4 dup.4 dup.4' with 'repeat.5 dup.4 end'. The
five-fold dup-from-depth-4 produces the same stack pattern (top word is
the duplicate address with addr0 at the very top) which is what
hash_token_address expects when it stores the 5-felt address into local
memory before computing Poseidon2.

* refactor(agglayer): unify get_faucet_metadata_hash key-prep with sibling proc

Rewrite get_faucet_metadata_hash to use the same 'prep both keys
first, then swapw between reads' pattern as get_faucet_conversion_info.
The previous shape used 'dup.1 dup.1 ... movup.5 movup.5' to reshuffle
faucet_id around the first read, which read awkwardly next to a sibling
proc with the opposite mechanism. This subsumes the swapw / movup.5
movup.5 oddity Marti called out.

* refactor(agglayer): correct stack-padding annotations in register_faucet

Re-walk stack annotations from proc-entry through Steps 2-4. Pads grow
on pops (via Miden's auto-padding to depth-16 floor) and stay constant
on pushes; the previous comments treated them as if pad+named always
equaled 16, which understated the depth after each new push.

* refactor(agglayer): clarify is_faucet_native trailing-zeros comment

'sink ... below the trailing zeros' was misleading because the stack
direction it implied is the opposite of what movdn.2 does. Reword to
'move is_native past the two trailing zeros'.

* refactor(agglayer): clarify unlock_and_send local-stash comment

The previous comment referenced 'claim's CLAIM_DEST_ID_*_LOCAL' and the
'exec invocations get their own local frame' detail, both of which are
distracting. State the actual reason for the stash: we'll need the
destination ID again after native_account::remove_asset clears the stack.

* docs(agglayer): document native faucet registration and updated bridge flows

- Section 2.1 (bridge-out): drop FPI references, replace with bridge-local
  faucet_metadata_map reads. Add the is_native dispatch (BURN vs lock_asset).
- Section 2.2 (bridge-in): drop the unconditional MINT description, replace
  with the is_native dispatch (MINT vs remove_asset + direct P2ID). Update
  the token-registry lookup to mention the (address, network) pair.
- Section 2.4 (faucet registration): describe the CONFIG_AGG_BRIDGE 18-felt
  payload and the two-call register_faucet / store_faucet_metadata_hash flow.
- Section 7.1: rename to 'Registering faucets on Miden' (covers both wrapped
  and Miden-native faucets), and add a new sub-section explaining the
  wrapped-vs-native distinction at the dispatch level.

* Apply suggestions from code review

Co-authored-by: Marti <marcin.gorny.94@protonmail.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Marti <marcin.gorny.94@protonmail.com>
Co-authored-by: Marti <marti@miden.team>
(cherry picked from commit 9c9f847)
…o-next-sync

# Conflicts:
#	crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm
…o-next-sync

# Conflicts:
#	crates/miden-agglayer/src/faucet.rs
#	crates/miden-testing/tests/agglayer/bridge_out.rs
Comment on lines +65 to +66
/// URI + external link). Conversion metadata is no longer stored on the faucet; the bridge holds
/// it in `faucet_metadata_map`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this comment should be adjusted to avoid documenting the changes specific to this PR.

I believe it would have been avoided had we already had e047a3a merged

Comment on lines +283 to +284
// mutability + description + logo URI + external link). Conversion metadata lives on
// the bridge, so the faucet adds no bridge-specific slots.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ditto

///
/// The builder includes:
/// - The `AggLayerFaucet` component (conversion metadata + token metadata).
/// - The `AggLayerFaucet` component (token metadata only; conversion metadata lives on the bridge).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here (this comment was cherry-picked verbatim, so it looks like we missed this comment on the original PR)

Comment on lines 198 to 207
// For mainnet and rollup fixtures, create the destination account so we can consume the P2ID
// note. The mock account is built from the destination ID encoded in the JSON test vector,
// since the claim note targets this account ID.
let destination_account =
if matches!(data_source, ClaimDataSource::L1ToMiden | ClaimDataSource::L2ToMiden) {
let dest = Account::mock(u128::from(destination_account_id), IncrNonceAuthComponent);
builder.add_account(dest.clone())?;
Some(dest)
} else {
None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the way we construct the destination account changed in the original PR, and it's not reflected here. Specifically, we no longer match on the data source, and instead always create a deterministic account

@mmagician mmagician force-pushed the ajl-claude/agglayer-to-next-sync branch from fb2341b to a8732d1 Compare May 20, 2026 08:34
@mmagician mmagician merged commit c0f60fd into next May 20, 2026
20 checks passed
@mmagician mmagician deleted the ajl-claude/agglayer-to-next-sync branch May 20, 2026 09:05
@mmagician
Copy link
Copy Markdown
Collaborator

Since these changes were supposed to be merged w/o squashing, I rebased the commits (squash the fmt commit into the last merge commit) and force pushed, to end up with a clean git history on next.

mmagician pushed a commit that referenced this pull request May 20, 2026
Brings the agglayer lock/unlock port (#2955) and other recent next
changes (Account renames, AssetAmount, AccountComponentName) into the
MINT-faucet-bind branch.

Conflict resolution: MINT note construction has moved out of
bridge_in.masm into bridge_in_output.masm on next. The PR's 22-felt
MINT layout (ASSET_KEY + ASSET_VALUE for faucet binding) and the
associated write_mint_note_storage / build_mint_recipient procedures
have been transplanted onto bridge_in_output.masm.

The cross-faucet regression test (test_mint_cannot_be_consumed_by_unrelated_faucet)
and the AuthNetworkAccount regression tests have been updated to the
new ConfigAggBridgeNote::create + create_existing_agglayer_faucet
signatures (ConversionMetadata struct, no per-faucet origin token /
network / scale / metadata_hash args).

make lint and the full make test suite (1084 tests) pass locally.
mmagician pushed a commit that referenced this pull request May 20, 2026
Brings the agglayer lock/unlock port (#2955) and other recent next
changes (Account renames, AssetAmount, AccountComponentName) into the
MINT-faucet-bind branch.

Conflict resolution: MINT note construction has moved out of
bridge_in.masm into bridge_in_output.masm on next. The PR's 22-felt
MINT layout (ASSET_KEY + ASSET_VALUE for faucet binding) and the
associated write_mint_note_storage / build_mint_recipient procedures
have been transplanted onto bridge_in_output.masm.

The cross-faucet regression test (test_mint_cannot_be_consumed_by_unrelated_faucet)
and the AuthNetworkAccount regression tests have been updated to the
new ConfigAggBridgeNote::create + create_existing_agglayer_faucet
signatures (ConversionMetadata struct, no per-faucet origin token /
network / scale / metadata_hash args).

make lint and the full make test suite (1084 tests) pass locally.

fixup: include MINT-bind merge fixes that were not staged

These three files were edited during the merge but never re-staged
before the merge commit (d843c4e), so the pushed commit contained
the auto-merged state without the manual fixups. CI failed because:

- bridge_in_output.masm was missing the 22-felt MINT layout
  (ASSET_KEY/ASSET_VALUE for faucet binding); kept the old 14-felt
  shape.
- bridge_in.rs's cross-faucet regression test still called the old
  10-arg create_existing_agglayer_faucet and the old 5-arg
  ConfigAggBridgeNote::create.
- network_account_regression.rs still called the old 10-arg
  create_existing_agglayer_faucet.

cargo clippy --workspace --all-targets --all-features -- -D warnings
and cargo check --workspace --all-targets --all-features are clean
locally after this commit; targeted runs of agglayer::bridge_in::*
and agglayer::network_account_regression::* tests pass.
onurinanc pushed a commit to onurinanc/miden-base that referenced this pull request May 21, 2026
* docs(agglayer): drop port-narrative phrases from faucet docstrings

The "conversion metadata lives on the bridge" clause was useful for the
PR 0xMiden#2771 / 0xMiden#2955 port narrative but is not load-bearing for readers of
the merged code. Trim it from the AggLayerFaucet docstring in lib.rs and
the inline comment in faucet.rs.

* test(agglayer): always construct deterministic destination account

The conditional matches! arm in test_bridge_in_claim_to_p2id enumerates
both ClaimDataSource variants (L1ToMiden | L2ToMiden) — the only two —
so the branch is always taken and the Option<Account> wrapping forces a
needless if let Some(...) around the post-mint consume+assert block.

Construct destination_account unconditionally and flatten the consume
block to match the pattern used by the other bridge_in tests.

---------

Co-authored-by: Claude (Opus) <noreply@anthropic.com>
Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com>
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.

3 participants