1use crate::{
4 blobstore::{BlobSidecarConverter, BlobStoreCanonTracker, BlobStoreUpdates},
5 error::PoolError,
6 metrics::MaintainPoolMetrics,
7 traits::{CanonicalStateUpdate, EthPoolTransaction, TransactionPool, TransactionPoolExt},
8 AllPoolTransactions, BlobTransactionSidecarVariant, BlockInfo, PoolTransaction, PoolUpdateKind,
9 TransactionOrigin,
10};
11use alloy_consensus::{transaction::TxHashRef, BlockHeader, Typed2718};
12use alloy_eips::{merge::SLOT_DURATION_SECS, BlockNumberOrTag, Decodable2718};
13use alloy_primitives::{
14 map::{AddressSet, B256Set, HashSet},
15 Address, BlockHash, BlockNumber, Bytes,
16};
17use alloy_rlp::Encodable;
18use futures_util::{
19 future::{BoxFuture, Fuse, FusedFuture},
20 FutureExt, Stream, StreamExt,
21};
22use reth_chain_state::CanonStateNotification;
23use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
24use reth_execution_types::ChangedAccount;
25use reth_fs_util::FsPathError;
26use reth_primitives_traits::{
27 transaction::signed::SignedTransaction, NodePrimitives, SealedHeader,
28};
29use reth_storage_api::{errors::provider::ProviderError, BlockReaderIdExt, StateProviderFactory};
30use reth_tasks::Runtime;
31use serde::{Deserialize, Serialize};
32use std::{
33 borrow::Borrow,
34 hash::{Hash, Hasher},
35 path::{Path, PathBuf},
36 sync::Arc,
37};
38use tokio::{
39 sync::oneshot,
40 time::{self, Duration},
41};
42use tracing::{debug, error, info, trace, warn};
43
44pub const MAX_QUEUED_TRANSACTION_LIFETIME: Duration = Duration::from_secs(3 * 60 * 60);
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct MaintainPoolConfig {
50 pub max_update_depth: u64,
55 pub max_reload_accounts: usize,
59
60 pub max_tx_lifetime: Duration,
63
64 pub no_local_exemptions: bool,
70}
71
72impl Default for MaintainPoolConfig {
73 fn default() -> Self {
74 Self {
75 max_update_depth: 64,
76 max_reload_accounts: 100,
77 max_tx_lifetime: MAX_QUEUED_TRANSACTION_LIFETIME,
78 no_local_exemptions: false,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct LocalTransactionBackupConfig {
86 pub transactions_path: Option<PathBuf>,
88}
89
90impl LocalTransactionBackupConfig {
91 pub const fn with_local_txs_backup(transactions_path: PathBuf) -> Self {
93 Self { transactions_path: Some(transactions_path) }
94 }
95}
96
97pub fn maintain_transaction_pool_future<N, Client, P, St>(
99 client: Client,
100 pool: P,
101 events: St,
102 task_spawner: Runtime,
103 config: MaintainPoolConfig,
104) -> BoxFuture<'static, ()>
105where
106 N: NodePrimitives,
107 Client: StateProviderFactory
108 + BlockReaderIdExt<Header = N::BlockHeader>
109 + ChainSpecProvider<ChainSpec: EthChainSpec<Header = N::BlockHeader> + EthereumHardforks>
110 + Clone
111 + 'static,
112 P: TransactionPoolExt<Transaction: PoolTransaction<Consensus = N::SignedTx>, Block = N::Block>
113 + 'static,
114 St: Stream<Item = CanonStateNotification<N>> + Send + Unpin + 'static,
115{
116 async move {
117 maintain_transaction_pool(client, pool, events, task_spawner, config).await;
118 }
119 .boxed()
120}
121
122pub async fn maintain_transaction_pool<N, Client, P, St>(
126 client: Client,
127 pool: P,
128 mut events: St,
129 task_spawner: Runtime,
130 config: MaintainPoolConfig,
131) where
132 N: NodePrimitives,
133 Client: StateProviderFactory
134 + BlockReaderIdExt<Header = N::BlockHeader>
135 + ChainSpecProvider<ChainSpec: EthChainSpec<Header = N::BlockHeader> + EthereumHardforks>
136 + Clone
137 + 'static,
138 P: TransactionPoolExt<Transaction: PoolTransaction<Consensus = N::SignedTx>, Block = N::Block>
139 + 'static,
140 St: Stream<Item = CanonStateNotification<N>> + Send + Unpin + 'static,
141{
142 let metrics = MaintainPoolMetrics::default();
143 let MaintainPoolConfig { max_update_depth, max_reload_accounts, .. } = config;
144 if let Ok(Some(latest)) = client.header_by_number_or_tag(BlockNumberOrTag::Latest) {
146 let latest = SealedHeader::seal_slow(latest);
147 let chain_spec = client.chain_spec();
148 let info = BlockInfo {
149 block_gas_limit: latest.gas_limit(),
150 last_seen_block_hash: latest.hash(),
151 last_seen_block_number: latest.number(),
152 pending_basefee: chain_spec
153 .next_block_base_fee(latest.header(), latest.timestamp())
154 .unwrap_or_default(),
155 pending_blob_fee: latest
156 .maybe_next_block_blob_fee(chain_spec.blob_params_at_timestamp(latest.timestamp())),
157 };
158 pool.set_block_info(info);
159 }
160
161 let mut blob_store_tracker = BlobStoreCanonTracker::default();
163
164 let blob_store = pool.blob_store();
166
167 let mut last_finalized_block =
169 FinalizedBlockTracker::new(client.finalized_block_number().ok().flatten());
170
171 let mut dirty_addresses = HashSet::default();
173
174 let mut maintained_state = MaintainedPoolState::InSync;
176
177 let mut reload_accounts_fut = Fuse::terminated();
179
180 let mut stale_eviction_interval = time::interval(config.max_tx_lifetime);
182
183 let mut first_event = true;
185
186 loop {
189 trace!(target: "txpool", state=?maintained_state, "awaiting new block or reorg");
190
191 metrics.set_dirty_accounts_len(dirty_addresses.len());
192 let pool_info = pool.block_info();
193
194 if maintained_state.is_drifted() {
198 metrics.inc_drift();
199 dirty_addresses = pool.unique_senders();
201 maintained_state = MaintainedPoolState::InSync;
203 }
204
205 if !dirty_addresses.is_empty() && reload_accounts_fut.is_terminated() {
207 let (tx, rx) = oneshot::channel();
208 let c = client.clone();
209 let at = pool_info.last_seen_block_hash;
210 let fut = if dirty_addresses.len() > max_reload_accounts {
211 let accs_to_reload =
213 dirty_addresses.iter().copied().take(max_reload_accounts).collect::<Vec<_>>();
214 for acc in &accs_to_reload {
215 dirty_addresses.remove(acc);
217 }
218 async move {
219 let res = load_accounts(c, at, accs_to_reload);
220 let _ = tx.send(res);
221 }
222 .boxed()
223 } else {
224 let accs_to_reload = std::mem::take(&mut dirty_addresses);
226 async move {
227 let res = load_accounts(c, at, accs_to_reload);
228 let _ = tx.send(res);
229 }
230 .boxed()
231 };
232 reload_accounts_fut = rx.fuse();
233 task_spawner.spawn_blocking_task(fut);
234 }
235
236 if let Some(finalized) =
238 last_finalized_block.update(client.finalized_block_number().ok().flatten())
239 {
240 if let BlobStoreUpdates::Finalized(blobs) =
241 blob_store_tracker.on_finalized_block(finalized)
242 {
243 metrics.inc_deleted_tracked_blobs(blobs.len());
244 pool.delete_blobs(blobs);
246 }
247 if blob_store.blobs_len() > 0 {
250 let pool = pool.clone();
251 task_spawner.spawn_blocking_task(async move {
252 debug!(target: "txpool", finalized_block = %finalized, "cleaning up blob store");
253 pool.cleanup_blobs();
254 });
255 }
256 }
257
258 let mut event = None;
260 let mut reloaded = None;
261
262 tokio::select! {
265 res = &mut reload_accounts_fut => {
266 reloaded = Some(res);
267 }
268 ev = events.next() => {
269 if ev.is_none() {
270 break;
272 }
273 event = ev;
274 if first_event {
277 maintained_state = MaintainedPoolState::Drifted;
278 first_event = false
279 }
280 }
281 _ = stale_eviction_interval.tick() => {
282 let queued = pool
283 .queued_transactions();
284 let mut stale_blobs = Vec::new();
285 let now = std::time::Instant::now();
286 let stale_txs: Vec<_> = queued
287 .into_iter()
288 .filter(|tx| {
289 (tx.origin.is_external() || config.no_local_exemptions) && now - tx.timestamp > config.max_tx_lifetime
291 })
292 .map(|tx| {
293 if tx.is_eip4844() {
294 stale_blobs.push(*tx.hash());
295 }
296 *tx.hash()
297 })
298 .collect();
299 debug!(target: "txpool", count=%stale_txs.len(), "removing stale transactions");
300 pool.remove_transactions(stale_txs);
301 pool.delete_blobs(stale_blobs);
302 }
303 }
304 match reloaded {
306 Some(Ok(Ok(LoadedAccounts { accounts, failed_to_load }))) => {
307 dirty_addresses.extend(failed_to_load);
310 pool.update_accounts(accounts);
312 }
313 Some(Ok(Err(res))) => {
314 let (accs, err) = *res;
316 debug!(target: "txpool", %err, "failed to load accounts");
317 dirty_addresses.extend(accs);
318 }
319 Some(Err(_)) => {
320 maintained_state = MaintainedPoolState::Drifted;
322 }
323 None => {}
324 }
325
326 let Some(event) = event else { continue };
328 match event {
329 CanonStateNotification::Reorg { old, new } => {
330 let (old_blocks, old_state) = old.inner();
331 let (new_blocks, new_state) = new.inner();
332 let new_tip = new_blocks.tip();
333 let new_first = new_blocks.first();
334 let old_first = old_blocks.first();
335
336 if !(old_first.parent_hash() == pool_info.last_seen_block_hash ||
338 new_first.parent_hash() == pool_info.last_seen_block_hash)
339 {
340 maintained_state = MaintainedPoolState::Drifted;
342 }
343
344 let chain_spec = client.chain_spec();
345
346 let pending_block_base_fee = chain_spec
348 .next_block_base_fee(new_tip.header(), new_tip.timestamp())
349 .unwrap_or_default();
350 let pending_block_blob_fee = new_tip.header().maybe_next_block_blob_fee(
351 chain_spec.blob_params_at_timestamp(new_tip.timestamp()),
352 );
353
354 let new_changed_accounts: HashSet<_> =
356 new_state.changed_accounts().map(ChangedAccountEntry).collect();
357
358 let missing_changed_acc = old_state
360 .accounts_iter()
361 .map(|(a, _)| a)
362 .filter(|addr| !new_changed_accounts.contains(addr));
363
364 let mut changed_accounts =
366 match load_accounts(client.clone(), new_tip.hash(), missing_changed_acc) {
367 Ok(LoadedAccounts { accounts, failed_to_load }) => {
368 dirty_addresses.extend(failed_to_load);
370
371 accounts
372 }
373 Err(err) => {
374 let (addresses, err) = *err;
375 debug!(
376 target: "txpool",
377 %err,
378 "failed to load missing changed accounts at new tip: {:?}",
379 new_tip.hash()
380 );
381 dirty_addresses.extend(addresses);
382 vec![]
383 }
384 };
385
386 changed_accounts.extend(new_changed_accounts.into_iter().map(|entry| entry.0));
389
390 let mined_transactions = new_blocks.transaction_hashes_vec();
392 let new_mined_transactions =
393 mined_transactions.iter().copied().collect::<B256Set>();
394
395 let pruned_old_transactions = old_blocks
398 .transactions_with_sender()
399 .filter(|(_, tx)| !new_mined_transactions.contains(tx.tx_hash()))
400 .filter_map(|(signer, tx)| {
401 let tx = tx.clone().with_signer(*signer);
402 if tx.is_eip4844() {
403 pool.get_blob(*tx.tx_hash())
409 .ok()
410 .flatten()
411 .map(Arc::unwrap_or_clone)
412 .and_then(|sidecar| {
413 <P as TransactionPool>::Transaction::try_from_eip4844(
414 tx, sidecar,
415 )
416 })
417 } else {
418 <P as TransactionPool>::Transaction::try_from_consensus(tx).ok()
419 }
420 })
421 .collect::<Vec<_>>();
422
423 let update = CanonicalStateUpdate {
425 new_tip: new_tip.sealed_block(),
426 pending_block_base_fee,
427 pending_block_blob_fee,
428 changed_accounts,
429 mined_transactions,
431 update_kind: PoolUpdateKind::Reorg,
432 };
433 pool.on_canonical_state_change(update);
434
435 metrics.inc_reinserted_transactions(pruned_old_transactions.len());
442 let _ = pool.add_external_transactions(pruned_old_transactions).await;
443
444 blob_store_tracker.add_new_chain_blocks(&new_blocks);
446 }
447 CanonStateNotification::Commit { new } => {
448 let (blocks, state) = new.inner();
449 let tip = blocks.tip();
450 let chain_spec = client.chain_spec();
451
452 let pending_block_base_fee = chain_spec
454 .next_block_base_fee(tip.header(), tip.timestamp())
455 .unwrap_or_default();
456 let pending_block_blob_fee = tip.header().maybe_next_block_blob_fee(
457 chain_spec.blob_params_at_timestamp(tip.timestamp()),
458 );
459
460 let first_block = blocks.first();
461 trace!(
462 target: "txpool",
463 first = first_block.number(),
464 tip = tip.number(),
465 pool_block = pool_info.last_seen_block_number,
466 "update pool on new commit"
467 );
468
469 let depth = tip.number().abs_diff(pool_info.last_seen_block_number);
472 if depth > max_update_depth {
473 maintained_state = MaintainedPoolState::Drifted;
474 debug!(target: "txpool", ?depth, "skipping deep canonical update");
475 let update = CanonicalStateUpdate {
478 new_tip: tip.sealed_block(),
479 pending_block_base_fee,
480 pending_block_blob_fee,
481 changed_accounts: Vec::new(),
482 mined_transactions: Vec::new(),
483 update_kind: PoolUpdateKind::Commit,
484 };
485 pool.on_canonical_state_change(update);
486
487 blob_store_tracker.add_new_chain_blocks(&blocks);
489
490 continue
491 }
492
493 let mut changed_accounts = Vec::with_capacity(state.state().len());
494 for acc in state.changed_accounts() {
495 dirty_addresses.remove(&acc.address);
497 changed_accounts.push(acc);
498 }
499
500 let mined_transactions = blocks.transaction_hashes_vec();
501
502 if first_block.parent_hash() != pool_info.last_seen_block_hash {
504 maintained_state = MaintainedPoolState::Drifted;
508 }
509
510 let update = CanonicalStateUpdate {
512 new_tip: tip.sealed_block(),
513 pending_block_base_fee,
514 pending_block_blob_fee,
515 changed_accounts,
516 mined_transactions,
517 update_kind: PoolUpdateKind::Commit,
518 };
519 pool.on_canonical_state_change(update);
520
521 blob_store_tracker.add_new_chain_blocks(&blocks);
523
524 if !chain_spec.is_osaka_active_at_timestamp(tip.timestamp()) &&
526 !chain_spec.is_osaka_active_at_timestamp(
527 tip.timestamp().saturating_add(SLOT_DURATION_SECS),
528 ) &&
529 chain_spec.is_osaka_active_at_timestamp(
530 tip.timestamp().saturating_add(2 * SLOT_DURATION_SECS),
531 )
532 {
533 let pool = pool.clone();
534 let spawner = task_spawner.clone();
535 let client = client.clone();
536 task_spawner.spawn_task(async move {
537 tokio::time::sleep(Duration::from_secs(4)).await;
541
542 let mut interval = tokio::time::interval(Duration::from_secs(1));
543 loop {
544 let last_iteration =
547 client.latest_header().ok().flatten().is_none_or(|header| {
548 client
549 .chain_spec()
550 .is_osaka_active_at_timestamp(header.timestamp())
551 });
552
553 let AllPoolTransactions { pending, queued } = pool.all_transactions();
554 for tx in pending.into_iter().chain(queued).filter(|tx| tx.is_eip4844())
555 {
556 let tx_hash = *tx.hash();
557
558 let Ok(Some(sidecar)) = pool.get_blob(tx_hash) else {
560 continue;
561 };
562 if !sidecar.is_eip4844() {
564 continue;
565 }
566 let Some(tx) = pool.remove_transactions(vec![tx_hash]).pop() else {
569 continue;
570 };
571 pool.delete_blob(tx_hash);
572
573 let BlobTransactionSidecarVariant::Eip4844(sidecar) =
574 Arc::unwrap_or_clone(sidecar)
575 else {
576 continue;
577 };
578
579 let converter = BlobSidecarConverter::new();
580 let pool = pool.clone();
581 spawner.spawn_task(async move {
582 let Some(sidecar) = converter.convert(sidecar).await else {
584 return;
585 };
586
587 let origin = tx.origin;
589 let Some(tx) = EthPoolTransaction::try_from_eip4844(
590 tx.to_consensus(),
591 sidecar.into(),
592 ) else {
593 return;
594 };
595 let _ = pool.add_transaction(origin, tx).await;
596 });
597 }
598
599 if last_iteration {
600 break;
601 }
602
603 interval.tick().await;
604 }
605 });
606 }
607 }
608 }
609 }
610}
611
612struct FinalizedBlockTracker {
613 last_finalized_block: Option<BlockNumber>,
614}
615
616impl FinalizedBlockTracker {
617 const fn new(last_finalized_block: Option<BlockNumber>) -> Self {
618 Self { last_finalized_block }
619 }
620
621 fn update(&mut self, finalized_block: Option<BlockNumber>) -> Option<BlockNumber> {
623 let finalized = finalized_block?;
624 self.last_finalized_block.is_none_or(|last| last < finalized).then(|| {
625 self.last_finalized_block = Some(finalized);
626 finalized
627 })
628 }
629}
630
631#[derive(Debug, PartialEq, Eq)]
634enum MaintainedPoolState {
635 InSync,
637 Drifted,
639}
640
641impl MaintainedPoolState {
642 #[inline]
644 const fn is_drifted(&self) -> bool {
645 matches!(self, Self::Drifted)
646 }
647}
648
649#[derive(Eq)]
651struct ChangedAccountEntry(ChangedAccount);
652
653impl PartialEq for ChangedAccountEntry {
654 fn eq(&self, other: &Self) -> bool {
655 self.0.address == other.0.address
656 }
657}
658
659impl Hash for ChangedAccountEntry {
660 fn hash<H: Hasher>(&self, state: &mut H) {
661 self.0.address.hash(state);
662 }
663}
664
665impl Borrow<Address> for ChangedAccountEntry {
666 fn borrow(&self) -> &Address {
667 &self.0.address
668 }
669}
670
671#[derive(Default)]
672struct LoadedAccounts {
673 accounts: Vec<ChangedAccount>,
675 failed_to_load: Vec<Address>,
677}
678
679fn load_accounts<Client, I>(
685 client: Client,
686 at: BlockHash,
687 addresses: I,
688) -> Result<LoadedAccounts, Box<(AddressSet, ProviderError)>>
689where
690 I: IntoIterator<Item = Address>,
691 Client: StateProviderFactory,
692{
693 let addresses = addresses.into_iter();
694 let mut res = LoadedAccounts::default();
695 let state = match client.history_by_block_hash(at) {
696 Ok(state) => state,
697 Err(err) => return Err(Box::new((addresses.collect(), err))),
698 };
699 for addr in addresses {
700 if let Ok(maybe_acc) = state.basic_account(&addr) {
701 let acc = maybe_acc
702 .map(|acc| ChangedAccount { address: addr, nonce: acc.nonce, balance: acc.balance })
703 .unwrap_or_else(|| ChangedAccount::empty(addr));
704 res.accounts.push(acc)
705 } else {
706 res.failed_to_load.push(addr);
708 }
709 }
710 Ok(res)
711}
712
713async fn load_and_reinsert_transactions<P>(
717 pool: P,
718 file_path: &Path,
719) -> Result<(), TransactionsBackupError>
720where
721 P: TransactionPool<Transaction: PoolTransaction<Consensus: SignedTransaction>>,
722{
723 if !file_path.exists() {
724 return Ok(())
725 }
726
727 debug!(target: "txpool", txs_file =?file_path, "Check local persistent storage for saved transactions");
728 let data = reth_fs_util::read(file_path)?;
729
730 if data.is_empty() {
731 return Ok(())
732 }
733
734 let pool_transactions: Vec<(TransactionOrigin, <P as TransactionPool>::Transaction)> =
735 if let Ok(tx_backups) = serde_json::from_slice::<Vec<TxBackup>>(&data) {
736 tx_backups
737 .into_iter()
738 .filter_map(|backup| {
739 let tx_signed =
740 <P::Transaction as PoolTransaction>::Consensus::decode_2718_exact(
741 backup.rlp.as_ref(),
742 )
743 .ok()?;
744 let recovered = tx_signed.try_into_recovered().ok()?;
745 let pool_tx =
746 <P::Transaction as PoolTransaction>::try_from_consensus(recovered).ok()?;
747
748 Some((backup.origin, pool_tx))
749 })
750 .collect()
751 } else {
752 let txs_signed: Vec<<P::Transaction as PoolTransaction>::Consensus> =
753 alloy_rlp::Decodable::decode(&mut data.as_slice())?;
754
755 txs_signed
756 .into_iter()
757 .filter_map(|tx| tx.try_into_recovered().ok())
758 .filter_map(|tx| {
759 <P::Transaction as PoolTransaction>::try_from_consensus(tx)
760 .ok()
761 .map(|pool_tx| (TransactionOrigin::Local, pool_tx))
762 })
763 .collect()
764 };
765
766 let inserted = futures_util::future::join_all(
767 pool_transactions.into_iter().map(|(origin, tx)| pool.add_transaction(origin, tx)),
768 )
769 .await;
770
771 info!(target: "txpool", txs_file =?file_path, num_txs=%inserted.len(), "Successfully reinserted local transactions from file");
772 reth_fs_util::remove_file(file_path)?;
773 Ok(())
774}
775
776fn save_local_txs_backup<P>(pool: P, file_path: &Path)
777where
778 P: TransactionPool<Transaction: PoolTransaction<Consensus: Encodable>>,
779{
780 let local_transactions = pool.get_local_transactions();
781 if local_transactions.is_empty() {
782 trace!(target: "txpool", "no local transactions to save");
783 return
784 }
785
786 let local_transactions = local_transactions
787 .into_iter()
788 .map(|tx| TxBackup { rlp: tx.encoded_2718_consensus(), origin: tx.origin })
789 .collect::<Vec<_>>();
790
791 let json_data = match serde_json::to_string(&local_transactions) {
792 Ok(data) => data,
793 Err(err) => {
794 warn!(target: "txpool", %err, txs_file=?file_path, "failed to serialize local transactions to json");
795 return
796 }
797 };
798
799 info!(target: "txpool", txs_file =?file_path, num_txs=%local_transactions.len(), "Saving current local transactions");
800 let parent_dir = file_path.parent().map(std::fs::create_dir_all).transpose();
801
802 match parent_dir.map(|_| reth_fs_util::write(file_path, json_data)) {
803 Ok(_) => {
804 info!(target: "txpool", txs_file=?file_path, "Wrote local transactions to file");
805 }
806 Err(err) => {
807 warn!(target: "txpool", %err, txs_file=?file_path, "Failed to write local transactions to file");
808 }
809 }
810}
811
812#[derive(Debug, Deserialize, Serialize)]
815pub struct TxBackup {
816 pub rlp: Bytes,
818 pub origin: TransactionOrigin,
820}
821
822#[derive(thiserror::Error, Debug)]
824pub enum TransactionsBackupError {
825 #[error("failed to apply transactions backup. Encountered RLP decode error: {0}")]
827 Decode(#[from] alloy_rlp::Error),
828 #[error("failed to apply transactions backup. Encountered JSON decode error: {0}")]
830 Json(#[from] serde_json::Error),
831 #[error("failed to apply transactions backup. Encountered file error: {0}")]
833 FsPath(#[from] FsPathError),
834 #[error("failed to insert transactions to the transactions pool. Encountered pool error: {0}")]
836 Pool(#[from] PoolError),
837}
838
839pub async fn backup_local_transactions_task<P>(
842 shutdown: reth_tasks::shutdown::GracefulShutdown,
843 pool: P,
844 config: LocalTransactionBackupConfig,
845) where
846 P: TransactionPool<Transaction: PoolTransaction<Consensus: SignedTransaction>>,
847{
848 let Some(transactions_path) = config.transactions_path else {
849 return
851 };
852
853 if let Err(err) = load_and_reinsert_transactions(pool.clone(), &transactions_path).await {
854 error!(target: "txpool", "{}", err)
855 }
856
857 let graceful_guard = shutdown.await;
858
859 save_local_txs_backup(pool, &transactions_path);
861
862 drop(graceful_guard)
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use crate::{
869 blobstore::InMemoryBlobStore,
870 error::{InvalidPoolTransactionError, PoolErrorKind},
871 test_utils::TransactionBuilder,
872 validate::EthTransactionValidatorBuilder,
873 CoinbaseTipOrdering, EthPooledTransaction, Pool, TransactionOrigin,
874 };
875 use alloy_consensus::Header;
876 use alloy_eips::{eip1559::ETHEREUM_BLOCK_GAS_LIMIT_30M, eip2718::Decodable2718};
877 use alloy_primitives::{address, hex, B256, U256};
878 use reth_ethereum_primitives::{Block, PooledTransactionVariant};
879 use reth_evm_ethereum::EthEvmConfig;
880 use reth_execution_types::Chain;
881 use reth_fs_util as fs;
882 use reth_primitives_traits::{RecoveredBlock, SealedBlock};
883 use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
884 use reth_tasks::Runtime;
885
886 #[test]
887 fn changed_acc_entry() {
888 let changed_acc = ChangedAccountEntry(ChangedAccount::empty(Address::random()));
889 let mut copy = changed_acc.0;
890 copy.nonce = 10;
891 assert!(changed_acc.eq(&ChangedAccountEntry(copy)));
892 }
893
894 const EXTENSION: &str = "json";
895 const FILENAME: &str = "test_transactions_backup";
896
897 #[tokio::test(flavor = "multi_thread")]
898 async fn test_save_local_txs_backup() {
899 let temp_dir = tempfile::tempdir().unwrap();
900 let transactions_path = temp_dir.path().join(FILENAME).with_extension(EXTENSION);
901 let tx_bytes = hex!(
902 "02f87201830655c2808505ef61f08482565f94388c818ca8b9251b393131c08a736a67ccb192978801049e39c4b5b1f580c001a01764ace353514e8abdfb92446de356b260e3c1225b73fc4c8876a6258d12a129a04f02294aa61ca7676061cd99f29275491218b4754b46a0248e5e42bc5091f507"
903 );
904 let tx = PooledTransactionVariant::decode_2718(&mut &tx_bytes[..]).unwrap();
905 let provider = MockEthProvider::default().with_genesis_block();
906 let transaction = EthPooledTransaction::from_pooled(tx.try_into_recovered().unwrap());
907 let tx_to_cmp = transaction.clone();
908 let sender = address!("1f9090aaE28b8a3dCeaDf281B0F12828e676c326");
909 provider.add_account(sender, ExtendedAccount::new(42, U256::MAX));
910 let blob_store = InMemoryBlobStore::default();
911 let validator = EthTransactionValidatorBuilder::new(provider, EthEvmConfig::mainnet())
912 .set_block_gas_limit(ETHEREUM_BLOCK_GAS_LIMIT_30M)
913 .build(blob_store.clone());
914
915 let txpool = Pool::new(
916 validator,
917 CoinbaseTipOrdering::default(),
918 blob_store.clone(),
919 Default::default(),
920 );
921
922 txpool.add_transaction(TransactionOrigin::Local, transaction.clone()).await.unwrap();
923
924 let rt = Runtime::test();
925 let config = LocalTransactionBackupConfig::with_local_txs_backup(transactions_path.clone());
926 rt.spawn_critical_with_graceful_shutdown_signal("test task", |shutdown| {
927 backup_local_transactions_task(shutdown, txpool.clone(), config)
928 });
929
930 let mut txns = txpool.get_local_transactions();
931 let tx_on_finish = txns.pop().expect("there should be 1 transaction");
932
933 assert_eq!(*tx_to_cmp.hash(), *tx_on_finish.hash());
934
935 rt.graceful_shutdown();
936
937 let data = fs::read(transactions_path).unwrap();
938
939 let txs: Vec<TxBackup> = serde_json::from_slice::<Vec<TxBackup>>(&data).unwrap();
940 assert_eq!(txs.len(), 1);
941
942 temp_dir.close().unwrap();
943 }
944
945 #[tokio::test(flavor = "multi_thread")]
946 async fn deep_commit_refreshes_validator_head() {
947 let provider = MockEthProvider::default().with_genesis_block();
950 let transaction = EthPooledTransaction::try_from_consensus(
951 TransactionBuilder::default()
952 .gas_limit(21_000)
953 .max_fee_per_gas(1_000_000_000)
954 .to(Address::ZERO)
955 .into_eip1559()
956 .try_into_recovered()
957 .unwrap(),
958 )
959 .unwrap();
960 provider.add_account(transaction.sender(), ExtendedAccount::new(0, U256::MAX));
961 let blob_store = InMemoryBlobStore::default();
962 let validator =
963 EthTransactionValidatorBuilder::new(provider.clone(), EthEvmConfig::mainnet())
964 .build(blob_store.clone());
965 let txpool =
966 Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
967
968 let err = txpool
969 .add_transaction(TransactionOrigin::External, transaction.clone())
970 .await
971 .unwrap_err();
972 assert!(matches!(
973 err.kind,
974 PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsGasLimit(
975 21_000, 5_000
976 ))
977 ));
978
979 let config = MaintainPoolConfig::default();
980 let tip = RecoveredBlock::<Block>::new_sealed(
981 SealedBlock::from_sealed_parts(
982 SealedHeader::new(
983 Header {
984 number: config.max_update_depth + 1,
985 gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
986 ..Default::default()
987 },
988 B256::random(),
989 ),
990 Default::default(),
991 ),
992 Default::default(),
993 );
994 let commit: CanonStateNotification = CanonStateNotification::Commit {
995 new: Arc::new(Chain::new(vec![tip], Default::default(), Default::default())),
996 };
997 let maintain = maintain_transaction_pool(
998 provider,
999 txpool.clone(),
1000 futures::stream::iter([commit]),
1001 Runtime::test(),
1002 config,
1003 );
1004 time::timeout(Duration::from_secs(10), maintain).await.unwrap();
1005 assert_eq!(txpool.block_info().last_seen_block_number, config.max_update_depth + 1);
1006
1007 let hash = *transaction.hash();
1008 let added = txpool.add_transaction(TransactionOrigin::External, transaction).await.unwrap();
1009 assert_eq!(added.hash, hash);
1010 }
1011
1012 #[test]
1013 fn test_update_with_higher_finalized_block() {
1014 let mut tracker = FinalizedBlockTracker::new(Some(10));
1015 assert_eq!(tracker.update(Some(15)), Some(15));
1016 assert_eq!(tracker.last_finalized_block, Some(15));
1017 }
1018
1019 #[test]
1020 fn test_update_with_lower_finalized_block() {
1021 let mut tracker = FinalizedBlockTracker::new(Some(20));
1022 assert_eq!(tracker.update(Some(15)), None);
1023 assert_eq!(tracker.last_finalized_block, Some(20));
1025 }
1026
1027 #[test]
1028 fn test_update_with_equal_finalized_block() {
1029 let mut tracker = FinalizedBlockTracker::new(Some(20));
1030 assert_eq!(tracker.update(Some(20)), None);
1031 assert_eq!(tracker.last_finalized_block, Some(20));
1032 }
1033
1034 #[test]
1035 fn test_update_with_no_last_finalized_block() {
1036 let mut tracker = FinalizedBlockTracker::new(None);
1037 assert_eq!(tracker.update(Some(10)), Some(10));
1038 assert_eq!(tracker.last_finalized_block, Some(10));
1039 }
1040
1041 #[test]
1042 fn test_update_with_no_new_finalized_block() {
1043 let mut tracker = FinalizedBlockTracker::new(Some(10));
1044 assert_eq!(tracker.update(None), None);
1045 assert_eq!(tracker.last_finalized_block, Some(10));
1046 }
1047
1048 #[test]
1049 fn test_update_with_no_finalized_blocks() {
1050 let mut tracker = FinalizedBlockTracker::new(None);
1051 assert_eq!(tracker.update(None), None);
1052 assert_eq!(tracker.last_finalized_block, None);
1053 }
1054}