Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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::core::sys
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(())
}
Loading