tempo_primitives/transaction/
key_authorization.rs

1use super::SignatureType;
2use crate::transaction::PrimitiveSignature;
3use alloy_consensus::crypto::RecoveryError;
4use alloy_primitives::{Address, B256, U256, keccak256};
5use alloy_rlp::Encodable;
6use core::mem;
7use reth_primitives_traits::InMemorySize;
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, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
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
27/// Key authorization for provisioning access keys
28///
29/// Used in TempoTransaction to add a new key to the AccountKeychain precompile.
30/// The transaction must be signed by the root key to authorize adding this access key.
31///
32/// RLP encoding: `[key_type, key_id, expiry?, limits?]`
33/// - Non-optional fields come first, followed by optional (trailing) fields
34/// - `expiry`: `None` (omitted or 0x80) = key never expires, `Some(timestamp)` = expires at timestamp
35/// - `limits`: `None` (omitted or 0x80) = unlimited spending, `Some([])` = no spending, `Some([...])` = specific limits
36#[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
37#[rlp(trailing)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
40#[cfg_attr(test, reth_codecs::add_arbitrary_tests(rlp))]
41pub struct KeyAuthorization {
42    /// Chain ID for replay protection (0 = valid on any chain)
43    pub chain_id: u64,
44
45    /// Type of key being authorized (Secp256k1, P256, or WebAuthn)
46    pub key_type: SignatureType,
47
48    /// Key identifier, is the address derived from the public key of the key type.
49    pub key_id: Address,
50
51    /// Unix timestamp when key expires.
52    /// - `None` (RLP 0x80) = key never expires (stored as u64::MAX in precompile)
53    /// - `Some(timestamp)` = key expires at this timestamp
54    pub expiry: Option<u64>,
55
56    /// TIP20 spending limits for this key.
57    /// - `None` (RLP 0x80) = unlimited spending (no limits enforced)
58    /// - `Some([])` = no spending allowed (enforce_limits=true but no tokens allowed)
59    /// - `Some([TokenLimit{...}])` = specific limits enforced
60    pub limits: Option<Vec<TokenLimit>>,
61}
62
63impl KeyAuthorization {
64    /// Computes the authorization message hash for this key authorization.
65    pub fn signature_hash(&self) -> B256 {
66        let mut buf = Vec::new();
67        self.encode(&mut buf);
68        keccak256(&buf)
69    }
70
71    /// Returns whether this key has unlimited spending (limits is None)
72    pub fn has_unlimited_spending(&self) -> bool {
73        self.limits.is_none()
74    }
75
76    /// Returns whether this key never expires (expiry is None)
77    pub fn never_expires(&self) -> bool {
78        self.expiry.is_none()
79    }
80
81    /// Convert the key authorization into a [`SignedKeyAuthorization`] with a signature.
82    pub fn into_signed(self, signature: PrimitiveSignature) -> SignedKeyAuthorization {
83        SignedKeyAuthorization {
84            authorization: self,
85            signature,
86        }
87    }
88}
89
90impl InMemorySize for KeyAuthorization {
91    fn size(&self) -> usize {
92        mem::size_of::<u64>() + // chain_id
93        mem::size_of::<u8>() + // key_type
94        mem::size_of::<Address>() + // key_id
95        mem::size_of::<Option<u64>>() + // expiry
96        self.limits.as_ref().map_or(0, |limits| {
97            limits.iter().map(|_limit| {
98                mem::size_of::<Address>() + mem::size_of::<U256>()
99            }).sum::<usize>()
100        })
101    }
102}
103
104/// Signed key authorization that can be attached to a transaction.
105#[derive(
106    Clone,
107    Debug,
108    PartialEq,
109    Eq,
110    Hash,
111    alloy_rlp::RlpEncodable,
112    alloy_rlp::RlpDecodable,
113    derive_more::Deref,
114)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
117#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
118#[rlp(trailing)]
119#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
120pub struct SignedKeyAuthorization {
121    /// Key authorization for provisioning access keys
122    #[cfg_attr(feature = "serde", serde(flatten))]
123    #[deref]
124    pub authorization: KeyAuthorization,
125
126    /// Signature authorizing this key (signed by root key)
127    pub signature: PrimitiveSignature,
128}
129
130impl SignedKeyAuthorization {
131    /// Recover the signer of the [`KeyAuthorization`].
132    pub fn recover_signer(&self) -> Result<Address, RecoveryError> {
133        self.signature
134            .recover_signer(&self.authorization.signature_hash())
135    }
136}
137
138#[cfg(feature = "reth-codec")]
139impl reth_codecs::Compact for SignedKeyAuthorization {
140    fn to_compact<B>(&self, buf: &mut B) -> usize
141    where
142        B: alloy_rlp::BufMut + AsMut<[u8]>,
143    {
144        // Use RLP encoding for compact representation
145        self.encode(buf);
146        self.length()
147    }
148
149    fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) {
150        let item = alloy_rlp::Decodable::decode(&mut buf)
151            .expect("Failed to decode KeyAuthorization from compact");
152        (item, buf)
153    }
154}
155
156impl reth_primitives_traits::InMemorySize for SignedKeyAuthorization {
157    fn size(&self) -> usize {
158        self.authorization.size() + self.signature.size()
159    }
160}
161
162#[cfg(any(test, feature = "arbitrary"))]
163impl<'a> arbitrary::Arbitrary<'a> for KeyAuthorization {
164    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
165        Ok(Self {
166            chain_id: u.arbitrary()?,
167            key_type: u.arbitrary()?,
168            key_id: u.arbitrary()?,
169            // Ensure that Some(0) is not generated as it's becoming `None` after RLP roundtrip.
170            expiry: u.arbitrary::<Option<u64>>()?.filter(|v| *v != 0),
171            limits: u.arbitrary()?,
172        })
173    }
174}