tempo_primitives/transaction/
tt_signed.rs1use super::{
2 tempo_transaction::{TEMPO_TX_TYPE_ID, TempoTransaction},
3 tt_signature::TempoSignature,
4};
5use alloc::vec::Vec;
6use alloy_consensus::{SignableTransaction, Transaction, transaction::TxHashRef};
7use alloy_eips::{
8 Decodable2718, Encodable2718, Typed2718,
9 eip2718::{Eip2718Error, Eip2718Result},
10 eip2930::AccessList,
11 eip7702::SignedAuthorization,
12};
13use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
14use alloy_rlp::{BufMut, Decodable, Encodable};
15use core::{
16 fmt::Debug,
17 hash::{Hash, Hasher},
18};
19
20#[cfg(not(feature = "std"))]
21use once_cell::race::OnceBox as OnceLock;
22#[cfg(feature = "std")]
23use std::sync::OnceLock;
24
25#[derive(Clone, Debug)]
30pub struct AASigned {
31 tx: TempoTransaction,
33 signature: TempoSignature,
35 #[doc(alias = "tx_hash", alias = "transaction_hash")]
37 hash: OnceLock<B256>,
38}
39
40impl AASigned {
41 pub fn new_unchecked(tx: TempoTransaction, signature: TempoSignature, hash: B256) -> Self {
44 let value = OnceLock::new();
45 #[allow(clippy::useless_conversion)]
46 value.get_or_init(|| hash.into());
47 Self {
48 tx,
49 signature,
50 hash: value,
51 }
52 }
53
54 pub const fn new_unhashed(tx: TempoTransaction, signature: TempoSignature) -> Self {
57 Self {
58 tx,
59 signature,
60 hash: OnceLock::new(),
61 }
62 }
63
64 #[doc(alias = "transaction")]
66 pub const fn tx(&self) -> &TempoTransaction {
67 &self.tx
68 }
69
70 pub const fn tx_mut(&mut self) -> &mut TempoTransaction {
72 &mut self.tx
73 }
74
75 pub const fn signature(&self) -> &TempoSignature {
77 &self.signature
78 }
79
80 pub fn strip_signature(self) -> TempoTransaction {
82 self.tx
83 }
84
85 #[doc(alias = "tx_hash", alias = "transaction_hash")]
87 pub fn hash(&self) -> &B256 {
88 #[allow(clippy::useless_conversion)]
89 self.hash.get_or_init(|| self.compute_hash().into())
90 }
91
92 fn compute_hash(&self) -> B256 {
94 let mut buf = Vec::new();
95 self.eip2718_encode(&mut buf);
96 alloy_primitives::keccak256(&buf)
97 }
98
99 pub fn signature_hash(&self) -> B256 {
101 self.tx.signature_hash()
102 }
103
104 pub fn expiring_nonce_hash(&self, sender: Address) -> B256 {
116 let mut buf =
117 Vec::with_capacity(self.tx.payload_len_for_signature() + sender.as_slice().len());
118 self.tx.encode_for_signing(&mut buf);
119 buf.extend_from_slice(sender.as_slice());
120 alloy_primitives::keccak256(&buf)
121 }
122
123 #[inline]
126 fn rlp_header(&self) -> alloy_rlp::Header {
127 let payload_length = self.tx.rlp_encoded_fields_length_default() + self.signature.length();
128 alloy_rlp::Header {
129 list: true,
130 payload_length,
131 }
132 }
133
134 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
136 self.rlp_header().encode(out);
138
139 self.tx.rlp_encode_fields_default(out);
141
142 self.signature.encode(out);
144 }
145
146 pub fn into_parts(self) -> (TempoTransaction, TempoSignature, B256) {
148 let hash = *self.hash();
149 (self.tx, self.signature, hash)
150 }
151
152 fn rlp_encoded_length(&self) -> usize {
154 self.rlp_header().length_with_payload()
155 }
156
157 fn eip2718_encoded_length(&self) -> usize {
159 1 + self.rlp_encoded_length()
160 }
161
162 pub fn eip2718_encode(&self, out: &mut dyn BufMut) {
164 out.put_u8(TEMPO_TX_TYPE_ID);
166 self.rlp_encode(out);
168 }
169
170 pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
172 let header = alloy_rlp::Header::decode(buf)?;
173 if !header.list {
174 return Err(alloy_rlp::Error::UnexpectedString);
175 }
176 let remaining = buf.len();
177
178 if header.payload_length > remaining {
179 return Err(alloy_rlp::Error::InputTooShort);
180 }
181
182 let tx = TempoTransaction::rlp_decode_fields(buf)?;
184
185 let sig_bytes: Bytes = Decodable::decode(buf)?;
187
188 let consumed = remaining - buf.len();
190 if consumed != header.payload_length {
191 return Err(alloy_rlp::Error::UnexpectedLength);
192 }
193
194 let signature = TempoSignature::from_bytes(&sig_bytes).map_err(alloy_rlp::Error::Custom)?;
196
197 Ok(Self::new_unhashed(tx, signature))
198 }
199}
200
201impl TxHashRef for AASigned {
202 fn tx_hash(&self) -> &B256 {
203 self.hash()
204 }
205}
206
207impl Typed2718 for AASigned {
208 fn ty(&self) -> u8 {
209 TEMPO_TX_TYPE_ID
210 }
211}
212
213impl Transaction for AASigned {
214 #[inline]
215 fn chain_id(&self) -> Option<u64> {
216 self.tx.chain_id()
217 }
218
219 #[inline]
220 fn nonce(&self) -> u64 {
221 self.tx.nonce()
222 }
223
224 #[inline]
225 fn gas_limit(&self) -> u64 {
226 self.tx.gas_limit()
227 }
228
229 #[inline]
230 fn gas_price(&self) -> Option<u128> {
231 self.tx.gas_price()
232 }
233
234 #[inline]
235 fn max_fee_per_gas(&self) -> u128 {
236 self.tx.max_fee_per_gas()
237 }
238
239 #[inline]
240 fn max_priority_fee_per_gas(&self) -> Option<u128> {
241 self.tx.max_priority_fee_per_gas()
242 }
243
244 #[inline]
245 fn max_fee_per_blob_gas(&self) -> Option<u128> {
246 None
247 }
248
249 #[inline]
250 fn priority_fee_or_price(&self) -> u128 {
251 self.tx.priority_fee_or_price()
252 }
253
254 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
255 self.tx.effective_gas_price(base_fee)
256 }
257
258 #[inline]
259 fn is_dynamic_fee(&self) -> bool {
260 true
261 }
262
263 #[inline]
264 fn kind(&self) -> TxKind {
265 self.tx
267 .calls
268 .first()
269 .map(|c| c.to)
270 .unwrap_or(TxKind::Create)
271 }
272
273 #[inline]
274 fn is_create(&self) -> bool {
275 self.kind().is_create()
276 }
277
278 #[inline]
279 fn value(&self) -> U256 {
280 self.tx
282 .calls
283 .iter()
284 .fold(U256::ZERO, |acc, call| acc + call.value)
285 }
286
287 #[inline]
288 fn input(&self) -> &Bytes {
289 static EMPTY_BYTES: Bytes = Bytes::new();
291 self.tx
292 .calls
293 .first()
294 .map(|c| &c.input)
295 .unwrap_or(&EMPTY_BYTES)
296 }
297
298 #[inline]
299 fn access_list(&self) -> Option<&AccessList> {
300 Some(&self.tx.access_list)
301 }
302
303 #[inline]
304 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
305 None
306 }
307
308 #[inline]
309 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
310 None
311 }
312}
313
314impl Hash for AASigned {
315 fn hash<H: Hasher>(&self, state: &mut H) {
316 self.hash().hash(state);
317 self.tx.hash(state);
318 self.signature.hash(state);
319 }
320}
321
322impl PartialEq for AASigned {
323 fn eq(&self, other: &Self) -> bool {
324 self.hash() == other.hash() && self.tx == other.tx && self.signature == other.signature
325 }
326}
327
328impl Eq for AASigned {}
329
330#[cfg(feature = "reth")]
331impl reth_primitives_traits::InMemorySize for AASigned {
332 fn size(&self) -> usize {
333 size_of::<Self>() + self.tx.size() + self.signature.size()
334 }
335}
336
337impl alloy_consensus::transaction::SignerRecoverable for AASigned {
338 fn recover_signer(
339 &self,
340 ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
341 let sig_hash = self.signature_hash();
342 self.signature.recover_signer(&sig_hash)
343 }
344
345 fn recover_signer_unchecked(
346 &self,
347 ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
348 self.recover_signer()
351 }
352}
353
354impl Encodable2718 for AASigned {
355 fn encode_2718_len(&self) -> usize {
356 self.eip2718_encoded_length()
357 }
358
359 fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
360 self.eip2718_encode(out)
361 }
362
363 fn trie_hash(&self) -> B256 {
364 *self.hash()
365 }
366}
367
368impl Decodable2718 for AASigned {
369 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
370 if ty != TEMPO_TX_TYPE_ID {
371 return Err(Eip2718Error::UnexpectedType(ty));
372 }
373 Self::rlp_decode(buf).map_err(Into::into)
374 }
375
376 fn fallback_decode(_: &mut &[u8]) -> Eip2718Result<Self> {
377 Err(Eip2718Error::UnexpectedType(0))
378 }
379}
380
381#[cfg(any(test, feature = "arbitrary"))]
382impl<'a> arbitrary::Arbitrary<'a> for AASigned {
383 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
384 let tx = TempoTransaction::arbitrary(u)?;
385 let signature = TempoSignature::arbitrary(u)?;
386 Ok(Self::new_unhashed(tx, signature))
387 }
388}
389
390#[cfg(feature = "serde")]
391mod serde_impl {
392 use super::*;
393 use alloc::borrow::Cow;
394 use serde::{Deserialize, Deserializer, Serialize, Serializer};
395
396 #[derive(Serialize, Deserialize)]
397 struct AASignedHelper<'a> {
398 #[serde(flatten)]
399 tx: Cow<'a, TempoTransaction>,
400 signature: Cow<'a, TempoSignature>,
401 hash: Cow<'a, B256>,
402 }
403
404 impl Serialize for super::AASigned {
405 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
406 where
407 S: Serializer,
408 {
409 if let TempoSignature::Keychain(keychain_sig) = &self.signature {
410 let _ = keychain_sig.key_id(&self.signature_hash());
412 }
413 AASignedHelper {
414 tx: Cow::Borrowed(&self.tx),
415 signature: Cow::Borrowed(&self.signature),
416 hash: Cow::Borrowed(self.hash()),
417 }
418 .serialize(serializer)
419 }
420 }
421
422 impl<'de> Deserialize<'de> for super::AASigned {
423 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
424 where
425 D: Deserializer<'de>,
426 {
427 AASignedHelper::deserialize(deserializer).map(|value| {
428 Self::new_unchecked(
429 value.tx.into_owned(),
430 value.signature.into_owned(),
431 value.hash.into_owned(),
432 )
433 })
434 }
435 }
436
437 #[cfg(test)]
438 mod tests {
439 use crate::transaction::{
440 tempo_transaction::{Call, TempoTransaction},
441 tt_signature::{PrimitiveSignature, TempoSignature},
442 };
443 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
444
445 #[test]
446 fn test_serde_output() {
447 let tx = TempoTransaction {
449 chain_id: 1337,
450 fee_token: None,
451 max_priority_fee_per_gas: 1000000000,
452 max_fee_per_gas: 2000000000,
453 gas_limit: 21000,
454 calls: vec![Call {
455 to: TxKind::Call(Address::repeat_byte(0x42)),
456 value: U256::from(1000),
457 input: Bytes::from(vec![1, 2, 3, 4]),
458 }],
459 nonce_key: U256::ZERO,
460 nonce: 5,
461 ..Default::default()
462 };
463
464 let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
466 Signature::test_signature(),
467 ));
468
469 let aa_signed = super::super::AASigned::new_unhashed(tx, signature);
470
471 let json = serde_json::to_string_pretty(&aa_signed).unwrap();
473
474 println!("\n=== AASigned JSON Output ===");
475 println!("{json}");
476 println!("============================\n");
477
478 let deserialized: super::super::AASigned = serde_json::from_str(&json).unwrap();
480 assert_eq!(aa_signed.tx(), deserialized.tx());
481 }
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use crate::transaction::{
489 tempo_transaction::Call,
490 tt_authorization::tests::{generate_secp256k1_keypair, sign_hash},
491 tt_signature::PrimitiveSignature,
492 };
493 use alloy_consensus::transaction::SignerRecoverable;
494 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
495
496 fn make_tx() -> TempoTransaction {
497 TempoTransaction {
498 chain_id: 1,
499 gas_limit: 21000,
500 calls: vec![Call {
501 to: TxKind::Call(Address::repeat_byte(0x42)),
502 value: U256::ZERO,
503 input: Bytes::new(),
504 }],
505 ..Default::default()
506 }
507 }
508
509 #[test]
510 fn test_hash_and_transaction_trait() {
511 let tx = make_tx();
512 let sig =
513 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
514
515 let signed = AASigned::new_unhashed(tx.clone(), sig.clone());
517
518 let hash1 = *signed.hash();
520 let hash2 = *signed.hash();
522 assert_eq!(hash1, hash2, "hash should be deterministic");
523 assert_ne!(hash1, B256::ZERO);
524
525 let known_hash = B256::random();
527 let signed_unchecked = AASigned::new_unchecked(tx.clone(), sig.clone(), known_hash);
528 assert_eq!(
529 *signed_unchecked.hash(),
530 known_hash,
531 "new_unchecked should use provided hash"
532 );
533
534 let signed_for_parts = AASigned::new_unhashed(tx.clone(), sig.clone());
536 let (returned_tx, returned_sig, returned_hash) = signed_for_parts.into_parts();
537 assert_eq!(returned_tx, tx);
538 assert_eq!(returned_sig, sig);
539 assert_eq!(returned_hash, hash1);
540 }
541
542 #[test]
543 fn test_rlp_encode_decode_roundtrip() {
544 use alloy_eips::eip2718::Encodable2718;
545
546 let tx = make_tx();
547 let sig =
548 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
549 let signed = AASigned::new_unhashed(tx, sig);
550
551 let mut buf = Vec::new();
553 signed.rlp_encode(&mut buf);
554
555 let decoded = AASigned::rlp_decode(&mut buf.as_slice()).unwrap();
557 assert_eq!(decoded.tx(), signed.tx());
558 assert_eq!(decoded.signature(), signed.signature());
559
560 let mut eip_buf = Vec::new();
562 signed.eip2718_encode(&mut eip_buf);
563 assert_eq!(eip_buf[0], TEMPO_TX_TYPE_ID);
564
565 let decoded_2718 =
566 AASigned::typed_decode(TEMPO_TX_TYPE_ID, &mut eip_buf[1..].as_ref()).unwrap();
567 assert_eq!(decoded_2718.tx(), signed.tx());
568
569 assert_eq!(signed.trie_hash(), *signed.hash());
571
572 let fallback_result = AASigned::fallback_decode(&mut [].as_ref());
574 assert!(fallback_result.is_err());
575
576 assert_eq!(signed.encode_2718_len(), eip_buf.len());
578 }
579
580 #[test]
581 fn test_rlp_decode_error_paths() {
582 let result = AASigned::rlp_decode(&mut [].as_ref());
584 assert!(result.is_err());
585
586 let result = AASigned::rlp_decode(&mut [0x80].as_ref());
588 assert!(result.is_err());
589
590 let result = AASigned::rlp_decode(&mut [0xc1, 0x00].as_ref()); assert!(result.is_err());
593
594 let result = AASigned::typed_decode(0x00, &mut [].as_ref());
596 assert!(result.is_err());
597 }
598
599 #[test]
600 fn test_expiring_nonce_hash_invariant_to_fee_payer() {
601 let sender = Address::repeat_byte(0x01);
602
603 let make_sponsored_tx = |fee_payer_sig: Signature| -> TempoTransaction {
604 TempoTransaction {
605 chain_id: 1,
606 gas_limit: 1_000_000,
607 nonce_key: U256::MAX, nonce: 0,
609 fee_token: Some(Address::repeat_byte(0xFE)),
610 fee_payer_signature: Some(fee_payer_sig),
611 valid_before: Some(100),
612 calls: vec![Call {
613 to: TxKind::Call(Address::repeat_byte(0x42)),
614 value: U256::ZERO,
615 input: Bytes::new(),
616 }],
617 ..Default::default()
618 }
619 };
620
621 let sig =
622 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
623
624 let tx1 = make_sponsored_tx(Signature::new(U256::from(1), U256::from(2), false));
626 let tx2 = make_sponsored_tx(Signature::new(U256::from(3), U256::from(4), true));
627
628 let signed1 = AASigned::new_unhashed(tx1, sig.clone());
629 let signed2 = AASigned::new_unhashed(tx2, sig);
630
631 assert_ne!(signed1.hash(), signed2.hash(), "tx hashes must differ");
633
634 let hash1 = signed1.expiring_nonce_hash(sender);
636 let hash2 = signed2.expiring_nonce_hash(sender);
637 assert_eq!(
638 hash1, hash2,
639 "expiring_nonce_hash must be invariant to fee payer signature changes"
640 );
641 assert_ne!(hash1, B256::ZERO);
642 }
643
644 #[test]
645 fn test_expiring_nonce_hash_unique_per_sender() {
646 let tx = TempoTransaction {
647 chain_id: 1,
648 gas_limit: 1_000_000,
649 nonce_key: U256::MAX,
650 nonce: 0,
651 valid_before: Some(100),
652 calls: vec![Call {
653 to: TxKind::Call(Address::repeat_byte(0x42)),
654 value: U256::ZERO,
655 input: Bytes::new(),
656 }],
657 ..Default::default()
658 };
659 let sig =
660 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
661 let signed = AASigned::new_unhashed(tx, sig);
662
663 let sender_a = Address::repeat_byte(0x01);
664 let sender_b = Address::repeat_byte(0x02);
665
666 assert_ne!(
667 signed.expiring_nonce_hash(sender_a),
668 signed.expiring_nonce_hash(sender_b),
669 "different senders must produce different expiring_nonce_hash"
670 );
671 }
672
673 #[test]
674 fn test_expiring_nonce_hash_deterministic() {
675 let tx = make_tx();
676 let sig =
677 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
678 let signed = AASigned::new_unhashed(tx, sig);
679 let sender = Address::repeat_byte(0xAB);
680
681 let h1 = signed.expiring_nonce_hash(sender);
682 let h2 = signed.expiring_nonce_hash(sender);
683 assert_eq!(h1, h2, "expiring_nonce_hash must be deterministic");
684 }
685
686 #[test]
687 fn test_recover_signer() {
688 let (signing_key, expected_address) = generate_secp256k1_keypair();
689
690 let tx = make_tx();
691
692 let placeholder =
694 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
695 let temp_signed = AASigned::new_unhashed(tx.clone(), placeholder);
696 let sig_hash = temp_signed.signature_hash();
697
698 let signature = sign_hash(&signing_key, &sig_hash);
700 let signed = AASigned::new_unhashed(tx.clone(), signature);
701
702 let recovered = signed.recover_signer().unwrap();
704 assert_eq!(recovered, expected_address);
705
706 let recovered_unchecked = signed.recover_signer_unchecked().unwrap();
708 assert_eq!(recovered_unchecked, expected_address);
709
710 let wrong_sig = sign_hash(&signing_key, &B256::random());
712 let bad_signed = AASigned::new_unhashed(tx, wrong_sig);
713 let bad_recovered = bad_signed.recover_signer().unwrap();
714 assert_ne!(bad_recovered, expected_address);
715 }
716}