diff --git a/README.md b/README.md index aed9a05..8e4f0bf 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ You can configure the proxy server via an optional `config.toml` file or command ```toml bind_address = "0.0.0.0:8080" -upstream_url = "http://validium-sequencer:8545" +validium_url = "http://validium-sequencer:8545" ``` ### Override with CLI ```sh -cargo run -- --bind-address 127.0.0.1:9000 --upstream-url http://localhost:8545 +cargo run -- --bind-address 127.0.0.1:9000 --validium-url http://localhost:8545 ``` ### Config file path @@ -38,7 +38,7 @@ By default, the server uses `config.toml` in the current directory. You can use ### Defaults -If not specified, `bind_address` defaults to `0.0.0.0:8080`, `upstream_url` defaults to `http://validium-sequencer:8545`. +If not specified, `bind_address` defaults to `0.0.0.0:8080`, `validium_url` defaults to `http://validium-sequencer:8545`. ### Precedence diff --git a/config.toml b/config.toml index 9268db0..9115da1 100644 --- a/config.toml +++ b/config.toml @@ -1,8 +1,11 @@ # The address for the server to bind to bind_address = "0.0.0.0:1234" -# The upstream HTTP endpoint (e.g. for forwarding requests) -upstream_url = "https://rpc.scroll.io" +# The validium RPC endpoint +validium_url = "https://rpc.scroll.io" + +# The withdraw-proofs RPC endpoint +withdraw_proofs_url = "http://withdraw-proofs-service:3000" # List of admin API tokens. Only users with these tokens can access admin functions. admin_keys = [ diff --git a/src/config.rs b/src/config.rs index 9285df3..08a2dd9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,9 +12,13 @@ struct CliArgs { #[arg(long)] bind_address: Option, - /// Upstream RPC endpoint to relay proxy requests to, e.g. http://validium-sequencer:8545 + /// Validium RPC endpoint to relay proxy requests to, e.g. http://validium-sequencer:8545 #[arg(long)] - upstream_url: Option, + validium_url: Option, + + /// Withdraw-proofs RPC endpoint to relay proxy requests to, e.g. http://cloak-withdraw-proofs:8545 + #[arg(long)] + withdraw_proofs_url: Option, } /// Structure of the config file @@ -22,8 +26,9 @@ struct CliArgs { pub struct AppConfig { #[serde(default = "default_bind_address")] pub bind_address: String, - #[serde(default = "default_upstream_url")] - pub upstream_url: String, + #[serde(default = "default_validium_url")] + pub validium_url: String, + pub withdraw_proofs_url: String, pub admin_keys: Vec, pub jwt_expiry_secs: usize, pub default_kid: String, @@ -35,8 +40,8 @@ fn default_bind_address() -> String { "0.0.0.0:8080".to_owned() } -/// Default upstream URL if not specified anywhere -fn default_upstream_url() -> String { +/// Default validium URL if not specified anywhere +fn default_validium_url() -> String { "http://validium-sequencer:8545".to_owned() } @@ -53,8 +58,11 @@ pub fn load_config() -> anyhow::Result { if let Some(val) = args.bind_address { cfg.bind_address = val; } - if let Some(val) = args.upstream_url { - cfg.upstream_url = val; + if let Some(val) = args.validium_url { + cfg.validium_url = val; + } + if let Some(val) = args.withdraw_proofs_url { + cfg.withdraw_proofs_url = val; } // Validate bind_address format @@ -67,11 +75,21 @@ pub fn load_config() -> anyhow::Result { ) })?; - // Validate upstream_url format - if !cfg.upstream_url.starts_with("http://") && !cfg.upstream_url.starts_with("https://") { + // Validate validium_url format + if !cfg.validium_url.starts_with("http://") && !cfg.validium_url.starts_with("https://") { + anyhow::bail!( + "Invalid validium_url: {}. Must start with http:// or https://", + cfg.validium_url + ); + } + + // Validate withdraw_proofs_url format + if !cfg.withdraw_proofs_url.starts_with("http://") + && !cfg.withdraw_proofs_url.starts_with("https://") + { anyhow::bail!( - "Invalid upstream_url: {}. Must start with http:// or https://", - cfg.upstream_url + "Invalid withdraw_proofs_url: {}. Must start with http:// or https://", + cfg.withdraw_proofs_url ); } @@ -88,7 +106,8 @@ mod tests { // Example TOML configuration string let toml = r#" bind_address = "127.0.0.1:12345" - upstream_url = "http://example.com:8545" + validium_url = "http://example.com:8545" + withdraw_proofs_url = "http://example.com:8546" admin_keys = [ "admin-token-1-abcdefg", "admin-token-2-hijklmn" @@ -112,7 +131,8 @@ mod tests { // Check values assert_eq!(cfg.bind_address, "127.0.0.1:12345"); - assert_eq!(cfg.upstream_url, "http://example.com:8545"); + assert_eq!(cfg.validium_url, "http://example.com:8545"); + assert_eq!(cfg.withdraw_proofs_url, "http://example.com:8546"); assert_eq!( cfg.admin_keys, vec![ diff --git a/src/main.rs b/src/main.rs index 9e8b0a9..1fa9cb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,11 +25,12 @@ use service::{RpcLoggerMiddleware, log_request}; fn all_apis( jwt: JwtSigner, jwt_expiry_secs: usize, - upstream_url: &str, + validium_url: &str, + withdraw_proofs_url: &str, ) -> anyhow::Result> { let auth_server = SiweAuthRpcImpl::new(jwt, jwt_expiry_secs); - let eth_proxy_server = RpcProxyImpl::new(upstream_url)?; - let scroll_proxy_server = RpcProxyImpl::new(upstream_url)?; + let eth_proxy_server = RpcProxyImpl::new(validium_url, withdraw_proofs_url)?; + let scroll_proxy_server = RpcProxyImpl::new(validium_url, withdraw_proofs_url)?; let mut module = RpcModule::new(()); module.merge(SiweAuthRpcServer::into_rpc(auth_server))?; @@ -70,9 +71,15 @@ pub async fn run_server() -> anyhow::Result { let addr = server.local_addr()?; info!("Server is listening on {addr}"); - info!("Upstream endpoint is {}", cfg.upstream_url); + info!("Validium endpoint is {}", cfg.validium_url); + info!("Withdraw proofs endpoint is {}", cfg.withdraw_proofs_url); - let handle = server.start(all_apis(jwt, cfg.jwt_expiry_secs, &cfg.upstream_url)?); + let handle = server.start(all_apis( + jwt, + cfg.jwt_expiry_secs, + &cfg.validium_url, + &cfg.withdraw_proofs_url, + )?); handle.stopped().await; Ok(addr) } diff --git a/src/proxy/error.rs b/src/proxy/error.rs index 0258b32..852e4ca 100644 --- a/src/proxy/error.rs +++ b/src/proxy/error.rs @@ -1,9 +1,13 @@ use jsonrpsee::core::ClientError; use jsonrpsee::types::ErrorObjectOwned; -use jsonrpsee::types::error::{INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG}; +use jsonrpsee::types::error::{INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG, INVALID_REQUEST_CODE}; pub fn unauthorized() -> ErrorObjectOwned { - ErrorObjectOwned::owned(INTERNAL_ERROR_CODE, "unauthorized", Some("unauthorized")) + ErrorObjectOwned::owned(INVALID_REQUEST_CODE, "unauthorized", Some("unauthorized")) +} + +pub fn internal_error(msg: impl AsRef) -> ErrorObjectOwned { + ErrorObjectOwned::owned(INTERNAL_ERROR_CODE, "internal_error", Some(msg.as_ref())) } pub fn proxy_call_failed(e: ClientError) -> ErrorObjectOwned { diff --git a/src/proxy/interface.rs b/src/proxy/interface.rs index ff3a651..d0bc6d2 100644 --- a/src/proxy/interface.rs +++ b/src/proxy/interface.rs @@ -8,9 +8,23 @@ use alloy_rpc_types::{ }; use jsonrpsee::core::RpcResult; use scroll_alloy_rpc_types::{ScrollTransactionReceipt as Receipt, Transaction}; +use serde::{Deserialize, Serialize}; pub type Block = EthBlock; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Withdrawal { + pub tx_hash: B256, + pub message_hash: B256, + pub from: Address, + pub to: Address, + pub value: U256, + pub nonce: u64, + pub message: Bytes, + pub batch_index: u64, + pub proof: Bytes, +} + #[rpc(server, client, namespace = "scroll")] pub trait ScrollRpcProxy { #[method(name = "getL1MessagesInBlock", with_extensions)] @@ -19,6 +33,13 @@ pub trait ScrollRpcProxy { block_id: String, mode: String, ) -> RpcResult>>; + + #[method(name = "withdrawalsByTransaction", with_extensions)] + async fn withdrawals_by_transaction(&self, tx_hash: B256) -> RpcResult>; + + #[method(name = "withdrawalByMessageHash", with_extensions)] + async fn withdrawal_by_message_hash(&self, message_hash: B256) + -> RpcResult>; } // see https://github.com/paradigmxyz/reth/blob/main/crates/rpc/rpc-eth-api/src/core.rs diff --git a/src/proxy/server.rs b/src/proxy/server.rs index ce7ccb0..325566b 100644 --- a/src/proxy/server.rs +++ b/src/proxy/server.rs @@ -13,9 +13,10 @@ use jsonrpsee::http_client::HttpClient; use reth_primitives::TransactionSigned; use scroll_alloy_rpc_types::{ScrollTransactionReceipt as Receipt, Transaction}; -use super::error::{proxy_call_failed, unauthorized}; +use super::error::{internal_error, proxy_call_failed, unauthorized}; use super::interface::{ Block, EthRpcProxyClient, EthRpcProxyServer, ScrollRpcProxyClient, ScrollRpcProxyServer, + Withdrawal, }; use crate::auth::AccessLevel; @@ -44,13 +45,21 @@ fn only_full_access(ext: &Extensions) -> RpcResult<()> { } pub struct RpcProxyImpl { - client: HttpClient, + validium_client: HttpClient, + withdraw_proofs_client: HttpClient, } impl RpcProxyImpl { - pub fn new(target: impl AsRef) -> anyhow::Result { - let client = HttpClient::builder().build(target)?; - Ok(Self { client }) + pub fn new( + validium_url: impl AsRef, + withdraw_proofs_url: impl AsRef, + ) -> anyhow::Result { + let validium_client = HttpClient::builder().build(validium_url)?; + let withdraw_proofs_client = HttpClient::builder().build(withdraw_proofs_url)?; + Ok(Self { + validium_client, + withdraw_proofs_client, + }) } } @@ -63,20 +72,100 @@ impl ScrollRpcProxyServer for RpcProxyImpl { mode: String, ) -> RpcResult>> { only_full_access(ext)?; - ScrollRpcProxyClient::l1_messages_in_block(&self.client, block_id, mode) + ScrollRpcProxyClient::l1_messages_in_block(&self.validium_client, block_id, mode) .await .map_err(proxy_call_failed) } + + async fn withdrawals_by_transaction( + &self, + ext: &Extensions, + tx_hash: B256, + ) -> RpcResult> { + let access = get_access(ext); + + // pre-check before proxy call + if access == &AccessLevel::None { + return Err(unauthorized()); + } + + // proxy call + let ws = + ScrollRpcProxyClient::withdrawals_by_transaction(&self.withdraw_proofs_client, tx_hash) + .await + .map_err(proxy_call_failed)?; + + if ws.is_empty() || access == &AccessLevel::Full { + return Ok(ws); + } + + // only allow sender to query the transaction + let maybe_tx = proxy_call!(self.validium_client, transaction_by_hash, tx_hash)?; + + let tx = match maybe_tx { + // If we found the withdrawal, the transaction must exist + None => return Err(internal_error("transaction not found")), + Some(tx) => tx, + }; + + if access.is_authorized(&tx.as_recovered().signer()) { + return Ok(ws); + } + + Err(unauthorized()) + } + + async fn withdrawal_by_message_hash( + &self, + ext: &Extensions, + message_hash: B256, + ) -> RpcResult> { + let access = get_access(ext); + + // pre-check before proxy call + if access == &AccessLevel::None { + return Err(unauthorized()); + } + + // proxy call + let maybe_w = ScrollRpcProxyClient::withdrawal_by_message_hash( + &self.withdraw_proofs_client, + message_hash, + ) + .await + .map_err(proxy_call_failed)?; + + if maybe_w.is_none() || access == &AccessLevel::Full { + return Ok(maybe_w); + } + + let w = maybe_w.expect("checked above"); + + // only allow sender to query the transaction + let maybe_tx = proxy_call!(self.validium_client, transaction_by_hash, w.tx_hash)?; + + let tx = match maybe_tx { + // If we found the withdrawal, the transaction must exist + None => return Err(internal_error("transaction not found")), + Some(tx) => tx, + }; + + if access.is_authorized(&tx.as_recovered().signer()) { + return Ok(Some(w)); + } + + Err(unauthorized()) + } } #[async_trait] impl EthRpcProxyServer for RpcProxyImpl { async fn block_number(&self, _ext: &Extensions) -> RpcResult { - proxy_call!(self.client, block_number) + proxy_call!(self.validium_client, block_number) } async fn chain_id(&self, _ext: &Extensions) -> RpcResult> { - proxy_call!(self.client, chain_id) + proxy_call!(self.validium_client, chain_id) } async fn block_by_hash( @@ -86,7 +175,7 @@ impl EthRpcProxyServer for RpcProxyImpl { full: bool, ) -> RpcResult> { only_full_access(ext)?; - proxy_call!(self.client, block_by_hash, hash, full) + proxy_call!(self.validium_client, block_by_hash, hash, full) } async fn block_by_number( @@ -96,7 +185,7 @@ impl EthRpcProxyServer for RpcProxyImpl { full: bool, ) -> RpcResult> { only_full_access(ext)?; - proxy_call!(self.client, block_by_number, number, full) + proxy_call!(self.validium_client, block_by_number, number, full) } async fn balance( @@ -106,7 +195,7 @@ impl EthRpcProxyServer for RpcProxyImpl { block_number: Option, ) -> RpcResult { only_authorized(ext, &address)?; - proxy_call!(self.client, balance, address, block_number) + proxy_call!(self.validium_client, balance, address, block_number) } async fn storage_at( @@ -117,7 +206,13 @@ impl EthRpcProxyServer for RpcProxyImpl { block_number: Option, ) -> RpcResult { only_full_access(ext)?; - proxy_call!(self.client, storage_at, address, index, block_number) + proxy_call!( + self.validium_client, + storage_at, + address, + index, + block_number + ) } async fn transaction_by_hash( @@ -133,7 +228,7 @@ impl EthRpcProxyServer for RpcProxyImpl { } // proxy call - let maybe_tx = proxy_call!(self.client, transaction_by_hash, hash)?; + let maybe_tx = proxy_call!(self.validium_client, transaction_by_hash, hash)?; let tx = match maybe_tx { None => return Ok(None), @@ -166,7 +261,7 @@ impl EthRpcProxyServer for RpcProxyImpl { } // proxy call - let maybe_receipt = proxy_call!(self.client, transaction_receipt, hash)?; + let maybe_receipt = proxy_call!(self.validium_client, transaction_receipt, hash)?; let receipt = match maybe_receipt { None => return Ok(None), @@ -193,7 +288,12 @@ impl EthRpcProxyServer for RpcProxyImpl { block_number: Option, ) -> RpcResult { only_authorized(ext, &address)?; - proxy_call!(self.client, transaction_count, address, block_number) + proxy_call!( + self.validium_client, + transaction_count, + address, + block_number + ) } async fn get_code( @@ -203,7 +303,7 @@ impl EthRpcProxyServer for RpcProxyImpl { block_number: Option, ) -> RpcResult { only_full_access(ext)?; - proxy_call!(self.client, get_code, address, block_number) + proxy_call!(self.validium_client, get_code, address, block_number) } async fn call( @@ -213,7 +313,7 @@ impl EthRpcProxyServer for RpcProxyImpl { block_number: Option, ) -> RpcResult { only_full_access(ext)?; - proxy_call!(self.client, call, request, block_number) + proxy_call!(self.validium_client, call, request, block_number) } async fn estimate_gas( @@ -223,7 +323,7 @@ impl EthRpcProxyServer for RpcProxyImpl { block_number: Option, ) -> RpcResult { only_full_access(ext)?; - proxy_call!(self.client, estimate_gas, request, block_number) + proxy_call!(self.validium_client, estimate_gas, request, block_number) } async fn gas_price(&self, _ext: &Extensions) -> RpcResult { @@ -242,7 +342,7 @@ impl EthRpcProxyServer for RpcProxyImpl { reward_percentiles: Option>, ) -> RpcResult { proxy_call!( - self.client, + self.validium_client, fee_history, block_count, newest_block, @@ -281,11 +381,11 @@ impl EthRpcProxyServer for RpcProxyImpl { } } - proxy_call!(self.client, send_raw_transaction, bytes) + proxy_call!(self.validium_client, send_raw_transaction, bytes) } async fn logs(&self, ext: &Extensions, filter: Filter) -> RpcResult> { only_full_access(ext)?; - proxy_call!(self.client, logs, filter) + proxy_call!(self.validium_client, logs, filter) } }