reth_optimism_rpc/eth/
ext.rs

1//! Eth API extension.
2
3use crate::{error::TxConditionalErr, OpEthApiError, SequencerClient};
4use alloy_consensus::BlockHeader;
5use alloy_eips::BlockNumberOrTag;
6use alloy_primitives::{Bytes, StorageKey, B256, U256};
7use alloy_rpc_types_eth::erc4337::{AccountStorage, TransactionConditional};
8use jsonrpsee_core::RpcResult;
9use reth_optimism_txpool::conditional::MaybeConditionalTransaction;
10use reth_rpc_eth_api::L2EthApiExtServer;
11use reth_rpc_eth_types::utils::recover_raw_transaction;
12use reth_storage_api::{BlockReaderIdExt, StateProviderFactory};
13use reth_transaction_pool::{
14    AddedTransactionOutcome, PoolTransaction, TransactionOrigin, TransactionPool,
15};
16use std::sync::Arc;
17use tokio::sync::Semaphore;
18
19/// Maximum execution const for conditional transactions.
20const MAX_CONDITIONAL_EXECUTION_COST: u64 = 5000;
21
22const MAX_CONCURRENT_CONDITIONAL_VALIDATIONS: usize = 3;
23
24/// OP-Reth `Eth` API extensions implementation.
25///
26/// Separate from [`super::OpEthApi`] to allow to enable it conditionally,
27#[derive(Clone, Debug)]
28pub struct OpEthExtApi<Pool, Provider> {
29    /// Sequencer client, configured to forward submitted transactions to sequencer of given OP
30    /// network.
31    sequencer_client: Option<SequencerClient>,
32    inner: Arc<OpEthExtApiInner<Pool, Provider>>,
33}
34
35impl<Pool, Provider> OpEthExtApi<Pool, Provider>
36where
37    Provider: BlockReaderIdExt + StateProviderFactory + Clone + 'static,
38{
39    /// Creates a new [`OpEthExtApi`].
40    pub fn new(sequencer_client: Option<SequencerClient>, pool: Pool, provider: Provider) -> Self {
41        let inner = Arc::new(OpEthExtApiInner::new(pool, provider));
42        Self { sequencer_client, inner }
43    }
44
45    /// Returns the configured sequencer client, if any.
46    const fn sequencer_client(&self) -> Option<&SequencerClient> {
47        self.sequencer_client.as_ref()
48    }
49
50    #[inline]
51    fn pool(&self) -> &Pool {
52        self.inner.pool()
53    }
54
55    #[inline]
56    fn provider(&self) -> &Provider {
57        self.inner.provider()
58    }
59
60    /// Validates the conditional's `known accounts` settings against the current state.
61    async fn validate_known_accounts(
62        &self,
63        condition: &TransactionConditional,
64    ) -> Result<(), TxConditionalErr> {
65        if condition.known_accounts.is_empty() {
66            return Ok(());
67        }
68
69        let _permit =
70            self.inner.validation_semaphore.acquire().await.map_err(TxConditionalErr::internal)?;
71
72        let state = self
73            .provider()
74            .state_by_block_number_or_tag(BlockNumberOrTag::Latest)
75            .map_err(TxConditionalErr::internal)?;
76
77        for (address, storage) in &condition.known_accounts {
78            match storage {
79                AccountStorage::Slots(slots) => {
80                    for (slot, expected_value) in slots {
81                        let current = state
82                            .storage(*address, StorageKey::from(*slot))
83                            .map_err(TxConditionalErr::internal)?
84                            .unwrap_or_default();
85
86                        if current != U256::from_be_bytes(**expected_value) {
87                            return Err(TxConditionalErr::StorageValueMismatch);
88                        }
89                    }
90                }
91                AccountStorage::RootHash(expected_root) => {
92                    let actual_root = state
93                        .storage_root(*address, Default::default())
94                        .map_err(TxConditionalErr::internal)?;
95
96                    if *expected_root != actual_root {
97                        return Err(TxConditionalErr::StorageRootMismatch);
98                    }
99                }
100            }
101        }
102
103        Ok(())
104    }
105}
106
107#[async_trait::async_trait]
108impl<Pool, Provider> L2EthApiExtServer for OpEthExtApi<Pool, Provider>
109where
110    Provider: BlockReaderIdExt + StateProviderFactory + Clone + 'static,
111    Pool: TransactionPool<Transaction: MaybeConditionalTransaction> + 'static,
112{
113    async fn send_raw_transaction_conditional(
114        &self,
115        bytes: Bytes,
116        condition: TransactionConditional,
117    ) -> RpcResult<B256> {
118        // calculate and validate cost
119        let cost = condition.cost();
120        if cost > MAX_CONDITIONAL_EXECUTION_COST {
121            return Err(TxConditionalErr::ConditionalCostExceeded.into());
122        }
123
124        let recovered_tx = recover_raw_transaction(&bytes).map_err(|_| {
125            OpEthApiError::Eth(reth_rpc_eth_types::EthApiError::FailedToDecodeSignedTransaction)
126        })?;
127
128        let mut tx = <Pool as TransactionPool>::Transaction::from_pooled(recovered_tx);
129
130        // get current header
131        let header_not_found = || {
132            OpEthApiError::Eth(reth_rpc_eth_types::EthApiError::HeaderNotFound(
133                alloy_eips::BlockId::Number(BlockNumberOrTag::Latest),
134            ))
135        };
136        let header = self
137            .provider()
138            .latest_header()
139            .map_err(|_| header_not_found())?
140            .ok_or_else(header_not_found)?;
141
142        // Ensure that the condition can still be met by checking the max bounds
143        if condition.has_exceeded_block_number(header.header().number()) ||
144            condition.has_exceeded_timestamp(header.header().timestamp())
145        {
146            return Err(TxConditionalErr::InvalidCondition.into());
147        }
148
149        // Validate Account
150        self.validate_known_accounts(&condition).await?;
151
152        if let Some(sequencer) = self.sequencer_client() {
153            // If we have a sequencer client, forward the transaction
154            let _ = sequencer
155                .forward_raw_transaction_conditional(bytes.as_ref(), condition)
156                .await
157                .map_err(OpEthApiError::Sequencer)?;
158            Ok(*tx.hash())
159        } else {
160            // otherwise, add to pool with the appended conditional
161            tx.set_conditional(condition);
162            let AddedTransactionOutcome { hash, .. } =
163                self.pool().add_transaction(TransactionOrigin::Private, tx).await.map_err(|e| {
164                    OpEthApiError::Eth(reth_rpc_eth_types::EthApiError::PoolError(e.into()))
165                })?;
166
167            Ok(hash)
168        }
169    }
170}
171
172#[derive(Debug)]
173struct OpEthExtApiInner<Pool, Provider> {
174    /// The transaction pool of the node.
175    pool: Pool,
176    /// The provider type used to interact with the node.
177    provider: Provider,
178    /// The semaphore used to limit the number of concurrent conditional validations.
179    validation_semaphore: Semaphore,
180}
181
182impl<Pool, Provider> OpEthExtApiInner<Pool, Provider> {
183    fn new(pool: Pool, provider: Provider) -> Self {
184        Self {
185            pool,
186            provider,
187            validation_semaphore: Semaphore::new(MAX_CONCURRENT_CONDITIONAL_VALIDATIONS),
188        }
189    }
190
191    #[inline]
192    const fn pool(&self) -> &Pool {
193        &self.pool
194    }
195
196    #[inline]
197    const fn provider(&self) -> &Provider {
198        &self.provider
199    }
200}