Skip to main content

tempo_node/rpc/
mod.rs

1pub mod admin;
2pub mod consensus;
3pub mod error;
4pub mod eth_ext;
5pub mod token;
6
7pub use admin::{TempoAdminApi, TempoAdminApiServer};
8use alloy_primitives::B256;
9use alloy_rpc_types_eth::{Log, ReceiptWithBloom};
10pub use consensus::{TempoConsensusApiServer, TempoConsensusRpc};
11pub use eth_ext::{TempoEthExt, TempoEthExtApiServer};
12use futures::{TryFutureExt, future::Either};
13use reth_errors::RethError;
14use reth_primitives_traits::{
15    Recovered, TransactionMeta, TxTy, WithEncoded, transaction::TxHashRef,
16};
17use reth_rpc_eth_api::{FromEthApiError, RpcTxReq};
18use reth_transaction_pool::{PoolPooledTx, TransactionOrigin};
19use std::sync::Arc;
20pub use tempo_alloy::rpc::TempoTransactionRequest;
21use tempo_chainspec::TempoChainSpec;
22use tempo_evm::TempoStateAccess;
23use tempo_precompiles::{NONCE_PRECOMPILE_ADDRESS, nonce::NonceManager};
24use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
25pub use token::{TempoToken, TempoTokenApiServer};
26
27use crate::{node::TempoNode, rpc::error::TempoEthApiError};
28use alloy::primitives::{U256, uint};
29use reth_ethereum::tasks::{
30    Runtime,
31    pool::{BlockingTaskGuard, BlockingTaskPool},
32};
33use reth_evm::{
34    EvmEnvFor, TxEnvFor,
35    revm::{Database, context::result::EVMError},
36};
37use reth_node_api::{FullNodeComponents, FullNodeTypes, HeaderTy, PrimitivesTy};
38use reth_node_builder::{
39    NodeAdapter,
40    rpc::{EthApiBuilder, EthApiCtx},
41};
42use reth_provider::{ChainSpecProvider, ProviderError};
43use reth_rpc::{DynRpcConverter, eth::EthApi};
44use reth_rpc_eth_api::{
45    EthApiTypes, RpcConverter, RpcNodeCore, RpcNodeCoreExt,
46    helpers::{
47        Call, EthApiSpec, EthBlocks, EthCall, EthFees, EthState, EthTransactions, LoadBlock,
48        LoadFee, LoadPendingBlock, LoadReceipt, LoadState, LoadTransaction, SpawnBlocking, Trace,
49        estimate::EstimateCall, pending_block::PendingEnvBuilder, spec::SignersForRpc,
50    },
51    transaction::{ConvertReceiptInput, ReceiptConverter},
52};
53use reth_rpc_eth_types::{
54    EthApiError, EthStateCache, FeeHistoryCache, FillTransaction, GasPriceOracle, PendingBlock,
55    builder::config::PendingBlockKind, receipt::EthReceiptConverter,
56};
57use tempo_alloy::{TempoNetwork, rpc::TempoTransactionReceipt};
58use tempo_evm::TempoEvmConfig;
59use tempo_primitives::{
60    TEMPO_GAS_PRICE_SCALING_FACTOR, TempoPrimitives, TempoReceipt, TempoTxEnvelope,
61    subblock::PartialValidatorKey,
62};
63use tokio::sync::{Mutex, broadcast};
64
65/// Placeholder constant for `eth_getBalance` calls because the native token balance is N/A on
66/// Tempo.
67pub const NATIVE_BALANCE_PLACEHOLDER: U256 =
68    uint!(4242424242424242424242424242424242424242424242424242424242424242424242424242_U256);
69
70/// Capacity of the subblock transactions broadcast channel.
71///
72/// This is set high enough to prevent legitimate transactions from being evicted
73/// during high-load scenarios. Transactions are filtered by validator key before
74/// being added to the channel to prevent DoS attacks.
75pub const SUBBLOCK_TX_CHANNEL_CAPACITY: usize = 10_000;
76
77/// Tempo `Eth` API implementation.
78///
79/// This type provides the functionality for handling `eth_` related requests.
80///
81/// This wraps a default `Eth` implementation, and provides additional functionality where the
82/// Tempo spec deviates from the default ethereum spec, e.g. gas estimation denominated in
83/// `feeToken`
84///
85/// This type implements the [`FullEthApi`](reth_rpc_eth_api::helpers::FullEthApi) by implemented
86/// all the `Eth` helper traits and prerequisite traits.
87#[derive(Clone)]
88pub struct TempoEthApi<N: FullNodeTypes<Types = TempoNode>> {
89    /// Gateway to node's core components.
90    inner: EthApi<NodeAdapter<N>, DynRpcConverter<TempoEvmConfig, TempoNetwork>>,
91
92    /// Channel for sending subblock transactions to the subblocks service.
93    subblock_transactions_tx: broadcast::Sender<Recovered<TempoTxEnvelope>>,
94
95    /// Validator public key used to filter subblock transactions.
96    ///
97    /// Only subblock transactions targeting this validator will be accepted.
98    /// This prevents DoS attacks via channel flooding with transactions
99    /// targeting other validators.
100    validator_key: Option<B256>,
101}
102
103impl<N: FullNodeTypes<Types = TempoNode>> TempoEthApi<N> {
104    /// Creates a new `TempoEthApi`.
105    pub fn new(
106        eth_api: EthApi<NodeAdapter<N>, DynRpcConverter<TempoEvmConfig, TempoNetwork>>,
107        validator_key: Option<B256>,
108    ) -> Self {
109        Self {
110            inner: eth_api,
111            subblock_transactions_tx: broadcast::channel(SUBBLOCK_TX_CHANNEL_CAPACITY).0,
112            validator_key,
113        }
114    }
115
116    /// Returns a [`broadcast::Receiver`] for subblock transactions.
117    pub fn subblock_transactions_rx(&self) -> broadcast::Receiver<Recovered<TempoTxEnvelope>> {
118        self.subblock_transactions_tx.subscribe()
119    }
120
121    /// Returns `true` if the given partial validator key matches this node's validator key.
122    ///
123    /// Returns `false` if no validator key is configured (non-validator nodes reject
124    /// all subblock transactions).
125    fn matches_validator_key(&self, partial_key: &PartialValidatorKey) -> bool {
126        self.validator_key
127            .is_some_and(|key| partial_key.matches(key.as_slice()))
128    }
129}
130
131impl<N: FullNodeTypes<Types = TempoNode>> EthApiTypes for TempoEthApi<N> {
132    type Error = TempoEthApiError;
133    type NetworkTypes = TempoNetwork;
134    type RpcConvert = DynRpcConverter<TempoEvmConfig, TempoNetwork>;
135
136    fn converter(&self) -> &Self::RpcConvert {
137        self.inner.converter()
138    }
139}
140
141impl<N: FullNodeTypes<Types = TempoNode>> RpcNodeCore for TempoEthApi<N> {
142    type Primitives = PrimitivesTy<N::Types>;
143    type Provider = N::Provider;
144    type Pool = <NodeAdapter<N> as FullNodeComponents>::Pool;
145    type Evm = <NodeAdapter<N> as FullNodeComponents>::Evm;
146    type Network = <NodeAdapter<N> as FullNodeComponents>::Network;
147
148    #[inline]
149    fn pool(&self) -> &Self::Pool {
150        self.inner.pool()
151    }
152
153    #[inline]
154    fn evm_config(&self) -> &Self::Evm {
155        self.inner.evm_config()
156    }
157
158    #[inline]
159    fn network(&self) -> &Self::Network {
160        self.inner.network()
161    }
162
163    #[inline]
164    fn provider(&self) -> &Self::Provider {
165        self.inner.provider()
166    }
167}
168
169impl<N: FullNodeTypes<Types = TempoNode>> RpcNodeCoreExt for TempoEthApi<N> {
170    #[inline]
171    fn cache(&self) -> &EthStateCache<PrimitivesTy<N::Types>> {
172        self.inner.cache()
173    }
174}
175
176impl<N: FullNodeTypes<Types = TempoNode>> EthApiSpec for TempoEthApi<N> {
177    #[inline]
178    fn starting_block(&self) -> U256 {
179        self.inner.starting_block()
180    }
181}
182
183impl<N: FullNodeTypes<Types = TempoNode>> SpawnBlocking for TempoEthApi<N> {
184    #[inline]
185    fn io_task_spawner(&self) -> &Runtime {
186        self.inner.task_spawner()
187    }
188
189    #[inline]
190    fn tracing_task_pool(&self) -> &BlockingTaskPool {
191        self.inner.blocking_task_pool()
192    }
193
194    #[inline]
195    fn tracing_task_guard(&self) -> &BlockingTaskGuard {
196        self.inner.blocking_task_guard()
197    }
198
199    #[inline]
200    fn blocking_io_task_guard(&self) -> &Arc<tokio::sync::Semaphore> {
201        self.inner.blocking_io_task_guard()
202    }
203}
204
205impl<N: FullNodeTypes<Types = TempoNode>> LoadPendingBlock for TempoEthApi<N> {
206    #[inline]
207    fn pending_block(&self) -> &Mutex<Option<PendingBlock<Self::Primitives>>> {
208        self.inner.pending_block()
209    }
210
211    #[inline]
212    fn pending_env_builder(&self) -> &dyn PendingEnvBuilder<Self::Evm> {
213        self.inner.pending_env_builder()
214    }
215
216    #[inline]
217    fn pending_block_kind(&self) -> PendingBlockKind {
218        // don't build a local pending block because we can't build a block without consensus data (system transaction)
219        PendingBlockKind::None
220    }
221}
222
223impl<N: FullNodeTypes<Types = TempoNode>> LoadFee for TempoEthApi<N> {
224    #[inline]
225    fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider> {
226        self.inner.gas_oracle()
227    }
228
229    #[inline]
230    fn fee_history_cache(&self) -> &FeeHistoryCache<HeaderTy<N::Types>> {
231        self.inner.fee_history_cache()
232    }
233}
234
235impl<N: FullNodeTypes<Types = TempoNode>> LoadState for TempoEthApi<N> {}
236
237impl<N: FullNodeTypes<Types = TempoNode>> EthState for TempoEthApi<N> {
238    #[inline]
239    async fn balance(
240        &self,
241        _address: alloy_primitives::Address,
242        _block_id: Option<alloy_eips::BlockId>,
243    ) -> Result<U256, Self::Error> {
244        Ok(NATIVE_BALANCE_PLACEHOLDER)
245    }
246
247    #[inline]
248    fn max_proof_window(&self) -> u64 {
249        self.inner.eth_proof_window()
250    }
251}
252
253impl<N: FullNodeTypes<Types = TempoNode>> EthFees for TempoEthApi<N> {}
254
255impl<N: FullNodeTypes<Types = TempoNode>> Trace for TempoEthApi<N> {}
256
257impl<N: FullNodeTypes<Types = TempoNode>> EthCall for TempoEthApi<N> {}
258
259impl<N: FullNodeTypes<Types = TempoNode>> Call for TempoEthApi<N> {
260    #[inline]
261    fn call_gas_limit(&self) -> u64 {
262        self.inner.gas_cap()
263    }
264
265    #[inline]
266    fn max_simulate_blocks(&self) -> u64 {
267        self.inner.max_simulate_blocks()
268    }
269
270    #[inline]
271    fn evm_memory_limit(&self) -> u64 {
272        self.inner.evm_memory_limit()
273    }
274
275    /// Returns the max gas limit that the caller can afford given a transaction environment.
276    fn caller_gas_allowance(
277        &self,
278        mut db: impl Database<Error: Into<EthApiError>>,
279        evm_env: &EvmEnvFor<Self::Evm>,
280        tx_env: &TxEnvFor<Self::Evm>,
281    ) -> Result<u64, Self::Error> {
282        let fee_payer = tx_env
283            .fee_payer()
284            .map_err(EVMError::<ProviderError, _>::from)?;
285
286        let fee_token = db
287            .get_fee_token(tx_env, fee_payer, evm_env.cfg_env.spec)
288            .map_err(ProviderError::other)?;
289        let fee_token_balance = db
290            .get_token_balance(fee_token, fee_payer, evm_env.cfg_env.spec)
291            .map_err(ProviderError::other)?;
292
293        Ok(fee_token_balance
294            // multiply by the scaling factor
295            .saturating_mul(TEMPO_GAS_PRICE_SCALING_FACTOR)
296            // Calculate the amount of gas the caller can afford with the specified gas price.
297            .checked_div(U256::from(tx_env.inner.gas_price))
298            // This will be 0 if gas price is 0. It is fine, because we check it before.
299            .unwrap_or_default()
300            .saturating_to())
301    }
302
303    fn create_txn_env(
304        &self,
305        evm_env: &EvmEnvFor<Self::Evm>,
306        mut request: TempoTransactionRequest,
307        mut db: impl Database<Error: Into<EthApiError>>,
308    ) -> Result<TxEnvFor<Self::Evm>, Self::Error> {
309        if let Some(nonce_key) = request.nonce_key
310            && !nonce_key.is_zero()
311            && request.nonce.is_none()
312        {
313            let nonce = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
314                0 // expiring nonce must be 0
315            } else {
316                // 2D nonce: fetch from storage
317                let slot =
318                    NonceManager::new().nonces[request.from.unwrap_or_default()][nonce_key].slot();
319                db.storage(NONCE_PRECOMPILE_ADDRESS, slot)
320                    .map_err(Into::into)?
321                    .saturating_to()
322            };
323            request.nonce = Some(nonce);
324        }
325
326        Ok(self.inner.create_txn_env(evm_env, request, db)?)
327    }
328}
329
330impl<N: FullNodeTypes<Types = TempoNode>> EstimateCall for TempoEthApi<N> {}
331impl<N: FullNodeTypes<Types = TempoNode>> LoadBlock for TempoEthApi<N> {}
332impl<N: FullNodeTypes<Types = TempoNode>> LoadReceipt for TempoEthApi<N> {}
333impl<N: FullNodeTypes<Types = TempoNode>> EthBlocks for TempoEthApi<N> {}
334impl<N: FullNodeTypes<Types = TempoNode>> LoadTransaction for TempoEthApi<N> {}
335
336impl<N: FullNodeTypes<Types = TempoNode>> EthTransactions for TempoEthApi<N> {
337    fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
338        self.inner.signers()
339    }
340
341    fn send_raw_transaction_sync_timeout(&self) -> std::time::Duration {
342        self.inner.send_raw_transaction_sync_timeout()
343    }
344
345    fn send_transaction(
346        &self,
347        origin: TransactionOrigin,
348        tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
349    ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
350        match tx.value().inner().subblock_proposer() {
351            Some(proposer) if self.matches_validator_key(&proposer) => {
352                let subblock_tx = self.subblock_transactions_tx.clone();
353                Either::Left(Either::Left(async move {
354                    let tx_hash = *tx.value().tx_hash();
355
356                    subblock_tx.send(tx.into_value()).map_err(|_| {
357                        EthApiError::from(RethError::msg("subblocks service channel closed"))
358                    })?;
359
360                    Ok(tx_hash)
361                }))
362            }
363            Some(_) => Either::Left(Either::Right(futures::future::err(
364                EthApiError::from(RethError::msg(
365                    "subblock transaction rejected: target validator mismatch",
366                ))
367                .into(),
368            ))),
369            None => Either::Right(self.inner.send_transaction(origin, tx).map_err(Into::into)),
370        }
371    }
372
373    async fn fill_transaction(
374        &self,
375        mut request: RpcTxReq<Self::NetworkTypes>,
376    ) -> Result<FillTransaction<TxTy<Self::Primitives>>, Self::Error> {
377        if let Some(nonce_key) = request.nonce_key
378            && !nonce_key.is_zero()
379        {
380            if request.nonce.is_none() {
381                let nonce = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
382                    0 // expiring nonce must be 0
383                } else {
384                    // 2D nonce: fetch from storage
385                    let slot = NonceManager::new().nonces[request.from.unwrap_or_default()]
386                        [nonce_key]
387                        .slot();
388                    self.spawn_blocking_io(move |this| {
389                        this.latest_state()?
390                            .storage(NONCE_PRECOMPILE_ADDRESS, slot.into())
391                            .map_err(Self::Error::from_eth_err)
392                    })
393                    .await?
394                    .unwrap_or_default()
395                    .saturating_to()
396                };
397                request.nonce = Some(nonce);
398            }
399
400            // Fill gas using self to ensure Tempo's create_txn_env handles 2D nonce correctly
401            if request.gas.is_none() {
402                let gas = EstimateCall::estimate_gas_at(
403                    self,
404                    request.clone(),
405                    alloy_eips::BlockId::pending(),
406                    None,
407                )
408                .await?;
409                request.gas = Some(gas.to());
410            }
411        }
412
413        Ok(self.inner.fill_transaction(request).await?)
414    }
415}
416
417/// Converter for Tempo receipts.
418#[derive(Debug, Clone)]
419#[expect(clippy::type_complexity)]
420pub struct TempoReceiptConverter {
421    inner: EthReceiptConverter<
422        TempoChainSpec,
423        fn(TempoReceipt, usize, TransactionMeta) -> ReceiptWithBloom<TempoReceipt<Log>>,
424    >,
425}
426
427impl TempoReceiptConverter {
428    pub fn new(chain_spec: Arc<TempoChainSpec>) -> Self {
429        Self {
430            inner: EthReceiptConverter::new(chain_spec).with_builder(
431                |receipt: TempoReceipt, next_log_index, meta| {
432                    let mut log_index = next_log_index;
433                    receipt
434                        .map_logs(|log| {
435                            let idx = log_index;
436                            log_index += 1;
437                            Log {
438                                inner: log,
439                                block_hash: Some(meta.block_hash),
440                                block_number: Some(meta.block_number),
441                                block_timestamp: Some(meta.timestamp),
442                                transaction_hash: Some(meta.tx_hash),
443                                transaction_index: Some(meta.index),
444                                log_index: Some(idx as u64),
445                                removed: false,
446                            }
447                        })
448                        .into()
449                },
450            ),
451        }
452    }
453}
454
455impl ReceiptConverter<TempoPrimitives> for TempoReceiptConverter {
456    type RpcReceipt = TempoTransactionReceipt;
457    type Error = EthApiError;
458
459    fn convert_receipts(
460        &self,
461        receipts: Vec<ConvertReceiptInput<'_, TempoPrimitives>>,
462    ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
463        let txs = receipts.iter().map(|r| r.tx).collect::<Vec<_>>();
464        self.inner
465            .convert_receipts(receipts)?
466            .into_iter()
467            .zip(txs)
468            .map(|(inner, tx)| {
469                let mut receipt = TempoTransactionReceipt {
470                    inner,
471                    fee_token: None,
472                    // should never fail, we only deal with valid transactions here
473                    fee_payer: tx
474                        .fee_payer(tx.signer())
475                        .map_err(|_| EthApiError::InvalidTransactionSignature)?,
476                };
477                if receipt.effective_gas_price == 0 || receipt.gas_used == 0 {
478                    return Ok(receipt);
479                }
480
481                // Set fee token to the address that emitted the last log.
482                //
483                // Assumption is that every non-free transaction will end with a
484                // fee token transfer to TIPFeeManager.
485                receipt.fee_token = receipt.logs().last().map(|log| log.address());
486                Ok(receipt)
487            })
488            .collect()
489    }
490}
491
492#[derive(Debug, Default)]
493pub struct TempoEthApiBuilder {
494    /// Validator public key used to filter subblock transactions.
495    pub validator_key: Option<B256>,
496}
497
498impl TempoEthApiBuilder {
499    /// Creates a new builder with the given validator key.
500    pub fn new(validator_key: Option<B256>) -> Self {
501        Self { validator_key }
502    }
503}
504
505impl<N> EthApiBuilder<NodeAdapter<N>> for TempoEthApiBuilder
506where
507    N: FullNodeTypes<Types = TempoNode>,
508{
509    type EthApi = TempoEthApi<N>;
510
511    async fn build_eth_api(self, ctx: EthApiCtx<'_, NodeAdapter<N>>) -> eyre::Result<Self::EthApi> {
512        let chain_spec = ctx.components.provider.chain_spec();
513        let eth_api = ctx
514            .eth_api_builder()
515            .modify_gas_oracle_config(|config| config.default_suggested_fee = Some(U256::ZERO))
516            .map_converter(|_| RpcConverter::new(TempoReceiptConverter::new(chain_spec)).erased())
517            .build();
518
519        Ok(TempoEthApi::new(eth_api, self.validator_key))
520    }
521}