Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions bin/bench-transaction/src/context_setups.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
pub use miden_agglayer::testing::ClaimDataSource;
use miden_agglayer::{
AggLayerBridge,
B2AggNote,
ClaimNoteStorage,
ConfigAggBridgeNote,
Expand All @@ -11,13 +12,14 @@ use miden_agglayer::{
create_existing_agglayer_faucet,
create_existing_bridge_account,
};
use miden_protocol::Felt;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{Account, StorageMapKey};
use miden_protocol::asset::{Asset, FungibleAsset};
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::note::{NoteAssets, NoteType};
use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER;
use miden_protocol::transaction::RawOutputNote;
use miden_protocol::{Felt, Word};
use miden_standards::code_builder::CodeBuilder;
use miden_standards::note::StandardNote;
use miden_testing::{Auth, MockChain, TransactionContext};
Expand Down Expand Up @@ -292,16 +294,75 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result<Trans
// B2AGG NOTE SETUPS
// ================================================================================================

/// Pre-populates the bridge account's LET (Local Exit Tree) frontier with dummy values,
/// simulating a tree that already has `num_leaves` entries.
///
/// This allows benchmarking bridge-out with different frontier occupancy levels without
/// performing actual sequential insertions. The frontier values are deterministic but not
/// cryptographically valid - cycle counts are independent of stored values.
fn populate_let_frontier(bridge: &mut Account, num_leaves: u32) {
let zero = Felt::ZERO;

// Set num_leaves
bridge
.storage_mut()
.set_item(
AggLayerBridge::let_num_leaves_slot_name(),
Word::new([Felt::new(num_leaves as u64), zero, zero, zero]),
)
.expect("should set LET num_leaves");

// Populate all 32 frontier double-word entries with dummy values.
// The double_word_array stores each entry under two map keys:
// Word 0: key [h, 0, 0, 0]
// Word 1: key [h, 1, 0, 0]
for h in 0u32..32 {
let key0 = StorageMapKey::from_array([h, 0, 0, 0]);
let val0 = Word::new([Felt::new(h as u64 + 1), Felt::new(2), Felt::new(3), Felt::new(4)]);
bridge
.storage_mut()
.set_map_item(AggLayerBridge::let_frontier_slot_name(), key0, val0)
.expect("should set frontier word 0");

let key1 = StorageMapKey::from_array([h, 1, 0, 0]);
let val1 = Word::new([Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(h as u64 + 8)]);
bridge
.storage_mut()
.set_map_item(AggLayerBridge::let_frontier_slot_name(), key1, val1)
.expect("should set frontier word 1");
}

// Set dummy root values (not used by the append logic, but stored for completeness)
bridge
.storage_mut()
.set_item(
AggLayerBridge::let_root_lo_slot_name(),
Word::new([Felt::new(0xdead), zero, zero, zero]),
)
.expect("should set LET root lo");
bridge
.storage_mut()
.set_item(
AggLayerBridge::let_root_hi_slot_name(),
Word::new([Felt::new(0xbeef), zero, zero, zero]),
)
.expect("should set LET root hi");
}

/// Sets up and returns the transaction context for executing a B2AGG (bridge-out) note against
/// the bridge account.
///
/// This requires executing a prerequisite CONFIG_AGG_BRIDGE transaction during setup to register
/// the faucet in the bridge. Only the returned B2AGG transaction context is benchmarked — the
/// prerequisite CONFIG_AGG_BRIDGE transaction is not included in cycle/time measurements.
///
/// When `pre_populate_leaves` is `Some(n)`, the bridge account's LET frontier is pre-populated
/// with dummy values for `n` leaves before building the B2AGG transaction context. This allows
/// benchmarking with different frontier occupancy levels.
///
/// The setup uses the first entry from the MTF (Merkle Tree Frontier) test vectors for destination
/// data.
pub async fn tx_consume_b2agg_note() -> Result<TransactionContext> {
pub async fn tx_consume_b2agg_note(pre_populate_leaves: Option<u32>) -> Result<TransactionContext> {
let vectors = &*miden_agglayer::testing::SOLIDITY_MTF_VECTORS;

let mut builder = MockChain::builder();
Expand All @@ -317,11 +378,17 @@ pub async fn tx_consume_b2agg_note() -> Result<TransactionContext> {
})?;

// CREATE BRIDGE ACCOUNT
let bridge_account = create_existing_bridge_account(
let mut bridge_account = create_existing_bridge_account(
builder.rng_mut().draw_word(),
bridge_admin.id(),
ger_manager.id(),
);

// Pre-populate frontier before adding the account to the mock chain
if let Some(num_leaves) = pre_populate_leaves {
populate_let_frontier(&mut bridge_account, num_leaves);
}

builder.add_account(bridge_account.clone())?;

// CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI)
Expand Down
8 changes: 8 additions & 0 deletions bin/bench-transaction/src/cycle_counting_benchmarks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum ExecutionBenchmark {
ConsumeClaimNoteL1ToMiden,
ConsumeClaimNoteL2ToMiden,
ConsumeB2AggNote,
ConsumeB2AggNotePopulated2p31,
ConsumeB2AggNotePopulated2p31m1,
}

impl fmt::Display for ExecutionBenchmark {
Expand All @@ -27,6 +29,12 @@ impl fmt::Display for ExecutionBenchmark {
ExecutionBenchmark::ConsumeB2AggNote => {
write!(f, "consume B2AGG note (bridge-out)")
},
ExecutionBenchmark::ConsumeB2AggNotePopulated2p31 => {
write!(f, "consume B2AGG note (bridge-out, 2^31 leaves)")
},
ExecutionBenchmark::ConsumeB2AggNotePopulated2p31m1 => {
write!(f, "consume B2AGG note (bridge-out, 2^31-1 leaves)")
},
}
}
}
20 changes: 19 additions & 1 deletion bin/bench-transaction/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,25 @@ async fn main() -> Result<()> {
),
(
ExecutionBenchmark::ConsumeB2AggNote,
tx_consume_b2agg_note()
tx_consume_b2agg_note(None)
.await?
.execute()
.await
.map(TransactionMeasurements::from)?
.into(),
),
(
ExecutionBenchmark::ConsumeB2AggNotePopulated2p31,
tx_consume_b2agg_note(Some(1 << 31))
.await?
.execute()
.await
.map(TransactionMeasurements::from)?
.into(),
),
(
ExecutionBenchmark::ConsumeB2AggNotePopulated2p31m1,
tx_consume_b2agg_note(Some((1u32 << 31) - 1))
.await?
.execute()
.await
Expand Down
4 changes: 2 additions & 2 deletions bin/bench-transaction/src/time_counting_benchmarks/prove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ fn core_benchmarks(c: &mut Criterion) {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("failed to build tokio runtime for setup");
rt.block_on(tx_consume_b2agg_note())
rt.block_on(tx_consume_b2agg_note(None))
.expect("failed to create a context which consumes B2AGG note")
},
|tx_context| async move {
Expand Down Expand Up @@ -264,7 +264,7 @@ fn core_benchmarks(c: &mut Criterion) {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("failed to build tokio runtime for setup");
rt.block_on(tx_consume_b2agg_note())
rt.block_on(tx_consume_b2agg_note(None))
.expect("failed to create a context which consumes B2AGG note")
},
|tx_context| async move {
Expand Down
132 changes: 80 additions & 52 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress)
exec.leaf_utils::compute_leaf_value
# => [LEAF_VALUE_LO, LEAF_VALUE_HI]

# Load the LET frontier from storage into memory at LET_FRONTIER_MEM_PTR
exec.load_let_frontier_to_memory
# Load num_leaves and only the frontier entries that will be read (bit h=1)
exec.load_let_frontier_selective
# => [LEAF_VALUE_LO, LEAF_VALUE_HI]

# Push frontier pointer below the leaf value
Expand All @@ -316,58 +316,71 @@ proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress)
exec.save_let_root_and_num_leaves
# => []

# Write the updated frontier from memory back to the map
exec.save_let_frontier_to_storage
# Write only the frontier entries that were modified (bit h=0 in original num_leaves)
exec.save_let_frontier_selective
# => []
end

#! Loads the LET (Local Exit Tree) frontier from account storage into memory.
#! Selectively loads the LET (Local Exit Tree) frontier from account storage into memory.
#!
#! The num_leaves is read from its dedicated value slot, and the 32 frontier entries are read from
#! the LET map slot (double-word array, indices 0..31). The data is placed into memory at
#! LET_FRONTIER_MEM_PTR, matching the layout expected by append_and_update_frontier:
#! [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]]
#! First loads num_leaves from its dedicated value slot. Then, based on the bit pattern of
#! num_leaves, loads only the frontier entries that will be READ by append_and_update_frontier
#! (those at heights where the corresponding bit in num_leaves is 1).
#!
#! Entries at heights where the bit is 0 are not loaded because they will be overwritten by
#! append_and_update_frontier before being read. Uninitialized memory defaults to zero, which
#! is safe since these entries are written before use.
#!
#! Empty (uninitialized) map entries return zeros, which is the correct initial state for the
#! frontier when there are no leaves.
#! The data is placed into memory at LET_FRONTIER_MEM_PTR, matching the layout expected by
#! append_and_update_frontier:
#! [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]]
#!
#! Inputs: []
#! Outputs: []
#!
#! Invocation: exec
proc load_let_frontier_to_memory
# 1. Load num_leaves from its value slot
proc load_let_frontier_selective
# 1. Load num_leaves from its value slot and store to memory
push.LET_NUM_LEAVES_SLOT[0..2]
exec.active_account::get_item
# => [num_leaves_word]
# => [num_leaves, 0, 0, 0]
Comment thread
Fumuran marked this conversation as resolved.

push.LET_FRONTIER_MEM_PTR mem_storew_le dropw
# => []

# 2. Load 32 frontier double-word entries from the map via double_word_array::get
# 2. Reload num_leaves for bit checking
push.LET_FRONTIER_MEM_PTR mem_load
# => [num_leaves]

push.0
# => [h=0]
# => [h=0, num_leaves]

repeat.32
# => [h]

# Read frontier[h] as a double word from the map
dup push.LET_FRONTIER_SLOT[0..2]
exec.double_word_array::get
# => [VALUE_0, VALUE_1, h]

# Compute memory address and store the double word
dup.8 mul.8 add.LET_FRONTIER_MEM_PTR add.4 movdn.8
# => [VALUE_0, VALUE_1, mem_addr, h]
exec.utils::mem_store_double_word
dropw dropw drop
# => [h]

# => [h, num_leaves_shifted]

# Check if bit h is set (this entry will be read by append_and_update_frontier)
dup.1 u32and.1
if.true
# Load frontier[h] from storage into memory
dup push.LET_FRONTIER_SLOT[0..2]
exec.double_word_array::get
# => [VALUE_0, VALUE_1, h, num_leaves_shifted]

# Compute memory address: LET_FRONTIER_MEM_PTR + 4 + h * 8
dup.8 mul.8 add.LET_FRONTIER_MEM_PTR add.4 movdn.8
# => [VALUE_0, VALUE_1, mem_addr, h, num_leaves_shifted]
Comment thread
Fumuran marked this conversation as resolved.
exec.utils::mem_store_double_word
dropw dropw drop
# => [h, num_leaves_shifted]
end

# Shift num_leaves right by 1, increment h
swap u32shr.1 swap
add.1
Comment thread
Fumuran marked this conversation as resolved.
Outdated
# => [h+1]
# => [h+1, num_leaves_shifted>>1]
end

drop
drop drop
# => []
end

Expand Down Expand Up @@ -400,38 +413,53 @@ proc save_let_root_and_num_leaves
# => []
end

#! Writes the 32 frontier entries from memory back to the LET map slot.
#! Selectively writes modified frontier entries from memory back to the LET map slot.
#!
#! Only entries that were WRITTEN by append_and_update_frontier are saved back to storage.
#! These are the entries at heights where the corresponding bit in the original num_leaves
#! (before increment) was 0. The original num_leaves is recovered as new_num_leaves - 1.
#!
#! Each frontier entry is a double word (Keccak256 digest) stored at
#! LET_FRONTIER_MEM_PTR + 4 + h * 8, and is written to the map at double_word_array index h.
#! Entries at heights where the bit was 1 were only READ (not modified) by
#! append_and_update_frontier, so they don't need to be saved back.
#!
#! Inputs: []
#! Outputs: []
#!
#! Invocation: exec
proc save_let_frontier_to_storage
proc save_let_frontier_selective
# Get old_num_leaves = new_num_leaves - 1
# (append_and_update_frontier stored new_num_leaves at LET_FRONTIER_MEM_PTR)
push.LET_FRONTIER_MEM_PTR mem_load sub.1
# => [old_num_leaves]

push.0
# => [h=0]
# => [h=0, old_num_leaves]

repeat.32
# => [h]

# Load frontier[h] double word from memory
dup mul.8 add.LET_FRONTIER_MEM_PTR add.4
exec.utils::mem_load_double_word
# => [VALUE_0, VALUE_1, h]

# Write it back to the map at index h
dup.8 push.LET_FRONTIER_SLOT[0..2]
exec.double_word_array::set
dropw dropw
# => [h]

# => [h, old_num_leaves_shifted]

# Check if bit h is clear (this entry was written and needs saving)
dup.1 u32and.1 eq.0
if.true
Comment thread
Fumuran marked this conversation as resolved.
Outdated
# Load frontier[h] double word from memory
dup mul.8 add.LET_FRONTIER_MEM_PTR add.4
exec.utils::mem_load_double_word
# => [VALUE_0, VALUE_1, h, old_num_leaves_shifted]

# Write it back to the map at index h
dup.8 push.LET_FRONTIER_SLOT[0..2]
exec.double_word_array::set
Comment thread
Fumuran marked this conversation as resolved.
dropw dropw
# => [h, old_num_leaves_shifted]
end

# Shift old_num_leaves right by 1, increment h
swap u32shr.1 swap
add.1
Comment thread
Fumuran marked this conversation as resolved.
Outdated
# => [h+1]
# => [h+1, old_num_leaves_shifted>>1]
end

drop
drop drop
# => []
end

Expand Down
Loading