tempo_primitives/transaction/
tt_signed.rs

1use super::{
2    tempo_transaction::{TEMPO_TX_TYPE_ID, TempoTransaction},
3    tt_signature::TempoSignature,
4};
5use alloy_consensus::{Transaction, transaction::TxHashRef};
6use alloy_eips::{
7    Decodable2718, Encodable2718, Typed2718,
8    eip2718::{Eip2718Error, Eip2718Result},
9    eip2930::AccessList,
10    eip7702::SignedAuthorization,
11};
12use alloy_primitives::{B256, Bytes, TxKind, U256};
13use alloy_rlp::{BufMut, Decodable, Encodable};
14use core::{
15    fmt::Debug,
16    hash::{Hash, Hasher},
17    mem,
18};
19use reth_primitives_traits::InMemorySize;
20use std::sync::OnceLock;
21
22/// A transaction with an AA signature and hash seal.
23///
24/// This wraps a TempoTransaction transaction with its multi-signature-type signature
25/// (secp256k1, P256, Webauthn, Keychain) and provides a cached transaction hash.
26#[derive(Clone, Debug)]
27pub struct AASigned {
28    /// The inner Tempo transaction
29    tx: TempoTransaction,
30    /// The signature (can be secp256k1, P256, Webauthn, Keychain)
31    signature: TempoSignature,
32    /// Cached transaction hash
33    #[doc(alias = "tx_hash", alias = "transaction_hash")]
34    hash: OnceLock<B256>,
35}
36
37impl AASigned {
38    /// Instantiate from a transaction and signature with a known hash.
39    /// Does not verify the signature.
40    pub fn new_unchecked(tx: TempoTransaction, signature: TempoSignature, hash: B256) -> Self {
41        let value = OnceLock::new();
42        #[allow(clippy::useless_conversion)]
43        value.get_or_init(|| hash.into());
44        Self {
45            tx,
46            signature,
47            hash: value,
48        }
49    }
50
51    /// Instantiate from a transaction and signature without computing the hash.
52    /// Does not verify the signature.
53    pub const fn new_unhashed(tx: TempoTransaction, signature: TempoSignature) -> Self {
54        Self {
55            tx,
56            signature,
57            hash: OnceLock::new(),
58        }
59    }
60
61    /// Returns a reference to the transaction.
62    #[doc(alias = "transaction")]
63    pub const fn tx(&self) -> &TempoTransaction {
64        &self.tx
65    }
66
67    /// Returns a mutable reference to the transaction.
68    pub const fn tx_mut(&mut self) -> &mut TempoTransaction {
69        &mut self.tx
70    }
71
72    /// Returns a reference to the signature.
73    pub const fn signature(&self) -> &TempoSignature {
74        &self.signature
75    }
76
77    /// Returns the transaction without signature.
78    pub fn strip_signature(self) -> TempoTransaction {
79        self.tx
80    }
81
82    /// Returns a reference to the transaction hash, computing it if needed.
83    #[doc(alias = "tx_hash", alias = "transaction_hash")]
84    pub fn hash(&self) -> &B256 {
85        self.hash.get_or_init(|| self.compute_hash())
86    }
87
88    /// Calculate the transaction hash
89    fn compute_hash(&self) -> B256 {
90        let mut buf = Vec::new();
91        self.eip2718_encode(&mut buf);
92        alloy_primitives::keccak256(&buf)
93    }
94
95    /// Calculate the signing hash for the transaction.
96    pub fn signature_hash(&self) -> B256 {
97        self.tx.signature_hash()
98    }
99
100    /// Returns the RLP header for the transaction and signature, encapsulating both
101    /// payload length calculation and header creation
102    #[inline]
103    fn rlp_header(&self) -> alloy_rlp::Header {
104        let payload_length = self.tx.rlp_encoded_fields_length_default() + self.signature.length();
105        alloy_rlp::Header {
106            list: true,
107            payload_length,
108        }
109    }
110
111    /// Encode the transaction fields and signature as RLP list (without type byte)
112    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
113        // RLP header
114        self.rlp_header().encode(out);
115
116        // Encode transaction fields
117        self.tx.rlp_encode_fields_default(out);
118
119        // Encode signature
120        self.signature.encode(out);
121    }
122
123    /// Splits the transaction into parts.
124    pub fn into_parts(self) -> (TempoTransaction, TempoSignature, B256) {
125        let hash = *self.hash();
126        (self.tx, self.signature, hash)
127    }
128
129    /// Get the length of the transaction when RLP encoded.
130    fn rlp_encoded_length(&self) -> usize {
131        self.rlp_header().length_with_payload()
132    }
133
134    /// Get the length of the transaction when EIP-2718 encoded (includes type byte).
135    fn eip2718_encoded_length(&self) -> usize {
136        1 + self.rlp_encoded_length()
137    }
138
139    /// EIP-2718 encode the signed transaction.
140    pub fn eip2718_encode(&self, out: &mut dyn BufMut) {
141        // Type byte
142        out.put_u8(TEMPO_TX_TYPE_ID);
143        // RLP fields
144        self.rlp_encode(out);
145    }
146
147    /// Decode the RLP fields (without type byte).
148    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
149        let header = alloy_rlp::Header::decode(buf)?;
150        if !header.list {
151            return Err(alloy_rlp::Error::UnexpectedString);
152        }
153        let remaining = buf.len();
154
155        if header.payload_length > remaining {
156            return Err(alloy_rlp::Error::InputTooShort);
157        }
158
159        // Decode transaction fields directly from the buffer
160        let tx = TempoTransaction::rlp_decode_fields(buf)?;
161
162        // Decode signature bytes
163        let sig_bytes: Bytes = Decodable::decode(buf)?;
164
165        // Check that we consumed the expected amount
166        let consumed = remaining - buf.len();
167        if consumed != header.payload_length {
168            return Err(alloy_rlp::Error::UnexpectedLength);
169        }
170
171        // Parse signature
172        let signature = TempoSignature::from_bytes(&sig_bytes).map_err(alloy_rlp::Error::Custom)?;
173
174        Ok(Self::new_unhashed(tx, signature))
175    }
176}
177
178impl TxHashRef for AASigned {
179    fn tx_hash(&self) -> &B256 {
180        self.hash()
181    }
182}
183
184impl Typed2718 for AASigned {
185    fn ty(&self) -> u8 {
186        TEMPO_TX_TYPE_ID
187    }
188}
189
190impl Transaction for AASigned {
191    #[inline]
192    fn chain_id(&self) -> Option<u64> {
193        self.tx.chain_id()
194    }
195
196    #[inline]
197    fn nonce(&self) -> u64 {
198        self.tx.nonce()
199    }
200
201    #[inline]
202    fn gas_limit(&self) -> u64 {
203        self.tx.gas_limit()
204    }
205
206    #[inline]
207    fn gas_price(&self) -> Option<u128> {
208        self.tx.gas_price()
209    }
210
211    #[inline]
212    fn max_fee_per_gas(&self) -> u128 {
213        self.tx.max_fee_per_gas()
214    }
215
216    #[inline]
217    fn max_priority_fee_per_gas(&self) -> Option<u128> {
218        self.tx.max_priority_fee_per_gas()
219    }
220
221    #[inline]
222    fn max_fee_per_blob_gas(&self) -> Option<u128> {
223        None
224    }
225
226    #[inline]
227    fn priority_fee_or_price(&self) -> u128 {
228        self.tx.priority_fee_or_price()
229    }
230
231    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
232        self.tx.effective_gas_price(base_fee)
233    }
234
235    #[inline]
236    fn is_dynamic_fee(&self) -> bool {
237        true
238    }
239
240    #[inline]
241    fn kind(&self) -> TxKind {
242        // Return first call's `to` or Create if empty
243        self.tx
244            .calls
245            .first()
246            .map(|c| c.to)
247            .unwrap_or(TxKind::Create)
248    }
249
250    #[inline]
251    fn is_create(&self) -> bool {
252        self.kind().is_create()
253    }
254
255    #[inline]
256    fn value(&self) -> U256 {
257        // Return sum of all call values
258        self.tx
259            .calls
260            .iter()
261            .fold(U256::ZERO, |acc, call| acc + call.value)
262    }
263
264    #[inline]
265    fn input(&self) -> &Bytes {
266        // Return first call's input or empty
267        static EMPTY_BYTES: Bytes = Bytes::new();
268        self.tx
269            .calls
270            .first()
271            .map(|c| &c.input)
272            .unwrap_or(&EMPTY_BYTES)
273    }
274
275    #[inline]
276    fn access_list(&self) -> Option<&AccessList> {
277        Some(&self.tx.access_list)
278    }
279
280    #[inline]
281    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
282        None
283    }
284
285    #[inline]
286    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
287        None
288    }
289}
290
291impl Hash for AASigned {
292    fn hash<H: Hasher>(&self, state: &mut H) {
293        self.hash().hash(state);
294        self.tx.hash(state);
295        self.signature.hash(state);
296    }
297}
298
299impl PartialEq for AASigned {
300    fn eq(&self, other: &Self) -> bool {
301        self.hash() == other.hash() && self.tx == other.tx && self.signature == other.signature
302    }
303}
304
305impl Eq for AASigned {}
306
307impl InMemorySize for AASigned {
308    fn size(&self) -> usize {
309        mem::size_of::<Self>()
310            + self.tx.size()
311            + self.signature.encoded_length()
312            + mem::size_of::<B256>()
313    }
314}
315
316impl alloy_consensus::transaction::SignerRecoverable for AASigned {
317    fn recover_signer(
318        &self,
319    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
320        let sig_hash = self.signature_hash();
321        self.signature.recover_signer(&sig_hash)
322    }
323
324    fn recover_signer_unchecked(
325        &self,
326    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
327        // For Tempo transactions, verified and unverified recovery are the same
328        // since signature verification happens during recover_signer
329        self.recover_signer()
330    }
331}
332
333impl Encodable2718 for AASigned {
334    fn encode_2718_len(&self) -> usize {
335        self.eip2718_encoded_length()
336    }
337
338    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
339        self.eip2718_encode(out)
340    }
341
342    fn trie_hash(&self) -> B256 {
343        *self.hash()
344    }
345}
346
347impl Decodable2718 for AASigned {
348    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
349        if ty != TEMPO_TX_TYPE_ID {
350            return Err(Eip2718Error::UnexpectedType(ty));
351        }
352        Self::rlp_decode(buf).map_err(Into::into)
353    }
354
355    fn fallback_decode(_: &mut &[u8]) -> Eip2718Result<Self> {
356        Err(Eip2718Error::UnexpectedType(0))
357    }
358}
359
360#[cfg(any(test, feature = "arbitrary"))]
361impl<'a> arbitrary::Arbitrary<'a> for AASigned {
362    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
363        let tx = TempoTransaction::arbitrary(u)?;
364        let signature = TempoSignature::arbitrary(u)?;
365        Ok(Self::new_unhashed(tx, signature))
366    }
367}
368
369#[cfg(feature = "serde")]
370mod serde_impl {
371    use super::*;
372    use serde::{Deserialize, Deserializer, Serialize, Serializer};
373    use std::borrow::Cow;
374
375    #[derive(Serialize, Deserialize)]
376    struct AASignedHelper<'a> {
377        #[serde(flatten)]
378        tx: Cow<'a, TempoTransaction>,
379        signature: Cow<'a, TempoSignature>,
380        hash: Cow<'a, B256>,
381    }
382
383    impl Serialize for super::AASigned {
384        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385        where
386            S: Serializer,
387        {
388            if let TempoSignature::Keychain(keychain_sig) = &self.signature {
389                // Initialize the `key_id` field for keychain signatures so that it's serialized.
390                let _ = keychain_sig.key_id(&self.signature_hash());
391            }
392            AASignedHelper {
393                tx: Cow::Borrowed(&self.tx),
394                signature: Cow::Borrowed(&self.signature),
395                hash: Cow::Borrowed(self.hash()),
396            }
397            .serialize(serializer)
398        }
399    }
400
401    impl<'de> Deserialize<'de> for super::AASigned {
402        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403        where
404            D: Deserializer<'de>,
405        {
406            AASignedHelper::deserialize(deserializer).map(|value| {
407                Self::new_unchecked(
408                    value.tx.into_owned(),
409                    value.signature.into_owned(),
410                    value.hash.into_owned(),
411                )
412            })
413        }
414    }
415
416    #[cfg(test)]
417    mod tests {
418        use crate::transaction::{
419            tempo_transaction::{Call, TempoTransaction},
420            tt_signature::{PrimitiveSignature, TempoSignature},
421        };
422        use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
423
424        #[test]
425        fn test_serde_output() {
426            // Create a simple Tempo transaction
427            let tx = TempoTransaction {
428                chain_id: 1337,
429                fee_token: None,
430                max_priority_fee_per_gas: 1000000000,
431                max_fee_per_gas: 2000000000,
432                gas_limit: 21000,
433                calls: vec![Call {
434                    to: TxKind::Call(Address::repeat_byte(0x42)),
435                    value: U256::from(1000),
436                    input: Bytes::from(vec![1, 2, 3, 4]),
437                }],
438                nonce_key: U256::ZERO,
439                nonce: 5,
440                ..Default::default()
441            };
442
443            // Create a secp256k1 signature
444            let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
445                Signature::test_signature(),
446            ));
447
448            let aa_signed = super::super::AASigned::new_unhashed(tx, signature);
449
450            // Serialize to JSON
451            let json = serde_json::to_string_pretty(&aa_signed).unwrap();
452
453            println!("\n=== AASigned JSON Output ===");
454            println!("{json}");
455            println!("============================\n");
456
457            // Also test deserialization round-trip
458            let deserialized: super::super::AASigned = serde_json::from_str(&json).unwrap();
459            assert_eq!(aa_signed.tx(), deserialized.tx());
460        }
461    }
462}