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/// A recovered EIP-7702 authorization with AA signature support.
171///
172/// This wraps an `TempoSignedAuthorization` with lazy authority recovery.
173/// The signature is preserved for gas calculation, and the authority
174/// is recovered on first access and cached.
175#[derive(Clone, Debug)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177pub struct RecoveredTempoAuthorization {
178    /// Signed authorization (contains inner auth and signature)
179    signed: TempoSignedAuthorization,
180    /// Lazily recovered authority (cached after first access)
181    #[cfg_attr(feature = "serde", serde(skip))]
182    authority: OnceLock<RecoveredAuthority>,
183}
184
185impl RecoveredTempoAuthorization {
186    /// Creates a new authorization from a signed authorization.
187    ///
188    /// Authority recovery is deferred until first access.
189    pub const fn new(signed: TempoSignedAuthorization) -> Self {
190        Self {
191            signed,
192            authority: OnceLock::new(),
193        }
194    }
195
196    /// Creates a new authorization with a pre-recovered authority.
197    ///
198    /// This is useful when you've already recovered the authority and want
199    /// to avoid re-recovery.
200    pub fn new_unchecked(signed: TempoSignedAuthorization, authority: RecoveredAuthority) -> Self {
201        Self {
202            signed,
203            authority: {
204                let value = OnceLock::new();
205                #[allow(clippy::useless_conversion)]
206                let _ = value.set(authority.into());
207                value
208            },
209        }
210    }
211
212    /// Creates a new authorization and immediately recovers the authority.
213    ///
214    /// Unlike `new()`, this eagerly recovers the authority upfront and caches it.
215    pub fn recover(signed: TempoSignedAuthorization) -> Self {
216        let authority = signed
217            .recover_authority()
218            .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
219        Self::new_unchecked(signed, authority)
220    }
221
222    /// Returns a reference to the signed authorization.
223    pub const fn signed(&self) -> &TempoSignedAuthorization {
224        &self.signed
225    }
226
227    /// Returns a reference to the inner [`Authorization`].
228    pub const fn inner(&self) -> &Authorization {
229        self.signed.inner()
230    }
231
232    /// Gets the `signature` for the authorization.
233    pub const fn signature(&self) -> &TempoSignature {
234        self.signed.signature()
235    }
236
237    /// Returns the recovered authority, if valid.
238    ///
239    /// Recovers the authority on first access and caches the result.
240    pub fn authority(&self) -> Option<Address> {
241        match self.authority_status() {
242            RecoveredAuthority::Valid(addr) => Some(*addr),
243            RecoveredAuthority::Invalid => None,
244        }
245    }
246
247    /// Returns the recovered authority status.
248    ///
249    /// Recovers the authority on first access and caches the result.
250    pub fn authority_status(&self) -> &RecoveredAuthority {
251        #[allow(clippy::useless_conversion)]
252        self.authority.get_or_init(|| {
253            self.signed
254                .recover_authority()
255                .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid)
256                .into()
257        })
258    }
259
260    /// Converts into a standard `RecoveredAuthorization`, dropping the signature.
261    pub fn into_recovered_authorization(self) -> RecoveredAuthorization {
262        let authority = self.authority_status().clone();
263        RecoveredAuthorization::new_unchecked(self.signed.strip_signature(), authority)
264    }
265}
266
267impl PartialEq for RecoveredTempoAuthorization {
268    fn eq(&self, other: &Self) -> bool {
269        self.signed == other.signed
270    }
271}
272
273impl Eq for RecoveredTempoAuthorization {}
274
275impl core::hash::Hash for RecoveredTempoAuthorization {
276    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
277        self.signed.hash(state);
278    }
279}
280
281impl Deref for RecoveredTempoAuthorization {
282    type Target = Authorization;
283
284    fn deref(&self) -> &Self::Target {
285        self.signed.inner()
286    }
287}
288
289impl AuthorizationTr for RecoveredTempoAuthorization {
290    fn chain_id(&self) -> U256 {
291        self.chain_id
292    }
293    fn address(&self) -> Address {
294        self.address
295    }
296    fn nonce(&self) -> u64 {
297        self.nonce
298    }
299
300    fn authority(&self) -> Option<Address> {
301        self.authority()
302    }
303}
304
305#[cfg(test)]
306pub mod tests {
307    use super::*;
308    use crate::TempoSignature;
309    use alloy_primitives::{U256, address};
310    use alloy_signer::SignerSync;
311    use alloy_signer_local::PrivateKeySigner;
312
313    #[test]
314    fn test_aa_signed_auth_encode_decode_roundtrip() {
315        let auth = Authorization {
316            chain_id: U256::from(1),
317            address: address!("0000000000000000000000000000000000000006"),
318            nonce: 1,
319        };
320
321        let signature = TempoSignature::default(); // Use secp256k1 test signature
322        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
323
324        let mut buf = Vec::new();
325        signed.encode(&mut buf);
326
327        let decoded = TempoSignedAuthorization::decode(&mut buf.as_slice()).unwrap();
328        assert_eq!(buf.len(), signed.length());
329        assert_eq!(decoded, signed);
330
331        // Test accessors
332        assert_eq!(signed.inner(), &auth);
333        assert_eq!(signed.signature(), &signature);
334        assert!(signed.size() > 0);
335
336        // Test Deref to Authorization
337        assert_eq!(signed.chain_id, auth.chain_id);
338        assert_eq!(signed.address, auth.address);
339        assert_eq!(signed.nonce, auth.nonce);
340
341        // Test strip_signature
342        let stripped = signed.strip_signature();
343        assert_eq!(stripped, auth);
344    }
345
346    #[test]
347    fn test_signature_hash() {
348        let auth = Authorization {
349            chain_id: U256::from(1),
350            address: address!("0000000000000000000000000000000000000006"),
351            nonce: 1,
352        };
353
354        let signature = TempoSignature::default();
355        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature);
356
357        // Signature hash should match alloy's calculation
358        let expected_hash = {
359            let mut buf = Vec::new();
360            buf.push(MAGIC);
361            auth.encode(&mut buf);
362            keccak256(buf)
363        };
364
365        assert_eq!(signed.signature_hash(), expected_hash);
366    }
367
368    pub fn generate_secp256k1_keypair() -> (PrivateKeySigner, Address) {
369        let signer = PrivateKeySigner::random();
370        let address = signer.address();
371        (signer, address)
372    }
373
374    pub fn sign_hash(signer: &PrivateKeySigner, hash: &B256) -> TempoSignature {
375        let signature = signer.sign_hash_sync(hash).expect("signing failed");
376        TempoSignature::from(signature)
377    }
378
379    #[test]
380    fn test_recover_authority() {
381        let (signing_key, expected_address) = generate_secp256k1_keypair();
382
383        let auth = Authorization {
384            chain_id: U256::ONE,
385            address: Address::random(),
386            nonce: 1,
387        };
388
389        // Create and sign auth
390        let placeholder_sig = TempoSignature::default();
391        let temp_signed = TempoSignedAuthorization::new_unchecked(auth.clone(), placeholder_sig);
392        let signature = sign_hash(&signing_key, &temp_signed.signature_hash());
393        let signed = TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
394
395        // Recovery should succeed
396        let recovered = signed.recover_authority();
397        assert!(recovered.is_ok());
398        assert_eq!(recovered.unwrap(), expected_address);
399
400        // into_recovered() returns RecoveredAuthorization
401        let signed_for_into =
402            TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
403        let std_recovered = signed_for_into.into_recovered();
404        assert_eq!(std_recovered.authority(), Some(expected_address));
405
406        // RecoveredTempoAuthorization - lazy recovery
407        let signed_for_lazy =
408            TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
409        let lazy_recovered = RecoveredTempoAuthorization::new(signed_for_lazy);
410        assert_eq!(lazy_recovered.authority(), Some(expected_address));
411        assert!(matches!(
412            lazy_recovered.authority_status(),
413            RecoveredAuthority::Valid(_)
414        ));
415
416        // RecoveredTempoAuthorization::recover() - eager recovery
417        let signed_for_eager =
418            TempoSignedAuthorization::new_unchecked(auth.clone(), signature.clone());
419        let eager_recovered = RecoveredTempoAuthorization::recover(signed_for_eager);
420        assert_eq!(eager_recovered.authority(), Some(expected_address));
421
422        // Accessors on RecoveredTempoAuthorization
423        assert_eq!(eager_recovered.signed().inner(), &auth);
424        assert_eq!(eager_recovered.inner(), &auth);
425        assert_eq!(eager_recovered.signature(), &signature);
426
427        // into_recovered_authorization()
428        let signed_for_convert = TempoSignedAuthorization::new_unchecked(auth.clone(), signature);
429        let converted = RecoveredTempoAuthorization::new(signed_for_convert);
430        let std_auth = converted.into_recovered_authorization();
431        assert_eq!(std_auth.authority(), Some(expected_address));
432
433        // Sign a different hash - invalid recovery
434        let wrong_hash = B256::random();
435        let wrong_signature = sign_hash(&signing_key, &wrong_hash);
436        let bad_signed = TempoSignedAuthorization::new_unchecked(auth, wrong_signature);
437
438        // Recovery succeeds but yields wrong address
439        let recovered = bad_signed.recover_authority();
440        assert!(recovered.is_ok());
441        assert_ne!(recovered.unwrap(), expected_address);
442
443        // RecoveredTempoAuthorization with wrong sig still recovers (to wrong address)
444        let bad_lazy = RecoveredTempoAuthorization::new(bad_signed);
445        assert!(bad_lazy.authority().is_some());
446        assert_ne!(bad_lazy.authority().unwrap(), expected_address);
447    }
448}