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