edb_engine/rpc/
server.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! JSON-RPC server implementation for EDB debugging API.
18//!
19//! This module implements the main HTTP server that handles JSON-RPC debugging requests
20//! using the Axum web framework. The server provides a thread-safe, multi-client debugging
21//! interface with graceful shutdown capabilities.
22//!
23//! # Features
24//!
25//! - **Multi-threaded**: Handles concurrent debugging requests from multiple clients
26//! - **Thread-safe**: Uses Arc-wrapped EngineContext for safe shared access
27//! - **Graceful shutdown**: Supports clean server termination
28//! - **Health monitoring**: Provides health check endpoint for monitoring
29//! - **Error handling**: Comprehensive error reporting with JSON-RPC 2.0 compliance
30//!
31//! # Server Architecture
32//!
33//! The server follows a standard Axum pattern:
34//! 1. Creates a router with RPC and health check endpoints
35//! 2. Spawns the server in a background task
36//! 3. Returns a handle for monitoring and shutdown
37//! 4. Routes requests to the appropriate method handlers
38//!
39//! # Endpoints
40//!
41//! - `POST /` - Main JSON-RPC endpoint for debugging methods
42//! - `GET /health` - Health check endpoint returning server status
43
44use super::methods::MethodHandler;
45use super::types::{RpcError, RpcRequest, RpcResponse};
46use super::utils::get_default_rpc_port;
47use crate::EngineContext;
48use axum::{
49    extract::{Json as JsonExtract, State},
50    response::Json as JsonResponse,
51    routing::{get, post},
52    Router,
53};
54use eyre::Result;
55use revm::database::CacheDB;
56use revm::{Database, DatabaseCommit, DatabaseRef};
57use std::net::SocketAddr;
58use std::sync::Arc;
59use tokio::sync::oneshot;
60use tracing::{error, info, warn};
61
62/// Handle to control a running RPC server.
63///
64/// This handle provides access to server information and allows for graceful shutdown.
65/// The server runs in a background task and can be monitored and controlled through this handle.
66#[derive(Debug)]
67pub struct RpcServerHandle {
68    /// Address the server is listening on
69    pub addr: SocketAddr,
70    /// Shutdown signal sender (consumed when shutting down)
71    shutdown_tx: oneshot::Sender<()>,
72}
73
74impl RpcServerHandle {
75    /// Get the server address
76    pub fn addr(&self) -> SocketAddr {
77        self.addr
78    }
79
80    /// Get the port number
81    pub fn port(&self) -> u16 {
82        self.addr.port()
83    }
84
85    /// Gracefully shutdown the RPC server
86    pub fn shutdown(self) -> Result<()> {
87        if self.shutdown_tx.send(()).is_err() {
88            warn!("RPC server already shut down");
89        }
90        Ok(())
91    }
92}
93
94/// Thread-safe RPC state for Axum request handling.
95///
96/// This wrapper provides the shared state needed by Axum handlers.
97/// It contains the server instance wrapped in Arc for thread-safe sharing.
98#[derive(Clone)]
99struct RpcState<DB>
100where
101    DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
102    <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
103    <DB as Database>::Error: Clone + Send + Sync,
104{
105    /// The debug RPC server instance (shared across request handlers)
106    server: Arc<DebugRpcServer<DB>>,
107}
108
109/// Main debug RPC server providing JSON-RPC debugging API.
110///
111/// This server provides read-only access to the debugging engine context through
112/// a JSON-RPC interface. It handles method dispatch, error handling, and response
113/// formatting according to the JSON-RPC 2.0 specification.
114///
115/// The server is thread-safe and can handle concurrent requests from multiple clients.
116/// All access to the engine context is read-only to ensure debugging session integrity.
117pub struct DebugRpcServer<DB>
118where
119    DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
120    <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
121    <DB as Database>::Error: Clone + Send + Sync,
122{
123    /// Immutable debugging context providing read-only access to debugging data
124    context: Arc<EngineContext<DB>>,
125    /// Method handler for dispatching RPC requests to appropriate implementations
126    method_handler: Arc<MethodHandler<DB>>,
127}
128
129impl<DB> DebugRpcServer<DB>
130where
131    DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
132    <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
133    <DB as Database>::Error: Clone + Send + Sync,
134{
135    /// Create a new debug RPC server
136    pub fn new(context: EngineContext<DB>) -> Self {
137        let context = Arc::new(context);
138        let method_handler = Arc::new(MethodHandler::new(context.clone()));
139
140        Self { context, method_handler }
141    }
142
143    /// Start the RPC server
144    pub async fn start(self) -> Result<RpcServerHandle> {
145        let port = get_default_rpc_port()?;
146        self.start_on_port(port).await
147    }
148
149    /// Start the RPC server on a specific port using standard multi-threaded pattern
150    ///
151    /// This method creates the Axum server with Send+Sync state, leveraging
152    /// the now thread-safe EngineContext.
153    pub async fn start_on_port(self, port: u16) -> Result<RpcServerHandle> {
154        // Create the Axum app with the server as state
155        let app = Router::new()
156            .route("/", post(handle_rpc_request))
157            .route("/health", get(health_check))
158            .with_state(RpcState { server: Arc::new(self) });
159
160        let addr = SocketAddr::from(([127, 0, 0, 1], port));
161        let listener = tokio::net::TcpListener::bind(addr).await?;
162        let actual_addr = listener.local_addr()?;
163
164        let (shutdown_tx, shutdown_rx) = oneshot::channel();
165
166        // Spawn the Axum server
167        tokio::spawn(async move {
168            axum::serve(listener, app)
169                .with_graceful_shutdown(async {
170                    shutdown_rx.await.ok();
171                })
172                .await
173                .expect("RPC server failed");
174        });
175
176        info!("Debug RPC server started on {}", actual_addr);
177
178        Ok(RpcServerHandle { addr: actual_addr, shutdown_tx })
179    }
180
181    /// Handle an RPC request (called from worker thread)
182    async fn handle_request(&self, request: RpcRequest) -> RpcResponse {
183        let id = request.id.clone();
184
185        // Dispatch to method handler
186        match self.method_handler.handle_method(&request.method, request.params).await {
187            Ok(result) => {
188                RpcResponse { jsonrpc: "2.0".to_string(), result: Some(result), error: None, id }
189            }
190            Err(err) => {
191                error!(target: "rpc", "Error handling RPC request: {:?}", err);
192                RpcResponse { jsonrpc: "2.0".to_string(), result: None, error: Some(err), id }
193            }
194        }
195    }
196
197    /// Get total snapshot count (stateless)
198    pub fn snapshot_count(&self) -> usize {
199        self.context.snapshots.len()
200    }
201
202    /// Validate snapshot index (stateless helper)
203    pub fn validate_snapshot_index(&self, index: usize) -> Result<()> {
204        if index >= self.snapshot_count() {
205            return Err(eyre::eyre!(
206                "Snapshot index {} out of bounds (max: {})",
207                index,
208                self.snapshot_count() - 1
209            ));
210        }
211        Ok(())
212    }
213
214    /// Get read-only access to the engine context
215    pub fn context(&self) -> &Arc<EngineContext<DB>> {
216        &self.context
217    }
218}
219
220/// Handle RPC requests directly with the thread-safe server
221async fn handle_rpc_request<DB>(
222    State(state): State<RpcState<DB>>,
223    JsonExtract(request): JsonExtract<RpcRequest>,
224) -> JsonResponse<RpcResponse>
225where
226    DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
227    <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
228    <DB as Database>::Error: Clone + Send + Sync,
229{
230    // Validate JSON-RPC version
231    if request.jsonrpc != "2.0" {
232        return JsonResponse(RpcResponse {
233            jsonrpc: "2.0".to_string(),
234            result: None,
235            error: Some(RpcError {
236                code: -32600,
237                message: "Invalid Request - JSON-RPC version must be 2.0".to_string(),
238                data: None,
239            }),
240            id: request.id.clone(),
241        });
242    }
243
244    // Handle request directly (no channel proxy needed)
245    let response = state.server.handle_request(request).await;
246    JsonResponse(response)
247}
248
249/// Health check endpoint
250async fn health_check() -> JsonResponse<serde_json::Value> {
251    JsonResponse(serde_json::json!({
252        "status": "healthy",
253        "service": "edb-debug-rpc-server",
254        "version": env!("CARGO_PKG_VERSION"),
255        "architecture": "multi-threaded"
256    }))
257}
258
259/// Create and start a debug RPC server with auto-port detection
260pub async fn start_debug_server<DB>(context: EngineContext<DB>) -> Result<RpcServerHandle>
261where
262    DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
263    <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
264    <DB as Database>::Error: Clone + Send + Sync,
265{
266    let server = DebugRpcServer::new(context);
267    server.start().await
268}