diff --git a/CHANGELOG.md b/CHANGELOG.md index 49064ae53..4fb943988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index 1d8662dc1..9b84b4cf0 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -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::{ diff --git a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs index a9df7e07e..999f552a3 100644 --- a/crates/miden-protocol/src/account/component/storage/init_storage_data.rs +++ b/crates/miden-protocol/src/account/component/storage/init_storage_data.rs @@ -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, + map_entries: BTreeMap>, + ) -> Result { + // 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 { &self.value_entries diff --git a/crates/miden-standards/asm/account_components/metadata/schema_commitment.masm b/crates/miden-standards/asm/account_components/metadata/schema_commitment.masm new file mode 100644 index 000000000..cc9b85230 --- /dev/null +++ b/crates/miden-standards/asm/account_components/metadata/schema_commitment.masm @@ -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 diff --git a/crates/miden-standards/asm/standards/metadata/storage_schema.masm b/crates/miden-standards/asm/standards/metadata/storage_schema.masm new file mode 100644 index 000000000..fe61927c3 --- /dev/null +++ b/crates/miden-standards/asm/standards/metadata/storage_schema.masm @@ -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 diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index de1edcb37..b99c0a24a 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -108,6 +108,18 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = 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 = 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() @@ -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() diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs new file mode 100644 index 000000000..8ab43e44a --- /dev/null +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -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 = 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 { + 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::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 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 { + 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()); + } +} diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index eea3a9214..af3d4ff69 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -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.