Skip to content
Merged
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 @@ -8,6 +8,7 @@
- [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 `AccountSchemaCommitment` component to expose account storage schema commitments ([#2253](https://github.com/0xMiden/miden-base/pull/2253)).
- Add `read_foreign_account_inputs()`, `read_vault_asset_witnesses()`, and `read_storage_map_witness()` for `TransactionInputs` ([#2246](https://github.com/0xMiden/miden-base/pull/2246)).
- [BREAKING] Introduce `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249)).
- [BREAKING] Introduce `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249), [#2252](https://github.com/0xMiden/miden-base/pull/2252)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::AccountError;
/// # Example
///
/// ```
/// use std::collections::BTreeSet;
/// use std::collections::{BTreeMap, BTreeSet};
///
/// use miden_protocol::account::StorageSlotName;
/// use miden_protocol::account::component::{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,43 @@ pub struct InitStorageData {
}

impl InitStorageData {
/// Creates a new instance of [InitStorageData], validating that there are no conflicting
/// entries.
///
/// # Errors
///
/// Returns an error if:
/// - A slot has both value entries and map entries
/// - A slot has both a slot-level value and field values
pub fn new(
value_entries: BTreeMap<StorageValueName, WordValue>,
map_entries: BTreeMap<StorageSlotName, Vec<(WordValue, WordValue)>>,
) -> Result<Self, InitStorageDataError> {
// Check for conflicts between value entries and map entries
for slot_name in map_entries.keys() {
if value_entries.keys().any(|v| v.slot_name() == slot_name) {
return Err(InitStorageDataError::ConflictingEntries(slot_name.as_str().into()));
}
}

// Check for conflicts between slot-level values and field values
for value_name in value_entries.keys() {
if value_name.field_name().is_none() {
// This is a slot-level value; check if there are field entries for this slot
let has_field_entries = value_entries.keys().any(|other| {
other.slot_name() == value_name.slot_name() && other.field_name().is_some()
});
if has_field_entries {
return Err(InitStorageDataError::ConflictingEntries(
value_name.slot_name().as_str().into(),
));
}
}
}

Ok(InitStorageData { value_entries, map_entries })
}

/// Returns a reference to the underlying init values map.
pub fn values(&self) -> &BTreeMap<StorageValueName, WordValue> {
&self.value_entries
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The MASM code of the Storage Commitment metadata Component.
#
# See the `AccountSchemaCommitment` Rust type's documentation for more details.

pub use ::miden::standards::metadata::storage_schema::get_schema_commitment
19 changes: 19 additions & 0 deletions crates/miden-standards/asm/standards/metadata/storage_schema.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This module defines the storage schema functionality for accounts. It exposes the storage
# slot at which an account stores a commitment to its storage schema, and provides a helper
# procedure to load that commitment from the active account's storage.

use miden::protocol::active_account

# CONSTANTS
# =================================================================================================

# The slot in this component's storage layout where the account storage schema commitment is stored
const SCHEMA_COMMITMENT_SLOT = word("miden::standards::metadata::storage_schema")

pub proc get_schema_commitment
dropw
# => [pad(16)]

push.SCHEMA_COMMITMENT_SLOT[0..2] exec.active_account::get_item
# => [SCHEMA_COMMITMENT, pad(16)]
end
17 changes: 17 additions & 0 deletions crates/miden-standards/src/account/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock<Library> = LazyLock::new(|| {
Library::read_from_bytes(bytes).expect("Shipped Network Fungible Faucet library is well-formed")
});

// METADATA LIBRARIES
// ================================================================================================

// Initialize the Storage Schema library only once.
static STORAGE_SCHEMA_LIBRARY: LazyLock<Library> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(
env!("OUT_DIR"),
"/assets/account_components/metadata/schema_commitment.masl"
));
Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed")
});

/// Returns the Basic Wallet Library.
pub fn basic_wallet_library() -> Library {
BASIC_WALLET_LIBRARY.clone()
Expand All @@ -123,6 +135,11 @@ pub fn network_fungible_faucet_library() -> Library {
NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone()
}

/// Returns the Storage Schema Library.
pub fn storage_schema_library() -> Library {
STORAGE_SCHEMA_LIBRARY.clone()
}

/// Returns the ECDSA K256 Keccak Library.
pub fn ecdsa_k256_keccak_library() -> Library {
ECDSA_K256_KECCAK_LIBRARY.clone()
Expand Down
185 changes: 185 additions & 0 deletions crates/miden-standards/src/account/metadata/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use alloc::collections::BTreeMap;

use miden_protocol::Word;
use miden_protocol::account::component::StorageSchema;
use miden_protocol::account::{AccountComponent, StorageSlot, StorageSlotName};
use miden_protocol::errors::AccountComponentTemplateError;
use miden_protocol::utils::sync::LazyLock;

use crate::account::components::storage_schema_library;

pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::metadata::storage_schema")
.expect("storage slot name should be valid")
});

/// An [`AccountComponent`] exposing the account storage schema commitment.
///
/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`],
/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`]
/// slot.
///
/// It reexports the `get_schema_commitment` procedure from
/// `miden::standards::metadata::storage_schema`.
///
/// ## Storage Layout
///
/// - [`Self::schema_commitment_slot`]: Storage schema commitment.
pub struct AccountSchemaCommitment {
schema_commitment: Word,
}

impl AccountSchemaCommitment {
/// Creates a new [`AccountSchemaCommitment`] component from a list of storage schemas.
///
/// The input schemas are merged into a single schema before the final commitment is computed.
///
/// # Errors
///
/// Returns an error if the schemas contain conflicting definitions for the same slot name.
pub fn new(schemas: &[StorageSchema]) -> Result<Self, AccountComponentTemplateError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if some other error type would make more sense to return from here instead of AccountComponentTemplateError - but also, don't have a good suggestion. Let's add it to the list of follow-ups.

Ok(Self {
schema_commitment: compute_schema_commitment(schemas)?,
})
}

/// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`].
pub fn from_schema(
storage_schema: &StorageSchema,
) -> Result<Self, AccountComponentTemplateError> {
Self::new(core::slice::from_ref(storage_schema))
}

/// Returns the [`StorageSlotName`] where the schema commitment is stored.
pub fn schema_commitment_slot() -> &'static StorageSlotName {
&SCHEMA_COMMITMENT_SLOT_NAME
}
}

impl From<AccountSchemaCommitment> for AccountComponent {
fn from(schema_commitment: AccountSchemaCommitment) -> Self {
AccountComponent::new(
storage_schema_library(),
vec![StorageSlot::with_value(
AccountSchemaCommitment::schema_commitment_slot().clone(),
schema_commitment.schema_commitment,
)],
)
.expect(
"AccountSchemaCommitment component should satisfy the requirements of a valid account component",
)
.with_supports_all_types()
}
}

/// Computes the schema commitment.
///
/// The account schema commitment is computed from the merged schema commitment.
/// If the passed list of schemas is empty, [`Word::empty()`] is returned.
fn compute_schema_commitment(
schemas: &[StorageSchema],
) -> Result<Word, AccountComponentTemplateError> {
if schemas.is_empty() {
return Ok(Word::empty());
}

let mut merged_slots = BTreeMap::new();
for schema in schemas {
for (slot_name, slot_schema) in schema.iter() {
match merged_slots.get(slot_name) {
None => {
merged_slots.insert(slot_name.clone(), slot_schema.clone());
},
// Slot exists, check if the schema is the same before erroring
// TODO: If we wanted to not error, we would have to decide on a winning schema
// for the StorageSlotName
Some(existing) => {
if existing != slot_schema {
return Err(AccountComponentTemplateError::InvalidSchema(format!(
"conflicting definitions for storage slot `{slot_name}`",
)));
}
},
}
}
}

let merged_schema = StorageSchema::new(merged_slots)?;

Ok(merged_schema.commitment())
}

// TESTS
// ================================================================================================

#[cfg(test)]
mod tests {
use miden_protocol::Word;
use miden_protocol::account::AccountBuilder;
use miden_protocol::account::component::AccountComponentMetadata;

use super::AccountSchemaCommitment;
use crate::account::auth::NoAuth;

#[test]
fn storage_schema_commitment_is_order_independent() {
let toml_a = r#"
name = "Component A"
description = "Component A schema"
version = "0.1.0"
supported-types = []

[[storage.slots]]
name = "test::slot_a"
type = "word"
"#;

let toml_b = r#"
name = "Component B"
description = "Component B schema"
version = "0.1.0"
supported-types = []

[[storage.slots]]
name = "test::slot_b"
description = "description is committed to"
type = "word"
"#;

let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap();
let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap();

let schema_a = metadata_a.storage_schema().clone();
let schema_b = metadata_b.storage_schema().clone();

// Create one component for each of two different accounts, but switch orderings
let component_a =
AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap();
let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap();

let account_a = AccountBuilder::new([1u8; 32])
.with_auth_component(NoAuth)
.with_component(component_a)
.build()
.unwrap();

let account_b = AccountBuilder::new([2u8; 32])
.with_auth_component(NoAuth)
.with_component(component_b)
.build()
.unwrap();

let slot_name = AccountSchemaCommitment::schema_commitment_slot();
let commitment_a = account_a.storage().get_item(slot_name).unwrap();
let commitment_b = account_b.storage().get_item(slot_name).unwrap();

assert_eq!(commitment_a, commitment_b);
}

#[test]
fn storage_schema_commitment_is_empty_for_no_schemas() {
let component = AccountSchemaCommitment::new(&[]).unwrap();

assert_eq!(component.schema_commitment, Word::empty());
}
}
1 change: 1 addition & 0 deletions crates/miden-standards/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod auth;
pub mod components;
pub mod faucets;
pub mod interface;
pub mod metadata;
pub mod wallets;

/// Macro to simplify the creation of static procedure digest constants.
Expand Down