use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cmp::Ordering;
use miden_core::mast::MastForest;
use miden_core::prettier::PrettyPrint;
use miden_mast_package::debug_info::PackageDebugInfo;
use miden_processor::LoadedMastForest;
use super::{
AccountError,
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Felt,
Hasher,
Serializable,
};
use crate::Word;
use crate::account::{AccountCodeInterface, AccountComponent, AccountId};
use crate::package::{loaded_mast_forest, package_debug_info};
pub mod procedure;
use procedure::{AccountProcedureRoot, PrintableProcedure};
#[derive(Debug, Clone)]
pub struct AccountCode {
mast: Arc<MastForest>,
procedures: Vec<AccountProcedureRoot>,
commitment: Word,
package_debug_info: Option<Arc<PackageDebugInfo>>,
}
impl AccountCode {
pub const MIN_NUM_PROCEDURES: usize = 2;
pub const MAX_NUM_PROCEDURES: usize = 256;
pub fn from_parts(
mast: Arc<MastForest>,
procedures: Vec<AccountProcedureRoot>,
) -> Result<Self, AccountError> {
if procedures.len() < Self::MIN_NUM_PROCEDURES {
return Err(AccountError::AccountCodeNoProcedures);
}
if procedures.len() > Self::MAX_NUM_PROCEDURES {
return Err(AccountError::AccountCodeTooManyProcedures(procedures.len()));
}
let (auth_proc, other_procs) = procedures
.split_first()
.expect("account code should contain at least two procedures");
let mut previous_proc: Option<&AccountProcedureRoot> = None;
for procedure in other_procs {
if procedure == auth_proc {
return Err(AccountError::AccountCodeDuplicateProcedureRoot(*procedure));
}
if let Some(previous_proc) = previous_proc {
match previous_proc.cmp(procedure) {
Ordering::Less => {},
Ordering::Equal => {
return Err(AccountError::AccountCodeDuplicateProcedureRoot(*procedure));
},
Ordering::Greater => return Err(AccountError::AccountCodeProceduresUnsorted),
}
}
previous_proc = Some(procedure);
}
for procedure in procedures.iter() {
if mast.find_procedure_root(procedure.as_word()).is_none() {
return Err(AccountError::AccountCodeProcedureNotInMastForest(*procedure));
}
}
Ok(Self {
commitment: build_procedure_commitment(&procedures),
procedures,
mast,
package_debug_info: None,
})
}
#[cfg(any(feature = "testing", test))]
pub fn from_components(components: &[AccountComponent]) -> Result<Self, AccountError> {
Self::from_components_unchecked(components)
}
pub(super) fn from_components_unchecked(
components: &[AccountComponent],
) -> Result<Self, AccountError> {
let (merged_mast_forest, root_map) =
MastForest::merge(components.iter().map(|component| component.mast_forest()))
.map_err(AccountError::AccountComponentMastForestMergeError)?;
let package_debug_info = merge_component_debug_info(components, &root_map)?;
let mut builder = AccountProcedureBuilder::new();
let mut num_auth_components = 0;
for component in components {
if component.is_auth_component() {
num_auth_components += 1;
builder.add_auth_component(component)?
} else {
builder.add_component(component)?;
}
}
if num_auth_components == 0 {
return Err(AccountError::AccountCodeNoAuthComponent);
} else if num_auth_components > 1 {
return Err(AccountError::AccountCodeMultipleAuthComponents);
}
let procedures = builder.build()?;
Self::from_parts(Arc::new(merged_mast_forest), procedures).map(|mut code| {
code.package_debug_info = package_debug_info;
code
})
}
pub fn commitment(&self) -> Word {
self.commitment
}
pub fn mast(&self) -> Arc<MastForest> {
self.mast.clone()
}
pub fn loaded_mast_forest(&self) -> LoadedMastForest {
loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
}
pub fn procedures(&self) -> &[AccountProcedureRoot] {
&self.procedures
}
pub fn procedure_roots(&self) -> impl Iterator<Item = Word> + '_ {
self.procedures().iter().map(|procedure| *procedure.mast_root())
}
pub fn num_procedures(&self) -> usize {
self.procedures.len()
}
pub fn has_procedure(&self, mast_root: Word) -> bool {
self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
}
pub fn get(&self, index: usize) -> Option<&AccountProcedureRoot> {
self.procedures.get(index)
}
pub fn to_elements(&self) -> Vec<Felt> {
procedures_as_elements(self.procedures())
}
pub fn interface(&self, account_id: AccountId) -> AccountCodeInterface {
AccountCodeInterface::new(account_id, self.procedures.iter().copied().collect())
.expect("account code procedure count is enforced by AccountCode invariants")
}
pub fn printable_procedures(&self) -> impl Iterator<Item = PrintableProcedure> {
self.procedures()
.iter()
.filter_map(move |proc_root| self.printable_procedure(proc_root).ok())
}
fn printable_procedure(
&self,
proc_root: &AccountProcedureRoot,
) -> Result<PrintableProcedure, AccountError> {
let node_id = self
.mast
.find_procedure_root(*proc_root.mast_root())
.expect("procedure root should be present in the mast forest");
Ok(PrintableProcedure::new(self.mast.clone(), *proc_root, node_id))
}
}
impl PartialEq for AccountCode {
fn eq(&self, other: &Self) -> bool {
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 {}
impl Serializable for AccountCode {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.mast.write_into(target);
target.write_u8((self.procedures.len() - 1) as u8);
target.write_many(self.procedures());
}
fn get_size_hint(&self) -> usize {
let mut mast_forest_target = Vec::new();
self.mast.write_into(&mut mast_forest_target);
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 mast = Arc::new(MastForest::read_from(source)?);
let num_procedures = (source.read_u8()? as usize) + 1;
let procedures = source
.read_many_iter(num_procedures)?
.collect::<Result<Vec<AccountProcedureRoot>, _>>()?;
Self::from_parts(mast, procedures)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
impl PrettyPrint for AccountCode {
fn render(&self) -> miden_core::prettier::Document {
use miden_core::prettier::*;
let mut partial = Document::Empty;
let len_procedures = self.num_procedures();
for (index, printable_procedure) in self.printable_procedures().enumerate() {
partial += indent(
0,
indent(
4,
text(format!("proc {}", printable_procedure.mast_root()))
+ nl()
+ printable_procedure.render(),
) + nl()
+ const_text("end"),
);
if index < len_procedures - 1 {
partial += nl();
}
}
partial
}
}
struct AccountProcedureBuilder {
procedures: Vec<AccountProcedureRoot>,
}
impl AccountProcedureBuilder {
fn new() -> Self {
Self { procedures: Vec::new() }
}
fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
let mut auth_proc_count = 0;
for (proc_root, is_auth) in component.procedures() {
let proc_idx = self.add_procedure(proc_root);
if is_auth {
self.procedures.swap(0, proc_idx);
auth_proc_count += 1;
}
}
if auth_proc_count == 0 {
return Err(AccountError::AccountCodeNoAuthComponent);
} else if auth_proc_count > 1 {
return Err(AccountError::AccountComponentMultipleAuthProcedures);
}
Ok(())
}
fn add_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
for (proc_root, is_auth) in component.procedures() {
if is_auth {
return Err(AccountError::AccountCodeMultipleAuthComponents);
}
self.add_procedure(proc_root);
}
Ok(())
}
fn add_procedure(&mut self, proc_root: AccountProcedureRoot) -> usize {
match self.procedures.iter().position(|existing_root| existing_root == &proc_root) {
Some(existing_idx) => existing_idx,
None => {
self.procedures.push(proc_root);
self.procedures.len() - 1
},
}
}
fn build(mut self) -> Result<Vec<AccountProcedureRoot>, AccountError> {
if self.procedures.len() < AccountCode::MIN_NUM_PROCEDURES {
return Err(AccountError::AccountCodeNoProcedures);
} else if self.procedures.len() > AccountCode::MAX_NUM_PROCEDURES {
return Err(AccountError::AccountCodeTooManyProcedures(self.procedures.len()));
}
self.procedures[1..].sort_unstable();
Ok(self.procedures)
}
}
fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word {
let elements = procedures_as_elements(procedures);
Hasher::hash_elements(&elements)
}
fn merge_component_debug_info(
components: &[AccountComponent],
root_map: &miden_core::mast::MastForestRootMap,
) -> Result<Option<Arc<PackageDebugInfo>>, AccountError> {
let component_debug_info = components
.iter()
.enumerate()
.filter_map(|(idx, component)| {
package_debug_info(component.component_code().as_package()).map(|debug| (idx, debug))
})
.collect::<Vec<_>>();
if component_debug_info.is_empty() {
return Ok(None);
}
let debug_info = PackageDebugInfo::merge_source_debug(
component_debug_info.iter().map(|(idx, debug)| (*idx, debug.as_ref())),
root_map,
)
.map_err(|err| {
AccountError::other_with_source("failed to merge account component debug info", err)
})?;
Ok(Some(Arc::new(debug_info)))
}
fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec<Felt> {
procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect()
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use anyhow::Context;
use assert_matches::assert_matches;
use rstest::rstest;
use super::{AccountCode, ByteWriter, Deserializable, DeserializationError, Serializable};
use crate::Word;
use crate::account::code::build_procedure_commitment;
use crate::account::component::AccountComponentMetadata;
use crate::account::{AccountComponent, AccountProcedureRoot};
use crate::errors::AccountError;
use crate::testing::account_code::CODE;
use crate::testing::assembler::assemble_test_package;
use crate::testing::noop_auth_component::NoopAuthComponent;
#[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_root() {
let code = AccountCode::mock();
let procedure_root = build_procedure_commitment(code.procedures());
assert_eq!(procedure_root, code.commitment())
}
#[test]
fn test_account_code_only_auth_component() {
let err = AccountCode::from_components(&[NoopAuthComponent.into()]).unwrap_err();
assert_matches!(err, AccountError::AccountCodeNoProcedures);
}
#[test]
fn test_account_code_no_auth_component() {
let package =
assemble_test_package("test-account-code-no-auth", "test::account_code", CODE);
let metadata = AccountComponentMetadata::new("test::no_auth");
let component = AccountComponent::new(package, vec![], metadata).unwrap();
let err = AccountCode::from_components(&[component]).unwrap_err();
assert_matches!(err, AccountError::AccountCodeNoAuthComponent);
}
#[test]
fn test_account_code_preserves_component_debug_info() {
let package =
assemble_test_package("test-account-code-debug-info", "test::account_code", CODE);
let metadata = AccountComponentMetadata::new("test::debug_info");
let component = AccountComponent::new(package, vec![], metadata).unwrap();
let code = AccountCode::from_components(&[NoopAuthComponent.into(), component]).unwrap();
assert!(code.loaded_mast_forest().package_debug_info().unwrap().is_some());
}
#[test]
fn test_account_code_multiple_auth_components() {
let err =
AccountCode::from_components(&[NoopAuthComponent.into(), NoopAuthComponent.into()])
.unwrap_err();
assert_matches!(err, AccountError::AccountCodeMultipleAuthComponents);
}
#[test]
fn test_account_component_multiple_auth_procedures() {
let code_with_multiple_auth = "
@auth_script
pub proc auth_basic
push.1 drop
end
@auth_script
pub proc auth_secondary
push.0 drop
end
";
let package = assemble_test_package(
"test-account-code-multiple-auth",
"test::account_code_multiple_auth",
code_with_multiple_auth,
);
let metadata = AccountComponentMetadata::new("test::multiple_auth");
let component = AccountComponent::new(package, vec![], metadata).unwrap();
let err = AccountCode::from_components(&[component]).unwrap_err();
assert_matches!(err, AccountError::AccountComponentMultipleAuthProcedures);
}
#[rstest]
#[case::duplicate_first(true)]
#[case::duplicate_second(false)]
fn test_account_code_auth_procedure_at_index_zero_on_duplicate_root(
#[case] duplicate_first: bool,
) -> anyhow::Result<()> {
let duplicate_of_auth = "
@account_procedure
pub proc noop
push.0 drop
end
";
let duplicate_component = AccountComponent::new(
assemble_test_package(
"test-account-code-duplicate-auth-root",
"test::duplicate_auth_root",
duplicate_of_auth,
),
vec![],
AccountComponentMetadata::new("test::duplicate_auth_root"),
)?;
let other_component = AccountComponent::new(
assemble_test_package("test-account-code-other", "test::other", CODE),
vec![],
AccountComponentMetadata::new("test::other"),
)?;
let auth_component = AccountComponent::from(NoopAuthComponent);
let auth_proc_root = auth_component
.procedures()
.find_map(|(proc_root, is_auth)| is_auth.then_some(proc_root))
.context("auth component should export an auth procedure")?;
let duplicate_proc_root = duplicate_component
.procedures()
.next()
.context("duplicate component should export a procedure")?
.0;
assert_eq!(duplicate_proc_root, auth_proc_root);
let components = if duplicate_first {
[duplicate_component, other_component, auth_component]
} else {
[other_component, duplicate_component, auth_component]
};
let code = AccountCode::from_components(&components)?;
assert_eq!(code.procedures()[0], auth_proc_root);
assert_eq!(code.num_procedures(), 3);
Ok(())
}
#[test]
fn test_account_code_from_parts_rejects_duplicate_roots() {
let code = AccountCode::mock();
let procedures = code.procedures();
let duplicated = vec![procedures[0], procedures[1], procedures[1]];
let err = AccountCode::from_parts(code.mast(), duplicated).unwrap_err();
assert_matches!(
err,
AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[1]
);
}
#[test]
fn test_account_code_from_parts_rejects_missing_root() {
let code = AccountCode::mock();
let procedures = code.procedures();
let non_existent_root = AccountProcedureRoot::from_raw(Word::from([1, 2, 3, 4u32]));
let procedures = vec![procedures[0], non_existent_root];
let err = AccountCode::from_parts(code.mast(), procedures).unwrap_err();
assert_matches!(
err,
AccountError::AccountCodeProcedureNotInMastForest(root) if root == non_existent_root
);
}
#[test]
fn test_account_code_deserialization_rejects_duplicate_roots() {
let code = AccountCode::mock();
let procedures = code.procedures();
let mut bytes = Vec::new();
code.mast().write_into(&mut bytes);
bytes.write_u8(3 - 1); procedures[0].write_into(&mut bytes);
procedures[1].write_into(&mut bytes);
procedures[1].write_into(&mut bytes);
let err = AccountCode::read_from_bytes(&bytes).unwrap_err();
assert_matches!(
err,
DeserializationError::InvalidValue(msg) if msg.contains("duplicate procedure with root")
);
}
#[test]
fn account_code_procedures_are_sorted_after_the_auth_procedure() {
let code = AccountCode::mock();
assert!(code.procedures()[1..].is_sorted());
}
#[test]
fn account_code_commitment_is_independent_of_component_order() -> anyhow::Result<()> {
let first = mock_component("test-account-code-first", "test::first", 1);
let second = mock_component("test-account-code-second", "test::second", 2);
let mut components = vec![NoopAuthComponent.into(), first.clone(), second.clone()];
let code = AccountCode::from_components(&components)?;
components.reverse();
let reversed_code = AccountCode::from_components(&components)?;
assert_eq!(code.commitment(), reversed_code.commitment());
assert_eq!(
code.procedures()[0],
reversed_code.procedures()[0],
"the auth procedure should stay at index 0"
);
Ok(())
}
#[test]
fn account_code_from_parts_rejects_unsorted_procedures() -> anyhow::Result<()> {
let code = AccountCode::mock();
let procedures = code.procedures();
let unsorted = vec![procedures[0], procedures[2], procedures[1]];
let err = AccountCode::from_parts(code.mast(), unsorted).unwrap_err();
assert_matches!(err, AccountError::AccountCodeProceduresUnsorted);
Ok(())
}
#[test]
fn account_code_from_parts_rejects_duplicated_auth_procedure() -> anyhow::Result<()> {
let code = AccountCode::mock();
let procedures = code.procedures();
let mut duplicated_auth = vec![procedures[0], procedures[1], procedures[0]];
duplicated_auth[1..].sort_unstable();
let err = AccountCode::from_parts(code.mast(), duplicated_auth).unwrap_err();
assert_matches!(
err,
AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[0]
);
Ok(())
}
fn mock_component(
package_name: &str,
module_path: &str,
unique_value: u32,
) -> AccountComponent {
let code = format!(
"
@account_procedure
pub proc account_procedure
push.{unique_value} drop
end
"
);
let package = assemble_test_package(package_name, module_path, &code);
let metadata = AccountComponentMetadata::new(module_path);
AccountComponent::new(package, vec![], metadata).expect("component should be valid")
}
}