Skip to main content

tempo_primitives/transaction/
tt_authorization.rs

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