miden_lib/account/auth/
ecdsa_k256_keccak_acl.rs

1use alloc::vec::Vec;
2
3use miden_objects::account::auth::PublicKeyCommitment;
4use miden_objects::account::{AccountCode, AccountComponent, StorageMap, StorageSlot};
5use miden_objects::{AccountError, Word};
6
7use crate::account::components::ecdsa_k256_keccak_acl_library;
8
9/// Configuration for [`AuthEcdsaK256KeccakAcl`] component.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AuthEcdsaK256KeccakAclConfig {
12    /// List of procedure roots that require authentication when called.
13    pub auth_trigger_procedures: Vec<Word>,
14    /// When `false`, creating output notes (sending notes to other accounts) requires
15    /// authentication. When `true`, output notes can be created without authentication.
16    pub allow_unauthorized_output_notes: bool,
17    /// When `false`, consuming input notes (processing notes sent to this account) requires
18    /// authentication. When `true`, input notes can be consumed without authentication.
19    pub allow_unauthorized_input_notes: bool,
20}
21
22impl AuthEcdsaK256KeccakAclConfig {
23    /// Creates a new configuration with no trigger procedures and both flags set to `false` (most
24    /// restrictive).
25    pub fn new() -> Self {
26        Self {
27            auth_trigger_procedures: vec![],
28            allow_unauthorized_output_notes: false,
29            allow_unauthorized_input_notes: false,
30        }
31    }
32
33    /// Sets the list of procedure roots that require authentication when called.
34    pub fn with_auth_trigger_procedures(mut self, procedures: Vec<Word>) -> Self {
35        self.auth_trigger_procedures = procedures;
36        self
37    }
38
39    /// Sets whether unauthorized output notes are allowed.
40    pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
41        self.allow_unauthorized_output_notes = allow;
42        self
43    }
44
45    /// Sets whether unauthorized input notes are allowed.
46    pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
47        self.allow_unauthorized_input_notes = allow;
48        self
49    }
50}
51
52impl Default for AuthEcdsaK256KeccakAclConfig {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58/// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the
59/// EcdsaK256Keccak signature scheme for authentication of transactions.
60///
61/// This component provides fine-grained authentication control based on three conditions:
62/// 1. **Procedure-based authentication**: Requires authentication when any of the specified trigger
63///    procedures are called during the transaction.
64/// 2. **Output note authentication**: Controls whether creating output notes requires
65///    authentication. Output notes are new notes created by the account and sent to other accounts
66///    (e.g., when transferring assets). When `allow_unauthorized_output_notes` is `false`, any
67///    transaction that creates output notes must be authenticated, ensuring account owners control
68///    when their account sends assets to other accounts.
69/// 3. **Input note authentication**: Controls whether consuming input notes requires
70///    authentication. Input notes are notes that were sent to this account by other accounts (e.g.,
71///    incoming asset transfers). When `allow_unauthorized_input_notes` is `false`, any transaction
72///    that consumes input notes must be authenticated, ensuring account owners control when their
73///    account processes incoming notes.
74///
75/// ## Authentication Logic
76///
77/// Authentication is required if ANY of the following conditions are true:
78/// - Any trigger procedure from the ACL was called
79/// - Output notes were created AND `allow_unauthorized_output_notes` is `false`
80/// - Input notes were consumed AND `allow_unauthorized_input_notes` is `false`
81///
82/// If none of these conditions are met, only the nonce is incremented without requiring a
83/// signature.
84///
85/// ## Use Cases
86///
87/// - **Restrictive mode** (`allow_unauthorized_output_notes=false`,
88///   `allow_unauthorized_input_notes=false`): All note operations require authentication, providing
89///   maximum security.
90/// - **Selective mode**: Allow some note operations without authentication while protecting
91///   specific procedures, useful for accounts that need to process certain operations
92///   automatically.
93/// - **Procedure-only mode** (`allow_unauthorized_output_notes=true`,
94///   `allow_unauthorized_input_notes=true`): Only specific procedures require authentication,
95///   allowing free note processing.
96///
97/// ## Storage Layout
98/// - Slot 0(value): Public key (same as EcdsaK256Keccak)
99/// - Slot 1(value): [num_tracked_procs, allow_unauthorized_output_notes,
100///   allow_unauthorized_input_notes, 0]
101/// - Slot 2(map): A map with trigger procedure roots
102///
103/// ## Important Note on Procedure Detection
104/// The procedure-based authentication relies on the `was_procedure_called` kernel function,
105/// which only returns `true` if the procedure in question called into a kernel account API
106/// that is restricted to the account context. Procedures that don't interact with account
107/// state or kernel APIs may not be detected as "called" even if they were executed during
108/// the transaction. This is an important limitation to consider when designing trigger
109/// procedures for authentication.
110///
111/// This component supports all account types.
112pub struct AuthEcdsaK256KeccakAcl {
113    pub_key: PublicKeyCommitment,
114    config: AuthEcdsaK256KeccakAclConfig,
115}
116
117impl AuthEcdsaK256KeccakAcl {
118    /// Creates a new [`AuthEcdsaK256KeccakAcl`] component with the given `public_key` and
119    /// configuration.
120    ///
121    /// # Panics
122    /// Panics if more than [AccountCode::MAX_NUM_PROCEDURES] procedures are specified.
123    pub fn new(
124        pub_key: PublicKeyCommitment,
125        config: AuthEcdsaK256KeccakAclConfig,
126    ) -> Result<Self, AccountError> {
127        let max_procedures = AccountCode::MAX_NUM_PROCEDURES;
128        if config.auth_trigger_procedures.len() > max_procedures {
129            return Err(AccountError::other(format!(
130                "Cannot track more than {max_procedures} procedures (account limit)"
131            )));
132        }
133
134        Ok(Self { pub_key, config })
135    }
136}
137
138impl From<AuthEcdsaK256KeccakAcl> for AccountComponent {
139    fn from(ecdsa: AuthEcdsaK256KeccakAcl) -> Self {
140        let mut storage_slots = Vec::with_capacity(3);
141
142        // Slot 0: Public key
143        storage_slots.push(StorageSlot::Value(ecdsa.pub_key.into()));
144        // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes,
145        // allow_unauthorized_input_notes, 0]
146        let num_procs = ecdsa.config.auth_trigger_procedures.len() as u32;
147        storage_slots.push(StorageSlot::Value(Word::from([
148            num_procs,
149            u32::from(ecdsa.config.allow_unauthorized_output_notes),
150            u32::from(ecdsa.config.allow_unauthorized_input_notes),
151            0,
152        ])));
153
154        // Slot 2: A map with tracked procedure roots
155        // We add the map even if there are no trigger procedures, to always maintain the same
156        // storage layout.
157        let map_entries = ecdsa
158            .config
159            .auth_trigger_procedures
160            .iter()
161            .enumerate()
162            .map(|(i, proc_root)| (Word::from([i as u32, 0, 0, 0]), *proc_root));
163
164        // Safe to unwrap because we know that the map keys are unique.
165        storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
166
167        AccountComponent::new(ecdsa_k256_keccak_acl_library(), storage_slots)
168            .expect(
169                "ACL auth component should satisfy the requirements of a valid account component",
170            )
171            .with_supports_all_types()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use miden_objects::Word;
178    use miden_objects::account::AccountBuilder;
179
180    use super::*;
181    use crate::account::components::WellKnownComponent;
182    use crate::account::wallets::BasicWallet;
183
184    /// Test configuration for parametrized ACL tests
185    struct AclTestConfig {
186        /// Whether to include auth trigger procedures
187        with_procedures: bool,
188        /// Allow unauthorized output notes flag
189        allow_unauthorized_output_notes: bool,
190        /// Allow unauthorized input notes flag
191        allow_unauthorized_input_notes: bool,
192        /// Expected slot 1 value [num_procs, allow_output, allow_input, 0]
193        expected_slot_1: Word,
194    }
195
196    /// Helper function to get the basic wallet procedures for testing
197    fn get_basic_wallet_procedures() -> Vec<Word> {
198        // Get the two trigger procedures from BasicWallet: `receive_asset`, `move_asset_to_note`.
199        let procedures: Vec<Word> = WellKnownComponent::BasicWallet.procedure_digests().collect();
200
201        assert_eq!(procedures.len(), 2);
202        procedures
203    }
204
205    /// Parametrized test helper for ACL component testing
206    fn test_acl_component(config: AclTestConfig) {
207        let public_key = PublicKeyCommitment::from(Word::empty());
208
209        // Build the configuration
210        let mut acl_config = AuthEcdsaK256KeccakAclConfig::new()
211            .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
212            .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
213
214        let auth_trigger_procedures = if config.with_procedures {
215            let procedures = get_basic_wallet_procedures();
216            acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
217            procedures
218        } else {
219            vec![]
220        };
221
222        // Create component and account
223        let component =
224            AuthEcdsaK256KeccakAcl::new(public_key, acl_config).expect("component creation failed");
225
226        let account = AccountBuilder::new([0; 32])
227            .with_auth_component(component)
228            .with_component(BasicWallet)
229            .build()
230            .expect("account building failed");
231
232        // Assert public key in slot 0
233        let public_key_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
234        assert_eq!(public_key_slot, public_key.into());
235
236        // Assert configuration in slot 1
237        let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed");
238        assert_eq!(slot_1, config.expected_slot_1);
239
240        // Assert procedure roots in map (slot 2)
241        if config.with_procedures {
242            for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
243                let proc_root = account
244                    .storage()
245                    .get_map_item(2, Word::from([i as u32, 0, 0, 0]))
246                    .expect("storage map access failed");
247                assert_eq!(proc_root, *expected_proc_root);
248            }
249        } else {
250            // When no procedures, the map should return empty for key [0,0,0,0]
251            let proc_root = account
252                .storage()
253                .get_map_item(2, Word::empty())
254                .expect("storage map access failed");
255            assert_eq!(proc_root, Word::empty());
256        }
257    }
258
259    /// Test ACL component with no procedures and both authorization flags set to false
260    #[test]
261    fn test_ecdsa_k256_keccak_acl_no_procedures() {
262        test_acl_component(AclTestConfig {
263            with_procedures: false,
264            allow_unauthorized_output_notes: false,
265            allow_unauthorized_input_notes: false,
266            expected_slot_1: Word::empty(), // [0, 0, 0, 0]
267        });
268    }
269
270    /// Test ACL component with two procedures and both authorization flags set to false
271    #[test]
272    fn test_ecdsa_k256_keccak_acl_with_two_procedures() {
273        test_acl_component(AclTestConfig {
274            with_procedures: true,
275            allow_unauthorized_output_notes: false,
276            allow_unauthorized_input_notes: false,
277            expected_slot_1: Word::from([2u32, 0, 0, 0]),
278        });
279    }
280
281    /// Test ACL component with no procedures and allow_unauthorized_output_notes set to true
282    #[test]
283    fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_output_notes() {
284        test_acl_component(AclTestConfig {
285            with_procedures: false,
286            allow_unauthorized_output_notes: true,
287            allow_unauthorized_input_notes: false,
288            expected_slot_1: Word::from([0u32, 1, 0, 0]),
289        });
290    }
291
292    /// Test ACL component with two procedures and allow_unauthorized_output_notes set to true
293    #[test]
294    fn test_ecdsa_k256_keccak_acl_with_procedures_and_allow_unauthorized_output_notes() {
295        test_acl_component(AclTestConfig {
296            with_procedures: true,
297            allow_unauthorized_output_notes: true,
298            allow_unauthorized_input_notes: false,
299            expected_slot_1: Word::from([2u32, 1, 0, 0]),
300        });
301    }
302
303    /// Test ACL component with no procedures and allow_unauthorized_input_notes set to true
304    #[test]
305    fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_input_notes() {
306        test_acl_component(AclTestConfig {
307            with_procedures: false,
308            allow_unauthorized_output_notes: false,
309            allow_unauthorized_input_notes: true,
310            expected_slot_1: Word::from([0u32, 0, 1, 0]),
311        });
312    }
313
314    /// Test ACL component with two procedures and both authorization flags set to true
315    #[test]
316    fn test_ecdsa_k256_keccak_acl_with_both_allow_flags() {
317        test_acl_component(AclTestConfig {
318            with_procedures: true,
319            allow_unauthorized_output_notes: true,
320            allow_unauthorized_input_notes: true,
321            expected_slot_1: Word::from([2u32, 1, 1, 0]),
322        });
323    }
324}