Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TransactionPool and TransactionValidator #59

Open
frisitano opened this issue Dec 3, 2024 · 0 comments
Open

Implement TransactionPool and TransactionValidator #59

frisitano opened this issue Dec 3, 2024 · 0 comments
Milestone

Comments

@frisitano
Copy link
Collaborator

frisitano commented Dec 3, 2024

Overview

We must implement both a TransactionPool and TransactionValidator. The transaction pool is responsible for gossiping transactions and preparing them for the PayloadBuilder as defined in #58. The TransactionValidator is responsible for validating transactions that it receives to ensure they are suitable candidates to be included in a new payload.

TransactionPool

  • The transaction pool should only allow and gossip transactions that are supported by scroll - see here. We should NOT gossip L1MessageTx as these should be provided via the engine API PayloadBuilderAttributes as defined in Engine API Types #57.

Optimism Example:

/// Type alias for default optimism transaction pool
pub type OpTransactionPool<Client, S> = Pool<
TransactionValidationTaskExecutor<OpTransactionValidator<Client, EthPooledTransaction>>,
CoinbaseTipOrdering<EthPooledTransaction>,
S,
>;

TransactionValidator

  • Should ensure that the transaction is of a supported type.
  • Should ensure that the transaction satisfies native ethereum transaction requirements.
  • Should ensure that the EOA has sufficient funds to pay for the transactions including the L1 cost.
  • Should perform some checks around transactions that are prohibitively expensive to prove.

Optimism Example:

/// Validator for Optimism transactions.
#[derive(Debug, Clone)]
pub struct OpTransactionValidator<Client, Tx> {
/// The type that performs the actual validation.
inner: EthTransactionValidator<Client, Tx>,
/// Additional block info required for validation.
block_info: Arc<OpL1BlockInfo>,
/// If true, ensure that the transaction's sender has enough balance to cover the L1 gas fee
/// derived from the tracked L1 block info that is extracted from the first transaction in the
/// L2 block.
require_l1_data_gas_fee: bool,
}
impl<Client, Tx> OpTransactionValidator<Client, Tx> {
/// Returns the configured chain spec
pub fn chain_spec(&self) -> Arc<ChainSpec> {
self.inner.chain_spec()
}
/// Returns the configured client
pub fn client(&self) -> &Client {
self.inner.client()
}
/// Returns the current block timestamp.
fn block_timestamp(&self) -> u64 {
self.block_info.timestamp.load(Ordering::Relaxed)
}
/// Whether to ensure that the transaction's sender has enough balance to also cover the L1 gas
/// fee.
pub fn require_l1_data_gas_fee(self, require_l1_data_gas_fee: bool) -> Self {
Self { require_l1_data_gas_fee, ..self }
}
/// Returns whether this validator also requires the transaction's sender to have enough balance
/// to cover the L1 gas fee.
pub const fn requires_l1_data_gas_fee(&self) -> bool {
self.require_l1_data_gas_fee
}
}
impl<Client, Tx> OpTransactionValidator<Client, Tx>
where
Client: StateProviderFactory + BlockReaderIdExt<Block = reth_primitives::Block>,
Tx: EthPoolTransaction,
{
/// Create a new [`OpTransactionValidator`].
pub fn new(inner: EthTransactionValidator<Client, Tx>) -> Self {
let this = Self::with_block_info(inner, OpL1BlockInfo::default());
if let Ok(Some(block)) =
this.inner.client().block_by_number_or_tag(alloy_eips::BlockNumberOrTag::Latest)
{
// genesis block has no txs, so we can't extract L1 info, we set the block info to empty
// so that we will accept txs into the pool before the first block
if block.number == 0 {
this.block_info.timestamp.store(block.timestamp, Ordering::Relaxed);
} else {
this.update_l1_block_info(&block);
}
}
this
}
/// Create a new [`OpTransactionValidator`] with the given [`OpL1BlockInfo`].
pub fn with_block_info(
inner: EthTransactionValidator<Client, Tx>,
block_info: OpL1BlockInfo,
) -> Self {
Self { inner, block_info: Arc::new(block_info), require_l1_data_gas_fee: true }
}
/// Update the L1 block info.
fn update_l1_block_info(&self, block: &Block) {
self.block_info.timestamp.store(block.timestamp, Ordering::Relaxed);
if let Ok(cost_addition) = reth_optimism_evm::extract_l1_info(&block.body) {
*self.block_info.l1_block_info.write() = cost_addition;
}
}
/// Validates a single transaction.
///
/// See also [`TransactionValidator::validate_transaction`]
///
/// This behaves the same as [`EthTransactionValidator::validate_one`], but in addition, ensures
/// that the account has enough balance to cover the L1 gas cost.
pub fn validate_one(
&self,
origin: TransactionOrigin,
transaction: Tx,
) -> TransactionValidationOutcome<Tx> {
if transaction.is_eip4844() {
return TransactionValidationOutcome::Invalid(
transaction,
InvalidTransactionError::TxTypeNotSupported.into(),
)
}
let outcome = self.inner.validate_one(origin, transaction);
if !self.requires_l1_data_gas_fee() {
// no need to check L1 gas fee
return outcome
}
// ensure that the account has enough balance to cover the L1 gas cost
if let TransactionValidationOutcome::Valid {
balance,
state_nonce,
transaction: valid_tx,
propagate,
} = outcome
{
let l1_block_info = self.block_info.l1_block_info.read().clone();
let mut encoded = Vec::with_capacity(valid_tx.transaction().encoded_length());
let tx: TransactionSigned = valid_tx.transaction().clone().into_consensus().into();
tx.encode_2718(&mut encoded);
let cost_addition = match l1_block_info.l1_tx_data_fee(
&self.chain_spec(),
self.block_timestamp(),
&encoded,
false,
) {
Ok(cost) => cost,
Err(err) => {
return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err))
}
};
let cost = valid_tx.transaction().cost().saturating_add(cost_addition);
// Checks for max cost
if cost > balance {
return TransactionValidationOutcome::Invalid(
valid_tx.into_transaction(),
InvalidTransactionError::InsufficientFunds(
GotExpected { got: balance, expected: cost }.into(),
)
.into(),
)
}
return TransactionValidationOutcome::Valid {
balance,
state_nonce,
transaction: valid_tx,
propagate,
}
}
outcome
}
/// Validates all given transactions.
///
/// Returns all outcomes for the given transactions in the same order.
///
/// See also [`Self::validate_one`]
pub fn validate_all(
&self,
transactions: Vec<(TransactionOrigin, Tx)>,
) -> Vec<TransactionValidationOutcome<Tx>> {
transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)).collect()
}
}

@frisitano frisitano added this to the Milestone 3 milestone Dec 3, 2024
@frisitano frisitano added this to Reth Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

No branches or pull requests

1 participant