miden_lib/account/interface/
mod.rs

1use alloc::collections::BTreeSet;
2use alloc::string::String;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use miden_objects::Word;
7use miden_objects::account::{Account, AccountCode, AccountId, AccountIdPrefix, AccountType};
8use miden_objects::assembly::mast::{MastForest, MastNode, MastNodeId};
9use miden_objects::note::{Note, NoteScript, PartialNote};
10use miden_objects::transaction::TransactionScript;
11use miden_processor::MastNodeExt;
12use thiserror::Error;
13
14use crate::AuthScheme;
15use crate::account::components::{
16    basic_fungible_faucet_library,
17    basic_wallet_library,
18    network_fungible_faucet_library,
19    no_auth_library,
20    rpo_falcon_512_acl_library,
21    rpo_falcon_512_library,
22    rpo_falcon_512_multisig_library,
23};
24use crate::errors::ScriptBuilderError;
25use crate::note::WellKnownNote;
26use crate::utils::ScriptBuilder;
27
28#[cfg(test)]
29mod test;
30
31mod component;
32pub use component::AccountComponentInterface;
33
34// ACCOUNT INTERFACE
35// ================================================================================================
36
37/// An [`AccountInterface`] describes the exported, callable procedures of an account.
38///
39/// A note script's compatibility with this interface can be inspected to check whether the note may
40/// result in a successful execution against this account.
41pub struct AccountInterface {
42    account_id: AccountId,
43    auth: Vec<AuthScheme>,
44    components: Vec<AccountComponentInterface>,
45}
46
47// ------------------------------------------------------------------------------------------------
48/// Constructors and public accessors
49impl AccountInterface {
50    // CONSTRUCTORS
51    // --------------------------------------------------------------------------------------------
52
53    /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication
54    /// schemes and account code.
55    pub fn new(account_id: AccountId, auth: Vec<AuthScheme>, code: &AccountCode) -> Self {
56        let components = AccountComponentInterface::from_procedures(code.procedures());
57
58        Self { account_id, auth, components }
59    }
60
61    // PUBLIC ACCESSORS
62    // --------------------------------------------------------------------------------------------
63
64    /// Returns a reference to the account ID.
65    pub fn id(&self) -> &AccountId {
66        &self.account_id
67    }
68
69    /// Returns the type of the reference account.
70    pub fn account_type(&self) -> AccountType {
71        self.account_id.account_type()
72    }
73
74    /// Returns true if the reference account can issue assets.
75    pub fn is_faucet(&self) -> bool {
76        self.account_id.is_faucet()
77    }
78
79    /// Returns true if the reference account is a regular.
80    pub fn is_regular_account(&self) -> bool {
81        self.account_id.is_regular_account()
82    }
83
84    /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are
85    /// [`AccountStorageMode::Public`](miden_objects::account::AccountStorageMode::Public) or
86    /// [`AccountStorageMode::Network`](miden_objects::account::AccountStorageMode::Network),
87    /// `false` otherwise.
88    pub fn has_public_state(&self) -> bool {
89        self.account_id.has_public_state()
90    }
91
92    /// Returns `true` if the reference account is a private account, `false` otherwise.
93    pub fn is_private(&self) -> bool {
94        self.account_id.is_private()
95    }
96
97    /// Returns true if the reference account is a public account, `false` otherwise.
98    pub fn is_public(&self) -> bool {
99        self.account_id.is_public()
100    }
101
102    /// Returns true if the reference account is a network account, `false` otherwise.
103    pub fn is_network(&self) -> bool {
104        self.account_id.is_network()
105    }
106
107    /// Returns a reference to the vector of used authentication schemes.
108    pub fn auth(&self) -> &Vec<AuthScheme> {
109        &self.auth
110    }
111
112    /// Returns a reference to the set of used component interfaces.
113    pub fn components(&self) -> &Vec<AccountComponentInterface> {
114        &self.components
115    }
116
117    /// Returns [NoteAccountCompatibility::Maybe] if the provided note is compatible with the
118    /// current [AccountInterface], and [NoteAccountCompatibility::No] otherwise.
119    pub fn is_compatible_with(&self, note: &Note) -> NoteAccountCompatibility {
120        if let Some(well_known_note) = WellKnownNote::from_note(note) {
121            if well_known_note.is_compatible_with(self) {
122                NoteAccountCompatibility::Maybe
123            } else {
124                NoteAccountCompatibility::No
125            }
126        } else {
127            verify_note_script_compatibility(note.script(), self.get_procedure_digests())
128        }
129    }
130
131    /// Returns a digests set of all procedures from all account component interfaces.
132    pub(crate) fn get_procedure_digests(&self) -> BTreeSet<Word> {
133        let mut component_proc_digests = BTreeSet::new();
134        for component in self.components.iter() {
135            match component {
136                AccountComponentInterface::BasicWallet => {
137                    component_proc_digests
138                        .extend(basic_wallet_library().mast_forest().procedure_digests());
139                },
140                AccountComponentInterface::BasicFungibleFaucet(_) => {
141                    component_proc_digests
142                        .extend(basic_fungible_faucet_library().mast_forest().procedure_digests());
143                },
144                AccountComponentInterface::NetworkFungibleFaucet(_) => {
145                    component_proc_digests.extend(
146                        network_fungible_faucet_library().mast_forest().procedure_digests(),
147                    );
148                },
149                AccountComponentInterface::AuthRpoFalcon512(_) => {
150                    component_proc_digests
151                        .extend(rpo_falcon_512_library().mast_forest().procedure_digests());
152                },
153                AccountComponentInterface::AuthRpoFalcon512Acl(_) => {
154                    component_proc_digests
155                        .extend(rpo_falcon_512_acl_library().mast_forest().procedure_digests());
156                },
157                AccountComponentInterface::AuthRpoFalcon512Multisig(_) => {
158                    component_proc_digests.extend(
159                        rpo_falcon_512_multisig_library().mast_forest().procedure_digests(),
160                    );
161                },
162                AccountComponentInterface::AuthNoAuth => {
163                    component_proc_digests
164                        .extend(no_auth_library().mast_forest().procedure_digests());
165                },
166                AccountComponentInterface::Custom(custom_procs) => {
167                    component_proc_digests
168                        .extend(custom_procs.iter().map(|info| *info.mast_root()));
169                },
170            }
171        }
172
173        component_proc_digests
174    }
175}
176
177// ------------------------------------------------------------------------------------------------
178/// Code generation
179impl AccountInterface {
180    /// Returns a transaction script which sends the specified notes using the procedures available
181    /// in the current interface.
182    ///
183    /// Provided `expiration_delta` parameter is used to specify how close to the transaction's
184    /// reference block the transaction must be included into the chain. For example, if the
185    /// transaction's reference block is 100 and transaction expiration delta is 10, the transaction
186    /// can be included into the chain by block 110. If this does not happen, the transaction is
187    /// considered expired and cannot be included into the chain.
188    ///
189    /// Currently only [`AccountComponentInterface::BasicWallet`] and
190    /// [`AccountComponentInterface::BasicFungibleFaucet`] interfaces are supported for the
191    /// `send_note` script creation. Attempt to generate the script using some other interface will
192    /// lead to an error. In case both supported interfaces are available in the account, the script
193    /// will be generated for the [`AccountComponentInterface::BasicFungibleFaucet`] interface.
194    ///
195    /// # Example
196    ///
197    /// Example of the `send_note` script with specified expiration delta and one output note:
198    ///
199    /// ```masm
200    /// begin
201    ///     push.{expiration_delta} exec.::miden::tx::update_expiration_block_delta
202    ///
203    ///     push.{note information}
204    ///
205    ///     push.{asset amount}
206    ///     call.::miden::contracts::faucets::basic_fungible::distribute dropw dropw drop
207    /// end
208    /// ```
209    ///
210    /// # Errors:
211    /// Returns an error if:
212    /// - the available interfaces does not support the generation of the standard `send_note`
213    ///   procedure.
214    /// - the sender of the note isn't the account for which the script is being built.
215    /// - the note created by the faucet doesn't contain exactly one asset.
216    /// - a faucet tries to distribute an asset with a different faucet ID.
217    ///
218    /// [wallet]: crate::account::interface::AccountComponentInterface::BasicWallet
219    /// [faucet]: crate::account::interface::AccountComponentInterface::BasicFungibleFaucet
220    pub fn build_send_notes_script(
221        &self,
222        output_notes: &[PartialNote],
223        expiration_delta: Option<u16>,
224        in_debug_mode: bool,
225    ) -> Result<TransactionScript, AccountInterfaceError> {
226        let note_creation_source = self.build_create_notes_section(output_notes)?;
227
228        let script = format!(
229            "begin\n{}\n{}\nend",
230            self.build_set_tx_expiration_section(expiration_delta),
231            note_creation_source,
232        );
233
234        let tx_script = ScriptBuilder::new(in_debug_mode)
235            .compile_tx_script(script)
236            .map_err(AccountInterfaceError::InvalidTransactionScript)?;
237
238        Ok(tx_script)
239    }
240
241    /// Generates a note creation code required for the `send_note` transaction script.
242    ///
243    /// For the example of the resulting code see [AccountComponentInterface::send_note_body]
244    /// description.
245    ///
246    /// # Errors:
247    /// Returns an error if:
248    /// - the available interfaces does not support the generation of the standard `send_note`
249    ///   procedure.
250    /// - the sender of the note isn't the account for which the script is being built.
251    /// - the note created by the faucet doesn't contain exactly one asset.
252    /// - a faucet tries to distribute an asset with a different faucet ID.
253    fn build_create_notes_section(
254        &self,
255        output_notes: &[PartialNote],
256    ) -> Result<String, AccountInterfaceError> {
257        if let Some(basic_fungible_faucet) = self.components().iter().find(|component_interface| {
258            matches!(component_interface, AccountComponentInterface::BasicFungibleFaucet(_))
259        }) {
260            basic_fungible_faucet.send_note_body(*self.id(), output_notes)
261        } else if let Some(_network_fungible_faucet) =
262            self.components().iter().find(|component_interface| {
263                matches!(component_interface, AccountComponentInterface::NetworkFungibleFaucet(_))
264            })
265        {
266            // Network fungible faucet doesn't support send_note_body, because minting
267            // is done via a MINT note.
268            Err(AccountInterfaceError::UnsupportedAccountInterface)
269        } else if self.components().contains(&AccountComponentInterface::BasicWallet) {
270            AccountComponentInterface::BasicWallet.send_note_body(*self.id(), output_notes)
271        } else {
272            Err(AccountInterfaceError::UnsupportedAccountInterface)
273        }
274    }
275
276    /// Returns a string with the expiration delta update procedure call for the script.
277    fn build_set_tx_expiration_section(&self, expiration_delta: Option<u16>) -> String {
278        if let Some(expiration_delta) = expiration_delta {
279            format!("push.{expiration_delta} exec.::miden::tx::update_expiration_block_delta\n")
280        } else {
281            String::new()
282        }
283    }
284}
285
286impl From<&Account> for AccountInterface {
287    fn from(account: &Account) -> Self {
288        let components = AccountComponentInterface::from_procedures(account.code().procedures());
289        let mut auth = Vec::new();
290
291        // Find the auth component and extract all auth schemes from it
292        // An account should have only one auth component
293        for component in components.iter() {
294            if component.is_auth_component() {
295                auth = component.get_auth_schemes(account.storage());
296                break;
297            }
298        }
299
300        Self {
301            account_id: account.id(),
302            auth,
303            components,
304        }
305    }
306}
307
308// NOTE ACCOUNT COMPATIBILITY
309// ================================================================================================
310
311/// Describes whether a note is compatible with a specific account.
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum NoteAccountCompatibility {
314    /// A note is incompatible with an account.
315    ///
316    /// The account interface does not have procedures for being able to execute at least one of
317    /// the program execution branches.
318    No,
319    /// The account has all necessary procedures of one execution branch of the note script. This
320    /// means the note may be able to be consumed by the account if that branch is executed.
321    Maybe,
322    /// A note could be successfully executed and consumed by the account.
323    Yes,
324}
325
326// HELPER FUNCTIONS
327// ------------------------------------------------------------------------------------------------
328
329/// Verifies that the provided note script is compatible with the target account interfaces.
330///
331/// This is achieved by checking that at least one execution branch in the note script is compatible
332/// with the account procedures vector.
333///
334/// This check relies on the fact that account procedures are the only procedures that are `call`ed
335/// from note scripts, while kernel procedures are `sycall`ed.
336fn verify_note_script_compatibility(
337    note_script: &NoteScript,
338    account_procedures: BTreeSet<Word>,
339) -> NoteAccountCompatibility {
340    // collect call branches of the note script
341    let branches = collect_call_branches(note_script);
342
343    // if none of the branches are compatible with the target account, return a `CheckResult::No`
344    if !branches.iter().any(|call_targets| call_targets.is_subset(&account_procedures)) {
345        return NoteAccountCompatibility::No;
346    }
347
348    NoteAccountCompatibility::Maybe
349}
350
351/// Collect call branches by recursively traversing through program execution branches and
352/// accumulating call targets.
353fn collect_call_branches(note_script: &NoteScript) -> Vec<BTreeSet<Word>> {
354    let mut branches = vec![BTreeSet::new()];
355
356    let entry_node = note_script.entrypoint();
357    recursively_collect_call_branches(entry_node, &mut branches, &note_script.mast());
358    branches
359}
360
361/// Generates a list of calls invoked in each execution branch of the provided code block.
362fn recursively_collect_call_branches(
363    mast_node_id: MastNodeId,
364    branches: &mut Vec<BTreeSet<Word>>,
365    note_script_forest: &Arc<MastForest>,
366) {
367    let mast_node = &note_script_forest[mast_node_id];
368
369    match mast_node {
370        MastNode::Block(_) => {},
371        MastNode::Join(join_node) => {
372            recursively_collect_call_branches(join_node.first(), branches, note_script_forest);
373            recursively_collect_call_branches(join_node.second(), branches, note_script_forest);
374        },
375        MastNode::Split(split_node) => {
376            let current_branch = branches.last().expect("at least one execution branch").clone();
377            recursively_collect_call_branches(split_node.on_false(), branches, note_script_forest);
378
379            // If the previous branch had additional calls we need to create a new branch
380            if branches.last().expect("at least one execution branch").len() > current_branch.len()
381            {
382                branches.push(current_branch);
383            }
384
385            recursively_collect_call_branches(split_node.on_true(), branches, note_script_forest);
386        },
387        MastNode::Loop(loop_node) => {
388            recursively_collect_call_branches(loop_node.body(), branches, note_script_forest);
389        },
390        MastNode::Call(call_node) => {
391            if call_node.is_syscall() {
392                return;
393            }
394
395            let callee_digest = note_script_forest[call_node.callee()].digest();
396
397            branches
398                .last_mut()
399                .expect("at least one execution branch")
400                .insert(callee_digest);
401        },
402        MastNode::Dyn(_) => {},
403        MastNode::External(_) => {},
404    }
405}
406
407// ACCOUNT INTERFACE ERROR
408// ============================================================================================
409
410/// Account interface related errors.
411#[derive(Debug, Error)]
412pub enum AccountInterfaceError {
413    #[error("note asset is not issued by this faucet: {0}")]
414    IssuanceFaucetMismatch(AccountIdPrefix),
415    #[error("note created by the basic fungible faucet doesn't contain exactly one asset")]
416    FaucetNoteWithoutAsset,
417    #[error("invalid transaction script")]
418    InvalidTransactionScript(#[source] ScriptBuilderError),
419    #[error("invalid sender account: {0}")]
420    InvalidSenderAccount(AccountId),
421    #[error("{} interface does not support the generation of the standard send_note script", interface.name())]
422    UnsupportedInterface { interface: AccountComponentInterface },
423    #[error(
424        "account does not contain the basic fungible faucet or basic wallet interfaces which are needed to support the send_note script generation"
425    )]
426    UnsupportedAccountInterface,
427}