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