Skip to main content

miden_standards/account/interface/
component.rs

1use alloc::string::{String, ToString};
2use alloc::vec::Vec;
3
4use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
5use miden_protocol::account::{AccountId, AccountProcedureRoot, AccountStorage, StorageSlotName};
6use miden_protocol::note::PartialNote;
7use miden_protocol::{Felt, Word};
8
9use crate::AuthMethod;
10use crate::account::auth::{
11    AuthGuardedMultisig,
12    AuthMultisig,
13    AuthMultisigSmart,
14    AuthSingleSig,
15    AuthSingleSigAcl,
16    NetworkAccountNoteAllowlist,
17    NetworkAccountTxScriptAllowlist,
18};
19use crate::account::interface::AccountInterfaceError;
20
21// ACCOUNT COMPONENT INTERFACE
22// ================================================================================================
23
24/// The enum holding all possible account interfaces which could be loaded to some account.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AccountComponentInterface {
27    /// Exposes procedures from the [`BasicWallet`][crate::account::wallets::BasicWallet] module.
28    BasicWallet,
29    /// Exposes procedures from the
30    /// [`FungibleFaucet`][crate::account::faucets::FungibleFaucet] module.
31    FungibleFaucet,
32    /// Exposes procedures from the
33    /// [`Authority`][crate::account::access::Authority] access component.
34    Authority,
35    /// Exposes procedures from the
36    /// [`Ownable2Step`][crate::account::access::Ownable2Step] access component.
37    Ownable2Step,
38    /// Exposes procedures from the
39    /// [`RoleBasedAccessControl`][crate::account::access::RoleBasedAccessControl] access
40    /// component.
41    RoleBasedAccessControl,
42    /// Exposes procedures from the
43    /// [`AuthSingleSig`][crate::account::auth::AuthSingleSig] module.
44    AuthSingleSig,
45    /// Exposes procedures from the
46    /// [`AuthSingleSigAcl`][crate::account::auth::AuthSingleSigAcl] module.
47    AuthSingleSigAcl,
48    /// Exposes procedures from the
49    /// [`AuthMultisig`][crate::account::auth::AuthMultisig] module.
50    AuthMultisig,
51    /// Exposes procedures from the
52    /// [`AuthMultisigSmart`][crate::account::auth::AuthMultisigSmart] module.
53    AuthMultisigSmart,
54    /// Exposes procedures from the
55    /// [`AuthGuardedMultisig`][crate::account::auth::AuthGuardedMultisig] module.
56    AuthGuardedMultisig,
57    /// Exposes procedures from the [`NoAuth`][crate::account::auth::NoAuth] module.
58    ///
59    /// This authentication scheme provides no cryptographic authentication and only increments
60    /// the nonce if the account state has actually changed during transaction execution.
61    AuthNoAuth,
62    /// Exposes procedures from the
63    /// [`AuthNetworkAccount`][crate::account::auth::AuthNetworkAccount] module.
64    ///
65    /// This authentication scheme is intended for network-owned accounts. It rejects transactions
66    /// that executed a tx script or consumed input notes outside of a fixed allowlist of note
67    /// script roots.
68    AuthNetworkAccount,
69    /// A non-standard, custom interface which exposes the contained procedures.
70    ///
71    /// Custom interface holds all procedures which are not part of some standard interface which is
72    /// used by this account.
73    Custom(Vec<AccountProcedureRoot>),
74}
75
76impl AccountComponentInterface {
77    /// Returns a string line with the name of the [AccountComponentInterface] enum variant.
78    ///
79    /// In case of a [AccountComponentInterface::Custom] along with the name of the enum variant
80    /// the vector of shortened hex representations of the used procedures is returned, e.g.
81    /// `Custom([0x6d93447, 0x0bf23d8])`.
82    pub fn name(&self) -> String {
83        match self {
84            AccountComponentInterface::BasicWallet => "Basic Wallet".to_string(),
85            AccountComponentInterface::FungibleFaucet => "Fungible Faucet".to_string(),
86            AccountComponentInterface::Authority => "Authority".to_string(),
87            AccountComponentInterface::Ownable2Step => "Ownable2Step".to_string(),
88            AccountComponentInterface::RoleBasedAccessControl => {
89                "Role Based Access Control".to_string()
90            },
91            AccountComponentInterface::AuthSingleSig => "SingleSig".to_string(),
92            AccountComponentInterface::AuthSingleSigAcl => "SingleSig ACL".to_string(),
93            AccountComponentInterface::AuthMultisig => "Multisig".to_string(),
94            AccountComponentInterface::AuthMultisigSmart => "Multisig Smart".to_string(),
95            AccountComponentInterface::AuthGuardedMultisig => "Guarded Multisig".to_string(),
96            AccountComponentInterface::AuthNoAuth => "No Auth".to_string(),
97            AccountComponentInterface::AuthNetworkAccount => "Network Account Auth".to_string(),
98            AccountComponentInterface::Custom(proc_root_vec) => {
99                let result = proc_root_vec
100                    .iter()
101                    .map(|proc_root| proc_root.mast_root().to_hex()[..9].to_string())
102                    .collect::<Vec<_>>()
103                    .join(", ");
104                format!("Custom([{result}])")
105            },
106        }
107    }
108
109    /// Returns true if this component interface is an authentication component.
110    ///
111    /// TODO: currently this can identify only standard auth components
112    pub fn is_auth_component(&self) -> bool {
113        matches!(
114            self,
115            AccountComponentInterface::AuthSingleSig
116                | AccountComponentInterface::AuthSingleSigAcl
117                | AccountComponentInterface::AuthMultisig
118                | AccountComponentInterface::AuthMultisigSmart
119                | AccountComponentInterface::AuthGuardedMultisig
120                | AccountComponentInterface::AuthNoAuth
121                | AccountComponentInterface::AuthNetworkAccount
122        )
123    }
124
125    /// Returns the authentication schemes associated with this component interface.
126    pub fn get_auth_methods(&self, storage: &AccountStorage) -> Vec<AuthMethod> {
127        match self {
128            AccountComponentInterface::AuthSingleSig => vec![extract_singlesig_auth_method(
129                storage,
130                AuthSingleSig::public_key_slot(),
131                AuthSingleSig::scheme_id_slot(),
132            )],
133            AccountComponentInterface::AuthSingleSigAcl => vec![extract_singlesig_auth_method(
134                storage,
135                AuthSingleSigAcl::public_key_slot(),
136                AuthSingleSigAcl::scheme_id_slot(),
137            )],
138            AccountComponentInterface::AuthMultisig => {
139                vec![extract_multisig_auth_method(
140                    storage,
141                    AuthMultisig::threshold_config_slot(),
142                    AuthMultisig::approver_public_keys_slot(),
143                    AuthMultisig::approver_scheme_ids_slot(),
144                )]
145            },
146            AccountComponentInterface::AuthGuardedMultisig => {
147                vec![extract_multisig_auth_method(
148                    storage,
149                    AuthGuardedMultisig::threshold_config_slot(),
150                    AuthGuardedMultisig::approver_public_keys_slot(),
151                    AuthGuardedMultisig::approver_scheme_ids_slot(),
152                )]
153            },
154            AccountComponentInterface::AuthMultisigSmart => {
155                vec![extract_multisig_auth_method(
156                    storage,
157                    AuthMultisigSmart::threshold_config_slot(),
158                    AuthMultisigSmart::approver_public_keys_slot(),
159                    AuthMultisigSmart::approver_scheme_ids_slot(),
160                )]
161            },
162            AccountComponentInterface::AuthNoAuth => vec![AuthMethod::NoAuth],
163            AccountComponentInterface::AuthNetworkAccount => {
164                vec![extract_network_account_auth_method(storage)]
165            },
166            _ => vec![], // Non-auth components return empty vector
167        }
168    }
169
170    /// Generates a body for the note creation of the `send_note` transaction script. The resulting
171    /// code could use different procedures for note creation, which depends on the used interface.
172    ///
173    /// The body consists of two sections:
174    /// - Pushing the note information on the stack.
175    /// - Creating a note:
176    ///   - For basic fungible faucet: pushing the amount of assets and distributing them.
177    ///   - For basic wallet: creating a note, pushing the assets on the stack and moving them to
178    ///     the created note.
179    ///
180    /// # Examples
181    ///
182    /// Example script for the [`AccountComponentInterface::BasicWallet`] with one note:
183    ///
184    /// ```masm
185    ///     push.{note_information}
186    ///     call.::miden::protocol::output_note::create
187    ///
188    ///     push.{note asset}
189    ///     call.::miden::standards::wallets::basic::move_asset_to_note dropw
190    ///     dropw dropw dropw drop
191    /// ```
192    ///
193    /// Example script for the [`AccountComponentInterface::FungibleFaucet`] with one note:
194    ///
195    /// ```masm
196    ///     push.{note information}
197    ///
198    ///     push.{ASSET_VALUE} push.{ASSET_KEY}
199    ///     call.::miden::standards::faucets::fungible::mint_and_send
200    ///     swapdw dropw dropw swapdw dropw dropw
201    /// ```
202    ///
203    /// # Errors:
204    /// Returns an error if:
205    /// - the interface does not support the generation of the standard `send_note` procedure.
206    /// - the sender of the note isn't the account for which the script is being built.
207    /// - the note created by the faucet doesn't contain exactly one asset.
208    /// - a faucet tries to mint an asset with a different faucet ID.
209    pub(crate) fn send_note_body(
210        &self,
211        sender_account_id: AccountId,
212        notes: &[PartialNote],
213    ) -> Result<String, AccountInterfaceError> {
214        let mut body = String::new();
215
216        for partial_note in notes {
217            if partial_note.metadata().sender() != sender_account_id {
218                return Err(AccountInterfaceError::InvalidSenderAccount(
219                    partial_note.metadata().sender(),
220                ));
221            }
222
223            body.push_str(&format!(
224                "
225                push.{recipient}
226                push.{note_type}
227                push.{tag}
228                # => [tag, note_type, RECIPIENT, pad(16)]
229                ",
230                recipient = partial_note.recipient_digest(),
231                note_type = Felt::from(partial_note.metadata().note_type()),
232                tag = Felt::from(partial_note.metadata().tag()),
233            ));
234
235            match self {
236                AccountComponentInterface::FungibleFaucet => {
237                    if partial_note.assets().num_assets() != 1 {
238                        return Err(AccountInterfaceError::FaucetNoteWithoutAsset);
239                    }
240
241                    // SAFETY: We checked that the note contains exactly one asset
242                    let asset =
243                        partial_note.assets().iter().next().expect("note should contain an asset");
244
245                    if asset.faucet_id() != sender_account_id {
246                        return Err(AccountInterfaceError::IssuanceFaucetMismatch(
247                            asset.faucet_id(),
248                        ));
249                    }
250
251                    body.push_str(&format!(
252                        "
253                        push.{ASSET_VALUE}
254                        push.{ASSET_KEY}
255                        # => [ASSET_KEY, ASSET_VALUE, tag, note_type, RECIPIENT, pad(16)]
256
257                        call.::miden::standards::faucets::fungible::mint_and_send
258                        # => [note_idx, pad(29)]
259
260                        swapdw dropw dropw swapdw dropw dropw
261                        # => [note_idx, pad(13)]\n
262                        ",
263                        ASSET_KEY = asset.to_key_word(),
264                        ASSET_VALUE = asset.to_value_word(),
265                    ));
266                },
267                AccountComponentInterface::BasicWallet => {
268                    body.push_str(
269                        "
270                    exec.::miden::protocol::output_note::create
271                    # => [note_idx, pad(16)]\n
272                    ",
273                    );
274
275                    for asset in partial_note.assets().iter() {
276                        body.push_str(&format!(
277                            "
278                            # duplicate note index
279                            padw push.0 push.0 push.0 dup.7
280                            # => [note_idx, pad(7), note_idx, pad(16)]
281
282                            push.{ASSET_VALUE}
283                            push.{ASSET_KEY}
284                            # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7), note_idx, pad(16)]
285
286                            call.::miden::standards::wallets::basic::move_asset_to_note
287                            # => [pad(16), note_idx, pad(16)]
288
289                            dropw dropw dropw dropw
290                            # => [note_idx, pad(16)]\n
291                            ",
292                            ASSET_KEY = asset.to_key_word(),
293                            ASSET_VALUE = asset.to_value_word(),
294                        ));
295                    }
296                },
297                _ => {
298                    return Err(AccountInterfaceError::UnsupportedInterface {
299                        interface: self.clone(),
300                    });
301                },
302            }
303
304            for attachment in partial_note.attachments().iter() {
305                let attachment_scheme = attachment.attachment_scheme().as_u16();
306                let attachment_commitment = attachment.content().to_commitment();
307
308                body.push_str(&format!(
309                    "
310                dup
311                push.{attachment_commitment}
312                push.{attachment_scheme}
313                # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx, pad(16)]
314                exec.::miden::protocol::output_note::add_attachment
315                # => [note_idx, pad(16)]
316            ",
317                ));
318            }
319
320            body.push_str(
321                "
322                # drop the note idx
323                drop
324                # => [pad(16)]
325            ",
326            );
327        }
328
329        Ok(body)
330    }
331}
332
333// HELPER FUNCTIONS
334// ================================================================================================
335
336/// Extracts authentication method from a single-signature component.
337fn extract_singlesig_auth_method(
338    storage: &AccountStorage,
339    public_key_slot: &StorageSlotName,
340    scheme_id_slot: &StorageSlotName,
341) -> AuthMethod {
342    let pub_key = PublicKeyCommitment::from(
343        storage
344            .get_item(public_key_slot)
345            .expect("invalid storage index of the public key"),
346    );
347
348    let scheme_id = storage
349        .get_item(scheme_id_slot)
350        .expect("invalid storage index of the scheme id")[0]
351        .as_canonical_u64() as u8;
352
353    let auth_scheme =
354        AuthScheme::try_from(scheme_id).expect("invalid auth scheme id in the scheme id slot");
355
356    AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }
357}
358
359/// Extracts authentication method from a multisig component.
360fn extract_multisig_auth_method(
361    storage: &AccountStorage,
362    config_slot: &StorageSlotName,
363    approver_public_keys_slot: &StorageSlotName,
364    approver_scheme_ids_slot: &StorageSlotName,
365) -> AuthMethod {
366    // Read the multisig configuration from the config slot
367    // Format: [threshold, num_approvers, 0, 0]
368    let config = storage
369        .get_item(config_slot)
370        .expect("invalid slot name of the multisig configuration");
371
372    let threshold = config[0].as_canonical_u64() as u32;
373    let num_approvers = config[1].as_canonical_u64() as u8;
374
375    let mut approvers = Vec::new();
376
377    // Read each public key from the map
378    for key_index in 0..num_approvers {
379        // The multisig component stores keys and scheme IDs using pattern [index, 0, 0, 0]
380        let map_key = Word::from([key_index as u32, 0, 0, 0]);
381
382        let pub_key_word =
383            storage.get_map_item(approver_public_keys_slot, map_key).unwrap_or_else(|_| {
384                panic!(
385                    "Failed to read public key {} from multisig configuration at storage slot {}. \
386                     Expected key pattern [index, 0, 0, 0].",
387                    key_index, approver_public_keys_slot
388                )
389            });
390
391        let pub_key = PublicKeyCommitment::from(pub_key_word);
392
393        let scheme_word = storage
394            .get_map_item(approver_scheme_ids_slot, map_key)
395            .unwrap_or_else(|_| {
396                panic!(
397                    "Failed to read scheme id for approver {} from multisig configuration at storage slot {}. \
398                     Expected key pattern [index, 0, 0, 0].",
399                    key_index, approver_scheme_ids_slot
400                )
401            });
402
403        let scheme_id = scheme_word[0].as_canonical_u64() as u8;
404        let auth_scheme =
405            AuthScheme::try_from(scheme_id).expect("invalid auth scheme id in the scheme id slot");
406        approvers.push((pub_key, auth_scheme));
407    }
408
409    AuthMethod::Multisig { threshold, approvers }
410}
411
412/// Extracts authentication method from a network-account component.
413fn extract_network_account_auth_method(storage: &AccountStorage) -> AuthMethod {
414    let allowlist = NetworkAccountNoteAllowlist::try_from(storage)
415        .expect("network account note allowlist slot should be present and valid");
416    let tx_script_allowlist = NetworkAccountTxScriptAllowlist::try_from(storage)
417        .expect("network account tx script allowlist slot should be present and valid");
418
419    AuthMethod::NetworkAccount {
420        allowed_script_roots: allowlist.into_allowed_script_roots(),
421        allowed_tx_script_roots: tx_script_allowlist.into_allowed_script_roots(),
422    }
423}