diff --git a/CHANGELOG.md b/CHANGELOG.md index f7626770e..6898de47d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ - [BREAKING] Refactored storage slots to be accessed by names instead of indices ([#1987](https://github.com/0xMiden/miden-base/pull/1987), [#2025](https://github.com/0xMiden/miden-base/pull/2025), [#2149](https://github.com/0xMiden/miden-base/pull/2149), [#2150](https://github.com/0xMiden/miden-base/pull/2150), [#2153](https://github.com/0xMiden/miden-base/pull/2153), [#2154](https://github.com/0xMiden/miden-base/pull/2154), [#2160](https://github.com/0xMiden/miden-base/pull/2160), [#2161](https://github.com/0xMiden/miden-base/pull/2161), [#2170](https://github.com/0xMiden/miden-base/pull/2170)). - [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)). -- Add `From<&ExecutedTransaction> for TransactionHeader` implementation ([#2178](https://github.com/0xMiden/miden-base/pull/2178)). -- Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). +- Added `From<&ExecutedTransaction> for TransactionHeader` implementation ([#2178](https://github.com/0xMiden/miden-base/pull/2178)). +- Added single-word `Array` account component ([#2203](https://github.com/0xMiden/miden-base/pull/2203)). +- Added `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). ### Changes diff --git a/crates/miden-standards/asm/account_components/array.masm b/crates/miden-standards/asm/account_components/array.masm new file mode 100644 index 000000000..97fdea01a --- /dev/null +++ b/crates/miden-standards/asm/account_components/array.masm @@ -0,0 +1,76 @@ +# The MASM code template for the Array Account Component. +# +# NOTE: This file is used as a template. The placeholder {{DATA_SLOT}} is substituted +# at construction time with the actual slot name provided by the user. +# +# This component provides an array data structure backed by a StorageMap. +# It supports "set" and "get" operations for storing and retrieving words by index. +# The array can store up to 2^64 - 2^32 + 1 elements (indices 0 to 2^64 - 2^32). +# +# See the `Array` Rust type's documentation for more details. + +use miden::protocol::active_account +use miden::protocol::native_account + +type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } + +# CONSTANTS +# ================================================================================================= + +# The slot where the array data is stored as a map. +# Keys are [index, 0, 0, 0], values are the stored words. +# This value is substituted at construction time. +const DATA_SLOT = word("{{DATA_SLOT}}") + +# PROCEDURES +# ================================================================================================= + +#! Sets a word in the array at the specified index. +#! +#! Inputs: [index, VALUE, pad(11)] +#! Outputs: [OLD_VALUE, pad(12)] +#! +#! Where: +#! - 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(index: felt, value: BeWord) -> BeWord + # Build KEY = [index, 0, 0, 0] + push.0.0.0 movup.3 + # => [index, 0, 0, 0, VALUE, pad(11)] + + # Push slot IDs + push.DATA_SLOT[0..2] + # => [slot_prefix, slot_suffix, KEY, VALUE, pad(11)] + + exec.native_account::set_map_item + # => [OLD_VALUE, pad(11)] + + # (auto-padding to 16 elements) + # => [OLD_VALUE, pad(12)] +end + +#! Gets a word from the array at the specified index. +#! +#! Inputs: [index, pad(15)] +#! Outputs: [VALUE, pad(12)] +#! +#! Where: +#! - 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(index: felt) -> BeWord + # Build KEY = [index, 0, 0, 0] + push.0.0.0 movup.3 + # => [index, 0, 0, 0, pad(15)] + + # Push slot IDs + push.DATA_SLOT[0..2] + # => [slot_prefix, slot_suffix, KEY, pad(15)] + + exec.active_account::get_map_item + # => [VALUE, pad(15)] + repeat.3 movup.4 drop end +end diff --git a/crates/miden-standards/src/account/array.rs b/crates/miden-standards/src/account/array.rs new file mode 100644 index 000000000..54879f4b9 --- /dev/null +++ b/crates/miden-standards/src/account/array.rs @@ -0,0 +1,285 @@ +use alloc::string::String; +use alloc::vec::Vec; + +use miden_protocol::account::{AccountComponent, StorageMap, StorageSlot, StorageSlotName}; +use miden_protocol::assembly::Library; +use miden_protocol::assembly::diagnostics::NamedSource; +use miden_protocol::transaction::TransactionKernel; +use miden_protocol::{Felt, FieldElement, StorageMapError, Word}; + +// CONSTANTS +// ================================================================================================ + +/// The maximum number of elements the array can store. +/// +/// Since indices are represented as a single [`Felt`] element in the key `[index, 0, 0, 0]`, +/// the array can store up to 2^64 - 2^32 + 1 elements (indices 0 to 2^64 - 2^32). +/// +/// [`Felt`]: miden_protocol::Felt +pub const ARRAY_MAX_ELEMENTS: u64 = 0xffffffff00000001; + +/// The MASM template for the Array component. +/// The placeholder `{{DATA_SLOT}}` is substituted with the actual slot name at construction time. +const ARRAY_MASM_TEMPLATE: &str = include_str!("../../asm/account_components/array.masm"); + +// ARRAY COMPONENT +// ================================================================================================ + +/// An [`AccountComponent`] providing an array data structure for storing words. +/// +/// This component provides a sparse array backed by a StorageMap that can store up to +/// [`ARRAY_MAX_ELEMENTS`] elements. It supports `set` and `get` operations for storing and +/// retrieving words by index. +/// +/// When linking against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. +/// +/// The procedures of this component are: +/// - `set`: Stores a word at the specified index. Returns the old value. +/// - `get`: Retrieves a word at the specified index. Returns zero if not set. +/// +/// This component supports all account types. +/// +/// ## Capacity +/// +/// The array can store up to [`ARRAY_MAX_ELEMENTS`] elements. +/// +/// ## Storage Layout +/// +/// - [`Self::data_slot`]: A StorageMap where key `[index, 0, 0, 0]` maps to the stored word +/// +/// ## Configurable Storage Slot +/// +/// The data slot can be configured at construction time, allowing multiple independent +/// arrays to coexist in the same account by using different slot names. +/// +/// [builder]: crate::code_builder::CodeBuilder +pub struct Array { + /// Initial elements to populate the array with, as (index, value) pairs. + initial_elements: Vec<(Felt, Word)>, + /// The storage slot name for the array data. + data_slot: StorageSlotName, +} + +impl Array { + /// Creates a new [`Array`] component with the specified data slot and no initial elements. + /// + /// # Arguments + /// * `data_slot` - The storage slot name where the array data will be stored. + pub fn new(data_slot: StorageSlotName) -> Self { + Self { initial_elements: Vec::new(), data_slot } + } + + /// Creates a new [`Array`] component with the given initial elements. + /// + /// Elements are provided as (index, value) pairs. Any index not specified will + /// return the zero word when accessed. + /// + /// # Arguments + /// * `data_slot` - The storage slot name where the array data will be stored. + /// * `elements` - Initial elements as (index, value) pairs. + pub fn with_elements( + data_slot: StorageSlotName, + elements: impl IntoIterator, + ) -> Self { + Self { + initial_elements: elements.into_iter().collect(), + data_slot, + } + } + + /// Creates a new [`Array`] component from a contiguous slice of words. + /// + /// The words are stored at indices 0, 1, 2, ... in order. + /// + /// # Arguments + /// * `data_slot` - The storage slot name where the array data will be stored. + /// * `elements` - Words to store at consecutive indices starting from 0. + pub fn from_slice(data_slot: StorageSlotName, elements: &[Word]) -> Self { + let initial_elements = elements + .iter() + .enumerate() + .map(|(i, word)| (Felt::new(i as u64), *word)) + .collect(); + Self { initial_elements, data_slot } + } + + /// Returns a reference to the [`StorageSlotName`] where the array data is stored. + pub fn data_slot(&self) -> &StorageSlotName { + &self.data_slot + } + + /// Generates the MASM source code for this array component by substituting + /// the data slot name into the template. + fn generate_masm_source(&self) -> String { + ARRAY_MASM_TEMPLATE.replace("{{DATA_SLOT}}", self.data_slot.as_str()) + } + + /// Generates the compiled [`Library`] for this array component with the given component name. + /// + /// The `component_name` determines the module path used to reference the array's procedures. + /// For example, with `component_name = "myarray::data"`, the procedures would be called as + /// `myarray::data::get` and `myarray::data::set`. + /// + /// This can be used to link the array component's procedures into transaction scripts + /// or other code that needs to call the array's `get` and `set` procedures. + pub fn generate_library(&self, component_name: &str) -> Library { + let masm_source = self.generate_masm_source(); + let source = NamedSource::new(component_name, masm_source); + TransactionKernel::assembler() + .assemble_library([source]) + .expect("Array MASM template should be valid") + } +} + +impl TryFrom for AccountComponent { + type Error = StorageMapError; + + fn try_from(array: Array) -> Result { + // Generate the MASM source with the configured slot name + let masm_source = array.generate_masm_source(); + + // Assemble the library dynamically + let source = NamedSource::new("array::component", masm_source); + let library = TransactionKernel::assembler() + .assemble_library([source]) + .expect("Array MASM template should be valid"); + + // Data slot: StorageMap with initial elements + let map_entries = array + .initial_elements + .into_iter() + .map(|(index, value)| (Word::from([index, Felt::ZERO, Felt::ZERO, Felt::ZERO]), value)); + + let storage_slots = + vec![StorageSlot::with_map(array.data_slot, StorageMap::with_entries(map_entries)?)]; + + Ok(AccountComponent::new(library, storage_slots) + .expect("Array component should satisfy the requirements of a valid account component") + .with_supports_all_types()) + } +} + +#[cfg(test)] +mod tests { + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; + + use super::*; + use crate::account::auth::NoAuth; + + #[test] + fn test_array_creation_empty() { + let slot = + StorageSlotName::new("myproject::myarray::data").expect("slot name should be valid"); + let array = Array::new(slot); + let component: AccountComponent = array.try_into().expect("should create component"); + + // Verify component was created successfully with one storage slot (data) + assert_eq!(component.storage_slots().len(), 1); + } + + #[test] + fn test_array_creation_with_elements() { + let elements = vec![ + (Felt::new(0), Word::from([1, 2, 3, 4u32])), + (Felt::new(5), Word::from([5, 6, 7, 8u32])), + (Felt::new(1000), Word::from([9, 10, 11, 12u32])), + ]; + let slot = + StorageSlotName::new("myproject::myarray::data").expect("slot name should be valid"); + let array = Array::with_elements(slot, elements); + let component: AccountComponent = array.try_into().expect("should create component"); + + // Verify component was created successfully + assert_eq!(component.storage_slots().len(), 1); + } + + #[test] + fn test_array_from_slice() { + let elements = vec![ + Word::from([1, 2, 3, 4u32]), + Word::from([5, 6, 7, 8u32]), + Word::from([9, 10, 11, 12u32]), + ]; + let slot = + StorageSlotName::new("myproject::myarray::data").expect("slot name should be valid"); + let array = Array::from_slice(slot, &elements); + let component: AccountComponent = array.try_into().expect("should create component"); + + // Verify component was created successfully + assert_eq!(component.storage_slots().len(), 1); + } + + #[test] + fn test_array_account_integration() { + let data_slot = + StorageSlotName::new("myproject::myarray::data").expect("slot name should be valid"); + let elements = vec![ + (Felt::new(0), Word::from([1, 2, 3, 4u32])), + (Felt::new(1), Word::from([5, 6, 7, 8u32])), + (Felt::new(1000), Word::from([9, 10, 11, 12u32])), + ]; + let array = Array::with_elements(data_slot.clone(), elements.clone()); + let array_component: AccountComponent = array.try_into().expect("should create component"); + + // Build an account with the Array component + let account = AccountBuilder::new([0u8; 32]) + .with_auth_component(NoAuth) + .with_component(array_component) + .build() + .expect("account building should succeed"); + + // Verify data elements are stored correctly + for (index, expected) in &elements { + let key = Word::from([*index, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let value = account + .storage() + .get_map_item(&data_slot, key) + .expect("data slot should contain element"); + assert_eq!(&value, expected, "element at index {} should match", index); + } + } + + #[test] + fn test_multiple_arrays_different_slots() { + // Create two arrays with different slot names + let slot1 = + StorageSlotName::new("myproject::array1::data").expect("slot name should be valid"); + let slot2 = + StorageSlotName::new("myproject::array2::data").expect("slot name should be valid"); + + let array1: AccountComponent = + Array::with_elements(slot1.clone(), [(Felt::new(0), Word::from([1, 1, 1, 1u32]))]) + .try_into() + .expect("should create component"); + let array2: AccountComponent = + Array::with_elements(slot2.clone(), [(Felt::new(0), Word::from([2, 2, 2, 2u32]))]) + .try_into() + .expect("should create component"); + + // Build an account with both Array components + let account = AccountBuilder::new([0u8; 32]) + .with_auth_component(NoAuth) + .with_component(array1) + .with_component(array2) + .build() + .expect("account building should succeed"); + + // Verify both arrays have their data stored correctly + let key = Word::from([0u32, 0, 0, 0]); + + let value1 = account + .storage() + .get_map_item(&slot1, key) + .expect("slot1 should contain element"); + assert_eq!(value1, Word::from([1, 1, 1, 1u32])); + + let value2 = account + .storage() + .get_map_item(&slot2, key) + .expect("slot2 should contain element"); + assert_eq!(value2, Word::from([2, 2, 2, 2u32])); + } +} diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index eea3a9214..fc73990b0 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -1,5 +1,6 @@ use super::auth_scheme::AuthScheme; +pub mod array; pub mod auth; pub mod components; pub mod faucets; diff --git a/crates/miden-testing/src/kernel_tests/tx/mod.rs b/crates/miden-testing/src/kernel_tests/tx/mod.rs index e59b033d0..42f390cf7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/mod.rs +++ b/crates/miden-testing/src/kernel_tests/tx/mod.rs @@ -33,6 +33,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; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_array.rs b/crates/miden-testing/src/kernel_tests/tx/test_array.rs new file mode 100644 index 000000000..a82532ea6 --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/tx/test_array.rs @@ -0,0 +1,151 @@ +//! Tests for the Array account component's `get` and `set` procedures. + +use alloc::sync::Arc; + +use miden_protocol::account::{AccountBuilder, AccountComponent, StorageSlotName}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::assembly::diagnostics::NamedSource; +use miden_protocol::{Felt, Word}; +use miden_standards::account::array::Array; +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"; + +/// The component name used for testing the array component. +const TEST_ARRAY_COMPONENT: &str = "test::array::component"; + +/// Comprehensive test for the Array component that verifies: +/// 1. Initial value can be retrieved via `get` +/// 2. Value can be updated via `set` +/// 3. Updated value can be retrieved via `get` +/// +/// Since we cannot use `exec` from a transaction script to invoke account procedures directly, +/// we create a wrapper account component that exposes procedures which internally use `exec` +/// to call the array component's procedures. +#[tokio::test] +async fn test_array_get_and_set() -> anyhow::Result<()> { + let data_slot = StorageSlotName::new(TEST_ARRAY_SLOT).expect("slot name should be valid"); + + // Initialize the array with the first entry (index 0) set to [42, 42, 42, 42] + let initial_value = Word::from([42u32, 42, 42, 42]); + + // Generate the array library for linking + let array = Array::with_elements(data_slot.clone(), [(Felt::new(0), initial_value)]); + let array_library = array.generate_library(TEST_ARRAY_COMPONENT); + + // Create a wrapper account component that uses `exec` to call the array procedures. + // This wrapper is needed because transaction scripts cannot use `exec` to call + // account procedures directly - they must use `call`. + let wrapper_component_code = format!( + r#" + use {component}->test_array + + #! Wrapper for array::get that uses exec internally. + #! Inputs: [index, pad(15)] + #! Outputs: [VALUE, pad(12)] + pub proc test_get + exec.test_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 + exec.test_array::set + end + "#, + component = TEST_ARRAY_COMPONENT + ); + + // Build the wrapper component by linking against the array library + let mut assembler: miden_protocol::assembly::Assembler = + CodeBuilder::with_mock_libraries_with_source_manager(Arc::new( + DefaultSourceManager::default(), + )) + .into(); + assembler + .link_static_library(&array_library) + .expect("should be able to link array library"); + + let wrapper_source = NamedSource::new("wrapper::component", wrapper_component_code); + let wrapper_library = assembler + .clone() + .assemble_library([wrapper_source]) + .expect("wrapper component MASM should be valid"); + + // Create the wrapper account component (no storage slots needed for the wrapper itself) + let wrapper_component = + AccountComponent::new(wrapper_library.clone(), vec![])?.with_supports_all_types(); + + // Build an account with both the Array component and the wrapper component + let array_for_account: AccountComponent = + Array::with_elements(data_slot.clone(), [(Felt::new(0), initial_value)]).try_into()?; + let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(array_for_account) + .with_component(wrapper_component) + .build_existing()?; + + // Verify the storage slot exists + assert!( + account.storage().get(&data_slot).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(()) +}