reth_seismic_rpc/eth/
ext.rs

1//! seismic implementation of eth api and its extensions
2//!
3//! Overrides the eth_ namespace to be compatible with seismic specific types
4//! Most endpoints handle transaction decrytpion before passing to the inner eth api
5//! For `eth_sendRawTransaction`, we directly call the inner eth api without decryption
6//! See that function's docs for more details
7
8use super::api::FullSeismicApi;
9use crate::{error::SeismicEthApiError, utils::convert_seismic_call_to_tx_request};
10use alloy_dyn_abi::TypedData;
11use alloy_json_rpc::RpcObject;
12use alloy_primitives::{Address, Bytes, B256, U256};
13use alloy_rpc_types::{
14    state::{EvmOverrides, StateOverride},
15    BlockId, BlockOverrides, TransactionRequest,
16};
17use alloy_rpc_types_eth::simulate::{
18    SimBlock as EthSimBlock, SimulatePayload as EthSimulatePayload, SimulatedBlock,
19};
20use futures::Future;
21use jsonrpsee::{
22    core::{async_trait, RpcResult},
23    proc_macros::rpc,
24};
25use reth_node_core::node_config::NodeConfig;
26use reth_rpc_eth_api::{
27    helpers::{EthCall, EthTransactions},
28    RpcBlock,
29};
30use reth_rpc_eth_types::EthApiError;
31use reth_tracing::tracing::*;
32use seismic_alloy_consensus::{InputDecryptionElements, TypedDataRequest};
33use seismic_alloy_rpc_types::{
34    SeismicCallRequest, SeismicRawTxRequest, SeismicTransactionRequest,
35    SimBlock as SeismicSimBlock, SimulatePayload as SeismicSimulatePayload,
36};
37use seismic_enclave::{
38    request_types::GetPurposeKeysRequest, rpc::EnclaveApiClient, EnclaveClient, PublicKey,
39};
40use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
41
42/// trait interface for a custom rpc namespace: `seismic`
43///
44/// This defines an additional namespace where all methods are configured as trait functions.
45#[cfg_attr(not(feature = "client"), rpc(server, namespace = "seismic"))]
46#[cfg_attr(feature = "client", rpc(server, client, namespace = "seismic"))]
47pub trait SeismicApi {
48    /// Returns the network public key
49    #[method(name = "getTeePublicKey")]
50    async fn get_tee_public_key(&self) -> RpcResult<PublicKey>;
51}
52
53/// Implementation of the seismic rpc api
54#[derive(Debug, Default)]
55pub struct SeismicApi {
56    enclave_client: EnclaveClient,
57}
58
59impl SeismicApi {
60    /// Creates a new seismic api instance
61    pub fn new<ChainSpec>(config: &NodeConfig<ChainSpec>) -> Self {
62        Self {
63            enclave_client: EnclaveClient::builder()
64                .ip(config.enclave.enclave_server_addr.to_string())
65                .port(config.enclave.enclave_server_port)
66                .timeout(std::time::Duration::from_secs(config.enclave.enclave_timeout))
67                .build()
68                .expect("Failed to build enclave client"),
69        }
70    }
71
72    /// Creates a new seismic api instance with an enclave client
73    pub fn with_enclave_client(mut self, enclave_client: EnclaveClient) -> Self {
74        self.enclave_client = enclave_client;
75        self
76    }
77}
78
79#[async_trait]
80impl SeismicApiServer for SeismicApi {
81    async fn get_tee_public_key(&self) -> RpcResult<PublicKey> {
82        trace!(target: "rpc::seismic", "Serving seismic_getTeePublicKey");
83        self.enclave_client
84            .get_purpose_keys(GetPurposeKeysRequest { epoch: 0 })
85            .await
86            .map(|keys| keys.tx_io_pk)
87            .map_err(|e| SeismicEthApiError::EnclaveError(e.to_string()).into())
88    }
89}
90
91/// Localhost with port 0 so a free port is used.
92pub const fn test_address() -> SocketAddr {
93    SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))
94}
95
96/// Extension trait for `EthTransactions` to add custom transaction sending functionalities.
97pub trait SeismicTransaction: EthTransactions {
98    /// Decodes, signs (if necessary via an internal signer or enclave),
99    /// and submits a typed data transaction to the pool.
100    /// Returns the hash of the transaction.
101    fn send_typed_data_transaction(
102        &self,
103        tx_request: TypedDataRequest,
104    ) -> impl Future<Output = Result<B256, Self::Error>> + Send;
105}
106
107/// Seismic `eth_` RPC namespace overrides.
108#[cfg_attr(not(feature = "client"), rpc(server, namespace = "eth"))]
109#[cfg_attr(feature = "client", rpc(server, client, namespace = "eth"))]
110pub trait EthApiOverride<B: RpcObject> {
111    /// Returns the account and storage values of the specified account including the Merkle-proof.
112    /// This call can be used to verify that the data you are pulling from is not tampered with.
113    #[method(name = "signTypedData_v4")]
114    async fn sign_typed_data_v4(&self, address: Address, data: TypedData) -> RpcResult<String>;
115
116    /// `eth_simulateV1` executes an arbitrary number of transactions on top of the requested state.
117    /// The transactions are packed into individual blocks. Overrides can be provided.
118    #[method(name = "simulateV1")]
119    async fn simulate_v1(
120        &self,
121        opts: SeismicSimulatePayload<SeismicCallRequest>,
122        block_number: Option<BlockId>,
123    ) -> RpcResult<Vec<SimulatedBlock<B>>>;
124
125    /// Executes a new message call immediately without creating a transaction on the block chain.
126    #[method(name = "call")]
127    async fn call(
128        &self,
129        request: SeismicCallRequest,
130        block_number: Option<BlockId>,
131        state_overrides: Option<StateOverride>,
132        block_overrides: Option<Box<BlockOverrides>>,
133    ) -> RpcResult<Bytes>;
134
135    /// Sends signed transaction, returning its hash.
136    #[method(name = "sendRawTransaction")]
137    async fn send_raw_transaction(&self, bytes: SeismicRawTxRequest) -> RpcResult<B256>;
138
139    /// Generates and returns an estimate of how much gas is necessary to allow the transaction to
140    /// complete.
141    #[method(name = "estimateGas")]
142    async fn estimate_gas(
143        &self,
144        request: SeismicTransactionRequest,
145        block_number: Option<BlockId>,
146        state_override: Option<StateOverride>,
147    ) -> RpcResult<U256>;
148}
149
150/// Implementation of the `eth_` namespace override
151#[derive(Debug)]
152pub struct EthApiExt<Eth> {
153    eth_api: Eth,
154    enclave_client: EnclaveClient,
155}
156
157impl<Eth> EthApiExt<Eth> {
158    /// Create a new `EthApiExt` module.
159    pub const fn new(eth_api: Eth, enclave_client: EnclaveClient) -> Self {
160        Self { eth_api, enclave_client }
161    }
162}
163
164#[async_trait]
165impl<Eth> EthApiOverrideServer<RpcBlock<Eth::NetworkTypes>> for EthApiExt<Eth>
166where
167    Eth: FullSeismicApi,
168    jsonrpsee_types::error::ErrorObject<'static>: From<Eth::Error>,
169{
170    /// Handler for: `eth_signTypedData_v4`
171    ///
172    /// TODO: determine if this should be removed, seems the same as eth functionality
173    async fn sign_typed_data_v4(&self, from: Address, data: TypedData) -> RpcResult<String> {
174        debug!(target: "reth-seismic-rpc::eth", "Serving seismic eth_signTypedData_v4 extension");
175        let signature = EthTransactions::sign_typed_data(&self.eth_api, &data, from)
176            .map_err(|err| err.into())?;
177        let signature = alloy_primitives::hex::encode(signature);
178        Ok(format!("0x{signature}"))
179    }
180
181    /// Handler for: `eth_simulateV1`
182    async fn simulate_v1(
183        &self,
184        payload: SeismicSimulatePayload<SeismicCallRequest>,
185        block_number: Option<BlockId>,
186    ) -> RpcResult<Vec<SimulatedBlock<RpcBlock<Eth::NetworkTypes>>>> {
187        debug!(target: "reth-seismic-rpc::eth", "Serving seismic eth_simulateV1 extension");
188
189        let seismic_sim_blocks: Vec<SeismicSimBlock<SeismicCallRequest>> =
190            payload.block_state_calls.clone();
191
192        // Recover EthSimBlocks from the SeismicSimulatePayload<SeismicCallRequest>
193        let mut eth_simulated_blocks: Vec<EthSimBlock> =
194            Vec::with_capacity(payload.block_state_calls.len());
195        for block in payload.block_state_calls {
196            let SeismicSimBlock { block_overrides, state_overrides, calls } = block;
197            let mut prepared_calls = Vec::with_capacity(calls.len());
198
199            for call in calls {
200                let seismic_tx_request = convert_seismic_call_to_tx_request(call)?;
201                let seismic_tx_request = seismic_tx_request
202                    .plaintext_copy(&self.enclave_client)
203                    .map_err(|e| ext_decryption_error(e.to_string()))?;
204                let tx_request: TransactionRequest = seismic_tx_request.inner;
205                prepared_calls.push(tx_request);
206            }
207
208            let prepared_block =
209                EthSimBlock { block_overrides, state_overrides, calls: prepared_calls };
210
211            eth_simulated_blocks.push(prepared_block);
212        }
213
214        // Call Eth simulate_v1, which only takes EthSimPayload/EthSimBlock
215        let mut result = EthCall::simulate_v1(
216            &self.eth_api,
217            EthSimulatePayload {
218                block_state_calls: eth_simulated_blocks.clone(),
219                trace_transfers: payload.trace_transfers,
220                validation: payload.validation,
221                return_full_transactions: payload.return_full_transactions,
222            },
223            block_number,
224        )
225        .await?;
226
227        // Convert Eth Blocks back to Seismic blocks
228        for (block, result) in seismic_sim_blocks.iter().zip(result.iter_mut()) {
229            let SeismicSimBlock::<SeismicCallRequest> { calls, .. } = block;
230            let SimulatedBlock { calls: call_results, .. } = result;
231
232            for (call_result, call) in call_results.iter_mut().zip(calls.iter()) {
233                let seismic_tx_request = convert_seismic_call_to_tx_request(call.clone())?;
234
235                if let Some(seismic_elements) = seismic_tx_request.seismic_elements {
236                    // if there are seismic elements, encrypt the output
237                    let encrypted_output = seismic_elements
238                        .server_encrypt(&self.enclave_client, &call_result.return_data)
239                        .map_err(|e| ext_encryption_error(e.to_string()))?;
240                    call_result.return_data = encrypted_output;
241                }
242            }
243        }
244
245        Ok(result)
246    }
247
248    /// Handler for: `eth_call`
249    async fn call(
250        &self,
251        request: SeismicCallRequest,
252        block_number: Option<BlockId>,
253        state_overrides: Option<StateOverride>,
254        block_overrides: Option<Box<BlockOverrides>>,
255    ) -> RpcResult<Bytes> {
256        debug!(target: "reth-seismic-rpc::eth", ?request, ?block_number, ?state_overrides, ?block_overrides, "Serving seismic eth_call extension");
257
258        // process different CallRequest types
259        let seismic_tx_request = convert_seismic_call_to_tx_request(request)?;
260
261        // decrypt seismic elements
262        let tx_request = seismic_tx_request
263            .plaintext_copy(&self.enclave_client)
264            .map_err(|e| ext_decryption_error(e.to_string()))?
265            .inner;
266
267        // call inner
268        let result = EthCall::call(
269            &self.eth_api,
270            tx_request,
271            block_number,
272            EvmOverrides::new(state_overrides, block_overrides),
273        )
274        .await?;
275
276        // encrypt result
277        if let Some(seismic_elements) = seismic_tx_request.seismic_elements {
278            return Ok(seismic_elements.server_encrypt(&self.enclave_client, &result).unwrap());
279        } else {
280            Ok(result)
281        }
282    }
283
284    /// Handler for: `eth_sendRawTransaction`
285    ///
286    /// Directly calls the inner eth api without decryption
287    /// We do this so that it is encrypted in the tx pool, so it is encrypted in blocks
288    /// decryption during execution is handled by the [`SeismicBlockExecutor`]
289    async fn send_raw_transaction(&self, tx: SeismicRawTxRequest) -> RpcResult<B256> {
290        debug!(target: "reth-seismic-rpc::eth", ?tx, "Serving overridden eth_sendRawTransaction extension");
291        match tx {
292            SeismicRawTxRequest::Bytes(bytes) => {
293                Ok(EthTransactions::send_raw_transaction(&self.eth_api, bytes).await?)
294            }
295            SeismicRawTxRequest::TypedData(typed_data) => {
296                Ok(SeismicTransaction::send_typed_data_transaction(&self.eth_api, typed_data)
297                    .await?)
298            }
299        }
300    }
301
302    async fn estimate_gas(
303        &self,
304        request: SeismicTransactionRequest,
305        block_number: Option<BlockId>,
306        state_override: Option<StateOverride>,
307    ) -> RpcResult<U256> {
308        debug!(target: "reth-seismic-rpc::eth", ?request, ?block_number, ?state_override, "serving seismic eth_estimateGas extension");
309        // decrypt
310        let decrypted_req = request
311            .plaintext_copy(&self.enclave_client)
312            .map_err(|e| ext_decryption_error(e.to_string()))?;
313
314        // call inner
315        Ok(EthCall::estimate_gas_at(
316            &self.eth_api,
317            decrypted_req.inner,
318            block_number.unwrap_or_default(),
319            state_override,
320        )
321        .await?)
322    }
323}
324
325/// Creates a EthApiError that says that seismic decryption failed
326pub fn ext_decryption_error(e_str: String) -> EthApiError {
327    EthApiError::Other(Box::new(jsonrpsee_types::ErrorObject::owned(
328        -32000, // TODO: pick a better error code?
329        "Error Decrypting in Seismic EthApiExt",
330        Some(e_str),
331    )))
332}
333
334/// Creates a EthApiError that says that seismic encryption failed
335pub fn ext_encryption_error(e_str: String) -> EthApiError {
336    EthApiError::Other(Box::new(jsonrpsee_types::ErrorObject::owned(
337        -32000, // TODO: pick a better error code?
338        "Error Encrypting in Seismic EthApiExt",
339        Some(e_str),
340    )))
341}