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