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