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 /// Builds a simple authentication script for the transaction that doesn't send any notes.
157 ///
158 /// Resulting transaction script is generated from this source:
159 ///
160 /// ```masm
161 /// begin
162 /// call.::miden::contracts::auth::basic::auth_tx_rpo_falcon512
163 /// end
164 /// ```
165 ///
166 /// # Errors:
167 /// Returns an error if:
168 /// - the account interface does not have any authentication schemes.
169 pub fn build_auth_script(
170 &self,
171 in_debug_mode: bool,
172 ) -> Result<TransactionScript, AccountInterfaceError> {
173 let auth_script_source = format!("begin\n{}\nend", self.build_tx_authentication_section());
174 let assembler = TransactionKernel::assembler().with_debug_mode(in_debug_mode);
175
176 TransactionScript::compile(auth_script_source, [], assembler)
177 .map_err(AccountInterfaceError::InvalidTransactionScript)
178 }
179
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, one output note and
198 /// RpoFalcon512 authentication:
199 ///
200 /// ```masm
201 /// begin
202 /// push.{expiration_delta} exec.::miden::tx::update_expiration_block_delta
203 ///
204 /// push.{note information}
205 ///
206 /// push.{asset amount}
207 /// call.::miden::contracts::faucets::basic_fungible::distribute dropw dropw drop
208 ///
209 /// call.::miden::contracts::auth::basic::auth_tx_rpo_falcon512
210 /// end
211 /// ```
212 ///
213 /// # Errors:
214 /// Returns an error if:
215 /// - the available interfaces does not support the generation of the standard `send_note`
216 /// procedure.
217 /// - the sender of the note isn't the account for which the script is being built.
218 /// - the note created by the faucet doesn't contain exactly one asset.
219 /// - a faucet tries to distribute an asset with a different faucet ID.
220 ///
221 /// [wallet]: crate::account::interface::AccountComponentInterface::BasicWallet
222 /// [faucet]: crate::account::interface::AccountComponentInterface::BasicFungibleFaucet
223 pub fn build_send_notes_script(
224 &self,
225 output_notes: &[PartialNote],
226 expiration_delta: Option<u16>,
227 in_debug_mode: bool,
228 ) -> Result<TransactionScript, AccountInterfaceError> {
229 let note_creation_source = self.build_create_notes_section(output_notes)?;
230
231 let script = format!(
232 "begin\n{}\n{}\n{}\nend",
233 self.build_set_tx_expiration_section(expiration_delta),
234 note_creation_source,
235 self.build_tx_authentication_section()
236 );
237
238 let assembler = TransactionKernel::assembler().with_debug_mode(in_debug_mode);
239 let tx_script = TransactionScript::compile(script, [], assembler)
240 .map_err(AccountInterfaceError::InvalidTransactionScript)?;
241
242 Ok(tx_script)
243 }
244
245 /// Returns a string with the authentication procedure call for the script.
246 fn build_tx_authentication_section(&self) -> String {
247 let mut auth_script = String::new();
248 self.auth().iter().for_each(|auth_scheme| match auth_scheme {
249 &AuthScheme::RpoFalcon512 { pub_key: _ } => {
250 auth_script
251 .push_str("call.::miden::contracts::auth::basic::auth_tx_rpo_falcon512\n");
252 },
253 });
254
255 auth_script
256 }
257
258 /// Generates a note creation code required for the `send_note` transaction script.
259 ///
260 /// For the example of the resulting code see [AccountComponentInterface::send_note_body]
261 /// description.
262 ///
263 /// # Errors:
264 /// Returns an error if:
265 /// - the available interfaces does not support the generation of the standard `send_note`
266 /// procedure.
267 /// - the sender of the note isn't the account for which the script is being built.
268 /// - the note created by the faucet doesn't contain exactly one asset.
269 /// - a faucet tries to distribute an asset with a different faucet ID.
270 fn build_create_notes_section(
271 &self,
272 output_notes: &[PartialNote],
273 ) -> Result<String, AccountInterfaceError> {
274 if let Some(basic_fungible_faucet) = self.components().iter().find(|component_interface| {
275 matches!(component_interface, AccountComponentInterface::BasicFungibleFaucet(_))
276 }) {
277 basic_fungible_faucet.send_note_body(*self.id(), output_notes)
278 } else if self.components().contains(&AccountComponentInterface::BasicWallet) {
279 AccountComponentInterface::BasicWallet.send_note_body(*self.id(), output_notes)
280 } else {
281 return Err(AccountInterfaceError::UnsupportedAccountInterface);
282 }
283 }
284
285 /// Returns a string with the expiration delta update procedure call for the script.
286 fn build_set_tx_expiration_section(&self, expiration_delta: Option<u16>) -> String {
287 if let Some(expiration_delta) = expiration_delta {
288 format!("push.{expiration_delta} exec.::miden::tx::update_expiration_block_delta\n")
289 } else {
290 String::new()
291 }
292 }
293}
294
295impl From<&Account> for AccountInterface {
296 fn from(account: &Account) -> Self {
297 let components = AccountComponentInterface::from_procedures(account.code().procedures());
298 let mut auth = Vec::new();
299 components.iter().for_each(|interface| {
300 if let AccountComponentInterface::RpoFalcon512(storage_index) = interface {
301 auth.push(AuthScheme::RpoFalcon512 {
302 pub_key: rpo_falcon512::PublicKey::new(
303 *account
304 .storage()
305 .get_item(*storage_index)
306 .expect("invalid storage index of the public key"),
307 ),
308 })
309 }
310 });
311
312 Self {
313 account_id: account.id(),
314 auth,
315 components,
316 }
317 }
318}
319
320// NOTE ACCOUNT COMPATIBILITY
321// ================================================================================================
322
323/// Describes whether a note is compatible with a specific account.
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum NoteAccountCompatibility {
326 /// A note is incompatible with an account.
327 ///
328 /// The account interface does not have procedures for being able to execute at least one of
329 /// the program execution branches.
330 No,
331 /// The account has all necessary procedures of one execution branch of the note script. This
332 /// means the note may be able to be consumed by the account if that branch is executed.
333 Maybe,
334 /// A note could be successfully executed and consumed by the account.
335 Yes,
336}
337
338// HELPER FUNCTIONS
339// ================================================================================================
340
341/// Verifies that the provided note script is compatible with the target account interfaces.
342///
343/// This is achieved by checking that at least one execution branch in the note script is compatible
344/// with the account procedures vector.
345///
346/// This check relies on the fact that account procedures are the only procedures that are `call`ed
347/// from note scripts, while kernel procedures are `sycall`ed.
348fn verify_note_script_compatibility(
349 note_script: &NoteScript,
350 account_procedures: BTreeSet<Digest>,
351) -> NoteAccountCompatibility {
352 // collect call branches of the note script
353 let branches = collect_call_branches(note_script);
354
355 // if none of the branches are compatible with the target account, return a `CheckResult::No`
356 if !branches.iter().any(|call_targets| call_targets.is_subset(&account_procedures)) {
357 return NoteAccountCompatibility::No;
358 }
359
360 NoteAccountCompatibility::Maybe
361}
362
363/// Collect call branches by recursively traversing through program execution branches and
364/// accumulating call targets.
365fn collect_call_branches(note_script: &NoteScript) -> Vec<BTreeSet<Digest>> {
366 let mut branches = vec![BTreeSet::new()];
367
368 let entry_node = note_script.entrypoint();
369 recursively_collect_call_branches(entry_node, &mut branches, ¬e_script.mast());
370 branches
371}
372
373/// Generates a list of calls invoked in each execution branch of the provided code block.
374fn recursively_collect_call_branches(
375 mast_node_id: MastNodeId,
376 branches: &mut Vec<BTreeSet<Digest>>,
377 note_script_forest: &Arc<MastForest>,
378) {
379 let mast_node = ¬e_script_forest[mast_node_id];
380
381 match mast_node {
382 MastNode::Block(_) => {},
383 MastNode::Join(join_node) => {
384 recursively_collect_call_branches(join_node.first(), branches, note_script_forest);
385 recursively_collect_call_branches(join_node.second(), branches, note_script_forest);
386 },
387 MastNode::Split(split_node) => {
388 let current_branch = branches.last().expect("at least one execution branch").clone();
389 recursively_collect_call_branches(split_node.on_false(), branches, note_script_forest);
390
391 // If the previous branch had additional calls we need to create a new branch
392 if branches.last().expect("at least one execution branch").len() > current_branch.len()
393 {
394 branches.push(current_branch);
395 }
396
397 recursively_collect_call_branches(split_node.on_true(), branches, note_script_forest);
398 },
399 MastNode::Loop(loop_node) => {
400 recursively_collect_call_branches(loop_node.body(), branches, note_script_forest);
401 },
402 MastNode::Call(call_node) => {
403 if call_node.is_syscall() {
404 return;
405 }
406
407 let callee_digest = note_script_forest[call_node.callee()].digest();
408
409 branches
410 .last_mut()
411 .expect("at least one execution branch")
412 .insert(callee_digest);
413 },
414 MastNode::Dyn(_) => {},
415 MastNode::External(_) => {},
416 }
417}
418
419// ACCOUNT INTERFACE ERROR
420// ============================================================================================
421
422/// Account interface related errors.
423#[derive(Debug, Error)]
424pub enum AccountInterfaceError {
425 #[error("note asset is not issued by this faucet: {0}")]
426 IssuanceFaucetMismatch(AccountIdPrefix),
427 #[error("note created by the basic fungible faucet doesn't contain exactly one asset")]
428 FaucetNoteWithoutAsset,
429 #[error("invalid transaction script")]
430 InvalidTransactionScript(#[source] TransactionScriptError),
431 #[error("invalid sender account: {0}")]
432 InvalidSenderAccount(AccountId),
433 #[error("{} interface does not support the generation of the standard send_note script", interface.name())]
434 UnsupportedInterface { interface: AccountComponentInterface },
435 #[error(
436 "account does not contain the basic fungible faucet or basic wallet interfaces which are needed to support the send_note script generation"
437 )]
438 UnsupportedAccountInterface,
439}