miden_objects/account/code/
mod.rs

1use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};
2
3use vm_core::mast::MastForest;
4
5use super::{
6    AccountError, ByteReader, ByteWriter, Deserializable, DeserializationError, Digest, Felt,
7    Hasher, Serializable,
8};
9use crate::account::{AccountComponent, AccountType};
10
11pub mod procedure;
12use procedure::AccountProcedureInfo;
13
14// ACCOUNT CODE
15// ================================================================================================
16
17/// A public interface of an account.
18///
19/// Account's public interface consists of a set of account procedures, each procedure being a
20/// Miden VM program. Thus, MAST root of each procedure commits to the underlying program.
21///
22/// Each exported procedure is associated with a storage offset and a storage size.
23///
24/// We commit to the entire account interface by building a sequential hash of all procedure MAST
25/// roots and associated storage_offset's. Specifically, each procedure contributes exactly 8 field
26/// elements to the sequence of elements to be hashed. These elements are defined as follows:
27///
28/// ```text
29/// [PROCEDURE_MAST_ROOT, storage_offset, 0, 0, storage_size]
30/// ```
31#[derive(Debug, Clone)]
32pub struct AccountCode {
33    mast: Arc<MastForest>,
34    procedures: Vec<AccountProcedureInfo>,
35    commitment: Digest,
36}
37
38impl AccountCode {
39    /// The maximum number of account interface procedures.
40    pub const MAX_NUM_PROCEDURES: usize = 256;
41
42    // CONSTRUCTORS
43    // --------------------------------------------------------------------------------------------
44
45    /// Creates a new [`AccountCode`] from the provided components' libraries.
46    ///
47    /// For testing use only.
48    #[cfg(any(feature = "testing", test))]
49    pub fn from_components(
50        components: &[AccountComponent],
51        account_type: AccountType,
52    ) -> Result<Self, AccountError> {
53        super::validate_components_support_account_type(components, account_type)?;
54        Self::from_components_unchecked(components, account_type)
55    }
56
57    /// Creates a new [`AccountCode`] from the provided components' libraries.
58    ///
59    /// # Warning
60    ///
61    /// This does not check whether the provided components are valid when combined.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if:
66    /// - The number of procedures in all merged libraries is 0 or exceeds
67    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
68    /// - Two or more libraries export a procedure with the same MAST root.
69    /// - The number of [`StorageSlot`](crate::account::StorageSlot)s of a component or of all
70    ///   components exceeds 255.
71    /// - [`MastForest::merge`] fails on all libraries.
72    pub(super) fn from_components_unchecked(
73        components: &[AccountComponent],
74        account_type: AccountType,
75    ) -> Result<Self, AccountError> {
76        let (merged_mast_forest, _) =
77            MastForest::merge(components.iter().map(|component| component.mast_forest()))
78                .map_err(AccountError::AccountComponentMastForestMergeError)?;
79
80        let mut procedures = Vec::new();
81        let mut proc_root_set = BTreeSet::new();
82
83        // Slot 0 is globally reserved for faucet accounts so the accessible slots begin at 1 if
84        // there is a faucet component present.
85        let mut component_storage_offset = if account_type.is_faucet() { 1 } else { 0 };
86
87        for component in components {
88            let component_storage_size = component.storage_size();
89
90            for module in component.library().module_infos() {
91                for proc_mast_root in module.procedure_digests() {
92                    // We cannot support procedures from multiple components with the same MAST root
93                    // since storage offsets/sizes are set per MAST root. Setting them again for
94                    // procedures where the offset has already been inserted would cause that
95                    // procedure of the earlier component to write to the wrong slot.
96                    if !proc_root_set.insert(proc_mast_root) {
97                        return Err(AccountError::AccountComponentDuplicateProcedureRoot(
98                            proc_mast_root,
99                        ));
100                    }
101
102                    // Components that do not access storage need to have offset and size set to 0.
103                    let (storage_offset, storage_size) = if component_storage_size == 0 {
104                        (0, 0)
105                    } else {
106                        (component_storage_offset, component_storage_size)
107                    };
108
109                    // Note: Offset and size are validated in `AccountProcedureInfo::new`.
110                    procedures.push(AccountProcedureInfo::new(
111                        proc_mast_root,
112                        storage_offset,
113                        storage_size,
114                    )?);
115                }
116            }
117
118            component_storage_offset = component_storage_offset.checked_add(component_storage_size)
119              .expect("account procedure info constructor should return an error if the addition overflows");
120        }
121
122        // make sure the number of procedures is between 1 and 256 (both inclusive)
123        if procedures.is_empty() {
124            return Err(AccountError::AccountCodeNoProcedures);
125        } else if procedures.len() > Self::MAX_NUM_PROCEDURES {
126            return Err(AccountError::AccountCodeTooManyProcedures(procedures.len()));
127        }
128
129        Ok(Self {
130            commitment: build_procedure_commitment(&procedures),
131            procedures,
132            mast: Arc::new(merged_mast_forest),
133        })
134    }
135
136    /// Returns a new [AccountCode] deserialized from the provided bytes.
137    ///
138    /// # Errors
139    /// Returns an error if account code deserialization fails.
140    pub fn from_bytes(bytes: &[u8]) -> Result<Self, AccountError> {
141        Self::read_from_bytes(bytes).map_err(AccountError::AccountCodeDeserializationError)
142    }
143
144    /// Returns a new definition of an account's interface instantiated from the provided
145    /// [MastForest] and a list of [AccountProcedureInfo]s.
146    ///
147    /// # Panics
148    /// Panics if:
149    /// - The number of procedures is smaller than 1 or greater than 256.
150    /// - If some any of the provided procedures does not have a corresponding root in the provided
151    ///   MAST forest.
152    pub fn from_parts(mast: Arc<MastForest>, procedures: Vec<AccountProcedureInfo>) -> Self {
153        assert!(!procedures.is_empty(), "no account procedures");
154        assert!(procedures.len() <= Self::MAX_NUM_PROCEDURES, "too many account procedures");
155
156        Self {
157            commitment: build_procedure_commitment(&procedures),
158            procedures,
159            mast,
160        }
161    }
162
163    // PUBLIC ACCESSORS
164    // --------------------------------------------------------------------------------------------
165
166    /// Returns a commitment to an account's public interface.
167    pub fn commitment(&self) -> Digest {
168        self.commitment
169    }
170
171    /// Returns a reference to the [MastForest] backing this account code.
172    pub fn mast(&self) -> Arc<MastForest> {
173        self.mast.clone()
174    }
175
176    /// Returns a reference to the account procedures.
177    pub fn procedures(&self) -> &[AccountProcedureInfo] {
178        &self.procedures
179    }
180
181    /// Returns an iterator over the procedure MAST roots of this account code.
182    pub fn procedure_roots(&self) -> impl Iterator<Item = Digest> + '_ {
183        self.procedures().iter().map(|procedure| *procedure.mast_root())
184    }
185
186    /// Returns the number of public interface procedures defined in this account code.
187    pub fn num_procedures(&self) -> usize {
188        self.procedures.len()
189    }
190
191    /// Returns true if a procedure with the specified MAST root is defined in this account code.
192    pub fn has_procedure(&self, mast_root: Digest) -> bool {
193        self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
194    }
195
196    /// Returns information about the procedure at the specified index.
197    ///
198    /// # Panics
199    /// Panics if the provided index is out of bounds.
200    pub fn get_procedure_by_index(&self, index: usize) -> &AccountProcedureInfo {
201        &self.procedures[index]
202    }
203
204    /// Returns the procedure index for the procedure with the specified MAST root or None if such
205    /// procedure is not defined in this [AccountCode].
206    pub fn get_procedure_index_by_root(&self, root: Digest) -> Option<usize> {
207        self.procedures
208            .iter()
209            .map(|procedure| procedure.mast_root())
210            .position(|r| r == &root)
211    }
212
213    /// Converts procedure information in this [AccountCode] into a vector of field elements.
214    ///
215    /// This is done by first converting each procedure into 8 field elements as follows:
216    /// ```text
217    /// [PROCEDURE_MAST_ROOT, storage_offset, storage_size, 0, 0]
218    /// ```
219    /// And then concatenating the resulting elements into a single vector.
220    pub fn as_elements(&self) -> Vec<Felt> {
221        procedures_as_elements(self.procedures())
222    }
223}
224
225// EQUALITY
226// ================================================================================================
227
228impl PartialEq for AccountCode {
229    fn eq(&self, other: &Self) -> bool {
230        // TODO: consider checking equality based only on the set of procedures
231        self.mast == other.mast && self.procedures == other.procedures
232    }
233}
234
235impl Ord for AccountCode {
236    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
237        self.commitment.cmp(&other.commitment)
238    }
239}
240
241impl PartialOrd for AccountCode {
242    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
243        Some(self.cmp(other))
244    }
245}
246
247impl Eq for AccountCode {}
248
249// SERIALIZATION
250// ================================================================================================
251
252impl Serializable for AccountCode {
253    fn write_into<W: ByteWriter>(&self, target: &mut W) {
254        self.mast.write_into(target);
255        // since the number of procedures is guaranteed to be between 1 and 256, we can store the
256        // number as a single byte - but we do have to subtract 1 to store 256 as 255.
257        target.write_u8((self.procedures.len() - 1) as u8);
258        target.write_many(self.procedures());
259    }
260
261    fn get_size_hint(&self) -> usize {
262        // TODO: Replace with proper calculation.
263        let mut mast_forest_target = Vec::new();
264        self.mast.write_into(&mut mast_forest_target);
265
266        // Size of the serialized procedures length.
267        let u8_size = 0u8.get_size_hint();
268        let mut size = u8_size + mast_forest_target.len();
269
270        for procedure in self.procedures() {
271            size += procedure.get_size_hint();
272        }
273
274        size
275    }
276}
277
278impl Deserializable for AccountCode {
279    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
280        let module = Arc::new(MastForest::read_from(source)?);
281        let num_procedures = (source.read_u8()? as usize) + 1;
282        let procedures = source.read_many::<AccountProcedureInfo>(num_procedures)?;
283
284        Ok(Self::from_parts(module, procedures))
285    }
286}
287
288// HELPER FUNCTIONS
289// ================================================================================================
290
291/// Converts given procedures into field elements
292fn procedures_as_elements(procedures: &[AccountProcedureInfo]) -> Vec<Felt> {
293    procedures
294        .iter()
295        .flat_map(|procedure| <[Felt; 8]>::from(procedure.clone()))
296        .collect()
297}
298
299/// Computes the commitment to the given procedures
300fn build_procedure_commitment(procedures: &[AccountProcedureInfo]) -> Digest {
301    let elements = procedures_as_elements(procedures);
302    Hasher::hash_elements(&elements)
303}
304
305// TESTS
306// ================================================================================================
307
308#[cfg(test)]
309mod tests {
310    use assembly::Assembler;
311    use assert_matches::assert_matches;
312    use vm_core::Word;
313
314    use super::{AccountCode, Deserializable, Serializable};
315    use crate::{
316        account::{code::build_procedure_commitment, AccountComponent, AccountType, StorageSlot},
317        AccountError,
318    };
319
320    #[test]
321    fn test_serde_account_code() {
322        let code = AccountCode::mock();
323        let serialized = code.to_bytes();
324        let deserialized = AccountCode::read_from_bytes(&serialized).unwrap();
325        assert_eq!(deserialized, code)
326    }
327
328    #[test]
329    fn test_account_code_procedure_commitment() {
330        let code = AccountCode::mock();
331        let procedure_commitment = build_procedure_commitment(code.procedures());
332        assert_eq!(procedure_commitment, code.commitment())
333    }
334
335    #[test]
336    fn test_account_code_procedure_offset_out_of_bounds() {
337        let code1 = "export.foo add end";
338        let library1 = Assembler::default().assemble_library([code1]).unwrap();
339        let code2 = "export.bar sub end";
340        let library2 = Assembler::default().assemble_library([code2]).unwrap();
341
342        let component1 =
343            AccountComponent::new(library1, vec![StorageSlot::Value(Word::default()); 250])
344                .unwrap()
345                .with_supports_all_types();
346        let mut component2 =
347            AccountComponent::new(library2, vec![StorageSlot::Value(Word::default()); 5])
348                .unwrap()
349                .with_supports_all_types();
350
351        // This is fine as the offset+size for component 2 is <= 255.
352        AccountCode::from_components(
353            &[component1.clone(), component2.clone()],
354            AccountType::RegularAccountUpdatableCode,
355        )
356        .unwrap();
357
358        // Push one more slot so offset+size exceeds 255.
359        component2.storage_slots.push(StorageSlot::Value(Word::default()));
360
361        let err = AccountCode::from_components(
362            &[component1, component2],
363            AccountType::RegularAccountUpdatableCode,
364        )
365        .unwrap_err();
366
367        assert_matches!(err, AccountError::StorageOffsetPlusSizeOutOfBounds(256))
368    }
369}