Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ reth-db-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-engine-local = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-engine-tree = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.10.0" }
Expand Down
3 changes: 3 additions & 0 deletions crates/block/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ alloy-evm = { workspace = true }
alloy-hardforks = { workspace = true }
alloy-primitives = { workspace = true }
alloy-rpc-types-eth = { workspace = true }
op-alloy-flz = { workspace = true }
reth-chainspec = { workspace = true }
reth-ethereum = { workspace = true }
reth-ethereum-forks = { workspace = true }
Expand All @@ -41,4 +42,6 @@ reth-primitives-traits = { workspace = true }
reth-revm = { workspace = true }
reth-rpc-eth-api = { workspace = true, optional = true }
reth-storage-errors = { workspace = true }
reth-transaction-pool = { workspace = true }
revm-database-interface = { workspace = true }
tracing = { workspace = true }
1 change: 1 addition & 0 deletions crates/block/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod assembler;
pub mod config;
pub mod executor;
pub mod factory;
pub mod tx_selection;
197 changes: 197 additions & 0 deletions crates/block/src/tx_selection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! Transaction selection utilities for building blocks from the transaction pool.
//!
//! This module provides a unified interface for selecting and executing transactions
//! from the mempool, used by both the payload builder and the RPC pre-building endpoint.

use alloy_eips::Encodable2718;
use alloy_primitives::Address;
use op_alloy_flz::tx_estimated_size_fjord_bytes;
use reth_ethereum::{EthPrimitives, TransactionSigned};
use reth_evm::{
block::{BlockExecutionError, BlockValidationError, InternalBlockExecutionError},
execute::BlockBuilder,
};
use reth_primitives::Recovered;
use reth_primitives_traits::transaction::error::InvalidTransactionError;
use reth_transaction_pool::{
BestTransactionsAttributes, PoolTransaction, TransactionPool,
error::InvalidPoolTransactionError,
};
use tracing::trace;

/// Configuration for transaction selection.
#[derive(Debug, Clone)]
pub struct TxSelectionConfig {
/// Base fee per gas for tip calculation.
pub base_fee: u64,
/// Maximum gas allowed per list.
pub gas_limit_per_list: u64,
/// Maximum DA bytes allowed per list (compressed size).
pub max_da_bytes_per_list: u64,
/// Maximum number of transaction lists to produce.
pub max_lists: usize,
/// Minimum tip required for a transaction to be included.
pub min_tip: u64,
/// Local accounts to prioritize.
/// If non-empty, only transactions from these accounts are included.
pub locals: Vec<Address>,
}

/// A successfully executed transaction with metadata.
#[derive(Debug, Clone)]
pub struct ExecutedTx {
/// The executed transaction.
pub tx: Recovered<TransactionSigned>,
/// Gas used by the transaction.
pub gas_used: u64,
/// Estimated DA size (compressed).
pub da_size: u64,
}

/// A list of executed transactions with cumulative statistics.
#[derive(Debug, Clone, Default)]
pub struct ExecutedTxList {
/// The executed transactions in this list.
pub transactions: Vec<ExecutedTx>,
/// Total gas used by all transactions in this list.
pub total_gas_used: u64,
/// Total DA bytes used by all transactions in this list.
pub total_da_bytes: u64,
}

/// Outcome of the transaction selection process.
#[derive(Debug)]
pub enum SelectionOutcome {
/// Selection was cancelled before completion.
Cancelled,
/// Selection completed successfully with the produced lists.
Completed(Vec<ExecutedTxList>),
}

/// Creates an internal error for when the transaction list invariant is violated.
fn lists_empty_error() -> BlockExecutionError {
BlockExecutionError::Internal(InternalBlockExecutionError::msg(
"tx selection invariant violated: lists is empty",
))
}

/// Selects and executes transactions from the pool.
///
/// This function iterates through the best transactions in the pool, applying
/// the configured filters and limits, and executes them against the provided
/// block builder.
///
/// # Arguments
///
/// * `builder` - The block builder to execute transactions against.
/// * `pool` - The transaction pool to select from.
/// * `config` - Configuration for transaction selection.
/// * `is_cancelled` - A function that returns true if selection should be cancelled.
///
/// # Returns
///
/// * `Ok(SelectionOutcome::Cancelled)` - If cancelled during selection.
/// * `Ok(SelectionOutcome::Completed(lists))` - If selection completed successfully.
/// * `Err(err)` - If a fatal execution error occurred.
pub fn select_and_execute_pool_transactions<B, Pool>(
builder: &mut B,
pool: &Pool,
config: &TxSelectionConfig,
is_cancelled: impl Fn() -> bool,
) -> Result<SelectionOutcome, BlockExecutionError>
where
B: BlockBuilder<Primitives = EthPrimitives>,
Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
{
let mut best_txs = pool
.best_transactions_with_attributes(BestTransactionsAttributes::new(config.base_fee, None));

let mut lists = vec![ExecutedTxList::default()];

while let Some(pool_tx) = best_txs.next() {
// 1. Check cancellation
if is_cancelled() {
return Ok(SelectionOutcome::Cancelled);
}

// 2. Filter by locals (if configured)
if !config.locals.is_empty() && !config.locals.contains(&pool_tx.sender()) {
// Mark as underpriced to skip this transaction and its dependents
best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Underpriced);
continue;
}

// 3. Filter by min_tip
let tip = pool_tx.effective_tip_per_gas(config.base_fee);
if tip.is_none_or(|t| t < config.min_tip as u128) {
trace!(target: "tx_selection", ?pool_tx, "skipping transaction with insufficient tip");
best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Underpriced);
continue;
}

// 4. Calculate DA size upfront (needed for limit checks)
let tx = pool_tx.to_consensus();
let da_size = tx_estimated_size_fjord_bytes(&tx.encoded_2718());

// 5. Check if transaction fits in current list; if not, try starting a new one
Comment thread
MatusKysel marked this conversation as resolved.
Outdated
let current = lists.last().ok_or_else(lists_empty_error)?;
let exceeds_gas = current.total_gas_used + pool_tx.gas_limit() > config.gas_limit_per_list;
let exceeds_da = current.total_da_bytes + da_size > config.max_da_bytes_per_list;

if exceeds_gas || exceeds_da {
if lists.len() >= config.max_lists {
// Can't fit in any list
if exceeds_gas {
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::ExceedsGasLimit(
pool_tx.gas_limit(),
config.gas_limit_per_list,
),
);
} else {
best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Underpriced);
}
continue;
}
// Start a new list
lists.push(ExecutedTxList::default());
}

// 6. Execute transaction
let gas_used = match builder.execute_transaction(tx.clone()) {
Ok(gas_used) => gas_used,
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
error, ..
})) => {
if error.is_nonce_too_low() {
// Nonce too low - just skip, don't mark invalid
// (could be a race condition, transaction might be valid later)
trace!(target: "tx_selection", %error, ?tx, "skipping nonce too low transaction");
} else {
// Other validation error - mark invalid to skip dependents
trace!(target: "tx_selection", %error, ?tx, "skipping invalid transaction and its descendants");
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::Consensus(
InvalidTransactionError::TxTypeNotSupported,
),
);
}
continue;
}
// Fatal error - stop selection
Err(err) => return Err(err),
};

// 7. Record successful transaction
let current = lists.last_mut().ok_or_else(lists_empty_error)?;
current.total_gas_used += gas_used;
current.total_da_bytes += da_size;
current.transactions.push(ExecutedTx { tx, gas_used, da_size });

trace!(target: "tx_selection", gas_used, da_size, "included transaction from pool");
}

Ok(SelectionOutcome::Completed(lists))
}
1 change: 1 addition & 0 deletions crates/payload/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ reth = { workspace = true }
reth-basic-payload-builder = { workspace = true }
reth-chainspec = { workspace = true }
reth-engine-local = { workspace = true }
reth-errors = { workspace = true }
reth-ethereum = { workspace = true }
reth-ethereum-engine-primitives = { workspace = true }
reth-evm = { workspace = true }
Expand Down
Loading