Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 12 additions & 5 deletions crates/miden-agglayer/asm/bridge/bridge_in.masm
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use miden::agglayer::crypto_utils
use miden::agglayer::utils
use miden::core::crypto::hashes::keccak256
use miden::core::mem
use miden::protocol::active_account
Expand Down Expand Up @@ -122,6 +123,9 @@ end

#! Assert the global index is valid for a mainnet deposit.
#!
#! Each element of the global index is a LE-packed u32 felt (as produced by
#! `bytes_to_packed_u32_felts` / `GlobalIndex::to_elements()`).
#!
#! Inputs: [GLOBAL_INDEX[8]]
#! Outputs: [leaf_index]
#!
Expand All @@ -136,15 +140,18 @@ pub proc process_global_index_mainnet
# the top 191 bits of the global index are zero
repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end

# the next element is a u32 mainnet flag bit
# enforce that this limb is one
# => [mainnet_flag, GLOBAL_INDEX[6..8], LEAF_VALUE[8]]
# the next element is the mainnet flag (LE-packed u32)
# byte-swap to get the BE value, then assert it is exactly 1
# => [mainnet_flag_le, rollup_index_le, leaf_index_le, ...]
exec.utils::swap_u32_bytes
assert.err=ERR_BRIDGE_NOT_MAINNET

# the next element is a u32 rollup index, must be zero for a mainnet deposit
# the next element is the rollup index, must be zero for a mainnet deposit
# (zero is byte-order-independent, so no swap needed)
assertz.err=ERR_ROLLUP_INDEX_NON_ZERO

# finally, the leaf index = lowest 32 bits = last limb
# the leaf index is the last element; byte-swap from LE to BE to get the actual index
exec.utils::swap_u32_bytes
# => [leaf_index]
end

Expand Down
30 changes: 3 additions & 27 deletions crates/miden-agglayer/asm/bridge/eth_address.masm
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use miden::agglayer::utils
use miden::core::crypto::hashes::keccak256
use miden::core::word

Expand All @@ -14,31 +15,6 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field"
# ETHEREUM ADDRESS PROCEDURES
# =================================================================================================

#! Swaps byte order in a u32 limb (LE <-> BE).
#!
#! Inputs: [value]
#! Outputs: [swapped]
proc swap_u32_bytes
# part0 = (value & 0xFF) << 24
dup u32and.0xFF u32shl.24
# => [value, part0]

# part1 = ((value >> 8) & 0xFF) << 16
dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or
# => [value, part01]

# part2 = ((value >> 16) & 0xFF) << 8
dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or
# => [value, part012]

# part3 = (value >> 24)
dup.1 u32shr.24 u32or
# => [value, swapped]

swap drop
# => [swapped]
end

#! Builds a single felt from two u32 limbs (little-endian limb order).
#! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element.
#! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping
Expand All @@ -52,9 +28,9 @@ proc build_felt
# => [lo_be, hi_be]

# limbs are little-endian bytes; swap to big-endian for building account ID
exec.swap_u32_bytes
exec.utils::swap_u32_bytes
swap
exec.swap_u32_bytes
exec.utils::swap_u32_bytes
swap
# => [lo, hi]

Expand Down
32 changes: 30 additions & 2 deletions crates/miden-agglayer/asm/bridge/utils.masm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Utility module containing helper procedures for the double word handling.
# Utility module containing helper procedures for double word handling and byte manipulation.

# TYPE ALIASES
# =================================================================================================
Expand All @@ -7,7 +7,35 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
type DoubleWord = struct { word_lo: BeWord, word_hi: BeWord }
type MemoryAddress = u32

# PUBLIC INTERFACE
# BYTE MANIPULATION
# =================================================================================================

#! Swaps byte order in a u32 limb (LE <-> BE).
#!
#! Inputs: [value]
#! Outputs: [swapped]
pub proc swap_u32_bytes
# part0 = (value & 0xFF) << 24
dup u32and.0xFF u32shl.24
# => [part0, value]

# part1 = ((value >> 8) & 0xFF) << 16
dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or
# => [part01, value]

# part2 = ((value >> 16) & 0xFF) << 8
dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or
# => [part012, value]

# part3 = (value >> 24)
dup.1 u32shr.24 u32or
# => [swapped, value]

swap drop
# => [swapped]
end

# DOUBLE WORD MEMORY OPERATIONS
# =================================================================================================

#! Stores two words to the provided global memory address.
Expand Down
10 changes: 5 additions & 5 deletions crates/miden-agglayer/src/claim_note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use miden_protocol::note::{
};
use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint};

use crate::{EthAddressFormat, EthAmount, MetadataHash, claim_script};
use crate::{EthAddressFormat, EthAmount, GlobalIndex, MetadataHash, claim_script};

// CLAIM NOTE STRUCTURES
// ================================================================================================
Expand Down Expand Up @@ -66,8 +66,8 @@ pub struct ProofData {
pub smt_proof_local_exit_root: [SmtNode; 32],
/// SMT proof for rollup exit root (32 SMT nodes)
pub smt_proof_rollup_exit_root: [SmtNode; 32],
/// Global index (uint256 as 8 u32 values)
pub global_index: [u32; 8],
/// Global index (uint256 as 32 bytes)
pub global_index: GlobalIndex,
/// Mainnet exit root hash
pub mainnet_exit_root: ExitRoot,
/// Rollup exit root hash
Expand All @@ -92,8 +92,8 @@ impl SequentialCommit for ProofData {
elements.extend(node_felts);
}

// Global index (uint256 as 8 u32 felts)
elements.extend(self.global_index.iter().map(|&v| Felt::new(v as u64)));
// Global index (uint256 as 32 bytes)
elements.extend(self.global_index.to_elements());

// Mainnet exit root (bytes32 as 8 u32 felts)
let mainnet_exit_root_felts = self.mainnet_exit_root.to_elements();
Expand Down
154 changes: 154 additions & 0 deletions crates/miden-agglayer/src/eth_types/global_index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use alloc::vec::Vec;

use miden_core_lib::handlers::bytes_to_packed_u32_felts;
use miden_protocol::Felt;
use miden_protocol::utils::{HexParseError, hex_to_bytes};

// ================================================================================================
// GLOBAL INDEX ERROR
// ================================================================================================

/// Error type for GlobalIndex validation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GlobalIndexError {
/// The leading 160 bits of the global index are not zero.
LeadingBitsNonZero,
/// The mainnet flag is not 1.
InvalidMainnetFlag,
/// The rollup index is not zero for a mainnet deposit.
RollupIndexNonZero,
}

// ================================================================================================
// GLOBAL INDEX
// ================================================================================================

/// Represents an AggLayer global index as a 256-bit value (32 bytes).
///
/// The global index is a uint256 that encodes (from MSB to LSB):
/// - Top 160 bits (limbs 0-4): must be zero
/// - 32 bits (limb 5): mainnet flag (value = 1 for mainnet, 0 for rollup)
/// - 32 bits (limb 6): rollup index (must be 0 for mainnet deposits)
/// - 32 bits (limb 7): leaf index (deposit index in the local exit tree)
///
/// Bytes are stored in big-endian order, matching Solidity's uint256 representation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlobalIndex([u8; 32]);
Comment on lines +22 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I can follow this same type of layout for implementing theu256 type for the u256 to Felt scaling PR: #2331


impl GlobalIndex {
/// Creates a [`GlobalIndex`] from a hex string (with or without "0x" prefix).
///
/// The hex string should represent a Solidity uint256 in big-endian format
/// (64 hex characters for 32 bytes).
pub fn from_hex(hex_str: &str) -> Result<Self, HexParseError> {
let bytes: [u8; 32] = hex_to_bytes(hex_str)?;
Ok(Self(bytes))
}
Comment on lines +43 to +46
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we do more validation here? We could return GlobalIndexError and hex parsing could be one reason to return an error. Other reasons would be if the global index is malformed.

The general idea is to enforce that if we have GlobalIndex struct, we know the underlying index is correctly formed.


/// Creates a new [`GlobalIndex`] from a 32-byte array (big-endian).
pub fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}

/// Validates that this is a valid mainnet deposit global index.
///
/// Checks that:
/// - The top 160 bits (limbs 0-4, bytes 0-19) are zero
/// - The mainnet flag (limb 5, bytes 20-23) is exactly 1
/// - The rollup index (limb 6, bytes 24-27) is 0
pub fn validate_mainnet(&self) -> Result<(), GlobalIndexError> {
Comment on lines +48 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to change this into a constructor - e.g., something like:

pub fn new_mainnet(bytes: [u8; 32]) -> Result<Self, GlobalIndexError> {
    ...
}

// Check limbs 0-4 are zero (bytes 0-19)
if self.0[0..20].iter().any(|&b| b != 0) {
return Err(GlobalIndexError::LeadingBitsNonZero);
}

// Check mainnet flag limb (bytes 20-23) is exactly 1
if !self.is_mainnet() {
return Err(GlobalIndexError::InvalidMainnetFlag);
}

// Check rollup index is zero (bytes 24-27)
if u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) != 0 {
return Err(GlobalIndexError::RollupIndexNonZero);
}

Ok(())
}

/// Returns the leaf index (limb 7, lowest 32 bits).
pub fn leaf_index(&self) -> u32 {
u32::from_be_bytes([self.0[28], self.0[29], self.0[30], self.0[31]])
}

/// Returns the rollup index (limb 6).
pub fn rollup_index(&self) -> u32 {
u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]])
}

/// Returns true if this is a mainnet deposit (mainnet flag = 1).
pub fn is_mainnet(&self) -> bool {
u32::from_be_bytes([self.0[20], self.0[21], self.0[22], self.0[23]]) == 1
}

/// Converts to field elements for note storage / MASM processing.
pub fn to_elements(&self) -> Vec<Felt> {
bytes_to_packed_u32_felts(&self.0)
}

/// Returns the raw 32-byte array (big-endian).
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}

#[cfg(test)]
mod tests {
use miden_core::FieldElement;

use super::*;

#[test]
fn test_mainnet_global_indices_from_production() {
// Real mainnet global indices from production
// Format: (1 << 64) + leaf_index for mainnet deposits
// 18446744073709786619 = 0x1_0000_0000_0003_95FB (leaf_index = 235003)
// 18446744073709786590 = 0x1_0000_0000_0003_95DE (leaf_index = 234974)
let test_cases = [
("0x00000000000000000000000000000000000000000000000100000000000395fb", 235003u32),
("0x00000000000000000000000000000000000000000000000100000000000395de", 234974u32),
];

for (hex, expected_leaf_index) in test_cases {
let gi = GlobalIndex::from_hex(hex).expect("valid hex");

// Validate as mainnet
assert!(gi.validate_mainnet().is_ok(), "should be valid mainnet global index");

// Construction sanity checks
assert!(gi.is_mainnet());
assert_eq!(gi.rollup_index(), 0);
assert_eq!(gi.leaf_index(), expected_leaf_index);

// Verify to_elements produces correct LE-packed u32 felts
// --------------------------------------------------------------------------------

let elements = gi.to_elements();
assert_eq!(elements.len(), 8);

// leading zeros
assert_eq!(elements[0..5], [Felt::ZERO; 5]);

// mainnet flag: BE value 1 → LE-packed as 0x01000000
assert_eq!(elements[5], Felt::new(u32::from_le_bytes(1u32.to_be_bytes()) as u64));

// rollup index
assert_eq!(elements[6], Felt::ZERO);

// leaf index: BE value → LE-packed
assert_eq!(
elements[7],
Felt::new(u32::from_le_bytes(expected_leaf_index.to_be_bytes()) as u64)
);
}
}
}
2 changes: 2 additions & 0 deletions crates/miden-agglayer/src/eth_types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod address;
pub mod amount;
pub mod global_index;
pub mod metadata_hash;

pub use address::EthAddressFormat;
pub use amount::EthAmount;
pub use global_index::{GlobalIndex, GlobalIndexError};
pub use metadata_hash::MetadataHash;
2 changes: 1 addition & 1 deletion crates/miden-agglayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub use claim_note::{
SmtNode,
create_claim_note,
};
pub use eth_types::{EthAddressFormat, EthAmount, MetadataHash};
pub use eth_types::{EthAddressFormat, EthAmount, GlobalIndex, GlobalIndexError, MetadataHash};
pub use update_ger_note::UpdateGerNote;

// AGGLAYER NOTE SCRIPTS
Expand Down
3 changes: 2 additions & 1 deletion crates/miden-testing/tests/agglayer/bridge_in.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use miden_agglayer::{
ClaimNoteStorage,
EthAddressFormat,
EthAmount,
GlobalIndex,
LeafData,
MetadataHash,
OutputNoteData,
Expand Down Expand Up @@ -121,7 +122,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> {
let proof_data = ProofData {
smt_proof_local_exit_root: local_proof_array,
smt_proof_rollup_exit_root: rollup_proof_array,
global_index,
global_index: GlobalIndex::new(global_index),
mainnet_exit_root: ExitRoot::from(mainnet_exit_root),
rollup_exit_root: ExitRoot::from(rollup_exit_root),
};
Expand Down
Loading