miden_lib/account/interface/
mod.rs

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