Skip to main content

tempo_node/rpc/
mod.rs

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