Skip to main content

miden_protocol/account/interface/
code_interface.rs

1use alloc::collections::BTreeSet;
2
3use crate::account::AccountId;
4use crate::account::code::AccountCode;
5use crate::account::code::procedure::AccountProcedureRoot;
6use crate::errors::AccountCodeInterfaceError;
7
8/// The set of procedures an account exposes.
9///
10/// This is the next-generation replacement for the enum-based interface in `miden-standards`. It
11/// is intentionally minimal: it only carries the account's ID and the set of procedure roots that
12/// make up its callable surface.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct AccountCodeInterface {
15    account_id: AccountId,
16    procedures: BTreeSet<AccountProcedureRoot>,
17}
18
19impl AccountCodeInterface {
20    /// Constructs a new [`AccountCodeInterface`] from the given account ID and set of procedure
21    /// roots.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if the number of procedures is outside the
26    /// `[AccountCode::MIN_NUM_PROCEDURES, AccountCode::MAX_NUM_PROCEDURES]` range.
27    pub fn new(
28        account_id: AccountId,
29        procedures: BTreeSet<AccountProcedureRoot>,
30    ) -> Result<Self, AccountCodeInterfaceError> {
31        let count = procedures.len();
32        if count < AccountCode::MIN_NUM_PROCEDURES {
33            return Err(AccountCodeInterfaceError::TooFewProcedures { actual: count });
34        }
35        if count > AccountCode::MAX_NUM_PROCEDURES {
36            return Err(AccountCodeInterfaceError::TooManyProcedures { actual: count });
37        }
38        Ok(Self { account_id, procedures })
39    }
40
41    /// Returns the ID of the account this interface belongs to.
42    pub fn id(&self) -> AccountId {
43        self.account_id
44    }
45
46    /// Returns the set of procedure roots exposed by this interface.
47    pub fn procedures(&self) -> &BTreeSet<AccountProcedureRoot> {
48        &self.procedures
49    }
50
51    /// Returns `true` if every procedure root yielded by `procedures` is contained in this
52    /// interface.
53    pub fn contains(&self, procedures: impl IntoIterator<Item = AccountProcedureRoot>) -> bool {
54        procedures
55            .into_iter()
56            .all(|procedure_root| self.procedures.contains(&procedure_root))
57    }
58}
59
60// TESTS
61// ================================================================================================
62
63#[cfg(test)]
64mod tests {
65    use alloc::vec;
66    use alloc::vec::Vec;
67
68    use rstest::rstest;
69
70    use super::*;
71    use crate::Word;
72    use crate::account::{AccountBuilder, PartialAccount};
73    use crate::testing::add_component::AddComponent;
74    use crate::testing::noop_auth_component::NoopAuthComponent;
75
76    fn procedure(value: u32) -> AccountProcedureRoot {
77        AccountProcedureRoot::from_raw(Word::from([value, value, value, value]))
78    }
79
80    fn build_code_interface(values: &[u32]) -> AccountCodeInterface {
81        let procedures: BTreeSet<AccountProcedureRoot> =
82            values.iter().copied().map(procedure).collect();
83        AccountCodeInterface::new(dummy_account_id(), procedures).unwrap()
84    }
85
86    fn dummy_account_id() -> AccountId {
87        AccountId::try_from(
88            crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
89        )
90        .unwrap()
91    }
92
93    #[rstest]
94    #[case::empty(vec![])]
95    #[case::subset(vec![1, 2])]
96    #[case::exact(vec![1, 2, 3])]
97    fn contains_returns_true(#[case] query: Vec<u32>) {
98        let interface = build_code_interface(&[1, 2, 3]);
99        let query: Vec<AccountProcedureRoot> = query.into_iter().map(procedure).collect();
100        assert!(interface.contains(query));
101    }
102
103    #[rstest]
104    #[case::missing_one(vec![1, 4])]
105    #[case::all_missing(vec![10, 11])]
106    fn contains_returns_false(#[case] query: Vec<u32>) {
107        let interface = build_code_interface(&[1, 2, 3]);
108        let query: Vec<AccountProcedureRoot> = query.into_iter().map(procedure).collect();
109        assert!(!interface.contains(query));
110    }
111
112    #[test]
113    fn new_rejects_too_few_procedures() {
114        let procedures: BTreeSet<AccountProcedureRoot> = BTreeSet::new();
115        let err = AccountCodeInterface::new(dummy_account_id(), procedures).unwrap_err();
116        assert_matches::assert_matches!(
117            err,
118            AccountCodeInterfaceError::TooFewProcedures { actual: 0 }
119        );
120
121        let one_procedure: BTreeSet<AccountProcedureRoot> = [procedure(1)].into_iter().collect();
122        let err = AccountCodeInterface::new(dummy_account_id(), one_procedure).unwrap_err();
123        assert_matches::assert_matches!(
124            err,
125            AccountCodeInterfaceError::TooFewProcedures { actual: 1 }
126        );
127    }
128
129    #[test]
130    fn new_rejects_too_many_procedures() {
131        let procedures: BTreeSet<AccountProcedureRoot> =
132            (0..=AccountCode::MAX_NUM_PROCEDURES as u32).map(procedure).collect();
133        let expected = AccountCode::MAX_NUM_PROCEDURES + 1;
134        let err = AccountCodeInterface::new(dummy_account_id(), procedures).unwrap_err();
135        assert_matches::assert_matches!(
136            err,
137            AccountCodeInterfaceError::TooManyProcedures { actual } if actual == expected
138        );
139    }
140
141    #[test]
142    fn new_accepts_min_and_max_procedures() {
143        let min: BTreeSet<AccountProcedureRoot> =
144            (0..AccountCode::MIN_NUM_PROCEDURES as u32).map(procedure).collect();
145        AccountCodeInterface::new(dummy_account_id(), min).unwrap();
146
147        let max: BTreeSet<AccountProcedureRoot> =
148            (0..AccountCode::MAX_NUM_PROCEDURES as u32).map(procedure).collect();
149        AccountCodeInterface::new(dummy_account_id(), max).unwrap();
150    }
151
152    #[test]
153    fn account_code_interface_matches_code_procedures() -> anyhow::Result<()> {
154        let account = AccountBuilder::new([7; 32])
155            .with_auth_component(NoopAuthComponent)
156            .with_component(AddComponent)
157            .build()?;
158
159        let code_interface = account.code_interface();
160        let expected: BTreeSet<AccountProcedureRoot> =
161            account.code().procedures().iter().copied().collect();
162
163        assert_eq!(code_interface.id(), account.id());
164        assert_eq!(code_interface.procedures(), &expected);
165
166        Ok(())
167    }
168
169    #[test]
170    fn partial_account_code_interface_matches_code_procedures() -> anyhow::Result<()> {
171        let account = AccountBuilder::new([8; 32])
172            .with_auth_component(NoopAuthComponent)
173            .with_component(AddComponent)
174            .build()?;
175        let partial = PartialAccount::from(&account);
176
177        let code_interface = partial.code_interface();
178        let expected: BTreeSet<AccountProcedureRoot> =
179            partial.code().procedures().iter().copied().collect();
180
181        assert_eq!(code_interface.id(), partial.id());
182        assert_eq!(code_interface.procedures(), &expected);
183
184        Ok(())
185    }
186}