Skip to main content

tempo_primitives/transaction/
key_authorization.rs

1use super::SignatureType;
2use crate::transaction::PrimitiveSignature;
3use alloc::vec::Vec;
4use alloy_consensus::crypto::RecoveryError;
5use alloy_primitives::{Address, B256, U256, keccak256};
6use alloy_rlp::Encodable;
7use core::num::NonZeroU64;
8
9/// Token spending limit for access keys
10///
11/// Defines a per-token spending limit for an access key provisioned via key_authorization.
12/// This limit is enforced by the AccountKeychain precompile when the key is used.
13#[derive(Clone, Debug, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
16#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
17#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
18#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
19pub struct TokenLimit {
20    /// TIP20 token address
21    pub token: Address,
22
23    /// Maximum spending amount for this token (enforced over the key's lifetime)
24    pub limit: U256,
25
26    /// Period duration in seconds.
27    ///
28    /// `0` means one-time limit. `>0` means the limit resets periodically.
29    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity"))]
30    pub period: u64,
31}
32
33/// Per-target call scope for an access key.
34///
35/// `selector_rules` semantics:
36/// - `[]` => allow any selector for this target
37/// - `[rule1, ...]` => allow exactly the listed selector rules
38#[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
41#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
42#[cfg_attr(test, reth_codecs::add_arbitrary_tests(rlp))]
43pub struct CallScope {
44    /// Target contract address.
45    pub target: Address,
46    /// Selector rules for this target. Empty means any selector is allowed.
47    #[cfg_attr(
48        feature = "serde",
49        serde(default, skip_serializing_if = "Vec::is_empty")
50    )]
51    pub selector_rules: Vec<SelectorRule>,
52}
53
54impl CallScope {
55    /// Returns the target contract address.
56    pub fn target(&self) -> Address {
57        self.target
58    }
59
60    /// Returns `true` when any call to this target is allowed (no selector restrictions).
61    pub fn allows_all_selectors(&self) -> bool {
62        self.selector_rules.is_empty()
63    }
64
65    /// Returns the selector rules for this target.
66    pub fn selector_rules(&self) -> &[SelectorRule] {
67        &self.selector_rules
68    }
69
70    fn heap_size(&self) -> usize {
71        self.selector_rules.capacity() * size_of::<SelectorRule>()
72            + self
73                .selector_rules
74                .iter()
75                .map(SelectorRule::heap_size)
76                .sum::<usize>()
77    }
78}
79
80/// Selector-level rule within a [`CallScope`].
81///
82/// `recipients` semantics:
83/// - `[]` => no recipient constraint
84/// - `[a1, ...]` => first ABI address argument must be in this list
85#[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
88#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
89#[cfg_attr(test, reth_codecs::add_arbitrary_tests(rlp))]
90pub struct SelectorRule {
91    /// 4-byte function selector.
92    #[cfg_attr(feature = "serde", serde(with = "selector_hex_serde"))]
93    pub selector: [u8; 4],
94    /// Recipient allowlist. Empty means no recipient restriction.
95    #[cfg_attr(
96        feature = "serde",
97        serde(default, skip_serializing_if = "Vec::is_empty")
98    )]
99    pub recipients: Vec<Address>,
100}
101
102impl SelectorRule {
103    /// Returns the 4-byte function selector.
104    pub fn selector(&self) -> [u8; 4] {
105        self.selector
106    }
107
108    /// Returns the allowed recipients for this selector.
109    pub fn recipients(&self) -> &[Address] {
110        &self.recipients
111    }
112
113    /// Returns `true` when any recipient is allowed (no recipient restriction).
114    pub fn allows_all_recipients(&self) -> bool {
115        self.recipients.is_empty()
116    }
117
118    fn heap_size(&self) -> usize {
119        self.recipients.capacity() * size_of::<Address>()
120    }
121}
122
123use tempo_contracts::precompiles::IAccountKeychain::{
124    CallScope as AbiCallScope, SelectorRule as AbiSelectorRule,
125};
126
127impl From<AbiCallScope> for CallScope {
128    fn from(scope: AbiCallScope) -> Self {
129        Self {
130            target: scope.target,
131            selector_rules: scope.selectorRules.into_iter().map(Into::into).collect(),
132        }
133    }
134}
135
136impl From<CallScope> for AbiCallScope {
137    fn from(scope: CallScope) -> Self {
138        Self {
139            target: scope.target,
140            selectorRules: scope.selector_rules.into_iter().map(Into::into).collect(),
141        }
142    }
143}
144
145impl From<AbiSelectorRule> for SelectorRule {
146    fn from(rule: AbiSelectorRule) -> Self {
147        Self {
148            selector: rule.selector.into(),
149            recipients: rule.recipients,
150        }
151    }
152}
153
154impl From<SelectorRule> for AbiSelectorRule {
155    fn from(rule: SelectorRule) -> Self {
156        Self {
157            selector: rule.selector.into(),
158            recipients: rule.recipients,
159        }
160    }
161}
162
163/// Key authorization for provisioning access keys
164///
165/// Used in TempoTransaction to add a new key to the AccountKeychain precompile.
166/// The transaction must be signed by the root key to authorize adding this access key.
167///
168/// RLP encoding: `[chain_id, key_type, key_id, expiry?, limits?, allowed_calls?]`
169/// - Non-optional fields come first, followed by optional (trailing) fields
170/// - `expiry`: `None` (omitted or 0x80) = key never expires, `Some(timestamp)` = expires at timestamp
171/// - `limits`: `None` (omitted or 0x80) = unlimited spending, `Some([])` = no spending, `Some([...])` = specific limits
172/// - `allowed_calls`: `None` (canonically omitted, explicit 0x80 accepted) = unrestricted,
173///   `Some([])` = scoped with no allowed calls, `Some([...])` = scoped calls
174#[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
175#[rlp(trailing(canonical))]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
178#[cfg_attr(test, reth_codecs::add_arbitrary_tests(rlp))]
179pub struct KeyAuthorization {
180    /// Chain ID for replay protection.
181    /// Pre-T1C: 0 = valid on any chain (wildcard). T1C+: must match current chain.
182    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
183    pub chain_id: u64,
184
185    /// Type of key being authorized (Secp256k1, P256, or WebAuthn)
186    pub key_type: SignatureType,
187
188    /// Key identifier, is the address derived from the public key of the key type.
189    pub key_id: Address,
190
191    /// Unix timestamp when key expires.
192    /// - `None` (RLP 0x80) = key never expires (stored as u64::MAX in precompile)
193    /// - `Some(timestamp)` = key expires at this timestamp
194    ///
195    /// This uses `Option<NonZeroU64>` so `Some(0)` is unrepresentable and cannot silently
196    /// roundtrip into `None`.
197    #[cfg_attr(feature = "serde", serde(with = "serde_nonzero_quantity_opt"))]
198    pub expiry: Option<NonZeroU64>,
199
200    /// TIP20 spending limits for this key.
201    /// - `None` (RLP 0x80) = unlimited spending (no limits enforced)
202    /// - `Some([])` = no spending allowed (enforce_limits=true but no tokens allowed)
203    /// - `Some([TokenLimit{...}])` = specific limits enforced
204    pub limits: Option<Vec<TokenLimit>>,
205
206    /// Optional call scopes for this key.
207    /// - `None` (canonically omitted, explicit 0x80 accepted) = unrestricted calls
208    /// - `Some([])` = scoped mode with no allowed calls
209    /// - `Some([CallScope{...}])` = explicit target/selector scope list
210    pub allowed_calls: Option<Vec<CallScope>>,
211}
212
213impl KeyAuthorization {
214    /// Create a fully unrestricted key authorization: no expiry, no spending limits, no call
215    /// scopes.
216    pub fn unrestricted(chain_id: u64, key_type: SignatureType, key_id: Address) -> Self {
217        Self {
218            chain_id,
219            key_type,
220            key_id,
221            expiry: None,
222            limits: None,
223            allowed_calls: None,
224        }
225    }
226
227    /// Set an expiry timestamp on this key authorization.
228    pub fn with_expiry(mut self, expiry: u64) -> Self {
229        self.expiry = NonZeroU64::new(expiry);
230        self
231    }
232
233    /// Set token spending limits on this key authorization.
234    pub fn with_limits(mut self, limits: Vec<TokenLimit>) -> Self {
235        self.limits = Some(limits);
236        self
237    }
238
239    /// Set call-scope restrictions on this key authorization.
240    pub fn with_allowed_calls(mut self, allowed_calls: Vec<CallScope>) -> Self {
241        self.allowed_calls = Some(allowed_calls);
242        self
243    }
244
245    /// Deny all spending (enforce limits with an empty allowlist).
246    pub fn with_no_spending(mut self) -> Self {
247        self.limits = Some(Vec::new());
248        self
249    }
250
251    /// Deny all calls (scoped mode with an empty allowlist).
252    pub fn with_no_calls(mut self) -> Self {
253        self.allowed_calls = Some(Vec::new());
254        self
255    }
256
257    /// Computes the authorization message hash for this key authorization.
258    pub fn signature_hash(&self) -> B256 {
259        let mut buf = Vec::new();
260        self.encode(&mut buf);
261        keccak256(&buf)
262    }
263
264    /// Returns whether any token limit uses periodic reset semantics.
265    pub fn has_periodic_limits(&self) -> bool {
266        self.limits
267            .as_ref()
268            .is_some_and(|limits| limits.iter().any(|limit| limit.period != 0))
269    }
270
271    /// Returns whether this authorization carries explicit call-scope restrictions.
272    pub fn has_call_scopes(&self) -> bool {
273        self.allowed_calls.is_some()
274    }
275
276    /// Returns whether this key has unlimited spending (limits is None)
277    pub fn has_unlimited_spending(&self) -> bool {
278        self.limits.is_none()
279    }
280
281    /// Returns whether this key never expires (expiry is None)
282    pub fn never_expires(&self) -> bool {
283        self.expiry.is_none()
284    }
285
286    /// Returns whether this authorization can be encoded with the legacy pre-T3 ABI.
287    pub fn is_legacy_compatible(&self) -> bool {
288        !(self.has_periodic_limits() || self.has_call_scopes())
289    }
290
291    /// Convert the key authorization into a [`SignedKeyAuthorization`] with a signature.
292    pub fn into_signed(self, signature: PrimitiveSignature) -> SignedKeyAuthorization {
293        SignedKeyAuthorization {
294            authorization: self,
295            signature,
296        }
297    }
298
299    /// Validates that this key authorization's `chain_id` is compatible with `expected_chain_id`.
300    ///
301    /// - Post-T1C: `chain_id` must exactly match (wildcard `0` is no longer allowed).
302    /// - Pre-T1C: `chain_id == 0` is a wildcard (valid on any chain), otherwise must match.
303    pub fn validate_chain_id(
304        &self,
305        expected_chain_id: u64,
306        is_t1c: bool,
307    ) -> Result<(), KeyAuthorizationChainIdError> {
308        if is_t1c {
309            if self.chain_id != expected_chain_id {
310                return Err(KeyAuthorizationChainIdError {
311                    expected: expected_chain_id,
312                    got: self.chain_id,
313                });
314            }
315        } else if self.chain_id != 0 && self.chain_id != expected_chain_id {
316            return Err(KeyAuthorizationChainIdError {
317                expected: expected_chain_id,
318                got: self.chain_id,
319            });
320        }
321        Ok(())
322    }
323
324    /// Calculates a heuristic for the in-memory size of the key authorization
325    pub fn size(&self) -> usize {
326        size_of::<Self>()
327            + self
328                .limits
329                .as_ref()
330                .map_or(0, |limits| limits.capacity() * size_of::<TokenLimit>())
331            + self.allowed_calls.as_ref().map_or(0, |scopes| {
332                scopes.capacity() * size_of::<CallScope>()
333                    + scopes.iter().map(CallScope::heap_size).sum::<usize>()
334            })
335    }
336}
337
338/// Error returned when a [`KeyAuthorization`]'s `chain_id` does not match the expected value.
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub struct KeyAuthorizationChainIdError {
341    /// The expected chain ID (current chain).
342    pub expected: u64,
343    /// The chain ID from the KeyAuthorization.
344    pub got: u64,
345}
346
347/// Signed key authorization that can be attached to a transaction.
348#[derive(
349    Clone,
350    Debug,
351    PartialEq,
352    Eq,
353    Hash,
354    alloy_rlp::RlpEncodable,
355    alloy_rlp::RlpDecodable,
356    derive_more::Deref,
357)]
358#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
359#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
360#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
361#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
362pub struct SignedKeyAuthorization {
363    /// Key authorization for provisioning access keys
364    #[cfg_attr(feature = "serde", serde(flatten))]
365    #[deref]
366    pub authorization: KeyAuthorization,
367
368    /// Signature authorizing this key (signed by root key)
369    pub signature: PrimitiveSignature,
370}
371
372impl SignedKeyAuthorization {
373    /// Recover the signer of the [`KeyAuthorization`].
374    pub fn recover_signer(&self) -> Result<Address, RecoveryError> {
375        self.signature
376            .recover_signer(&self.authorization.signature_hash())
377    }
378
379    /// Calculates a heuristic for the in-memory size of the signed key authorization
380    pub fn size(&self) -> usize {
381        self.authorization.size() + self.signature.size()
382    }
383}
384
385#[cfg(any(test, feature = "arbitrary"))]
386impl<'a> arbitrary::Arbitrary<'a> for KeyAuthorization {
387    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
388        Ok(Self {
389            chain_id: u.arbitrary()?,
390            key_type: u.arbitrary()?,
391            key_id: u.arbitrary()?,
392            expiry: u.arbitrary()?,
393            limits: u.arbitrary()?,
394            allowed_calls: u.arbitrary()?,
395        })
396    }
397}
398
399#[cfg(feature = "serde")]
400#[doc(hidden)]
401pub mod serde_nonzero_quantity_opt {
402    use core::num::NonZeroU64;
403
404    use serde::{Deserializer, Serializer, de::Error as _};
405
406    pub fn serialize<S>(value: &Option<NonZeroU64>, serializer: S) -> Result<S::Ok, S::Error>
407    where
408        S: Serializer,
409    {
410        alloy_serde::quantity::opt::serialize(&value.map(NonZeroU64::get), serializer)
411    }
412
413    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NonZeroU64>, D::Error>
414    where
415        D: Deserializer<'de>,
416    {
417        alloy_serde::quantity::opt::deserialize(deserializer).and_then(|value: Option<u64>| {
418            value
419                .map(|value| {
420                    NonZeroU64::new(value)
421                        .ok_or_else(|| D::Error::custom("expected non-zero quantity"))
422                })
423                .transpose()
424        })
425    }
426}
427
428mod rlp {
429    use super::*;
430    use alloy_rlp::{Decodable, Encodable};
431
432    #[derive(
433        Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable,
434    )]
435    #[rlp(trailing(canonical))]
436    struct TokenLimitWire {
437        token: Address,
438        limit: U256,
439        period: Option<NonZeroU64>,
440    }
441
442    impl From<&TokenLimit> for TokenLimitWire {
443        fn from(value: &TokenLimit) -> Self {
444            let TokenLimit {
445                token,
446                limit,
447                period,
448            } = value;
449            Self {
450                token: *token,
451                limit: *limit,
452                period: NonZeroU64::new(*period),
453            }
454        }
455    }
456
457    impl From<TokenLimitWire> for TokenLimit {
458        fn from(value: TokenLimitWire) -> Self {
459            Self {
460                token: value.token,
461                limit: value.limit,
462                period: value.period.map(|period| period.get()).unwrap_or(0),
463            }
464        }
465    }
466
467    impl Decodable for TokenLimit {
468        fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
469            Ok(TokenLimitWire::decode(buf)?.into())
470        }
471    }
472
473    impl Encodable for TokenLimit {
474        fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
475            TokenLimitWire::from(self).encode(out)
476        }
477
478        fn length(&self) -> usize {
479            TokenLimitWire::from(self).length()
480        }
481    }
482}
483
484#[cfg(feature = "serde")]
485mod selector_hex_serde {
486    use alloy_primitives::FixedBytes;
487    use serde::{Deserialize, Deserializer, Serialize, Serializer};
488
489    #[derive(Deserialize)]
490    #[serde(untagged)]
491    enum SelectorValue {
492        Hex(FixedBytes<4>),
493        Array([u8; 4]),
494    }
495
496    pub(super) fn serialize<S>(selector: &[u8; 4], serializer: S) -> Result<S::Ok, S::Error>
497    where
498        S: Serializer,
499    {
500        FixedBytes::<4>::from(*selector).serialize(serializer)
501    }
502
503    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 4], D::Error>
504    where
505        D: Deserializer<'de>,
506    {
507        Ok(match SelectorValue::deserialize(deserializer)? {
508            SelectorValue::Hex(selector) => selector.into(),
509            SelectorValue::Array(selector) => selector,
510        })
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::transaction::{
518        TempoSignature,
519        tt_authorization::tests::{generate_secp256k1_keypair, sign_hash},
520    };
521    use alloy_rlp::{Decodable, Encodable};
522
523    fn nonzero(value: u64) -> NonZeroU64 {
524        NonZeroU64::new(value).expect("test expiry must be non-zero")
525    }
526
527    fn make_auth(expiry: Option<u64>, limits: Option<Vec<TokenLimit>>) -> KeyAuthorization {
528        KeyAuthorization {
529            chain_id: 1,
530            key_type: SignatureType::Secp256k1,
531            key_id: Address::random(),
532            expiry: expiry.and_then(NonZeroU64::new),
533            limits,
534            allowed_calls: None,
535        }
536    }
537
538    #[test]
539    fn test_signature_hash_and_recover_signer() {
540        let (signing_key, expected_address) = generate_secp256k1_keypair();
541
542        let auth = make_auth(Some(1000), None);
543
544        // Hash determinism
545        let hash1 = auth.signature_hash();
546        let hash2 = auth.signature_hash();
547        assert_eq!(hash1, hash2, "signature_hash should be deterministic");
548        assert_ne!(hash1, B256::ZERO);
549
550        // Different auth produces different hash
551        let auth2 = make_auth(Some(2000), None);
552        assert_ne!(auth.signature_hash(), auth2.signature_hash());
553
554        // Sign and recover
555        let signature = sign_hash(&signing_key, &auth.signature_hash());
556        let inner_sig = match signature {
557            TempoSignature::Primitive(p) => p,
558            _ => panic!("Expected primitive signature"),
559        };
560        let signed = auth.clone().into_signed(inner_sig);
561
562        // Recovery should succeed with correct address
563        let recovered = signed.recover_signer();
564        assert!(recovered.is_ok());
565        assert_eq!(recovered.unwrap(), expected_address);
566
567        // Wrong signature hash yields wrong address
568        let wrong_sig = sign_hash(&signing_key, &B256::random());
569        let wrong_inner = match wrong_sig {
570            TempoSignature::Primitive(p) => p,
571            _ => panic!("Expected primitive signature"),
572        };
573        let bad_signed = auth.into_signed(wrong_inner);
574        let bad_recovered = bad_signed.recover_signer();
575        assert!(bad_recovered.is_ok());
576        assert_ne!(bad_recovered.unwrap(), expected_address);
577    }
578
579    #[test]
580    fn test_spending_expiry_and_size() {
581        // has_unlimited_spending: None = true, Some = false
582        assert!(make_auth(None, None).has_unlimited_spending());
583        assert!(!make_auth(None, Some(vec![])).has_unlimited_spending());
584        assert!(
585            !make_auth(
586                None,
587                Some(vec![TokenLimit {
588                    token: Address::ZERO,
589                    limit: U256::from(100),
590                    period: 0,
591                }])
592            )
593            .has_unlimited_spending()
594        );
595
596        // never_expires: None = true, Some = false
597        assert!(make_auth(None, None).never_expires());
598        assert!(!make_auth(Some(1000), None).never_expires());
599        assert_eq!(NonZeroU64::new(0), None);
600    }
601
602    #[test]
603    fn test_size_does_not_double_count_call_scope_structs() {
604        let recipients = vec![Address::repeat_byte(0x11), Address::repeat_byte(0x22)];
605        let mut rules = Vec::with_capacity(3);
606        rules.push(SelectorRule {
607            selector: [1, 2, 3, 4],
608            recipients,
609        });
610
611        let mut scopes = Vec::with_capacity(2);
612        scopes.push(CallScope {
613            target: Address::repeat_byte(0x33),
614            selector_rules: rules,
615        });
616
617        let auth =
618            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::repeat_byte(0x44))
619                .with_allowed_calls(scopes);
620
621        let scope_rules = auth.allowed_calls.as_ref().unwrap();
622        let selector_rules = &scope_rules[0].selector_rules;
623        let recipients = &selector_rules[0].recipients;
624
625        let expected = size_of::<KeyAuthorization>()
626            + scope_rules.capacity() * size_of::<CallScope>()
627            + selector_rules.capacity() * size_of::<SelectorRule>()
628            + recipients.capacity() * size_of::<Address>();
629
630        assert_eq!(auth.size(), expected);
631    }
632
633    #[test]
634    fn test_zero_expiry_is_unrepresentable() {
635        assert_eq!(NonZeroU64::new(0), None);
636        assert_eq!(Some(NonZeroU64::get(nonzero(1))), Some(1));
637    }
638
639    fn make_auth_with_chain_id(chain_id: u64) -> KeyAuthorization {
640        KeyAuthorization {
641            chain_id,
642            key_type: SignatureType::Secp256k1,
643            key_id: Address::random(),
644            expiry: None,
645            limits: None,
646            allowed_calls: None,
647        }
648    }
649
650    #[test]
651    fn test_token_limit_legacy_decode_defaults_period_to_zero() {
652        let token = Address::random();
653        let limit = U256::from(42);
654
655        // Legacy pre-T3 payloads encode TokenLimit as [token, limit].
656        let mut encoded = Vec::new();
657        alloy_rlp::Header {
658            list: true,
659            payload_length: token.length() + limit.length(),
660        }
661        .encode(&mut encoded);
662        token.encode(&mut encoded);
663        limit.encode(&mut encoded);
664
665        let decoded: TokenLimit =
666            Decodable::decode(&mut encoded.as_slice()).expect("decode legacy token limit");
667        assert_eq!(decoded.token, token);
668        assert_eq!(decoded.limit, limit);
669        assert_eq!(decoded.period, 0);
670    }
671
672    #[test]
673    fn test_token_limit_encoding_omits_zero_period() {
674        let token_limit = TokenLimit {
675            token: Address::random(),
676            limit: U256::from(1234),
677            period: 0,
678        };
679
680        let mut encoded = Vec::new();
681        token_limit.encode(&mut encoded);
682
683        let mut payload = &encoded[..];
684        let header = alloy_rlp::Header::decode(&mut payload).expect("decode list header");
685        assert!(header.list);
686        assert_eq!(
687            header.payload_length,
688            token_limit.token.length() + token_limit.limit.length()
689        );
690    }
691
692    #[test]
693    fn test_token_limit_decode_accepts_explicit_zero_period_field() {
694        let token = Address::random();
695        let limit = U256::from(42);
696
697        let mut encoded = Vec::new();
698        alloy_rlp::Header {
699            list: true,
700            payload_length: token.length() + limit.length(),
701        }
702        .encode(&mut encoded);
703        token.encode(&mut encoded);
704        limit.encode(&mut encoded);
705
706        let decoded: TokenLimit =
707            <TokenLimit as Decodable>::decode(&mut encoded.as_slice()).expect("decode token limit");
708        assert_eq!(decoded.token, token);
709        assert_eq!(decoded.limit, limit);
710        assert_eq!(decoded.period, 0);
711    }
712
713    #[test]
714    fn test_key_authorization_roundtrip_preserves_explicit_nested_allow_all_lists() {
715        let auth =
716            KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::repeat_byte(0x11))
717                .with_allowed_calls(vec![
718                    CallScope {
719                        target: Address::repeat_byte(0x22),
720                        selector_rules: vec![],
721                    },
722                    CallScope {
723                        target: Address::repeat_byte(0x33),
724                        selector_rules: vec![SelectorRule {
725                            selector: [0xaa, 0xbb, 0xcc, 0xdd],
726                            recipients: vec![],
727                        }],
728                    },
729                ]);
730
731        let mut encoded = Vec::new();
732        auth.encode(&mut encoded);
733
734        let decoded =
735            <KeyAuthorization as Decodable>::decode(&mut encoded.as_slice()).expect("decode auth");
736
737        let mut reencoded = Vec::new();
738        decoded.encode(&mut reencoded);
739
740        assert_eq!(reencoded, encoded);
741    }
742
743    #[test]
744    fn test_call_scope_decode_rejects_omitted_selector_rules() {
745        let target = Address::repeat_byte(0x11);
746
747        let mut encoded = Vec::new();
748        alloy_rlp::Header {
749            list: true,
750            payload_length: target.length(),
751        }
752        .encode(&mut encoded);
753        target.encode(&mut encoded);
754
755        <CallScope as Decodable>::decode(&mut encoded.as_slice())
756            .expect_err("omitted selector_rules should be rejected");
757    }
758
759    #[test]
760    fn test_call_scope_explicit_empty_selector_rules_roundtrip() {
761        let scope = CallScope {
762            target: Address::repeat_byte(0x11),
763            selector_rules: Vec::new(),
764        };
765
766        let mut encoded = Vec::new();
767        scope.encode(&mut encoded);
768
769        let mut payload = &encoded[..];
770        let header = alloy_rlp::Header::decode(&mut payload).expect("decode list header");
771        assert!(header.list);
772        assert_eq!(
773            header.payload_length,
774            scope.target.length() + Vec::<SelectorRule>::new().length()
775        );
776
777        let decoded =
778            <CallScope as Decodable>::decode(&mut encoded.as_slice()).expect("decode scope");
779        assert_eq!(decoded, scope);
780    }
781
782    #[test]
783    fn test_call_scope_decode_accepts_explicit_empty_selector_rules_list() {
784        let target = Address::repeat_byte(0x11);
785
786        let mut encoded = Vec::new();
787        alloy_rlp::Header {
788            list: true,
789            payload_length: target.length() + Vec::<SelectorRule>::new().length(),
790        }
791        .encode(&mut encoded);
792        target.encode(&mut encoded);
793        Vec::<SelectorRule>::new().encode(&mut encoded);
794
795        let decoded =
796            <CallScope as Decodable>::decode(&mut encoded.as_slice()).expect("decode scope");
797        assert_eq!(decoded.target, target);
798        assert!(decoded.selector_rules.is_empty());
799
800        let mut reencoded = Vec::new();
801        decoded.encode(&mut reencoded);
802        assert_eq!(reencoded, encoded);
803    }
804
805    #[test]
806    fn test_selector_rule_decode_rejects_omitted_recipients() {
807        let selector = [0xaa, 0xbb, 0xcc, 0xdd];
808
809        let mut encoded = Vec::new();
810        alloy_rlp::Header {
811            list: true,
812            payload_length: selector.length(),
813        }
814        .encode(&mut encoded);
815        selector.encode(&mut encoded);
816
817        <SelectorRule as Decodable>::decode(&mut encoded.as_slice())
818            .expect_err("omitted recipients should be rejected");
819    }
820
821    #[test]
822    fn test_selector_rule_empty_recipients_roundtrip() {
823        let rule = SelectorRule {
824            selector: [0xaa, 0xbb, 0xcc, 0xdd],
825            recipients: Vec::new(),
826        };
827
828        let mut encoded = Vec::new();
829        rule.encode(&mut encoded);
830
831        let mut payload = &encoded[..];
832        let header = alloy_rlp::Header::decode(&mut payload).expect("decode list header");
833        assert!(header.list);
834        assert_eq!(
835            header.payload_length,
836            rule.selector.length() + Vec::<Address>::new().length()
837        );
838
839        let decoded =
840            <SelectorRule as Decodable>::decode(&mut encoded.as_slice()).expect("decode rule");
841        assert_eq!(decoded, rule);
842    }
843
844    #[test]
845    fn test_selector_rule_decode_accepts_explicit_empty_recipient_list() {
846        let selector = [0xaa, 0xbb, 0xcc, 0xdd];
847
848        let mut encoded = Vec::new();
849        alloy_rlp::Header {
850            list: true,
851            payload_length: selector.length() + Vec::<Address>::new().length(),
852        }
853        .encode(&mut encoded);
854        selector.encode(&mut encoded);
855        Vec::<Address>::new().encode(&mut encoded);
856
857        let decoded =
858            <SelectorRule as Decodable>::decode(&mut encoded.as_slice()).expect("decode rule");
859        assert_eq!(decoded.selector, selector);
860        assert!(decoded.recipients.is_empty());
861
862        let mut reencoded = Vec::new();
863        decoded.encode(&mut reencoded);
864        assert_eq!(reencoded, encoded);
865    }
866
867    #[test]
868    fn test_selector_rule_roundtrip_preserves_non_empty_recipient_list() {
869        let rule = SelectorRule {
870            selector: [0xaa, 0xbb, 0xcc, 0xdd],
871            recipients: vec![Address::repeat_byte(0x11), Address::repeat_byte(0x22)],
872        };
873
874        let mut encoded = Vec::new();
875        rule.encode(&mut encoded);
876
877        let decoded =
878            <SelectorRule as Decodable>::decode(&mut encoded.as_slice()).expect("decode rule");
879        assert_eq!(decoded, rule);
880    }
881
882    #[cfg(feature = "serde")]
883    #[test]
884    fn test_token_limit_json_defaults_period_to_zero() {
885        let token = Address::repeat_byte(0x11);
886
887        let decoded: TokenLimit = serde_json::from_value(serde_json::json!({
888            "token": token,
889            "limit": "0x2a",
890        }))
891        .expect("deserialize legacy JSON token limit");
892
893        assert_eq!(decoded.token, token);
894        assert_eq!(decoded.limit, U256::from(42));
895        assert_eq!(decoded.period, 0);
896    }
897
898    #[cfg(feature = "serde")]
899    #[test]
900    fn test_token_limit_json_serializes_period_as_quantity() {
901        let value = serde_json::to_value(TokenLimit {
902            token: Address::repeat_byte(0x11),
903            limit: U256::from(42),
904            period: 7,
905        })
906        .expect("serialize token limit");
907
908        assert_eq!(value["period"], serde_json::json!("0x7"));
909    }
910
911    #[cfg(feature = "serde")]
912    #[test]
913    fn test_selector_rule_json_accepts_hex_selector() {
914        let recipient = Address::repeat_byte(0x11);
915
916        let decoded: SelectorRule = serde_json::from_value(serde_json::json!({
917            "selector": "0xaabbccdd",
918            "recipients": [recipient],
919        }))
920        .expect("deserialize selector rule with hex selector");
921
922        assert_eq!(decoded.selector, [0xaa, 0xbb, 0xcc, 0xdd]);
923        assert_eq!(decoded.recipients, vec![recipient]);
924    }
925
926    #[cfg(feature = "serde")]
927    #[test]
928    fn test_selector_rule_json_accepts_legacy_selector_array() {
929        let decoded: SelectorRule = serde_json::from_value(serde_json::json!({
930            "selector": [170, 187, 204, 221],
931            "recipients": [],
932        }))
933        .expect("deserialize selector rule with legacy selector array");
934
935        assert_eq!(decoded.selector, [0xaa, 0xbb, 0xcc, 0xdd]);
936        assert!(decoded.recipients.is_empty());
937    }
938
939    #[cfg(feature = "serde")]
940    #[test]
941    fn test_selector_rule_json_serializes_selector_as_hex() {
942        let value = serde_json::to_value(SelectorRule {
943            selector: [0xaa, 0xbb, 0xcc, 0xdd],
944            recipients: Vec::new(),
945        })
946        .expect("serialize selector rule");
947
948        assert_eq!(value["selector"], serde_json::json!("0xaabbccdd"));
949    }
950
951    #[cfg(feature = "serde")]
952    #[test]
953    fn test_key_authorization_json_rejects_zero_expiry() {
954        let err = serde_json::from_value::<KeyAuthorization>(serde_json::json!({
955            "chainId": "0x1",
956            "keyType": "secp256k1",
957            "keyId": Address::repeat_byte(0x11),
958            "expiry": "0x0",
959        }))
960        .expect_err("zero expiry must be rejected");
961
962        assert!(err.to_string().contains("expected non-zero quantity"));
963    }
964
965    #[test]
966    fn test_key_authorization_decode_accepts_explicit_unrestricted_allowed_calls_field() {
967        let chain_id = 1u64;
968        let key_type = SignatureType::Secp256k1;
969        let key_id = Address::random();
970
971        let mut payload = Vec::new();
972        chain_id.encode(&mut payload);
973        key_type.encode(&mut payload);
974        key_id.encode(&mut payload);
975
976        let mut encoded = Vec::new();
977        alloy_rlp::Header {
978            list: true,
979            payload_length: payload.len(),
980        }
981        .encode(&mut encoded);
982        encoded.extend_from_slice(&payload);
983
984        let decoded =
985            <KeyAuthorization as Decodable>::decode(&mut encoded.as_slice()).expect("decode auth");
986        assert_eq!(decoded.chain_id, chain_id);
987        assert_eq!(decoded.key_type, key_type);
988        assert_eq!(decoded.key_id, key_id);
989        assert_eq!(decoded.expiry, None);
990        assert_eq!(decoded.limits, None);
991        assert_eq!(decoded.allowed_calls, None);
992
993        let mut reencoded = Vec::new();
994        decoded.encode(&mut reencoded);
995        assert_eq!(reencoded.len(), encoded.len());
996    }
997
998    #[test]
999    fn test_key_authorization_decode_accepts_explicit_deny_all_allowed_calls_field() {
1000        let chain_id = 1u64;
1001        let key_type = SignatureType::Secp256k1;
1002        let key_id = Address::random();
1003
1004        let mut payload = Vec::new();
1005        chain_id.encode(&mut payload);
1006        key_type.encode(&mut payload);
1007        key_id.encode(&mut payload);
1008        payload.extend_from_slice(&[
1009            alloy_rlp::EMPTY_STRING_CODE,
1010            alloy_rlp::EMPTY_STRING_CODE,
1011            0xc0,
1012        ]);
1013
1014        let mut encoded = Vec::new();
1015        alloy_rlp::Header {
1016            list: true,
1017            payload_length: payload.len(),
1018        }
1019        .encode(&mut encoded);
1020        encoded.extend_from_slice(&payload);
1021
1022        let decoded =
1023            <KeyAuthorization as Decodable>::decode(&mut encoded.as_slice()).expect("decode auth");
1024        assert_eq!(decoded.chain_id, chain_id);
1025        assert_eq!(decoded.key_type, key_type);
1026        assert_eq!(decoded.key_id, key_id);
1027        assert_eq!(decoded.expiry, None);
1028        assert_eq!(decoded.limits, None);
1029        assert_eq!(decoded.allowed_calls, Some(vec![]));
1030
1031        let mut reencoded = Vec::new();
1032        decoded.encode(&mut reencoded);
1033        assert_eq!(reencoded, encoded);
1034    }
1035
1036    #[test]
1037    fn test_validate_chain_id_pre_t1c() {
1038        let expected = 42431;
1039
1040        // Matching chain_id → ok
1041        assert!(
1042            make_auth_with_chain_id(expected)
1043                .validate_chain_id(expected, false)
1044                .is_ok()
1045        );
1046
1047        // Wildcard chain_id=0 → ok pre-T1C
1048        assert!(
1049            make_auth_with_chain_id(0)
1050                .validate_chain_id(expected, false)
1051                .is_ok()
1052        );
1053
1054        // Wrong chain_id → err
1055        let err = make_auth_with_chain_id(999)
1056            .validate_chain_id(expected, false)
1057            .unwrap_err();
1058        assert_eq!(err.expected, expected);
1059        assert_eq!(err.got, 999);
1060    }
1061
1062    #[test]
1063    fn test_validate_chain_id_post_t1c() {
1064        let expected = 42431;
1065
1066        // Matching chain_id → ok
1067        assert!(
1068            make_auth_with_chain_id(expected)
1069                .validate_chain_id(expected, true)
1070                .is_ok()
1071        );
1072
1073        // Wildcard chain_id=0 → rejected post-T1C
1074        let err = make_auth_with_chain_id(0)
1075            .validate_chain_id(expected, true)
1076            .unwrap_err();
1077        assert_eq!(err.expected, expected);
1078        assert_eq!(err.got, 0);
1079
1080        // Wrong chain_id → rejected
1081        let err = make_auth_with_chain_id(999)
1082            .validate_chain_id(expected, true)
1083            .unwrap_err();
1084        assert_eq!(err.expected, expected);
1085        assert_eq!(err.got, 999);
1086    }
1087
1088    #[test]
1089    fn test_call_scope_accessors() {
1090        let target = Address::repeat_byte(0x11);
1091        let rule = SelectorRule {
1092            selector: [0xaa, 0xbb, 0xcc, 0xdd],
1093            recipients: vec![Address::repeat_byte(0x22)],
1094        };
1095        let scope = CallScope {
1096            target,
1097            selector_rules: vec![rule],
1098        };
1099
1100        assert_eq!(scope.target(), target);
1101        assert!(!scope.allows_all_selectors());
1102        assert_eq!(scope.selector_rules().len(), 1);
1103    }
1104
1105    #[test]
1106    fn test_call_scope_allows_all_selectors_when_empty() {
1107        let scope = CallScope {
1108            target: Address::repeat_byte(0x11),
1109            selector_rules: vec![],
1110        };
1111        assert!(scope.allows_all_selectors());
1112    }
1113
1114    #[test]
1115    fn test_selector_rule_accessors() {
1116        let selector = [0x12, 0x34, 0x56, 0x78];
1117        let recipients = vec![Address::repeat_byte(0x33), Address::repeat_byte(0x44)];
1118        let rule = SelectorRule {
1119            selector,
1120            recipients: recipients.clone(),
1121        };
1122
1123        assert_eq!(rule.selector(), selector);
1124        assert_eq!(rule.recipients(), &recipients);
1125        assert!(!rule.allows_all_recipients());
1126    }
1127
1128    #[test]
1129    fn test_selector_rule_allows_all_recipients_when_empty() {
1130        let rule = SelectorRule {
1131            selector: [0xaa, 0xbb, 0xcc, 0xdd],
1132            recipients: vec![],
1133        };
1134        assert!(rule.allows_all_recipients());
1135    }
1136}
1137
1138#[cfg(all(test, feature = "reth-codec"))]
1139mod compact_tests {
1140    use super::*;
1141    use alloy_primitives::{address, hex};
1142    use reth_codecs::Compact;
1143
1144    /// Ensures backwards compatibility of compact bitflags.
1145    ///
1146    /// See reth's `HeaderExt` pattern:
1147    /// <https://github.com/paradigmxyz/reth-core/blob/0476d1bc4b71f3c3b080622be297edd91ee4e70c/crates/codecs/src/alloy/header.rs>
1148    #[test]
1149    fn compact_types_have_unused_bits() {
1150        assert_ne!(TokenLimit::bitflag_unused_bits(), 0, "TokenLimit");
1151    }
1152
1153    #[test]
1154    fn token_limit_compact_roundtrip() {
1155        let token_limit = TokenLimit {
1156            token: address!("0x0000000000000000000000000000000000000042"),
1157            limit: U256::from(1_000_000u64),
1158            period: 86400,
1159        };
1160
1161        let expected = hex!("c30000000000000000000000000000000000000000420f4240015180");
1162
1163        let mut buf = vec![];
1164        let len = token_limit.to_compact(&mut buf);
1165        assert_eq!(buf, expected, "TokenLimit compact encoding changed");
1166        assert_eq!(len, expected.len());
1167
1168        let (decoded, _) = TokenLimit::from_compact(&expected, expected.len());
1169        assert_eq!(decoded, token_limit);
1170    }
1171}