feat: add l1 cost check

This commit is contained in:
Matthias Seitz
2024-02-05 18:35:38 +01:00
parent 8a7fb727cc
commit 57d05ea196
5 changed files with 232 additions and 117 deletions

View File

@@ -22,7 +22,14 @@ reth-revm.workspace = true
# misc
async-trait.workspace = true
parking_lot.workspace = true
serde.workspace = true
[features]
optimism = []
optimism = [
"reth-revm/optimism",
"reth-primitives/optimism",
"reth-rpc-types/optimism",
"reth-provider/optimism",
"reth-payload-builder/optimism",
]

View File

@@ -19,4 +19,4 @@ pub use engine::OptimismEngineTypes;
pub mod evm;
pub use evm::OptimismEvmConfig;
pub mod txpool;
pub mod txpool;

View File

@@ -1,22 +1,147 @@
//! OP transaction pool types
use reth_primitives::SealedBlock;
use parking_lot::RwLock;
use reth_primitives::{Block, ChainSpec, GotExpected, InvalidTransactionError, SealedBlock};
use reth_provider::{BlockReaderIdExt, StateProviderFactory};
use reth_transaction_pool::{EthPoolTransaction, EthTransactionValidator, TransactionOrigin, TransactionValidationOutcome, TransactionValidator};
use reth_revm::{optimism::RethL1BlockInfo, L1BlockInfo};
use reth_transaction_pool::{
EthPoolTransaction, EthTransactionValidator, TransactionOrigin, TransactionValidationOutcome,
TransactionValidator,
};
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
/// Validator for Ethereum transactions.
/// Validator for Optimism transactions.
#[derive(Debug, Clone)]
pub struct OpTransactionValidator<Client, T>
{
pub struct OpTransactionValidator<Client, Tx> {
/// The type that performs the actual validation.
inner: EthTransactionValidator<Client, T>,
inner: EthTransactionValidator<Client, Tx>,
/// Additional block info required for validation.
block_info: Arc<OpBlockInfo>,
}
impl<Client, Tx> OpTransactionValidator<Client, Tx> {
/// Returns the configured chain spec
pub fn chain_spec(&self) -> Arc<ChainSpec> {
self.inner.chain_spec()
}
/// Returns the current block timestamp.
fn block_timestamp(&self) -> u64 {
self.block_info.timestamp.load(Ordering::Relaxed)
}
}
impl<Client, Tx> OpTransactionValidator<Client, Tx>
where
Client: StateProviderFactory + BlockReaderIdExt,
Tx: EthPoolTransaction,
{
/// Create a new [OpTransactionValidator].
pub fn new(inner: EthTransactionValidator<Client, Tx>) -> Self {
let this = Self { inner, block_info: Arc::new(OpBlockInfo::default()) };
if let Ok(Some(block)) =
this.inner.client().block_by_number_or_tag(reth_primitives::BlockNumberOrTag::Latest)
{
this.update_l1_block_info(&block);
}
this
}
/// Update the L1 block info.
fn update_l1_block_info(&self, block: &Block) {
self.block_info.timestamp.store(block.timestamp, Ordering::Relaxed);
let cost_addition = reth_revm::optimism::extract_l1_info(block).ok();
*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> {
let outcome = self.inner.validate_one(origin, transaction);
// 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 Some(l1_block_info) = self.block_info.l1_block_info.read().clone() else {
return TransactionValidationOutcome::Error(
*valid_tx.hash(),
"L1BlockInfoError".into(),
)
};
let mut encoded = reth_primitives::bytes::BytesMut::default();
valid_tx.transaction().to_recovered_transaction().encode_enveloped(&mut encoded);
let cost_addition = match l1_block_info.l1_tx_data_fee(
&self.chain_spec(),
self.block_timestamp(),
&encoded.freeze().into(),
valid_tx.transaction().is_deposit(),
) {
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()
}
}
#[async_trait::async_trait]
impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
where
Client: StateProviderFactory + BlockReaderIdExt,
Tx: EthPoolTransaction,
where
Client: StateProviderFactory + BlockReaderIdExt,
Tx: EthPoolTransaction,
{
type Transaction = Tx;
@@ -25,17 +150,77 @@ impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
origin: TransactionOrigin,
transaction: Self::Transaction,
) -> TransactionValidationOutcome<Self::Transaction> {
self.inner.validate_one(origin, transaction).await
self.validate_one(origin, transaction)
}
async fn validate_transactions(
&self,
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
self.inner.validate_all(transactions).await
self.validate_all(transactions)
}
fn on_new_head_block(&self, new_tip_block: &SealedBlock) {
self.inner.on_new_head_block(new_tip_block)
self.inner.on_new_head_block(new_tip_block);
self.update_l1_block_info(&new_tip_block.clone().unseal());
}
}
}
/// Tracks additional infos for the current block.
#[derive(Debug, Default)]
struct OpBlockInfo {
/// The current L1 block info.
l1_block_info: RwLock<Option<L1BlockInfo>>,
/// Current block timestamp.
timestamp: AtomicU64,
}
#[cfg(test)]
mod tests {
use crate::txpool::OpTransactionValidator;
use reth_primitives::{
Signature, Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered,
TxDeposit, MAINNET, U256,
};
use reth_provider::test_utils::MockEthProvider;
use reth_transaction_pool::{
blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
EthPooledTransaction, TransactionOrigin, TransactionValidationOutcome,
};
#[test]
fn validate_optimism_transaction() {
let client = MockEthProvider::default();
let validator = EthTransactionValidatorBuilder::new(MAINNET.clone())
.no_shanghai()
.no_cancun()
.build(client, InMemoryBlobStore::default());
let validator = OpTransactionValidator::new(validator);
let origin = TransactionOrigin::External;
let signer = Default::default();
let deposit_tx = Transaction::Deposit(TxDeposit {
source_hash: Default::default(),
from: signer,
to: TransactionKind::Create,
mint: None,
value: reth_primitives::TxValue::from(U256::ZERO),
gas_limit: 0u64,
is_system_transaction: false,
input: Default::default(),
});
let signature = Signature { r: U256::ZERO, s: U256::ZERO, odd_y_parity: false };
let signed_tx = TransactionSigned::from_transaction_and_signature(deposit_tx, signature);
let signed_recovered =
TransactionSignedEcRecovered::from_signed_transaction(signed_tx, signer);
let len = signed_recovered.length_without_header();
let pooled_tx = EthPooledTransaction::new(signed_recovered, len);
let outcome = validator.validate_one(origin, pooled_tx);
let err = match outcome {
TransactionValidationOutcome::Invalid(_, err) => err,
_ => panic!("Expected invalid transaction"),
};
assert_eq!(err.to_string(), "transaction type not supported");
}
}

View File

@@ -26,9 +26,6 @@ use std::{
};
use tokio::sync::Mutex;
#[cfg(feature = "optimism")]
use reth_revm::optimism::RethL1BlockInfo;
/// Validator for Ethereum transactions.
#[derive(Debug, Clone)]
pub struct EthTransactionValidator<Client, T> {
@@ -36,6 +33,18 @@ pub struct EthTransactionValidator<Client, T> {
inner: Arc<EthTransactionValidatorInner<Client, T>>,
}
impl<Client, Tx> EthTransactionValidator<Client, Tx> {
/// Returns the configured chain spec
pub fn chain_spec(&self) -> Arc<ChainSpec> {
self.inner.chain_spec.clone()
}
/// Returns the configured client
pub fn client(&self) -> &Client {
&self.inner.client
}
}
impl<Client, Tx> EthTransactionValidator<Client, Tx>
where
Client: StateProviderFactory + BlockReaderIdExt,
@@ -142,14 +151,6 @@ where
origin: TransactionOrigin,
mut transaction: Tx,
) -> TransactionValidationOutcome<Tx> {
#[cfg(feature = "optimism")]
if transaction.is_deposit() {
return TransactionValidationOutcome::Invalid(
transaction,
InvalidTransactionError::TxTypeNotSupported.into(),
)
}
// Checks for tx_type
match transaction.tx_type() {
LEGACY_TX_TYPE_ID => {
@@ -314,52 +315,8 @@ where
)
}
#[cfg(not(feature = "optimism"))]
let cost = transaction.cost();
#[cfg(feature = "optimism")]
let cost = {
let block = match self
.client
.block_by_number_or_tag(reth_primitives::BlockNumberOrTag::Latest)
{
Ok(Some(block)) => block,
Ok(None) => {
return TransactionValidationOutcome::Error(
*transaction.hash(),
"Latest block should be found".into(),
)
}
Err(err) => {
return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err))
}
};
let mut encoded = reth_primitives::bytes::BytesMut::default();
transaction.to_recovered_transaction().encode_enveloped(&mut encoded);
let cost_addition = match reth_revm::optimism::extract_l1_info(&block).map(|info| {
info.l1_tx_data_fee(
&self.chain_spec,
block.timestamp,
&encoded.freeze().into(),
transaction.is_deposit(),
)
}) {
Ok(Ok(cost)) => cost,
Err(err) => {
return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err))
}
_ => {
return TransactionValidationOutcome::Error(
*transaction.hash(),
"L1BlockInfoError".into(),
)
}
};
transaction.cost().saturating_add(cost_addition)
};
// Checks for max cost
if cost > account.balance {
return TransactionValidationOutcome::Invalid(
@@ -785,47 +742,4 @@ mod tests {
let tx = pool.get(transaction.hash());
assert!(tx.is_some());
}
#[cfg(feature = "optimism")]
#[tokio::test(flavor = "multi_thread")]
async fn validate_optimism_transaction() {
use crate::{blobstore::InMemoryBlobStore, traits::EthPooledTransaction};
use reth_primitives::{
Signature, Transaction, TransactionKind, TransactionSigned,
TransactionSignedEcRecovered, TxDeposit, MAINNET, U256,
};
use reth_provider::test_utils::MockEthProvider;
use reth_tasks::TokioTaskExecutor;
let client = MockEthProvider::default();
let validator =
// EthTransactionValidator::new(client, MAINNET.clone(), TokioTaskExecutor::default());
crate::validate::EthTransactionValidatorBuilder::new(MAINNET.clone()).no_shanghai().no_cancun().build_with_tasks(client, TokioTaskExecutor::default(), InMemoryBlobStore::default());
let origin = crate::TransactionOrigin::External;
let signer = Default::default();
let deposit_tx = Transaction::Deposit(TxDeposit {
source_hash: Default::default(),
from: signer,
to: TransactionKind::Create,
mint: None,
value: reth_primitives::TxValue::from(U256::ZERO),
gas_limit: 0u64,
is_system_transaction: false,
input: Default::default(),
});
let signature = Signature { r: U256::ZERO, s: U256::ZERO, odd_y_parity: false };
let signed_tx = TransactionSigned::from_transaction_and_signature(deposit_tx, signature);
let signed_recovered =
TransactionSignedEcRecovered::from_signed_transaction(signed_tx, signer);
let len = signed_recovered.length_without_header();
let pooled_tx = EthPooledTransaction::new(signed_recovered, len);
let outcome =
crate::TransactionValidator::validate_transaction(&validator, origin, pooled_tx).await;
let err = match outcome {
crate::TransactionValidationOutcome::Invalid(_, err) => err,
_ => panic!("Expected invalid transaction"),
};
assert_eq!(err.to_string(), "transaction type not supported");
}
}

View File

@@ -112,17 +112,26 @@ impl<T> ValidTransaction<T> {
Self::Valid(transaction)
}
}
}
impl<T: PoolTransaction> ValidTransaction<T> {
/// Returns the transaction.
#[inline]
pub(crate) const fn transaction(&self) -> &T {
pub const fn transaction(&self) -> &T {
match self {
Self::Valid(transaction) => transaction,
Self::ValidWithSidecar { transaction, .. } => transaction,
}
}
/// Consumes the wrapper and returns the transaction.
pub fn into_transaction(self) -> T {
match self {
Self::Valid(transaction) => transaction,
Self::ValidWithSidecar { transaction, .. } => transaction,
}
}
}
impl<T: PoolTransaction> ValidTransaction<T> {
/// Returns the address of that transaction.
#[inline]
pub(crate) fn sender(&self) -> Address {
@@ -131,7 +140,7 @@ impl<T: PoolTransaction> ValidTransaction<T> {
/// Returns the hash of the transaction.
#[inline]
pub(crate) fn hash(&self) -> &B256 {
pub fn hash(&self) -> &B256 {
self.transaction().hash()
}