1use crate::TempoInvalidTransaction;
2use alloy_consensus::{EthereumTxEnvelope, TxEip4844, Typed2718, crypto::secp256k1};
3use alloy_evm::{FromRecoveredTx, FromTxWithEncoded, IntoTxEnv};
4use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
5use reth_evm::TransactionEnv;
6use revm::context::{
7 Transaction, TxEnv,
8 either::Either,
9 result::InvalidTransaction,
10 transaction::{
11 AccessList, AccessListItem, RecoveredAuthority, RecoveredAuthorization, SignedAuthorization,
12 },
13};
14use tempo_primitives::{
15 AASigned, TempoSignature, TempoTransaction, TempoTxEnvelope,
16 transaction::{
17 Call, RecoveredTempoAuthorization, SignedKeyAuthorization, calc_gas_balance_spending,
18 },
19};
20
21#[derive(Debug, Clone, Default)]
23pub struct TempoBatchCallEnv {
24 pub signature: TempoSignature,
26
27 pub valid_before: Option<u64>,
29
30 pub valid_after: Option<u64>,
32
33 pub aa_calls: Vec<Call>,
35
36 pub tempo_authorization_list: Vec<RecoveredTempoAuthorization>,
41
42 pub nonce_key: U256,
44
45 pub subblock_transaction: bool,
47
48 pub key_authorization: Option<SignedKeyAuthorization>,
50
51 pub signature_hash: B256,
53
54 pub tx_hash: B256,
56
57 pub expiring_nonce_hash: Option<B256>,
62
63 pub override_key_id: Option<Address>,
67}
68#[derive(Debug, Clone, Default, derive_more::Deref, derive_more::DerefMut)]
70pub struct TempoTxEnv {
71 #[deref]
73 #[deref_mut]
74 pub inner: TxEnv,
75
76 pub fee_token: Option<Address>,
78
79 pub is_system_tx: bool,
81
82 pub fee_payer: Option<Option<Address>>,
88
89 pub tempo_tx_env: Option<Box<TempoBatchCallEnv>>,
91}
92
93impl TempoTxEnv {
94 pub fn fee_payer(&self) -> Result<Address, TempoInvalidTransaction> {
96 if let Some(fee_payer) = self.fee_payer {
97 fee_payer.ok_or(TempoInvalidTransaction::InvalidFeePayerSignature)
98 } else {
99 Ok(self.caller())
100 }
101 }
102
103 pub fn has_fee_payer_signature(&self) -> bool {
105 self.fee_payer.is_some()
106 }
107
108 pub fn is_subblock_transaction(&self) -> bool {
110 self.tempo_tx_env
111 .as_ref()
112 .is_some_and(|aa| aa.subblock_transaction)
113 }
114
115 pub fn first_call(&self) -> Option<(&TxKind, &[u8])> {
117 if let Some(aa) = self.tempo_tx_env.as_ref() {
118 aa.aa_calls
119 .first()
120 .map(|call| (&call.to, call.input.as_ref()))
121 } else {
122 Some((&self.inner.kind, &self.inner.data))
123 }
124 }
125
126 pub fn calls(&self) -> impl Iterator<Item = (&TxKind, &[u8])> {
131 if let Some(aa) = self.tempo_tx_env.as_ref() {
132 Either::Left(
133 aa.aa_calls
134 .iter()
135 .map(|call| (&call.to, call.input.as_ref())),
136 )
137 } else {
138 Either::Right(core::iter::once((
139 &self.inner.kind,
140 self.inner.input().as_ref(),
141 )))
142 }
143 }
144}
145
146impl From<TxEnv> for TempoTxEnv {
147 fn from(inner: TxEnv) -> Self {
148 Self {
149 inner,
150 ..Default::default()
151 }
152 }
153}
154
155impl Transaction for TempoTxEnv {
156 type AccessListItem<'a> = &'a AccessListItem;
157 type Authorization<'a> = &'a Either<SignedAuthorization, RecoveredAuthorization>;
158
159 fn tx_type(&self) -> u8 {
160 self.inner.tx_type()
161 }
162
163 fn kind(&self) -> TxKind {
164 self.inner.kind()
165 }
166
167 fn caller(&self) -> Address {
168 self.inner.caller()
169 }
170
171 fn gas_limit(&self) -> u64 {
172 self.inner.gas_limit()
173 }
174
175 fn gas_price(&self) -> u128 {
176 self.inner.gas_price()
177 }
178
179 fn value(&self) -> U256 {
180 self.inner.value()
181 }
182
183 fn nonce(&self) -> u64 {
184 Transaction::nonce(&self.inner)
185 }
186
187 fn chain_id(&self) -> Option<u64> {
188 self.inner.chain_id()
189 }
190
191 fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>> {
192 self.inner.access_list()
193 }
194
195 fn max_fee_per_gas(&self) -> u128 {
196 self.inner.max_fee_per_gas()
197 }
198
199 fn max_fee_per_blob_gas(&self) -> u128 {
200 self.inner.max_fee_per_blob_gas()
201 }
202
203 fn authorization_list_len(&self) -> usize {
204 self.inner.authorization_list_len()
205 }
206
207 fn authorization_list(&self) -> impl Iterator<Item = Self::Authorization<'_>> {
208 self.inner.authorization_list()
209 }
210
211 fn input(&self) -> &Bytes {
212 self.inner.input()
213 }
214
215 fn blob_versioned_hashes(&self) -> &[B256] {
216 self.inner.blob_versioned_hashes()
217 }
218
219 fn max_priority_fee_per_gas(&self) -> Option<u128> {
220 self.inner.max_priority_fee_per_gas()
221 }
222
223 fn max_balance_spending(&self) -> Result<U256, InvalidTransaction> {
224 calc_gas_balance_spending(self.gas_limit(), self.max_fee_per_gas())
225 .checked_add(self.value())
226 .ok_or(InvalidTransaction::OverflowPaymentInTransaction)
227 }
228
229 fn effective_balance_spending(
230 &self,
231 base_fee: u128,
232 _blob_price: u128,
233 ) -> Result<U256, InvalidTransaction> {
234 calc_gas_balance_spending(self.gas_limit(), self.effective_gas_price(base_fee))
235 .checked_add(self.value())
236 .ok_or(InvalidTransaction::OverflowPaymentInTransaction)
237 }
238}
239
240impl TransactionEnv for TempoTxEnv {
241 fn set_gas_limit(&mut self, gas_limit: u64) {
242 self.inner.set_gas_limit(gas_limit);
243 }
244
245 fn nonce(&self) -> u64 {
246 Transaction::nonce(&self.inner)
247 }
248
249 fn set_nonce(&mut self, nonce: u64) {
250 self.inner.set_nonce(nonce);
251 }
252
253 fn set_access_list(&mut self, access_list: AccessList) {
254 self.inner.set_access_list(access_list);
255 }
256}
257
258impl IntoTxEnv<Self> for TempoTxEnv {
259 fn into_tx_env(self) -> Self {
260 self
261 }
262}
263
264impl FromRecoveredTx<EthereumTxEnvelope<TxEip4844>> for TempoTxEnv {
265 fn from_recovered_tx(tx: &EthereumTxEnvelope<TxEip4844>, sender: Address) -> Self {
266 TxEnv::from_recovered_tx(tx, sender).into()
267 }
268}
269
270impl FromRecoveredTx<AASigned> for TempoTxEnv {
271 fn from_recovered_tx(aa_signed: &AASigned, caller: Address) -> Self {
272 let tx = aa_signed.tx();
273 let signature = aa_signed.signature();
274
275 if let Some(keychain_sig) = signature.as_keychain() {
278 let _ = keychain_sig.key_id(&aa_signed.signature_hash());
279 }
280
281 let TempoTransaction {
282 chain_id,
283 fee_token,
284 max_priority_fee_per_gas,
285 max_fee_per_gas,
286 gas_limit,
287 calls,
288 access_list,
289 nonce_key,
290 nonce,
291 fee_payer_signature,
292 valid_before,
293 valid_after,
294 key_authorization,
295 tempo_authorization_list,
296 } = tx;
297
298 let (to, value, input) = if let Some(first_call) = calls.first() {
300 (first_call.to, first_call.value, first_call.input.clone())
301 } else {
302 (
303 alloy_primitives::TxKind::Create,
304 alloy_primitives::U256::ZERO,
305 alloy_primitives::Bytes::new(),
306 )
307 };
308
309 Self {
310 inner: TxEnv {
311 tx_type: tx.ty(),
312 caller,
313 gas_limit: *gas_limit,
314 gas_price: *max_fee_per_gas,
315 kind: to,
316 value,
317 data: input,
318 nonce: *nonce, chain_id: Some(*chain_id),
320 gas_priority_fee: Some(*max_priority_fee_per_gas),
321 access_list: access_list.clone(),
322 authorization_list: tempo_authorization_list
324 .iter()
325 .map(|auth| {
326 let authority = auth
327 .recover_authority()
328 .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
329 Either::Right(RecoveredAuthorization::new_unchecked(
330 auth.inner().clone(),
331 authority,
332 ))
333 })
334 .collect(),
335 ..Default::default()
336 },
337 fee_token: *fee_token,
338 is_system_tx: false,
339 fee_payer: fee_payer_signature.map(|sig| {
340 secp256k1::recover_signer(&sig, tx.fee_payer_signature_hash(caller)).ok()
341 }),
342 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
344 signature: signature.clone(),
345 valid_before: *valid_before,
346 valid_after: *valid_after,
347 aa_calls: calls.clone(),
348 tempo_authorization_list: tempo_authorization_list
350 .iter()
351 .map(|auth| RecoveredTempoAuthorization::recover(auth.clone()))
352 .collect(),
353 nonce_key: *nonce_key,
354 subblock_transaction: aa_signed.tx().subblock_proposer().is_some(),
355 key_authorization: key_authorization.clone(),
356 signature_hash: aa_signed.signature_hash(),
357 tx_hash: *aa_signed.hash(),
358 expiring_nonce_hash: aa_signed
359 .tx()
360 .is_expiring_nonce_tx()
361 .then(|| aa_signed.expiring_nonce_hash(caller)),
362 override_key_id: None,
364 })),
365 }
366 }
367}
368
369impl FromRecoveredTx<TempoTxEnvelope> for TempoTxEnv {
370 fn from_recovered_tx(tx: &TempoTxEnvelope, sender: Address) -> Self {
371 match tx {
372 tx @ TempoTxEnvelope::Legacy(inner) => Self {
373 inner: TxEnv::from_recovered_tx(inner.tx(), sender),
374 fee_token: None,
375 is_system_tx: tx.is_system_tx(),
376 fee_payer: None,
377 tempo_tx_env: None, },
379 TempoTxEnvelope::Eip2930(tx) => TxEnv::from_recovered_tx(tx.tx(), sender).into(),
380 TempoTxEnvelope::Eip1559(tx) => TxEnv::from_recovered_tx(tx.tx(), sender).into(),
381 TempoTxEnvelope::Eip7702(tx) => TxEnv::from_recovered_tx(tx.tx(), sender).into(),
382 TempoTxEnvelope::AA(tx) => Self::from_recovered_tx(tx, sender),
383 }
384 }
385}
386
387impl FromTxWithEncoded<EthereumTxEnvelope<TxEip4844>> for TempoTxEnv {
388 fn from_encoded_tx(
389 tx: &EthereumTxEnvelope<TxEip4844>,
390 sender: Address,
391 _encoded: Bytes,
392 ) -> Self {
393 Self::from_recovered_tx(tx, sender)
394 }
395}
396
397impl FromTxWithEncoded<AASigned> for TempoTxEnv {
398 fn from_encoded_tx(tx: &AASigned, sender: Address, _encoded: Bytes) -> Self {
399 Self::from_recovered_tx(tx, sender)
400 }
401}
402
403impl FromTxWithEncoded<TempoTxEnvelope> for TempoTxEnv {
404 fn from_encoded_tx(tx: &TempoTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
405 Self::from_recovered_tx(tx, sender)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use alloy_evm::FromRecoveredTx;
412 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
413 use proptest::prelude::*;
414 use revm::context::{Transaction, TxEnv, result::InvalidTransaction};
415 use tempo_primitives::transaction::{
416 Call, calc_gas_balance_spending,
417 tempo_transaction::TEMPO_EXPIRING_NONCE_KEY,
418 tt_signature::{PrimitiveSignature, TempoSignature},
419 tt_signed::AASigned,
420 validate_calls,
421 };
422
423 use crate::{TempoInvalidTransaction, TempoTxEnv};
424
425 fn create_call(to: TxKind) -> Call {
426 Call {
427 to,
428 value: alloy_primitives::U256::ZERO,
429 input: alloy_primitives::Bytes::new(),
430 }
431 }
432
433 #[test]
434 fn test_validate_empty_calls_list() {
435 let result = validate_calls(&[], false);
436 assert!(result.is_err());
437 assert!(result.unwrap_err().contains("empty"));
438 }
439
440 #[test]
441 fn test_validate_single_call_ok() {
442 let calls = vec![create_call(TxKind::Call(alloy_primitives::Address::ZERO))];
443 assert!(validate_calls(&calls, false).is_ok());
444 }
445
446 #[test]
447 fn test_validate_single_create_ok() {
448 let calls = vec![create_call(TxKind::Create)];
449 assert!(validate_calls(&calls, false).is_ok());
450 }
451
452 #[test]
453 fn test_validate_create_with_authorization_list_fails() {
454 let calls = vec![create_call(TxKind::Create)];
455 let result = validate_calls(&calls, true); assert!(result.is_err());
457 assert!(result.unwrap_err().contains("CREATE"));
458 }
459
460 #[test]
461 fn test_validate_create_not_first_call_fails() {
462 let calls = vec![
463 create_call(TxKind::Call(alloy_primitives::Address::ZERO)),
464 create_call(TxKind::Create), ];
466 let result = validate_calls(&calls, false);
467 assert!(result.is_err());
468 assert!(result.unwrap_err().contains("first call"));
469 }
470
471 #[test]
472 fn test_validate_multiple_creates_fails() {
473 let calls = vec![
474 create_call(TxKind::Create),
475 create_call(TxKind::Create), ];
477 let result = validate_calls(&calls, false);
478 assert!(result.is_err());
479 assert!(result.unwrap_err().contains("first call"));
480 }
481
482 #[test]
483 fn test_validate_create_first_then_calls_ok() {
484 let calls = vec![
485 create_call(TxKind::Create),
486 create_call(TxKind::Call(alloy_primitives::Address::ZERO)),
487 create_call(TxKind::Call(alloy_primitives::Address::random())),
488 ];
489 assert!(validate_calls(&calls, false).is_ok());
491 }
492
493 #[test]
494 fn test_validate_multiple_calls_ok() {
495 let calls = vec![
496 create_call(TxKind::Call(alloy_primitives::Address::ZERO)),
497 create_call(TxKind::Call(alloy_primitives::Address::random())),
498 create_call(TxKind::Call(alloy_primitives::Address::random())),
499 ];
500 assert!(validate_calls(&calls, false).is_ok());
501 }
502
503 #[test]
504 fn test_from_recovered_tx_expiring_nonce_hash() {
505 let caller = Address::repeat_byte(0xAA);
506
507 let make_aa_signed = |nonce_key: U256| -> AASigned {
508 let tx = tempo_primitives::transaction::TempoTransaction {
509 chain_id: 1,
510 gas_limit: 1_000_000,
511 nonce_key,
512 nonce: 0,
513 valid_before: Some(100),
514 calls: vec![Call {
515 to: TxKind::Call(Address::repeat_byte(0x42)),
516 value: U256::ZERO,
517 input: Bytes::new(),
518 }],
519 ..Default::default()
520 };
521 let sig = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
522 Signature::test_signature(),
523 ));
524 AASigned::new_unhashed(tx, sig)
525 };
526
527 let expiring_signed = make_aa_signed(TEMPO_EXPIRING_NONCE_KEY);
529 let expiring_env = TempoTxEnv::from_recovered_tx(&expiring_signed, caller);
530 let tempo_env = expiring_env.tempo_tx_env.as_ref().unwrap();
531 let expected_hash = expiring_signed.expiring_nonce_hash(caller);
532 assert_eq!(
533 tempo_env.expiring_nonce_hash,
534 Some(expected_hash),
535 "expiring nonce tx must have expiring_nonce_hash set"
536 );
537
538 let regular_signed = make_aa_signed(U256::from(42));
540 let regular_env = super::TempoTxEnv::from_recovered_tx(®ular_signed, caller);
541 let regular_tempo_env = regular_env.tempo_tx_env.as_ref().unwrap();
542 assert_eq!(
543 regular_tempo_env.expiring_nonce_hash, None,
544 "regular 2D nonce tx must NOT have expiring_nonce_hash"
545 );
546 }
547
548 #[test]
549 fn test_tx_env() {
550 let tx_env = super::TempoTxEnv::default();
551
552 assert_eq!(tx_env.inner.nonce, 0);
554 assert!(tx_env.inner.access_list.is_empty());
555 assert!(tx_env.fee_token.is_none());
556 assert!(!tx_env.is_system_tx);
557 assert!(tx_env.fee_payer.is_none());
558 assert!(tx_env.tempo_tx_env.is_none());
559 }
560
561 #[test]
562 fn test_fee_payer_without_signature_uses_caller() {
563 let caller = Address::repeat_byte(0xAB);
564 let tx_env = super::TempoTxEnv {
565 inner: TxEnv {
566 caller,
567 ..Default::default()
568 },
569 fee_payer: None,
570 ..Default::default()
571 };
572
573 assert_eq!(tx_env.fee_payer(), Ok(caller));
574 }
575
576 #[test]
577 fn test_fee_payer_invalid_signature_rejected() {
578 let tx_env = super::TempoTxEnv {
579 fee_payer: Some(None),
580 ..Default::default()
581 };
582
583 assert!(matches!(
584 tx_env.fee_payer(),
585 Err(TempoInvalidTransaction::InvalidFeePayerSignature)
586 ));
587 }
588
589 #[test]
590 fn test_fee_payer_resolving_to_sender_is_allowed_in_tx_env() {
591 let caller = Address::repeat_byte(0xAB);
592 let tx_env = super::TempoTxEnv {
593 inner: TxEnv {
594 caller,
595 ..Default::default()
596 },
597 fee_payer: Some(Some(caller)),
598 ..Default::default()
599 };
600
601 assert_eq!(tx_env.fee_payer(), Ok(caller));
602 }
603
604 #[test]
605 fn test_has_fee_payer_signature() {
606 let without_sig = super::TempoTxEnv {
607 fee_payer: None,
608 ..Default::default()
609 };
610 assert!(!without_sig.has_fee_payer_signature());
611
612 let with_sig = super::TempoTxEnv {
613 fee_payer: Some(Some(Address::repeat_byte(0xAB))),
614 ..Default::default()
615 };
616 assert!(with_sig.has_fee_payer_signature());
617 }
618
619 #[test]
620 fn test_transaction_env_set_gas_limit() {
621 use reth_evm::TransactionEnv;
622
623 let mut tx_env = super::TempoTxEnv::default();
624
625 tx_env.set_gas_limit(21000);
626 assert_eq!(tx_env.inner.gas_limit, 21000);
627
628 tx_env.set_gas_limit(1_000_000);
629 assert_eq!(tx_env.inner.gas_limit, 1_000_000);
630 }
631
632 #[test]
633 fn test_transaction_env_nonce() {
634 use reth_evm::TransactionEnv;
635
636 let mut tx_env = super::TempoTxEnv::default();
637 assert_eq!(TransactionEnv::nonce(&tx_env), 0);
638
639 tx_env.set_nonce(42);
640 assert_eq!(TransactionEnv::nonce(&tx_env), 42);
641
642 tx_env.set_nonce(u64::MAX);
643 assert_eq!(TransactionEnv::nonce(&tx_env), u64::MAX);
644 }
645
646 #[test]
647 fn test_transaction_env_set_access_list() {
648 use reth_evm::TransactionEnv;
649 use revm::context::transaction::{AccessList, AccessListItem};
650
651 let mut tx_env = super::TempoTxEnv::default();
652 assert!(tx_env.inner.access_list.is_empty());
653
654 let access_list = AccessList(vec![
655 AccessListItem {
656 address: alloy_primitives::Address::ZERO,
657 storage_keys: vec![alloy_primitives::B256::ZERO],
658 },
659 AccessListItem {
660 address: alloy_primitives::Address::repeat_byte(0x01),
661 storage_keys: vec![
662 alloy_primitives::B256::repeat_byte(0x01),
663 alloy_primitives::B256::repeat_byte(0x02),
664 ],
665 },
666 ]);
667
668 tx_env.set_access_list(access_list);
669 assert_eq!(tx_env.inner.access_list.0.len(), 2);
670 assert_eq!(
671 tx_env.inner.access_list.0[0].address,
672 alloy_primitives::Address::ZERO
673 );
674 assert_eq!(tx_env.inner.access_list.0[0].storage_keys.len(), 1);
675 assert_eq!(tx_env.inner.access_list.0[1].storage_keys.len(), 2);
676 }
677
678 #[test]
679 fn test_transaction_env_combined_operations() {
680 use reth_evm::TransactionEnv;
681 use revm::context::transaction::{AccessList, AccessListItem};
682
683 let mut tx_env = super::TempoTxEnv::default();
684
685 tx_env.set_gas_limit(50_000);
687 tx_env.set_nonce(100);
688 tx_env.set_access_list(AccessList(vec![AccessListItem {
689 address: alloy_primitives::Address::repeat_byte(0xAB),
690 storage_keys: vec![],
691 }]));
692
693 assert_eq!(tx_env.inner.gas_limit, 50_000);
695 assert_eq!(TransactionEnv::nonce(&tx_env), 100);
696 assert_eq!(tx_env.inner.access_list.0.len(), 1);
697 assert_eq!(
698 tx_env.inner.access_list.0[0].address,
699 alloy_primitives::Address::repeat_byte(0xAB)
700 );
701 }
702
703 #[test]
704 fn test_transaction_env_from_tx_env() {
705 use reth_evm::TransactionEnv;
706 use revm::context::TxEnv;
707
708 let inner = TxEnv {
709 gas_limit: 75_000,
710 nonce: 55,
711 ..Default::default()
712 };
713
714 let tx_env: super::TempoTxEnv = inner.into();
715
716 assert_eq!(tx_env.inner.gas_limit, 75_000);
717 assert_eq!(TransactionEnv::nonce(&tx_env), 55);
718 assert!(tx_env.fee_token.is_none());
719 assert!(!tx_env.is_system_tx);
720 assert!(tx_env.fee_payer.is_none());
721 assert!(tx_env.tempo_tx_env.is_none());
722 }
723
724 #[test]
725 fn test_first_call_without_aa() {
726 use alloy_primitives::{Address, Bytes};
727 use revm::context::TxEnv;
728
729 let addr = Address::repeat_byte(0x42);
731 let data = Bytes::from(vec![0x01, 0x02, 0x03]);
732
733 let tx_env = super::TempoTxEnv {
734 inner: TxEnv {
735 kind: TxKind::Call(addr),
736 data: data.clone(),
737 ..Default::default()
738 },
739 ..Default::default()
740 };
741
742 let first_call = tx_env.first_call();
743 assert!(first_call.is_some());
744 let (kind, input) = first_call.unwrap();
745 assert_eq!(*kind, TxKind::Call(addr));
746 assert_eq!(input, data.as_ref());
747 }
748
749 #[test]
750 fn test_first_call_with_aa() {
751 use alloy_primitives::{Address, Bytes, U256};
752 use tempo_primitives::transaction::Call;
753
754 let addr1 = Address::repeat_byte(0x11);
756 let addr2 = Address::repeat_byte(0x22);
757 let input1 = Bytes::from(vec![0xAA, 0xBB]);
758 let input2 = Bytes::from(vec![0xCC, 0xDD]);
759
760 let tx_env = super::TempoTxEnv {
761 tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
762 aa_calls: vec![
763 Call {
764 to: TxKind::Call(addr1),
765 value: U256::ZERO,
766 input: input1.clone(),
767 },
768 Call {
769 to: TxKind::Call(addr2),
770 value: U256::from(100),
771 input: input2,
772 },
773 ],
774 ..Default::default()
775 })),
776 ..Default::default()
777 };
778
779 let first_call = tx_env.first_call();
780 assert!(first_call.is_some());
781 let (kind, input) = first_call.unwrap();
782 assert_eq!(*kind, TxKind::Call(addr1));
783 assert_eq!(input, input1.as_ref());
784 }
785
786 #[test]
787 fn test_first_call_with_empty_aa_calls() {
788 let tx_env = super::TempoTxEnv {
790 tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
791 aa_calls: vec![],
792 ..Default::default()
793 })),
794 ..Default::default()
795 };
796
797 assert!(tx_env.first_call().is_none());
798 }
799
800 #[test]
801 fn test_calls() {
802 use alloy_primitives::{Address, Bytes, U256};
803 use revm::context::TxEnv;
804 use tempo_primitives::transaction::Call;
805
806 let addr1 = Address::repeat_byte(0x11);
807 let addr2 = Address::repeat_byte(0x22);
808 let input1 = Bytes::from(vec![0x01]);
809 let input2 = Bytes::from(vec![0x02, 0x03]);
810 let input3 = Bytes::from(vec![0x04, 0x05, 0x06]);
811
812 let non_aa_tx = super::TempoTxEnv {
814 inner: TxEnv {
815 kind: TxKind::Call(addr1),
816 data: input1.clone(),
817 ..Default::default()
818 },
819 ..Default::default()
820 };
821 let calls: Vec<_> = non_aa_tx.calls().collect();
822 assert_eq!(calls.len(), 1);
823 assert_eq!(*calls[0].0, TxKind::Call(addr1));
824 assert_eq!(calls[0].1, input1.as_ref());
825
826 let aa_tx = super::TempoTxEnv {
828 tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
829 aa_calls: vec![
830 Call {
831 to: TxKind::Call(addr1),
832 value: U256::ZERO,
833 input: input1.clone(),
834 },
835 Call {
836 to: TxKind::Call(addr2),
837 value: U256::from(50),
838 input: input2.clone(),
839 },
840 Call {
841 to: TxKind::Create,
842 value: U256::from(100),
843 input: input3.clone(),
844 },
845 ],
846 ..Default::default()
847 })),
848 ..Default::default()
849 };
850 let calls: Vec<_> = aa_tx.calls().collect();
851 assert_eq!(calls.len(), 3);
852 assert_eq!(*calls[0].0, TxKind::Call(addr1));
853 assert_eq!(calls[0].1, input1.as_ref());
854 assert_eq!(*calls[1].0, TxKind::Call(addr2));
855 assert_eq!(calls[1].1, input2.as_ref());
856 assert_eq!(*calls[2].0, TxKind::Create);
857 assert_eq!(calls[2].1, input3.as_ref());
858
859 let empty_aa_tx = super::TempoTxEnv {
861 tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
862 aa_calls: vec![],
863 ..Default::default()
864 })),
865 ..Default::default()
866 };
867 let calls: Vec<_> = empty_aa_tx.calls().collect();
868 assert!(calls.is_empty());
869 }
870
871 fn arb_u256() -> impl Strategy<Value = alloy_primitives::U256> {
873 any::<[u64; 4]>().prop_map(alloy_primitives::U256::from_limbs)
874 }
875
876 fn make_tx_env(
878 gas_limit: u64,
879 gas_price: u128,
880 value: alloy_primitives::U256,
881 ) -> super::TempoTxEnv {
882 super::TempoTxEnv {
883 inner: revm::context::TxEnv {
884 gas_limit,
885 gas_price,
886 value,
887 ..Default::default()
888 },
889 ..Default::default()
890 }
891 }
892
893 proptest! {
894 #![proptest_config(ProptestConfig::with_cases(500))]
895
896 #[test]
898 fn proptest_max_balance_spending_no_panic(
899 gas_limit in any::<u64>(),
900 max_fee_per_gas in any::<u128>(),
901 value in arb_u256(),
902 ) {
903 let tx_env = make_tx_env(gas_limit, max_fee_per_gas, value);
904 let result = tx_env.max_balance_spending();
905 prop_assert!(
906 result.is_ok()
907 || result == Err(InvalidTransaction::OverflowPaymentInTransaction)
908 );
909 }
910
911 #[test]
913 fn proptest_max_balance_spending_overflow_detection(
914 gas_limit in any::<u64>(),
915 max_fee_per_gas in any::<u128>(),
916 value in arb_u256(),
917 ) {
918 let tx_env = make_tx_env(gas_limit, max_fee_per_gas, value);
919 let gas_spending = calc_gas_balance_spending(gas_limit, max_fee_per_gas);
920 let result = tx_env.max_balance_spending();
921
922 match gas_spending.checked_add(value) {
923 Some(expected) => prop_assert_eq!(result, Ok(expected)),
924 None => prop_assert_eq!(result, Err(InvalidTransaction::OverflowPaymentInTransaction)),
925 }
926 }
927
928 #[test]
931 fn proptest_effective_le_max_balance_spending(
932 gas_limit in 0u64..30_000_000u64, max_fee_per_gas in 0u128..1_000_000_000_000u128, max_priority_fee in 0u128..100_000_000_000u128, base_fee in 0u128..500_000_000_000u128, value in 0u128..10_000_000_000_000_000_000_000u128, ) {
938 let mut tx_env = make_tx_env(gas_limit, max_fee_per_gas, alloy_primitives::U256::from(value));
939 tx_env.inner.gas_priority_fee = Some(max_priority_fee);
940
941 let max_result = tx_env.max_balance_spending();
942 let effective_result = tx_env.effective_balance_spending(base_fee, 0);
943
944 let max_spending = max_result.expect("max_balance_spending should succeed with constrained inputs");
946 let effective_spending = effective_result.expect("effective_balance_spending should succeed with constrained inputs");
947
948 prop_assert!(
949 effective_spending <= max_spending,
950 "effective_balance_spending ({}) should be <= max_balance_spending ({})",
951 effective_spending,
952 max_spending
953 );
954 }
955
956 #[test]
962 fn proptest_effective_balance_spending_zero_base_fee(
963 gas_limit in 0u64..30_000_000u64,
964 max_fee_per_gas in 0u128..1_000_000_000_000u128,
965 priority_fee in 0u128..500_000_000_000u128,
966 value in 0u128..10_000_000_000_000_000_000_000u128,
967 ) {
968 use revm::context::Transaction;
969
970 let mut tx_env = make_tx_env(gas_limit, max_fee_per_gas, alloy_primitives::U256::from(value));
971 tx_env.inner.tx_type = 2; tx_env.inner.gas_priority_fee = Some(priority_fee);
974
975 let result = tx_env.effective_balance_spending(0, 0);
976
977 let effective_price = std::cmp::min(max_fee_per_gas, priority_fee);
979 let expected_gas_spending = calc_gas_balance_spending(gas_limit, effective_price);
980 let expected = expected_gas_spending.checked_add(alloy_primitives::U256::from(value));
981
982 match expected {
983 Some(expected_val) => prop_assert_eq!(result, Ok(expected_val)),
984 None => prop_assert_eq!(result, Err(InvalidTransaction::OverflowPaymentInTransaction)),
985 }
986 }
987
988 #[test]
990 fn proptest_calls_count_aa_tx(num_calls in 0usize..20) {
991 let aa_tx = super::TempoTxEnv {
992 tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
993 aa_calls: (0..num_calls)
994 .map(|_| Call {
995 to: TxKind::Call(alloy_primitives::Address::ZERO),
996 value: alloy_primitives::U256::ZERO,
997 input: alloy_primitives::Bytes::new(),
998 })
999 .collect(),
1000 ..Default::default()
1001 })),
1002 ..Default::default()
1003 };
1004 prop_assert_eq!(aa_tx.calls().count(), num_calls);
1005 }
1006
1007 }
1008
1009 #[test]
1010 fn test_calls_count_non_aa_tx() {
1011 let non_aa_tx = make_tx_env(21_000, 0, alloy_primitives::U256::ZERO);
1012 assert_eq!(non_aa_tx.calls().count(), 1);
1013 }
1014}