1use crate::{
4 identifier::{SenderIdentifiers, TransactionId},
5 pool::txpool::TxPool,
6 traits::TransactionOrigin,
7 CoinbaseTipOrdering, EthBlobTransactionSidecar, EthPoolTransaction, PoolTransaction,
8 ValidPoolTransaction,
9};
10use alloy_consensus::{
11 constants::{EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, LEGACY_TX_TYPE_ID},
12 transaction::TxSeismicElements,
13 TxEip1559, TxEip2930, TxEip4844, TxLegacy,
14};
15use alloy_eips::{
16 eip1559::MIN_PROTOCOL_BASE_FEE,
17 eip2930::AccessList,
18 eip4844::{BlobTransactionSidecar, BlobTransactionValidationError, DATA_GAS_PER_BLOB},
19};
20use alloy_primitives::{
21 Address, Bytes, ChainId, PrimitiveSignature as Signature, TxHash, TxKind, B256, U256,
22};
23use paste::paste;
24use rand::{
25 distributions::{Uniform, WeightedIndex},
26 prelude::Distribution,
27};
28use reth_primitives::{
29 transaction::{SignedTransactionIntoRecoveredExt, TryFromRecoveredTransactionError},
30 PooledTransactionsElement, PooledTransactionsElementEcRecovered, RecoveredTx, Transaction,
31 TransactionSigned, TxType,
32};
33use reth_primitives_traits::InMemorySize;
34use std::{ops::Range, sync::Arc, time::Instant, vec::IntoIter};
35
36pub type MockTxPool = TxPool<MockOrdering>;
40
41pub type MockValidTx = ValidPoolTransaction<MockTransaction>;
46
47pub fn mock_tx_pool() -> MockTxPool {
49 MockTxPool::new(Default::default(), Default::default())
50}
51
52macro_rules! set_value {
54 ($this:ident => $field:ident) => {
55 let new_value = $field;
56 match $this {
57 MockTransaction::Legacy { ref mut $field, .. } |
58 MockTransaction::Eip1559 { ref mut $field, .. } |
59 MockTransaction::Eip4844 { ref mut $field, .. } |
60 MockTransaction::Eip2930 { ref mut $field, .. } => {
61 *$field = new_value;
62 }
63 }
64 $this.update_cost();
66 };
67}
68
69macro_rules! get_value {
71 ($this:tt => $field:ident) => {
72 match $this {
73 MockTransaction::Legacy { $field, .. } |
74 MockTransaction::Eip1559 { $field, .. } |
75 MockTransaction::Eip4844 { $field, .. } |
76 MockTransaction::Eip2930 { $field, .. } => $field,
77 }
78 };
79}
80
81macro_rules! make_setters_getters {
83 ($($name:ident => $t:ty);*) => {
84 paste! {$(
85 pub fn [<set_ $name>](&mut self, $name: $t) -> &mut Self {
87 set_value!(self => $name);
88 self
89 }
90
91 pub fn [<with_ $name>](mut self, $name: $t) -> Self {
93 set_value!(self => $name);
94 self
95 }
96
97 pub const fn [<get_ $name>](&self) -> &$t {
99 get_value!(self => $name)
100 }
101 )*}
102 };
103}
104
105#[derive(Debug, Clone, Eq, PartialEq)]
107pub enum MockTransaction {
108 Legacy {
110 chain_id: Option<ChainId>,
112 hash: B256,
114 sender: Address,
116 nonce: u64,
118 gas_price: u128,
120 gas_limit: u64,
122 to: TxKind,
124 value: U256,
126 input: Bytes,
128 size: usize,
130 cost: U256,
132 },
133 Eip2930 {
135 chain_id: ChainId,
137 hash: B256,
139 sender: Address,
141 nonce: u64,
143 to: TxKind,
145 gas_limit: u64,
147 input: Bytes,
149 value: U256,
151 gas_price: u128,
153 access_list: AccessList,
155 size: usize,
157 cost: U256,
159 },
160 Eip1559 {
162 chain_id: ChainId,
164 hash: B256,
166 sender: Address,
168 nonce: u64,
170 max_fee_per_gas: u128,
172 max_priority_fee_per_gas: u128,
174 gas_limit: u64,
176 to: TxKind,
178 value: U256,
180 access_list: AccessList,
182 input: Bytes,
184 size: usize,
186 cost: U256,
188 },
189 Eip4844 {
191 chain_id: ChainId,
193 hash: B256,
195 sender: Address,
197 nonce: u64,
199 max_fee_per_gas: u128,
201 max_priority_fee_per_gas: u128,
203 max_fee_per_blob_gas: u128,
205 gas_limit: u64,
207 to: Address,
209 value: U256,
211 access_list: AccessList,
213 input: Bytes,
215 sidecar: BlobTransactionSidecar,
217 size: usize,
219 cost: U256,
221 },
222}
223
224impl MockTransaction {
227 make_setters_getters! {
228 nonce => u64;
229 hash => B256;
230 sender => Address;
231 gas_limit => u64;
232 value => U256;
233 input => Bytes;
234 size => usize
235 }
236
237 pub fn legacy() -> Self {
239 Self::Legacy {
240 chain_id: Some(1),
241 hash: B256::random(),
242 sender: Address::random(),
243 nonce: 0,
244 gas_price: 0,
245 gas_limit: 0,
246 to: Address::random().into(),
247 value: Default::default(),
248 input: Default::default(),
249 size: Default::default(),
250 cost: U256::ZERO,
251 }
252 }
253
254 pub fn eip2930() -> Self {
256 Self::Eip2930 {
257 chain_id: 1,
258 hash: B256::random(),
259 sender: Address::random(),
260 nonce: 0,
261 to: Address::random().into(),
262 gas_limit: 0,
263 input: Bytes::new(),
264 value: Default::default(),
265 gas_price: 0,
266 access_list: Default::default(),
267 size: Default::default(),
268 cost: U256::ZERO,
269 }
270 }
271
272 pub fn eip1559() -> Self {
274 Self::Eip1559 {
275 chain_id: 1,
276 hash: B256::random(),
277 sender: Address::random(),
278 nonce: 0,
279 max_fee_per_gas: MIN_PROTOCOL_BASE_FEE as u128,
280 max_priority_fee_per_gas: MIN_PROTOCOL_BASE_FEE as u128,
281 gas_limit: 0,
282 to: Address::random().into(),
283 value: Default::default(),
284 input: Bytes::new(),
285 access_list: Default::default(),
286 size: Default::default(),
287 cost: U256::ZERO,
288 }
289 }
290
291 pub fn eip4844() -> Self {
293 Self::Eip4844 {
294 chain_id: 1,
295 hash: B256::random(),
296 sender: Address::random(),
297 nonce: 0,
298 max_fee_per_gas: MIN_PROTOCOL_BASE_FEE as u128,
299 max_priority_fee_per_gas: MIN_PROTOCOL_BASE_FEE as u128,
300 max_fee_per_blob_gas: DATA_GAS_PER_BLOB as u128,
301 gas_limit: 0,
302 to: Address::random(),
303 value: Default::default(),
304 input: Bytes::new(),
305 access_list: Default::default(),
306 sidecar: Default::default(),
307 size: Default::default(),
308 cost: U256::ZERO,
309 }
310 }
311
312 pub fn eip4844_with_sidecar(sidecar: BlobTransactionSidecar) -> Self {
314 let mut transaction = Self::eip4844();
315 if let Self::Eip4844 { sidecar: ref mut existing_sidecar, .. } = &mut transaction {
316 *existing_sidecar = sidecar;
317 }
318 transaction
319 }
320
321 pub fn new_from_type(tx_type: TxType) -> Self {
330 #[allow(unreachable_patterns)]
331 match tx_type {
332 TxType::Legacy => Self::legacy(),
333 TxType::Eip2930 => Self::eip2930(),
334 TxType::Eip1559 => Self::eip1559(),
335 TxType::Eip4844 => Self::eip4844(),
336
337 _ => unreachable!("Invalid transaction type"),
338 }
339 }
340
341 pub fn with_blob_fee(mut self, val: u128) -> Self {
343 self.set_blob_fee(val);
344 self
345 }
346
347 pub fn set_blob_fee(&mut self, val: u128) -> &mut Self {
349 if let Self::Eip4844 { max_fee_per_blob_gas, .. } = self {
350 *max_fee_per_blob_gas = val;
351 }
352 self
353 }
354
355 pub fn set_priority_fee(&mut self, val: u128) -> &mut Self {
357 if let Self::Eip1559 { max_priority_fee_per_gas, .. } |
358 Self::Eip4844 { max_priority_fee_per_gas, .. } = self
359 {
360 *max_priority_fee_per_gas = val;
361 }
362 self
363 }
364
365 pub fn with_priority_fee(mut self, val: u128) -> Self {
367 self.set_priority_fee(val);
368 self
369 }
370
371 pub const fn get_priority_fee(&self) -> Option<u128> {
373 match self {
374 Self::Eip1559 { max_priority_fee_per_gas, .. } |
375 Self::Eip4844 { max_priority_fee_per_gas, .. } => Some(*max_priority_fee_per_gas),
376 _ => None,
377 }
378 }
379
380 pub fn set_max_fee(&mut self, val: u128) -> &mut Self {
382 if let Self::Eip1559 { max_fee_per_gas, .. } | Self::Eip4844 { max_fee_per_gas, .. } = self
383 {
384 *max_fee_per_gas = val;
385 }
386 self
387 }
388
389 pub fn with_max_fee(mut self, val: u128) -> Self {
391 self.set_max_fee(val);
392 self
393 }
394
395 pub const fn get_max_fee(&self) -> Option<u128> {
397 match self {
398 Self::Eip1559 { max_fee_per_gas, .. } | Self::Eip4844 { max_fee_per_gas, .. } => {
399 Some(*max_fee_per_gas)
400 }
401 _ => None,
402 }
403 }
404
405 pub fn set_accesslist(&mut self, list: AccessList) -> &mut Self {
407 match self {
408 Self::Legacy { .. } => {}
409 Self::Eip1559 { access_list: accesslist, .. } |
410 Self::Eip4844 { access_list: accesslist, .. } |
411 Self::Eip2930 { access_list: accesslist, .. } => {
412 *accesslist = list;
413 }
414 }
415 self
416 }
417
418 pub fn set_gas_price(&mut self, val: u128) -> &mut Self {
420 match self {
421 Self::Legacy { gas_price, .. } | Self::Eip2930 { gas_price, .. } => {
422 *gas_price = val;
423 }
424 Self::Eip1559 { max_fee_per_gas, max_priority_fee_per_gas, .. } |
425 Self::Eip4844 { max_fee_per_gas, max_priority_fee_per_gas, .. } => {
426 *max_fee_per_gas = val;
427 *max_priority_fee_per_gas = val;
428 }
429 }
430 self
431 }
432
433 pub fn with_gas_price(mut self, val: u128) -> Self {
435 match self {
436 Self::Legacy { ref mut gas_price, .. } | Self::Eip2930 { ref mut gas_price, .. } => {
437 *gas_price = val;
438 }
439 Self::Eip1559 { ref mut max_fee_per_gas, ref mut max_priority_fee_per_gas, .. } |
440 Self::Eip4844 { ref mut max_fee_per_gas, ref mut max_priority_fee_per_gas, .. } => {
441 *max_fee_per_gas = val;
442 *max_priority_fee_per_gas = val;
443 }
444 }
445 self
446 }
447
448 pub const fn get_gas_price(&self) -> u128 {
450 match self {
451 Self::Legacy { gas_price, .. } | Self::Eip2930 { gas_price, .. } => *gas_price,
452 Self::Eip1559 { max_fee_per_gas, .. } | Self::Eip4844 { max_fee_per_gas, .. } => {
453 *max_fee_per_gas
454 }
455 }
456 }
457
458 pub fn prev(&self) -> Self {
460 self.clone().with_hash(B256::random()).with_nonce(self.get_nonce() - 1)
461 }
462
463 pub fn next(&self) -> Self {
465 self.clone().with_hash(B256::random()).with_nonce(self.get_nonce() + 1)
466 }
467
468 pub fn skip(&self, skip: u64) -> Self {
470 self.clone().with_hash(B256::random()).with_nonce(self.get_nonce() + skip + 1)
471 }
472
473 pub fn inc_nonce(self) -> Self {
475 let nonce = self.get_nonce() + 1;
476 self.with_nonce(nonce)
477 }
478
479 pub fn rng_hash(self) -> Self {
481 self.with_hash(B256::random())
482 }
483
484 pub fn inc_price(&self) -> Self {
486 self.inc_price_by(1)
487 }
488
489 pub fn inc_price_by(&self, value: u128) -> Self {
491 self.clone().with_gas_price(self.get_gas_price().checked_add(value).unwrap())
492 }
493
494 pub fn decr_price(&self) -> Self {
496 self.decr_price_by(1)
497 }
498
499 pub fn decr_price_by(&self, value: u128) -> Self {
501 self.clone().with_gas_price(self.get_gas_price().checked_sub(value).unwrap())
502 }
503
504 pub fn inc_value(&self) -> Self {
506 self.clone().with_value(self.get_value().checked_add(U256::from(1)).unwrap())
507 }
508
509 pub fn inc_limit(&self) -> Self {
511 self.clone().with_gas_limit(self.get_gas_limit() + 1)
512 }
513
514 pub fn inc_blob_fee(&self) -> Self {
518 self.inc_blob_fee_by(1)
519 }
520
521 pub fn inc_blob_fee_by(&self, value: u128) -> Self {
525 let mut this = self.clone();
526 if let Self::Eip4844 { max_fee_per_blob_gas, .. } = &mut this {
527 *max_fee_per_blob_gas = max_fee_per_blob_gas.checked_add(value).unwrap();
528 }
529 this
530 }
531
532 pub fn decr_blob_fee(&self) -> Self {
536 self.decr_price_by(1)
537 }
538
539 pub fn decr_blob_fee_by(&self, value: u128) -> Self {
543 let mut this = self.clone();
544 if let Self::Eip4844 { max_fee_per_blob_gas, .. } = &mut this {
545 *max_fee_per_blob_gas = max_fee_per_blob_gas.checked_sub(value).unwrap();
546 }
547 this
548 }
549
550 pub const fn tx_type(&self) -> u8 {
552 match self {
553 Self::Legacy { .. } => LEGACY_TX_TYPE_ID,
554 Self::Eip1559 { .. } => EIP1559_TX_TYPE_ID,
555 Self::Eip4844 { .. } => EIP4844_TX_TYPE_ID,
556 Self::Eip2930 { .. } => EIP2930_TX_TYPE_ID,
557 }
558 }
559
560 pub const fn is_legacy(&self) -> bool {
562 matches!(self, Self::Legacy { .. })
563 }
564
565 pub const fn is_eip1559(&self) -> bool {
567 matches!(self, Self::Eip1559 { .. })
568 }
569
570 pub const fn is_eip4844(&self) -> bool {
572 matches!(self, Self::Eip4844 { .. })
573 }
574
575 pub const fn is_eip2930(&self) -> bool {
577 matches!(self, Self::Eip2930 { .. })
578 }
579
580 fn update_cost(&mut self) {
581 match self {
582 Self::Legacy { cost, gas_limit, gas_price, value, .. } |
583 Self::Eip2930 { cost, gas_limit, gas_price, value, .. } => {
584 *cost = U256::from(*gas_limit) * U256::from(*gas_price) + *value
585 }
586 Self::Eip1559 { cost, gas_limit, max_fee_per_gas, value, .. } |
587 Self::Eip4844 { cost, gas_limit, max_fee_per_gas, value, .. } => {
588 *cost = U256::from(*gas_limit) * U256::from(*max_fee_per_gas) + *value
589 }
590 };
591 }
592}
593
594impl PoolTransaction for MockTransaction {
595 type TryFromConsensusError = TryFromRecoveredTransactionError;
596
597 type Consensus = TransactionSigned;
598
599 type Pooled = PooledTransactionsElement;
600
601 fn try_from_consensus(
602 tx: RecoveredTx<Self::Consensus>,
603 ) -> Result<Self, Self::TryFromConsensusError> {
604 tx.try_into()
605 }
606
607 fn into_consensus(self) -> RecoveredTx<Self::Consensus> {
608 self.into()
609 }
610
611 fn from_pooled(pooled: RecoveredTx<Self::Pooled>) -> Self {
612 pooled.into()
613 }
614
615 fn try_consensus_into_pooled(
616 tx: RecoveredTx<Self::Consensus>,
617 ) -> Result<RecoveredTx<Self::Pooled>, Self::TryFromConsensusError> {
618 let (tx, signer) = tx.to_components();
619 Self::Pooled::try_from(tx)
620 .map(|tx| tx.with_signer(signer))
621 .map_err(|_| TryFromRecoveredTransactionError::BlobSidecarMissing)
622 }
623
624 fn hash(&self) -> &TxHash {
625 self.get_hash()
626 }
627
628 fn sender(&self) -> Address {
629 *self.get_sender()
630 }
631
632 fn sender_ref(&self) -> &Address {
633 self.get_sender()
634 }
635
636 fn nonce(&self) -> u64 {
637 *self.get_nonce()
638 }
639
640 fn cost(&self) -> &U256 {
645 match self {
646 Self::Legacy { cost, .. } |
647 Self::Eip2930 { cost, .. } |
648 Self::Eip1559 { cost, .. } |
649 Self::Eip4844 { cost, .. } => cost,
650 }
651 }
652
653 fn gas_limit(&self) -> u64 {
654 *self.get_gas_limit()
655 }
656
657 fn max_fee_per_gas(&self) -> u128 {
658 match self {
659 Self::Legacy { gas_price, .. } | Self::Eip2930 { gas_price, .. } => *gas_price,
660 Self::Eip1559 { max_fee_per_gas, .. } | Self::Eip4844 { max_fee_per_gas, .. } => {
661 *max_fee_per_gas
662 }
663 }
664 }
665
666 fn access_list(&self) -> Option<&AccessList> {
667 match self {
668 Self::Legacy { .. } => None,
669 Self::Eip1559 { access_list: accesslist, .. } |
670 Self::Eip4844 { access_list: accesslist, .. } |
671 Self::Eip2930 { access_list: accesslist, .. } => Some(accesslist),
672 }
673 }
674
675 fn max_priority_fee_per_gas(&self) -> Option<u128> {
676 match self {
677 Self::Legacy { .. } | Self::Eip2930 { .. } => None,
678 Self::Eip1559 { max_priority_fee_per_gas, .. } |
679 Self::Eip4844 { max_priority_fee_per_gas, .. } => Some(*max_priority_fee_per_gas),
680 }
681 }
682
683 fn max_fee_per_blob_gas(&self) -> Option<u128> {
684 match self {
685 Self::Eip4844 { max_fee_per_blob_gas, .. } => Some(*max_fee_per_blob_gas),
686 _ => None,
687 }
688 }
689
690 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
692 let base_fee = base_fee as u128;
694
695 let max_fee_per_gas = self.max_fee_per_gas();
697
698 if max_fee_per_gas < base_fee {
700 return None
701 }
702
703 let fee = max_fee_per_gas - base_fee;
705
706 if let Some(priority_fee) = self.max_priority_fee_per_gas() {
709 return Some(fee.min(priority_fee))
710 }
711
712 Some(fee)
714 }
715
716 fn priority_fee_or_price(&self) -> u128 {
718 match self {
719 Self::Legacy { gas_price, .. } | Self::Eip2930 { gas_price, .. } => *gas_price,
720 Self::Eip1559 { max_priority_fee_per_gas, .. } |
721 Self::Eip4844 { max_priority_fee_per_gas, .. } => *max_priority_fee_per_gas,
722 }
723 }
724
725 fn kind(&self) -> TxKind {
727 match self {
728 Self::Legacy { to, .. } | Self::Eip1559 { to, .. } | Self::Eip2930 { to, .. } => *to,
729 Self::Eip4844 { to, .. } => TxKind::Call(*to),
730 }
731 }
732
733 fn is_create(&self) -> bool {
735 match self {
736 Self::Legacy { to, .. } | Self::Eip1559 { to, .. } | Self::Eip2930 { to, .. } => {
737 to.is_create()
738 }
739 Self::Eip4844 { .. } => false,
740 }
741 }
742
743 fn input(&self) -> &[u8] {
745 self.get_input()
746 }
747
748 fn size(&self) -> usize {
750 *self.get_size()
751 }
752
753 fn tx_type(&self) -> u8 {
755 match self {
756 Self::Legacy { .. } => TxType::Legacy.into(),
757 Self::Eip1559 { .. } => TxType::Eip1559.into(),
758 Self::Eip4844 { .. } => TxType::Eip4844.into(),
759 Self::Eip2930 { .. } => TxType::Eip2930.into(),
760 }
761 }
762
763 fn encoded_length(&self) -> usize {
765 self.size()
766 }
767
768 fn chain_id(&self) -> Option<u64> {
770 match self {
771 Self::Legacy { chain_id, .. } => *chain_id,
772 Self::Eip1559 { chain_id, .. } |
773 Self::Eip4844 { chain_id, .. } |
774 Self::Eip2930 { chain_id, .. } => Some(*chain_id),
775 }
776 }
777
778 fn seismic_elements(&self) -> Option<&TxSeismicElements> {
779 None
780 }
781}
782
783impl EthPoolTransaction for MockTransaction {
784 fn take_blob(&mut self) -> EthBlobTransactionSidecar {
785 match self {
786 Self::Eip4844 { sidecar, .. } => EthBlobTransactionSidecar::Present(sidecar.clone()),
787 _ => EthBlobTransactionSidecar::None,
788 }
789 }
790
791 fn blob_count(&self) -> usize {
792 match self {
793 Self::Eip4844 { sidecar, .. } => sidecar.blobs.len(),
794 _ => 0,
795 }
796 }
797
798 fn try_into_pooled_eip4844(
799 self,
800 sidecar: Arc<BlobTransactionSidecar>,
801 ) -> Option<RecoveredTx<Self::Pooled>> {
802 let (tx, signer) = self.into_consensus().to_components();
803 Self::Pooled::try_from_blob_transaction(tx, Arc::unwrap_or_clone(sidecar))
804 .map(|tx| tx.with_signer(signer))
805 .ok()
806 }
807
808 fn try_from_eip4844(
809 tx: RecoveredTx<Self::Consensus>,
810 sidecar: BlobTransactionSidecar,
811 ) -> Option<Self> {
812 let (tx, signer) = tx.to_components();
813 Self::Pooled::try_from_blob_transaction(tx, sidecar)
814 .map(|tx| tx.with_signer(signer))
815 .ok()
816 .map(Self::from_pooled)
817 }
818
819 fn validate_blob(
820 &self,
821 _blob: &BlobTransactionSidecar,
822 _settings: &reth_primitives::kzg::KzgSettings,
823 ) -> Result<(), alloy_eips::eip4844::BlobTransactionValidationError> {
824 match &self {
825 Self::Eip4844 { .. } => Ok(()),
826 _ => Err(BlobTransactionValidationError::NotBlobTransaction(self.tx_type())),
827 }
828 }
829
830 fn authorization_count(&self) -> usize {
831 0
832 }
833}
834
835impl TryFrom<RecoveredTx> for MockTransaction {
836 type Error = TryFromRecoveredTransactionError;
837
838 fn try_from(tx: RecoveredTx) -> Result<Self, Self::Error> {
839 let sender = tx.signer();
840 let transaction = tx.into_signed();
841 let hash = transaction.hash();
842 let size = transaction.size();
843
844 #[allow(unreachable_patterns)]
845 match transaction.transaction {
846 Transaction::Legacy(TxLegacy {
847 chain_id,
848 nonce,
849 gas_price,
850 gas_limit,
851 to,
852 value,
853 input,
854 }) => Ok(Self::Legacy {
855 chain_id,
856 hash,
857 sender,
858 nonce,
859 gas_price,
860 gas_limit,
861 to,
862 value,
863 input,
864 size,
865 cost: U256::from(gas_limit) * U256::from(gas_price) + value,
866 }),
867 Transaction::Eip2930(TxEip2930 {
868 chain_id,
869 nonce,
870 gas_price,
871 gas_limit,
872 to,
873 value,
874 input,
875 access_list,
876 }) => Ok(Self::Eip2930 {
877 chain_id,
878 hash,
879 sender,
880 nonce,
881 gas_price,
882 gas_limit,
883 to,
884 value,
885 input,
886 access_list,
887 size,
888 cost: U256::from(gas_limit) * U256::from(gas_price) + value,
889 }),
890 Transaction::Eip1559(TxEip1559 {
891 chain_id,
892 nonce,
893 gas_limit,
894 max_fee_per_gas,
895 max_priority_fee_per_gas,
896 to,
897 value,
898 input,
899 access_list,
900 }) => Ok(Self::Eip1559 {
901 chain_id,
902 hash,
903 sender,
904 nonce,
905 max_fee_per_gas,
906 max_priority_fee_per_gas,
907 gas_limit,
908 to,
909 value,
910 input,
911 access_list,
912 size,
913 cost: U256::from(gas_limit) * U256::from(max_fee_per_gas) + value,
914 }),
915 Transaction::Eip4844(TxEip4844 {
916 chain_id,
917 nonce,
918 gas_limit,
919 max_fee_per_gas,
920 max_priority_fee_per_gas,
921 to,
922 value,
923 input,
924 access_list,
925 blob_versioned_hashes: _,
926 max_fee_per_blob_gas,
927 }) => Ok(Self::Eip4844 {
928 chain_id,
929 hash,
930 sender,
931 nonce,
932 max_fee_per_gas,
933 max_priority_fee_per_gas,
934 max_fee_per_blob_gas,
935 gas_limit,
936 to,
937 value,
938 input,
939 access_list,
940 sidecar: BlobTransactionSidecar::default(),
941 size,
942 cost: U256::from(gas_limit) * U256::from(max_fee_per_gas) + value,
943 }),
944 _ => unreachable!("Invalid transaction type"),
945 }
946 }
947}
948
949impl From<PooledTransactionsElementEcRecovered> for MockTransaction {
950 fn from(tx: PooledTransactionsElementEcRecovered) -> Self {
951 tx.into_ecrecovered_transaction().try_into().expect(
952 "Failed to convert from PooledTransactionsElementEcRecovered to MockTransaction",
953 )
954 }
955}
956
957impl From<MockTransaction> for RecoveredTx {
958 fn from(tx: MockTransaction) -> Self {
959 let signed_tx =
960 TransactionSigned::new(tx.clone().into(), Signature::test_signature(), *tx.hash());
961
962 Self::from_signed_transaction(signed_tx, tx.sender())
963 }
964}
965
966impl From<MockTransaction> for Transaction {
967 fn from(mock: MockTransaction) -> Self {
968 match mock {
969 MockTransaction::Legacy {
970 chain_id,
971 nonce,
972 gas_price,
973 gas_limit,
974 to,
975 value,
976 input,
977 ..
978 } => Self::Legacy(TxLegacy { chain_id, nonce, gas_price, gas_limit, to, value, input }),
979 MockTransaction::Eip2930 {
980 chain_id,
981 nonce,
982 gas_price,
983 gas_limit,
984 to,
985 value,
986 access_list,
987 input,
988 ..
989 } => Self::Eip2930(TxEip2930 {
990 chain_id,
991 nonce,
992 gas_price,
993 gas_limit,
994 to,
995 value,
996 access_list,
997 input,
998 }),
999 MockTransaction::Eip1559 {
1000 chain_id,
1001 nonce,
1002 gas_limit,
1003 max_fee_per_gas,
1004 max_priority_fee_per_gas,
1005 to,
1006 value,
1007 access_list,
1008 input,
1009 ..
1010 } => Self::Eip1559(TxEip1559 {
1011 chain_id,
1012 nonce,
1013 gas_limit,
1014 max_fee_per_gas,
1015 max_priority_fee_per_gas,
1016 to,
1017 value,
1018 access_list,
1019 input,
1020 }),
1021 MockTransaction::Eip4844 {
1022 chain_id,
1023 nonce,
1024 gas_limit,
1025 max_fee_per_gas,
1026 max_priority_fee_per_gas,
1027 to,
1028 value,
1029 access_list,
1030 sidecar,
1031 max_fee_per_blob_gas,
1032 input,
1033 ..
1034 } => Self::Eip4844(TxEip4844 {
1035 chain_id,
1036 nonce,
1037 gas_limit,
1038 max_fee_per_gas,
1039 max_priority_fee_per_gas,
1040 to,
1041 value,
1042 access_list,
1043 blob_versioned_hashes: sidecar.versioned_hashes().collect(),
1044 max_fee_per_blob_gas,
1045 input,
1046 }),
1047 }
1048 }
1049}
1050
1051#[cfg(any(test, feature = "arbitrary"))]
1052impl proptest::arbitrary::Arbitrary for MockTransaction {
1053 type Parameters = ();
1054 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1055 use proptest::prelude::Strategy;
1056 use proptest_arbitrary_interop::arb;
1057
1058 arb::<(TransactionSigned, Address)>()
1059 .prop_map(|(signed_transaction, signer)| {
1060 RecoveredTx::from_signed_transaction(signed_transaction, signer)
1061 .try_into()
1062 .expect("Failed to create an Arbitrary MockTransaction via RecoveredTx")
1063 })
1064 .boxed()
1065 }
1066
1067 type Strategy = proptest::strategy::BoxedStrategy<Self>;
1068}
1069
1070#[derive(Debug, Default)]
1072pub struct MockTransactionFactory {
1073 pub(crate) ids: SenderIdentifiers,
1074}
1075
1076impl MockTransactionFactory {
1079 pub fn tx_id(&mut self, tx: &MockTransaction) -> TransactionId {
1081 let sender = self.ids.sender_id_or_create(tx.sender());
1082 TransactionId::new(sender, tx.nonce())
1083 }
1084
1085 pub fn validated(&mut self, transaction: MockTransaction) -> MockValidTx {
1087 self.validated_with_origin(TransactionOrigin::External, transaction)
1088 }
1089
1090 pub fn validated_arc(&mut self, transaction: MockTransaction) -> Arc<MockValidTx> {
1092 Arc::new(self.validated(transaction))
1093 }
1094
1095 pub fn validated_with_origin(
1097 &mut self,
1098 origin: TransactionOrigin,
1099 transaction: MockTransaction,
1100 ) -> MockValidTx {
1101 MockValidTx {
1102 propagate: false,
1103 transaction_id: self.tx_id(&transaction),
1104 transaction,
1105 timestamp: Instant::now(),
1106 origin,
1107 }
1108 }
1109
1110 pub fn create_legacy(&mut self) -> MockValidTx {
1112 self.validated(MockTransaction::legacy())
1113 }
1114
1115 pub fn create_eip1559(&mut self) -> MockValidTx {
1117 self.validated(MockTransaction::eip1559())
1118 }
1119
1120 pub fn create_eip4844(&mut self) -> MockValidTx {
1122 self.validated(MockTransaction::eip4844())
1123 }
1124}
1125
1126pub type MockOrdering = CoinbaseTipOrdering<MockTransaction>;
1128
1129#[derive(Debug, Clone)]
1132pub struct MockTransactionRatio {
1133 pub legacy_pct: u32,
1135 pub access_list_pct: u32,
1137 pub dynamic_fee_pct: u32,
1139 pub blob_pct: u32,
1141}
1142
1143impl MockTransactionRatio {
1144 pub fn new(legacy_pct: u32, access_list_pct: u32, dynamic_fee_pct: u32, blob_pct: u32) -> Self {
1150 let total = legacy_pct + access_list_pct + dynamic_fee_pct + blob_pct;
1151 assert_eq!(
1152 total,
1153 100,
1154 "percentages must sum up to 100, instead got legacy: {legacy_pct}, access_list: {access_list_pct}, dynamic_fee: {dynamic_fee_pct}, blob: {blob_pct}, total: {total}",
1155 );
1156
1157 Self { legacy_pct, access_list_pct, dynamic_fee_pct, blob_pct }
1158 }
1159
1160 pub fn weighted_index(&self) -> WeightedIndex<u32> {
1168 WeightedIndex::new([
1169 self.legacy_pct,
1170 self.access_list_pct,
1171 self.dynamic_fee_pct,
1172 self.blob_pct,
1173 ])
1174 .unwrap()
1175 }
1176}
1177
1178#[derive(Debug, Clone)]
1180pub struct MockFeeRange {
1181 pub gas_price: Uniform<u128>,
1183 pub priority_fee: Uniform<u128>,
1185 pub max_fee: Uniform<u128>,
1187 pub max_fee_blob: Uniform<u128>,
1189}
1190
1191impl MockFeeRange {
1192 pub fn new(
1197 gas_price: Range<u128>,
1198 priority_fee: Range<u128>,
1199 max_fee: Range<u128>,
1200 max_fee_blob: Range<u128>,
1201 ) -> Self {
1202 assert!(
1203 max_fee.start <= priority_fee.end,
1204 "max_fee_range should be strictly below the priority fee range"
1205 );
1206 Self {
1207 gas_price: gas_price.into(),
1208 priority_fee: priority_fee.into(),
1209 max_fee: max_fee.into(),
1210 max_fee_blob: max_fee_blob.into(),
1211 }
1212 }
1213
1214 pub fn sample_gas_price(&self, rng: &mut impl rand::Rng) -> u128 {
1217 self.gas_price.sample(rng)
1218 }
1219
1220 pub fn sample_priority_fee(&self, rng: &mut impl rand::Rng) -> u128 {
1223 self.priority_fee.sample(rng)
1224 }
1225
1226 pub fn sample_max_fee(&self, rng: &mut impl rand::Rng) -> u128 {
1229 self.max_fee.sample(rng)
1230 }
1231
1232 pub fn sample_max_fee_blob(&self, rng: &mut impl rand::Rng) -> u128 {
1235 self.max_fee_blob.sample(rng)
1236 }
1237}
1238
1239#[derive(Debug, Clone)]
1241pub struct MockTransactionDistribution {
1242 transaction_ratio: MockTransactionRatio,
1244 gas_limit_range: Uniform<u64>,
1246 size_range: Uniform<usize>,
1248 fee_ranges: MockFeeRange,
1250}
1251
1252impl MockTransactionDistribution {
1253 pub fn new(
1255 transaction_ratio: MockTransactionRatio,
1256 fee_ranges: MockFeeRange,
1257 gas_limit_range: Range<u64>,
1258 size_range: Range<usize>,
1259 ) -> Self {
1260 Self {
1261 transaction_ratio,
1262 gas_limit_range: gas_limit_range.into(),
1263 fee_ranges,
1264 size_range: size_range.into(),
1265 }
1266 }
1267
1268 pub fn tx(&self, nonce: u64, rng: &mut impl rand::Rng) -> MockTransaction {
1270 let transaction_sample = self.transaction_ratio.weighted_index().sample(rng);
1271 let tx = match transaction_sample {
1272 0 => MockTransaction::legacy().with_gas_price(self.fee_ranges.sample_gas_price(rng)),
1273 1 => MockTransaction::eip2930().with_gas_price(self.fee_ranges.sample_gas_price(rng)),
1274 2 => MockTransaction::eip1559()
1275 .with_priority_fee(self.fee_ranges.sample_priority_fee(rng))
1276 .with_max_fee(self.fee_ranges.sample_max_fee(rng)),
1277 3 => MockTransaction::eip4844()
1278 .with_priority_fee(self.fee_ranges.sample_priority_fee(rng))
1279 .with_max_fee(self.fee_ranges.sample_max_fee(rng))
1280 .with_blob_fee(self.fee_ranges.sample_max_fee_blob(rng)),
1281 _ => unreachable!("unknown transaction type returned by the weighted index"),
1282 };
1283
1284 let size = self.size_range.sample(rng);
1285
1286 tx.with_nonce(nonce).with_gas_limit(self.gas_limit_range.sample(rng)).with_size(size)
1287 }
1288
1289 pub fn tx_set(
1293 &self,
1294 sender: Address,
1295 nonce_range: Range<u64>,
1296 rng: &mut impl rand::Rng,
1297 ) -> MockTransactionSet {
1298 let txs =
1299 nonce_range.map(|nonce| self.tx(nonce, rng).with_sender(sender)).collect::<Vec<_>>();
1300 MockTransactionSet::new(txs)
1301 }
1302
1303 pub fn tx_set_non_conflicting_types(
1309 &self,
1310 sender: Address,
1311 nonce_range: Range<u64>,
1312 rng: &mut impl rand::Rng,
1313 ) -> NonConflictingSetOutcome {
1314 let mut modified_distribution = self.clone();
1322 let first_tx = self.tx(nonce_range.start, rng);
1323
1324 if first_tx.is_eip4844() {
1327 modified_distribution.transaction_ratio = MockTransactionRatio {
1328 legacy_pct: 0,
1329 access_list_pct: 0,
1330 dynamic_fee_pct: 0,
1331 blob_pct: 100,
1332 };
1333
1334 NonConflictingSetOutcome::BlobsOnly(modified_distribution.tx_set(
1336 sender,
1337 nonce_range,
1338 rng,
1339 ))
1340 } else {
1341 let MockTransactionRatio { legacy_pct, access_list_pct, dynamic_fee_pct, .. } =
1342 modified_distribution.transaction_ratio;
1343
1344 let total_non_blob_weight: u32 = legacy_pct + access_list_pct + dynamic_fee_pct;
1346
1347 let new_weights: Vec<u32> = [legacy_pct, access_list_pct, dynamic_fee_pct]
1349 .into_iter()
1350 .map(|weight| weight * 100 / total_non_blob_weight)
1351 .collect();
1352
1353 let new_ratio = MockTransactionRatio {
1354 legacy_pct: new_weights[0],
1355 access_list_pct: new_weights[1],
1356 dynamic_fee_pct: new_weights[2],
1357 blob_pct: 0,
1358 };
1359
1360 modified_distribution.transaction_ratio = new_ratio;
1363
1364 NonConflictingSetOutcome::Mixed(modified_distribution.tx_set(sender, nonce_range, rng))
1366 }
1367 }
1368}
1369
1370#[derive(Debug, Clone)]
1373pub enum NonConflictingSetOutcome {
1374 BlobsOnly(MockTransactionSet),
1376 Mixed(MockTransactionSet),
1378}
1379
1380impl NonConflictingSetOutcome {
1381 pub fn into_inner(self) -> MockTransactionSet {
1383 match self {
1384 Self::BlobsOnly(set) | Self::Mixed(set) => set,
1385 }
1386 }
1387
1388 pub fn with_nonce_gaps(
1396 &mut self,
1397 gap_pct: u32,
1398 gap_range: Range<u64>,
1399 rng: &mut impl rand::Rng,
1400 ) {
1401 match self {
1402 Self::BlobsOnly(_) => {}
1403 Self::Mixed(set) => set.with_nonce_gaps(gap_pct, gap_range, rng),
1404 }
1405 }
1406}
1407
1408#[derive(Debug, Clone)]
1410pub struct MockTransactionSet {
1411 pub(crate) transactions: Vec<MockTransaction>,
1412}
1413
1414impl MockTransactionSet {
1415 const fn new(transactions: Vec<MockTransaction>) -> Self {
1417 Self { transactions }
1418 }
1419
1420 pub fn dependent(sender: Address, from_nonce: u64, tx_count: usize, tx_type: TxType) -> Self {
1427 let mut txs = Vec::with_capacity(tx_count);
1428 let mut curr_tx =
1429 MockTransaction::new_from_type(tx_type).with_nonce(from_nonce).with_sender(sender);
1430 for _ in 0..tx_count {
1431 txs.push(curr_tx.clone());
1432 curr_tx = curr_tx.next();
1433 }
1434
1435 Self::new(txs)
1436 }
1437
1438 pub fn sequential_transactions_by_sender(
1443 sender: Address,
1444 tx_count: usize,
1445 tx_type: TxType,
1446 ) -> Self {
1447 Self::dependent(sender, 0, tx_count, tx_type)
1448 }
1449
1450 pub fn with_nonce_gaps(
1464 &mut self,
1465 gap_pct: u32,
1466 gap_range: Range<u64>,
1467 rng: &mut impl rand::Rng,
1468 ) {
1469 assert!(gap_pct <= 100, "gap_pct must be between 0 and 100");
1470 assert!(gap_range.start >= 1, "gap_range must have a lower bound of at least one");
1471
1472 let mut prev_nonce = 0;
1473 for tx in &mut self.transactions {
1474 if rng.gen_bool(gap_pct as f64 / 100.0) {
1475 prev_nonce += gap_range.start;
1476 } else {
1477 prev_nonce += 1;
1478 }
1479 tx.set_nonce(prev_nonce);
1480 }
1481 }
1482
1483 pub fn extend<T: IntoIterator<Item = MockTransaction>>(&mut self, txs: T) {
1485 self.transactions.extend(txs);
1486 }
1487
1488 pub fn into_vec(self) -> Vec<MockTransaction> {
1490 self.transactions
1491 }
1492
1493 pub fn iter(&self) -> impl Iterator<Item = &MockTransaction> {
1495 self.transactions.iter()
1496 }
1497
1498 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut MockTransaction> {
1500 self.transactions.iter_mut()
1501 }
1502}
1503
1504impl IntoIterator for MockTransactionSet {
1505 type Item = MockTransaction;
1506 type IntoIter = IntoIter<MockTransaction>;
1507
1508 fn into_iter(self) -> Self::IntoIter {
1509 self.transactions.into_iter()
1510 }
1511}
1512
1513#[test]
1514fn test_mock_priority() {
1515 use crate::TransactionOrdering;
1516
1517 let o = MockOrdering::default();
1518 let lo = MockTransaction::eip1559().with_gas_limit(100_000);
1519 let hi = lo.next().inc_price();
1520 assert!(o.priority(&hi, 0) > o.priority(&lo, 0));
1521}