Skip to content

Commit ea1e208

Browse files
authored
feat(l1, l2): overwrite txs in mempool if fees are higher (#3238)
**Motivation** Most Ethereum clients let you speed up or overwrite transactions by accepting new transactions with the same nonce but higher fees. This PR adds validations similar to what [Geth does](https://github.com/ethereum/go-ethereum/blob/09289fd154a45420ec916eb842bfb172df7e0d83/core/txpool/legacypool/list.go#L298-L345) but without the `PriceBump` minimum bump percentage **Description** - for eip-1559 check that both `max_fee_per_gas` and `max_priority_fee_per_gas` are greater in the new tx - for legacy tx check that new `gas_price` is greater in the new tx - for eip-4844 txs check that `max_fee_per_gas`, `max_priority_fee_per_gas` and `max_fee_per_blob_gas` are grater in the new tx **How to test** - Send a tx with very low gas price ```shell rex send --gas-price 1 --priority-gas-price 1 --rpc-url http://localhost:1729 0x2B29Bea668B044b2b355C370f85b729bcb43EC40 100000000000000 0x8f87d3aca3eff8132256f69e17df5ba3c605e1b5f4e2071d56f7e6cd66047cc2 ``` - Check tx pool the you should see something like `"maxPriorityFeePerGas":"0x1","maxFeePerGas":"0x1","gasPrice":"0x1"` the tx will probably get stuck ``` curl 'http://localhost:1729' --data '{ "id": 1, "jsonrpc": "2.0", "method": "txpool_content", "params": [] }' -H 'accept: application/json' -H 'Content-Type: application/json' ``` - Send tx with higher gas ```shell rex send --gas-price 100000000 --priority-gas-price 100000000 --rpc-url http://localhost:1729 0x2B29Bea668B044b2b355C370f85b729bcb43EC40 100000000000000 0x8f87d3aca3eff8132256f69e17df5ba3c605e1b5f4e2071d56f7e6cd66047cc2 ``` - Check that the tx pool you should see something like `"maxPriorityFeePerGas":"0x5f5e100","maxFeePerGas":"0x5f5e100","gasPrice":"0x5f5e100"` ```shell curl 'http://localhost:1729' --data '{ "id": 1, "jsonrpc": "2.0", "method": "txpool_content", "params": [] }' -H 'accept: application/json' -H 'Content-Type: application/json' ```
1 parent fd61888 commit ea1e208

File tree

2 files changed

+69
-18
lines changed

2 files changed

+69
-18
lines changed

crates/blockchain/blockchain.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,9 @@ impl Blockchain {
569569
let sender = transaction.sender()?;
570570

571571
// Validate transaction
572-
self.validate_transaction(&transaction, sender).await?;
572+
if let Some(tx_to_replace) = self.validate_transaction(&transaction, sender).await? {
573+
self.remove_transaction_from_pool(&tx_to_replace)?;
574+
}
573575

574576
// Add transaction and blobs bundle to storage
575577
let hash = transaction.compute_hash();
@@ -590,7 +592,9 @@ impl Blockchain {
590592
}
591593
let sender = transaction.sender()?;
592594
// Validate transaction
593-
self.validate_transaction(&transaction, sender).await?;
595+
if let Some(tx_to_replace) = self.validate_transaction(&transaction, sender).await? {
596+
self.remove_transaction_from_pool(&tx_to_replace)?;
597+
}
594598

595599
let hash = transaction.compute_hash();
596600

@@ -637,16 +641,16 @@ impl Blockchain {
637641
5. Ensure the transactor is able to add a new transaction. The number of transactions sent by an account may be limited by a certain configured value
638642
639643
*/
640-
644+
/// Returns the hash of the transaction to replace in case the nonce already exists
641645
pub async fn validate_transaction(
642646
&self,
643647
tx: &Transaction,
644648
sender: Address,
645-
) -> Result<(), MempoolError> {
649+
) -> Result<Option<H256>, MempoolError> {
646650
let nonce = tx.nonce();
647651

648652
if matches!(tx, &Transaction::PrivilegedL2Transaction(_)) {
649-
return Ok(());
653+
return Ok(None);
650654
}
651655

652656
let header_no = self.storage.get_latest_block_number().await?;
@@ -713,20 +717,16 @@ impl Blockchain {
713717
}
714718

715719
// Check the nonce of pendings TXs in the mempool from the same sender
716-
if self
717-
.mempool
718-
.contains_sender_nonce(sender, nonce, tx.compute_hash())?
719-
{
720-
return Err(MempoolError::InvalidNonce);
721-
}
720+
// If it exists check if the new tx has higher fees
721+
let tx_to_replace_hash = self.mempool.find_tx_to_replace(sender, nonce, tx)?;
722722

723723
if let Some(chain_id) = tx.chain_id() {
724724
if chain_id != config.chain_id {
725725
return Err(MempoolError::InvalidChainId(config.chain_id));
726726
}
727727
}
728728

729-
Ok(())
729+
Ok(tx_to_replace_hash)
730730
}
731731

732732
/// Marks the node's chain as up to date with the current chain

crates/blockchain/mempool.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,20 +248,71 @@ impl Mempool {
248248
sender: Address,
249249
nonce: u64,
250250
received_hash: H256,
251-
) -> Result<bool, MempoolError> {
251+
) -> Result<Option<MempoolTransaction>, MempoolError> {
252252
let pooled_transactions = self
253253
.transaction_pool
254254
.read()
255255
.map_err(|error| StoreError::MempoolReadLock(error.to_string()))?;
256256

257-
let count = pooled_transactions
257+
let mut txs = pooled_transactions
258258
.iter()
259-
.filter(|(hash, tx)| {
260-
tx.sender() == sender && tx.nonce() == nonce && *hash != &received_hash
259+
.filter_map(|(hash, tx)| {
260+
if tx.sender() == sender && tx.nonce() == nonce && *hash != received_hash {
261+
Some(tx.clone())
262+
} else {
263+
None
264+
}
261265
})
262-
.count();
266+
.collect::<Vec<_>>();
267+
268+
Ok(txs.pop())
269+
}
270+
271+
pub fn find_tx_to_replace(
272+
&self,
273+
sender: Address,
274+
nonce: u64,
275+
tx: &Transaction,
276+
) -> Result<Option<H256>, MempoolError> {
277+
let Some(tx_in_pool) = self.contains_sender_nonce(sender, nonce, tx.compute_hash())? else {
278+
return Ok(None);
279+
};
280+
281+
let is_a_replacement_tx = {
282+
// EIP-1559 values
283+
let old_tx_max_fee_per_gas = tx_in_pool.max_fee_per_gas().unwrap_or_default();
284+
let old_tx_max_priority_fee_per_gas = tx_in_pool.max_priority_fee().unwrap_or_default();
285+
let new_tx_max_fee_per_gas = tx.max_fee_per_gas().unwrap_or_default();
286+
let new_tx_max_priority_fee_per_gas = tx.max_priority_fee().unwrap_or_default();
287+
288+
// Legacy tx values
289+
let old_tx_gas_price = tx_in_pool.gas_price();
290+
let new_tx_gas_price = tx.gas_price();
291+
292+
// EIP-4844 values
293+
let old_tx_max_fee_per_blob = tx_in_pool.max_fee_per_blob_gas();
294+
let new_tx_max_fee_per_blob = tx.max_fee_per_blob_gas();
295+
296+
let eip4844_higher_fees = if let (Some(old_blob_fee), Some(new_blob_fee)) =
297+
(old_tx_max_fee_per_blob, new_tx_max_fee_per_blob)
298+
{
299+
new_blob_fee > old_blob_fee
300+
} else {
301+
true // We are marking it as always true if the tx is not eip-4844
302+
};
303+
304+
let eip1559_higher_fees = new_tx_max_fee_per_gas > old_tx_max_fee_per_gas
305+
&& new_tx_max_priority_fee_per_gas > old_tx_max_priority_fee_per_gas;
306+
let legacy_higher_fees = new_tx_gas_price > old_tx_gas_price;
307+
308+
eip4844_higher_fees && (eip1559_higher_fees || legacy_higher_fees)
309+
};
310+
311+
if !is_a_replacement_tx {
312+
return Err(MempoolError::NonceTooLow);
313+
}
263314

264-
Ok(count > 0)
315+
Ok(Some(tx_in_pool.compute_hash()))
265316
}
266317
}
267318

0 commit comments

Comments
 (0)