From af0cd5024d86e85a72265ec9896747fa1fdff8e3 Mon Sep 17 00:00:00 2001 From: zeapoz Date: Thu, 16 Jul 2026 13:25:13 +0200 Subject: [PATCH 1/2] refactor: introduce double word newtype --- crates/miden-agglayer/src/bridge.rs | 14 +- .../src/account/storage/header.rs | 29 +- .../miden-protocol/src/account/storage/mod.rs | 2 +- crates/miden-protocol/src/asset/mod.rs | 8 +- crates/miden-protocol/src/batch/kernel.rs | 11 +- crates/miden-protocol/src/lib.rs | 2 + .../src/transaction/kernel/mod.rs | 22 +- crates/miden-protocol/src/types.rs | 588 ++++++++++++++++++ crates/miden-tx/src/host/kernel_process.rs | 23 +- 9 files changed, 628 insertions(+), 71 deletions(-) create mode 100644 crates/miden-protocol/src/types.rs diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index ed25fe36d0..2542fbd26a 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -5,6 +5,7 @@ use alloc::vec; use alloc::vec::Vec; use miden_core::{Felt, ONE, Word, ZERO}; +use miden_protocol::DoubleWord; use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata}; use miden_protocol::account::{ Account, @@ -467,9 +468,9 @@ impl AggLayerBridge { Self::assert_bridge_account(bridge_account)?; // Compute the expected GER hash: poseidon2::merge(GER_LOWER, GER_UPPER) - let ger_lower: Word = ger.to_elements()[0..4].try_into().unwrap(); - let ger_upper: Word = ger.to_elements()[4..8].try_into().unwrap(); - let ger_hash = Poseidon2::merge(&[ger_lower, ger_upper]); + let ger_words = DoubleWord::try_from(ger.to_elements().as_slice()) + .expect("exit root should contain 8 felts"); + let ger_hash = Poseidon2::merge(&[ger_words.lo(), ger_words.hi()]); // Get the value stored by the GER hash. If this GER was registered, the value would be // equal to [1, 0, 0, 0] @@ -514,11 +515,8 @@ impl AggLayerBridge { .get_item(root_hi_slot) .expect("should be able to read LET root hi"); - let mut root = Vec::with_capacity(8); - root.extend(root_lo.to_vec()); - root.extend(root_hi.to_vec()); - - Ok(root) + let dword = DoubleWord::new(root_lo, root_hi); + Ok(dword.to_vec()) } /// Returns the number of leaves in the Local Exit Tree (LET) frontier. diff --git a/crates/miden-protocol/src/account/storage/header.rs b/crates/miden-protocol/src/account/storage/header.rs index a9a401c12c..3e5fe623c6 100644 --- a/crates/miden-protocol/src/account/storage/header.rs +++ b/crates/miden-protocol/src/account/storage/header.rs @@ -5,7 +5,7 @@ use alloc::vec::Vec; use super::map::EMPTY_STORAGE_MAP_ROOT; use super::{AccountStorage, Felt, StorageSlotType, Word}; -use crate::ZERO; +use crate::DoubleWord; use crate::account::{StorageSlot, StorageSlotId, StorageSlotName}; use crate::crypto::SequentialCommit; use crate::errors::AccountError; @@ -215,7 +215,7 @@ impl SequentialCommit for AccountStorageHeader { type Commitment = Word; fn to_elements(&self) -> Vec { - self.slots().flat_map(|slot| slot.to_elements()).collect() + self.slots().flat_map(|slot| slot.to_dword()).collect() } } @@ -297,23 +297,14 @@ impl StorageSlotHeader { self.value } - /// Returns this storage slot header as field elements. + /// Returns this storage slot header as a [`DoubleWord`]. /// - /// This is done by converting this storage slot into 8 field elements as follows: - /// ```text - /// [[0, slot_type, slot_id_suffix, slot_id_prefix], SLOT_VALUE] - /// ``` - pub(crate) fn to_elements(&self) -> [Felt; StorageSlot::NUM_ELEMENTS] { + /// The low word is `[0, slot_type, slot_id_suffix, slot_id_prefix]` and the high word is the + /// slot value. + pub(crate) fn to_dword(&self) -> DoubleWord { let id = self.id(); - let mut elements = [ZERO; StorageSlot::NUM_ELEMENTS]; - elements[0..4].copy_from_slice(&[ - Felt::ZERO, - self.r#type.as_felt(), - id.suffix(), - id.prefix(), - ]); - elements[4..8].copy_from_slice(self.value.as_elements()); - elements + let header_word = Word::new([Felt::ZERO, self.r#type.as_felt(), id.suffix(), id.prefix()]); + DoubleWord::new(header_word, self.value) } } @@ -503,7 +494,7 @@ mod tests { ); // Serialize the single slot to elements - let elements = slot1.to_elements(); + let elements = slot1.to_dword(); // Create slot names map using the slot's ID let mut slot_names = BTreeMap::new(); @@ -511,7 +502,7 @@ mod tests { // Test from_elements with provided slot names on raw slot elements. let reconstructed_header = - AccountStorageHeader::try_from_elements(&elements, &slot_names).unwrap(); + AccountStorageHeader::try_from_elements(elements.as_elements(), &slot_names).unwrap(); // Verify that the original slot names are preserved. assert_eq!(reconstructed_header.slots().count(), 1); diff --git a/crates/miden-protocol/src/account/storage/mod.rs b/crates/miden-protocol/src/account/storage/mod.rs index 900838f057..a2e4039f3a 100644 --- a/crates/miden-protocol/src/account/storage/mod.rs +++ b/crates/miden-protocol/src/account/storage/mod.rs @@ -438,7 +438,7 @@ impl SequentialCommit for AccountStorage { slot.content().slot_type(), slot.content().value(), ) - .to_elements() + .to_dword() }) .collect() } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 62de5ebe6e..54468b5d09 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -6,7 +6,7 @@ use super::utils::serde::{ DeserializationError, Serializable, }; -use super::{Felt, Word}; +use super::{DoubleWord, Felt, Word}; use crate::account::AccountId; mod asset_amount; @@ -182,10 +182,8 @@ impl Asset { /// The first four elements contain the asset ID and the last four elements contain the asset /// value. pub fn as_elements(&self) -> [Felt; 8] { - let mut elements = [Felt::ZERO; 8]; - elements[0..4].copy_from_slice(self.to_id_word().as_elements()); - elements[4..8].copy_from_slice(self.to_value_word().as_elements()); - elements + let dword = DoubleWord::new(self.to_id_word(), self.to_value_word()); + dword.into() } /// Returns the inner [`FungibleAsset`]. diff --git a/crates/miden-protocol/src/batch/kernel.rs b/crates/miden-protocol/src/batch/kernel.rs index 957209e0b1..156ad8ccea 100644 --- a/crates/miden-protocol/src/batch/kernel.rs +++ b/crates/miden-protocol/src/batch/kernel.rs @@ -1,12 +1,10 @@ -use alloc::vec::Vec; - use miden_core::program::Kernel; use crate::batch::{BatchId, ProposedBatch}; use crate::utils::serde::Deserializable; use crate::utils::sync::LazyLock; use crate::vm::{AdviceInputs, Package, Program, ProgramInfo, StackInputs}; -use crate::{Felt, Word}; +use crate::{DoubleWord, Word}; // CONSTANTS // ================================================================================================ @@ -75,11 +73,8 @@ impl BatchKernel { /// - `BLOCK_COMMITMENT` is the commitment of the batch's reference block. /// - `BATCH_ID` is the batch's [`BatchId`]. pub fn build_input_stack(block_commitment: Word, batch_id: BatchId) -> StackInputs { - let mut inputs: Vec = Vec::with_capacity(8); - inputs.extend_from_slice(block_commitment.as_elements()); - inputs.extend_from_slice(batch_id.as_word().as_elements()); - - StackInputs::new(&inputs).expect("number of stack inputs should be <= 16") + let inputs = DoubleWord::new(block_commitment, batch_id.as_word()); + StackInputs::new(inputs.as_elements()).expect("number of stack inputs should be <= 16") } // ADVICE BUILDER diff --git a/crates/miden-protocol/src/lib.rs b/crates/miden-protocol/src/lib.rs index 678fa90281..c391072cb8 100644 --- a/crates/miden-protocol/src/lib.rs +++ b/crates/miden-protocol/src/lib.rs @@ -16,6 +16,7 @@ pub mod note; pub mod package; mod protocol; pub mod transaction; +pub mod types; #[cfg(any(feature = "testing", test))] pub mod testing; @@ -34,6 +35,7 @@ pub use miden_crypto::hash::poseidon2::Poseidon2 as Hasher; pub use miden_crypto::word; pub use miden_crypto::word::{Word, WordError}; pub use protocol::ProtocolLib; +pub use types::DoubleWord; pub mod assembly { pub use miden_assembly::ast::{Module, ModuleKind, ProcedureName, QualifiedProcedureName}; diff --git a/crates/miden-protocol/src/transaction/kernel/mod.rs b/crates/miden-protocol/src/transaction/kernel/mod.rs index df0651ba81..98e35b3923 100644 --- a/crates/miden-protocol/src/transaction/kernel/mod.rs +++ b/crates/miden-protocol/src/transaction/kernel/mod.rs @@ -24,7 +24,7 @@ use crate::vm::{ StackInputs, StackOutputs, }; -use crate::{Felt, Hasher, Word}; +use crate::{DoubleWord, Felt, Hasher, Word}; mod procedures { include!(concat!(env!("OUT_DIR"), "/procedures.rs")); @@ -431,23 +431,13 @@ impl TransactionKernel { ) })?; - if account_update_data.len() != 8 { - return Err(TransactionOutputError::AccountUpdateCommitment( + let account_update = DoubleWord::try_from(account_update_data.as_ref()).map_err(|_| { + TransactionOutputError::AccountUpdateCommitment( "expected account update commitment advice map entry to contain exactly 8 elements" .into(), - )); - } - - // SAFETY: We just asserted that the data is of length 8 so slicing the data into two words - // is fine. - let final_account_commitment = Word::from( - <[Felt; 4]>::try_from(&account_update_data[0..4]) - .expect("we should have sliced off exactly four elements"), - ); - let account_patch_commitment = Word::from( - <[Felt; 4]>::try_from(&account_update_data[4..8]) - .expect("we should have sliced off exactly four elements"), - ); + ) + })?; + let (final_account_commitment, account_patch_commitment) = account_update.into_tuple(); let computed_account_update_commitment = Hasher::merge(&[final_account_commitment, account_patch_commitment]); diff --git a/crates/miden-protocol/src/types.rs b/crates/miden-protocol/src/types.rs new file mode 100644 index 0000000000..17701a8798 --- /dev/null +++ b/crates/miden-protocol/src/types.rs @@ -0,0 +1,588 @@ +//! A [`DoubleWord`] type used in the Miden protocol and associated utilities. + +use alloc::vec::Vec; +use core::hash::{Hash, Hasher}; +use core::mem::size_of; +use core::ops::{Deref, DerefMut}; + +use crate::{Felt, Word, WordError}; + +// DOUBLE WORD +// ================================================================================================ + +/// A unit of data consisting of 8 field elements (a "double word"). +/// +/// Conceptually this is two [`Word`]s: `word_lo` (elements 0..4) followed by `word_hi` (elements +/// 4..8). +/// +/// # Examples +/// +/// ``` +/// use miden_protocol::{DoubleWord, Felt, Word}; +/// +/// let lo = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]); +/// let hi = Word::new([Felt::ZERO, Felt::ONE, Felt::ZERO, Felt::ZERO]); +/// let dword = DoubleWord::new(lo, hi); +/// assert_eq!(dword.lo(), lo); +/// assert_eq!(dword.hi(), hi); +/// ``` +#[derive(Default, Copy, Clone, Eq, PartialEq)] +#[repr(C)] +pub struct DoubleWord { + word_lo: Word, + word_hi: Word, +} + +// Compile-time assertions to ensure `DoubleWord` has the same layout as `[Felt; 8]`. This is +// relied upon in `as_elements_array`/`as_elements_array_mut`. +const _: () = { + assert!(DoubleWord::NUM_ELEMENTS == 8, "DoubleWord::NUM_ELEMENTS is assumed to be 8"); + assert!( + DoubleWord::SERIALIZED_SIZE == 64, + "DoubleWord::SERIALIZED_SIZE is assumed to be 64" + ); + assert!(size_of::() == DoubleWord::NUM_ELEMENTS * size_of::()); + assert!(core::mem::offset_of!(DoubleWord, word_lo) == 0); + assert!(core::mem::offset_of!(DoubleWord, word_hi) == Word::SERIALIZED_SIZE); +}; + +impl core::fmt::Debug for DoubleWord { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("DoubleWord").field(&self.into_elements()).finish() + } +} + +impl DoubleWord { + /// The number of field elements in the double word. + pub const NUM_ELEMENTS: usize = Word::NUM_ELEMENTS * 2; + + /// The serialized size of the double word in bytes. + pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE * 2; + + /// Creates a new [`DoubleWord`] from two [`Word`]s. + pub const fn new(word_lo: Word, word_hi: Word) -> Self { + Self { word_lo, word_hi } + } + + /// Returns the low word of this double word. + pub const fn lo(&self) -> Word { + self.word_lo + } + + /// Returns the high word of this double word. + pub const fn hi(&self) -> Word { + self.word_hi + } + + /// Returns the elements of this double word as an array. + /// + /// # Examples + /// + /// ``` + /// use miden_protocol::{DoubleWord, Felt, Word}; + /// + /// let lo = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + /// let hi = Word::new([Felt::ZERO, Felt::ONE, Felt::ZERO, Felt::ZERO]); + /// let dword = DoubleWord::new(lo, hi); + /// assert_eq!( + /// dword.into_elements(), + /// [ + /// Felt::ONE, + /// Felt::ZERO, + /// Felt::ZERO, + /// Felt::ZERO, + /// Felt::ZERO, + /// Felt::ONE, + /// Felt::ZERO, + /// Felt::ZERO + /// ] + /// ); + /// ``` + pub fn into_elements(self) -> [Felt; Self::NUM_ELEMENTS] { + let [a, b, c, d] = self.word_lo.into_elements(); + let [e, f, g, h] = self.word_hi.into_elements(); + [a, b, c, d, e, f, g, h] + } + + /// Returns the two [`Word`]s of this double word as a tuple in the following format: `(low, + /// high)`. + pub fn into_tuple(self) -> (Word, Word) { + (self.word_lo, self.word_hi) + } + + /// Returns the elements of this double word as an array reference. + /// + /// # Safety + /// This assumes the two [`Word`] fields of [`DoubleWord`] are laid out contiguously with no + /// padding, in the same order as `[Felt; 8]`. + fn as_elements_array(&self) -> &[Felt; Self::NUM_ELEMENTS] { + unsafe { &*(&self.word_lo as *const Word as *const [Felt; Self::NUM_ELEMENTS]) } + } + + /// Returns the elements of this double word as a mutable array reference. + /// + /// # Safety + /// This assumes the two [`Word`] fields of [`DoubleWord`] are laid out contiguously with no + /// padding, in the same order as `[Felt; 8]`. + fn as_elements_array_mut(&mut self) -> &mut [Felt; Self::NUM_ELEMENTS] { + unsafe { &mut *(&mut self.word_lo as *mut Word as *mut [Felt; Self::NUM_ELEMENTS]) } + } + + /// Returns the double word as a slice of field elements. + pub fn as_elements(&self) -> &[Felt] { + self.as_elements_array() + } + + /// Returns the double word as a byte array. + pub fn as_bytes(&self) -> [u8; Self::SERIALIZED_SIZE] { + let mut result = [0; Self::SERIALIZED_SIZE]; + result[..Word::SERIALIZED_SIZE].copy_from_slice(&self.word_lo.as_bytes()); + result[Word::SERIALIZED_SIZE..].copy_from_slice(&self.word_hi.as_bytes()); + result + } + + /// Returns internal elements of this double word as a vector. + pub fn to_vec(&self) -> Vec { + self.as_elements().to_vec() + } + + /// Returns a new [`DoubleWord`] consisting of eight ZERO elements. + /// + /// # Examples + /// + /// ``` + /// use miden_protocol::{DoubleWord, Felt}; + /// + /// let dword = DoubleWord::empty(); + /// assert!(dword.is_empty()); + /// ``` + pub const fn empty() -> Self { + Self::new(Word::empty(), Word::empty()) + } + + /// Returns true if the double word consists of eight ZERO elements. + /// + /// # Examples + /// + /// ``` + /// use miden_protocol::{DoubleWord, Felt, Word}; + /// + /// let dword = DoubleWord::new(Word::empty(), Word::empty()); + /// assert!(dword.is_empty()); + /// + /// let lo = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + /// let dword2 = DoubleWord::new(lo, Word::empty()); + /// assert!(!dword2.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.word_lo.is_empty() && self.word_hi.is_empty() + } +} + +// TRAIT IMPLEMENTATIONS +// ================================================================================================ + +impl Hash for DoubleWord { + fn hash(&self, state: &mut H) { + state.write(&self.as_bytes()); + } +} + +impl Deref for DoubleWord { + type Target = [Felt; DoubleWord::NUM_ELEMENTS]; + + fn deref(&self) -> &Self::Target { + self.as_elements_array() + } +} + +impl DerefMut for DoubleWord { + fn deref_mut(&mut self) -> &mut Self::Target { + self.as_elements_array_mut() + } +} + +impl IntoIterator for DoubleWord { + type Item = Felt; + type IntoIter = <[Felt; 8] as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.into_elements().into_iter() + } +} + +// CONVERSIONS: FROM DOUBLE WORD +// ================================================================================================ + +impl From for [Felt; DoubleWord::NUM_ELEMENTS] { + fn from(value: DoubleWord) -> Self { + value.into_elements() + } +} + +impl From for Vec { + fn from(value: DoubleWord) -> Self { + value.to_vec() + } +} + +impl From for (Word, Word) { + fn from(value: DoubleWord) -> Self { + value.into_tuple() + } +} + +// CONVERSIONS: TO DOUBLE WORD +// ================================================================================================ + +impl From<[Felt; DoubleWord::NUM_ELEMENTS]> for DoubleWord { + fn from(value: [Felt; DoubleWord::NUM_ELEMENTS]) -> Self { + let word_lo = Word::new([value[0], value[1], value[2], value[3]]); + let word_hi = Word::new([value[4], value[5], value[6], value[7]]); + Self::new(word_lo, word_hi) + } +} + +impl From<&[Felt; DoubleWord::NUM_ELEMENTS]> for DoubleWord { + fn from(value: &[Felt; DoubleWord::NUM_ELEMENTS]) -> Self { + Self::from(*value) + } +} + +impl TryFrom<&[Felt]> for DoubleWord { + type Error = WordError; + + fn try_from(value: &[Felt]) -> Result { + let value: [Felt; DoubleWord::NUM_ELEMENTS] = value.try_into().map_err(|_| { + WordError::InvalidInputLength("elements", DoubleWord::NUM_ELEMENTS, value.len()) + })?; + Ok(value.into()) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use core::hash::Hasher; + use std::collections::hash_map::DefaultHasher; + + use rstest::rstest; + + use super::*; + + fn felt(n: u64) -> Felt { + Felt::new_unchecked(n) + } + + fn make_lo() -> Word { + Word::new([felt(1), felt(2), felt(3), felt(4)]) + } + + fn make_hi() -> Word { + Word::new([felt(5), felt(6), felt(7), felt(8)]) + } + + fn make_dword() -> DoubleWord { + DoubleWord::new(make_lo(), make_hi()) + } + + // CONSTRUCTOR AND ACCESSORS + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_new_and_accessors() { + let dword = make_dword(); + assert_eq!(dword.lo(), make_lo()); + assert_eq!(dword.hi(), make_hi()); + } + + #[test] + fn dword_into_elements_ordering() { + let dword = make_dword(); + let elements = dword.into_elements(); + assert_eq!( + elements, + [felt(1), felt(2), felt(3), felt(4), felt(5), felt(6), felt(7), felt(8)] + ); + } + + #[test] + fn dword_as_elements_matches_into_elements() { + let dword = make_dword(); + let owned = dword.into_elements(); + let borrowed: &[Felt; DoubleWord::NUM_ELEMENTS] = dword.deref(); + assert_eq!(&owned, borrowed); + } + + #[test] + fn dword_into_tuple() { + let dword = make_dword(); + let (lo, hi) = dword.into_tuple(); + assert_eq!(lo, make_lo()); + assert_eq!(hi, make_hi()); + } + + #[test] + fn dword_as_bytes_layout() { + let dword = make_dword(); + let bytes = dword.as_bytes(); + assert_eq!(bytes.len(), DoubleWord::SERIALIZED_SIZE); + + let lo_bytes = make_lo().as_bytes(); + let hi_bytes = make_hi().as_bytes(); + let mut expected = [0u8; DoubleWord::SERIALIZED_SIZE]; + expected[..Word::SERIALIZED_SIZE].copy_from_slice(&lo_bytes); + expected[Word::SERIALIZED_SIZE..].copy_from_slice(&hi_bytes); + assert_eq!(bytes, expected); + } + + #[test] + fn dword_to_vec() { + let dword = make_dword(); + let v: Vec = dword.to_vec(); + assert_eq!(v.len(), DoubleWord::NUM_ELEMENTS); + assert_eq!(v, dword.into_elements().to_vec()); + } + + // EMPTY / DEFAULT + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_empty() { + let dword = DoubleWord::empty(); + assert_eq!(dword.lo(), Word::empty()); + assert_eq!(dword.hi(), Word::empty()); + } + + #[rstest] + #[case(true, Word::empty(), Word::empty())] + #[case(false, make_lo(), Word::empty())] + #[case(false, Word::empty(), make_hi())] + #[case(false, make_lo(), make_hi())] + fn dword_is_empty(#[case] expected: bool, #[case] lo: Word, #[case] hi: Word) { + assert_eq!(DoubleWord::new(lo, hi).is_empty(), expected); + } + + #[test] + fn dword_default_equals_empty() { + assert_eq!(DoubleWord::default(), DoubleWord::empty()); + } + + // LAYOUT (unsafe pointer correctness) + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_elements_array_layout() { + let dword = make_dword(); + + let elements = dword.as_elements(); + assert_eq!( + elements, + &[felt(1), felt(2), felt(3), felt(4), felt(5), felt(6), felt(7), felt(8)] + ); + + let lo_ptr = core::ptr::addr_of!(dword.word_lo); + assert_eq!(elements.as_ptr() as *const Word, lo_ptr); + + let hi_ptr = core::ptr::addr_of!(dword.word_hi); + assert_eq!(unsafe { elements.as_ptr().add(4) } as *const Word, hi_ptr); + } + + // DEREF / DEREFMUT + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_deref_read() { + let dword = make_dword(); + assert_eq!(dword[0], felt(1)); + assert_eq!(dword[3], felt(4)); + assert_eq!(dword[4], felt(5)); + assert_eq!(dword[7], felt(8)); + } + + #[test] + fn dword_deref_mut_write() { + let mut dword = make_dword(); + dword[0] = felt(99); + dword[7] = felt(100); + assert_eq!(dword.lo()[0], felt(99)); + assert_eq!(dword.hi()[3], felt(100)); + } + + #[test] + fn dword_index_matches_into_elements() { + let dword = make_dword(); + let elements = dword.into_elements(); + for idx in 0..DoubleWord::NUM_ELEMENTS { + assert_eq!(dword[idx], elements[idx]); + } + } + + #[test] + fn dword_index_mut_updates_all_elements() { + let mut dword = make_dword(); + let new_values: [Felt; DoubleWord::NUM_ELEMENTS] = + [felt(10), felt(20), felt(30), felt(40), felt(50), felt(60), felt(70), felt(80)]; + for idx in 0..DoubleWord::NUM_ELEMENTS { + dword[idx] = new_values[idx]; + } + assert_eq!(dword.into_elements(), new_values); + } + + #[test] + fn dword_index_mut_range_updates_slice() { + let mut dword = make_dword(); + let replacement = [felt(90), felt(91)]; + dword[2..4].copy_from_slice(&replacement); + assert_eq!(dword[2], felt(90)); + assert_eq!(dword[3], felt(91)); + } + + // INTO ITERATOR + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_into_iter() { + let dword = make_dword(); + let collected: Vec = dword.into_iter().collect(); + assert_eq!(collected, dword.into_elements().to_vec()); + } + + // HASH + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_hash_equal_for_equal_values() { + let a = make_dword(); + let b = make_dword(); + + let hasher = DefaultHasher::new(); + let mut ha = hasher.clone(); + let mut hb = hasher; + a.hash(&mut ha); + b.hash(&mut hb); + assert_eq!(ha.finish(), hb.finish()); + } + + #[test] + fn dword_hash_differs_for_different_values() { + let a = make_dword(); + let b = DoubleWord::empty(); + + let hasher = DefaultHasher::new(); + let mut ha = hasher.clone(); + let mut hb = hasher; + a.hash(&mut ha); + b.hash(&mut hb); + assert_ne!(ha.finish(), hb.finish()); + } + + // COPY / CLONE / EQ + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_copy_independence() { + let original = make_dword(); + let copy = original; + // Both should be equal (Copy semantics). + assert_eq!(original, copy); + // Mutating a copy through a rebinding does not affect the original. + let mut modified = copy; + modified[0] = felt(999); + assert_eq!(original[0], felt(1)); + assert_eq!(modified[0], felt(999)); + } + + #[test] + fn dword_eq_inequality() { + assert_eq!(make_dword(), make_dword()); + assert_ne!(make_dword(), DoubleWord::empty()); + assert_ne!(DoubleWord::new(make_lo(), make_hi()), DoubleWord::new(make_hi(), make_lo())); + } + + // DEBUG + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_debug_format() { + let dword = make_dword(); + let debug = alloc::format!("{dword:?}"); + assert!(debug.starts_with("DoubleWord("), "unexpected debug format: {debug}"); + } + + // CONSTANTS + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_constants() { + assert_eq!(DoubleWord::NUM_ELEMENTS, 8); + assert_eq!(DoubleWord::SERIALIZED_SIZE, 64); + } + + // CONVERSIONS + // ---------------------------------------------------------------------------------------- + + #[test] + fn dword_felt_array_roundtrip() { + let elements: [Felt; DoubleWord::NUM_ELEMENTS] = + [felt(10), felt(20), felt(30), felt(40), felt(50), felt(60), felt(70), felt(80)]; + let dword = DoubleWord::from(elements); + let round_trip: [Felt; DoubleWord::NUM_ELEMENTS] = dword.into(); + assert_eq!(elements, round_trip); + } + + #[test] + fn dword_from_ref_felt_array() { + let elements: [Felt; DoubleWord::NUM_ELEMENTS] = + [felt(10), felt(20), felt(30), felt(40), felt(50), felt(60), felt(70), felt(80)]; + let from_owned = DoubleWord::from(elements); + let from_ref = DoubleWord::from(&elements); + assert_eq!(from_owned, from_ref); + } + + #[test] + fn dword_word_tuple_roundtrip() { + let dword = make_dword(); + let tuple: (Word, Word) = dword.into(); + let round_trip = DoubleWord::new(tuple.0, tuple.1); + assert_eq!(make_dword(), round_trip); + } + + #[test] + fn dword_from_double_word_to_vec() { + let dword = make_dword(); + let v: Vec = dword.into(); + assert_eq!(v, dword.into_elements().to_vec()); + } + + #[test] + fn dword_from_double_word_to_array() { + let dword = make_dword(); + let arr: [Felt; DoubleWord::NUM_ELEMENTS] = dword.into(); + assert_eq!(arr, dword.into_elements()); + } + + #[test] + fn dword_try_from_felt_slice_correct_length() { + let elements: Vec = (1..=8).map(felt).collect(); + let dword = DoubleWord::try_from(elements.as_slice()).unwrap(); + assert_eq!( + dword.into_elements(), + [felt(1), felt(2), felt(3), felt(4), felt(5), felt(6), felt(7), felt(8)] + ); + } + + #[rstest] + #[case::empty(&[])] + #[case::too_few_4(&[felt(1); 4])] + #[case::too_few_7(&[felt(1); 7])] + #[case::too_many_9(&[felt(1); 9])] + #[case::too_many_16(&[felt(1); 16])] + fn dword_try_from_felt_slice_wrong_length(#[case] slice: &[Felt]) { + let result = DoubleWord::try_from(slice); + assert!(result.is_err()); + } +} diff --git a/crates/miden-tx/src/host/kernel_process.rs b/crates/miden-tx/src/host/kernel_process.rs index dc8ec218aa..27411cccee 100644 --- a/crates/miden-tx/src/host/kernel_process.rs +++ b/crates/miden-tx/src/host/kernel_process.rs @@ -1,5 +1,4 @@ use miden_processor::{ExecutionError, Felt, ProcessorState}; -use miden_protocol::Word; use miden_protocol::account::{AccountId, StorageSlotId, StorageSlotType}; use miden_protocol::note::{NoteId, NoteStorage}; use miden_protocol::transaction::memory::{ @@ -15,6 +14,7 @@ use miden_protocol::transaction::memory::{ NATIVE_NUM_ACCT_STORAGE_SLOTS_PTR, NUM_OUTPUT_NOTES_PTR, }; +use miden_protocol::{DoubleWord, Word}; use crate::errors::TransactionKernelError; @@ -262,9 +262,10 @@ impl<'a> TransactionKernelProcess for ProcessorState<'a> { recipient_digest: Word, ) -> Result<(NoteStorage, Word, Word), TransactionKernelError> { let (sn_script_hash, storage_commitment) = - read_double_word_from_adv_map(self, recipient_digest)?; - let (sn_hash, script_root) = read_double_word_from_adv_map(self, sn_script_hash)?; - let (serial_num, _) = read_double_word_from_adv_map(self, sn_hash)?; + read_double_word_from_adv_map(self, recipient_digest)?.into_tuple(); + let (sn_hash, script_root) = + read_double_word_from_adv_map(self, sn_script_hash)?.into_tuple(); + let (serial_num, _) = read_double_word_from_adv_map(self, sn_hash)?.into_tuple(); let inputs = self.read_note_storage_from_adv_map(&storage_commitment)?; @@ -319,7 +320,7 @@ impl<'a> TransactionKernelProcess for ProcessorState<'a> { // HELPER FUNCTIONS // ================================================================================================ -/// Reads a double word (two [`Word`]s, 8 [`Felt`]s total) from the advice map. +/// Reads a [`DoubleWord`] from the advice map. /// /// # Errors /// Returns an error if the key is not present in the advice map or if the data is malformed @@ -327,18 +328,12 @@ impl<'a> TransactionKernelProcess for ProcessorState<'a> { fn read_double_word_from_adv_map( process: &ProcessorState, key: Word, -) -> Result<(Word, Word), TransactionKernelError> { +) -> Result { let data = process .advice_provider() .get_mapped_values(&key) .ok_or_else(|| TransactionKernelError::MalformedRecipientData(vec![]))?; - if data.len() != 8 { - return Err(TransactionKernelError::MalformedRecipientData(data.to_vec())); - } - - let first_word = Word::new([data[0], data[1], data[2], data[3]]); - let second_word = Word::new([data[4], data[5], data[6], data[7]]); - - Ok((first_word, second_word)) + DoubleWord::try_from(data) + .map_err(|_| TransactionKernelError::MalformedRecipientData(data.to_vec())) } From 0f7c8e83b0716bc2c2d3740bc1deda744f76eff1 Mon Sep 17 00:00:00 2001 From: zeapoz Date: Thu, 16 Jul 2026 14:13:47 +0200 Subject: [PATCH 2/2] chore: add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb39be8dac..1e4963b6aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added the `miden::standards::assets::non_fungible_asset::validate` MASM procedure, which validates a non-fungible asset's composition and the binding of its value to the asset class, and used it in the `NonFungibleFaucet` burn procedure ([#3308](https://github.com/0xMiden/protocol/pull/3308)). ### Changes +- Introduced the `DoubleWord` newtype (8 `Felt`s) and used it internally to replace ad-hoc `[Felt; 8]` / `Vec` representations ([#3319](https://github.com/0xMiden/protocol/pull/3319)). - [BREAKING] Transaction fees are now paid by the authentication procedure creating a public TX_FEE note before the transaction summary is created, so the fee payment is covered by the signature (`miden::standards::fee`). The payment asset and conversion rate are committed to via the auth args (see `FeeConversionInfo`); on zero-base-fee chains no note is created ([#2899](https://github.com/0xMiden/protocol/discussions/2899)).