mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-02-07 13:35:04 -05:00
359 lines
11 KiB
Rust
359 lines
11 KiB
Rust
use crate::{
|
|
conditional::MaybeConditionalTransaction, estimated_da_size::DataAvailabilitySized,
|
|
interop::MaybeInteropTransaction,
|
|
};
|
|
use alloy_consensus::{transaction::Recovered, BlobTransactionValidationError, Typed2718};
|
|
use alloy_eips::{
|
|
eip2718::{Encodable2718, WithEncoded},
|
|
eip2930::AccessList,
|
|
eip7594::BlobTransactionSidecarVariant,
|
|
eip7702::SignedAuthorization,
|
|
};
|
|
use alloy_primitives::{Address, Bytes, TxHash, TxKind, B256, U256};
|
|
use alloy_rpc_types_eth::erc4337::TransactionConditional;
|
|
use c_kzg::KzgSettings;
|
|
use core::fmt::Debug;
|
|
use reth_optimism_primitives::OpTransactionSigned;
|
|
use reth_primitives_traits::{InMemorySize, SignedTransaction};
|
|
use reth_transaction_pool::{
|
|
EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
|
|
};
|
|
use std::{
|
|
borrow::Cow,
|
|
sync::{
|
|
atomic::{AtomicU64, Ordering},
|
|
Arc, OnceLock,
|
|
},
|
|
};
|
|
|
|
/// Marker for no-interop transactions
|
|
pub(crate) const NO_INTEROP_TX: u64 = 0;
|
|
|
|
/// Pool transaction for OP.
|
|
///
|
|
/// This type wraps the actual transaction and caches values that are frequently used by the pool.
|
|
/// For payload building this lazily tracks values that are required during payload building:
|
|
/// - Estimated compressed size of this transaction
|
|
#[derive(Debug, Clone, derive_more::Deref)]
|
|
pub struct OpPooledTransaction<
|
|
Cons = OpTransactionSigned,
|
|
Pooled = op_alloy_consensus::OpPooledTransaction,
|
|
> {
|
|
#[deref]
|
|
inner: EthPooledTransaction<Cons>,
|
|
/// The estimated size of this transaction, lazily computed.
|
|
estimated_tx_compressed_size: OnceLock<u64>,
|
|
/// The pooled transaction type.
|
|
_pd: core::marker::PhantomData<Pooled>,
|
|
|
|
/// Optional conditional attached to this transaction.
|
|
conditional: Option<Box<TransactionConditional>>,
|
|
|
|
/// Optional interop deadline attached to this transaction.
|
|
interop: Arc<AtomicU64>,
|
|
|
|
/// Cached EIP-2718 encoded bytes of the transaction, lazily computed.
|
|
encoded_2718: OnceLock<Bytes>,
|
|
}
|
|
|
|
impl<Cons: SignedTransaction, Pooled> OpPooledTransaction<Cons, Pooled> {
|
|
/// Create new instance of [Self].
|
|
pub fn new(transaction: Recovered<Cons>, encoded_length: usize) -> Self {
|
|
Self {
|
|
inner: EthPooledTransaction::new(transaction, encoded_length),
|
|
estimated_tx_compressed_size: Default::default(),
|
|
conditional: None,
|
|
interop: Arc::new(AtomicU64::new(NO_INTEROP_TX)),
|
|
_pd: core::marker::PhantomData,
|
|
encoded_2718: Default::default(),
|
|
}
|
|
}
|
|
|
|
/// Returns the estimated compressed size of a transaction in bytes scaled by 1e6.
|
|
/// This value is computed based on the following formula:
|
|
/// `max(minTransactionSize, intercept + fastlzCoef*fastlzSize)`
|
|
/// Uses cached EIP-2718 encoded bytes to avoid recomputing the encoding for each estimation.
|
|
pub fn estimated_compressed_size(&self) -> u64 {
|
|
*self
|
|
.estimated_tx_compressed_size
|
|
.get_or_init(|| op_alloy_flz::tx_estimated_size_fjord(self.encoded_2718()))
|
|
}
|
|
|
|
/// Returns lazily computed EIP-2718 encoded bytes of the transaction.
|
|
pub fn encoded_2718(&self) -> &Bytes {
|
|
self.encoded_2718.get_or_init(|| self.inner.transaction().encoded_2718().into())
|
|
}
|
|
|
|
/// Conditional setter.
|
|
pub fn with_conditional(mut self, conditional: TransactionConditional) -> Self {
|
|
self.conditional = Some(Box::new(conditional));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<Cons, Pooled> MaybeConditionalTransaction for OpPooledTransaction<Cons, Pooled> {
|
|
fn set_conditional(&mut self, conditional: TransactionConditional) {
|
|
self.conditional = Some(Box::new(conditional))
|
|
}
|
|
|
|
fn conditional(&self) -> Option<&TransactionConditional> {
|
|
self.conditional.as_deref()
|
|
}
|
|
}
|
|
|
|
impl<Cons, Pooled> MaybeInteropTransaction for OpPooledTransaction<Cons, Pooled> {
|
|
fn set_interop_deadline(&self, deadline: u64) {
|
|
self.interop.store(deadline, Ordering::Relaxed);
|
|
}
|
|
|
|
fn interop_deadline(&self) -> Option<u64> {
|
|
let interop = self.interop.load(Ordering::Relaxed);
|
|
if interop > NO_INTEROP_TX {
|
|
return Some(interop)
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
impl<Cons: SignedTransaction, Pooled> DataAvailabilitySized for OpPooledTransaction<Cons, Pooled> {
|
|
fn estimated_da_size(&self) -> u64 {
|
|
self.estimated_compressed_size()
|
|
}
|
|
}
|
|
|
|
impl<Cons, Pooled> PoolTransaction for OpPooledTransaction<Cons, Pooled>
|
|
where
|
|
Cons: SignedTransaction + From<Pooled>,
|
|
Pooled: SignedTransaction + TryFrom<Cons, Error: core::error::Error>,
|
|
{
|
|
type TryFromConsensusError = <Pooled as TryFrom<Cons>>::Error;
|
|
type Consensus = Cons;
|
|
type Pooled = Pooled;
|
|
|
|
fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
|
|
self.inner.transaction().clone()
|
|
}
|
|
|
|
fn into_consensus(self) -> Recovered<Self::Consensus> {
|
|
self.inner.transaction
|
|
}
|
|
|
|
fn into_consensus_with2718(self) -> WithEncoded<Recovered<Self::Consensus>> {
|
|
let encoding = self.encoded_2718().clone();
|
|
self.inner.transaction.into_encoded_with(encoding)
|
|
}
|
|
|
|
fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
|
|
let encoded_len = tx.encode_2718_len();
|
|
Self::new(tx.convert(), encoded_len)
|
|
}
|
|
|
|
fn hash(&self) -> &TxHash {
|
|
self.inner.transaction.tx_hash()
|
|
}
|
|
|
|
fn sender(&self) -> Address {
|
|
self.inner.transaction.signer()
|
|
}
|
|
|
|
fn sender_ref(&self) -> &Address {
|
|
self.inner.transaction.signer_ref()
|
|
}
|
|
|
|
fn cost(&self) -> &U256 {
|
|
&self.inner.cost
|
|
}
|
|
|
|
fn encoded_length(&self) -> usize {
|
|
self.inner.encoded_length
|
|
}
|
|
}
|
|
|
|
impl<Cons: Typed2718, Pooled> Typed2718 for OpPooledTransaction<Cons, Pooled> {
|
|
fn ty(&self) -> u8 {
|
|
self.inner.ty()
|
|
}
|
|
}
|
|
|
|
impl<Cons: InMemorySize, Pooled> InMemorySize for OpPooledTransaction<Cons, Pooled> {
|
|
fn size(&self) -> usize {
|
|
self.inner.size()
|
|
}
|
|
}
|
|
|
|
impl<Cons, Pooled> alloy_consensus::Transaction for OpPooledTransaction<Cons, Pooled>
|
|
where
|
|
Cons: alloy_consensus::Transaction,
|
|
Pooled: Debug + Send + Sync + 'static,
|
|
{
|
|
fn chain_id(&self) -> Option<u64> {
|
|
self.inner.chain_id()
|
|
}
|
|
|
|
fn nonce(&self) -> u64 {
|
|
self.inner.nonce()
|
|
}
|
|
|
|
fn gas_limit(&self) -> u64 {
|
|
self.inner.gas_limit()
|
|
}
|
|
|
|
fn gas_price(&self) -> Option<u128> {
|
|
self.inner.gas_price()
|
|
}
|
|
|
|
fn max_fee_per_gas(&self) -> u128 {
|
|
self.inner.max_fee_per_gas()
|
|
}
|
|
|
|
fn max_priority_fee_per_gas(&self) -> Option<u128> {
|
|
self.inner.max_priority_fee_per_gas()
|
|
}
|
|
|
|
fn max_fee_per_blob_gas(&self) -> Option<u128> {
|
|
self.inner.max_fee_per_blob_gas()
|
|
}
|
|
|
|
fn priority_fee_or_price(&self) -> u128 {
|
|
self.inner.priority_fee_or_price()
|
|
}
|
|
|
|
fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
|
|
self.inner.effective_gas_price(base_fee)
|
|
}
|
|
|
|
fn is_dynamic_fee(&self) -> bool {
|
|
self.inner.is_dynamic_fee()
|
|
}
|
|
|
|
fn kind(&self) -> TxKind {
|
|
self.inner.kind()
|
|
}
|
|
|
|
fn is_create(&self) -> bool {
|
|
self.inner.is_create()
|
|
}
|
|
|
|
fn value(&self) -> U256 {
|
|
self.inner.value()
|
|
}
|
|
|
|
fn input(&self) -> &Bytes {
|
|
self.inner.input()
|
|
}
|
|
|
|
fn access_list(&self) -> Option<&AccessList> {
|
|
self.inner.access_list()
|
|
}
|
|
|
|
fn blob_versioned_hashes(&self) -> Option<&[B256]> {
|
|
self.inner.blob_versioned_hashes()
|
|
}
|
|
|
|
fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
|
|
self.inner.authorization_list()
|
|
}
|
|
}
|
|
|
|
impl<Cons, Pooled> EthPoolTransaction for OpPooledTransaction<Cons, Pooled>
|
|
where
|
|
Cons: SignedTransaction + From<Pooled>,
|
|
Pooled: SignedTransaction + TryFrom<Cons>,
|
|
<Pooled as TryFrom<Cons>>::Error: core::error::Error,
|
|
{
|
|
fn take_blob(&mut self) -> EthBlobTransactionSidecar {
|
|
EthBlobTransactionSidecar::None
|
|
}
|
|
|
|
fn try_into_pooled_eip4844(
|
|
self,
|
|
_sidecar: Arc<BlobTransactionSidecarVariant>,
|
|
) -> Option<Recovered<Self::Pooled>> {
|
|
None
|
|
}
|
|
|
|
fn try_from_eip4844(
|
|
_tx: Recovered<Self::Consensus>,
|
|
_sidecar: BlobTransactionSidecarVariant,
|
|
) -> Option<Self> {
|
|
None
|
|
}
|
|
|
|
fn validate_blob(
|
|
&self,
|
|
_sidecar: &BlobTransactionSidecarVariant,
|
|
_settings: &KzgSettings,
|
|
) -> Result<(), BlobTransactionValidationError> {
|
|
Err(BlobTransactionValidationError::NotBlobTransaction(self.ty()))
|
|
}
|
|
}
|
|
|
|
/// Helper trait to provide payload builder with access to conditionals and encoded bytes of
|
|
/// transaction.
|
|
pub trait OpPooledTx:
|
|
MaybeConditionalTransaction + MaybeInteropTransaction + PoolTransaction + DataAvailabilitySized
|
|
{
|
|
/// Returns the EIP-2718 encoded bytes of the transaction.
|
|
fn encoded_2718(&self) -> Cow<'_, Bytes>;
|
|
}
|
|
|
|
impl<Cons, Pooled> OpPooledTx for OpPooledTransaction<Cons, Pooled>
|
|
where
|
|
Cons: SignedTransaction + From<Pooled>,
|
|
Pooled: SignedTransaction + TryFrom<Cons>,
|
|
<Pooled as TryFrom<Cons>>::Error: core::error::Error,
|
|
{
|
|
fn encoded_2718(&self) -> Cow<'_, Bytes> {
|
|
Cow::Borrowed(self.encoded_2718())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{OpPooledTransaction, OpTransactionValidator};
|
|
use alloy_consensus::transaction::Recovered;
|
|
use alloy_eips::eip2718::Encodable2718;
|
|
use alloy_primitives::{TxKind, U256};
|
|
use op_alloy_consensus::TxDeposit;
|
|
use reth_optimism_chainspec::OP_MAINNET;
|
|
use reth_optimism_primitives::OpTransactionSigned;
|
|
use reth_provider::test_utils::MockEthProvider;
|
|
use reth_transaction_pool::{
|
|
blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, TransactionOrigin,
|
|
TransactionValidationOutcome,
|
|
};
|
|
#[tokio::test]
|
|
async fn validate_optimism_transaction() {
|
|
let client = MockEthProvider::default().with_chain_spec(OP_MAINNET.clone());
|
|
let validator = EthTransactionValidatorBuilder::new(client)
|
|
.no_shanghai()
|
|
.no_cancun()
|
|
.build(InMemoryBlobStore::default());
|
|
let validator = OpTransactionValidator::new(validator);
|
|
|
|
let origin = TransactionOrigin::External;
|
|
let signer = Default::default();
|
|
let deposit_tx = TxDeposit {
|
|
source_hash: Default::default(),
|
|
from: signer,
|
|
to: TxKind::Create,
|
|
mint: 0,
|
|
value: U256::ZERO,
|
|
gas_limit: 0,
|
|
is_system_transaction: false,
|
|
input: Default::default(),
|
|
};
|
|
let signed_tx: OpTransactionSigned = deposit_tx.into();
|
|
let signed_recovered = Recovered::new_unchecked(signed_tx, signer);
|
|
let len = signed_recovered.encode_2718_len();
|
|
let pooled_tx: OpPooledTransaction = OpPooledTransaction::new(signed_recovered, len);
|
|
let outcome = validator.validate_one(origin, pooled_tx).await;
|
|
|
|
let err = match outcome {
|
|
TransactionValidationOutcome::Invalid(_, err) => err,
|
|
_ => panic!("Expected invalid transaction"),
|
|
};
|
|
assert_eq!(err.to_string(), "transaction type not supported");
|
|
}
|
|
}
|