seismic_rpc_api/
rpc.rs

1//! Seismic rpc logic.
2//!
3//! `seismic_` namespace overrides:
4//!
5//! - `seismic_getTeePublicKey` will return the public key of the Seismic enclave.
6//!
7//! `eth_` namespace overrides:
8//!
9//! - `eth_signTypedData_v4` will sign a typed data request using the Seismic enclave.
10
11use alloy_dyn_abi::TypedData;
12use alloy_json_rpc::RpcObject;
13use alloy_primitives::{Address, Bytes, B256};
14use alloy_rpc_types::{
15    simulate::SimBlock,
16    state::{EvmOverrides, StateOverride},
17    BlockId, BlockOverrides, SeismicCallRequest, SeismicRawTxRequest,
18};
19use alloy_rpc_types_eth::{
20    simulate::{SimulatePayload, SimulatedBlock},
21    transaction::TransactionRequest,
22};
23use jsonrpsee::{
24    core::{async_trait, RpcResult},
25    proc_macros::rpc,
26};
27use reth_evm::ConfigureEvmEnv;
28use reth_node_core::node_config::NodeConfig;
29use reth_rpc_eth_api::{
30    helpers::{EthCall, EthTransactions, FullEthApi},
31    RpcBlock,
32};
33use reth_rpc_eth_types::{
34    utils::{recover_raw_transaction, recover_typed_data_request},
35    EthApiError,
36};
37use reth_tracing::tracing::*;
38use reth_transaction_pool::{PoolPooledTx, PoolTransaction, TransactionPool};
39use secp256k1::PublicKey;
40use seismic_enclave::{rpc::EnclaveApiClient, EnclaveClient};
41use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
42
43use crate::{error::SeismicApiError, utils::seismic_override_call_request};
44/// trait interface for a custom rpc namespace: `seismic`
45///
46/// This defines an additional namespace where all methods are configured as trait functions.
47#[cfg_attr(not(feature = "client"), rpc(server, namespace = "seismic"))]
48#[cfg_attr(feature = "client", rpc(server, client, namespace = "seismic"))]
49pub trait SeismicApi {
50    /// Returns the network public key
51    #[method(name = "getTeePublicKey")]
52    async fn get_tee_public_key(&self) -> RpcResult<PublicKey>;
53}
54
55/// Implementation of the seismic rpc api
56#[derive(Debug, Default)]
57pub struct SeismicApi {
58    enclave_client: EnclaveClient,
59}
60
61impl SeismicApi {
62    /// Creates a new seismic api instance
63    pub fn new<ChainSpec>(config: &NodeConfig<ChainSpec>) -> Self {
64        Self {
65            enclave_client: EnclaveClient::builder()
66                .addr(config.enclave.enclave_server_addr.to_string())
67                .port(config.enclave.enclave_server_port)
68                .timeout(std::time::Duration::from_secs(config.enclave.enclave_timeout))
69                .build(),
70        }
71    }
72
73    /// Creates a new seismic api instance with an enclave client
74    pub fn with_enclave_client(mut self, enclave_client: EnclaveClient) -> Self {
75        self.enclave_client = enclave_client;
76        self
77    }
78}
79
80#[async_trait]
81impl SeismicApiServer for SeismicApi {
82    async fn get_tee_public_key(&self) -> RpcResult<PublicKey> {
83        trace!(target: "rpc::seismic", "Serving seismic_getTeePublicKey");
84        self.enclave_client
85            .get_public_key()
86            .await
87            .map_err(|e| SeismicApiError::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/// Seismic `eth_` RPC namespace overrides.
97#[cfg_attr(not(feature = "client"), rpc(server, namespace = "eth"))]
98#[cfg_attr(feature = "client", rpc(server, client, namespace = "eth"))]
99pub trait EthApiOverride<B: RpcObject> {
100    /// Returns the account and storage values of the specified account including the Merkle-proof.
101    /// This call can be used to verify that the data you are pulling from is not tampered with.
102    #[method(name = "signTypedData_v4")]
103    async fn sign_typed_data_v4(&self, address: Address, data: TypedData) -> RpcResult<String>;
104
105    /// `eth_simulateV1` executes an arbitrary number of transactions on top of the requested state.
106    /// The transactions are packed into individual blocks. Overrides can be provided.
107    #[method(name = "simulateV1")]
108    async fn simulate_v1(
109        &self,
110        opts: SimulatePayload<SeismicCallRequest>,
111        block_number: Option<BlockId>,
112    ) -> RpcResult<Vec<SimulatedBlock<B>>>;
113
114    /// Executes a new message call immediately without creating a transaction on the block chain.
115    #[method(name = "call")]
116    async fn call(
117        &self,
118        request: alloy_rpc_types::SeismicCallRequest,
119        block_number: Option<BlockId>,
120        state_overrides: Option<StateOverride>,
121        block_overrides: Option<Box<BlockOverrides>>,
122    ) -> RpcResult<Bytes>;
123
124    /// Sends signed transaction, returning its hash.
125    #[method(name = "sendRawTransaction")]
126    async fn send_raw_transaction(&self, bytes: SeismicRawTxRequest) -> RpcResult<B256>;
127}
128
129/// Implementation of the `eth_` namespace override
130#[derive(Debug)]
131pub struct EthApiExt<Eth> {
132    eth_api: Eth,
133}
134
135impl<Eth> EthApiExt<Eth> {
136    /// Create a new `EthApiExt` module.
137    pub const fn new(eth_api: Eth) -> Self {
138        Self { eth_api }
139    }
140}
141
142#[async_trait]
143impl<Eth> EthApiOverrideServer<RpcBlock<Eth::NetworkTypes>> for EthApiExt<Eth>
144where
145    Eth: FullEthApi,
146    jsonrpsee_types::error::ErrorObject<'static>: From<Eth::Error>,
147{
148    /// Handler for: `eth_signTypedData_v4`
149    async fn sign_typed_data_v4(&self, from: Address, data: TypedData) -> RpcResult<String> {
150        trace!(target: "rpc::eth", "Serving eth_signTypedData_v4");
151        let signature = EthTransactions::sign_typed_data(&self.eth_api, &data, from)
152            .map_err(|err| err.into())?;
153        let signature = alloy_primitives::hex::encode(signature);
154        Ok(format!("0x{signature}"))
155    }
156
157    /// Handler for: `eth_simulateV1`
158    async fn simulate_v1(
159        &self,
160        payload: SimulatePayload<SeismicCallRequest>,
161        block_number: Option<BlockId>,
162    ) -> RpcResult<Vec<SimulatedBlock<RpcBlock<Eth::NetworkTypes>>>> {
163        trace!(target: "rpc::eth", "Serving eth_simulateV1");
164
165        let mut simulated_blocks = Vec::with_capacity(payload.block_state_calls.len());
166
167        for block in payload.block_state_calls {
168            let SimBlock { block_overrides, state_overrides, calls } = block;
169            let mut prepared_calls: Vec<TransactionRequest> = Vec::with_capacity(calls.len());
170
171            for call in calls {
172                let tx_request = match call {
173                    alloy_rpc_types::SeismicCallRequest::TransactionRequest(_tx_request) => {
174                        return Err(EthApiError::InvalidParams(
175                            "Invalid Transaction Request".to_string(),
176                        )
177                        .into())
178                    }
179
180                    alloy_rpc_types::SeismicCallRequest::TypedData(typed_request) => {
181                        let tx =
182                            recover_typed_data_request::<PoolPooledTx<Eth::Pool>>(&typed_request)?
183                                .map_transaction(
184                                <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus,
185                            );
186
187                        TransactionRequest::from_transaction_with_sender(
188                            tx.as_signed().clone(),
189                            tx.signer(),
190                        )
191                    }
192
193                    alloy_rpc_types::SeismicCallRequest::Bytes(bytes) => {
194                        let tx = recover_raw_transaction::<PoolPooledTx<Eth::Pool>>(&bytes)?
195                            .map_transaction(
196                                <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus,
197                            );
198
199                        TransactionRequest::from_transaction_with_sender(
200                            tx.as_signed().clone(),
201                            tx.signer(),
202                        )
203                    }
204                };
205                prepared_calls.push(tx_request);
206            }
207
208            let prepared_block =
209                SimBlock { block_overrides, state_overrides, calls: prepared_calls };
210
211            simulated_blocks.push(prepared_block);
212        }
213
214        let result = EthCall::simulate_v1(
215            &self.eth_api,
216            SimulatePayload {
217                block_state_calls: simulated_blocks.clone(),
218                trace_transfers: payload.trace_transfers,
219                validation: payload.validation,
220                return_full_transactions: payload.return_full_transactions,
221            },
222            block_number,
223        )
224        .await;
225        let mut result = result.unwrap();
226
227        for (block, result) in simulated_blocks.iter().zip(result.iter_mut()) {
228            let SimBlock { calls, .. } = block;
229            let SimulatedBlock { calls: call_results, .. } = result;
230
231            for (call_result, call) in call_results.iter_mut().zip(calls.iter()) {
232                call.seismic_elements.map(|seismic_elements| {
233                    let encrypted_output = self
234                        .eth_api
235                        .evm_config()
236                        .encrypt(&call_result.return_data, &seismic_elements)
237                        .map(|encrypted_output| Bytes::from(encrypted_output))
238                        .unwrap();
239                    call_result.return_data = encrypted_output;
240                });
241            }
242        }
243
244        Ok(result)
245    }
246
247    /// Handler for: `eth_call`
248    async fn call(
249        &self,
250        request: alloy_rpc_types::SeismicCallRequest,
251        block_number: Option<BlockId>,
252        state_overrides: Option<StateOverride>,
253        block_overrides: Option<Box<BlockOverrides>>,
254    ) -> RpcResult<Bytes> {
255        trace!(target: "rpc::eth", ?request, ?block_number, ?state_overrides, ?block_overrides, "Serving overridden eth_call");
256        let tx_request = match request {
257            alloy_rpc_types::SeismicCallRequest::TransactionRequest(tx_request) => {
258                let mut request = tx_request.inner;
259                seismic_override_call_request(&mut request);
260                request
261            }
262
263            alloy_rpc_types::SeismicCallRequest::TypedData(typed_request) => {
264                let tx = recover_typed_data_request::<PoolPooledTx<Eth::Pool>>(&typed_request)?
265                    .map_transaction(
266                        <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus,
267                    );
268
269                TransactionRequest::from_transaction_with_sender(
270                    tx.as_signed().clone(),
271                    tx.signer(),
272                )
273            }
274
275            alloy_rpc_types::SeismicCallRequest::Bytes(bytes) => {
276                let tx = recover_raw_transaction::<PoolPooledTx<Eth::Pool>>(&bytes)?
277                    .map_transaction(
278                        <Eth::Pool as TransactionPool>::Transaction::pooled_into_consensus,
279                    );
280
281                TransactionRequest::from_transaction_with_sender(
282                    tx.as_signed().clone(),
283                    tx.signer(),
284                )
285            }
286        };
287
288        let seismic_elements = tx_request.seismic_elements;
289
290        let result = EthCall::call(
291            &self.eth_api,
292            tx_request,
293            block_number,
294            EvmOverrides::new(state_overrides, block_overrides),
295        )
296        .await?;
297
298        if let Some(seismic_elements) = seismic_elements {
299            let encrypted_output = self
300                .eth_api
301                .evm_config()
302                .encrypt(&result, &seismic_elements)
303                .map(|encrypted_output| Bytes::from(encrypted_output))
304                .unwrap();
305            Ok(encrypted_output)
306        } else {
307            Ok(result)
308        }
309    }
310
311    /// Handler for: `eth_sendRawTransaction`
312    async fn send_raw_transaction(&self, tx: SeismicRawTxRequest) -> RpcResult<B256> {
313        trace!(target: "rpc::eth", ?tx, "Serving overridden eth_sendRawTransaction");
314        match tx {
315            SeismicRawTxRequest::Bytes(bytes) => {
316                Ok(EthTransactions::send_raw_transaction(&self.eth_api, bytes).await?)
317            }
318            SeismicRawTxRequest::TypedData(typed_data) => {
319                Ok(EthTransactions::send_typed_data_transaction(&self.eth_api, typed_data).await?)
320            }
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use crate::utils::test_utils::{build_test_eth_api, launch_http};
328    use alloy_eips::eip712::TypedDataRequest;
329    use alloy_primitives::{b256, hex, PrimitiveSignature};
330    use alloy_rpc_types::Block;
331    use jsonrpsee::core::client::{ClientT, SubscriptionClientT};
332    use reth_enclave::start_mock_enclave_server_random_port;
333    use reth_provider::test_utils::MockEthProvider;
334    use seismic_node::utils::test_utils::get_seismic_tx;
335
336    use super::*;
337
338    async fn test_basic_seismic_calls<C>(client: &C)
339    where
340        C: ClientT + SubscriptionClientT + Sync,
341    {
342        let _pk = SeismicApiClient::get_tee_public_key(client).await.unwrap();
343    }
344
345    async fn test_basic_eth_calls<C>(client: &C)
346    where
347        C: ClientT + SubscriptionClientT + Sync,
348    {
349        let typed_data = get_seismic_tx().eip712_to_type_data();
350        let typed_data_request = TypedDataRequest {
351            data: typed_data.clone(),
352            signature: PrimitiveSignature::new(
353                b256!("1fd474b1f9404c0c5df43b7620119ffbc3a1c3f942c73b6e14e9f55255ed9b1d").into(),
354                b256!("29aca24813279a901ec13b5f7bb53385fa1fc627b946592221417ff74a49600d").into(),
355                false,
356            ),
357        };
358        let tx = Bytes::from(hex!("02f871018303579880850555633d1b82520894eee27662c2b8eba3cd936a23f039f3189633e4c887ad591c62bdaeb180c080a07ea72c68abfb8fca1bd964f0f99132ed9280261bdca3e549546c0205e800f7d0a05b4ef3039e9c9b9babc179a1878fb825b5aaf5aed2fa8744854150157b08d6f3"));
359        let call_request = TransactionRequest::default();
360
361        let _signature = EthApiOverrideClient::<Block>::sign_typed_data_v4(
362            client,
363            Address::ZERO,
364            typed_data.clone(),
365        )
366        .await
367        .unwrap_err();
368        let _result = EthApiOverrideClient::<Block>::call(
369            client,
370            call_request.clone().into(),
371            None,
372            None,
373            None,
374        )
375        .await
376        .unwrap_err();
377        let _result =
378            EthApiOverrideClient::<Block>::call(client, tx.clone().into(), None, None, None)
379                .await
380                .unwrap_err();
381        let _result = EthApiOverrideClient::<Block>::call(
382            client,
383            typed_data_request.clone().into(),
384            None,
385            None,
386            None,
387        )
388        .await
389        .unwrap_err();
390        let _result =
391            EthApiOverrideClient::<Block>::send_raw_transaction(client, tx.clone().into())
392                .await
393                .unwrap();
394    }
395
396    #[tokio::test(flavor = "multi_thread")]
397    async fn test_call_seismic_functions_http() {
398        reth_tracing::init_test_tracing();
399        let enclave_client = start_mock_enclave_server_random_port().await;
400
401        let seismic_api = SeismicApi::default().with_enclave_client(enclave_client);
402
403        let handle = launch_http(seismic_api.into_rpc()).await;
404        let client = handle.http_client().unwrap();
405        test_basic_seismic_calls(&client).await;
406    }
407
408    #[tokio::test(flavor = "multi_thread")]
409    async fn test_call_eth_functions_http() {
410        reth_tracing::init_test_tracing();
411
412        let eth_api = build_test_eth_api(MockEthProvider::default());
413        let eth_api = EthApiExt::new(eth_api);
414        let handle = launch_http(eth_api.into_rpc()).await;
415        test_basic_eth_calls(&handle.http_client().unwrap()).await;
416    }
417}