diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 8c60aaf0fb..2031e4c2f4 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -18,6 +18,7 @@ tempo-chainspec.workspace = true tempo-consensus.workspace = true tempo-payload-builder.workspace = true tempo-payload-types.workspace = true +tempo-contracts.workspace = true tempo-precompiles.workspace = true tempo-primitives = { workspace = true, features = ["default"] } diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index e8069a2f66..6da0b30330 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -22,7 +22,8 @@ pub use tempo_primitives as primitives; mod version; -type TempoNodeAdapter = NodeAdapter>; +type TempoFullNodeTypes = RethFullAdapter; +type TempoNodeAdapter = NodeAdapter; /// Type alias for a launched tempo node. -pub type TempoFullNode = FullNode>; +pub type TempoFullNode = FullNode>; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index f453f5aead..7ad45c8129 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -2,14 +2,15 @@ use crate::{ TempoPayloadTypes, engine::TempoEngineValidator, rpc::{ - TempoAdminApi, TempoAdminApiServer, TempoEthApiBuilder, TempoEthExt, TempoEthExtApiServer, - TempoForkScheduleApiServer, TempoForkScheduleRpc, TempoToken, TempoTokenApiServer, + TempoAdminApi, TempoAdminApiServer, TempoEthApi, TempoEthApiBuilder, TempoEthExt, + TempoEthExtApiServer, TempoForkScheduleApiServer, TempoForkScheduleRpc, TempoSimulate, + TempoSimulateApiServer, TempoToken, TempoTokenApiServer, }, }; use alloy_primitives::B256; use reth_evm::revm::primitives::Address; use reth_node_api::{ - AddOnsContext, FullNodeComponents, FullNodeTypes, NodeAddOns, NodePrimitives, NodeTypes, + AddOnsContext, FullNodeComponents, FullNodeTypes, NodeAddOns, NodeTypes, PayloadAttributesBuilder, PayloadTypes, }; use reth_node_builder::{ @@ -19,8 +20,8 @@ use reth_node_builder::{ PayloadBuilderBuilder, PoolBuilder, TxPoolBuilder, spawn_maintenance_tasks, }, rpc::{ - BasicEngineValidatorBuilder, EngineValidatorAddOn, EngineValidatorBuilder, EthApiBuilder, - NoopEngineApiBuilder, PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, + BasicEngineValidatorBuilder, EngineValidatorAddOn, NoopEngineApiBuilder, + PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, RpcHandle, RpcHooks, }, }; use reth_node_ethereum::EthereumNetworkBuilder; @@ -152,18 +153,19 @@ impl NodeTypes for TempoNode { } #[derive(Debug)] -pub struct TempoAddOns< - N: FullNodeComponents, - EthB: EthApiBuilder = TempoEthApiBuilder, - PVB = TempoEngineValidatorBuilder, - EVB = BasicEngineValidatorBuilder, - RpcMiddleware = Identity, -> { - inner: RpcAddOns, +pub struct TempoAddOns> { + inner: RpcAddOns< + NodeAdapter, + TempoEthApiBuilder, + TempoEngineValidatorBuilder, + NoopEngineApiBuilder, + BasicEngineValidatorBuilder, + Identity, + >, validator_key: Option, } -impl TempoAddOns, TempoEthApiBuilder> +impl TempoAddOns where N: FullNodeTypes, { @@ -182,20 +184,20 @@ where } } -impl NodeAddOns for TempoAddOns +impl NodeAddOns> for TempoAddOns where - N: FullNodeComponents, - EthB: EthApiBuilder, - PVB: Send + PayloadValidatorBuilder, - EVB: EngineValidatorBuilder, - EthB::EthApi: - RpcNodeCore>, + N: FullNodeTypes, { - type Handle = as NodeAddOns>::Handle; + type Handle = RpcHandle, TempoEthApi>; - async fn launch_add_ons(self, ctx: AddOnsContext<'_, N>) -> eyre::Result { - let eth_config = - EthConfigHandler::new(ctx.node.provider().clone(), ctx.node.evm_config().clone()); + async fn launch_add_ons( + self, + ctx: AddOnsContext<'_, NodeAdapter>, + ) -> eyre::Result { + let eth_config = EthConfigHandler::new( + ctx.node.provider.clone(), + ctx.node.components.evm_config.clone(), + ); self.inner .launch_add_ons_with(ctx, move |container| { @@ -205,13 +207,15 @@ where let eth_api = registry.eth_api().clone(); let token = TempoToken::new(eth_api.clone()); - let eth_ext = TempoEthExt::new(eth_api); + let eth_ext = TempoEthExt::new(eth_api.clone()); + let simulate = TempoSimulate::new(eth_api); let admin = TempoAdminApi::new(self.validator_key); let fork_schedule = TempoForkScheduleRpc::new(registry.eth_api().provider().clone()); modules.merge_configured(token.into_rpc())?; modules.merge_configured(eth_ext.into_rpc())?; + modules.merge_if_module_configured(RethRpcModule::Eth, simulate.into_rpc())?; modules.merge_configured(fork_schedule.into_rpc())?; modules.merge_if_module_configured(RethRpcModule::Admin, admin.into_rpc())?; modules.merge_if_module_configured(RethRpcModule::Eth, eth_config.into_rpc())?; @@ -222,30 +226,22 @@ where } } -impl RethRpcAddOns for TempoAddOns +impl RethRpcAddOns> for TempoAddOns where - N: FullNodeComponents, - EthB: EthApiBuilder, - PVB: PayloadValidatorBuilder, - EVB: EngineValidatorBuilder, - EthB::EthApi: - RpcNodeCore>, + N: FullNodeTypes, { - type EthApi = EthB::EthApi; + type EthApi = TempoEthApi; - fn hooks_mut(&mut self) -> &mut reth_node_builder::rpc::RpcHooks { + fn hooks_mut(&mut self) -> &mut RpcHooks, Self::EthApi> { self.inner.hooks_mut() } } -impl EngineValidatorAddOn for TempoAddOns +impl EngineValidatorAddOn> for TempoAddOns where - N: FullNodeComponents, - EthB: EthApiBuilder, - PVB: Send, - EVB: EngineValidatorBuilder, + N: FullNodeTypes, { - type ValidatorBuilder = EVB; + type ValidatorBuilder = BasicEngineValidatorBuilder; fn engine_validator_builder(&self) -> Self::ValidatorBuilder { self.inner.engine_validator_builder() @@ -265,7 +261,7 @@ where TempoConsensusBuilder, >; - type AddOns = TempoAddOns>; + type AddOns = TempoAddOns; fn components_builder(&self) -> Self::ComponentsBuilder { Self::components(self.pool_builder, self.payload_builder_builder) diff --git a/crates/node/src/rpc/mod.rs b/crates/node/src/rpc/mod.rs index b01255064e..a1ed7094ca 100644 --- a/crates/node/src/rpc/mod.rs +++ b/crates/node/src/rpc/mod.rs @@ -3,6 +3,7 @@ pub mod consensus; pub mod error; pub mod eth_ext; pub mod fork_schedule; +pub mod simulate; pub mod token; pub use admin::{TempoAdminApi, TempoAdminApiServer}; @@ -18,6 +19,7 @@ use reth_primitives_traits::{ }; use reth_rpc_eth_api::{FromEthApiError, RpcTxReq}; use reth_transaction_pool::{PoolPooledTx, TransactionOrigin}; +pub use simulate::{TempoSimulate, TempoSimulateApiServer, TempoSimulateV1Response}; use std::sync::Arc; pub use tempo_alloy::rpc::TempoTransactionRequest; use tempo_chainspec::TempoChainSpec; @@ -87,7 +89,7 @@ pub const SUBBLOCK_TX_CHANNEL_CAPACITY: usize = 10_000; /// /// This type implements the [`FullEthApi`](reth_rpc_eth_api::helpers::FullEthApi) by implemented /// all the `Eth` helper traits and prerequisite traits. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct TempoEthApi> { /// Gateway to node's core components. inner: EthApi, DynRpcConverter>, diff --git a/crates/node/src/rpc/simulate.rs b/crates/node/src/rpc/simulate.rs new file mode 100644 index 0000000000..8b333028b2 --- /dev/null +++ b/crates/node/src/rpc/simulate.rs @@ -0,0 +1,259 @@ +use crate::{node::TempoNode, rpc::TempoEthApi}; +use alloy_primitives::{Address, B256, keccak256}; +use alloy_rpc_types_eth::simulate::SimulatedBlock; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use reth_ethereum::evm::revm::database::StateProviderDatabase; +use reth_node_api::FullNodeTypes; +use reth_primitives_traits::AlloyBlockHeader as _; +use reth_provider::{BlockIdReader, ChainSpecProvider, HeaderProvider}; +use reth_rpc_eth_api::{ + RpcBlock, RpcNodeCore, + helpers::{EthCall, LoadState, SpawnBlocking}, +}; +use reth_tracing::tracing; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, HashSet}, + sync::LazyLock, +}; +use tempo_chainspec::hardfork::TempoHardforks; +use tempo_contracts::precompiles::DECIMALS as TIP20_DECIMALS; +use tempo_evm::TempoStateAccess; +use tempo_precompiles::{ + error::TempoPrecompileError, + tip20::{TIP20Token, is_tip20_prefix}, +}; + +/// keccak256("Transfer(address,address,uint256)") +static TRANSFER_TOPIC: LazyLock = + LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)")); + +/// TIP-20 token metadata returned alongside simulation results. +/// +/// `decimals` is omitted because all TIP-20 tokens use a fixed decimal count +/// ([`TIP20_DECIMALS`]). The top-level response includes a `decimals` field instead. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Tip20TokenMetadata { + pub name: String, + pub symbol: String, + pub currency: String, +} + +/// Response for `tempo_simulateV1`. +/// +/// Wraps the standard `eth_simulateV1` response with a top-level `tokenMetadata` map +/// containing TIP-20 token info for all tokens involved in transfer logs. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TempoSimulateV1Response { + /// Standard simulation results (one per simulated block). + pub blocks: Vec>, + /// Decimal count shared by all TIP-20 tokens. + #[serde(with = "alloy_serde::quantity")] + pub decimals: u8, + /// Token metadata for TIP-20 addresses that appear in Transfer logs. + pub token_metadata: BTreeMap, +} + +#[rpc(server, namespace = "tempo")] +pub trait TempoSimulateApi { + /// Simulates transactions like `eth_simulateV1` but enriches the response with + /// TIP-20 token metadata for all tokens involved in Transfer events. + /// + /// This eliminates the need for a second roundtrip to fetch token symbols/decimals + /// after simulation. + #[method(name = "simulateV1")] + async fn simulate_v1( + &self, + payload: alloy_rpc_types_eth::simulate::SimulatePayload< + tempo_alloy::rpc::TempoTransactionRequest, + >, + block: Option, + ) -> RpcResult>>; +} + +/// Implementation of `tempo_simulateV1`. +#[derive(Debug, Clone)] +pub struct TempoSimulate> { + eth_api: TempoEthApi, +} + +impl> TempoSimulate { + pub fn new(eth_api: TempoEthApi) -> Self { + Self { eth_api } + } +} + +/// Extract TIP-20 addresses from the simulation request's call targets. +/// +/// This allows metadata resolution to start before simulation completes. +fn extract_tip20_targets( + payload: &alloy_rpc_types_eth::simulate::SimulatePayload< + tempo_alloy::rpc::TempoTransactionRequest, + >, +) -> Vec
{ + let mut addrs = std::collections::BTreeSet::new(); + for block in &payload.block_state_calls { + for call in &block.calls { + // Standard `to` field + if let Some(to) = call.to.as_ref().and_then(|k| k.to()) + && is_tip20_prefix(*to) + { + addrs.insert(*to); + } + // AA calls array + for c in &call.calls { + if let Some(to) = c.to.to() + && is_tip20_prefix(*to) + { + addrs.insert(*to); + } + } + // Fee token + if let Some(ft) = call.fee_token + && is_tip20_prefix(ft) + { + addrs.insert(ft); + } + } + } + addrs.into_iter().collect() +} + +#[async_trait::async_trait] +impl> TempoSimulateApiServer for TempoSimulate { + async fn simulate_v1( + &self, + payload: alloy_rpc_types_eth::simulate::SimulatePayload< + tempo_alloy::rpc::TempoTransactionRequest, + >, + block: Option, + ) -> RpcResult>> { + // Pre-extract TIP-20 addresses from call targets so we can start + // metadata resolution concurrently with the simulation. + let prefetched = extract_tip20_targets(&payload); + + // Run simulation and metadata prefetch concurrently + let (sim_result, mut token_metadata) = tokio::join!( + self.eth_api.simulate_v1(payload, block), + self.resolve_token_metadata(prefetched, block), + ); + + let blocks = sim_result.map_err(|e| { + let err: jsonrpsee::types::ErrorObject<'static> = e.into(); + err + })?; + + // Scan simulation logs for any additional TIP-20 addresses not in the + // prefetched set (e.g. tokens touched indirectly via contract calls). + let mut extra = HashSet::new(); + for sim_block in &blocks { + for call in &sim_block.calls { + for log in &call.logs { + if is_tip20_prefix(log.address()) + && log.topics().first() == Some(&*TRANSFER_TOPIC) + && !token_metadata.contains_key(&log.address()) + { + extra.insert(log.address()); + } + } + } + } + + if !extra.is_empty() { + let extra_metadata = self + .resolve_token_metadata(extra.into_iter().collect(), block) + .await; + token_metadata.extend(extra_metadata); + } + + Ok(TempoSimulateV1Response { + blocks, + decimals: TIP20_DECIMALS, + token_metadata, + }) + } +} + +impl> TempoSimulate { + /// Resolves TIP-20 token metadata for the given addresses using state at the target block. + async fn resolve_token_metadata( + &self, + addresses: Vec
, + block: Option, + ) -> BTreeMap { + if addresses.is_empty() { + return BTreeMap::new(); + } + + let result = self + .eth_api + .spawn_blocking_io_fut(async move |this| { + let state = this.state_at_block_id_or_latest(block).await?; + + // Derive hardfork spec from the target block's timestamp. + let timestamp = block + .and_then(|id| { + this.provider() + .block_number_for_id(id) + .ok() + .flatten() + .and_then(|num| { + this.provider() + .header_by_number(num) + .ok() + .flatten() + .map(|h| h.timestamp()) + }) + }) + .unwrap_or(u64::MAX); + + let spec = this.provider().chain_spec().tempo_hardfork_at(timestamp); + let mut db = StateProviderDatabase::new(state); + + let mut metadata = BTreeMap::new(); + for addr in &addresses { + let result = db.with_read_only_storage_ctx(spec, || { + let token = TIP20Token::from_address(*addr)?; + Ok::<_, TempoPrecompileError>(( + token.name()?, + token.symbol()?, + token.currency()?, + )) + }); + + match result { + Ok((name, symbol, currency)) => { + metadata.insert( + *addr, + Tip20TokenMetadata { + name, + symbol, + currency, + }, + ); + } + Err(e) => { + tracing::warn!( + token = %addr, + error = %e, + "failed to resolve TIP-20 metadata, skipping" + ); + } + } + } + + Ok(metadata) + }) + .await; + + match result { + Ok(m) => m, + Err(e) => { + tracing::warn!(error = ?e, "failed to resolve token metadata"); + BTreeMap::new() + } + } + } +}