diff --git a/Cargo.lock b/Cargo.lock index 1bf6da2e74c..76942039123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8422,6 +8422,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "alloy-rpc-types-eth", "arbitrary", "bytes", "derive_more", diff --git a/crates/optimism/node/src/args.rs b/crates/optimism/node/src/args.rs index 87c8c1be645..c5571e63ec6 100644 --- a/crates/optimism/node/src/args.rs +++ b/crates/optimism/node/src/args.rs @@ -10,6 +10,10 @@ pub struct RollupArgs { #[arg(long = "rollup.sequencer-http", value_name = "HTTP_URL")] pub sequencer_http: Option, + /// Enable transaction conditional support on sequencer + #[arg(long = "rollup.sequencer-transaction-conditional-enabled", default_value = "false")] + pub sequencer_transaction_conditional_enabled: bool, + /// Disable transaction pool gossip #[arg(long = "rollup.disable-tx-pool-gossip")] pub disable_txpool_gossip: bool, diff --git a/crates/optimism/node/src/txpool.rs b/crates/optimism/node/src/txpool.rs index a0e6c6119c5..820a78c2a6d 100644 --- a/crates/optimism/node/src/txpool.rs +++ b/crates/optimism/node/src/txpool.rs @@ -46,6 +46,13 @@ pub struct OpPooledTransaction { inner: EthPooledTransaction, /// The estimated size of this transaction, lazily computed. estimated_tx_compressed_size: OnceLock, + + /// Optional conditional attached to this transaction. Is this + /// needed if this field is on OpTransactionSigned? + conditional: Option, + + /// Indiciator if this transaction has been marked as rejected + rejected: AtomicBool // (is AtomicBool appropriate here?) } impl OpPooledTransaction { @@ -54,6 +61,7 @@ impl OpPooledTransaction { Self { inner: EthPooledTransaction::new(transaction, encoded_length), estimated_tx_compressed_size: Default::default(), + conditional: None, } } @@ -65,6 +73,21 @@ impl OpPooledTransaction { .estimated_tx_compressed_size .get_or_init(|| tx_estimated_size_fjord(&self.inner.transaction().encoded_2718())) } + + // TODO: Setter with the conditional + pub fn conditional(&self) -> Option<&TransactionConditional> { + self.conditional.as_ref() + } + + /// Mark this transaction as rejected + pub fn reject(&self) { + self.rejected.store(true, Ordering::Relaxed); + } + + /// Returns true if this transaction has been marked as rejected + pub fn rejected(&self) -> bool { + self.rejected.load(Ordering::Relaxed) + } } /// Calculate the estimated compressed transaction size in bytes, scaled by 1e6. @@ -343,6 +366,19 @@ where ) } + // If validated at the RPC layer pre-submission, this is not needed. The pool simply + // needs handle the rejected status on the pooled transaction set by the builder + if let Some(conditional) = transaction.conditional() { + //let client = self.client(); + //let header = client.latest_header()?.header(); + //if !conditional.matches_block_number(header.number()) { + // return TransactionValidationOutcome::Invalid( + // transaction, + // InvalidTransactionError::TxTypeNotSupported.into(), + // ) + //} + } + let outcome = self.inner.validate_one(origin, transaction); if !self.requires_l1_data_gas_fee() { @@ -388,6 +424,13 @@ where ) } + // Conditional transactions should not be propagated + // let propagate = if transaction.transaction_conditional().is_some() { + // false + // } else { + // propagate + // }; + return TransactionValidationOutcome::Valid { balance, state_nonce, diff --git a/crates/optimism/payload/src/builder.rs b/crates/optimism/payload/src/builder.rs index a25471ff82c..b003b5446df 100644 --- a/crates/optimism/payload/src/builder.rs +++ b/crates/optimism/payload/src/builder.rs @@ -873,6 +873,17 @@ where return Ok(Some(())) } + // check the conditional if present on the transaction + if let Some(conditional) = tx.transaction_conditional() { + best_txs.mark_invalid(tx.signer(), tx.nonce()); + + // This rejected transaction should be removed by the pool. Can we effectively + // do this with the pool wrapper? Can `best_transactions` yield non-rejected txs + tx.reject(); + + continue + } + // Configure the environment for the tx. let tx_env = self.evm_config.tx_env(tx.tx(), tx.signer()); diff --git a/crates/optimism/primitives/Cargo.toml b/crates/optimism/primitives/Cargo.toml index 3938a70ecbe..1f1a0360c4d 100644 --- a/crates/optimism/primitives/Cargo.toml +++ b/crates/optimism/primitives/Cargo.toml @@ -21,6 +21,7 @@ reth-zstd-compressors = { workspace = true, optional = true } alloy-primitives.workspace = true alloy-consensus.workspace = true alloy-rlp.workspace = true +alloy-rpc-types-eth.workspace = true alloy-eips.workspace = true revm-primitives = { workspace = true, optional = true } secp256k1 = { workspace = true, optional = true } diff --git a/crates/optimism/primitives/src/transaction/signed.rs b/crates/optimism/primitives/src/transaction/signed.rs index cde794efae2..ef36733e564 100644 --- a/crates/optimism/primitives/src/transaction/signed.rs +++ b/crates/optimism/primitives/src/transaction/signed.rs @@ -15,6 +15,7 @@ use alloy_primitives::{ keccak256, Address, Bytes, PrimitiveSignature as Signature, TxHash, TxKind, Uint, B256, }; use alloy_rlp::Header; +use alloy_rpc_types_eth::erc4337::TransactionConditional; use core::{ hash::{Hash, Hasher}, mem, @@ -33,7 +34,7 @@ use reth_primitives_traits::{ /// Signed transaction. #[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(rlp))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Eq, AsRef, Deref)] +#[derive(Debug, Clone, AsRef, Deref)] pub struct OpTransactionSigned { /// Transaction hash #[cfg_attr(feature = "serde", serde(skip))] @@ -44,8 +45,14 @@ pub struct OpTransactionSigned { #[deref] #[as_ref] pub transaction: OpTypedTransaction, + + /// Can we attach a conditional the moment a transaction is deserialized? + pub conditional: Option } +// TEMPORARY since TransactionConditional does not impl eq +impl Eq for OpTransactionSigned {} + impl OpTransactionSigned { /// Calculates hash of given transaction and signature and returns new instance. pub fn new(transaction: OpTypedTransaction, signature: Signature) -> Self { @@ -61,9 +68,10 @@ impl OpTransactionSigned { /// /// Note: this only calculates the hash on the first [`OpTransactionSigned::hash`] call. pub fn new_unhashed(transaction: OpTypedTransaction, signature: Signature) -> Self { - Self { hash: Default::default(), signature, transaction } + Self { hash: Default::default(), signature, transaction, conditional: None } } + /// Returns whether this transaction is a deposit. pub const fn is_deposit(&self) -> bool { matches!(self.transaction, OpTypedTransaction::Deposit(_)) diff --git a/crates/optimism/rpc/src/eth/transaction.rs b/crates/optimism/rpc/src/eth/transaction.rs index a2a425dc772..113fe758b3e 100644 --- a/crates/optimism/rpc/src/eth/transaction.rs +++ b/crates/optimism/rpc/src/eth/transaction.rs @@ -15,6 +15,7 @@ use reth_provider::{ use reth_rpc_eth_api::{ helpers::{EthSigner, EthTransactions, LoadTransaction, SpawnBlocking}, FromEthApiError, FullEthApiTypes, RpcNodeCore, RpcNodeCoreExt, TransactionCompat, + L2EthApiExt }; use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError}; use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool}; @@ -57,6 +58,39 @@ where } } +// Likely not the best place for this api. This RpcAddOn should only be present +// when the config for it is enabled. +impl L2EthApiExt for OpEthApi +where + Self: LoadTransaction, + N: OpNodeCore>>, +{ + async fn send_raw_transaction_conditional(&self, tx: Bytes, conditional: TransactionConditional) -> RpcResult { + // (1) sanity check the conditional + // conditional.validate() (< max cost, max > min, etc, etc) + + // (2) forward using the sequencer client if set and skip pool submission + + // (3) validation. block & state + let header = self.provider().latest_header()?.header(); + // conditional.matches_block_number(header.number); + // conditional.matches_timestamp(header.timestamp); + // ... + + // Can we attach attach a condititional to this transaction? Type here is not OpTransactionSigned. + // If we can't do this, then perhaps we need a custom pool interface. + let recovered = recover_raw_transaction(&tx)?.with_conditional(conditional); + let pool_transaction = ::Transaction::from_pooled(recovered); + let hash = self + .pool() + .add_transaction(TransactionOrigin::Local, pool_transaction) + .await + .map_err(Self::Error::from_eth_err)?; + + Ok(hash) + } +} + impl LoadTransaction for OpEthApi where Self: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt,