Skip to content

Commit 4af1931

Browse files
committed
txpool reload queued senders on usdc storage changes
1 parent 1a3012d commit 4af1931

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::StateProviderFactory;
67
use reth_transaction_pool::maintain::ChangedAccountsHook;
8+
use std::collections::HashSet;
79
use tracing::{debug, warn};
810

911
/// A [`ChangedAccountsHook`] that reads each sender's USDC predeploy balance
@@ -50,4 +52,122 @@ where
5052
}
5153
}
5254
}
55+
56+
fn extend_reload_queued_senders<R>(
57+
&self,
58+
queued_senders: &HashSet<Address>,
59+
old: Option<&ExecutionOutcome<R>>,
60+
new: &ExecutionOutcome<R>,
61+
dirty_addresses: &mut HashSet<Address>,
62+
) {
63+
dirty_addresses.extend(queued_senders_with_changed_usdc_slots(queued_senders, old, new));
64+
}
65+
}
66+
67+
fn queued_senders_with_changed_usdc_slots<'a, R>(
68+
queued_senders: &'a HashSet<Address>,
69+
old: Option<&ExecutionOutcome<R>>,
70+
new: &ExecutionOutcome<R>,
71+
) -> impl Iterator<Item = Address> + 'a {
72+
let changed_slots = changed_usdc_storage_slots(new)
73+
.into_iter()
74+
.chain(old.into_iter().flat_map(changed_usdc_storage_slots))
75+
.collect::<HashSet<_>>();
76+
77+
queued_senders.iter().copied().filter(move |address| {
78+
changed_slots.contains(&crate::usdc::usdc_balance_storage_key(address))
79+
})
80+
}
81+
82+
fn changed_usdc_storage_slots<R>(state: &ExecutionOutcome<R>) -> impl Iterator<Item = B256> + '_ {
83+
state
84+
.bundle_accounts_iter()
85+
.filter_map(|(address, account)| (address == crate::usdc::USDC_CONTRACT).then_some(account))
86+
.flat_map(|account| {
87+
account.storage.iter().filter_map(|(slot, value)| {
88+
value.is_changed().then(|| B256::from(slot.to_be_bytes::<32>()))
89+
})
90+
})
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::*;
96+
use alloy_primitives::{map::HashMap, FlaggedStorage, U256};
97+
use reth_execution_types::{BundleStateInit, RevertsInit};
98+
99+
#[test]
100+
fn reloads_only_queued_senders_with_changed_usdc_slots() {
101+
let affected = alloy_primitives::address!("000000000000000000000000000000000000000a");
102+
let unaffected = alloy_primitives::address!("000000000000000000000000000000000000000b");
103+
let queued_senders = HashSet::from([affected, unaffected]);
104+
let state = ExecutionOutcome::<()>::new_init(
105+
{
106+
let mut init = BundleStateInit::default();
107+
init.insert(
108+
crate::usdc::USDC_CONTRACT,
109+
(
110+
None,
111+
None,
112+
HashMap::from_iter([(
113+
crate::usdc::usdc_balance_storage_key(&affected),
114+
(FlaggedStorage::ZERO, FlaggedStorage::from(U256::from(1))),
115+
)]),
116+
),
117+
);
118+
init
119+
},
120+
RevertsInit::default(),
121+
[],
122+
vec![],
123+
0,
124+
vec![],
125+
);
126+
127+
let dirty = queued_senders_with_changed_usdc_slots(&queued_senders, None, &state)
128+
.collect::<HashSet<_>>();
129+
130+
assert_eq!(dirty, HashSet::from([affected]));
131+
}
132+
133+
#[test]
134+
fn includes_changed_slots_from_old_and_new_state() {
135+
let queued_sender = alloy_primitives::address!("000000000000000000000000000000000000000a");
136+
let queued_senders = HashSet::from([queued_sender]);
137+
let old = ExecutionOutcome::<()>::new_init(
138+
{
139+
let mut init = BundleStateInit::default();
140+
init.insert(
141+
crate::usdc::USDC_CONTRACT,
142+
(
143+
None,
144+
None,
145+
HashMap::from_iter([(
146+
crate::usdc::usdc_balance_storage_key(&queued_sender),
147+
(FlaggedStorage::ZERO, FlaggedStorage::from(U256::from(1))),
148+
)]),
149+
),
150+
);
151+
init
152+
},
153+
RevertsInit::default(),
154+
[],
155+
vec![],
156+
0,
157+
vec![],
158+
);
159+
let new = ExecutionOutcome::<()>::new_init(
160+
BundleStateInit::default(),
161+
RevertsInit::default(),
162+
[],
163+
vec![],
164+
0,
165+
vec![],
166+
);
167+
168+
let dirty = queued_senders_with_changed_usdc_slots(&queued_senders, Some(&old), &new)
169+
.collect::<HashSet<_>>();
170+
171+
assert_eq!(dirty, HashSet::from([queued_sender]));
172+
}
53173
}

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,
@@ -100,6 +100,17 @@ pub trait ChangedAccountsHook: Send + Sync + 'static {
100100
/// additional state (e.g. ERC-20 storage) and adjust the `balance` field of
101101
/// each [`ChangedAccount`].
102102
fn transform(&self, accounts: &mut Vec<ChangedAccount>);
103+
104+
/// Extends the dirty set with queued senders whose executability may have
105+
/// changed due to off-account state updates in the canonical update.
106+
fn extend_reload_queued_senders<R>(
107+
&self,
108+
_queued_senders: &HashSet<Address>,
109+
_old: Option<&ExecutionOutcome<R>>,
110+
_new: &ExecutionOutcome<R>,
111+
_dirty_addresses: &mut HashSet<Address>,
112+
) {
113+
}
103114
}
104115

105116
/// No-op implementation for chains that don't need balance augmentation.
@@ -383,8 +394,8 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
383394
let old_first = old_blocks.first();
384395

385396
// check if the reorg is not canonical with the pool's block
386-
if !(old_first.parent_hash() == pool_info.last_seen_block_hash ||
387-
new_first.parent_hash() == pool_info.last_seen_block_hash)
397+
if !(old_first.parent_hash() == pool_info.last_seen_block_hash
398+
|| new_first.parent_hash() == pool_info.last_seen_block_hash)
388399
{
389400
// the new block points to a higher block than the oldest block in the old chain
390401
maintained_state = MaintainedPoolState::Drifted;
@@ -436,7 +447,6 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
436447
// we can use extend here because they are unique
437448
changed_accounts.extend(new_changed_accounts.into_iter().map(|entry| entry.0));
438449
hook.transform(&mut changed_accounts);
439-
440450
// all transactions mined in the new chain
441451
let new_mined_transactions: HashSet<_> = new_blocks.transaction_hashes().collect();
442452

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

501+
let queued_senders = queued_senders(&pool);
502+
hook.extend_reload_queued_senders(
503+
&queued_senders,
504+
Some(old_state),
505+
new_state,
506+
&mut dirty_addresses,
507+
);
508+
491509
// keep track of new mined blob transactions
492510
blob_store_tracker.add_new_chain_blocks(&new_blocks);
493511
}
@@ -531,7 +549,7 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
531549
// keep track of mined blob transactions
532550
blob_store_tracker.add_new_chain_blocks(&blocks);
533551

534-
continue
552+
continue;
535553
}
536554

537555
let mut changed_accounts = Vec::with_capacity(state.state().len());
@@ -541,7 +559,6 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
541559
changed_accounts.push(acc);
542560
}
543561
hook.transform(&mut changed_accounts);
544-
545562
let mined_transactions = blocks.transaction_hashes().collect();
546563

547564
// check if the range of the commit is canonical with the pool's block
@@ -563,6 +580,14 @@ pub async fn maintain_transaction_pool_with_hook<N, Client, P, St, Tasks, H>(
563580
};
564581
pool.on_canonical_state_change(update);
565582

583+
let queued_senders = queued_senders(&pool);
584+
hook.extend_reload_queued_senders(
585+
&queued_senders,
586+
None,
587+
state,
588+
&mut dirty_addresses,
589+
);
590+
566591
// keep track of mined blob transactions
567592
blob_store_tracker.add_new_chain_blocks(&blocks);
568593
}
@@ -671,6 +696,10 @@ where
671696
Ok(res)
672697
}
673698

699+
fn queued_senders<P: TransactionPool>(pool: &P) -> HashSet<Address> {
700+
pool.queued_transactions().into_iter().map(|tx| tx.sender()).collect()
701+
}
702+
674703
/// Loads transactions from a file, decodes them from the JSON or RLP format, and
675704
/// inserts them into the transaction pool on node boot up.
676705
/// The file is removed after the transactions have been successfully processed.
@@ -682,14 +711,14 @@ where
682711
P: TransactionPool<Transaction: PoolTransaction<Consensus: SignedTransaction>>,
683712
{
684713
if !file_path.exists() {
685-
return Ok(())
714+
return Ok(());
686715
}
687716

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

691720
if data.is_empty() {
692-
return Ok(())
721+
return Ok(());
693722
}
694723

695724
let pool_transactions: Vec<(TransactionOrigin, <P as TransactionPool>::Transaction)> =
@@ -740,7 +769,7 @@ where
740769
let local_transactions = pool.get_local_transactions();
741770
if local_transactions.is_empty() {
742771
trace!(target: "txpool", "no local transactions to save");
743-
return
772+
return;
744773
}
745774

746775
let local_transactions = local_transactions
@@ -757,7 +786,7 @@ where
757786
Ok(data) => data,
758787
Err(err) => {
759788
warn!(target: "txpool", %err, txs_file=?file_path, "failed to serialize local transactions to json");
760-
return
789+
return;
761790
}
762791
};
763792

@@ -812,7 +841,7 @@ pub async fn backup_local_transactions_task<P>(
812841
{
813842
let Some(transactions_path) = config.transactions_path else {
814843
// nothing to do
815-
return
844+
return;
816845
};
817846

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

0 commit comments

Comments
 (0)