Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Introduced standard `NetworkAccountTarget` attachment for use in network transactions which replaces `NoteTag::NetworkAccount` ([#2257](https://github.com/0xMiden/miden-base/pull/2257)).
- Added an `AccountBuilder` extension trait to help build the schema commitment; added `AccountComponentMetadata` to `AccountComponent` ([#2269](https://github.com/0xMiden/miden-base/pull/2269)).
- Added `miden::standards::access::ownable` standard module for component ownership management, and integrated it into the `network_fungible` faucet (including new tests). ([#2228](https://github.com/0xMiden/miden-base/pull/2228)).
- Added single-word `Array` data structure utility ([#2203](https://github.com/0xMiden/miden-base/pull/2203)).

### Changes

Expand Down
78 changes: 78 additions & 0 deletions crates/miden-standards/asm/standards/data_structures/array.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# The MASM code for the Array abstraction.
#
# It provides an abstraction layer over a storage map, treating it as an array,
# with "set" and "get" for storing and retrieving words by (slot_id, index).
# The array can store up to 2^64 - 2^32 + 1 elements (indices 0 to 2^64 - 2^32).
#
# Using this Array utility requires that the underlying storage map is already created and
# initialized as part of an account component, under the given slot ID.

use miden::protocol::active_account
use miden::protocol::native_account

type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }

# PROCEDURES
# =================================================================================================

#! Sets a word in the array at the specified index.
#!
#! Inputs: [slot_id_prefix, slot_id_suffix, index, VALUE, pad(9)]
#! Outputs: [OLD_VALUE, pad(12)]
#!
#! Where:
#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier.
#! - index is the index at which to store the value (0 to 2^64 - 2^32).
#! - VALUE is the word to store at the specified index.
#!
#! Invocation: call
pub proc set(slot_id_prefix: felt, slot_id_suffix: felt, index: felt, value: BeWord) -> BeWord
# Build KEY = [index, 0, 0, 0]
push.0.0.0 movup.5
# => [index, 0, 0, 0, slot_id_prefix, slot_id_suffix, VALUE, pad(9)]

# truncate the stack
movup.10 drop
movup.10 drop
movup.10 drop
# => [index, 0, 0, 0, slot_id_prefix, slot_id_suffix, VALUE, pad(6)]

movup.5 movup.5
# => [slot_id_prefix, slot_id_suffix, KEY, VALUE, pad(6)]

exec.native_account::set_map_item
# => [OLD_VALUE, pad(12)]
end

#! Gets a word from the array at the specified index.
#!
#! Inputs: [slot_id_prefix, slot_id_suffix, index, pad(13)]
#! Outputs: [VALUE, pad(12)]
#!
#! Where:
#! - slot_id_{prefix, suffix} are the prefix and suffix felts of the slot identifier.
#! - index is the index of the element to retrieve (0 to 2^64 - 2^32).
#! - VALUE is the word stored at the specified index (zero if not set).
#!
#! Invocation: call
pub proc get(slot_id_prefix: felt, slot_id_suffix: felt, index: felt) -> BeWord
# Build KEY = [index, 0, 0, 0]
push.0.0.0 movup.5
# => [index, 0, 0, 0, slot_id_prefix, slot_id_suffix, pad(13)]

# truncate the stack
movup.6 drop
movup.6 drop
movup.6 drop
# => [index, 0, 0, 0, slot_id_prefix, slot_id_suffix, pad(10)]

movup.5 movup.5
# => [slot_id_prefix, slot_id_suffix, KEY, pad(10)]

exec.active_account::get_map_item
# => [VALUE, pad(15)]

# truncate the stack
repeat.3 movup.4 drop end
# => [VALUE, pad(12)]
end
1 change: 1 addition & 0 deletions crates/miden-testing/src/kernel_tests/tx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod test_account;
mod test_account_delta;
mod test_account_interface;
mod test_active_note;
mod test_array;
mod test_asset;
mod test_asset_vault;
mod test_auth;
Expand Down
135 changes: 135 additions & 0 deletions crates/miden-testing/src/kernel_tests/tx/test_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! Tests for the Array utility `get` and `set` procedures.

use miden_protocol::account::{
AccountBuilder,
AccountComponent,
StorageMap,
StorageSlot,
StorageSlotName,
};
use miden_protocol::{Felt, FieldElement, Word};
use miden_standards::code_builder::CodeBuilder;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;

use crate::{Auth, TransactionContextBuilder};

/// The slot name used for testing the array component.
const TEST_ARRAY_SLOT: &str = "test::array::data";

/// Verify that, given an account component with a storage map to hold the array data,
/// we can use the array utility to:
/// 1. Retrieve the initial value via `get`
/// 2. Update the value via `set`
/// 3. Retrieve the updated value via `get`
#[tokio::test]
async fn test_array_get_and_set() -> anyhow::Result<()> {
let slot_name = StorageSlotName::new(TEST_ARRAY_SLOT).expect("slot name should be valid");

let wrapper_component_code = format!(
r#"
use miden::core::word
use miden::standards::data_structures::array

const ARRAY_SLOT_NAME = word("{slot_name}")

#! Wrapper for array::get that uses exec internally.
#! Inputs: [index, pad(15)]
#! Outputs: [VALUE, pad(12)]
pub proc test_get
push.ARRAY_SLOT_NAME[0..2]
exec.array::get
end

#! Wrapper for array::set that uses exec internally.
#! Inputs: [index, VALUE, pad(11)]
#! Outputs: [OLD_VALUE, pad(12)]
pub proc test_set
push.ARRAY_SLOT_NAME[0..2]
exec.array::set
end
"#,
);

// Build the wrapper component by linking against the array library
let wrapper_library = CodeBuilder::default()
.compile_component_code("wrapper::component", wrapper_component_code)?;

// Create the wrapper account component with a storage map to hold the array data
let initial_value = Word::from([42u32, 42, 42, 42]);
let wrapper_component = AccountComponent::new(
wrapper_library.clone(),
vec![StorageSlot::with_map(
slot_name.clone(),
StorageMap::with_entries([(
Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]),
initial_value,
)])?,
)],
)?
.with_supports_all_types();

// Build an account with the wrapper component that uses the array utility
let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random())
.with_auth_component(Auth::IncrNonce)
.with_component(wrapper_component)
.build_existing()?;

// Verify the storage slot exists
assert!(
account.storage().get(&slot_name).is_some(),
"Array data slot should exist in account storage"
);

// Transaction script that:
// 1. Gets the initial value at index 0 (should be [42, 42, 42, 42])
// 2. Sets index 0 to [43, 43, 43, 43]
// 3. Gets the updated value at index 0 (should be [43, 43, 43, 43])
let tx_script_code = r#"
use wrapper::component->wrapper

begin
# Step 1: Get value at index 0 (should return [42, 42, 42, 42])
push.0
# => [index, pad(16)]
call.wrapper::test_get
# => [VALUE, pad(13)]

# Verify value is [42, 42, 42, 42]
push.42.42.42.42
assert_eqw.err="get(0) should return [42, 42, 42, 42] initially"
# => [pad(16)] (auto-padding)

# Step 2: Set value at index 0 to [43, 43, 43, 43]
push.43.43.43.43
push.0
# => [index, VALUE, pad(16)]
call.wrapper::test_set
# => [OLD_VALUE, pad(17)]
dropw

# Step 3: Get value at index 0 (should return [43, 43, 43, 43])
push.0
# => [index, pad(17)]
call.wrapper::test_get
# => [VALUE, pad(14)]

# Verify value is [43, 43, 43, 43]
push.43.43.43.43
assert_eqw.err="get(0) should return [43, 43, 43, 43] after set"
# => [pad(16)] (auto-padding)
end
"#;

// Compile the transaction script with the wrapper library linked
let tx_script = CodeBuilder::default()
.with_dynamically_linked_library(&wrapper_library)?
.compile_tx_script(tx_script_code)?;

// Create transaction context and execute
let tx_context = TransactionContextBuilder::new(account).tx_script(tx_script).build()?;

tx_context.execute().await?;

Ok(())
}