1use crate::{
2 subblock::{PartialValidatorKey, has_sub_block_nonce_key_prefix},
3 transaction::{
4 AASigned, TempoSignature, TempoSignedAuthorization,
5 key_authorization::SignedKeyAuthorization,
6 },
7};
8use alloc::vec::Vec;
9use alloy_consensus::{SignableTransaction, Transaction, crypto::RecoveryError};
10use alloy_eips::{Typed2718, eip2930::AccessList, eip7702::SignedAuthorization};
11use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, keccak256};
12use alloy_rlp::{Buf, BufMut, Decodable, EMPTY_STRING_CODE, Encodable};
13
14pub const TEMPO_TX_TYPE_ID: u8 = 0x76;
16
17pub const FEE_PAYER_SIGNATURE_MAGIC_BYTE: u8 = 0x78;
19
20pub const SECP256K1_SIGNATURE_LENGTH: usize = 65;
22pub const P256_SIGNATURE_LENGTH: usize = 129;
23pub const MAX_WEBAUTHN_SIGNATURE_LENGTH: usize = 2048; pub const TEMPO_EXPIRING_NONCE_KEY: U256 = U256::MAX;
27
28pub const TEMPO_EXPIRING_NONCE_MAX_EXPIRY_SECS: u64 = 30;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
35#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
36#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
37#[repr(u8)]
38pub enum SignatureType {
39 Secp256k1 = 0,
40 P256 = 1,
41 WebAuthn = 2,
42}
43
44impl From<SignatureType> for u8 {
45 fn from(sig_type: SignatureType) -> Self {
46 match sig_type {
47 SignatureType::Secp256k1 => 0,
48 SignatureType::P256 => 1,
49 SignatureType::WebAuthn => 2,
50 }
51 }
52}
53
54impl alloy_rlp::Encodable for SignatureType {
55 fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
56 (*self as u8).encode(out);
57 }
58
59 fn length(&self) -> usize {
60 1
61 }
62}
63
64impl alloy_rlp::Decodable for SignatureType {
65 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
66 let byte: u8 = alloy_rlp::Decodable::decode(buf)?;
67 match byte {
68 0 => Ok(Self::Secp256k1),
69 1 => Ok(Self::P256),
70 2 => Ok(Self::WebAuthn),
71 _ => Err(alloy_rlp::Error::Custom("Invalid signature type")),
72 }
73 }
74}
75
76#[inline]
78fn rlp_header(payload_length: usize) -> alloy_rlp::Header {
79 alloy_rlp::Header {
80 list: true,
81 payload_length,
82 }
83}
84
85#[derive(Clone, Debug, PartialEq, Eq, Hash)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
88#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
89#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
90#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
91pub struct Call {
92 pub to: TxKind,
94
95 pub value: U256,
97
98 #[cfg_attr(feature = "serde", serde(flatten, with = "serde_input"))]
100 pub input: Bytes,
101}
102
103impl Call {
104 #[inline]
106 fn rlp_header(&self) -> alloy_rlp::Header {
107 let payload_length = self.to.length() + self.value.length() + self.input.length();
108 alloy_rlp::Header {
109 list: true,
110 payload_length,
111 }
112 }
113
114 fn size(&self) -> usize {
115 size_of::<Self>() + self.input.len()
116 }
117}
118
119impl Encodable for Call {
120 fn encode(&self, out: &mut dyn BufMut) {
121 self.rlp_header().encode(out);
122 self.to.encode(out);
123 self.value.encode(out);
124 self.input.encode(out);
125 }
126
127 fn length(&self) -> usize {
128 self.rlp_header().length_with_payload()
129 }
130}
131
132impl Decodable for Call {
133 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
134 let header = alloy_rlp::Header::decode(buf)?;
135 if !header.list {
136 return Err(alloy_rlp::Error::UnexpectedString);
137 }
138 let remaining = buf.len();
139
140 if header.payload_length > remaining {
141 return Err(alloy_rlp::Error::InputTooShort);
142 }
143
144 let this = Self {
145 to: Decodable::decode(buf)?,
146 value: Decodable::decode(buf)?,
147 input: Decodable::decode(buf)?,
148 };
149
150 if buf.len() + header.payload_length != remaining {
151 return Err(alloy_rlp::Error::UnexpectedLength);
152 }
153
154 Ok(this)
155 }
156}
157
158#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
169#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
170pub struct TempoTransaction {
171 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
173 pub chain_id: ChainId,
174
175 pub fee_token: Option<Address>,
177
178 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
180 pub max_priority_fee_per_gas: u128,
181
182 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
184 pub max_fee_per_gas: u128,
185
186 #[cfg_attr(
188 feature = "serde",
189 serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
190 )]
191 pub gas_limit: u64,
192
193 pub calls: Vec<Call>,
195
196 pub access_list: AccessList,
198
199 pub nonce_key: U256,
204
205 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
207 pub nonce: u64,
208
209 pub fee_payer_signature: Option<Signature>,
213
214 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity::opt"))]
216 pub valid_before: Option<u64>,
217
218 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity::opt"))]
220 pub valid_after: Option<u64>,
221
222 pub key_authorization: Option<SignedKeyAuthorization>,
228
229 #[cfg_attr(feature = "serde", serde(rename = "aaAuthorizationList"))]
231 pub tempo_authorization_list: Vec<TempoSignedAuthorization>,
232}
233
234pub fn validate_calls(calls: &[Call], has_authorization_list: bool) -> Result<(), &'static str> {
244 if calls.is_empty() {
246 return Err("calls list cannot be empty");
247 }
248
249 let mut calls_iter = calls.iter();
250
251 if let Some(call) = calls_iter.next()
253 && has_authorization_list
256 && call.to.is_create()
257 {
258 return Err("calls cannot contain CREATE when 'aa_authorization_list' is non-empty");
259 }
260
261 for call in calls_iter {
263 if call.to.is_create() {
264 return Err(
265 "only one CREATE call is allowed per transaction, and it must be the first call of the batch",
266 );
267 }
268 }
269
270 Ok(())
271}
272
273impl TempoTransaction {
274 #[doc(alias = "transaction_type")]
276 pub const fn tx_type() -> u8 {
277 TEMPO_TX_TYPE_ID
278 }
279
280 #[inline]
285 pub fn is_expiring_nonce_tx(&self) -> bool {
286 self.nonce_key == TEMPO_EXPIRING_NONCE_KEY
287 }
288
289 pub fn validate(&self) -> Result<(), &'static str> {
295 validate_calls(&self.calls, !self.tempo_authorization_list.is_empty())?;
297
298 if let Some(valid_after) = self.valid_after
300 && let Some(valid_before) = self.valid_before
301 && valid_before <= valid_after
302 {
303 return Err("valid_before must be greater than valid_after");
304 }
305
306 Ok(())
307 }
308
309 #[inline]
311 pub fn size(&self) -> usize {
312 size_of::<Self>()
313 + self.calls.iter().map(|call| call.size()).sum::<usize>()
314 + self.access_list.size()
315 + self.key_authorization.as_ref().map_or(0, |k| k.size())
316 + self
317 .tempo_authorization_list
318 .iter()
319 .map(|auth| auth.size())
320 .sum::<usize>()
321 }
322
323 pub fn into_signed(self, signature: TempoSignature) -> AASigned {
325 AASigned::new_unhashed(self, signature)
326 }
327
328 pub fn signature_hash(&self) -> B256 {
331 let mut buf = Vec::new();
332 self.encode_for_signing(&mut buf);
333 keccak256(&buf)
334 }
335
336 pub fn fee_payer_signature_hash(&self, sender: Address) -> B256 {
340 let payload_length = self.rlp_encoded_fields_length(|_| sender.length(), false);
342
343 let mut buf = Vec::with_capacity(1 + rlp_header(payload_length).length_with_payload());
344
345 buf.put_u8(FEE_PAYER_SIGNATURE_MAGIC_BYTE);
347
348 rlp_header(payload_length).encode(&mut buf);
350
351 self.rlp_encode_fields(
353 &mut buf,
354 |_, out| {
355 sender.encode(out);
357 },
358 false, );
360
361 keccak256(&buf)
362 }
363
364 pub fn recover_fee_payer(&self, sender: Address) -> Result<Address, RecoveryError> {
368 if let Some(fee_payer_signature) = &self.fee_payer_signature {
369 alloy_consensus::crypto::secp256k1::recover_signer(
370 fee_payer_signature,
371 self.fee_payer_signature_hash(sender),
372 )
373 } else {
374 Ok(sender)
375 }
376 }
377
378 fn rlp_encoded_fields_length(
382 &self,
383 signature_length: impl FnOnce(&Option<Signature>) -> usize,
384 skip_fee_token: bool,
385 ) -> usize {
386 self.chain_id.length() +
387 self.max_priority_fee_per_gas.length() +
388 self.max_fee_per_gas.length() +
389 self.gas_limit.length() +
390 self.calls.length() +
391 self.access_list.length() +
392 self.nonce_key.length() +
393 self.nonce.length() +
394 if let Some(valid_before) = self.valid_before {
395 valid_before.length()
396 } else {
397 1 } +
399 if let Some(valid_after) = self.valid_after {
401 valid_after.length()
402 } else {
403 1 } +
405 if !skip_fee_token && let Some(addr) = self.fee_token {
407 addr.length()
408 } else {
409 1 } +
411 signature_length(&self.fee_payer_signature) +
412 self.tempo_authorization_list.length() +
414 if let Some(key_auth) = &self.key_authorization {
416 key_auth.length()
417 } else {
418 0 }
420 }
421
422 fn rlp_encode_fields(
423 &self,
424 out: &mut dyn BufMut,
425 encode_signature: impl FnOnce(&Option<Signature>, &mut dyn BufMut),
426 skip_fee_token: bool,
427 ) {
428 self.chain_id.encode(out);
429 self.max_priority_fee_per_gas.encode(out);
430 self.max_fee_per_gas.encode(out);
431 self.gas_limit.encode(out);
432 self.calls.encode(out);
433 self.access_list.encode(out);
434 self.nonce_key.encode(out);
435 self.nonce.encode(out);
436
437 if let Some(valid_before) = self.valid_before {
438 valid_before.encode(out);
439 } else {
440 out.put_u8(EMPTY_STRING_CODE);
441 }
442
443 if let Some(valid_after) = self.valid_after {
444 valid_after.encode(out);
445 } else {
446 out.put_u8(EMPTY_STRING_CODE);
447 }
448
449 if !skip_fee_token && let Some(addr) = self.fee_token {
450 addr.encode(out);
451 } else {
452 out.put_u8(EMPTY_STRING_CODE);
453 }
454
455 encode_signature(&self.fee_payer_signature, out);
456
457 self.tempo_authorization_list.encode(out);
459
460 if let Some(key_auth) = &self.key_authorization {
462 key_auth.encode(out);
463 }
464 }
466
467 pub(crate) fn rlp_encoded_fields_length_default(&self) -> usize {
469 self.rlp_encoded_fields_length(
470 |signature| {
471 signature.map_or(1, |s| {
472 rlp_header(s.rlp_rs_len() + s.v().length()).length_with_payload()
473 })
474 },
475 false,
476 )
477 }
478
479 pub(crate) fn rlp_encode_fields_default(&self, out: &mut dyn BufMut) {
481 self.rlp_encode_fields(
482 out,
483 |signature, out| {
484 if let Some(signature) = signature {
485 let payload_length = signature.rlp_rs_len() + signature.v().length();
486 rlp_header(payload_length).encode(out);
487 signature.write_rlp_vrs(out, signature.v());
488 } else {
489 out.put_u8(EMPTY_STRING_CODE);
490 }
491 },
492 false,
493 )
494 }
495
496 pub(crate) fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
498 let chain_id = Decodable::decode(buf)?;
499 let max_priority_fee_per_gas = Decodable::decode(buf)?;
500 let max_fee_per_gas = Decodable::decode(buf)?;
501 let gas_limit = Decodable::decode(buf)?;
502 let calls = Decodable::decode(buf)?;
503 let access_list = Decodable::decode(buf)?;
504 let nonce_key = Decodable::decode(buf)?;
505 let nonce = Decodable::decode(buf)?;
506
507 let valid_before = if let Some(first) = buf.first() {
508 if *first == EMPTY_STRING_CODE {
509 buf.advance(1);
510 None
511 } else {
512 Some(Decodable::decode(buf)?)
513 }
514 } else {
515 return Err(alloy_rlp::Error::InputTooShort);
516 };
517
518 let valid_after = if let Some(first) = buf.first() {
519 if *first == EMPTY_STRING_CODE {
520 buf.advance(1);
521 None
522 } else {
523 Some(Decodable::decode(buf)?)
524 }
525 } else {
526 return Err(alloy_rlp::Error::InputTooShort);
527 };
528
529 let fee_token = if let Some(first) = buf.first() {
530 if *first == EMPTY_STRING_CODE {
531 buf.advance(1);
532 None
533 } else {
534 TxKind::decode(buf)?.into_to()
535 }
536 } else {
537 return Err(alloy_rlp::Error::InputTooShort);
538 };
539
540 let fee_payer_signature = if let Some(first) = buf.first() {
541 if *first == EMPTY_STRING_CODE {
542 buf.advance(1);
543 None
544 } else {
545 let header = alloy_rlp::Header::decode(buf)?;
546 if buf.len() < header.payload_length {
547 return Err(alloy_rlp::Error::InputTooShort);
548 }
549 if !header.list {
550 return Err(alloy_rlp::Error::UnexpectedString);
551 }
552 Some(Signature::decode_rlp_vrs(buf, bool::decode)?)
553 }
554 } else {
555 return Err(alloy_rlp::Error::InputTooShort);
556 };
557
558 let tempo_authorization_list = Decodable::decode(buf)?;
559
560 let key_authorization = if let Some(&first) = buf.first() {
566 if first >= 0xc0 {
568 Some(Decodable::decode(buf)?)
570 } else {
571 None
573 }
574 } else {
575 None
576 };
577
578 let tx = Self {
579 chain_id,
580 fee_token,
581 max_priority_fee_per_gas,
582 max_fee_per_gas,
583 gas_limit,
584 calls,
585 access_list,
586 nonce_key,
587 nonce,
588 fee_payer_signature,
589 valid_before,
590 valid_after,
591 key_authorization,
592 tempo_authorization_list,
593 };
594
595 tx.validate().map_err(alloy_rlp::Error::Custom)?;
597
598 Ok(tx)
599 }
600
601 pub fn has_sub_block_nonce_key_prefix(&self) -> bool {
603 has_sub_block_nonce_key_prefix(&self.nonce_key)
604 }
605
606 pub fn subblock_proposer(&self) -> Option<PartialValidatorKey> {
608 if self.has_sub_block_nonce_key_prefix() {
609 Some(PartialValidatorKey::from_slice(
610 &self.nonce_key.to_be_bytes::<32>()[1..16],
611 ))
612 } else {
613 None
614 }
615 }
616}
617
618impl Transaction for TempoTransaction {
619 #[inline]
620 fn chain_id(&self) -> Option<ChainId> {
621 Some(self.chain_id)
622 }
623
624 #[inline]
625 fn nonce(&self) -> u64 {
626 self.nonce
627 }
628
629 #[inline]
630 fn gas_limit(&self) -> u64 {
631 self.gas_limit
632 }
633
634 #[inline]
635 fn gas_price(&self) -> Option<u128> {
636 None
637 }
638
639 #[inline]
640 fn max_fee_per_gas(&self) -> u128 {
641 self.max_fee_per_gas
642 }
643
644 #[inline]
645 fn max_priority_fee_per_gas(&self) -> Option<u128> {
646 Some(self.max_priority_fee_per_gas)
647 }
648
649 #[inline]
650 fn max_fee_per_blob_gas(&self) -> Option<u128> {
651 None
652 }
653
654 #[inline]
655 fn priority_fee_or_price(&self) -> u128 {
656 self.max_priority_fee_per_gas
657 }
658
659 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
660 alloy_eips::eip1559::calc_effective_gas_price(
661 self.max_fee_per_gas,
662 self.max_priority_fee_per_gas,
663 base_fee,
664 )
665 }
666
667 #[inline]
668 fn is_dynamic_fee(&self) -> bool {
669 true
670 }
671
672 #[inline]
673 fn kind(&self) -> TxKind {
674 self.calls.first().map(|c| c.to).unwrap_or(TxKind::Create)
676 }
677
678 #[inline]
679 fn is_create(&self) -> bool {
680 self.kind().is_create()
681 }
682
683 #[inline]
684 fn value(&self) -> U256 {
685 self.calls
687 .iter()
688 .fold(U256::ZERO, |acc, call| acc.saturating_add(call.value))
689 }
690
691 #[inline]
692 fn input(&self) -> &Bytes {
693 static EMPTY_BYTES: Bytes = Bytes::new();
695 self.calls.first().map(|c| &c.input).unwrap_or(&EMPTY_BYTES)
696 }
697
698 #[inline]
699 fn access_list(&self) -> Option<&AccessList> {
700 Some(&self.access_list)
701 }
702
703 #[inline]
704 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
705 None
706 }
707
708 #[inline]
709 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
710 None
711 }
712}
713
714impl Typed2718 for TempoTransaction {
715 fn ty(&self) -> u8 {
716 TEMPO_TX_TYPE_ID
717 }
718}
719
720impl SignableTransaction<Signature> for TempoTransaction {
721 fn set_chain_id(&mut self, chain_id: ChainId) {
722 self.chain_id = chain_id;
723 }
724
725 fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
726 let skip_fee_token = self.fee_payer_signature.is_some();
728
729 out.put_u8(Self::tx_type());
731
732 let payload_length = self.rlp_encoded_fields_length(|_| 1, skip_fee_token);
734
735 rlp_header(payload_length).encode(out);
736
737 self.rlp_encode_fields(
739 out,
740 |signature, out| {
741 if signature.is_some() {
742 out.put_u8(0); } else {
744 out.put_u8(EMPTY_STRING_CODE);
745 }
746 },
747 skip_fee_token,
748 );
749 }
750
751 fn payload_len_for_signature(&self) -> usize {
752 let skip_fee_token = self.fee_payer_signature.is_some();
753 let payload_length = self.rlp_encoded_fields_length(|_| 1, skip_fee_token);
754
755 1 + rlp_header(payload_length).length_with_payload()
756 }
757}
758
759impl Encodable for TempoTransaction {
760 fn encode(&self, out: &mut dyn BufMut) {
761 let payload_length = self.rlp_encoded_fields_length_default();
763 rlp_header(payload_length).encode(out);
764 self.rlp_encode_fields_default(out);
765 }
766
767 fn length(&self) -> usize {
768 let payload_length = self.rlp_encoded_fields_length_default();
769 rlp_header(payload_length).length_with_payload()
770 }
771}
772
773impl Decodable for TempoTransaction {
774 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
775 let header = alloy_rlp::Header::decode(buf)?;
776 if !header.list {
777 return Err(alloy_rlp::Error::UnexpectedString);
778 }
779 let remaining = buf.len();
780
781 if header.payload_length > remaining {
782 return Err(alloy_rlp::Error::InputTooShort);
783 }
784
785 let mut fields_buf = &buf[..header.payload_length];
786 let this = Self::rlp_decode_fields(&mut fields_buf)?;
787
788 if !fields_buf.is_empty() {
789 return Err(alloy_rlp::Error::UnexpectedLength);
790 }
791 buf.advance(header.payload_length);
792
793 Ok(this)
794 }
795}
796
797#[cfg(feature = "reth")]
798impl reth_primitives_traits::InMemorySize for TempoTransaction {
799 fn size(&self) -> usize {
800 Self::size(self)
801 }
802}
803
804#[cfg(feature = "serde-bincode-compat")]
805impl reth_primitives_traits::serde_bincode_compat::RlpBincode for TempoTransaction {}
806
807#[cfg(any(test, feature = "arbitrary"))]
809impl<'a> arbitrary::Arbitrary<'a> for TempoTransaction {
810 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
811 let chain_id = u.arbitrary()?;
813 let fee_token = u.arbitrary()?;
814 let max_priority_fee_per_gas = u.arbitrary()?;
815 let max_fee_per_gas = u.arbitrary()?;
816 let gas_limit = u.arbitrary()?;
817
818 let mut calls: Vec<Call> = u.arbitrary()?;
821 if calls.is_empty() {
822 calls.push(Call {
823 to: u.arbitrary()?,
824 value: u.arbitrary()?,
825 input: u.arbitrary()?,
826 });
827 }
828
829 let first_is_create = calls.first().map(|c| c.to.is_create()).unwrap_or(false);
831 if first_is_create {
832 for call in calls.iter_mut().skip(1) {
834 if call.to.is_create() {
835 call.to = TxKind::Call(u.arbitrary()?);
837 }
838 }
839 } else {
840 for call in &mut calls {
842 if call.to.is_create() {
843 call.to = TxKind::Call(u.arbitrary()?);
844 }
845 }
846 }
847
848 let access_list = u.arbitrary()?;
849
850 let nonce_key = U256::ZERO;
852 let nonce = u.arbitrary()?;
853 let fee_payer_signature = u.arbitrary()?;
854
855 let valid_after: Option<u64> = u.arbitrary::<Option<u64>>()?.filter(|v| *v != 0);
860 let valid_before: Option<u64> = match valid_after {
861 Some(after) => {
862 let offset: u64 = u.int_in_range(1..=1000)?;
864 Some(after.saturating_add(offset))
865 }
866 None => {
867 u.arbitrary::<Option<u64>>()?.filter(|v| *v != 0)
869 }
870 };
871
872 Ok(Self {
873 chain_id,
874 fee_token,
875 max_priority_fee_per_gas,
876 max_fee_per_gas,
877 gas_limit,
878 calls,
879 access_list,
880 nonce_key,
881 nonce,
882 fee_payer_signature,
883 valid_before,
884 valid_after,
885 key_authorization: u.arbitrary()?,
886 tempo_authorization_list: vec![],
887 })
888 }
889}
890
891#[cfg(feature = "serde")]
892mod serde_input {
893 use alloc::borrow::Cow;
896
897 use super::*;
898 use serde::{Deserialize, Deserializer, Serialize, Serializer};
899
900 #[derive(Serialize, Deserialize)]
901 struct SerdeHelper<'a> {
902 input: Option<Cow<'a, Bytes>>,
903 data: Option<Cow<'a, Bytes>>,
904 }
905
906 pub(super) fn serialize<S>(input: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
907 where
908 S: Serializer,
909 {
910 SerdeHelper {
911 input: Some(Cow::Borrowed(input)),
912 data: None,
913 }
914 .serialize(serializer)
915 }
916
917 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
918 where
919 D: Deserializer<'de>,
920 {
921 let helper = SerdeHelper::deserialize(deserializer)?;
922 Ok(helper
923 .input
924 .or(helper.data)
925 .ok_or(serde::de::Error::missing_field(
926 "missing `input` or `data` field",
927 ))?
928 .into_owned())
929 }
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935 use crate::{
936 TempoTxEnvelope,
937 transaction::{
938 KeyAuthorization, TempoSignedAuthorization,
939 tt_signature::{
940 PrimitiveSignature, SIGNATURE_TYPE_P256, SIGNATURE_TYPE_WEBAUTHN, TempoSignature,
941 derive_p256_address,
942 },
943 },
944 };
945 use alloy_eips::{Decodable2718, Encodable2718, eip7702::Authorization};
946 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256, address, bytes, hex};
947 use alloy_rlp::{Decodable, Encodable};
948
949 #[test]
950 fn test_tempo_transaction_validation() {
951 let dummy_call = Call {
953 to: TxKind::Create,
954 value: U256::ZERO,
955 input: Bytes::new(),
956 };
957
958 let tx1 = TempoTransaction {
960 valid_before: Some(100),
961 valid_after: Some(50),
962 tempo_authorization_list: vec![],
963 calls: vec![dummy_call.clone()],
964 ..Default::default()
965 };
966 assert!(tx1.validate().is_ok());
967
968 let tx2 = TempoTransaction {
970 valid_before: Some(50),
971 valid_after: Some(100),
972 tempo_authorization_list: vec![],
973 calls: vec![dummy_call.clone()],
974 ..Default::default()
975 };
976 assert!(tx2.validate().is_err());
977
978 let tx3 = TempoTransaction {
980 valid_before: Some(100),
981 valid_after: Some(100),
982 tempo_authorization_list: vec![],
983 calls: vec![dummy_call.clone()],
984 ..Default::default()
985 };
986 assert!(tx3.validate().is_err());
987
988 let tx4 = TempoTransaction {
990 valid_before: Some(100),
991 valid_after: None,
992 tempo_authorization_list: vec![],
993 calls: vec![dummy_call],
994 ..Default::default()
995 };
996 assert!(tx4.validate().is_ok());
997
998 let tx5 = TempoTransaction {
1000 ..Default::default()
1001 };
1002 assert!(tx5.validate().is_err());
1003 }
1004
1005 #[test]
1006 fn test_tx_type() {
1007 assert_eq!(TempoTransaction::tx_type(), 0x76);
1008 assert_eq!(TEMPO_TX_TYPE_ID, 0x76);
1009 }
1010
1011 #[test]
1012 fn test_signature_type_detection() {
1013 let sig1_bytes = vec![0u8; SECP256K1_SIGNATURE_LENGTH];
1015 let sig1 = TempoSignature::from_bytes(&sig1_bytes).unwrap();
1016 assert_eq!(sig1.signature_type(), SignatureType::Secp256k1);
1017
1018 let mut sig2_bytes = vec![SIGNATURE_TYPE_P256];
1020 sig2_bytes.extend_from_slice(&[0u8; P256_SIGNATURE_LENGTH]);
1021 let sig2 = TempoSignature::from_bytes(&sig2_bytes).unwrap();
1022 assert_eq!(sig2.signature_type(), SignatureType::P256);
1023
1024 let mut sig3_bytes = vec![SIGNATURE_TYPE_WEBAUTHN];
1026 sig3_bytes.extend_from_slice(&[0u8; 200]);
1027 let sig3 = TempoSignature::from_bytes(&sig3_bytes).unwrap();
1028 assert_eq!(sig3.signature_type(), SignatureType::WebAuthn);
1029 }
1030
1031 #[test]
1032 fn test_rlp_roundtrip() {
1033 let call = Call {
1034 to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1035 value: U256::from(1000),
1036 input: Bytes::from(vec![1, 2, 3, 4]),
1037 };
1038
1039 let tx = TempoTransaction {
1040 chain_id: 1,
1041 fee_token: Some(address!("0000000000000000000000000000000000000001")),
1042 max_priority_fee_per_gas: 1000000000,
1043 max_fee_per_gas: 2000000000,
1044 gas_limit: 21000,
1045 calls: vec![call.clone()],
1046 access_list: Default::default(),
1047 nonce_key: U256::ZERO,
1048 nonce: 1,
1049 fee_payer_signature: Some(Signature::test_signature()),
1050 valid_before: Some(1000000),
1051 valid_after: Some(500000),
1052 key_authorization: None,
1053 tempo_authorization_list: vec![],
1054 };
1055
1056 let mut buf = Vec::new();
1058 tx.encode(&mut buf);
1059
1060 let decoded = TempoTransaction::decode(&mut buf.as_slice()).unwrap();
1062
1063 assert_eq!(decoded.chain_id, tx.chain_id);
1065 assert_eq!(decoded.fee_token, tx.fee_token);
1066 assert_eq!(
1067 decoded.max_priority_fee_per_gas,
1068 tx.max_priority_fee_per_gas
1069 );
1070 assert_eq!(decoded.max_fee_per_gas, tx.max_fee_per_gas);
1071 assert_eq!(decoded.gas_limit, tx.gas_limit);
1072 assert_eq!(decoded.calls.len(), 1);
1073 assert_eq!(decoded.calls[0].to, call.to);
1074 assert_eq!(decoded.calls[0].value, call.value);
1075 assert_eq!(decoded.calls[0].input, call.input);
1076 assert_eq!(decoded.nonce_key, tx.nonce_key);
1077 assert_eq!(decoded.nonce, tx.nonce);
1078 assert_eq!(decoded.valid_before, tx.valid_before);
1079 assert_eq!(decoded.valid_after, tx.valid_after);
1080 assert_eq!(decoded.fee_payer_signature, tx.fee_payer_signature);
1081 }
1082
1083 #[test]
1084 fn test_rlp_roundtrip_no_optional_fields() {
1085 let call = Call {
1086 to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1087 value: U256::from(1000),
1088 input: Bytes::new(),
1089 };
1090
1091 let tx = TempoTransaction {
1092 chain_id: 1,
1093 fee_token: None,
1094 max_priority_fee_per_gas: 1000000000,
1095 max_fee_per_gas: 2000000000,
1096 gas_limit: 21000,
1097 calls: vec![call],
1098 access_list: Default::default(),
1099 nonce_key: U256::ZERO,
1100 nonce: 1,
1101 fee_payer_signature: None,
1102 valid_before: Some(1000),
1103 valid_after: None,
1104 key_authorization: None,
1105 tempo_authorization_list: vec![],
1106 };
1107
1108 let mut buf = Vec::new();
1110 tx.encode(&mut buf);
1111
1112 let decoded = TempoTransaction::decode(&mut buf.as_slice()).unwrap();
1114
1115 assert_eq!(decoded.chain_id, tx.chain_id);
1117 assert_eq!(decoded.fee_token, None);
1118 assert_eq!(decoded.fee_payer_signature, None);
1119 assert_eq!(decoded.valid_after, None);
1120 assert_eq!(decoded.calls.len(), 1);
1121 }
1122
1123 #[test]
1124 fn test_p256_address_derivation() {
1125 let pub_key_x =
1126 hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").into();
1127 let pub_key_y =
1128 hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321").into();
1129
1130 let addr1 = derive_p256_address(&pub_key_x, &pub_key_y);
1131 let addr2 = derive_p256_address(&pub_key_x, &pub_key_y);
1132
1133 assert_eq!(addr1, addr2);
1135
1136 assert_ne!(addr1, Address::ZERO);
1138 }
1139
1140 #[test]
1141 fn test_nonce_system() {
1142 let dummy_call = Call {
1144 to: TxKind::Create,
1145 value: U256::ZERO,
1146 input: Bytes::new(),
1147 };
1148
1149 let tx1 = TempoTransaction {
1151 nonce_key: U256::ZERO,
1152 nonce: 1,
1153 calls: vec![dummy_call.clone()],
1154 ..Default::default()
1155 };
1156 assert!(tx1.validate().is_ok());
1157 assert_eq!(tx1.nonce(), 1);
1158 assert_eq!(tx1.nonce_key, U256::ZERO);
1159
1160 let tx2 = TempoTransaction {
1162 nonce_key: U256::from(1),
1163 nonce: 0,
1164 calls: vec![dummy_call.clone()],
1165 ..Default::default()
1166 };
1167 assert!(tx2.validate().is_ok());
1168 assert_eq!(tx2.nonce(), 0);
1169 assert_eq!(tx2.nonce_key, U256::from(1));
1170
1171 let tx3 = TempoTransaction {
1173 nonce_key: U256::from(42),
1174 nonce: 10,
1175 calls: vec![dummy_call.clone()],
1176 ..Default::default()
1177 };
1178 assert!(tx3.validate().is_ok());
1179 assert_eq!(tx3.nonce(), 10);
1180 assert_eq!(tx3.nonce_key, U256::from(42));
1181
1182 let tx4a = TempoTransaction {
1185 nonce_key: U256::from(1),
1186 nonce: 100,
1187 calls: vec![dummy_call.clone()],
1188 ..Default::default()
1189 };
1190 let tx4b = TempoTransaction {
1191 nonce_key: U256::from(2),
1192 nonce: 100,
1193 calls: vec![dummy_call],
1194 ..Default::default()
1195 };
1196 assert!(tx4a.validate().is_ok());
1197 assert!(tx4b.validate().is_ok());
1198 assert_eq!(tx4a.nonce(), tx4b.nonce()); assert_ne!(tx4a.nonce_key, tx4b.nonce_key); }
1201
1202 #[test]
1203 fn test_transaction_trait_impl() {
1204 let call = Call {
1205 to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1206 value: U256::from(1000),
1207 input: Bytes::new(),
1208 };
1209
1210 let tx = TempoTransaction {
1211 chain_id: 1,
1212 max_priority_fee_per_gas: 1000000000,
1213 max_fee_per_gas: 2000000000,
1214 gas_limit: 21000,
1215 calls: vec![call],
1216 ..Default::default()
1217 };
1218
1219 assert_eq!(tx.chain_id(), Some(1));
1220 assert_eq!(tx.gas_limit(), 21000);
1221 assert_eq!(tx.max_fee_per_gas(), 2000000000);
1222 assert_eq!(tx.max_priority_fee_per_gas(), Some(1000000000));
1223 assert_eq!(tx.value(), U256::from(1000));
1224 assert!(tx.is_dynamic_fee());
1225 assert!(!tx.is_create());
1226 }
1227
1228 #[test]
1229 fn test_effective_gas_price() {
1230 let dummy_call = Call {
1232 to: TxKind::Create,
1233 value: U256::ZERO,
1234 input: Bytes::new(),
1235 };
1236
1237 let tx = TempoTransaction {
1238 max_priority_fee_per_gas: 1000000000,
1239 max_fee_per_gas: 2000000000,
1240 calls: vec![dummy_call],
1241 ..Default::default()
1242 };
1243
1244 let effective1 = tx.effective_gas_price(Some(500000000));
1246 assert_eq!(effective1, 1500000000); let effective2 = tx.effective_gas_price(None);
1250 assert_eq!(effective2, 2000000000); }
1252
1253 #[test]
1254 fn test_fee_payer_commits_to_fee_token() {
1255 let sender = address!("0000000000000000000000000000000000000001");
1259 let token1 = address!("0000000000000000000000000000000000000002");
1260 let token2 = address!("0000000000000000000000000000000000000003");
1261
1262 let dummy_call = Call {
1263 to: TxKind::Create,
1264 value: U256::ZERO,
1265 input: Bytes::new(),
1266 };
1267
1268 let tx_no_token = TempoTransaction {
1270 chain_id: 1,
1271 fee_token: None,
1272 max_priority_fee_per_gas: 1000000000,
1273 max_fee_per_gas: 2000000000,
1274 gas_limit: 21000,
1275 calls: vec![dummy_call],
1276 nonce_key: U256::ZERO,
1277 nonce: 1,
1278 fee_payer_signature: Some(Signature::test_signature()),
1279 valid_before: Some(1000),
1280 valid_after: None,
1281 ..Default::default()
1282 };
1283
1284 let tx_token1 = TempoTransaction {
1286 fee_token: Some(token1),
1287 ..tx_no_token.clone()
1288 };
1289
1290 let tx_token2 = TempoTransaction {
1292 fee_token: Some(token2),
1293 ..tx_no_token.clone()
1294 };
1295
1296 let fee_payer_hash_no_token = tx_no_token.fee_payer_signature_hash(sender);
1298 let fee_payer_hash_token1 = tx_token1.fee_payer_signature_hash(sender);
1299 let fee_payer_hash_token2 = tx_token2.fee_payer_signature_hash(sender);
1300
1301 assert_ne!(
1303 fee_payer_hash_no_token, fee_payer_hash_token1,
1304 "Fee payer hash should change when fee_token changes from None to Some"
1305 );
1306 assert_ne!(
1307 fee_payer_hash_token1, fee_payer_hash_token2,
1308 "Fee payer hash should change when fee_token changes from token1 to token2"
1309 );
1310 assert_ne!(
1311 fee_payer_hash_no_token, fee_payer_hash_token2,
1312 "Fee payer hash should be different for None vs token2"
1313 );
1314
1315 let user_hash_no_token = tx_no_token.signature_hash();
1317 let user_hash_token1 = tx_token1.signature_hash();
1318 let user_hash_token2 = tx_token2.signature_hash();
1319
1320 assert_eq!(
1322 user_hash_no_token, user_hash_token1,
1323 "User hash should be the same regardless of fee_token (user skips fee_token)"
1324 );
1325 assert_eq!(
1326 user_hash_token1, user_hash_token2,
1327 "User hash should be the same regardless of fee_token (user skips fee_token)"
1328 );
1329 assert_eq!(
1330 user_hash_no_token, user_hash_token2,
1331 "User hash should be the same regardless of fee_token (user skips fee_token)"
1332 );
1333 }
1334
1335 #[test]
1336 fn test_fee_payer_signature_uses_magic_byte() {
1337 let sender = address!("0000000000000000000000000000000000000001");
1340 let dummy_call = Call {
1341 to: TxKind::Create,
1342 value: U256::ZERO,
1343 input: Bytes::new(),
1344 };
1345
1346 let tx = TempoTransaction {
1347 chain_id: 1,
1348 fee_token: None,
1349 max_priority_fee_per_gas: 1000000000,
1350 max_fee_per_gas: 2000000000,
1351 gas_limit: 21000,
1352 calls: vec![dummy_call],
1353 nonce_key: U256::ZERO,
1354 nonce: 1,
1355 fee_payer_signature: Some(Signature::test_signature()),
1356 valid_before: Some(1000),
1357 ..Default::default()
1358 };
1359
1360 let sender_hash = tx.signature_hash();
1364 let fee_payer_hash = tx.fee_payer_signature_hash(sender);
1365
1366 assert_ne!(
1368 sender_hash, fee_payer_hash,
1369 "Sender and fee payer hashes should be different (different magic bytes)"
1370 );
1371 }
1372
1373 #[test]
1374 fn test_user_signature_without_fee_payer() {
1375 let token1 = address!("0000000000000000000000000000000000000002");
1378 let token2 = address!("0000000000000000000000000000000000000003");
1379
1380 let dummy_call = Call {
1381 to: TxKind::Create,
1382 value: U256::ZERO,
1383 input: Bytes::new(),
1384 };
1385
1386 let tx_no_payer_no_token = TempoTransaction {
1388 chain_id: 1,
1389 fee_token: None,
1390 max_priority_fee_per_gas: 1000000000,
1391 max_fee_per_gas: 2000000000,
1392 gas_limit: 21000,
1393 calls: vec![dummy_call],
1394 nonce_key: U256::ZERO,
1395 nonce: 1,
1396 fee_payer_signature: None, valid_before: Some(1000),
1398 valid_after: None,
1399 tempo_authorization_list: vec![],
1400 access_list: Default::default(),
1401 key_authorization: None,
1402 };
1403
1404 let tx_no_payer_token1 = TempoTransaction {
1406 fee_token: Some(token1),
1407 ..tx_no_payer_no_token.clone()
1408 };
1409
1410 let tx_no_payer_token2 = TempoTransaction {
1412 fee_token: Some(token2),
1413 ..tx_no_payer_no_token.clone()
1414 };
1415
1416 let hash_no_token = tx_no_payer_no_token.signature_hash();
1418 let hash_token1 = tx_no_payer_token1.signature_hash();
1419 let hash_token2 = tx_no_payer_token2.signature_hash();
1420
1421 assert_ne!(
1423 hash_no_token, hash_token1,
1424 "User hash should change when fee_token changes (no fee_payer)"
1425 );
1426 assert_ne!(
1427 hash_token1, hash_token2,
1428 "User hash should change when fee_token changes (no fee_payer)"
1429 );
1430 assert_ne!(
1431 hash_no_token, hash_token2,
1432 "User hash should change when fee_token changes (no fee_payer)"
1433 );
1434 }
1435
1436 #[test]
1437 fn test_rlp_encoding_includes_fee_token() {
1438 let token = address!("0000000000000000000000000000000000000002");
1441
1442 let dummy_call = Call {
1443 to: TxKind::Create,
1444 value: U256::ZERO,
1445 input: Bytes::new(),
1446 };
1447
1448 let tx_with_token = TempoTransaction {
1450 chain_id: 1,
1451 fee_token: Some(token),
1452 max_priority_fee_per_gas: 1000000000,
1453 max_fee_per_gas: 2000000000,
1454 gas_limit: 21000,
1455 calls: vec![dummy_call],
1456 nonce_key: U256::ZERO,
1457 nonce: 1,
1458 fee_payer_signature: Some(Signature::test_signature()),
1459 valid_before: Some(1000),
1460 valid_after: None,
1461 tempo_authorization_list: vec![],
1462 access_list: Default::default(),
1463 key_authorization: None,
1464 };
1465
1466 let tx_without_token = TempoTransaction {
1468 fee_token: None,
1469 ..tx_with_token.clone()
1470 };
1471
1472 let mut buf_with = Vec::new();
1474 tx_with_token.encode(&mut buf_with);
1475
1476 let mut buf_without = Vec::new();
1477 tx_without_token.encode(&mut buf_without);
1478
1479 assert_ne!(
1481 buf_with.len(),
1482 buf_without.len(),
1483 "RLP encoding should include fee_token in the encoded data"
1484 );
1485
1486 assert!(
1488 buf_with.len() > buf_without.len(),
1489 "Transaction with fee_token should have longer encoding"
1490 );
1491
1492 let decoded_with = TempoTransaction::decode(&mut buf_with.as_slice()).unwrap();
1494 let decoded_without = TempoTransaction::decode(&mut buf_without.as_slice()).unwrap();
1495
1496 assert_eq!(decoded_with.fee_token, Some(token));
1497 assert_eq!(decoded_without.fee_token, None);
1498 }
1499
1500 #[test]
1501 fn test_signature_hash_behavior_with_and_without_fee_payer() {
1502 let token = address!("0000000000000000000000000000000000000002");
1505
1506 let dummy_call = Call {
1507 to: TxKind::Create,
1508 value: U256::ZERO,
1509 input: Bytes::new(),
1510 };
1511
1512 let tx_no_payer_no_token = TempoTransaction {
1514 chain_id: 1,
1515 fee_token: None,
1516 max_priority_fee_per_gas: 1000000000,
1517 max_fee_per_gas: 2000000000,
1518 gas_limit: 21000,
1519 calls: vec![dummy_call],
1520 nonce_key: U256::ZERO,
1521 nonce: 1,
1522 fee_payer_signature: None,
1523 valid_before: Some(1000),
1524 valid_after: None,
1525 tempo_authorization_list: vec![],
1526 access_list: Default::default(),
1527 key_authorization: None,
1528 };
1529
1530 let tx_no_payer_with_token = TempoTransaction {
1532 fee_token: Some(token),
1533 ..tx_no_payer_no_token.clone()
1534 };
1535
1536 let tx_with_payer_no_token = TempoTransaction {
1538 fee_payer_signature: Some(Signature::test_signature()),
1539 ..tx_no_payer_no_token.clone()
1540 };
1541
1542 let tx_with_payer_with_token = TempoTransaction {
1544 fee_token: Some(token),
1545 fee_payer_signature: Some(Signature::test_signature()),
1546 ..tx_no_payer_no_token.clone()
1547 };
1548
1549 let hash1 = tx_no_payer_no_token.signature_hash();
1551 let hash2 = tx_no_payer_with_token.signature_hash();
1552 let hash3 = tx_with_payer_no_token.signature_hash();
1553 let hash4 = tx_with_payer_with_token.signature_hash();
1554
1555 assert_ne!(
1557 hash1, hash2,
1558 "User hash changes with fee_token when no fee_payer"
1559 );
1560
1561 assert_eq!(
1563 hash3, hash4,
1564 "User hash ignores fee_token when fee_payer is present"
1565 );
1566
1567 assert_ne!(hash1, hash3, "User hash changes when fee_payer is added");
1570 }
1571
1572 #[test]
1573 fn test_backwards_compatibility_key_authorization() {
1574 let call = Call {
1578 to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1579 value: U256::from(1000),
1580 input: Bytes::from(vec![1, 2, 3, 4]),
1581 };
1582
1583 let tx_without = TempoTransaction {
1585 chain_id: 1,
1586 fee_token: Some(address!("0000000000000000000000000000000000000001")),
1587 max_priority_fee_per_gas: 1000000000,
1588 max_fee_per_gas: 2000000000,
1589 gas_limit: 21000,
1590 calls: vec![call],
1591 access_list: Default::default(),
1592 nonce_key: U256::ZERO,
1593 nonce: 1,
1594 fee_payer_signature: Some(Signature::test_signature()),
1595 valid_before: Some(1000000),
1596 valid_after: Some(500000),
1597 key_authorization: None, tempo_authorization_list: vec![],
1599 };
1600
1601 let mut buf_without = Vec::new();
1603 tx_without.encode(&mut buf_without);
1604
1605 let decoded_without = TempoTransaction::decode(&mut buf_without.as_slice()).unwrap();
1607
1608 assert_eq!(decoded_without.key_authorization, None);
1610 assert_eq!(decoded_without.chain_id, tx_without.chain_id);
1611 assert_eq!(decoded_without.calls.len(), tx_without.calls.len());
1612
1613 let key_auth = KeyAuthorization {
1615 chain_id: 1, key_type: SignatureType::Secp256k1,
1617 expiry: Some(1234567890),
1618 limits: Some(vec![crate::transaction::TokenLimit {
1619 token: address!("0000000000000000000000000000000000000003"),
1620 limit: U256::from(10000),
1621 }]),
1622 key_id: address!("0000000000000000000000000000000000000004"),
1623 }
1624 .into_signed(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1625
1626 let tx_with = TempoTransaction {
1627 key_authorization: Some(key_auth.clone()),
1628 ..tx_without.clone()
1629 };
1630
1631 let mut buf_with = Vec::new();
1633 tx_with.encode(&mut buf_with);
1634
1635 let decoded_with = TempoTransaction::decode(&mut buf_with.as_slice()).unwrap();
1637
1638 assert!(decoded_with.key_authorization.is_some());
1640 let decoded_key_auth = decoded_with.key_authorization.unwrap();
1641 assert_eq!(decoded_key_auth.key_type, key_auth.key_type);
1642 assert_eq!(decoded_key_auth.expiry, key_auth.expiry);
1643 assert_eq!(
1644 decoded_key_auth.limits.as_ref().map(|l| l.len()),
1645 key_auth.limits.as_ref().map(|l| l.len())
1646 );
1647 assert_eq!(decoded_key_auth.key_id, key_auth.key_id);
1648
1649 assert!(
1652 buf_without.len() < buf_with.len(),
1653 "Transaction without key_authorization should have shorter encoding"
1654 );
1655
1656 let decoded_old_format = TempoTransaction::decode(&mut buf_without.as_slice()).unwrap();
1660 assert_eq!(decoded_old_format.key_authorization, None);
1661 }
1662
1663 #[test]
1664 fn test_aa_signed_rlp_direct() {
1665 let call = Call {
1667 to: TxKind::Create,
1668 value: U256::ZERO,
1669 input: Bytes::new(),
1670 };
1671
1672 let tx = TempoTransaction {
1673 chain_id: 0,
1674 fee_token: None,
1675 max_priority_fee_per_gas: 0,
1676 max_fee_per_gas: 0,
1677 gas_limit: 0,
1678 calls: vec![call],
1679 access_list: Default::default(),
1680 nonce_key: U256::ZERO,
1681 nonce: 0,
1682 fee_payer_signature: None,
1683 valid_before: None,
1684 valid_after: None,
1685 key_authorization: None, tempo_authorization_list: vec![],
1687 };
1688
1689 let signature =
1690 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1691 let signed = AASigned::new_unhashed(tx, signature);
1692
1693 let mut buf = Vec::new();
1695 signed.rlp_encode(&mut buf);
1696
1697 let decoded =
1699 AASigned::rlp_decode(&mut buf.as_slice()).expect("Should decode AASigned RLP");
1700 assert_eq!(decoded.tx().key_authorization, None);
1701 }
1702
1703 #[test]
1704 fn test_tempo_transaction_envelope_roundtrip_without_key_auth() {
1705 let call = Call {
1707 to: TxKind::Create,
1708 value: U256::ZERO,
1709 input: Bytes::new(),
1710 };
1711
1712 let tx = TempoTransaction {
1713 chain_id: 0,
1714 fee_token: None,
1715 max_priority_fee_per_gas: 0,
1716 max_fee_per_gas: 0,
1717 gas_limit: 0,
1718 calls: vec![call],
1719 access_list: Default::default(),
1720 nonce_key: U256::ZERO,
1721 nonce: 0,
1722 fee_payer_signature: None,
1723 valid_before: None,
1724 valid_after: None,
1725 key_authorization: None, tempo_authorization_list: vec![],
1727 };
1728
1729 let signature =
1730 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1731 let signed = AASigned::new_unhashed(tx, signature);
1732 let envelope = TempoTxEnvelope::AA(signed);
1733
1734 let mut buf = Vec::new();
1736 envelope.encode_2718(&mut buf);
1737 let decoded = TempoTxEnvelope::decode_2718(&mut buf.as_slice())
1738 .expect("Should decode envelope successfully");
1739
1740 if let TempoTxEnvelope::AA(aa_signed) = decoded {
1742 assert_eq!(aa_signed.tx().key_authorization, None);
1743 assert_eq!(aa_signed.tx().calls.len(), 1);
1744 assert_eq!(aa_signed.tx().chain_id, 0);
1745 } else {
1746 panic!("Expected AA envelope");
1747 }
1748 }
1749
1750 #[test]
1751 fn test_call_decode_rejects_malformed_rlp() {
1752 let call = Call {
1754 to: TxKind::Call(Address::random()),
1755 value: U256::random(),
1756 input: Bytes::from(vec![1, 2, 3, 4]),
1757 };
1758
1759 let mut buf = Vec::new();
1761 call.encode(&mut buf);
1762
1763 let original_len = buf.len();
1766 buf.truncate(original_len - 2); let result = Call::decode(&mut buf.as_slice());
1769 assert!(
1770 result.is_err(),
1771 "Decoding should fail when header length doesn't match"
1772 );
1773 assert!(matches!(
1775 result.unwrap_err(),
1776 alloy_rlp::Error::InputTooShort | alloy_rlp::Error::UnexpectedLength
1777 ));
1778 }
1779
1780 #[test]
1781 fn test_tempo_transaction_decode_rejects_malformed_rlp() {
1782 let call = Call {
1784 to: TxKind::Call(Address::random()),
1785 value: U256::random(),
1786 input: Bytes::from(vec![1, 2, 3, 4]),
1787 };
1788
1789 let tx = TempoTransaction {
1790 chain_id: 1,
1791 fee_token: Some(Address::random()),
1792 max_priority_fee_per_gas: 1000000000,
1793 max_fee_per_gas: 2000000000,
1794 gas_limit: 21000,
1795 calls: vec![call],
1796 access_list: Default::default(),
1797 nonce_key: U256::ZERO,
1798 nonce: 1,
1799 fee_payer_signature: Some(Signature::test_signature()),
1800 valid_before: Some(1000000),
1801 valid_after: Some(500000),
1802 key_authorization: None,
1803 tempo_authorization_list: vec![],
1804 };
1805
1806 let mut buf = Vec::new();
1808 tx.encode(&mut buf);
1809
1810 let original_len = buf.len();
1812 buf.truncate(original_len - 5); let result = TempoTransaction::decode(&mut buf.as_slice());
1815 assert!(
1816 result.is_err(),
1817 "Decoding should fail when data is truncated"
1818 );
1819 assert!(matches!(
1821 result.unwrap_err(),
1822 alloy_rlp::Error::InputTooShort | alloy_rlp::Error::UnexpectedLength
1823 ));
1824 }
1825
1826 #[test]
1827 #[cfg(feature = "serde")]
1828 fn call_serde() {
1829 let call: Call = serde_json::from_str(
1830 r#"{"to":"0x0000000000000000000000000000000000000002","value":"0x1","input":"0x1234"}"#,
1831 )
1832 .unwrap();
1833 assert_eq!(
1834 call.to,
1835 TxKind::Call(address!("0000000000000000000000000000000000000002"))
1836 );
1837 assert_eq!(call.value, U256::ONE);
1838 assert_eq!(call.input, bytes!("0x1234"));
1839 }
1840
1841 #[test]
1842 fn test_create_must_be_first_call() {
1843 let create_call = Call {
1844 to: TxKind::Create,
1845 value: U256::ZERO,
1846 input: Bytes::new(),
1847 };
1848 let call_call = Call {
1849 to: TxKind::Call(Address::random()),
1850 value: U256::ZERO,
1851 input: Bytes::new(),
1852 };
1853
1854 let tx_valid = TempoTransaction {
1856 calls: vec![create_call.clone(), call_call.clone()],
1857 ..Default::default()
1858 };
1859 assert!(tx_valid.validate().is_ok());
1860
1861 let tx_invalid = TempoTransaction {
1863 calls: vec![call_call, create_call],
1864 ..Default::default()
1865 };
1866 assert!(tx_invalid.validate().is_err());
1867 assert!(tx_invalid.validate().unwrap_err().contains("first call"));
1868 }
1869
1870 #[test]
1871 fn test_only_one_create_allowed() {
1872 let create_call = Call {
1873 to: TxKind::Create,
1874 value: U256::ZERO,
1875 input: Bytes::new(),
1876 };
1877
1878 let tx_valid = TempoTransaction {
1880 calls: vec![create_call.clone()],
1881 ..Default::default()
1882 };
1883 assert!(tx_valid.validate().is_ok());
1884
1885 let tx_invalid = TempoTransaction {
1887 calls: vec![create_call.clone(), create_call],
1888 ..Default::default()
1889 };
1890 assert!(tx_invalid.validate().is_err());
1891 assert!(
1892 tx_invalid
1893 .validate()
1894 .unwrap_err()
1895 .contains("only one CREATE")
1896 );
1897 }
1898
1899 #[test]
1900 fn test_create_forbidden_with_auth_list() {
1901 let create_call = Call {
1902 to: TxKind::Create,
1903 value: U256::ZERO,
1904 input: Bytes::new(),
1905 };
1906
1907 let signed_auth = TempoSignedAuthorization::new_unchecked(
1908 Authorization {
1909 chain_id: U256::ONE,
1910 address: Address::random(),
1911 nonce: 1,
1912 },
1913 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1914 );
1915
1916 let tx = TempoTransaction {
1918 calls: vec![create_call],
1919 tempo_authorization_list: vec![signed_auth],
1920 ..Default::default()
1921 };
1922
1923 let result = tx.validate();
1924 assert!(result.is_err());
1925 assert!(result.unwrap_err().contains("aa_authorization_list"));
1926 }
1927
1928 #[test]
1929 fn test_create_validation_allows_call_only_batch() {
1930 let call1 = Call {
1932 to: TxKind::Call(Address::random()),
1933 value: U256::ZERO,
1934 input: Bytes::new(),
1935 };
1936 let call2 = Call {
1937 to: TxKind::Call(Address::random()),
1938 value: U256::random(),
1939 input: Bytes::from(vec![1, 2, 3]),
1940 };
1941
1942 let tx = TempoTransaction {
1943 calls: vec![call1, call2],
1944 ..Default::default()
1945 };
1946 assert!(tx.validate().is_ok());
1947 }
1948
1949 #[test]
1950 fn test_value_saturates_on_overflow() {
1951 let call1 = Call {
1952 to: TxKind::Call(Address::ZERO),
1953 value: U256::MAX,
1954 input: Bytes::new(),
1955 };
1956 let call2 = Call {
1957 to: TxKind::Call(Address::ZERO),
1958 value: U256::from(1),
1959 input: Bytes::new(),
1960 };
1961
1962 let tx = TempoTransaction {
1963 calls: vec![call1, call2],
1964 ..Default::default()
1965 };
1966
1967 assert_eq!(tx.value(), U256::MAX);
1968 }
1969
1970 #[test]
1971 fn test_validate_does_not_check_expiring_nonce_constraints() {
1972 let dummy_call = Call {
1973 to: TxKind::Call(Address::ZERO),
1974 value: U256::ZERO,
1975 input: Bytes::new(),
1976 };
1977
1978 let tx_with_nonzero_nonce = TempoTransaction {
1981 nonce_key: TEMPO_EXPIRING_NONCE_KEY,
1982 nonce: 42,
1983 valid_before: None,
1984 calls: vec![dummy_call.clone()],
1985 ..Default::default()
1986 };
1987 assert!(
1988 tx_with_nonzero_nonce.validate().is_ok(),
1989 "validate() should not enforce expiring nonce constraints (hardfork-dependent)"
1990 );
1991
1992 let tx_without_valid_before = TempoTransaction {
1994 nonce_key: TEMPO_EXPIRING_NONCE_KEY,
1995 nonce: 0,
1996 valid_before: None,
1997 calls: vec![dummy_call.clone()],
1998 ..Default::default()
1999 };
2000 assert!(
2001 tx_without_valid_before.validate().is_ok(),
2002 "validate() should not enforce expiring nonce constraints (hardfork-dependent)"
2003 );
2004
2005 let valid_expiring_tx = TempoTransaction {
2007 nonce_key: TEMPO_EXPIRING_NONCE_KEY,
2008 nonce: 0,
2009 valid_before: Some(1000),
2010 calls: vec![dummy_call],
2011 ..Default::default()
2012 };
2013 assert!(valid_expiring_tx.validate().is_ok());
2014 }
2015}