diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d16abb35..8a01e6a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ - 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)). +- Added single-word array data structure abstraction over storage maps ([#2203](https://github.com/0xMiden/miden-base/pull/2203)). +- Added double-word array data structure abstraction over storage maps ([#2299](https://github.com/0xMiden/miden-base/pull/2299)). ### Changes diff --git a/crates/miden-standards/asm/standards/data_structures/double_word_array.masm b/crates/miden-standards/asm/standards/data_structures/double_word_array.masm new file mode 100644 index 000000000..878268455 --- /dev/null +++ b/crates/miden-standards/asm/standards/data_structures/double_word_array.masm @@ -0,0 +1,130 @@ +# The MASM code for the Double-Word Array abstraction. +# +# It provides an abstraction layer over a storage map, treating it as an array +# of double-words, with "set" and "get" for storing and retrieving values 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 Double-Word 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 } +type BeDoubleWord = struct @bigendian { + a: felt, + b: felt, + c: felt, + d: felt, + e: felt, + f: felt, + g: felt, + h: felt +} + +# PROCEDURES +# ================================================================================================= + +#! Sets a double-word in the array at the specified index. +#! +#! Inputs: [slot_id_prefix, slot_id_suffix, index, VALUE_0, VALUE_1, pad(5)] +#! Outputs: [OLD_VALUE_0, OLD_VALUE_1, pad(8)] +#! +#! 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_0 is the first word to store at the specified index. +#! - VALUE_1 is the second word to store at the specified index. +#! +#! Internally, the words are stored under keys [index, 0, 0, 0] and [index, 1, 0, 0], +#! for the first and second word, respectively. +#! +#! Invocation: call +@locals(16) +pub proc set( + slot_id_prefix: felt, + slot_id_suffix: felt, + index: felt, + value: BeDoubleWord +) -> BeDoubleWord + # save inputs to locals for reuse + loc_store.0 + loc_store.1 + loc_store.2 + # => [VALUE_0, VALUE_1, pad(8)] auto-padding + + # Set the first word under key [index, 0, 0, 0]. + push.0.0.0 + loc_load.2 + # => [index, 0, 0, 0, VALUE_0, VALUE_1, pad(8)] + + loc_load.1 + loc_load.0 + # => [slot_id_prefix, slot_id_suffix, KEY_0, VALUE_0, VALUE_1, pad(8)] + + exec.native_account::set_map_item + # => [OLD_VALUE_0, VALUE_1, pad(8)] + swapw + + # Set the second word under key [index, 1, 0, 0]. + push.0.0.1 + loc_load.2 + # => [index, 1, 0, 0, VALUE_1, OLD_VALUE_0, pad(8)] + + loc_load.1 + loc_load.0 + # => [slot_id_prefix, slot_id_suffix, KEY_1, VALUE_1, OLD_VALUE_0, pad(8)] + + exec.native_account::set_map_item + # => [OLD_VALUE_1, OLD_VALUE_0, pad(8)] + swapw +end + +#! Gets a double-word from the array at the specified index. +#! +#! Inputs: [slot_id_prefix, slot_id_suffix, index, pad(13)] +#! Outputs: [VALUE_0, VALUE_1, pad(8)] +#! +#! 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_0 is the first word stored at the specified index (zero if not set). +#! - VALUE_1 is the second word stored at the specified index (zero if not set). +#! +#! Invocation: call +@locals(12) +pub proc get(slot_id_prefix: felt, slot_id_suffix: felt, index: felt) -> BeDoubleWord + # Save inputs to locals for reuse. + loc_store.0 + loc_store.1 + loc_store.2 + # => [pad(16)] auto-padding + + # Get the first word from key [index, 0, 0, 0]. + push.0.0.0 + loc_load.2 + # => [index, 0, 0, 0, pad(16)] + + loc_load.1 + loc_load.0 + # => [slot_id_prefix, slot_id_suffix, KEY_0, pad(16)] + + exec.active_account::get_map_item + # => [VALUE_0, pad(16)] + + # Get the second word from key [index, 1, 0, 0]. + push.0.0.1 + loc_load.2 + # => [index, 1, 0, 0, pad(16)] + + loc_load.1 + loc_load.0 + # => [slot_id_prefix, slot_id_suffix, KEY_1, pad(16)] + + exec.active_account::get_map_item + swapw + # => [VALUE_0, VALUE_1, pad(16)] + swapdw dropw dropw +end diff --git a/crates/miden-testing/src/kernel_tests/tx/test_array.rs b/crates/miden-testing/src/kernel_tests/tx/test_array.rs index a925d391f..98572b896 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_array.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_array.rs @@ -16,6 +16,7 @@ use crate::{Auth, TransactionContextBuilder}; /// The slot name used for testing the array component. const TEST_ARRAY_SLOT: &str = "test::array::data"; +const TEST_DOUBLE_WORD_ARRAY_SLOT: &str = "test::double_word_array::data"; /// Verify that, given an account component with a storage map to hold the array data, /// we can use the array utility to: @@ -133,3 +134,115 @@ async fn test_array_get_and_set() -> anyhow::Result<()> { Ok(()) } + +/// Verify that the double-word array utility can store and retrieve two words per index. +#[tokio::test] +async fn test_double_word_array_get_and_set() -> anyhow::Result<()> { + let slot_name = + StorageSlotName::new(TEST_DOUBLE_WORD_ARRAY_SLOT).expect("slot name should be valid"); + let index = Felt::new(7); + + let wrapper_component_code = format!( + r#" + use miden::core::word + use miden::standards::data_structures::double_word_array + + const ARRAY_SLOT_NAME = word("{slot_name}") + + #! Wrapper for double_word_array::get that uses exec internally. + #! Inputs: [index, pad(15)] + #! Outputs: [VALUE_0, VALUE_1, pad(8)] + pub proc test_get + push.ARRAY_SLOT_NAME[0..2] + exec.double_word_array::get + end + + #! Wrapper for double_word_array::set that uses exec internally. + #! Inputs: [index, VALUE_0, VALUE_1, pad(7)] + #! Outputs: [OLD_VALUE_0, OLD_VALUE_1, pad(8)] + pub proc test_set + push.ARRAY_SLOT_NAME[0..2] + exec.double_word_array::set + end + "#, + ); + + let wrapper_library = CodeBuilder::default() + .compile_component_code("wrapper::component", wrapper_component_code)?; + + let initial_value_0 = Word::from([1u32, 2, 3, 4]); + let initial_value_1 = Word::from([5u32, 6, 7, 8]); + 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, index]), initial_value_0), + (Word::from([Felt::ZERO, Felt::ZERO, Felt::ONE, index]), initial_value_1), + ])?, + )], + )? + .with_supports_all_types(); + + let account = AccountBuilder::new(ChaCha20Rng::from_os_rng().random()) + .with_auth_component(Auth::IncrNonce) + .with_component(wrapper_component) + .build_existing()?; + + assert!( + account.storage().get(&slot_name).is_some(), + "Double-word array data slot should exist in account storage" + ); + + let updated_value_0 = Word::from([9u32, 9, 9, 9]); + let updated_value_1 = Word::from([10u32, 10, 10, 10]); + let tx_script_code = format!( + r#" + use wrapper::component->wrapper + + begin + # Step 1: Get value at index {index} (should return the initial double-word) + push.{index} + call.wrapper::test_get + + push.{initial_value_0} + assert_eqw.err="get(index) should return initial word 0" + + push.{initial_value_1} + assert_eqw.err="get(index) should return initial word 1" + + # Step 2: Set the double-word at index {index} to the updated values + push.{updated_value_1} + push.{updated_value_0} + push.{index} + call.wrapper::test_set + push.{initial_value_0} + assert_eqw.err="set(index) should return the original double-word, left word" + push.{initial_value_1} + assert_eqw.err="set(index) should return the original double-word, right word" + + # Step 3: Get value at index {index} (should return the updated double-word) + push.{index} + call.wrapper::test_get + + push.{updated_value_0} + assert_eqw.err="get(index) should return the updated double-word, left word" + + push.{updated_value_1} + assert_eqw.err="get(index) should return the updated double-word, right word" + + repeat.8 drop end + end + "#, + ); + + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(&wrapper_library)? + .compile_tx_script(tx_script_code)?; + + let tx_context = TransactionContextBuilder::new(account).tx_script(tx_script).build()?; + + tx_context.execute().await?; + + Ok(()) +}