miden_lib/account/interface/
mod.rs1use 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
28pub struct AccountInterface {
36 account_id: AccountId,
37 auth: Vec<AuthScheme>,
38 components: Vec<AccountComponentInterface>,
39}
40
41impl AccountInterface {
44 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 pub fn id(&self) -> &AccountId {
60 &self.account_id
61 }
62
63 pub fn account_type(&self) -> AccountType {
65 self.account_id.account_type()
66 }
67
68 pub fn is_faucet(&self) -> bool {
70 self.account_id.is_faucet()
71 }
72
73 pub fn is_regular_account(&self) -> bool {
75 self.account_id.is_regular_account()
76 }
77
78 pub fn is_public(&self) -> bool {
80 self.account_id.is_public()
81 }
82
83 pub fn auth(&self) -> &Vec<AuthScheme> {
85 &self.auth
86 }
87
88 pub fn components(&self) -> &Vec<AccountComponentInterface> {
90 &self.components
91 }
92
93 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 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
135impl AccountInterface {
138 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum NoteAccountCompatibility {
306 No,
311 Maybe,
314}
315
316fn verify_note_script_compatibility(
327 note_script: &NoteScript,
328 account_procedures: BTreeSet<Digest>,
329) -> NoteAccountCompatibility {
330 let branches = collect_call_branches(note_script);
332
333 if !branches.iter().any(|call_targets| call_targets.is_subset(&account_procedures)) {
335 return NoteAccountCompatibility::No;
336 }
337
338 NoteAccountCompatibility::Maybe
339}
340
341fn 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, ¬e_script.mast());
348 branches
349}
350
351fn 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 = ¬e_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 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#[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}