1pub mod dispatch;
10
11use __packing_authorized_key::{
12 ENFORCE_LIMITS_LOC, EXPIRY_LOC, IS_REVOKED_LOC, SIGNATURE_TYPE_LOC,
13};
14use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent};
15pub use tempo_contracts::precompiles::{
16 IAccountKeychain,
17 IAccountKeychain::{
18 KeyInfo, SignatureType, TokenLimit, authorizeKeyCall, getKeyCall, getRemainingLimitCall,
19 getTransactionKeyCall, revokeKeyCall, updateSpendingLimitCall,
20 },
21};
22
23use crate::{
24 ACCOUNT_KEYCHAIN_ADDRESS,
25 error::Result,
26 storage::{Handler, Mapping, packing::insert_into_word},
27};
28use alloy::primitives::{Address, B256, U256};
29use tempo_precompiles_macros::{Storable, contract};
30
31#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
39pub struct AuthorizedKey {
40 pub signature_type: u8,
42 pub expiry: u64,
44 pub enforce_limits: bool,
46 pub is_revoked: bool,
49}
50
51impl AuthorizedKey {
53 pub fn decode_from_slot(slot_value: U256) -> Self {
58 use crate::storage::{LayoutCtx, Storable, packing::PackedSlot};
59
60 Self::load(&PackedSlot(slot_value), U256::ZERO, LayoutCtx::FULL)
62 .expect("unable to decode AuthorizedKey from slot")
63 }
64
65 pub fn encode_to_slot(&self) -> U256 {
69 let encoded = insert_into_word(
70 U256::ZERO,
71 &self.signature_type,
72 SIGNATURE_TYPE_LOC.offset_bytes,
73 SIGNATURE_TYPE_LOC.size,
74 )
75 .expect("unable to insert 'signature_type'");
76
77 let encoded = insert_into_word(
78 encoded,
79 &self.expiry,
80 EXPIRY_LOC.offset_bytes,
81 EXPIRY_LOC.size,
82 )
83 .expect("unable to insert 'expiry'");
84
85 let encoded = insert_into_word(
86 encoded,
87 &self.enforce_limits,
88 ENFORCE_LIMITS_LOC.offset_bytes,
89 ENFORCE_LIMITS_LOC.size,
90 )
91 .expect("unable to insert 'enforce_limits'");
92
93 insert_into_word(
94 encoded,
95 &self.is_revoked,
96 IS_REVOKED_LOC.offset_bytes,
97 IS_REVOKED_LOC.size,
98 )
99 .expect("unable to insert 'is_revoked'")
100 }
101}
102
103#[contract(addr = ACCOUNT_KEYCHAIN_ADDRESS)]
108pub struct AccountKeychain {
109 keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
111 spending_limits: Mapping<B256, Mapping<Address, U256>>,
114
115 transaction_key: Address,
119 tx_origin: Address,
122}
123
124impl AccountKeychain {
125 pub fn spending_limit_key(account: Address, key_id: Address) -> B256 {
130 use alloy::primitives::keccak256;
131 let mut data = [0u8; 40];
132 data[..20].copy_from_slice(account.as_slice());
133 data[20..].copy_from_slice(key_id.as_slice());
134 keccak256(data)
135 }
136
137 pub fn initialize(&mut self) -> Result<()> {
139 self.__initialize()
140 }
141
142 pub fn authorize_key(&mut self, msg_sender: Address, call: authorizeKeyCall) -> Result<()> {
154 self.ensure_admin_caller(msg_sender)?;
155
156 if call.keyId == Address::ZERO {
158 return Err(AccountKeychainError::zero_public_key().into());
159 }
160
161 if self.storage.spec().is_t0() {
163 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
164 if call.expiry <= current_timestamp {
165 return Err(AccountKeychainError::expiry_in_past().into());
166 }
167 }
168
169 let existing_key = self.keys[msg_sender][call.keyId].read()?;
171 if existing_key.expiry > 0 {
172 return Err(AccountKeychainError::key_already_exists().into());
173 }
174
175 if existing_key.is_revoked {
177 return Err(AccountKeychainError::key_already_revoked().into());
178 }
179
180 let signature_type = match call.signatureType {
182 SignatureType::Secp256k1 => 0,
183 SignatureType::P256 => 1,
184 SignatureType::WebAuthn => 2,
185 _ => return Err(AccountKeychainError::invalid_signature_type().into()),
186 };
187
188 let new_key = AuthorizedKey {
190 signature_type,
191 expiry: call.expiry,
192 enforce_limits: call.enforceLimits,
193 is_revoked: false,
194 };
195
196 self.keys[msg_sender][call.keyId].write(new_key)?;
197
198 if call.enforceLimits {
200 let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
201 for limit in call.limits {
202 self.spending_limits[limit_key][limit.token].write(limit.amount)?;
203 }
204 }
205
206 self.emit_event(AccountKeychainEvent::KeyAuthorized(
208 IAccountKeychain::KeyAuthorized {
209 account: msg_sender,
210 publicKey: call.keyId,
211 signatureType: signature_type,
212 expiry: call.expiry,
213 },
214 ))
215 }
216
217 pub fn revoke_key(&mut self, msg_sender: Address, call: revokeKeyCall) -> Result<()> {
225 self.ensure_admin_caller(msg_sender)?;
226
227 let key = self.keys[msg_sender][call.keyId].read()?;
228
229 if key.expiry == 0 {
231 return Err(AccountKeychainError::key_not_found().into());
232 }
233
234 let revoked_key = AuthorizedKey {
238 is_revoked: true,
239 ..Default::default()
240 };
241 self.keys[msg_sender][call.keyId].write(revoked_key)?;
242
243 self.emit_event(AccountKeychainEvent::KeyRevoked(
247 IAccountKeychain::KeyRevoked {
248 account: msg_sender,
249 publicKey: call.keyId,
250 },
251 ))
252 }
253
254 pub fn update_spending_limit(
264 &mut self,
265 msg_sender: Address,
266 call: updateSpendingLimitCall,
267 ) -> Result<()> {
268 self.ensure_admin_caller(msg_sender)?;
269
270 let mut key = self.load_active_key(msg_sender, call.keyId)?;
272
273 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
274 if current_timestamp >= key.expiry {
275 return Err(AccountKeychainError::key_expired().into());
276 }
277
278 if !key.enforce_limits {
280 key.enforce_limits = true;
281 self.keys[msg_sender][call.keyId].write(key)?;
282 }
283
284 let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
286 self.spending_limits[limit_key][call.token].write(call.newLimit)?;
287
288 self.emit_event(AccountKeychainEvent::SpendingLimitUpdated(
290 IAccountKeychain::SpendingLimitUpdated {
291 account: msg_sender,
292 publicKey: call.keyId,
293 token: call.token,
294 newLimit: call.newLimit,
295 },
296 ))
297 }
298
299 pub fn get_key(&self, call: getKeyCall) -> Result<KeyInfo> {
301 let key = self.keys[call.account][call.keyId].read()?;
302
303 if key.expiry == 0 || key.is_revoked {
305 return Ok(KeyInfo {
306 signatureType: SignatureType::Secp256k1,
307 keyId: Address::ZERO,
308 expiry: 0,
309 enforceLimits: false,
310 isRevoked: key.is_revoked,
311 });
312 }
313
314 let signature_type = match key.signature_type {
316 0 => SignatureType::Secp256k1,
317 1 => SignatureType::P256,
318 2 => SignatureType::WebAuthn,
319 _ => SignatureType::Secp256k1, };
321
322 Ok(KeyInfo {
323 signatureType: signature_type,
324 keyId: call.keyId,
325 expiry: key.expiry,
326 enforceLimits: key.enforce_limits,
327 isRevoked: key.is_revoked,
328 })
329 }
330
331 pub fn get_remaining_limit(&self, call: getRemainingLimitCall) -> Result<U256> {
334 if self.storage.spec().is_t2() {
336 let key = self.keys[call.account][call.keyId].read()?;
337 if key.expiry == 0 || key.is_revoked {
338 return Ok(U256::ZERO);
339 }
340 }
341
342 let limit_key = Self::spending_limit_key(call.account, call.keyId);
343 self.spending_limits[limit_key][call.token].read()
344 }
345
346 pub fn get_transaction_key(
348 &self,
349 _call: getTransactionKeyCall,
350 _msg_sender: Address,
351 ) -> Result<Address> {
352 self.transaction_key.t_read()
353 }
354
355 pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
366 self.transaction_key.t_write(key_id)
367 }
368
369 pub fn set_tx_origin(&mut self, origin: Address) -> Result<()> {
374 self.tx_origin.t_write(origin)
375 }
376
377 fn ensure_admin_caller(&self, msg_sender: Address) -> Result<()> {
395 if !self.transaction_key.t_read()?.is_zero() {
396 return Err(AccountKeychainError::unauthorized_caller().into());
397 }
398
399 if self.storage.spec().is_t2() {
400 let tx_origin = self.tx_origin.t_read()?;
401 if tx_origin != Address::ZERO && tx_origin != msg_sender {
402 return Err(AccountKeychainError::unauthorized_caller().into());
403 }
404 }
405
406 Ok(())
407 }
408
409 fn load_active_key(&self, account: Address, key_id: Address) -> Result<AuthorizedKey> {
418 let key = self.keys[account][key_id].read()?;
419
420 if key.is_revoked {
421 return Err(AccountKeychainError::key_already_revoked().into());
422 }
423
424 if key.expiry == 0 {
425 return Err(AccountKeychainError::key_not_found().into());
426 }
427
428 Ok(key)
429 }
430
431 pub fn validate_keychain_authorization(
446 &self,
447 account: Address,
448 key_id: Address,
449 current_timestamp: u64,
450 expected_sig_type: Option<u8>,
451 ) -> Result<()> {
452 let key = self.load_active_key(account, key_id)?;
453
454 if current_timestamp >= key.expiry {
455 return Err(AccountKeychainError::key_expired().into());
456 }
457
458 if let Some(sig_type) = expected_sig_type
461 && key.signature_type != sig_type
462 {
463 return Err(AccountKeychainError::signature_type_mismatch(
464 key.signature_type,
465 sig_type,
466 )
467 .into());
468 }
469
470 Ok(())
471 }
472
473 pub fn verify_and_update_spending(
480 &mut self,
481 account: Address,
482 key_id: Address,
483 token: Address,
484 amount: U256,
485 ) -> Result<()> {
486 if key_id == Address::ZERO {
488 return Ok(());
489 }
490
491 let key = self.load_active_key(account, key_id)?;
493
494 if !key.enforce_limits {
496 return Ok(());
497 }
498
499 let limit_key = Self::spending_limit_key(account, key_id);
501 let remaining = self.spending_limits[limit_key][token].read()?;
502
503 if amount > remaining {
504 return Err(AccountKeychainError::spending_limit_exceeded().into());
505 }
506
507 self.spending_limits[limit_key][token].write(remaining - amount)
509 }
510
511 pub fn refund_spending_limit(
517 &mut self,
518 account: Address,
519 token: Address,
520 amount: U256,
521 ) -> Result<()> {
522 let transaction_key = self.transaction_key.t_read()?;
523
524 if transaction_key == Address::ZERO {
525 return Ok(());
526 }
527
528 let tx_origin = self.tx_origin.t_read()?;
529 if account != tx_origin {
530 return Ok(());
531 }
532
533 let key = match self.load_active_key(account, transaction_key) {
536 Ok(key) => key,
537 Err(_) => return Ok(()),
538 };
539
540 if !key.enforce_limits {
541 return Ok(());
542 }
543
544 let limit_key = Self::spending_limit_key(account, transaction_key);
545 let remaining = self.spending_limits[limit_key][token].read()?;
546
547 let new_remaining = remaining.saturating_add(amount);
548
549 self.spending_limits[limit_key][token].write(new_remaining)
550 }
551
552 pub fn authorize_transfer(
563 &mut self,
564 account: Address,
565 token: Address,
566 amount: U256,
567 ) -> Result<()> {
568 let transaction_key = self.transaction_key.t_read()?;
570
571 if transaction_key == Address::ZERO {
573 return Ok(());
574 }
575
576 let tx_origin = self.tx_origin.t_read()?;
578 if account != tx_origin {
579 return Ok(());
580 }
581
582 self.verify_and_update_spending(account, transaction_key, token, amount)
584 }
585
586 pub fn authorize_approve(
597 &mut self,
598 account: Address,
599 token: Address,
600 old_approval: U256,
601 new_approval: U256,
602 ) -> Result<()> {
603 let transaction_key = self.transaction_key.t_read()?;
605
606 if transaction_key == Address::ZERO {
608 return Ok(());
609 }
610
611 let tx_origin = self.tx_origin.t_read()?;
613 if account != tx_origin {
614 return Ok(());
615 }
616
617 let approval_increase = new_approval.saturating_sub(old_approval);
621
622 if approval_increase.is_zero() {
624 return Ok(());
625 }
626
627 self.verify_and_update_spending(account, transaction_key, token, approval_increase)
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635 use crate::{
636 error::TempoPrecompileError,
637 storage::{StorageCtx, hashmap::HashMapStorageProvider},
638 };
639 use alloy::primitives::{Address, U256};
640 use revm::state::Bytecode;
641 use tempo_chainspec::hardfork::TempoHardfork;
642 use tempo_contracts::precompiles::IAccountKeychain::SignatureType;
643
644 fn assert_unauthorized_error(error: TempoPrecompileError) {
646 match error {
647 TempoPrecompileError::AccountKeychainError(e) => {
648 assert!(
649 matches!(e, AccountKeychainError::UnauthorizedCaller(_)),
650 "Expected UnauthorizedCaller error, got: {e:?}"
651 );
652 }
653 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
654 }
655 }
656
657 #[test]
658 fn test_transaction_key_transient_storage() -> eyre::Result<()> {
659 let mut storage = HashMapStorageProvider::new(1);
660 let access_key_addr = Address::random();
661 StorageCtx::enter(&mut storage, || {
662 let mut keychain = AccountKeychain::new();
663
664 let initial_key = keychain.transaction_key.t_read()?;
666 assert_eq!(
667 initial_key,
668 Address::ZERO,
669 "Initial transaction key should be zero"
670 );
671
672 keychain.set_transaction_key(access_key_addr)?;
674
675 let loaded_key = keychain.transaction_key.t_read()?;
677 assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
678
679 let get_tx_key_call = getTransactionKeyCall {};
681 let result = keychain.get_transaction_key(get_tx_key_call, Address::ZERO)?;
682 assert_eq!(
683 result, access_key_addr,
684 "getTransactionKey should return the set key"
685 );
686
687 keychain.set_transaction_key(Address::ZERO)?;
689 let cleared_key = keychain.transaction_key.t_read()?;
690 assert_eq!(
691 cleared_key,
692 Address::ZERO,
693 "Transaction key should be cleared"
694 );
695
696 Ok(())
697 })
698 }
699
700 #[test]
701 fn test_admin_operations_blocked_with_access_key() -> eyre::Result<()> {
702 let mut storage = HashMapStorageProvider::new(1);
703 let msg_sender = Address::random();
704 let existing_key = Address::random();
705 let access_key = Address::random();
706 let token = Address::random();
707 let other = Address::random();
708 StorageCtx::enter(&mut storage, || {
709 let mut keychain = AccountKeychain::new();
711 keychain.initialize()?;
712
713 keychain.set_transaction_key(Address::ZERO)?;
715 let setup_call = authorizeKeyCall {
716 keyId: existing_key,
717 signatureType: SignatureType::Secp256k1,
718 expiry: u64::MAX,
719 enforceLimits: true,
720 limits: vec![],
721 };
722 keychain.authorize_key(msg_sender, setup_call)?;
723
724 keychain.set_transaction_key(access_key)?;
726
727 let auth_call = authorizeKeyCall {
729 keyId: other,
730 signatureType: SignatureType::P256,
731 expiry: u64::MAX,
732 enforceLimits: true,
733 limits: vec![],
734 };
735 let auth_result = keychain.authorize_key(msg_sender, auth_call);
736 assert!(
737 auth_result.is_err(),
738 "authorize_key should fail when using access key"
739 );
740 assert_unauthorized_error(auth_result.unwrap_err());
741
742 let revoke_call = revokeKeyCall {
744 keyId: existing_key,
745 };
746 let revoke_result = keychain.revoke_key(msg_sender, revoke_call);
747 assert!(
748 revoke_result.is_err(),
749 "revoke_key should fail when using access key"
750 );
751 assert_unauthorized_error(revoke_result.unwrap_err());
752
753 let update_call = updateSpendingLimitCall {
755 keyId: existing_key,
756 token,
757 newLimit: U256::from(1000),
758 };
759 let update_result = keychain.update_spending_limit(msg_sender, update_call);
760 assert!(
761 update_result.is_err(),
762 "update_spending_limit should fail when using access key"
763 );
764 assert_unauthorized_error(update_result.unwrap_err());
765
766 Ok(())
767 })
768 }
769
770 #[test]
771 fn test_admin_operations_require_tx_origin_on_t2() -> eyre::Result<()> {
772 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
773 let tx_origin = Address::random();
774 let delegated_sender = Address::random();
775 let existing_key = Address::random();
776 let token = Address::random();
777 let other = Address::random();
778
779 StorageCtx::enter(&mut storage, || {
780 let mut keychain = AccountKeychain::new();
781 keychain.initialize()?;
782
783 keychain
785 .storage
786 .set_code(delegated_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
787
788 keychain.set_transaction_key(Address::ZERO)?;
790 keychain.set_tx_origin(delegated_sender)?;
791 keychain.authorize_key(
792 delegated_sender,
793 authorizeKeyCall {
794 keyId: existing_key,
795 signatureType: SignatureType::Secp256k1,
796 expiry: u64::MAX,
797 enforceLimits: true,
798 limits: vec![],
799 },
800 )?;
801
802 keychain.set_tx_origin(tx_origin)?;
804
805 let auth_result = keychain.authorize_key(
806 delegated_sender,
807 authorizeKeyCall {
808 keyId: other,
809 signatureType: SignatureType::P256,
810 expiry: u64::MAX,
811 enforceLimits: true,
812 limits: vec![],
813 },
814 );
815 assert!(auth_result.is_err());
816 assert_unauthorized_error(auth_result.unwrap_err());
817
818 let revoke_result = keychain.revoke_key(
819 delegated_sender,
820 revokeKeyCall {
821 keyId: existing_key,
822 },
823 );
824 assert!(revoke_result.is_err());
825 assert_unauthorized_error(revoke_result.unwrap_err());
826
827 let update_result = keychain.update_spending_limit(
828 delegated_sender,
829 updateSpendingLimitCall {
830 keyId: existing_key,
831 token,
832 newLimit: U256::from(1000),
833 },
834 );
835 assert!(update_result.is_err());
836 assert_unauthorized_error(update_result.unwrap_err());
837
838 Ok(())
839 })
840 }
841
842 #[test]
843 fn test_admin_operations_allow_contract_origin_on_t2() -> eyre::Result<()> {
844 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
845 let contract_sender = Address::random();
846 let key_id = Address::random();
847 let token = Address::random();
848
849 StorageCtx::enter(&mut storage, || {
850 let mut keychain = AccountKeychain::new();
851 keychain.initialize()?;
852
853 keychain
854 .storage
855 .set_code(contract_sender, Bytecode::new_raw(vec![0x60, 0x00].into()))?;
856
857 keychain.set_transaction_key(Address::ZERO)?;
860 keychain.set_tx_origin(contract_sender)?;
861
862 keychain.authorize_key(
863 contract_sender,
864 authorizeKeyCall {
865 keyId: key_id,
866 signatureType: SignatureType::Secp256k1,
867 expiry: u64::MAX,
868 enforceLimits: true,
869 limits: vec![TokenLimit {
870 token,
871 amount: U256::from(100),
872 }],
873 },
874 )?;
875
876 keychain.update_spending_limit(
877 contract_sender,
878 updateSpendingLimitCall {
879 keyId: key_id,
880 token,
881 newLimit: U256::from(200),
882 },
883 )?;
884
885 assert_eq!(
886 keychain.get_remaining_limit(getRemainingLimitCall {
887 account: contract_sender,
888 keyId: key_id,
889 token,
890 })?,
891 U256::from(200)
892 );
893
894 keychain.revoke_key(contract_sender, revokeKeyCall { keyId: key_id })?;
895
896 let key_info = keychain.get_key(getKeyCall {
897 account: contract_sender,
898 keyId: key_id,
899 })?;
900 assert!(key_info.isRevoked);
901
902 Ok(())
903 })
904 }
905
906 #[test]
907 fn test_admin_operations_allow_origin_mismatch_pre_t2() -> eyre::Result<()> {
908 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
909 let msg_sender = Address::random();
910 let other_origin = Address::random();
911 let key_id = Address::random();
912 let token = Address::random();
913
914 StorageCtx::enter(&mut storage, || {
915 let mut keychain = AccountKeychain::new();
916 keychain.initialize()?;
917
918 keychain.set_transaction_key(Address::ZERO)?;
920 keychain.set_tx_origin(other_origin)?;
921
922 keychain.authorize_key(
923 msg_sender,
924 authorizeKeyCall {
925 keyId: key_id,
926 signatureType: SignatureType::Secp256k1,
927 expiry: u64::MAX,
928 enforceLimits: true,
929 limits: vec![TokenLimit {
930 token,
931 amount: U256::from(100),
932 }],
933 },
934 )?;
935
936 keychain.update_spending_limit(
937 msg_sender,
938 updateSpendingLimitCall {
939 keyId: key_id,
940 token,
941 newLimit: U256::from(200),
942 },
943 )?;
944
945 keychain.revoke_key(msg_sender, revokeKeyCall { keyId: key_id })?;
946
947 let key_info = keychain.get_key(getKeyCall {
948 account: msg_sender,
949 keyId: key_id,
950 })?;
951 assert!(key_info.isRevoked);
952
953 Ok(())
954 })
955 }
956
957 #[test]
958 fn test_admin_operations_reject_eoa_mismatch_on_t2() -> eyre::Result<()> {
959 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
960 let account = Address::random();
961 let other_origin = Address::random();
962 let key_id = Address::random();
963 let token = Address::random();
964
965 StorageCtx::enter(&mut storage, || {
966 let mut keychain = AccountKeychain::new();
967 keychain.initialize()?;
968
969 keychain.set_transaction_key(Address::ZERO)?;
971 keychain.set_tx_origin(account)?;
972 keychain.authorize_key(
973 account,
974 authorizeKeyCall {
975 keyId: key_id,
976 signatureType: SignatureType::Secp256k1,
977 expiry: u64::MAX,
978 enforceLimits: true,
979 limits: vec![TokenLimit {
980 token,
981 amount: U256::from(100),
982 }],
983 },
984 )?;
985
986 keychain.set_tx_origin(other_origin)?;
988 let result = keychain.update_spending_limit(
989 account,
990 updateSpendingLimitCall {
991 keyId: key_id,
992 token,
993 newLimit: U256::from(200),
994 },
995 );
996 assert!(result.is_err());
997 assert_unauthorized_error(result.unwrap_err());
998
999 Ok(())
1000 })
1001 }
1002
1003 #[test]
1004 fn test_replay_protection_revoked_key_cannot_be_reauthorized() -> eyre::Result<()> {
1005 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
1006 let account = Address::random();
1007 let key_id = Address::random();
1008 let token = Address::random();
1009 StorageCtx::enter(&mut storage, || {
1010 let mut keychain = AccountKeychain::new();
1011 keychain.initialize()?;
1012
1013 keychain.set_transaction_key(Address::ZERO)?;
1015 keychain.set_tx_origin(account)?;
1016
1017 let auth_call = authorizeKeyCall {
1019 keyId: key_id,
1020 signatureType: SignatureType::Secp256k1,
1021 expiry: u64::MAX,
1022 enforceLimits: true,
1023 limits: vec![TokenLimit {
1024 token,
1025 amount: U256::from(100),
1026 }],
1027 };
1028 keychain.authorize_key(account, auth_call.clone())?;
1029
1030 let key_info = keychain.get_key(getKeyCall {
1032 account,
1033 keyId: key_id,
1034 })?;
1035 assert_eq!(key_info.expiry, u64::MAX);
1036 assert!(!key_info.isRevoked);
1037 assert_eq!(
1038 keychain.get_remaining_limit(getRemainingLimitCall {
1039 account,
1040 keyId: key_id,
1041 token,
1042 })?,
1043 U256::from(100)
1044 );
1045
1046 let revoke_call = revokeKeyCall { keyId: key_id };
1048 keychain.revoke_key(account, revoke_call)?;
1049
1050 let key_info = keychain.get_key(getKeyCall {
1052 account,
1053 keyId: key_id,
1054 })?;
1055 assert_eq!(key_info.expiry, 0);
1056 assert!(key_info.isRevoked);
1057 assert_eq!(
1058 keychain.get_remaining_limit(getRemainingLimitCall {
1059 account,
1060 keyId: key_id,
1061 token,
1062 })?,
1063 U256::ZERO
1064 );
1065
1066 let replay_result = keychain.authorize_key(account, auth_call);
1069 assert!(
1070 replay_result.is_err(),
1071 "Re-authorizing a revoked key should fail"
1072 );
1073
1074 match replay_result.unwrap_err() {
1076 TempoPrecompileError::AccountKeychainError(e) => {
1077 assert!(
1078 matches!(e, AccountKeychainError::KeyAlreadyRevoked(_)),
1079 "Expected KeyAlreadyRevoked error, got: {e:?}"
1080 );
1081 }
1082 e => panic!("Expected AccountKeychainError, got: {e:?}"),
1083 }
1084 Ok(())
1085 })
1086 }
1087
1088 #[test]
1089 fn test_authorize_key_rejects_expiry_in_past() -> eyre::Result<()> {
1090 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
1092 let account = Address::random();
1093 let key_id = Address::random();
1094 StorageCtx::enter(&mut storage, || {
1095 let mut keychain = AccountKeychain::new();
1096 keychain.initialize()?;
1097
1098 keychain.set_transaction_key(Address::ZERO)?;
1100
1101 let auth_call = authorizeKeyCall {
1103 keyId: key_id,
1104 signatureType: SignatureType::Secp256k1,
1105 expiry: 0, enforceLimits: false,
1107 limits: vec![],
1108 };
1109 let result = keychain.authorize_key(account, auth_call);
1110 assert!(
1111 result.is_err(),
1112 "Authorizing with expiry in past should fail"
1113 );
1114
1115 match result.unwrap_err() {
1117 TempoPrecompileError::AccountKeychainError(e) => {
1118 assert!(
1119 matches!(e, AccountKeychainError::ExpiryInPast(_)),
1120 "Expected ExpiryInPast error, got: {e:?}"
1121 );
1122 }
1123 e => panic!("Expected AccountKeychainError, got: {e:?}"),
1124 }
1125
1126 let auth_call_past = authorizeKeyCall {
1128 keyId: key_id,
1129 signatureType: SignatureType::Secp256k1,
1130 expiry: 1, enforceLimits: false,
1132 limits: vec![],
1133 };
1134 let result_past = keychain.authorize_key(account, auth_call_past);
1135 assert!(
1136 matches!(
1137 result_past,
1138 Err(TempoPrecompileError::AccountKeychainError(
1139 AccountKeychainError::ExpiryInPast(_)
1140 ))
1141 ),
1142 "Expected ExpiryInPast error for past expiry, got: {result_past:?}"
1143 );
1144
1145 Ok(())
1146 })
1147 }
1148
1149 #[test]
1150 fn test_different_key_id_can_be_authorized_after_revocation() -> eyre::Result<()> {
1151 let mut storage = HashMapStorageProvider::new(1);
1152 let account = Address::random();
1153 let key_id_1 = Address::random();
1154 let key_id_2 = Address::random();
1155 StorageCtx::enter(&mut storage, || {
1156 let mut keychain = AccountKeychain::new();
1157 keychain.initialize()?;
1158
1159 keychain.set_transaction_key(Address::ZERO)?;
1161
1162 let auth_call_1 = authorizeKeyCall {
1164 keyId: key_id_1,
1165 signatureType: SignatureType::Secp256k1,
1166 expiry: u64::MAX,
1167 enforceLimits: false,
1168 limits: vec![],
1169 };
1170 keychain.authorize_key(account, auth_call_1)?;
1171
1172 keychain.revoke_key(account, revokeKeyCall { keyId: key_id_1 })?;
1174
1175 let auth_call_2 = authorizeKeyCall {
1177 keyId: key_id_2,
1178 signatureType: SignatureType::P256,
1179 expiry: u64::MAX,
1180 enforceLimits: true,
1181 limits: vec![],
1182 };
1183 keychain.authorize_key(account, auth_call_2)?;
1184
1185 let key_info = keychain.get_key(getKeyCall {
1187 account,
1188 keyId: key_id_2,
1189 })?;
1190 assert_eq!(key_info.expiry, u64::MAX);
1191 assert!(!key_info.isRevoked);
1192
1193 Ok(())
1194 })
1195 }
1196
1197 #[test]
1198 fn test_authorize_approve() -> eyre::Result<()> {
1199 let mut storage = HashMapStorageProvider::new(1);
1200
1201 let eoa = Address::random();
1202 let access_key = Address::random();
1203 let token = Address::random();
1204 let contract = Address::random();
1205
1206 StorageCtx::enter(&mut storage, || {
1207 let mut keychain = AccountKeychain::new();
1208 keychain.initialize()?;
1209
1210 keychain.set_transaction_key(Address::ZERO)?;
1212 keychain.set_tx_origin(eoa)?;
1213
1214 let auth_call = authorizeKeyCall {
1215 keyId: access_key,
1216 signatureType: SignatureType::Secp256k1,
1217 expiry: u64::MAX,
1218 enforceLimits: true,
1219 limits: vec![TokenLimit {
1220 token,
1221 amount: U256::from(100),
1222 }],
1223 };
1224 keychain.authorize_key(eoa, auth_call)?;
1225
1226 let initial_limit = keychain.get_remaining_limit(getRemainingLimitCall {
1227 account: eoa,
1228 keyId: access_key,
1229 token,
1230 })?;
1231 assert_eq!(initial_limit, U256::from(100));
1232
1233 keychain.set_transaction_key(access_key)?;
1235
1236 keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(30))?;
1238
1239 let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
1240 account: eoa,
1241 keyId: access_key,
1242 token,
1243 })?;
1244 assert_eq!(limit_after, U256::from(70));
1245
1246 keychain.authorize_approve(eoa, token, U256::from(30), U256::from(20))?;
1248
1249 let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
1250 account: eoa,
1251 keyId: access_key,
1252 token,
1253 })?;
1254 assert_eq!(limit_unchanged, U256::from(70));
1255
1256 keychain.authorize_approve(eoa, token, U256::from(20), U256::from(50))?;
1258
1259 let limit_after_increase = keychain.get_remaining_limit(getRemainingLimitCall {
1260 account: eoa,
1261 keyId: access_key,
1262 token,
1263 })?;
1264 assert_eq!(limit_after_increase, U256::from(40));
1265
1266 keychain.authorize_approve(contract, token, U256::ZERO, U256::from(1000))?;
1268
1269 let limit_after_contract = keychain.get_remaining_limit(getRemainingLimitCall {
1270 account: eoa,
1271 keyId: access_key,
1272 token,
1273 })?;
1274 assert_eq!(limit_after_contract, U256::from(40)); let exceed_result = keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(50));
1278 assert!(matches!(
1279 exceed_result,
1280 Err(TempoPrecompileError::AccountKeychainError(
1281 AccountKeychainError::SpendingLimitExceeded(_)
1282 ))
1283 ));
1284
1285 keychain.set_transaction_key(Address::ZERO)?;
1287 keychain.authorize_approve(eoa, token, U256::ZERO, U256::from(1000))?;
1288
1289 let limit_main_key = keychain.get_remaining_limit(getRemainingLimitCall {
1290 account: eoa,
1291 keyId: access_key,
1292 token,
1293 })?;
1294 assert_eq!(limit_main_key, U256::from(40));
1295
1296 Ok(())
1297 })
1298 }
1299
1300 #[test]
1310 fn test_spending_limits_only_apply_to_tx_origin() -> eyre::Result<()> {
1311 let mut storage = HashMapStorageProvider::new(1);
1312
1313 let eoa_alice = Address::random(); let access_key = Address::random(); let contract_address = Address::random(); let token = Address::random();
1317
1318 StorageCtx::enter(&mut storage, || {
1319 let mut keychain = AccountKeychain::new();
1320 keychain.initialize()?;
1321
1322 keychain.set_transaction_key(Address::ZERO)?; keychain.set_tx_origin(eoa_alice)?;
1325
1326 let auth_call = authorizeKeyCall {
1327 keyId: access_key,
1328 signatureType: SignatureType::Secp256k1,
1329 expiry: u64::MAX,
1330 enforceLimits: true,
1331 limits: vec![TokenLimit {
1332 token,
1333 amount: U256::from(100),
1334 }],
1335 };
1336 keychain.authorize_key(eoa_alice, auth_call)?;
1337
1338 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
1340 account: eoa_alice,
1341 keyId: access_key,
1342 token,
1343 })?;
1344 assert_eq!(
1345 limit,
1346 U256::from(100),
1347 "Initial spending limit should be 100"
1348 );
1349
1350 keychain.set_transaction_key(access_key)?;
1352 keychain.set_tx_origin(eoa_alice)?;
1353
1354 keychain.authorize_transfer(eoa_alice, token, U256::from(30))?;
1357
1358 let limit_after = keychain.get_remaining_limit(getRemainingLimitCall {
1359 account: eoa_alice,
1360 keyId: access_key,
1361 token,
1362 })?;
1363 assert_eq!(
1364 limit_after,
1365 U256::from(70),
1366 "Spending limit should be reduced to 70 after Alice's direct transfer"
1367 );
1368
1369 keychain.authorize_transfer(contract_address, token, U256::from(1000))?;
1372
1373 let limit_unchanged = keychain.get_remaining_limit(getRemainingLimitCall {
1374 account: eoa_alice,
1375 keyId: access_key,
1376 token,
1377 })?;
1378 assert_eq!(
1379 limit_unchanged,
1380 U256::from(70),
1381 "Spending limit should remain 70 - contract transfer doesn't affect Alice's limit"
1382 );
1383
1384 keychain.authorize_transfer(eoa_alice, token, U256::from(70))?;
1386
1387 let limit_depleted = keychain.get_remaining_limit(getRemainingLimitCall {
1388 account: eoa_alice,
1389 keyId: access_key,
1390 token,
1391 })?;
1392 assert_eq!(
1393 limit_depleted,
1394 U256::ZERO,
1395 "Spending limit should be depleted after Alice spends remaining 70"
1396 );
1397
1398 let exceed_result = keychain.authorize_transfer(eoa_alice, token, U256::from(1));
1400 assert!(
1401 exceed_result.is_err(),
1402 "Should fail when Alice tries to exceed spending limit"
1403 );
1404
1405 let contract_result =
1407 keychain.authorize_transfer(contract_address, token, U256::from(999999));
1408 assert!(
1409 contract_result.is_ok(),
1410 "Contract should still be able to transfer even though Alice's limit is depleted"
1411 );
1412
1413 Ok(())
1414 })
1415 }
1416
1417 #[test]
1418 fn test_authorized_key_encode_decode_roundtrip() {
1419 let original = AuthorizedKey {
1420 signature_type: 2, expiry: 1234567890, enforce_limits: true,
1423 is_revoked: false,
1424 };
1425
1426 let encoded = original.encode_to_slot();
1427 let decoded = AuthorizedKey::decode_from_slot(encoded);
1428
1429 assert_eq!(
1430 decoded, original,
1431 "encode/decode roundtrip should be lossless"
1432 );
1433
1434 let revoked = AuthorizedKey {
1436 signature_type: 0,
1437 expiry: 0,
1438 enforce_limits: false,
1439 is_revoked: true,
1440 };
1441 let encoded = revoked.encode_to_slot();
1442 let decoded = AuthorizedKey::decode_from_slot(encoded);
1443 assert_eq!(decoded, revoked);
1444 }
1445
1446 #[test]
1447 fn test_authorize_key_rejects_existing_key_boundary() -> eyre::Result<()> {
1448 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::Genesis);
1450 let account = Address::random();
1451 let key_id = Address::random();
1452 StorageCtx::enter(&mut storage, || {
1453 let mut keychain = AccountKeychain::new();
1454 keychain.initialize()?;
1455 keychain.set_transaction_key(Address::ZERO)?;
1456
1457 let auth_call = authorizeKeyCall {
1459 keyId: key_id,
1460 signatureType: SignatureType::Secp256k1,
1461 expiry: 1, enforceLimits: false,
1463 limits: vec![],
1464 };
1465 keychain.authorize_key(account, auth_call.clone())?;
1466
1467 let key_info = keychain.get_key(getKeyCall {
1469 account,
1470 keyId: key_id,
1471 })?;
1472 assert_eq!(key_info.expiry, 1, "Key should have expiry = 1");
1473
1474 let result = keychain.authorize_key(account, auth_call);
1476 assert!(result.is_err(), "Should reject when key.expiry > 0");
1477 match result.unwrap_err() {
1478 TempoPrecompileError::AccountKeychainError(e) => {
1479 assert!(
1480 matches!(e, AccountKeychainError::KeyAlreadyExists(_)),
1481 "Expected KeyAlreadyExists, got: {e:?}"
1482 );
1483 }
1484 e => panic!("Expected AccountKeychainError, got: {e:?}"),
1485 }
1486
1487 Ok(())
1488 })
1489 }
1490
1491 #[test]
1492 fn test_spending_limit_key_derivation() {
1493 let account1 = Address::repeat_byte(0x01);
1494 let account2 = Address::repeat_byte(0x02);
1495 let key_id1 = Address::repeat_byte(0xAA);
1496 let key_id2 = Address::repeat_byte(0xBB);
1497
1498 let hash1a = AccountKeychain::spending_limit_key(account1, key_id1);
1500 let hash1b = AccountKeychain::spending_limit_key(account1, key_id1);
1501 assert_eq!(hash1a, hash1b, "Same inputs must produce same hash");
1502
1503 let hash2 = AccountKeychain::spending_limit_key(account2, key_id1);
1505 assert_ne!(
1506 hash1a, hash2,
1507 "Different accounts must produce different hashes"
1508 );
1509
1510 let hash3 = AccountKeychain::spending_limit_key(account1, key_id2);
1512 assert_ne!(
1513 hash1a, hash3,
1514 "Different key_ids must produce different hashes"
1515 );
1516
1517 let hash_swapped = AccountKeychain::spending_limit_key(key_id1, account1);
1520 assert_ne!(
1521 hash1a, hash_swapped,
1522 "Swapped order must produce different hash"
1523 );
1524
1525 assert_ne!(hash1a, B256::ZERO, "Hash should not be zero");
1527 }
1528
1529 #[test]
1530 fn test_initialize_sets_up_storage_state() -> eyre::Result<()> {
1531 let mut storage = HashMapStorageProvider::new(1);
1532 StorageCtx::enter(&mut storage, || {
1533 let mut keychain = AccountKeychain::new();
1534
1535 keychain.initialize()?;
1537
1538 keychain.set_transaction_key(Address::ZERO)?;
1540
1541 let account = Address::random();
1542 let key_id = Address::random();
1543 let auth_call = authorizeKeyCall {
1544 keyId: key_id,
1545 signatureType: SignatureType::Secp256k1,
1546 expiry: u64::MAX,
1547 enforceLimits: false,
1548 limits: vec![],
1549 };
1550
1551 keychain.authorize_key(account, auth_call)?;
1553
1554 let key_info = keychain.get_key(getKeyCall {
1556 account,
1557 keyId: key_id,
1558 })?;
1559 assert_eq!(key_info.expiry, u64::MAX, "Key should be stored after init");
1560
1561 Ok(())
1562 })
1563 }
1564
1565 #[test]
1566 fn test_authorize_key_webauthn_signature_type() -> eyre::Result<()> {
1567 let mut storage = HashMapStorageProvider::new(1);
1568 let account = Address::random();
1569 let key_id = Address::random();
1570 StorageCtx::enter(&mut storage, || {
1571 let mut keychain = AccountKeychain::new();
1572 keychain.initialize()?;
1573 keychain.set_transaction_key(Address::ZERO)?;
1574
1575 let auth_call = authorizeKeyCall {
1577 keyId: key_id,
1578 signatureType: SignatureType::WebAuthn,
1579 expiry: u64::MAX,
1580 enforceLimits: false,
1581 limits: vec![],
1582 };
1583 keychain.authorize_key(account, auth_call)?;
1584
1585 let key_info = keychain.get_key(getKeyCall {
1587 account,
1588 keyId: key_id,
1589 })?;
1590 assert_eq!(
1591 key_info.signatureType,
1592 SignatureType::WebAuthn,
1593 "Signature type should be WebAuthn"
1594 );
1595
1596 let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
1598 assert!(
1599 result.is_ok(),
1600 "WebAuthn (type 2) validation should succeed"
1601 );
1602
1603 let mismatch = keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
1605 assert!(mismatch.is_err(), "Secp256k1 should not match WebAuthn key");
1606
1607 Ok(())
1608 })
1609 }
1610
1611 #[test]
1612 fn test_update_spending_limit_expiry_boundary() -> eyre::Result<()> {
1613 let mut storage = HashMapStorageProvider::new(1);
1614 let account = Address::random();
1615 let key_id = Address::random();
1616 let token = Address::random();
1617 StorageCtx::enter(&mut storage, || {
1618 let mut keychain = AccountKeychain::new();
1619 keychain.initialize()?;
1620 keychain.set_transaction_key(Address::ZERO)?;
1621
1622 let auth_call = authorizeKeyCall {
1624 keyId: key_id,
1625 signatureType: SignatureType::Secp256k1,
1626 expiry: u64::MAX,
1627 enforceLimits: true,
1628 limits: vec![TokenLimit {
1629 token,
1630 amount: U256::from(100),
1631 }],
1632 };
1633 keychain.authorize_key(account, auth_call)?;
1634
1635 let update_call = updateSpendingLimitCall {
1637 keyId: key_id,
1638 token,
1639 newLimit: U256::from(200),
1640 };
1641 let result = keychain.update_spending_limit(account, update_call);
1642 assert!(
1643 result.is_ok(),
1644 "Update should succeed when key not expired: {result:?}"
1645 );
1646
1647 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
1649 account,
1650 keyId: key_id,
1651 token,
1652 })?;
1653 assert_eq!(limit, U256::from(200), "Limit should be updated to 200");
1654
1655 Ok(())
1656 })
1657 }
1658
1659 #[test]
1660 fn test_update_spending_limit_enforce_limits_toggle() -> eyre::Result<()> {
1661 let mut storage = HashMapStorageProvider::new(1);
1662 let account = Address::random();
1663 let key_id = Address::random();
1664 let token = Address::random();
1665 StorageCtx::enter(&mut storage, || {
1666 let mut keychain = AccountKeychain::new();
1667 keychain.initialize()?;
1668 keychain.set_transaction_key(Address::ZERO)?;
1669
1670 let auth_call = authorizeKeyCall {
1672 keyId: key_id,
1673 signatureType: SignatureType::Secp256k1,
1674 expiry: u64::MAX,
1675 enforceLimits: false, limits: vec![],
1677 };
1678 keychain.authorize_key(account, auth_call)?;
1679
1680 let key_before = keychain.get_key(getKeyCall {
1682 account,
1683 keyId: key_id,
1684 })?;
1685 assert!(
1686 !key_before.enforceLimits,
1687 "Key should start with enforce_limits=false"
1688 );
1689
1690 let update_call = updateSpendingLimitCall {
1692 keyId: key_id,
1693 token,
1694 newLimit: U256::from(500),
1695 };
1696 keychain.update_spending_limit(account, update_call)?;
1697
1698 let key_after = keychain.get_key(getKeyCall {
1700 account,
1701 keyId: key_id,
1702 })?;
1703 assert!(
1704 key_after.enforceLimits,
1705 "enforce_limits should be true after update"
1706 );
1707
1708 let limit = keychain.get_remaining_limit(getRemainingLimitCall {
1710 account,
1711 keyId: key_id,
1712 token,
1713 })?;
1714 assert_eq!(limit, U256::from(500), "Spending limit should be 500");
1715
1716 Ok(())
1717 })
1718 }
1719
1720 #[test]
1721 fn test_get_key_or_logic_existence_check() -> eyre::Result<()> {
1722 let mut storage = HashMapStorageProvider::new(1);
1723 let account = Address::random();
1724 let key_id_revoked = Address::random();
1725 let key_id_valid = Address::random();
1726 let key_id_never_existed = Address::random();
1727 StorageCtx::enter(&mut storage, || {
1728 let mut keychain = AccountKeychain::new();
1729 keychain.initialize()?;
1730 keychain.set_transaction_key(Address::ZERO)?;
1731
1732 let auth_call = authorizeKeyCall {
1734 keyId: key_id_revoked,
1735 signatureType: SignatureType::P256,
1736 expiry: u64::MAX,
1737 enforceLimits: false,
1738 limits: vec![],
1739 };
1740 keychain.authorize_key(account, auth_call)?;
1741 keychain.revoke_key(
1742 account,
1743 revokeKeyCall {
1744 keyId: key_id_revoked,
1745 },
1746 )?;
1747
1748 let auth_valid = authorizeKeyCall {
1750 keyId: key_id_valid,
1751 signatureType: SignatureType::Secp256k1,
1752 expiry: u64::MAX,
1753 enforceLimits: false,
1754 limits: vec![],
1755 };
1756 keychain.authorize_key(account, auth_valid)?;
1757
1758 let revoked_info = keychain.get_key(getKeyCall {
1760 account,
1761 keyId: key_id_revoked,
1762 })?;
1763 assert_eq!(
1764 revoked_info.keyId,
1765 Address::ZERO,
1766 "Revoked key should return zero keyId"
1767 );
1768 assert!(
1769 revoked_info.isRevoked,
1770 "Revoked key should have isRevoked=true"
1771 );
1772
1773 let never_info = keychain.get_key(getKeyCall {
1775 account,
1776 keyId: key_id_never_existed,
1777 })?;
1778 assert_eq!(
1779 never_info.keyId,
1780 Address::ZERO,
1781 "Non-existent key should return zero keyId"
1782 );
1783 assert_eq!(
1784 never_info.expiry, 0,
1785 "Non-existent key should have expiry=0"
1786 );
1787
1788 let valid_info = keychain.get_key(getKeyCall {
1790 account,
1791 keyId: key_id_valid,
1792 })?;
1793 assert_eq!(
1794 valid_info.keyId, key_id_valid,
1795 "Valid key should return actual keyId"
1796 );
1797 assert_eq!(
1798 valid_info.expiry,
1799 u64::MAX,
1800 "Valid key should have correct expiry"
1801 );
1802 assert!(!valid_info.isRevoked, "Valid key should not be revoked");
1803
1804 Ok(())
1805 })
1806 }
1807
1808 #[test]
1809 fn test_get_key_signature_type_match_arms() -> eyre::Result<()> {
1810 let mut storage = HashMapStorageProvider::new(1);
1811 let account = Address::random();
1812 let key_secp = Address::random();
1813 let key_p256 = Address::random();
1814 let key_webauthn = Address::random();
1815 StorageCtx::enter(&mut storage, || {
1816 let mut keychain = AccountKeychain::new();
1817 keychain.initialize()?;
1818 keychain.set_transaction_key(Address::ZERO)?;
1819
1820 keychain.authorize_key(
1822 account,
1823 authorizeKeyCall {
1824 keyId: key_secp,
1825 signatureType: SignatureType::Secp256k1, expiry: u64::MAX,
1827 enforceLimits: false,
1828 limits: vec![],
1829 },
1830 )?;
1831
1832 keychain.authorize_key(
1833 account,
1834 authorizeKeyCall {
1835 keyId: key_p256,
1836 signatureType: SignatureType::P256, expiry: u64::MAX,
1838 enforceLimits: false,
1839 limits: vec![],
1840 },
1841 )?;
1842
1843 keychain.authorize_key(
1844 account,
1845 authorizeKeyCall {
1846 keyId: key_webauthn,
1847 signatureType: SignatureType::WebAuthn, expiry: u64::MAX,
1849 enforceLimits: false,
1850 limits: vec![],
1851 },
1852 )?;
1853
1854 let secp_info = keychain.get_key(getKeyCall {
1856 account,
1857 keyId: key_secp,
1858 })?;
1859 assert_eq!(
1860 secp_info.signatureType,
1861 SignatureType::Secp256k1,
1862 "Secp256k1 key should return Secp256k1"
1863 );
1864
1865 let p256_info = keychain.get_key(getKeyCall {
1866 account,
1867 keyId: key_p256,
1868 })?;
1869 assert_eq!(
1870 p256_info.signatureType,
1871 SignatureType::P256,
1872 "P256 key should return P256"
1873 );
1874
1875 let webauthn_info = keychain.get_key(getKeyCall {
1876 account,
1877 keyId: key_webauthn,
1878 })?;
1879 assert_eq!(
1880 webauthn_info.signatureType,
1881 SignatureType::WebAuthn,
1882 "WebAuthn key should return WebAuthn"
1883 );
1884
1885 assert_ne!(secp_info.signatureType, p256_info.signatureType);
1887 assert_ne!(secp_info.signatureType, webauthn_info.signatureType);
1888 assert_ne!(p256_info.signatureType, webauthn_info.signatureType);
1889
1890 Ok(())
1891 })
1892 }
1893
1894 #[test]
1895 fn test_validate_keychain_authorization_checks_signature_type() -> eyre::Result<()> {
1896 let mut storage = HashMapStorageProvider::new(1);
1897 let account = Address::random();
1898 let key_id = Address::random();
1899 StorageCtx::enter(&mut storage, || {
1900 let mut keychain = AccountKeychain::new();
1901 keychain.initialize()?;
1902
1903 keychain.set_transaction_key(Address::ZERO)?;
1905
1906 let auth_call = authorizeKeyCall {
1908 keyId: key_id,
1909 signatureType: SignatureType::P256,
1910 expiry: u64::MAX,
1911 enforceLimits: false,
1912 limits: vec![],
1913 };
1914 keychain.authorize_key(account, auth_call)?;
1915
1916 let result = keychain.validate_keychain_authorization(account, key_id, 0, Some(1));
1918 assert!(
1919 result.is_ok(),
1920 "Validation should succeed with matching signature type"
1921 );
1922
1923 let mismatch_result =
1925 keychain.validate_keychain_authorization(account, key_id, 0, Some(0));
1926 assert!(
1927 mismatch_result.is_err(),
1928 "Validation should fail with mismatched signature type"
1929 );
1930 match mismatch_result.unwrap_err() {
1931 TempoPrecompileError::AccountKeychainError(e) => {
1932 assert!(
1933 matches!(e, AccountKeychainError::SignatureTypeMismatch(_)),
1934 "Expected SignatureTypeMismatch error, got: {e:?}"
1935 );
1936 }
1937 e => panic!("Expected AccountKeychainError, got: {e:?}"),
1938 }
1939
1940 let webauthn_mismatch =
1942 keychain.validate_keychain_authorization(account, key_id, 0, Some(2));
1943 assert!(
1944 webauthn_mismatch.is_err(),
1945 "Validation should fail with WebAuthn when key is P256"
1946 );
1947
1948 let none_result = keychain.validate_keychain_authorization(account, key_id, 0, None);
1950 assert!(
1951 none_result.is_ok(),
1952 "Validation should succeed when signature type check is skipped (pre-T1)"
1953 );
1954
1955 Ok(())
1956 })
1957 }
1958
1959 #[test]
1960 fn test_refund_spending_limit_restores_limit() -> eyre::Result<()> {
1961 let mut storage = HashMapStorageProvider::new(1);
1962 let eoa = Address::random();
1963 let access_key = Address::random();
1964 let token = Address::random();
1965
1966 StorageCtx::enter(&mut storage, || {
1967 let mut keychain = AccountKeychain::new();
1968 keychain.initialize()?;
1969
1970 keychain.set_transaction_key(Address::ZERO)?;
1971
1972 let auth_call = authorizeKeyCall {
1973 keyId: access_key,
1974 signatureType: SignatureType::Secp256k1,
1975 expiry: u64::MAX,
1976 enforceLimits: true,
1977 limits: vec![TokenLimit {
1978 token,
1979 amount: U256::from(100),
1980 }],
1981 };
1982 keychain.authorize_key(eoa, auth_call)?;
1983
1984 keychain.set_transaction_key(access_key)?;
1985 keychain.set_tx_origin(eoa)?;
1986
1987 keychain.authorize_transfer(eoa, token, U256::from(60))?;
1988
1989 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
1990 account: eoa,
1991 keyId: access_key,
1992 token,
1993 })?;
1994 assert_eq!(remaining, U256::from(40));
1995
1996 keychain.refund_spending_limit(eoa, token, U256::from(25))?;
1997
1998 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
1999 account: eoa,
2000 keyId: access_key,
2001 token,
2002 })?;
2003 assert_eq!(after_refund, U256::from(65));
2004
2005 Ok(())
2006 })
2007 }
2008
2009 #[test]
2010 fn test_refund_spending_limit_noop_for_main_key() -> eyre::Result<()> {
2011 let mut storage = HashMapStorageProvider::new(1);
2012 let eoa = Address::random();
2013 let token = Address::random();
2014
2015 StorageCtx::enter(&mut storage, || {
2016 let mut keychain = AccountKeychain::new();
2017 keychain.initialize()?;
2018
2019 keychain.set_transaction_key(Address::ZERO)?;
2020 keychain.set_tx_origin(eoa)?;
2021
2022 let result = keychain.refund_spending_limit(eoa, token, U256::from(50));
2023 assert!(result.is_ok());
2024
2025 Ok(())
2026 })
2027 }
2028
2029 #[test]
2030 fn test_refund_spending_limit_noop_after_key_revocation() -> eyre::Result<()> {
2031 let mut storage = HashMapStorageProvider::new(1);
2032 let eoa = Address::random();
2033 let access_key = Address::random();
2034 let token = Address::random();
2035
2036 StorageCtx::enter(&mut storage, || {
2037 let mut keychain = AccountKeychain::new();
2038 keychain.initialize()?;
2039
2040 keychain.set_transaction_key(Address::ZERO)?;
2041
2042 let auth_call = authorizeKeyCall {
2043 keyId: access_key,
2044 signatureType: SignatureType::Secp256k1,
2045 expiry: u64::MAX,
2046 enforceLimits: true,
2047 limits: vec![TokenLimit {
2048 token,
2049 amount: U256::from(100),
2050 }],
2051 };
2052 keychain.authorize_key(eoa, auth_call)?;
2053
2054 keychain.set_transaction_key(access_key)?;
2055 keychain.set_tx_origin(eoa)?;
2056
2057 keychain.authorize_transfer(eoa, token, U256::from(60))?;
2058
2059 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
2060 account: eoa,
2061 keyId: access_key,
2062 token,
2063 })?;
2064 assert_eq!(remaining, U256::from(40));
2065
2066 keychain.set_transaction_key(Address::ZERO)?;
2067 keychain.revoke_key(eoa, revokeKeyCall { keyId: access_key })?;
2068
2069 keychain.set_transaction_key(access_key)?;
2070
2071 let result = keychain.refund_spending_limit(eoa, token, U256::from(25));
2072 assert!(result.is_ok());
2073
2074 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2075 account: eoa,
2076 keyId: access_key,
2077 token,
2078 })?;
2079 assert_eq!(
2080 after_refund,
2081 U256::from(40),
2082 "limit should be unchanged after revoked key refund"
2083 );
2084
2085 Ok(())
2086 })
2087 }
2088
2089 #[test]
2090 fn test_refund_spending_limit_clamped_by_saturating_add() -> eyre::Result<()> {
2091 let mut storage = HashMapStorageProvider::new(1);
2092 let eoa = Address::random();
2093 let access_key = Address::random();
2094 let token = Address::random();
2095 let original_limit = U256::from(100);
2096
2097 StorageCtx::enter(&mut storage, || {
2098 let mut keychain = AccountKeychain::new();
2099 keychain.initialize()?;
2100
2101 keychain.set_transaction_key(Address::ZERO)?;
2102
2103 let auth_call = authorizeKeyCall {
2104 keyId: access_key,
2105 signatureType: SignatureType::Secp256k1,
2106 expiry: u64::MAX,
2107 enforceLimits: true,
2108 limits: vec![TokenLimit {
2109 token,
2110 amount: original_limit,
2111 }],
2112 };
2113 keychain.authorize_key(eoa, auth_call)?;
2114
2115 keychain.set_transaction_key(access_key)?;
2116 keychain.set_tx_origin(eoa)?;
2117
2118 keychain.authorize_transfer(eoa, token, U256::from(10))?;
2119
2120 let remaining = keychain.get_remaining_limit(getRemainingLimitCall {
2121 account: eoa,
2122 keyId: access_key,
2123 token,
2124 })?;
2125 assert_eq!(remaining, U256::from(90));
2126
2127 keychain.refund_spending_limit(eoa, token, U256::from(50))?;
2128
2129 let after_refund = keychain.get_remaining_limit(getRemainingLimitCall {
2130 account: eoa,
2131 keyId: access_key,
2132 token,
2133 })?;
2134 assert_eq!(
2135 after_refund,
2136 U256::from(140),
2137 "saturating_add should allow refund beyond original limit without overflow"
2138 );
2139
2140 Ok(())
2141 })
2142 }
2143}