miden_protocol/account/code/
mod.rs1use alloc::string::ToString;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::cmp::Ordering;
5
6use miden_core::mast::MastForest;
7use miden_core::prettier::PrettyPrint;
8use miden_mast_package::debug_info::PackageDebugInfo;
9use miden_processor::LoadedMastForest;
10
11use super::{
12 AccountError,
13 ByteReader,
14 ByteWriter,
15 Deserializable,
16 DeserializationError,
17 Felt,
18 Hasher,
19 Serializable,
20};
21use crate::Word;
22use crate::account::{AccountCodeInterface, AccountComponent, AccountId};
23use crate::package::{loaded_mast_forest, package_debug_info};
24
25pub mod procedure;
26use procedure::{AccountProcedureRoot, PrintableProcedure};
27
28#[derive(Debug, Clone)]
49pub struct AccountCode {
50 mast: Arc<MastForest>,
51 procedures: Vec<AccountProcedureRoot>,
52 commitment: Word,
53 package_debug_info: Option<Arc<PackageDebugInfo>>,
54}
55
56impl AccountCode {
57 pub const MIN_NUM_PROCEDURES: usize = 2;
62
63 pub const MAX_NUM_PROCEDURES: usize = 256;
65
66 pub fn from_parts(
81 mast: Arc<MastForest>,
82 procedures: Vec<AccountProcedureRoot>,
83 ) -> Result<Self, AccountError> {
84 if procedures.len() < Self::MIN_NUM_PROCEDURES {
85 return Err(AccountError::AccountCodeNoProcedures);
86 }
87 if procedures.len() > Self::MAX_NUM_PROCEDURES {
88 return Err(AccountError::AccountCodeTooManyProcedures(procedures.len()));
89 }
90
91 let (auth_proc, other_procs) = procedures
96 .split_first()
97 .expect("account code should contain at least two procedures");
98
99 let mut previous_proc: Option<&AccountProcedureRoot> = None;
100 for procedure in other_procs {
101 if procedure == auth_proc {
102 return Err(AccountError::AccountCodeDuplicateProcedureRoot(*procedure));
103 }
104
105 if let Some(previous_proc) = previous_proc {
106 match previous_proc.cmp(procedure) {
107 Ordering::Less => {},
108 Ordering::Equal => {
109 return Err(AccountError::AccountCodeDuplicateProcedureRoot(*procedure));
110 },
111 Ordering::Greater => return Err(AccountError::AccountCodeProceduresUnsorted),
112 }
113 }
114
115 previous_proc = Some(procedure);
116 }
117
118 for procedure in procedures.iter() {
120 if mast.find_procedure_root(procedure.as_word()).is_none() {
121 return Err(AccountError::AccountCodeProcedureNotInMastForest(*procedure));
122 }
123 }
124
125 Ok(Self {
126 commitment: build_procedure_commitment(&procedures),
127 procedures,
128 mast,
129 package_debug_info: None,
130 })
131 }
132
133 #[cfg(any(feature = "testing", test))]
137 pub fn from_components(components: &[AccountComponent]) -> Result<Self, AccountError> {
138 Self::from_components_unchecked(components)
139 }
140
141 pub(super) fn from_components_unchecked(
158 components: &[AccountComponent],
159 ) -> Result<Self, AccountError> {
160 let (merged_mast_forest, root_map) =
161 MastForest::merge(components.iter().map(|component| component.mast_forest()))
162 .map_err(AccountError::AccountComponentMastForestMergeError)?;
163 let package_debug_info = merge_component_debug_info(components, &root_map)?;
164
165 let mut builder = AccountProcedureBuilder::new();
166 let mut num_auth_components = 0;
167
168 for component in components {
169 if component.is_auth_component() {
170 num_auth_components += 1;
171 builder.add_auth_component(component)?
172 } else {
173 builder.add_component(component)?;
174 }
175 }
176
177 if num_auth_components == 0 {
178 return Err(AccountError::AccountCodeNoAuthComponent);
179 } else if num_auth_components > 1 {
180 return Err(AccountError::AccountCodeMultipleAuthComponents);
181 }
182
183 let procedures = builder.build()?;
184
185 Self::from_parts(Arc::new(merged_mast_forest), procedures).map(|mut code| {
186 code.package_debug_info = package_debug_info;
187 code
188 })
189 }
190
191 pub fn commitment(&self) -> Word {
196 self.commitment
197 }
198
199 pub fn mast(&self) -> Arc<MastForest> {
201 self.mast.clone()
202 }
203
204 pub fn loaded_mast_forest(&self) -> LoadedMastForest {
206 loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
207 }
208
209 pub fn procedures(&self) -> &[AccountProcedureRoot] {
211 &self.procedures
212 }
213
214 pub fn procedure_roots(&self) -> impl Iterator<Item = Word> + '_ {
216 self.procedures().iter().map(|procedure| *procedure.mast_root())
217 }
218
219 pub fn num_procedures(&self) -> usize {
221 self.procedures.len()
222 }
223
224 pub fn has_procedure(&self, mast_root: Word) -> bool {
226 self.procedures.iter().any(|procedure| procedure.mast_root() == &mast_root)
227 }
228
229 pub fn get(&self, index: usize) -> Option<&AccountProcedureRoot> {
231 self.procedures.get(index)
232 }
233
234 pub fn to_elements(&self) -> Vec<Felt> {
244 procedures_as_elements(self.procedures())
245 }
246
247 pub fn interface(&self, account_id: AccountId) -> AccountCodeInterface {
250 AccountCodeInterface::new(account_id, self.procedures.iter().copied().collect())
251 .expect("account code procedure count is enforced by AccountCode invariants")
252 }
253
254 pub fn printable_procedures(&self) -> impl Iterator<Item = PrintableProcedure> {
261 self.procedures()
262 .iter()
263 .filter_map(move |proc_root| self.printable_procedure(proc_root).ok())
264 }
265
266 fn printable_procedure(
274 &self,
275 proc_root: &AccountProcedureRoot,
276 ) -> Result<PrintableProcedure, AccountError> {
277 let node_id = self
278 .mast
279 .find_procedure_root(*proc_root.mast_root())
280 .expect("procedure root should be present in the mast forest");
281
282 Ok(PrintableProcedure::new(self.mast.clone(), *proc_root, node_id))
283 }
284}
285
286impl PartialEq for AccountCode {
290 fn eq(&self, other: &Self) -> bool {
291 self.mast == other.mast && self.procedures == other.procedures
293 }
294}
295
296impl Ord for AccountCode {
297 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
298 self.commitment.cmp(&other.commitment)
299 }
300}
301
302impl PartialOrd for AccountCode {
303 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
304 Some(self.cmp(other))
305 }
306}
307
308impl Eq for AccountCode {}
309
310impl Serializable for AccountCode {
314 fn write_into<W: ByteWriter>(&self, target: &mut W) {
315 self.mast.write_into(target);
316 target.write_u8((self.procedures.len() - 1) as u8);
319 target.write_many(self.procedures());
320 }
321
322 fn get_size_hint(&self) -> usize {
323 let mut mast_forest_target = Vec::new();
325 self.mast.write_into(&mut mast_forest_target);
326
327 let u8_size = 0u8.get_size_hint();
329 let mut size = u8_size + mast_forest_target.len();
330
331 for procedure in self.procedures() {
332 size += procedure.get_size_hint();
333 }
334
335 size
336 }
337}
338
339impl Deserializable for AccountCode {
340 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
341 let mast = Arc::new(MastForest::read_from(source)?);
342 let num_procedures = (source.read_u8()? as usize) + 1;
343
344 let procedures = source
345 .read_many_iter(num_procedures)?
346 .collect::<Result<Vec<AccountProcedureRoot>, _>>()?;
347
348 Self::from_parts(mast, procedures)
349 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
350 }
351}
352
353impl PrettyPrint for AccountCode {
357 fn render(&self) -> miden_core::prettier::Document {
358 use miden_core::prettier::*;
359 let mut partial = Document::Empty;
360 let len_procedures = self.num_procedures();
361
362 for (index, printable_procedure) in self.printable_procedures().enumerate() {
363 partial += indent(
364 0,
365 indent(
366 4,
367 text(format!("proc {}", printable_procedure.mast_root()))
368 + nl()
369 + printable_procedure.render(),
370 ) + nl()
371 + const_text("end"),
372 );
373 if index < len_procedures - 1 {
374 partial += nl();
375 }
376 }
377 partial
378 }
379}
380
381struct AccountProcedureBuilder {
389 procedures: Vec<AccountProcedureRoot>,
390}
391
392impl AccountProcedureBuilder {
393 fn new() -> Self {
394 Self { procedures: Vec::new() }
395 }
396
397 fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
398 let mut auth_proc_count = 0;
399
400 for (proc_root, is_auth) in component.procedures() {
401 let proc_idx = self.add_procedure(proc_root);
402
403 if is_auth {
404 self.procedures.swap(0, proc_idx);
405 auth_proc_count += 1;
406 }
407 }
408
409 if auth_proc_count == 0 {
410 return Err(AccountError::AccountCodeNoAuthComponent);
411 } else if auth_proc_count > 1 {
412 return Err(AccountError::AccountComponentMultipleAuthProcedures);
413 }
414
415 Ok(())
416 }
417
418 fn add_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> {
419 for (proc_root, is_auth) in component.procedures() {
420 if is_auth {
421 return Err(AccountError::AccountCodeMultipleAuthComponents);
422 }
423 self.add_procedure(proc_root);
424 }
425
426 Ok(())
427 }
428
429 fn add_procedure(&mut self, proc_root: AccountProcedureRoot) -> usize {
435 match self.procedures.iter().position(|existing_root| existing_root == &proc_root) {
436 Some(existing_idx) => existing_idx,
437 None => {
438 self.procedures.push(proc_root);
439 self.procedures.len() - 1
440 },
441 }
442 }
443
444 fn build(mut self) -> Result<Vec<AccountProcedureRoot>, AccountError> {
445 if self.procedures.len() < AccountCode::MIN_NUM_PROCEDURES {
446 return Err(AccountError::AccountCodeNoProcedures);
447 } else if self.procedures.len() > AccountCode::MAX_NUM_PROCEDURES {
448 return Err(AccountError::AccountCodeTooManyProcedures(self.procedures.len()));
449 }
450
451 self.procedures[1..].sort_unstable();
455
456 Ok(self.procedures)
457 }
458}
459
460fn build_procedure_commitment(procedures: &[AccountProcedureRoot]) -> Word {
465 let elements = procedures_as_elements(procedures);
466 Hasher::hash_elements(&elements)
467}
468
469fn merge_component_debug_info(
470 components: &[AccountComponent],
471 root_map: &miden_core::mast::MastForestRootMap,
472) -> Result<Option<Arc<PackageDebugInfo>>, AccountError> {
473 let component_debug_info = components
474 .iter()
475 .enumerate()
476 .filter_map(|(idx, component)| {
477 package_debug_info(component.component_code().as_package()).map(|debug| (idx, debug))
478 })
479 .collect::<Vec<_>>();
480
481 if component_debug_info.is_empty() {
482 return Ok(None);
483 }
484
485 let debug_info = PackageDebugInfo::merge_source_debug(
486 component_debug_info.iter().map(|(idx, debug)| (*idx, debug.as_ref())),
487 root_map,
488 )
489 .map_err(|err| {
490 AccountError::other_with_source("failed to merge account component debug info", err)
491 })?;
492
493 Ok(Some(Arc::new(debug_info)))
494}
495
496fn procedures_as_elements(procedures: &[AccountProcedureRoot]) -> Vec<Felt> {
498 procedures.iter().flat_map(AccountProcedureRoot::as_elements).copied().collect()
499}
500
501#[cfg(test)]
505mod tests {
506 use alloc::vec::Vec;
507
508 use anyhow::Context;
509 use assert_matches::assert_matches;
510 use rstest::rstest;
511
512 use super::{AccountCode, ByteWriter, Deserializable, DeserializationError, Serializable};
513 use crate::Word;
514 use crate::account::code::build_procedure_commitment;
515 use crate::account::component::AccountComponentMetadata;
516 use crate::account::{AccountComponent, AccountProcedureRoot};
517 use crate::errors::AccountError;
518 use crate::testing::account_code::CODE;
519 use crate::testing::assembler::assemble_test_package;
520 use crate::testing::noop_auth_component::NoopAuthComponent;
521
522 #[test]
523 fn test_serde_account_code() {
524 let code = AccountCode::mock();
525 let serialized = code.to_bytes();
526 let deserialized = AccountCode::read_from_bytes(&serialized).unwrap();
527 assert_eq!(deserialized, code)
528 }
529
530 #[test]
531 fn test_account_code_procedure_root() {
532 let code = AccountCode::mock();
533 let procedure_root = build_procedure_commitment(code.procedures());
534 assert_eq!(procedure_root, code.commitment())
535 }
536
537 #[test]
538 fn test_account_code_only_auth_component() {
539 let err = AccountCode::from_components(&[NoopAuthComponent.into()]).unwrap_err();
540
541 assert_matches!(err, AccountError::AccountCodeNoProcedures);
542 }
543
544 #[test]
545 fn test_account_code_no_auth_component() {
546 let package =
547 assemble_test_package("test-account-code-no-auth", "test::account_code", CODE);
548 let metadata = AccountComponentMetadata::new("test::no_auth");
549 let component = AccountComponent::new(package, vec![], metadata).unwrap();
550
551 let err = AccountCode::from_components(&[component]).unwrap_err();
552
553 assert_matches!(err, AccountError::AccountCodeNoAuthComponent);
554 }
555
556 #[test]
557 fn test_account_code_preserves_component_debug_info() {
558 let package =
559 assemble_test_package("test-account-code-debug-info", "test::account_code", CODE);
560 let metadata = AccountComponentMetadata::new("test::debug_info");
561 let component = AccountComponent::new(package, vec![], metadata).unwrap();
562
563 let code = AccountCode::from_components(&[NoopAuthComponent.into(), component]).unwrap();
564
565 assert!(code.loaded_mast_forest().package_debug_info().unwrap().is_some());
566 }
567
568 #[test]
569 fn test_account_code_multiple_auth_components() {
570 let err =
571 AccountCode::from_components(&[NoopAuthComponent.into(), NoopAuthComponent.into()])
572 .unwrap_err();
573
574 assert_matches!(err, AccountError::AccountCodeMultipleAuthComponents);
575 }
576
577 #[test]
578 fn test_account_component_multiple_auth_procedures() {
579 let code_with_multiple_auth = "
580 @auth_script
581 pub proc auth_basic
582 push.1 drop
583 end
584
585 @auth_script
586 pub proc auth_secondary
587 push.0 drop
588 end
589 ";
590
591 let package = assemble_test_package(
592 "test-account-code-multiple-auth",
593 "test::account_code_multiple_auth",
594 code_with_multiple_auth,
595 );
596 let metadata = AccountComponentMetadata::new("test::multiple_auth");
597 let component = AccountComponent::new(package, vec![], metadata).unwrap();
598
599 let err = AccountCode::from_components(&[component]).unwrap_err();
600
601 assert_matches!(err, AccountError::AccountComponentMultipleAuthProcedures);
602 }
603
604 #[rstest]
607 #[case::duplicate_first(true)]
608 #[case::duplicate_second(false)]
609 fn test_account_code_auth_procedure_at_index_zero_on_duplicate_root(
610 #[case] duplicate_first: bool,
611 ) -> anyhow::Result<()> {
612 let duplicate_of_auth = "
614 @account_procedure
615 pub proc noop
616 push.0 drop
617 end
618 ";
619 let duplicate_component = AccountComponent::new(
620 assemble_test_package(
621 "test-account-code-duplicate-auth-root",
622 "test::duplicate_auth_root",
623 duplicate_of_auth,
624 ),
625 vec![],
626 AccountComponentMetadata::new("test::duplicate_auth_root"),
627 )?;
628
629 let other_component = AccountComponent::new(
630 assemble_test_package("test-account-code-other", "test::other", CODE),
631 vec![],
632 AccountComponentMetadata::new("test::other"),
633 )?;
634
635 let auth_component = AccountComponent::from(NoopAuthComponent);
636 let auth_proc_root = auth_component
637 .procedures()
638 .find_map(|(proc_root, is_auth)| is_auth.then_some(proc_root))
639 .context("auth component should export an auth procedure")?;
640
641 let duplicate_proc_root = duplicate_component
643 .procedures()
644 .next()
645 .context("duplicate component should export a procedure")?
646 .0;
647 assert_eq!(duplicate_proc_root, auth_proc_root);
648
649 let components = if duplicate_first {
650 [duplicate_component, other_component, auth_component]
651 } else {
652 [other_component, duplicate_component, auth_component]
653 };
654
655 let code = AccountCode::from_components(&components)?;
656
657 assert_eq!(code.procedures()[0], auth_proc_root);
658 assert_eq!(code.num_procedures(), 3);
660
661 Ok(())
662 }
663
664 #[test]
665 fn test_account_code_from_parts_rejects_duplicate_roots() {
666 let code = AccountCode::mock();
667 let procedures = code.procedures();
668
669 let duplicated = vec![procedures[0], procedures[1], procedures[1]];
671 let err = AccountCode::from_parts(code.mast(), duplicated).unwrap_err();
672
673 assert_matches!(
674 err,
675 AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[1]
676 );
677 }
678
679 #[test]
680 fn test_account_code_from_parts_rejects_missing_root() {
681 let code = AccountCode::mock();
682 let procedures = code.procedures();
683 let non_existent_root = AccountProcedureRoot::from_raw(Word::from([1, 2, 3, 4u32]));
684
685 let procedures = vec![procedures[0], non_existent_root];
687 let err = AccountCode::from_parts(code.mast(), procedures).unwrap_err();
688
689 assert_matches!(
690 err,
691 AccountError::AccountCodeProcedureNotInMastForest(root) if root == non_existent_root
692 );
693 }
694
695 #[test]
696 fn test_account_code_deserialization_rejects_duplicate_roots() {
697 let code = AccountCode::mock();
698 let procedures = code.procedures();
699
700 let mut bytes = Vec::new();
701 code.mast().write_into(&mut bytes);
702 bytes.write_u8(3 - 1); procedures[0].write_into(&mut bytes);
704 procedures[1].write_into(&mut bytes);
705 procedures[1].write_into(&mut bytes);
706
707 let err = AccountCode::read_from_bytes(&bytes).unwrap_err();
708
709 assert_matches!(
710 err,
711 DeserializationError::InvalidValue(msg) if msg.contains("duplicate procedure with root")
712 );
713 }
714
715 #[test]
716 fn account_code_procedures_are_sorted_after_the_auth_procedure() {
717 let code = AccountCode::mock();
718
719 assert!(code.procedures()[1..].is_sorted());
720 }
721
722 #[test]
723 fn account_code_commitment_is_independent_of_component_order() -> anyhow::Result<()> {
724 let first = mock_component("test-account-code-first", "test::first", 1);
725 let second = mock_component("test-account-code-second", "test::second", 2);
726
727 let mut components = vec![NoopAuthComponent.into(), first.clone(), second.clone()];
728
729 let code = AccountCode::from_components(&components)?;
730 components.reverse();
731 let reversed_code = AccountCode::from_components(&components)?;
732
733 assert_eq!(code.commitment(), reversed_code.commitment());
734 assert_eq!(
735 code.procedures()[0],
736 reversed_code.procedures()[0],
737 "the auth procedure should stay at index 0"
738 );
739
740 Ok(())
741 }
742
743 #[test]
744 fn account_code_from_parts_rejects_unsorted_procedures() -> anyhow::Result<()> {
745 let code = AccountCode::mock();
746 let procedures = code.procedures();
747
748 let unsorted = vec![procedures[0], procedures[2], procedures[1]];
750 let err = AccountCode::from_parts(code.mast(), unsorted).unwrap_err();
751
752 assert_matches!(err, AccountError::AccountCodeProceduresUnsorted);
753
754 Ok(())
755 }
756
757 #[test]
758 fn account_code_from_parts_rejects_duplicated_auth_procedure() -> anyhow::Result<()> {
759 let code = AccountCode::mock();
760 let procedures = code.procedures();
761
762 let mut duplicated_auth = vec![procedures[0], procedures[1], procedures[0]];
764 duplicated_auth[1..].sort_unstable();
765 let err = AccountCode::from_parts(code.mast(), duplicated_auth).unwrap_err();
766
767 assert_matches!(
768 err,
769 AccountError::AccountCodeDuplicateProcedureRoot(root) if root == procedures[0]
770 );
771
772 Ok(())
773 }
774
775 fn mock_component(
778 package_name: &str,
779 module_path: &str,
780 unique_value: u32,
781 ) -> AccountComponent {
782 let code = format!(
783 "
784 @account_procedure
785 pub proc account_procedure
786 push.{unique_value} drop
787 end
788 "
789 );
790 let package = assemble_test_package(package_name, module_path, &code);
791 let metadata = AccountComponentMetadata::new(module_path);
792
793 AccountComponent::new(package, vec![], metadata).expect("component should be valid")
794 }
795}