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