Skip to main content

tempo_primitives/transaction/
tt_authorization.rs

1use alloc::vec::Vec;
2use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization};
3use alloy_primitives::{Address, B256, U256, keccak256};
4use alloy_rlp::{BufMut, Decodable, Encodable, Header, Result as RlpResult, length_of_length};
5use core::ops::Deref;
6use revm::context::transaction::AuthorizationTr;
7
8#[cfg(not(feature = "std"))]
9use once_cell::race::OnceBox as OnceLock;
10#[cfg(feature = "std")]
11use std::sync::OnceLock;
12
13use crate::TempoSignature;
14
15/// EIP-7702 authorization magic byte
16pub const MAGIC: u8 = 0x05;
17
18/// A signed EIP-7702 authorization with AA signature support.
19///
20/// This is a 1:1 parallel to alloy's `SignedAuthorization`, but using `TempoSignature`
21/// instead of hardcoded (y_parity, r, s) components. This allows supporting multiple
22/// signature types: Secp256k1, P256, and WebAuthn.
23///
24/// The structure and methods mirror `SignedAuthorization` exactly to maintain
25/// compatibility with the EIP-7702 spec.
26#[derive(Clone, Debug, Eq, PartialEq, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
29#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
30pub struct TempoSignedAuthorization {
31    /// Inner authorization (reuses alloy's Authorization)
32    #[cfg_attr(feature = "serde", serde(flatten))]
33    inner: Authorization,
34    /// The AA signature (Secp256k1, P256, or WebAuthn)
35    signature: TempoSignature,
36}
37
38impl TempoSignedAuthorization {
39    /// Creates a new signed authorization from an authorization and signature.
40    ///
41    /// This is the unchecked version - signature is not validated.
42    pub const fn new_unchecked(inner: Authorization, signature: TempoSignature) -> Self {
43        Self { inner, signature }
44    }
45
46    /// Gets the `signature` for the authorization.
47    ///
48    /// Returns a reference to the AA signature, which can be Secp256k1, P256, or WebAuthn.
49    pub const fn signature(&self) -> &TempoSignature {
50        &self.signature
51    }
52
53    /// Returns the inner [`Authorization`].
54    pub fn strip_signature(self) -> Authorization {
55        self.inner
56    }
57
58    /// Returns a reference to the inner [`Authorization`].
59    pub const fn inner(&self) -> &Authorization {
60        &self.inner
61    }
62
63    /// Computes the signature hash used to sign the authorization.
64    ///
65    /// The signature hash is `keccak(MAGIC || rlp([chain_id, address, nonce]))`
66    /// following EIP-7702 spec.
67    #[inline]
68    pub fn signature_hash(&self) -> B256 {
69        let mut buf = Vec::new();
70        buf.push(MAGIC);
71        self.inner.encode(&mut buf);
72        keccak256(buf)
73    }
74
75    /// Recover the authority for the authorization.
76    ///
77    /// # Note
78    ///
79    /// Implementers should check that the authority has no code.
80    pub fn recover_authority(&self) -> Result<Address, alloy_consensus::crypto::RecoveryError> {
81        let sig_hash = self.signature_hash();
82        self.signature.recover_signer(&sig_hash)
83    }
84
85    /// Recover the authority and transform the signed authorization into a
86    /// [`RecoveredAuthorization`].
87    pub fn into_recovered(self) -> RecoveredAuthorization {
88        let authority_result = self.recover_authority();
89        let authority =
90            authority_result.map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
91
92        RecoveredAuthorization::new_unchecked(self.inner, authority)
93    }
94
95    /// Decodes the authorization from RLP bytes, including the signature.
96    fn decode_fields(buf: &mut &[u8]) -> RlpResult<Self> {
97        Ok(Self {
98            inner: Authorization {
99                chain_id: Decodable::decode(buf)?,
100                address: Decodable::decode(buf)?,
101                nonce: Decodable::decode(buf)?,
102            },
103            signature: Decodable::decode(buf)?,
104        })
105    }
106
107    /// Outputs the length of the authorization's fields, without a RLP header.
108    fn fields_len(&self) -> usize {
109        self.inner.chain_id.length()
110            + self.inner.address.length()
111            + self.inner.nonce.length()
112            + self.signature.length()
113    }
114
115    /// Calculates a heuristic for the in-memory size of this authorization
116    pub fn size(&self) -> usize {
117        size_of::<Self>()
118    }
119}
120
121impl Decodable for TempoSignedAuthorization {
122    fn decode(buf: &mut &[u8]) -> RlpResult<Self> {
123        let header = Header::decode(buf)?;
124        if !header.list {
125            return Err(alloy_rlp::Error::UnexpectedString);
126        }
127        let started_len = buf.len();
128
129        let this = Self::decode_fields(buf)?;
130
131        let consumed = started_len - buf.len();
132        if consumed != header.payload_length {
133            return Err(alloy_rlp::Error::ListLengthMismatch {
134                expected: header.payload_length,
135                got: consumed,
136            });
137        }
138
139        Ok(this)
140    }
141}
142
143impl Encodable for TempoSignedAuthorization {
144    fn encode(&self, buf: &mut dyn BufMut) {
145        Header {
146            list: true,
147            payload_length: self.fields_len(),
148        }
149        .encode(buf);
150        self.inner.chain_id.encode(buf);
151        self.inner.address.encode(buf);
152        self.inner.nonce.encode(buf);
153        self.signature.encode(buf);
154    }
155
156    fn length(&self) -> usize {
157        let len = self.fields_len();
158        len + length_of_length(len)
159    }
160}
161
162impl Deref for TempoSignedAuthorization {
163    type Target = Authorization;
164
165    fn deref(&self) -> &Self::Target {
166        &self.inner
167    }
168}
169
170// Compact implementation for reth storage
171#[cfg(feature = "reth-codec")]
172impl reth_codecs::Compact for TempoSignedAuthorization {
173    fn to_compact<B>(&self, buf: &mut B) -> usize
174    where
175        B: alloy_rlp::BufMut + AsMut<[u8]>,
176    {
177        // Encode using RLP
178        let start_len = buf.remaining_mut();
179        self.encode(buf);
180        start_len - buf.remaining_mut()
181    }
182
183    fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
184        let mut buf_slice = &buf[..len];
185        let auth = Self::decode(&mut buf_slice).expect("valid RLP encoding");
186        (auth, &buf[len..])
187    }
188}
189
190/// A recovered EIP-7702 authorization with AA signature support.
191///
192/// This wraps an `TempoSignedAuthorization` with lazy authority recovery.
193/// The signature is preserved for gas calculation, and the authority
194/// is recovered on first access and cached.
195#[derive(Clone, Debug)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub struct RecoveredTempoAuthorization {
198    /// Signed authorization (contains inner auth and signature)
199    signed: TempoSignedAuthorization,
200    /// Lazily recovered authority (cached after first access)
201    #[cfg_attr(feature = "serde", serde(skip))]
202    authority: OnceLock<RecoveredAuthority>,
203}
204
205impl RecoveredTempoAuthorization {
206    /// Creates a new authorization from a signed authorization.
207    ///
208    /// Authority recovery is deferred until first access.
209    pub const fn new(signed: TempoSignedAuthorization) -> Self {
210        Self {
211            signed,
212            authority: OnceLock::new(),
213        }
214    }
215
216    /// Creates a new authorization with a pre-recovered authority.
217    ///
218    /// This is useful when you've already recovered the authority and want
219    /// to avoid re-recovery.
220    pub fn new_unchecked(signed: TempoSignedAuthorization, authority: RecoveredAuthority) -> Self {
221        Self {
222            signed,
223            authority: {
224                let value = OnceLock::new();
225                #[allow(clippy::useless_conversion)]
226                let _ = value.set(authority.into());
227                value
228            },
229        }
230    }
231
232    /// Creates a new authorization and immediately recovers the authority.
233    ///
234    /// Unlike `new()`, this eagerly recovers the authority upfront and caches it.
235    pub fn recover(signed: TempoSignedAuthorization) -> Self {
236        let authority = signed
237            .recover_authority()
238            .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
239        Self::new_unchecked(signed, authority)
240    }
241
242    /// Returns a reference to the signed authorization.
243    pub const fn signed(&self) -> &TempoSignedAuthorization {
244        &self.signed
245    }
246
247    /// Returns a reference to the inner [`Authorization`].
248    pub const fn inner(&self) -> &Authorization {
249        self.signed.inner()
250    }
251
252    /// Gets the `signature` for the authorization.
253    pub const fn signature(&self) -> &TempoSignature {
254        self.signed.signature()
255    }
256
257    /// Returns the recovered authority, if valid.
258    ///
259    /// Recovers the authority on first access and caches the result.
260    pub fn authority(&self) -> Option<Address> {
261        match self.authority_status() {
262            RecoveredAuthority::Valid(addr) => Some(*addr),
263            RecoveredAuthority::Invalid => None,
264        }
265    }
266
267    /// Returns the recovered authority status.
268    ///
269    /// Recovers the authority on first access and caches the result.
270    pub fn authority_status(&self) -> &RecoveredAuthority {
271        #[allow(clippy::useless_conversion)]
272        self.authority.get_or_init(|| {
273            self.signed
274                .recover_authority()
275                .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid)
276                .into()
277        })
278    }
279
280    /// Converts into a standard `RecoveredAuthorization`, dropping the signature.
281    pub fn into_recovered_authorization(self) -> RecoveredAuthorization {
282        let authority = self.authority_status().clone();
283        RecoveredAuthorization::new_unchecked(self.signed.strip_signature(), authority)
284    }
285}
286
287impl PartialEq for RecoveredTempoAuthorization {
288    fn eq(&self, other: &Self) -> bool {
289        self.signed == other.signed
290    }
291}
292
293impl Eq for RecoveredTempoAuthorization {}
294
295impl core::hash::Hash for RecoveredTempoAuthorization {
296    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
297        self.signed.hash(state);
298    }
299}
300
301impl Deref for RecoveredTempoAuthorization {
302    type Target = Authorization;
303
304    fn deref(&self) -> &Self::Target {
305        self.signed.inner()
306    }
307}
308
309impl AuthorizationTr for RecoveredTempoAuthorization {
310    fn chain_id(&self) -> U256 {
311        self.chain_id
312    }
313    fn address(&self) -> Address {
314        self.address
315    }
316    fn nonce(&self) -> u64 {
317        self.nonce
318    }
319
320    fn authority(&self) -> Option<Address> {
321        self.authority()
322    }
323}
324
325#[cfg(test)]
326pub mod tests {
327    use super::*;
328    use crate::TempoSignature;
329    use alloy_primitives::{U256, address};
330    use alloy_signer::SignerSync;
331    use alloy_signer_local::PrivateKeySigner;
332
333    #[test]
334    fn test_aa_signed_auth_encode_decode_roundtrip() {
335        let auth = Authorization {
336            chain_id: U256::from(1),
337            address: address!("0000000000000000000000000000000000000006"),
338            nonce: 1,
339        };
340
341        let signature = TempoSignature::default(); // Use secp256k1 test signature
342        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
343
344        let mut buf = Vec::new();
345        signed.encode(&mut buf);
346
347        let decoded = TempoSignedAuthorization::decode(&mut buf.as_slice()).unwrap();
348        assert_eq!(buf.len(), signed.length());
349        assert_eq!(decoded, signed);
350
351        // Test accessors
352        assert_eq!(signed.inner(), &auth);
353        assert_eq!(signed.signature(), &signature);
354        assert!(signed.size() > 0);
355
356        // Test Deref to Authorization
357        assert_eq!(signed.chain_id, auth.chain_id);
358        assert_eq!(signed.address, auth.address);
359        assert_eq!(signed.nonce, auth.nonce);
360
361        // Test strip_signature
362        let stripped = signed.strip_signature();
363        assert_eq!(stripped, auth);
364    }
365
366    #[test]
367    fn test_signature_hash() {
368        let auth = Authorization {
369            chain_id: U256::from(1),
370            address: address!("0000000000000000000000000000000000000006"),
371            nonce: 1,
372        };
373
374        let signature = TempoSignature::default();
375        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature);
376
377        // Signature hash should match alloy's calculation
378        let expected_hash = {
379            let mut buf = Vec::new();
380            buf.push(MAGIC);
381            auth.encode(&mut buf);
382            keccak256(buf)
383        };
384
385        assert_eq!(signed.signature_hash(), expected_hash);
386    }
387
388    pub fn generate_secp256k1_keypair() -> (PrivateKeySigner, Address) {
389        let signer = PrivateKeySigner::random();
390        let address = signer.address();
391        (signer, address)
392    }
393
394    pub fn sign_hash(signer: &PrivateKeySigner, hash: &B256) -> TempoSignature {
395        let signature = signer.sign_hash_sync(hash).expect("signing failed");
396        TempoSignature::from(signature)
397    }
398
399    #[test]
400    fn test_recover_authority() {
401        let (signing_key, expected_address) = generate_secp256k1_keypair();
402
403        let auth = Authorization {
404            chain_id: U256::ONE,
405            address: Address::random(),
406            nonce: 1,
407        };
408
409        // Create and sign auth
410        let placeholder_sig = TempoSignature::default();
411        let temp_signed = TempoSignedAuthorization::new_unchecked(auth.clone(), placeholder_sig);
412        let signature = sign_hash(&signing_key, &temp_signed.signature_hash());
413        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
414
415        // Recovery should succeed
416        let recovered = signed.recover_authority();
417        assert!(recovered.is_ok());
418        assert_eq!(recovered.unwrap(), expected_address);
419
420        // into_recovered() returns RecoveredAuthorization
421        let signed_for_into =
422            TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
423        let std_recovered = signed_for_into.into_recovered();
424        assert_eq!(std_recovered.authority(), Some(expected_address));
425
426        // RecoveredTempoAuthorization - lazy recovery
427        let signed_for_lazy =
428            TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
429        let lazy_recovered = RecoveredTempoAuthorization::new(signed_for_lazy);
430        assert_eq!(lazy_recovered.authority(), Some(expected_address));
431        assert!(matches!(
432            lazy_recovered.authority_status(),
433            RecoveredAuthority::Valid(_)
434        ));
435
436        // RecoveredTempoAuthorization::recover() - eager recovery
437        let signed_for_eager =
438            TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
439        let eager_recovered = RecoveredTempoAuthorization::recover(signed_for_eager);
440        assert_eq!(eager_recovered.authority(), Some(expected_address));
441
442        // Accessors on RecoveredTempoAuthorization
443        assert_eq!(eager_recovered.signed().inner(), &auth);
444        assert_eq!(eager_recovered.inner(), &auth);
445        assert_eq!(eager_recovered.signature(), &signature);
446
447        // into_recovered_authorization()
448        let signed_for_convert = TempoSignedAuthorization::new_unchecked(auth.clone(), signature);
449        let converted = RecoveredTempoAuthorization::new(signed_for_convert);
450        let std_auth = converted.into_recovered_authorization();
451        assert_eq!(std_auth.authority(), Some(expected_address));
452
453        // Sign a different hash - invalid recovery
454        let wrong_hash = B256::random();
455        let wrong_signature = sign_hash(&signing_key, &wrong_hash);
456        let bad_signed = TempoSignedAuthorization::new_unchecked(auth, wrong_signature);
457
458        // Recovery succeeds but yields wrong address
459        let recovered = bad_signed.recover_authority();
460        assert!(recovered.is_ok());
461        assert_ne!(recovered.unwrap(), expected_address);
462
463        // RecoveredTempoAuthorization with wrong sig still recovers (to wrong address)
464        let bad_lazy = RecoveredTempoAuthorization::new(bad_signed);
465        assert!(bad_lazy.authority().is_some());
466        assert_ne!(bad_lazy.authority().unwrap(), expected_address);
467    }
468}