Skip to content

Commit 09b46c9

Browse files
committed
fix: reload queued senders on USDC storage changes
When USDC predeploy storage slots change in a canonical update, queued transactions whose senders have affected balance slots must be re-evaluated. This adds extend_reload_queued_senders to the ChangedAccountsHook trait and implements it in SeismicBalanceHook to detect USDC slot mutations and mark the corresponding queued senders as dirty.
1 parent 088c73e commit 09b46c9

3 files changed

Lines changed: 72 additions & 3 deletions

File tree

crates/seismic/txpool/src/maintain.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
//! Seismic-specific pool maintenance hook that augments native balances with
22
//! USDC predeploy balances.
33
4-
use reth_execution_types::ChangedAccount;
4+
use alloy_primitives::{Address, B256};
5+
use reth_execution_types::{ChangedAccount, ExecutionOutcome};
56
use reth_provider::StateProvider;
67
use reth_transaction_pool::maintain::ChangedAccountsHook;
8+
use std::collections::HashSet;
79
use tracing::debug;
810

911
/// A [`ChangedAccountsHook`] that reads each sender's USDC predeploy balance
@@ -34,4 +36,40 @@ impl ChangedAccountsHook for SeismicBalanceHook {
3436
}
3537
}
3638
}
39+
40+
fn extend_reload_queued_senders<R>(
41+
&self,
42+
queued_senders: &HashSet<Address>,
43+
old: Option<&ExecutionOutcome<R>>,
44+
new: &ExecutionOutcome<R>,
45+
dirty_addresses: &mut HashSet<Address>,
46+
) {
47+
dirty_addresses.extend(queued_senders_with_changed_usdc_slots(queued_senders, old, new));
48+
}
49+
}
50+
51+
fn queued_senders_with_changed_usdc_slots<'a, R>(
52+
queued_senders: &'a HashSet<Address>,
53+
old: Option<&ExecutionOutcome<R>>,
54+
new: &ExecutionOutcome<R>,
55+
) -> impl Iterator<Item = Address> + 'a {
56+
let changed_slots = changed_usdc_storage_slots(new)
57+
.into_iter()
58+
.chain(old.into_iter().flat_map(changed_usdc_storage_slots))
59+
.collect::<HashSet<_>>();
60+
61+
queued_senders.iter().copied().filter(move |address| {
62+
changed_slots.contains(&crate::usdc::usdc_balance_storage_key(address))
63+
})
64+
}
65+
66+
fn changed_usdc_storage_slots<R>(state: &ExecutionOutcome<R>) -> impl Iterator<Item = B256> + '_ {
67+
state
68+
.bundle_accounts_iter()
69+
.filter_map(|(address, account)| (address == crate::usdc::USDC_CONTRACT).then_some(account))
70+
.flat_map(|account| {
71+
account.storage.iter().filter_map(|(slot, value)| {
72+
value.is_changed().then(|| B256::from(slot.to_be_bytes::<32>()))
73+
})
74+
})
3775
}

crates/seismic/txpool/src/usdc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const BALANCES_MAPPING_SLOT: U256 = U256::from_limbs([3, 0, 0, 0]);
2222
/// For a Solidity `mapping(address => uint256)` at slot `s`, the value for key
2323
/// `k` is stored at `keccak256(abi.encode(k, s))` — i.e. `k` left-padded to 32
2424
/// bytes concatenated with `s` as a 32-byte big-endian integer.
25-
fn usdc_balance_storage_key(address: &Address) -> B256 {
25+
pub(crate) fn usdc_balance_storage_key(address: &Address) -> B256 {
2626
let mut buf = [0u8; 64];
2727
// address is 20 bytes, right-aligned in the first 32-byte word
2828
buf[12..32].copy_from_slice(address.as_slice());

crates/transaction-pool/src/maintain.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use futures_util::{
1717
};
1818
use reth_chain_state::CanonStateNotification;
1919
use reth_chainspec::{ChainSpecProvider, EthChainSpec};
20-
use reth_execution_types::ChangedAccount;
20+
use reth_execution_types::{ChangedAccount, ExecutionOutcome};
2121
use reth_fs_util::FsPathError;
2222
use reth_primitives_traits::{
2323
transaction::signed::SignedTransaction, NodePrimitives, SealedHeader,
@@ -106,6 +106,17 @@ pub trait ChangedAccountsHook: Send + Sync + 'static {
106106
/// load native balance/nonce, so implementations always see a consistent
107107
/// view of the chain.
108108
fn transform(&self, state: &dyn StateProvider, accounts: &mut Vec<ChangedAccount>);
109+
110+
/// Extends the dirty set with queued senders whose executability may have
111+
/// changed due to off-account state updates in the canonical update.
112+
fn extend_reload_queued_senders<R>(
113+
&self,
114+
_queued_senders: &HashSet<Address>,
115+
_old: Option<&ExecutionOutcome<R>>,
116+
_new: &ExecutionOutcome<R>,
117+
_dirty_addresses: &mut HashSet<Address>,
118+
) {
119+
}
109120
}
110121

111122
/// No-op implementation for chains that don't need balance augmentation.
@@ -498,6 +509,14 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
498509
metrics.inc_reinserted_transactions(pruned_old_transactions.len());
499510
let _ = pool.add_external_transactions(pruned_old_transactions).await;
500511

512+
let queued_senders = queued_senders(&pool);
513+
hook.extend_reload_queued_senders(
514+
&queued_senders,
515+
Some(old_state),
516+
new_state,
517+
&mut dirty_addresses,
518+
);
519+
501520
// keep track of new mined blob transactions
502521
blob_store_tracker.add_new_chain_blocks(&new_blocks);
503522
}
@@ -575,6 +594,14 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
575594
};
576595
pool.on_canonical_state_change(update);
577596

597+
let queued_senders = queued_senders(&pool);
598+
hook.extend_reload_queued_senders(
599+
&queued_senders,
600+
None,
601+
state,
602+
&mut dirty_addresses,
603+
);
604+
578605
// keep track of mined blob transactions
579606
blob_store_tracker.add_new_chain_blocks(&blocks);
580607
}
@@ -683,6 +710,10 @@ where
683710
Ok(res)
684711
}
685712

713+
fn queued_senders<P: TransactionPool>(pool: &P) -> HashSet<Address> {
714+
pool.queued_transactions().into_iter().map(|tx| tx.sender()).collect()
715+
}
716+
686717
/// Loads transactions from a file, decodes them from the JSON or RLP format, and
687718
/// inserts them into the transaction pool on node boot up.
688719
/// The file is removed after the transactions have been successfully processed.

0 commit comments

Comments
 (0)