Skip to main content

miden_standards/account/wallets/
mod.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
4use miden_protocol::account::{
5    Account,
6    AccountBuilder,
7    AccountComponent,
8    AccountComponentName,
9    AccountProcedureRoot,
10    AccountType,
11};
12use miden_protocol::errors::AccountError;
13
14use crate::account::account_component_code;
15use crate::account::auth::{
16    Approver,
17    ApproverSet,
18    AuthGuardedMultisig,
19    AuthGuardedMultisigConfig,
20    AuthMultisig,
21    AuthMultisigConfig,
22    AuthSingleSig,
23    GuardianConfig,
24};
25use crate::procedure_root;
26
27mod note_creator;
28pub use note_creator::NoteCreator;
29
30// BASIC WALLET
31// ================================================================================================
32
33account_component_code!(BASIC_WALLET_CODE, "miden-standards-wallets-basic-wallet.masp");
34
35// PROCEDURE ROOTS
36// ================================================================================================
37
38/// MASL library namespace used for procedure-root lookups. Distinct from [`BasicWallet::NAME`],
39/// which mirrors the standards-side MASM module path.
40const BASIC_WALLET_LIBRARY_PATH: &str = "miden::standards::components::wallets::basic_wallet";
41
42// Initialize the procedure root of the `receive_asset` procedure of the Basic Wallet only once.
43procedure_root!(
44    BASIC_WALLET_RECEIVE_ASSET,
45    BASIC_WALLET_LIBRARY_PATH,
46    BasicWallet::RECEIVE_ASSET_PROC_NAME,
47    BasicWallet::code()
48);
49
50// Initialize the procedure root of the `move_asset_to_note` procedure of the Basic Wallet only
51// once.
52procedure_root!(
53    BASIC_WALLET_MOVE_ASSET_TO_NOTE,
54    BASIC_WALLET_LIBRARY_PATH,
55    BasicWallet::MOVE_ASSET_TO_NOTE_PROC_NAME,
56    BasicWallet::code()
57);
58
59// Initialize the procedure root of the `create_note` procedure of the Basic Wallet only once.
60procedure_root!(
61    BASIC_WALLET_CREATE_NOTE,
62    BASIC_WALLET_LIBRARY_PATH,
63    BasicWallet::CREATE_NOTE_PROC_NAME,
64    BasicWallet::code()
65);
66
67/// An [`AccountComponent`] implementing a basic wallet.
68///
69/// It reexports the procedures from `miden::standards::wallets::basic` and
70/// `miden::standards::note::create_note` modules. When linking against this component, the `miden`
71/// library (i.e. [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler
72/// which is the case when using [`CodeBuilder`][builder]. The procedures of this component are:
73/// - `receive_asset`, which can be used to add an asset to the account.
74/// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add
75///   it to the output note with the specified index.
76/// - `create_note`, which can be used to create a new output note and return its index.
77///
78/// All methods require authentication. Thus, this component must be combined with a component
79/// providing authentication.
80///
81/// [builder]: crate::code_builder::CodeBuilder
82pub struct BasicWallet;
83
84impl BasicWallet {
85    // CONSTANTS
86    // --------------------------------------------------------------------------------------------
87
88    /// The name of the component.
89    pub const NAME: &'static str = "miden::standards::wallets::basic_wallet";
90
91    const RECEIVE_ASSET_PROC_NAME: &str = "receive_asset";
92    const MOVE_ASSET_TO_NOTE_PROC_NAME: &str = "move_asset_to_note";
93    const CREATE_NOTE_PROC_NAME: &str = "create_note";
94
95    /// Returns the canonical [`AccountComponentName`] of this component.
96    pub const fn name() -> AccountComponentName {
97        AccountComponentName::from_static_str(Self::NAME)
98    }
99
100    // PUBLIC ACCESSORS
101    // --------------------------------------------------------------------------------------------
102
103    /// Returns the [`AccountComponentCode`] of this component.
104    pub fn code() -> &'static AccountComponentCode {
105        &BASIC_WALLET_CODE
106    }
107
108    /// Returns the procedure root of the `receive_asset` wallet procedure.
109    pub fn receive_asset_root() -> AccountProcedureRoot {
110        *BASIC_WALLET_RECEIVE_ASSET
111    }
112
113    /// Returns the procedure root of the `move_asset_to_note` wallet procedure.
114    pub fn move_asset_to_note_root() -> AccountProcedureRoot {
115        *BASIC_WALLET_MOVE_ASSET_TO_NOTE
116    }
117
118    /// Returns the procedure root of the `create_note` wallet procedure.
119    pub fn create_note_root() -> AccountProcedureRoot {
120        *BASIC_WALLET_CREATE_NOTE
121    }
122
123    /// Returns the [`AccountComponentMetadata`] for this component.
124    pub fn component_metadata() -> AccountComponentMetadata {
125        AccountComponentMetadata::new(Self::NAME)
126            .with_description("Basic wallet component for receiving and sending assets")
127    }
128}
129
130impl From<BasicWallet> for AccountComponent {
131    fn from(_: BasicWallet) -> Self {
132        let metadata = BasicWallet::component_metadata();
133
134        AccountComponent::new(BasicWallet::code().clone(), vec![], metadata).expect(
135            "basic wallet component should satisfy the requirements of a valid account component",
136        )
137    }
138}
139
140// WALLET CREATION
141// ================================================================================================
142
143/// Creates a new account with a basic wallet interface, single signature authentication and the
144/// specified account type.
145///
146/// The basic wallet interface exposes two procedures:
147/// - `receive_asset`, which can be used to add an asset to the account.
148/// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add
149///   it to the output note with the specified index.
150///
151/// All methods require authentication, which is provided by an [`AuthSingleSig`] component
152/// configured with the given approver.
153pub fn create_basic_wallet(
154    init_seed: [u8; 32],
155    approver: Approver,
156    account_type: AccountType,
157) -> Result<Account, AccountError> {
158    let auth_component: AccountComponent = AuthSingleSig::new(approver).into();
159
160    create_wallet(init_seed, auth_component, account_type)
161}
162
163/// Creates a new account with a basic wallet interface, multi-signature authentication and the
164/// specified account type.
165///
166/// Authentication is provided by an [`AuthMultisig`] component requiring the default threshold of
167/// `approver_set` approver signatures, with optional per-procedure threshold overrides in
168/// `proc_thresholds`.
169///
170/// # Security
171///
172/// See [`AuthMultisig`] for important caveats regarding per-procedure thresholds and private
173/// account state withholding. For private accounts this constructor rejects per-procedure
174/// thresholds below the default threshold (a lower threshold would let a sub-quorum advance and
175/// withhold the private account state); public accounts allow any per-procedure threshold.
176pub fn create_multisig_wallet(
177    init_seed: [u8; 32],
178    approver_set: ApproverSet,
179    proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
180    account_type: AccountType,
181) -> Result<Account, AccountError> {
182    let default_threshold = approver_set.threshold().get();
183    if account_type == AccountType::Private
184        && proc_thresholds
185            .iter()
186            .any(|(_, proc_threshold)| *proc_threshold < default_threshold)
187    {
188        return Err(AccountError::other(
189            "private multisig wallets do not allow per-procedure thresholds below the default \
190             threshold, as a lower threshold would let a sub-quorum advance and withhold the \
191             private account state; use a guarded wallet to lower thresholds safely",
192        ));
193    }
194
195    let config = AuthMultisigConfig::new(approver_set).with_proc_thresholds(proc_thresholds)?;
196    let auth_component: AccountComponent = AuthMultisig::new(config)?.into();
197
198    create_wallet(init_seed, auth_component, account_type)
199}
200
201/// Creates a new account with a basic wallet interface, guarded multi-signature authentication and
202/// the specified account type.
203///
204/// Authentication is provided by an [`AuthGuardedMultisig`] component: every operation requires
205/// both the default threshold of `approver_set` approver signatures (with optional per-procedure
206/// overrides in `proc_thresholds`) and a valid signature from the configured `guardian`.
207pub fn create_guarded_wallet(
208    init_seed: [u8; 32],
209    approver_set: ApproverSet,
210    proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
211    guardian: GuardianConfig,
212    account_type: AccountType,
213) -> Result<Account, AccountError> {
214    let config = AuthGuardedMultisigConfig::new(approver_set, guardian)?
215        .with_proc_thresholds(proc_thresholds)?;
216    let auth_component: AccountComponent = AuthGuardedMultisig::new(config)?.into();
217
218    create_wallet(init_seed, auth_component, account_type)
219}
220
221/// Creates a basic wallet account from the given authentication component and account type.
222fn create_wallet(
223    init_seed: [u8; 32],
224    auth_component: AccountComponent,
225    account_type: AccountType,
226) -> Result<Account, AccountError> {
227    AccountBuilder::new(init_seed)
228        .account_type(account_type)
229        .with_component(auth_component)
230        .with_component(BasicWallet)
231        .build()
232}
233
234// TESTS
235// ================================================================================================
236
237#[cfg(test)]
238mod tests {
239    use alloc::string::ToString;
240
241    use miden_protocol::account::auth::{self, PublicKeyCommitment};
242    use miden_protocol::utils::serde::{Deserializable, Serializable};
243    use miden_protocol::{ONE, Word};
244
245    use super::{
246        Account,
247        AccountType,
248        Approver,
249        ApproverSet,
250        AuthMultisig,
251        GuardianConfig,
252        create_basic_wallet,
253        create_guarded_wallet,
254        create_multisig_wallet,
255    };
256    use crate::account::wallets::BasicWallet;
257
258    fn approver(seed: u32) -> Approver {
259        Approver::new(
260            PublicKeyCommitment::from(Word::from([seed, seed, seed, seed])),
261            auth::AuthScheme::Falcon512Poseidon2,
262        )
263    }
264
265    #[test]
266    fn test_create_basic_wallet() -> anyhow::Result<()> {
267        create_basic_wallet([1; 32], approver(1), AccountType::Public)?;
268        Ok(())
269    }
270
271    #[test]
272    fn test_serialize_basic_wallet() -> anyhow::Result<()> {
273        let approver = Approver::new(
274            PublicKeyCommitment::from(Word::from([ONE; 4])),
275            auth::AuthScheme::EcdsaK256Keccak,
276        );
277        let wallet = create_basic_wallet([1; 32], approver, AccountType::Public)?;
278
279        let bytes = wallet.to_bytes();
280        let deserialized_wallet = Account::read_from_bytes(&bytes)?;
281        assert_eq!(wallet, deserialized_wallet);
282
283        Ok(())
284    }
285
286    #[test]
287    fn test_create_multisig_wallet_public_allows_lower_override() -> anyhow::Result<()> {
288        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
289        let proc_thresholds = vec![(BasicWallet::receive_asset_root(), 1)];
290
291        // A public account may use a per-procedure threshold below the default.
292        create_multisig_wallet([1; 32], approver_set, proc_thresholds, AccountType::Public)?;
293
294        Ok(())
295    }
296
297    #[test]
298    fn test_create_multisig_wallet_private_no_override_succeeds() -> anyhow::Result<()> {
299        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
300
301        // No overrides is always allowed for private accounts.
302        create_multisig_wallet([1; 32], approver_set, vec![], AccountType::Private)?;
303
304        Ok(())
305    }
306
307    #[test]
308    fn test_create_multisig_wallet_private_higher_override_succeeds() -> anyhow::Result<()> {
309        let approver_set = ApproverSet::new(vec![approver(1), approver(2), approver(3)], 2)?;
310        // Hardening a procedure above the default (2 -> 3) is safe for private accounts, as long as
311        // the override editing procedure is hardened to the same level so the override cannot be
312        // lowered by a smaller quorum.
313        let proc_thresholds = vec![
314            (BasicWallet::move_asset_to_note_root(), 3),
315            (AuthMultisig::set_procedure_threshold_root(), 3),
316        ];
317
318        create_multisig_wallet([1; 32], approver_set, proc_thresholds, AccountType::Private)?;
319
320        Ok(())
321    }
322
323    #[test]
324    fn test_create_multisig_wallet_private_lower_override_rejected() -> anyhow::Result<()> {
325        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
326        let proc_thresholds = vec![(BasicWallet::receive_asset_root(), 1)];
327
328        let err =
329            create_multisig_wallet([1; 32], approver_set, proc_thresholds, AccountType::Private)
330                .expect_err("private multisig with a below-default threshold must be rejected");
331
332        assert!(
333            err.to_string()
334                .contains("do not allow per-procedure thresholds below the default threshold")
335        );
336
337        Ok(())
338    }
339
340    #[test]
341    fn test_create_guarded_wallet_private_override_allowed() -> anyhow::Result<()> {
342        let approver_set = ApproverSet::new(vec![approver(1), approver(2)], 2)?;
343        let proc_thresholds = vec![(BasicWallet::receive_asset_root(), 1)];
344        let guardian = GuardianConfig::new(approver(3));
345
346        // The guardian forwards state, so a private guarded wallet may use overrides.
347        create_guarded_wallet(
348            [1; 32],
349            approver_set,
350            proc_thresholds,
351            guardian,
352            AccountType::Private,
353        )?;
354
355        Ok(())
356    }
357
358    /// Check that the obtaining of the basic wallet procedure roots does not panic.
359    #[test]
360    fn get_faucet_procedures() {
361        let _receive_asset_root = BasicWallet::receive_asset_root();
362        let _move_asset_to_note_root = BasicWallet::move_asset_to_note_root();
363    }
364}