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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions crates/miden-testing/src/kernel_tests/tx/test_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(())
}