Skip to content

Commit a248354

Browse files
committed
txpool reload queued senders on usdc storage changes
1 parent 088c73e commit a248354

3 files changed

Lines changed: 162 additions & 13 deletions

File tree

crates/seismic/txpool/src/maintain.rs

Lines changed: 121 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,122 @@ 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+
})
75+
}
76+
77+
#[cfg(test)]
78+
mod tests {
79+
use super::*;
80+
use alloy_primitives::{map::HashMap, FlaggedStorage, U256};
81+
use reth_execution_types::{BundleStateInit, RevertsInit};
82+
83+
#[test]
84+
fn reloads_only_queued_senders_with_changed_usdc_slots() {
85+
let affected = alloy_primitives::address!("000000000000000000000000000000000000000a");
86+
let unaffected = alloy_primitives::address!("000000000000000000000000000000000000000b");
87+
let queued_senders = HashSet::from([affected, unaffected]);
88+
let state = ExecutionOutcome::<()>::new_init(
89+
{
90+
let mut init = BundleStateInit::default();
91+
init.insert(
92+
crate::usdc::USDC_CONTRACT,
93+
(
94+
None,
95+
None,
96+
HashMap::from_iter([(
97+
crate::usdc::usdc_balance_storage_key(&affected),
98+
(FlaggedStorage::ZERO, FlaggedStorage::from(U256::from(1))),
99+
)]),
100+
),
101+
);
102+
init
103+
},
104+
RevertsInit::default(),
105+
[],
106+
vec![],
107+
0,
108+
vec![],
109+
);
110+
111+
let dirty = queued_senders_with_changed_usdc_slots(&queued_senders, None, &state)
112+
.collect::<HashSet<_>>();
113+
114+
assert_eq!(dirty, HashSet::from([affected]));
115+
}
116+
117+
#[test]
118+
fn includes_changed_slots_from_old_and_new_state() {
119+
let queued_sender = alloy_primitives::address!("000000000000000000000000000000000000000a");
120+
let queued_senders = HashSet::from([queued_sender]);
121+
let old = ExecutionOutcome::<()>::new_init(
122+
{
123+
let mut init = BundleStateInit::default();
124+
init.insert(
125+
crate::usdc::USDC_CONTRACT,
126+
(
127+
None,
128+
None,
129+
HashMap::from_iter([(
130+
crate::usdc::usdc_balance_storage_key(&queued_sender),
131+
(FlaggedStorage::ZERO, FlaggedStorage::from(U256::from(1))),
132+
)]),
133+
),
134+
);
135+
init
136+
},
137+
RevertsInit::default(),
138+
[],
139+
vec![],
140+
0,
141+
vec![],
142+
);
143+
let new = ExecutionOutcome::<()>::new_init(
144+
BundleStateInit::default(),
145+
RevertsInit::default(),
146+
[],
147+
vec![],
148+
0,
149+
vec![],
150+
);
151+
152+
let dirty = queued_senders_with_changed_usdc_slots(&queued_senders, Some(&old), &new)
153+
.collect::<HashSet<_>>();
154+
155+
assert_eq!(dirty, HashSet::from([queued_sender]));
156+
}
37157
}

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: 40 additions & 11 deletions
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.
@@ -391,8 +402,8 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
391402
let old_first = old_blocks.first();
392403

393404
// check if the reorg is not canonical with the pool's block
394-
if !(old_first.parent_hash() == pool_info.last_seen_block_hash ||
395-
new_first.parent_hash() == pool_info.last_seen_block_hash)
405+
if !(old_first.parent_hash() == pool_info.last_seen_block_hash
406+
|| new_first.parent_hash() == pool_info.last_seen_block_hash)
396407
{
397408
// the new block points to a higher block than the oldest block in the old chain
398409
maintained_state = MaintainedPoolState::Drifted;
@@ -446,7 +457,6 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
446457
if let Ok(state) = client.history_by_block_hash(new_tip.hash()) {
447458
hook.transform(&*state, &mut changed_accounts);
448459
}
449-
450460
// all transactions mined in the new chain
451461
let new_mined_transactions: HashSet<_> = new_blocks.transaction_hashes().collect();
452462

@@ -498,6 +508,14 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
498508
metrics.inc_reinserted_transactions(pruned_old_transactions.len());
499509
let _ = pool.add_external_transactions(pruned_old_transactions).await;
500510

511+
let queued_senders = queued_senders(&pool);
512+
hook.extend_reload_queued_senders(
513+
&queued_senders,
514+
Some(old_state),
515+
new_state,
516+
&mut dirty_addresses,
517+
);
518+
501519
// keep track of new mined blob transactions
502520
blob_store_tracker.add_new_chain_blocks(&new_blocks);
503521
}
@@ -541,7 +559,7 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
541559
// keep track of mined blob transactions
542560
blob_store_tracker.add_new_chain_blocks(&blocks);
543561

544-
continue
562+
continue;
545563
}
546564

547565
let mut changed_accounts = Vec::with_capacity(state.state().len());
@@ -553,7 +571,6 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
553571
if let Ok(tip_state) = client.history_by_block_hash(tip.hash()) {
554572
hook.transform(&*tip_state, &mut changed_accounts);
555573
}
556-
557574
let mined_transactions = blocks.transaction_hashes().collect();
558575

559576
// check if the range of the commit is canonical with the pool's block
@@ -575,6 +592,14 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
575592
};
576593
pool.on_canonical_state_change(update);
577594

595+
let queued_senders = queued_senders(&pool);
596+
hook.extend_reload_queued_senders(
597+
&queued_senders,
598+
None,
599+
state,
600+
&mut dirty_addresses,
601+
);
602+
578603
// keep track of mined blob transactions
579604
blob_store_tracker.add_new_chain_blocks(&blocks);
580605
}
@@ -683,6 +708,10 @@ where
683708
Ok(res)
684709
}
685710

711+
fn queued_senders<P: TransactionPool>(pool: &P) -> HashSet<Address> {
712+
pool.queued_transactions().into_iter().map(|tx| tx.sender()).collect()
713+
}
714+
686715
/// Loads transactions from a file, decodes them from the JSON or RLP format, and
687716
/// inserts them into the transaction pool on node boot up.
688717
/// The file is removed after the transactions have been successfully processed.
@@ -694,14 +723,14 @@ where
694723
P: TransactionPool<Transaction: PoolTransaction<Consensus: SignedTransaction>>,
695724
{
696725
if !file_path.exists() {
697-
return Ok(())
726+
return Ok(());
698727
}
699728

700729
debug!(target: "txpool", txs_file =?file_path, "Check local persistent storage for saved transactions");
701730
let data = reth_fs_util::read(file_path)?;
702731

703732
if data.is_empty() {
704-
return Ok(())
733+
return Ok(());
705734
}
706735

707736
let pool_transactions: Vec<(TransactionOrigin, <P as TransactionPool>::Transaction)> =
@@ -752,7 +781,7 @@ where
752781
let local_transactions = pool.get_local_transactions();
753782
if local_transactions.is_empty() {
754783
trace!(target: "txpool", "no local transactions to save");
755-
return
784+
return;
756785
}
757786

758787
let local_transactions = local_transactions
@@ -769,7 +798,7 @@ where
769798
Ok(data) => data,
770799
Err(err) => {
771800
warn!(target: "txpool", %err, txs_file=?file_path, "failed to serialize local transactions to json");
772-
return
801+
return;
773802
}
774803
};
775804

@@ -824,7 +853,7 @@ pub async fn backup_local_transactions_task<P>(
824853
{
825854
let Some(transactions_path) = config.transactions_path else {
826855
// nothing to do
827-
return
856+
return;
828857
};
829858

830859
if let Err(err) = load_and_reinsert_transactions(pool.clone(), &transactions_path).await {

0 commit comments

Comments
 (0)