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