tempo_primitives/transaction/
tt_authorization.rs

1use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization};
2use alloy_primitives::{Address, B256, keccak256};
3use alloy_rlp::{BufMut, Decodable, Encodable, Header, Result as RlpResult, length_of_length};
4use core::ops::Deref;
5use std::sync::OnceLock;
6
7use crate::TempoSignature;
8
9/// EIP-7702 authorization magic byte
10pub const MAGIC: u8 = 0x05;
11
12/// A signed EIP-7702 authorization with AA signature support.
13///
14/// This is a 1:1 parallel to alloy's `SignedAuthorization`, but using `TempoSignature`
15/// instead of hardcoded (y_parity, r, s) components. This allows supporting multiple
16/// signature types: Secp256k1, P256, and WebAuthn.
17///
18/// The structure and methods mirror `SignedAuthorization` exactly to maintain
19/// compatibility with the EIP-7702 spec.
20#[derive(Clone, Debug, Eq, PartialEq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
23#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
24pub struct TempoSignedAuthorization {
25    /// Inner authorization (reuses alloy's Authorization)
26    #[cfg_attr(feature = "serde", serde(flatten))]
27    inner: Authorization,
28    /// The AA signature (Secp256k1, P256, or WebAuthn)
29    signature: TempoSignature,
30}
31
32impl TempoSignedAuthorization {
33    /// Creates a new signed authorization from an authorization and signature.
34    ///
35    /// This is the unchecked version - signature is not validated.
36    pub const fn new_unchecked(inner: Authorization, signature: TempoSignature) -> Self {
37        Self { inner, signature }
38    }
39
40    /// Gets the `signature` for the authorization.
41    ///
42    /// Returns a reference to the AA signature, which can be Secp256k1, P256, or WebAuthn.
43    pub const fn signature(&self) -> &TempoSignature {
44        &self.signature
45    }
46
47    /// Returns the inner [`Authorization`].
48    pub fn strip_signature(self) -> Authorization {
49        self.inner
50    }
51
52    /// Returns a reference to the inner [`Authorization`].
53    pub const fn inner(&self) -> &Authorization {
54        &self.inner
55    }
56
57    /// Computes the signature hash used to sign the authorization.
58    ///
59    /// The signature hash is `keccak(MAGIC || rlp([chain_id, address, nonce]))`
60    /// following EIP-7702 spec.
61    #[inline]
62    pub fn signature_hash(&self) -> B256 {
63        let mut buf = Vec::new();
64        buf.push(MAGIC);
65        self.inner.encode(&mut buf);
66        keccak256(buf)
67    }
68
69    /// Recover the authority for the authorization.
70    ///
71    /// # Note
72    ///
73    /// Implementers should check that the authority has no code.
74    pub fn recover_authority(&self) -> Result<Address, alloy_consensus::crypto::RecoveryError> {
75        let sig_hash = self.signature_hash();
76        self.signature.recover_signer(&sig_hash)
77    }
78
79    /// Recover the authority and transform the signed authorization into a
80    /// [`RecoveredAuthorization`].
81    pub fn into_recovered(self) -> RecoveredAuthorization {
82        let authority_result = self.recover_authority();
83        let authority =
84            authority_result.map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
85
86        RecoveredAuthorization::new_unchecked(self.inner, authority)
87    }
88
89    /// Decodes the authorization from RLP bytes, including the signature.
90    fn decode_fields(buf: &mut &[u8]) -> RlpResult<Self> {
91        Ok(Self {
92            inner: Authorization {
93                chain_id: Decodable::decode(buf)?,
94                address: Decodable::decode(buf)?,
95                nonce: Decodable::decode(buf)?,
96            },
97            signature: Decodable::decode(buf)?,
98        })
99    }
100
101    /// Outputs the length of the authorization's fields, without a RLP header.
102    fn fields_len(&self) -> usize {
103        self.inner.chain_id.length()
104            + self.inner.address.length()
105            + self.inner.nonce.length()
106            + self.signature.length()
107    }
108
109    /// Calculates a heuristic for the in-memory size of this authorization
110    pub fn size(&self) -> usize {
111        core::mem::size_of::<Authorization>() + self.signature.size()
112    }
113}
114
115impl Decodable for TempoSignedAuthorization {
116    fn decode(buf: &mut &[u8]) -> RlpResult<Self> {
117        let header = Header::decode(buf)?;
118        if !header.list {
119            return Err(alloy_rlp::Error::UnexpectedString);
120        }
121        let started_len = buf.len();
122
123        let this = Self::decode_fields(buf)?;
124
125        let consumed = started_len - buf.len();
126        if consumed != header.payload_length {
127            return Err(alloy_rlp::Error::ListLengthMismatch {
128                expected: header.payload_length,
129                got: consumed,
130            });
131        }
132
133        Ok(this)
134    }
135}
136
137impl Encodable for TempoSignedAuthorization {
138    fn encode(&self, buf: &mut dyn BufMut) {
139        Header {
140            list: true,
141            payload_length: self.fields_len(),
142        }
143        .encode(buf);
144        self.inner.chain_id.encode(buf);
145        self.inner.address.encode(buf);
146        self.inner.nonce.encode(buf);
147        self.signature.encode(buf);
148    }
149
150    fn length(&self) -> usize {
151        let len = self.fields_len();
152        len + length_of_length(len)
153    }
154}
155
156impl Deref for TempoSignedAuthorization {
157    type Target = Authorization;
158
159    fn deref(&self) -> &Self::Target {
160        &self.inner
161    }
162}
163
164// Compact implementation for reth storage
165#[cfg(feature = "reth-codec")]
166impl reth_codecs::Compact for TempoSignedAuthorization {
167    fn to_compact<B>(&self, buf: &mut B) -> usize
168    where
169        B: alloy_rlp::BufMut + AsMut<[u8]>,
170    {
171        // Encode using RLP
172        let start_len = buf.remaining_mut();
173        self.encode(buf);
174        start_len - buf.remaining_mut()
175    }
176
177    fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
178        let mut buf_slice = &buf[..len];
179        let auth = Self::decode(&mut buf_slice).expect("valid RLP encoding");
180        (auth, &buf[len..])
181    }
182}
183
184/// A recovered EIP-7702 authorization with AA signature support.
185///
186/// This wraps an `TempoSignedAuthorization` with lazy authority recovery.
187/// The signature is preserved for gas calculation, and the authority
188/// is recovered on first access and cached.
189#[derive(Clone, Debug)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191pub struct RecoveredTempoAuthorization {
192    /// Signed authorization (contains inner auth and signature)
193    signed: TempoSignedAuthorization,
194    /// Lazily recovered authority (cached after first access)
195    #[cfg_attr(feature = "serde", serde(skip))]
196    authority: OnceLock<RecoveredAuthority>,
197}
198
199impl RecoveredTempoAuthorization {
200    /// Creates a new authorization from a signed authorization.
201    ///
202    /// Authority recovery is deferred until first access.
203    pub const fn new(signed: TempoSignedAuthorization) -> Self {
204        Self {
205            signed,
206            authority: OnceLock::new(),
207        }
208    }
209
210    /// Creates a new authorization with a pre-recovered authority.
211    ///
212    /// This is useful when you've already recovered the authority and want
213    /// to avoid re-recovery.
214    pub fn new_unchecked(signed: TempoSignedAuthorization, authority: RecoveredAuthority) -> Self {
215        Self {
216            signed,
217            authority: authority.into(),
218        }
219    }
220
221    /// Creates a new authorization and immediately recovers the authority.
222    ///
223    /// Unlike `new()`, this eagerly recovers the authority upfront and caches it.
224    pub fn recover(signed: TempoSignedAuthorization) -> Self {
225        let authority = signed
226            .recover_authority()
227            .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
228        Self::new_unchecked(signed, authority)
229    }
230
231    /// Returns a reference to the signed authorization.
232    pub const fn signed(&self) -> &TempoSignedAuthorization {
233        &self.signed
234    }
235
236    /// Returns a reference to the inner [`Authorization`].
237    pub const fn inner(&self) -> &Authorization {
238        self.signed.inner()
239    }
240
241    /// Gets the `signature` for the authorization.
242    pub const fn signature(&self) -> &TempoSignature {
243        self.signed.signature()
244    }
245
246    /// Returns the recovered authority, if valid.
247    ///
248    /// Recovers the authority on first access and caches the result.
249    pub fn authority(&self) -> Option<Address> {
250        match self.authority_status() {
251            RecoveredAuthority::Valid(addr) => Some(*addr),
252            RecoveredAuthority::Invalid => None,
253        }
254    }
255
256    /// Returns the recovered authority status.
257    ///
258    /// Recovers the authority on first access and caches the result.
259    pub fn authority_status(&self) -> &RecoveredAuthority {
260        self.authority.get_or_init(|| {
261            self.signed
262                .recover_authority()
263                .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid)
264        })
265    }
266
267    /// Converts into a standard `RecoveredAuthorization`, dropping the signature.
268    pub fn into_recovered_authorization(self) -> RecoveredAuthorization {
269        let authority = self.authority_status().clone();
270        RecoveredAuthorization::new_unchecked(self.signed.strip_signature(), authority)
271    }
272}
273
274impl PartialEq for RecoveredTempoAuthorization {
275    fn eq(&self, other: &Self) -> bool {
276        self.signed == other.signed
277    }
278}
279
280impl Eq for RecoveredTempoAuthorization {}
281
282impl core::hash::Hash for RecoveredTempoAuthorization {
283    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
284        self.signed.hash(state);
285    }
286}
287
288impl Deref for RecoveredTempoAuthorization {
289    type Target = Authorization;
290
291    fn deref(&self) -> &Self::Target {
292        self.signed.inner()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::TempoSignature;
300    use alloy_primitives::{U256, address};
301
302    #[test]
303    fn test_aa_signed_auth_encode_decode_roundtrip() {
304        let auth = Authorization {
305            chain_id: U256::from(1),
306            address: address!("0000000000000000000000000000000000000006"),
307            nonce: 1,
308        };
309
310        let signature = TempoSignature::default(); // Use secp256k1 test signature
311        let signed = TempoSignedAuthorization::new_unchecked(auth, signature);
312
313        let mut buf = Vec::new();
314        signed.encode(&mut buf);
315
316        let decoded = TempoSignedAuthorization::decode(&mut buf.as_slice()).unwrap();
317        assert_eq!(buf.len(), signed.length());
318        assert_eq!(decoded, signed);
319    }
320
321    #[test]
322    fn test_signature_hash() {
323        let auth = Authorization {
324            chain_id: U256::from(1),
325            address: address!("0000000000000000000000000000000000000006"),
326            nonce: 1,
327        };
328
329        let signature = TempoSignature::default();
330        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature);
331
332        // Signature hash should match alloy's calculation
333        let expected_hash = {
334            let mut buf = Vec::new();
335            buf.push(MAGIC);
336            auth.encode(&mut buf);
337            keccak256(buf)
338        };
339
340        assert_eq!(signed.signature_hash(), expected_hash);
341    }
342}