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
10 changes: 5 additions & 5 deletions crates/miden-protocol/src/account/component/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ use crate::AccountError;
///
/// - The metadata's storage schema does not contain duplicate slot names.
/// - The schema cannot contain protocol-reserved slot names.
/// - Each init-time value name uniquely identifies a single value. The expected init-time metadata
/// can be retrieved with [AccountComponentMetadata::schema_requirements()], which returns a map
/// from keys to [SchemaRequirement] (which indicates the expected value type and optional
/// defaults).
/// - Each init-time value name uniquely identifies a single value. The expected init-time
/// requirements can be retrieved with [AccountComponentMetadata::schema_requirements()], which
/// returns a map from keys to [SchemaRequirement] (which indicates the expected value type and
/// optional defaults).
///
/// # Example
///
Expand Down Expand Up @@ -123,7 +123,7 @@ impl AccountComponentMetadata {
}
}

/// Returns the init-time values's requirements for this schema.
/// Returns the init-time value requirements for this schema.
///
/// These values are used for initializing storage slot values or storage map entries. For a
/// full example, refer to the docs for [AccountComponentMetadata].
Expand Down
77 changes: 21 additions & 56 deletions crates/miden-protocol/src/account/component/storage/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use miden_processor::DeserializationError;

use super::type_registry::{SCHEMA_TYPE_REGISTRY, SchemaRequirement, SchemaTypeId};
use super::{InitStorageData, StorageValueName, WordValue};
use crate::account::{AccountStorage, StorageMap, StorageSlot, StorageSlotName};
use crate::account::storage::is_reserved_slot_name;
use crate::account::{StorageMap, StorageSlot, StorageSlotName};
use crate::errors::AccountComponentTemplateError;
use crate::{Felt, FieldElement, Word};

Expand Down Expand Up @@ -83,7 +84,7 @@ impl AccountStorageSchema {
let mut init_values = BTreeMap::new();

for (slot_name, schema) in self.slots.iter() {
if slot_name.id() == AccountStorage::faucet_sysdata_slot().id() {
if is_reserved_slot_name(slot_name) {
return Err(AccountComponentTemplateError::ReservedSlotName(slot_name.clone()));
}

Expand Down Expand Up @@ -277,8 +278,9 @@ impl WordSchema {
return Ok(());
}

let default_value =
default_value.map(|word| SCHEMA_TYPE_REGISTRY.display_word(r#type, word));
let default_value = default_value.map(|word| {
SCHEMA_TYPE_REGISTRY.display_word(r#type, word).value().to_string()
});

if requirements
.insert(
Expand Down Expand Up @@ -320,7 +322,8 @@ impl WordSchema {
default_value: Some(default_value),
} = self
{
validate_word_value(word_type_kind(r#type), r#type, *default_value)
SCHEMA_TYPE_REGISTRY
.validate_word_value(r#type, *default_value)
.map_err(AccountComponentTemplateError::StorageValueParsingError)?;
}

Expand Down Expand Up @@ -356,7 +359,8 @@ impl WordSchema {
.map_err(AccountComponentTemplateError::StorageValueParsingError)?;
let felts: [Felt; 4] = felts.try_into().expect("length is 4");
let word = Word::from(felts);
validate_word_value(word_type_kind(r#type), r#type, word)
SCHEMA_TYPE_REGISTRY
.validate_word_value(r#type, word)
.map_err(AccountComponentTemplateError::StorageValueParsingError)?;
Ok(word)
},
Expand Down Expand Up @@ -390,7 +394,7 @@ impl WordSchema {
) -> Result<(), AccountComponentTemplateError> {
match self {
WordSchema::Simple { r#type, .. } => {
validate_word_value(word_type_kind(r#type), r#type, word).map_err(|err| {
SCHEMA_TYPE_REGISTRY.validate_word_value(r#type, word).map_err(|err| {
AccountComponentTemplateError::InvalidInitStorageValue(
slot_prefix.clone(),
format!("{label} does not match `{}`: {err}", r#type),
Expand All @@ -400,12 +404,14 @@ impl WordSchema {
WordSchema::Composite { value } => {
for (index, felt_schema) in value.iter().enumerate() {
let felt_type = felt_schema.felt_type();
validate_felt_value(&felt_type, word[index]).map_err(|err| {
AccountComponentTemplateError::InvalidInitStorageValue(
slot_prefix.clone(),
format!("{label}[{index}] does not match `{felt_type}`: {err}"),
)
})?;
SCHEMA_TYPE_REGISTRY.validate_felt_value(&felt_type, word[index]).map_err(
|err| {
AccountComponentTemplateError::InvalidInitStorageValue(
slot_prefix.clone(),
format!("{label}[{index}] does not match `{felt_type}`: {err}"),
)
},
)?;
}

Ok(())
Expand Down Expand Up @@ -663,7 +669,8 @@ impl FeltSchema {
}

if let Some(value) = self.default_value {
validate_felt_value(&self.felt_type(), value)
SCHEMA_TYPE_REGISTRY
.validate_felt_value(&self.felt_type(), value)
.map_err(AccountComponentTemplateError::StorageValueParsingError)?;
}
Ok(())
Expand All @@ -689,48 +696,6 @@ impl Deserializable for FeltSchema {
}
}

// VALUE VALIDATION HELPERS
// ================================================================================================

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WordTypeKind {
Word,
Felt,
}

fn word_type_kind(schema_type: &SchemaTypeId) -> WordTypeKind {
if SCHEMA_TYPE_REGISTRY.contains_felt_type(schema_type) {
WordTypeKind::Felt
} else {
WordTypeKind::Word
}
}

fn validate_word_value(
kind: WordTypeKind,
schema_type: &SchemaTypeId,
word: Word,
) -> Result<(), super::SchemaTypeError> {
match kind {
WordTypeKind::Word => Ok(()),
WordTypeKind::Felt => {
if word[0] != Felt::ZERO || word[1] != Felt::ZERO || word[2] != Felt::ZERO {
return Err(super::SchemaTypeError::ConversionError(format!(
"expected a word of the form [0, 0, 0, <felt>] for type `{schema_type}`"
)));
}
validate_felt_value(schema_type, word[3])
},
}
}

fn validate_felt_value(
schema_type: &SchemaTypeId,
felt: Felt,
) -> Result<(), super::SchemaTypeError> {
SCHEMA_TYPE_REGISTRY.validate_felt_value(schema_type, felt)
}

/// Describes the schema for a storage value slot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueSlotSchema {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ pub enum InitStorageDataError {
DuplicateKey(String),

#[error(
"invalid input for `{key}`: unsupported array value (len {len}); expected either a map entry list (array of inline tables with `key` and `value`) or a 4-element word array of strings"
"invalid input for `{key}`: unsupported array value (length {len}); expected either a map entry list (array of inline tables with `key` and `value`) or a 4-element word array of strings"
)]
ArraysNotSupported { key: String, len: usize },

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,6 @@ impl WordValue {
}

pub(super) fn from_word(schema_type: &SchemaTypeId, word: Word) -> Self {
WordValue::Atomic(SCHEMA_TYPE_REGISTRY.display_word(schema_type, word))
WordValue::Atomic(SCHEMA_TYPE_REGISTRY.display_word(schema_type, word).value().to_string())
}
}
29 changes: 25 additions & 4 deletions crates/miden-protocol/src/account/component/storage/toml/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ fn from_toml_rejects_non_string_atomics() {
#[test]
fn test_error_on_array() {
let toml_str = r#"
"demo::token_metadata.v" = ["1", "2", "3", "4", "5"]
["demo::token_metadata"]
v = ["1", "2", "3", "4", "5"]
"#;

let err = InitStorageData::from_toml(toml_str).unwrap_err();
Expand All @@ -74,9 +75,6 @@ fn test_error_on_array() {
InitStorageDataError::ArraysNotSupported { key, len }
if key == "demo::token_metadata.v" && *len == 5
);
let msg = err.to_string();
assert!(msg.contains("demo::token_metadata.v"));
assert!(msg.contains("len 5"));
}

#[test]
Expand Down Expand Up @@ -202,6 +200,29 @@ fn metadata_from_toml_rejects_typed_fields_in_static_map_values() {
);
}

#[test]
fn metadata_from_toml_rejects_short_composite_schema() {
let toml_str = r#"
name = "Test Component"
description = "Test description"
version = "0.1.0"
supported-types = []

[[storage.slots]]
name = "demo::short_composite"
type = [
{ type = "u8", name = "a" },
{ type = "void" },
{ type = "void" },
]
"#;
assert_matches::assert_matches!(
AccountComponentMetadata::from_toml(toml_str),
Err(AccountComponentTemplateError::InvalidSchema(msg))
if msg.contains("array of 4 elements")
);
}

#[test]
fn metadata_from_toml_rejects_reserved_slot_names() {
let reserved_slot = AccountStorage::faucet_sysdata_slot().as_str();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use core::error::Error;
use core::fmt::{self, Display};

use miden_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable};
use miden_core::{Felt, Word};
use miden_core::{Felt, FieldElement, Word};
use miden_crypto::dsa::{ecdsa_k256_keccak, falcon512_rpo};
use miden_processor::DeserializationError;
use thiserror::Error;
Expand Down Expand Up @@ -491,17 +491,41 @@ impl WordType for ecdsa_k256_keccak::PublicKey {
// ================================================================================================

/// Type alias for a function that converts a string into a [`Felt`] value.
type FeltTypeConverter = fn(&str) -> Result<Felt, SchemaTypeError>;
type FeltFromStrConverter = fn(&str) -> Result<Felt, SchemaTypeError>;

/// Type alias for a function that converts a string into a [`Word`].
type WordTypeConverter = fn(&str) -> Result<Word, SchemaTypeError>;
type WordFromStrConverter = fn(&str) -> Result<Word, SchemaTypeError>;

/// Type alias for a function that converts a [`Felt`] into a canonical string representation.
type FeltTypeDisplayer = fn(Felt) -> Result<String, SchemaTypeError>;

/// Type alias for a function that converts a [`Word`] into a canonical string representation.
type WordTypeDisplayer = fn(Word) -> Result<String, SchemaTypeError>;

/// Result of a word display conversion.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WordDisplay {
Word(String),
Felt(String),
Hex(String),
}

impl WordDisplay {
pub fn value(&self) -> &str {
match self {
WordDisplay::Word(v) => v,
WordDisplay::Felt(v) => v,
WordDisplay::Hex(v) => v,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TypeKind {
Word,
Felt,
}

// SCHEMA TYPE REGISTRY
// ================================================================================================

Expand All @@ -512,8 +536,8 @@ type WordTypeDisplayer = fn(Word) -> Result<String, SchemaTypeError>;
/// corresponding storage values.
#[derive(Clone, Debug, Default)]
pub struct SchemaTypeRegistry {
felt: BTreeMap<SchemaTypeId, FeltTypeConverter>,
word: BTreeMap<SchemaTypeId, WordTypeConverter>,
felt: BTreeMap<SchemaTypeId, FeltFromStrConverter>,
word: BTreeMap<SchemaTypeId, WordFromStrConverter>,
felt_display: BTreeMap<SchemaTypeId, FeltTypeDisplayer>,
word_display: BTreeMap<SchemaTypeId, WordTypeDisplayer>,
}
Expand Down Expand Up @@ -577,6 +601,29 @@ impl SchemaTypeRegistry {
display(felt).map(|_| ())
}

// VALUE VALIDATION HELPERS
// ============================================================================================

/// Validates that the given [`Word`] conforms to the specified schema type.
pub fn validate_word_value(
&self,
type_name: &SchemaTypeId,
word: Word,
) -> Result<(), SchemaTypeError> {
match self.type_kind(type_name) {
TypeKind::Word => Ok(()),
TypeKind::Felt => {
// Felt types stored as words must have the form [0, 0, 0, <felt>]
if word[0] != Felt::ZERO || word[1] != Felt::ZERO || word[2] != Felt::ZERO {
return Err(SchemaTypeError::ConversionError(format!(
"expected a word of the form [0, 0, 0, <felt>] for type `{type_name}`"
)));
}
self.validate_felt_value(type_name, word[3])
},
}
}

/// Converts a [`Felt`] into a canonical string representation for the given schema type.
///
/// This is intended for serializing schemas to TOML (e.g. default values).
Expand All @@ -588,24 +635,20 @@ impl SchemaTypeRegistry {
.unwrap_or_else(|| format!("0x{:x}", felt.as_int()))
}

/// Converts a [`Word`] into a canonical string representation for the given schema type.
///
/// Tries displaying as word if the converter is known, otherwise tests as a felt type, and
/// if it doesn't work, the word as hex is returned
// TODO: This should return richer information (how it was converted, if it succededs as word,
// etc.)
#[allow(dead_code)]
pub fn display_word(&self, type_name: &SchemaTypeId, word: Word) -> String {
/// Converts a [`Word`] into a canonical string representation and reports how it was produced.
pub fn display_word(&self, type_name: &SchemaTypeId, word: Word) -> WordDisplay {
if let Some(display) = self.word_display.get(type_name) {
return display(word).unwrap_or_else(|_| word.to_string());
let value = display(word).unwrap_or_else(|_| word.to_string());
return WordDisplay::Word(value);
}

// Treat any registered felt type as a word type by zero-padding the remaining felts.
if self.contains_felt_type(type_name) {
return self.display_felt(type_name, word[3]);
let value = self.display_felt(type_name, word[3]);
return WordDisplay::Felt(value);
}

word.to_hex()
WordDisplay::Hex(word.to_hex())
}

/// Attempts to parse a string into a `Word` using the registered converter for the given type
Expand Down Expand Up @@ -642,6 +685,14 @@ impl SchemaTypeRegistry {
self.felt.contains_key(type_name)
}

fn type_kind(&self, type_name: &SchemaTypeId) -> TypeKind {
if self.contains_felt_type(type_name) {
TypeKind::Felt
} else {
TypeKind::Word
}
}

/// Returns `true` if a `WordType` is registered for the given type.
///
/// This also returns `true` for any registered felt type (as those can be embedded into a word
Expand Down
Loading
Loading