tempo_precompiles/account_keychain/
mod.rs

1pub mod dispatch;
2
3use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent};
4pub use tempo_contracts::precompiles::{
5    IAccountKeychain,
6    IAccountKeychain::{
7        KeyInfo, SignatureType, TokenLimit, authorizeKeyCall, getKeyCall, getRemainingLimitCall,
8        getTransactionKeyCall, revokeKeyCall, updateSpendingLimitCall,
9    },
10};
11
12use crate::{
13    ACCOUNT_KEYCHAIN_ADDRESS,
14    error::Result,
15    storage::{Mapping, PrecompileStorageProvider, Storable, double_mapping_slot},
16};
17use alloy::primitives::{Address, B256, Bytes, IntoLogData, U256};
18use revm::{
19    interpreter::instructions::utility::{IntoAddress, IntoU256},
20    state::Bytecode,
21};
22use tempo_precompiles_macros::{Storable, contract};
23
24/// Key information stored in the precompile
25///
26/// Storage layout (packed into single slot, right-aligned):
27/// - byte 0: signature_type (u8)
28/// - bytes 1-8: expiry (u64, little-endian)
29/// - byte 9: enforce_limits (bool)
30/// - byte 10: is_revoked (bool)
31#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
32pub struct AuthorizedKey {
33    /// Signature type: 0 = secp256k1, 1 = P256, 2 = WebAuthn
34    pub signature_type: u8,
35    /// Block timestamp when key expires
36    pub expiry: u64,
37    /// Whether to enforce spending limits for this key
38    pub enforce_limits: bool,
39    /// Whether this key has been revoked. Once revoked, a key cannot be re-authorized
40    /// with the same key_id. This prevents replay attacks.
41    pub is_revoked: bool,
42}
43
44impl AuthorizedKey {
45    /// Decode AuthorizedKey from a storage slot value
46    ///
47    /// This is useful for read-only contexts (like pool validation) that don't have
48    /// access to PrecompileStorageProvider but need to decode the packed struct.
49    pub fn decode_from_slot(slot_value: U256) -> Self {
50        Self::from_evm_words([slot_value]).unwrap()
51    }
52}
53
54/// Account Keychain contract for managing authorized keys
55#[contract]
56pub struct AccountKeychain {
57    // keys[account][keyId] -> AuthorizedKey
58    keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
59    // spendingLimits[(account, keyId)][token] -> amount
60    // Using a hash of account and keyId as the key to avoid triple nesting
61    spending_limits: Mapping<B256, Mapping<Address, U256>>,
62}
63
64/// Transient storage slot for the transaction key
65/// Using slot 0 since there's only one transaction key at a time
66const TRANSACTION_KEY_SLOT: U256 = U256::ZERO;
67
68/// Compute the storage slot for keys\[account\]\[key_id\]
69///
70/// This is useful for read-only contexts (like pool validation) that need to
71/// directly read the keychain state using StateProvider without going through
72/// the precompile abstraction.
73///
74/// The keys mapping is at slot 0 (first field in the contract).
75pub fn compute_keys_slot(account: Address, key_id: Address) -> U256 {
76    double_mapping_slot(account, key_id, slots::KEYS)
77}
78
79impl<'a, S: PrecompileStorageProvider> AccountKeychain<'a, S> {
80    /// Creates an instance of the precompile.
81    ///
82    /// Caution: This does not initialize the account, see [`Self::initialize`].
83    pub fn new(storage: &'a mut S) -> Self {
84        Self::_new(ACCOUNT_KEYCHAIN_ADDRESS, storage)
85    }
86
87    /// Load transaction key from transient storage
88    fn tload_transaction_key(&mut self) -> Result<Address> {
89        let value = self.storage.tload(self.address, TRANSACTION_KEY_SLOT)?;
90        Ok(value.into_address())
91    }
92
93    /// Store transaction key in transient storage
94    fn tstore_transaction_key(&mut self, key_id: Address) -> Result<()> {
95        self.storage.tstore(
96            ACCOUNT_KEYCHAIN_ADDRESS,
97            TRANSACTION_KEY_SLOT,
98            key_id.into_u256(),
99        )?;
100        Ok(())
101    }
102
103    /// Create a hash key for spending limits mapping from account and keyId
104    fn spending_limit_key(account: Address, key_id: Address) -> B256 {
105        use alloy::primitives::keccak256;
106        let mut data = [0u8; 40];
107        data[..20].copy_from_slice(account.as_slice());
108        data[20..].copy_from_slice(key_id.as_slice());
109        keccak256(data)
110    }
111
112    /// Initializes the account keychain contract.
113    pub fn initialize(&mut self) -> Result<()> {
114        self.storage.set_code(
115            ACCOUNT_KEYCHAIN_ADDRESS,
116            Bytecode::new_legacy(Bytes::from_static(&[0xef])),
117        )?;
118
119        Ok(())
120    }
121
122    /// Authorize a new key for an account
123    /// This can only be called by the account itself (using main key)
124    pub fn authorize_key(&mut self, msg_sender: Address, call: authorizeKeyCall) -> Result<()> {
125        // Check that the transaction key for this transaction is zero (main key)
126        let transaction_key = self.tload_transaction_key()?;
127
128        // If transaction_key is not zero, it means a secondary key is being used
129        if transaction_key != Address::ZERO {
130            return Err(AccountKeychainError::unauthorized_caller().into());
131        }
132
133        // Validate inputs
134        if call.keyId == Address::ZERO {
135            return Err(AccountKeychainError::zero_public_key().into());
136        }
137
138        // Check if key already exists (key exists if expiry > 0)
139        let existing_key = self.sload_keys(msg_sender, call.keyId)?;
140        if existing_key.expiry > 0 {
141            return Err(AccountKeychainError::key_already_exists().into());
142        }
143
144        // Check if this key was previously revoked - prevents replay attacks
145        if existing_key.is_revoked {
146            return Err(AccountKeychainError::key_already_revoked().into());
147        }
148
149        // Convert SignatureType enum to u8 for storage
150        let signature_type = match call.signatureType {
151            SignatureType::Secp256k1 => 0,
152            SignatureType::P256 => 1,
153            SignatureType::WebAuthn => 2,
154            _ => return Err(AccountKeychainError::invalid_signature_type().into()),
155        };
156
157        // Create and store the new key
158        let new_key = AuthorizedKey {
159            signature_type,
160            expiry: call.expiry,
161            enforce_limits: call.enforceLimits,
162            is_revoked: false,
163        };
164
165        self.sstore_keys(msg_sender, call.keyId, new_key)?;
166
167        // Set initial spending limits (only if enforce_limits is true)
168        if call.enforceLimits {
169            let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
170            for limit in call.limits {
171                self.sstore_spending_limits(limit_key, limit.token, limit.amount)?;
172            }
173        }
174
175        // Emit event
176        let mut public_key_bytes = [0u8; 32];
177        public_key_bytes[12..].copy_from_slice(call.keyId.as_slice());
178        self.storage.emit_event(
179            ACCOUNT_KEYCHAIN_ADDRESS,
180            AccountKeychainEvent::KeyAuthorized(IAccountKeychain::KeyAuthorized {
181                account: msg_sender,
182                publicKey: B256::from(public_key_bytes),
183                signatureType: signature_type,
184                expiry: call.expiry,
185            })
186            .into_log_data(),
187        )?;
188
189        Ok(())
190    }
191
192    /// Revoke an authorized key
193    ///
194    /// This marks the key as revoked by setting is_revoked to true and expiry to 0.
195    /// Once revoked, a key_id can never be re-authorized for this account, preventing
196    /// replay attacks where old KeyAuthorization signatures could be reused.
197    pub fn revoke_key(&mut self, msg_sender: Address, call: revokeKeyCall) -> Result<()> {
198        let transaction_key = self.tload_transaction_key()?;
199
200        if transaction_key != Address::ZERO {
201            return Err(AccountKeychainError::unauthorized_caller().into());
202        }
203
204        let key = self.sload_keys(msg_sender, call.keyId)?;
205
206        // Key exists if expiry > 0
207        if key.expiry == 0 {
208            return Err(AccountKeychainError::key_not_found().into());
209        }
210
211        // Mark the key as revoked - this prevents replay attacks by ensuring
212        // the same key_id can never be re-authorized for this account.
213        // We keep is_revoked=true but clear other fields.
214        let revoked_key = AuthorizedKey {
215            is_revoked: true,
216            ..Default::default()
217        };
218        self.sstore_keys(msg_sender, call.keyId, revoked_key)?;
219
220        // Note: We don't clear spending limits here - they become inaccessible
221
222        // Emit event
223        let mut public_key_bytes = [0u8; 32];
224        public_key_bytes[12..].copy_from_slice(call.keyId.as_slice());
225        self.storage.emit_event(
226            ACCOUNT_KEYCHAIN_ADDRESS,
227            AccountKeychainEvent::KeyRevoked(IAccountKeychain::KeyRevoked {
228                account: msg_sender,
229                publicKey: B256::from(public_key_bytes),
230            })
231            .into_log_data(),
232        )?;
233
234        Ok(())
235    }
236
237    /// Update spending limit for a key-token pair
238    ///
239    /// This can be used to add limits to an unlimited key (converting it to limited)
240    /// or to update existing limits.
241    pub fn update_spending_limit(
242        &mut self,
243        msg_sender: Address,
244        call: updateSpendingLimitCall,
245    ) -> Result<()> {
246        let transaction_key = self.tload_transaction_key()?;
247
248        if transaction_key != Address::ZERO {
249            return Err(AccountKeychainError::unauthorized_caller().into());
250        }
251
252        // Verify key exists, hasn't been revoked, and hasn't expired
253        let mut key = self.load_active_key(msg_sender, call.keyId)?;
254
255        let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
256        if current_timestamp >= key.expiry {
257            return Err(AccountKeychainError::key_expired().into());
258        }
259
260        // If this key had unlimited spending (enforce_limits=false), enable limits now
261        if !key.enforce_limits {
262            key.enforce_limits = true;
263            self.sstore_keys(msg_sender, call.keyId, key)?;
264        }
265
266        // Update the spending limit
267        let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
268        self.sstore_spending_limits(limit_key, call.token, call.newLimit)?;
269
270        // Emit event
271        let mut public_key_bytes = [0u8; 32];
272        public_key_bytes[12..].copy_from_slice(call.keyId.as_slice());
273        self.storage.emit_event(
274            ACCOUNT_KEYCHAIN_ADDRESS,
275            AccountKeychainEvent::SpendingLimitUpdated(IAccountKeychain::SpendingLimitUpdated {
276                account: msg_sender,
277                publicKey: B256::from(public_key_bytes),
278                token: call.token,
279                newLimit: call.newLimit,
280            })
281            .into_log_data(),
282        )?;
283
284        Ok(())
285    }
286
287    /// Get key information
288    pub fn get_key(&mut self, call: getKeyCall) -> Result<KeyInfo> {
289        let key = self.sload_keys(call.account, call.keyId)?;
290
291        // Key doesn't exist if expiry == 0, or key has been revoked
292        if key.expiry == 0 || key.is_revoked {
293            return Ok(KeyInfo {
294                signatureType: SignatureType::Secp256k1,
295                keyId: Address::ZERO,
296                expiry: 0,
297                enforceLimits: false,
298                isRevoked: key.is_revoked,
299            });
300        }
301
302        // Convert u8 signature_type to SignatureType enum
303        let signature_type = match key.signature_type {
304            0 => SignatureType::Secp256k1,
305            1 => SignatureType::P256,
306            2 => SignatureType::WebAuthn,
307            _ => SignatureType::Secp256k1, // Default fallback
308        };
309
310        Ok(KeyInfo {
311            signatureType: signature_type,
312            keyId: call.keyId,
313            expiry: key.expiry,
314            enforceLimits: key.enforce_limits,
315            isRevoked: key.is_revoked,
316        })
317    }
318
319    /// Get remaining spending limit
320    pub fn get_remaining_limit(&mut self, call: getRemainingLimitCall) -> Result<U256> {
321        let limit_key = Self::spending_limit_key(call.account, call.keyId);
322        self.sload_spending_limits(limit_key, call.token)
323    }
324
325    /// Get the transaction key used in the current transaction
326    pub fn get_transaction_key(
327        &mut self,
328        _call: getTransactionKeyCall,
329        _msg_sender: Address,
330    ) -> Result<Address> {
331        self.tload_transaction_key()
332    }
333
334    /// Internal: Set the transaction key (called during transaction validation)
335    ///
336    /// SECURITY CRITICAL: This must be called by the transaction validation logic
337    /// BEFORE the transaction is executed, to store which key authorized the transaction.
338    /// - If key_id is Address::ZERO (main key), this should store Address::ZERO
339    /// - If key_id is a specific key address, this should store that key
340    ///
341    /// This creates a secure channel between validation and the precompile to ensure
342    /// only the main key can authorize/revoke other keys.
343    /// Uses transient storage, so the key is automatically cleared after the transaction.
344    pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
345        self.tstore_transaction_key(key_id)?;
346        Ok(())
347    }
348
349    /// Load and validate a key exists and is not revoked.
350    ///
351    /// Returns the key if valid, or an error if:
352    /// - Key doesn't exist (expiry == 0)
353    /// - Key has been revoked
354    ///
355    /// Note: This does NOT check expiry against current timestamp.
356    /// Callers should check expiry separately if needed.
357    fn load_active_key(&mut self, account: Address, key_id: Address) -> Result<AuthorizedKey> {
358        let key = self.sload_keys(account, key_id)?;
359
360        if key.is_revoked {
361            return Err(AccountKeychainError::key_already_revoked().into());
362        }
363
364        if key.expiry == 0 {
365            return Err(AccountKeychainError::key_not_found().into());
366        }
367
368        Ok(key)
369    }
370
371    /// Validate keychain authorization (existence, revocation, and expiry)
372    ///
373    /// This consolidates all validation checks into one method.
374    /// Returns Ok(()) if the key is valid and authorized, Err otherwise.
375    pub fn validate_keychain_authorization(
376        &mut self,
377        account: Address,
378        key_id: Address,
379        current_timestamp: u64,
380    ) -> Result<()> {
381        let key = self.load_active_key(account, key_id)?;
382
383        if current_timestamp >= key.expiry {
384            return Err(AccountKeychainError::key_expired().into());
385        }
386
387        Ok(())
388    }
389
390    /// Internal: Verify and update spending for a token transfer
391    pub fn verify_and_update_spending(
392        &mut self,
393        account: Address,
394        key_id: Address,
395        token: Address,
396        amount: U256,
397    ) -> Result<()> {
398        // If using main key (zero address), no spending limits apply
399        if key_id == Address::ZERO {
400            return Ok(());
401        }
402
403        // Check key is valid (exists and not revoked)
404        let key = self.load_active_key(account, key_id)?;
405
406        // If enforce_limits is false, this key has unlimited spending
407        if !key.enforce_limits {
408            return Ok(());
409        }
410
411        // Check and update spending limit
412        let limit_key = Self::spending_limit_key(account, key_id);
413        let remaining = self.sload_spending_limits(limit_key, token)?;
414
415        if amount > remaining {
416            return Err(AccountKeychainError::spending_limit_exceeded().into());
417        }
418
419        // Update remaining limit
420        self.sstore_spending_limits(limit_key, token, remaining - amount)?;
421
422        Ok(())
423    }
424
425    /// Authorize a token transfer with access key spending limits
426    ///
427    /// This method checks if the transaction is using an access key, and if so,
428    /// verifies and updates the spending limits for that key.
429    /// Should be called before executing a transfer.
430    ///
431    /// # Arguments
432    /// * `account` - The account performing the transfer
433    /// * `token` - The token being transferred
434    /// * `amount` - The amount being transferred
435    ///
436    /// # Returns
437    /// Ok(()) if authorized (either using main key or access key with sufficient limits)
438    /// Err if unauthorized or spending limit exceeded
439    pub fn authorize_transfer(
440        &mut self,
441        account: Address,
442        token: Address,
443        amount: U256,
444    ) -> Result<()> {
445        // Get the transaction key for this account
446        let transaction_key = self.tload_transaction_key()?;
447
448        // If using main key (Address::ZERO), no spending limits apply
449        if transaction_key == Address::ZERO {
450            return Ok(());
451        }
452
453        // Verify and update spending limits for this access key
454        self.verify_and_update_spending(account, transaction_key, token, amount)
455    }
456
457    /// Authorize a token approval with access key spending limits
458    ///
459    /// This method checks if the transaction is using an access key, and if so,
460    /// verifies and updates the spending limits for that key.
461    /// Should be called before executing an approval.
462    ///
463    /// # Arguments
464    /// * `account` - The account performing the approval
465    /// * `token` - The token being approved
466    /// * `old_approval` - The current approval amount
467    /// * `new_approval` - The new approval amount being set
468    ///
469    /// # Returns
470    /// Ok(()) if authorized (either using main key or access key with sufficient limits)
471    /// Err if unauthorized or spending limit exceeded
472    pub fn authorize_approve(
473        &mut self,
474        account: Address,
475        token: Address,
476        old_approval: U256,
477        new_approval: U256,
478    ) -> Result<()> {
479        // Get the transaction key for this account
480        let transaction_key = self.tload_transaction_key()?;
481
482        // If using main key (Address::ZERO), no spending limits apply
483        if transaction_key == Address::ZERO {
484            return Ok(());
485        }
486
487        // Calculate the increase in approval (only deduct if increasing)
488        // If old approval is 100 and new approval is 120, deduct 20 from spending limit
489        // If old approval is 100 and new approval is 80, deduct 0 (decreasing approval is free)
490        let approval_increase = new_approval.saturating_sub(old_approval);
491
492        // Only check spending limits if there's an increase in approval
493        if approval_increase.is_zero() {
494            return Ok(());
495        }
496
497        // Verify and update spending limits for this access key
498        self.verify_and_update_spending(account, transaction_key, token, approval_increase)
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::storage::hashmap::HashMapStorageProvider;
506    use alloy::primitives::{Address, U256};
507    use tempo_contracts::precompiles::IAccountKeychain::SignatureType;
508
509    #[test]
510    fn test_transaction_key_transient_storage() {
511        let mut storage = HashMapStorageProvider::new(1); // chain_id = 1 for testing
512        let mut keychain = AccountKeychain::new(&mut storage);
513
514        // Test 1: Initially transaction key should be zero
515        let initial_key = keychain.tload_transaction_key().unwrap();
516        assert_eq!(
517            initial_key,
518            Address::ZERO,
519            "Initial transaction key should be zero"
520        );
521
522        // Test 2: Set transaction key to an access key address
523        let access_key_addr = Address::from([0x01; 20]);
524        keychain.set_transaction_key(access_key_addr).unwrap();
525
526        // Test 3: Verify it was stored
527        let loaded_key = keychain.tload_transaction_key().unwrap();
528        assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
529
530        // Test 4: Verify getTransactionKey works
531        let get_tx_key_call = getTransactionKeyCall {};
532        let result = keychain
533            .get_transaction_key(get_tx_key_call, Address::ZERO)
534            .unwrap();
535        assert_eq!(
536            result, access_key_addr,
537            "getTransactionKey should return the set key"
538        );
539
540        // Test 5: Clear transaction key
541        keychain.set_transaction_key(Address::ZERO).unwrap();
542        let cleared_key = keychain.tload_transaction_key().unwrap();
543        assert_eq!(
544            cleared_key,
545            Address::ZERO,
546            "Transaction key should be cleared"
547        );
548    }
549
550    #[test]
551    fn test_admin_operations_blocked_with_access_key() {
552        let mut storage = HashMapStorageProvider::new(1); // chain_id = 1 for testing
553        let mut keychain = AccountKeychain::new(&mut storage);
554
555        // Initialize the keychain
556        keychain.initialize().unwrap();
557
558        let msg_sender = Address::from([0x01; 20]);
559        let existing_key = Address::from([0x02; 20]);
560        let access_key = Address::from([0x03; 20]);
561        let token = Address::from([0x04; 20]);
562
563        // First, authorize a key with main key (transaction_key = 0) to set up the test
564        keychain.set_transaction_key(Address::ZERO).unwrap();
565        let setup_call = authorizeKeyCall {
566            keyId: existing_key,
567            signatureType: SignatureType::Secp256k1,
568            expiry: u64::MAX,
569            enforceLimits: true,
570            limits: vec![],
571        };
572        keychain.authorize_key(msg_sender, setup_call).unwrap();
573
574        // Now set transaction key to non-zero (simulating access key usage)
575        keychain.set_transaction_key(access_key).unwrap();
576
577        // Test 1: authorize_key should fail with access key
578        let auth_call = authorizeKeyCall {
579            keyId: Address::from([0x05; 20]),
580            signatureType: SignatureType::P256,
581            expiry: u64::MAX,
582            enforceLimits: true,
583            limits: vec![],
584        };
585        let auth_result = keychain.authorize_key(msg_sender, auth_call);
586        assert!(
587            auth_result.is_err(),
588            "authorize_key should fail when using access key"
589        );
590        assert_unauthorized_error(auth_result.unwrap_err());
591
592        // Test 2: revoke_key should fail with access key
593        let revoke_call = revokeKeyCall {
594            keyId: existing_key,
595        };
596        let revoke_result = keychain.revoke_key(msg_sender, revoke_call);
597        assert!(
598            revoke_result.is_err(),
599            "revoke_key should fail when using access key"
600        );
601        assert_unauthorized_error(revoke_result.unwrap_err());
602
603        // Test 3: update_spending_limit should fail with access key
604        let update_call = updateSpendingLimitCall {
605            keyId: existing_key,
606            token,
607            newLimit: U256::from(1000),
608        };
609        let update_result = keychain.update_spending_limit(msg_sender, update_call);
610        assert!(
611            update_result.is_err(),
612            "update_spending_limit should fail when using access key"
613        );
614        assert_unauthorized_error(update_result.unwrap_err());
615
616        // Helper function to assert unauthorized error
617        fn assert_unauthorized_error(error: crate::error::TempoPrecompileError) {
618            match error {
619                crate::error::TempoPrecompileError::AccountKeychainError(e) => {
620                    assert!(
621                        matches!(e, AccountKeychainError::UnauthorizedCaller(_)),
622                        "Expected UnauthorizedCaller error, got: {e:?}"
623                    );
624                }
625                _ => panic!("Expected AccountKeychainError, got: {error:?}"),
626            }
627        }
628    }
629
630    #[test]
631    fn test_replay_protection_revoked_key_cannot_be_reauthorized() {
632        let mut storage = HashMapStorageProvider::new(1);
633        let mut keychain = AccountKeychain::new(&mut storage);
634        keychain.initialize().unwrap();
635
636        let account = Address::from([0x01; 20]);
637        let key_id = Address::from([0x02; 20]);
638
639        // Use main key for all operations
640        keychain.set_transaction_key(Address::ZERO).unwrap();
641
642        // Step 1: Authorize a key
643        let auth_call = authorizeKeyCall {
644            keyId: key_id,
645            signatureType: SignatureType::Secp256k1,
646            expiry: u64::MAX,
647            enforceLimits: false,
648            limits: vec![],
649        };
650        keychain.authorize_key(account, auth_call.clone()).unwrap();
651
652        // Verify key exists
653        let key_info = keychain
654            .get_key(getKeyCall {
655                account,
656                keyId: key_id,
657            })
658            .unwrap();
659        assert_eq!(key_info.expiry, u64::MAX);
660        assert!(!key_info.isRevoked);
661
662        // Step 2: Revoke the key
663        let revoke_call = revokeKeyCall { keyId: key_id };
664        keychain.revoke_key(account, revoke_call).unwrap();
665
666        // Verify key is revoked
667        let key_info = keychain
668            .get_key(getKeyCall {
669                account,
670                keyId: key_id,
671            })
672            .unwrap();
673        assert_eq!(key_info.expiry, 0);
674        assert!(key_info.isRevoked);
675
676        // Step 3: Try to re-authorize the same key (replay attack)
677        // This should fail because the key was revoked
678        let replay_result = keychain.authorize_key(account, auth_call);
679        assert!(
680            replay_result.is_err(),
681            "Re-authorizing a revoked key should fail"
682        );
683
684        // Verify it's the correct error
685        match replay_result.unwrap_err() {
686            crate::error::TempoPrecompileError::AccountKeychainError(e) => {
687                assert!(
688                    matches!(e, AccountKeychainError::KeyAlreadyRevoked(_)),
689                    "Expected KeyAlreadyRevoked error, got: {e:?}"
690                );
691            }
692            e => panic!("Expected AccountKeychainError, got: {e:?}"),
693        }
694    }
695
696    #[test]
697    fn test_different_key_id_can_be_authorized_after_revocation() {
698        let mut storage = HashMapStorageProvider::new(1);
699        let mut keychain = AccountKeychain::new(&mut storage);
700        keychain.initialize().unwrap();
701
702        let account = Address::from([0x01; 20]);
703        let key_id_1 = Address::from([0x02; 20]);
704        let key_id_2 = Address::from([0x03; 20]);
705
706        // Use main key for all operations
707        keychain.set_transaction_key(Address::ZERO).unwrap();
708
709        // Authorize key 1
710        let auth_call_1 = authorizeKeyCall {
711            keyId: key_id_1,
712            signatureType: SignatureType::Secp256k1,
713            expiry: u64::MAX,
714            enforceLimits: false,
715            limits: vec![],
716        };
717        keychain.authorize_key(account, auth_call_1).unwrap();
718
719        // Revoke key 1
720        keychain
721            .revoke_key(account, revokeKeyCall { keyId: key_id_1 })
722            .unwrap();
723
724        // Authorizing a different key (key 2) should still work
725        let auth_call_2 = authorizeKeyCall {
726            keyId: key_id_2,
727            signatureType: SignatureType::P256,
728            expiry: 1000,
729            enforceLimits: true,
730            limits: vec![],
731        };
732        keychain.authorize_key(account, auth_call_2).unwrap();
733
734        // Verify key 2 is authorized
735        let key_info = keychain
736            .get_key(getKeyCall {
737                account,
738                keyId: key_id_2,
739            })
740            .unwrap();
741        assert_eq!(key_info.expiry, 1000);
742        assert!(!key_info.isRevoked);
743    }
744}