Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
563 changes: 321 additions & 242 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ Helios will now run a local RPC server at `http://127.0.0.1:8545`.

`--rpc-bind-ip` or `-b` sets the ip that binds to the JSON-RPC server. By default, Helios will use `127.0.0.1`. Use `0.0.0.0` to allow remote access.

`--allowed-origins` specifies a comma-separated list of origins allowed to make CORS requests to the RPC server. Useful when exposing the RPC to web dApps, e.g. `--allowed-origins https://my-dapp.com,https://localhost:3000`.

`--data-dir` or `-d` sets the directory that Helios should use to store cached weak subjectivity checkpoints in. Each network only stores the latest checkpoint, which is just 32 bytes.

`--fallback` or `-f` sets the checkpoint fallback url (a string). This is only used if the checkpoint provided by the `--checkpoint` flag is too outdated for Helios to use to sync.
Expand Down Expand Up @@ -144,6 +146,11 @@ helios ethereum \
--checkpoint 0xe1912ca8ca3b45dac497cae7825bab055b0f60285533721b046e8fefb5b076f2
```

To expose the RPC to a web dApp, add `--rpc-bind-ip 0.0.0.0` and specify allowed CORS origins:
```bash
helios ethereum --execution-rpc $ETH_RPC_URL --rpc-bind-ip 0.0.0.0 --allowed-origins "*"
```

If you wish to use a [Configuration File](#configuration-files) instead of CLI arguments then you should replace the example checkpoints in the configuration file with the latest checkpoints obtained above.

## Testing
Expand Down
27 changes: 27 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@
rpc_bind_ip: Option<IpAddr>,
#[arg(short = 'p', long, env)]
rpc_port: Option<u16>,
#[arg(
long,
env,
value_delimiter = ',',
help = "Comma-separated list of allowed CORS origins"
)]
allowed_origins: Option<Vec<String>>,
#[arg(short = 'w', long, env)]
checkpoint: Option<B256>,
#[arg(short, long, env, value_parser = parse_url)]
Expand Down Expand Up @@ -204,6 +211,7 @@
.map(|s| PathBuf::from_str(s).expect("cannot find data dir")),
rpc_bind_ip: self.rpc_bind_ip,
rpc_port: self.rpc_port,
allowed_origins: self.allowed_origins.clone(),
fallback: self.fallback.clone(),
load_external_fallback: true_or_none(self.load_external_fallback),
strict_checkpoint_age: true_or_none(self.strict_checkpoint_age),
Expand All @@ -219,6 +227,13 @@
rpc_bind_ip: Option<IpAddr>,
#[arg(short = 'p', long, env, default_value = "8545")]
rpc_port: Option<u16>,
#[arg(
long,
env,
value_delimiter = ',',
help = "Comma-separated list of allowed CORS origins"
)]
allowed_origins: Option<Vec<String>>,
#[arg(short, long, env, value_parser = parse_url)]
execution_rpc: Option<Url>,
#[arg(long, env, value_parser = parse_url)]
Expand Down Expand Up @@ -272,10 +287,14 @@
}

if self.rpc_bind_ip.is_some() && self.rpc_port.is_some() {
let rpc_socket = SocketAddr::new(self.rpc_bind_ip.unwrap(), self.rpc_port.unwrap());

Check failure on line 290 in cli/src/main.rs

View workflow job for this annotation

GitHub Actions / clippy

called `unwrap` on `self.rpc_port` after checking its variant with `is_some`

Check failure on line 290 in cli/src/main.rs

View workflow job for this annotation

GitHub Actions / clippy

called `unwrap` on `self.rpc_bind_ip` after checking its variant with `is_some`
user_dict.insert("rpc_socket", Value::from(rpc_socket.to_string()));
}

if let Some(allowed_origins) = &self.allowed_origins {
user_dict.insert("allowed_origins", Value::from(allowed_origins.clone()));
}

if let Some(ip) = self.rpc_bind_ip {
user_dict.insert("rpc_bind_ip", Value::from(ip.to_string()));
}
Expand Down Expand Up @@ -304,6 +323,13 @@
rpc_bind_ip: Option<IpAddr>,
#[arg(short = 'p', long, env)]
rpc_port: Option<u16>,
#[arg(
long,
env,
value_delimiter = ',',
help = "Comma-separated list of allowed CORS origins"
)]
allowed_origins: Option<Vec<String>>,
#[arg(short, long, env, value_parser = parse_url)]
execution_rpc: Option<Url>,
}
Expand All @@ -328,6 +354,7 @@
execution_rpc: self.execution_rpc.clone(),
rpc_bind_ip: self.rpc_bind_ip,
rpc_port: self.rpc_port,
allowed_origins: self.allowed_origins.clone(),
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ thiserror.workspace = true
url.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
jsonrpsee = { version = "0.19.0", features = ["full"] }
jsonrpsee = { version = "0.26.0", features = ["full"] }
openssl.workspace = true
tower-http = { version = "0.6", features = ["compression-full", "trace", "cors"] }
tower = "0.5"
http = "1"

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4.33"
Expand Down
20 changes: 13 additions & 7 deletions core/src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
use std::{ops::Deref, sync::Arc};

#[cfg(not(target_arch = "wasm32"))]
Expand All @@ -10,7 +8,7 @@ use helios_common::{

use crate::consensus::Consensus;
#[cfg(not(target_arch = "wasm32"))]
use crate::jsonrpc;
use crate::jsonrpc::{self, RpcServerConfig};

use self::{api::HeliosApi, node::Node};

Expand All @@ -26,16 +24,24 @@ impl<N: NetworkSpec> HeliosClient<N> {
consensus: C,
execution: E,
fork_schedule: ForkSchedule,
#[cfg(not(target_arch = "wasm32"))] rpc_address: Option<SocketAddr>,
#[cfg(not(target_arch = "wasm32"))] rpc_config: Option<RpcServerConfig>,
) -> Self {
let inner = Arc::new(Node::new(consensus, execution, fork_schedule));

#[cfg(not(target_arch = "wasm32"))]
if let Some(rpc_address) = rpc_address {
if let Some(rpc_config) = rpc_config {
let inner_ref = inner.clone();
tokio::spawn(async move {
let _handle = jsonrpc::start(inner_ref, rpc_address).await;
let () = pending().await;
let shutdown_ref = inner_ref.clone();
match jsonrpc::start(inner_ref, rpc_config).await {
Ok(_handle) => {
let () = pending().await;
}
Err(err) => {
tracing::error!(?err, "failed to start JSON-RPC server");
shutdown_ref.shutdown().await;
}
}
});
}

Expand Down
7 changes: 0 additions & 7 deletions core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ impl From<ExecutionError> for ClientError {
}
}

#[cfg(not(target_arch = "wasm32"))]
impl From<ClientError> for jsonrpsee::core::Error {
fn from(value: ClientError) -> Self {
jsonrpsee::core::Error::Custom(value.to_string())
}
}

#[derive(Debug, Error)]
#[error("rpc error on method: {method}, message: {error}")]
pub struct RpcError<E: ToString> {
Expand Down
74 changes: 67 additions & 7 deletions core/src/jsonrpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ use alloy::rpc::types::{
Log, SyncStatus,
};
use eyre::{eyre, Result};
use http::Method;
use jsonrpsee::{
core::{async_trait, server::Methods, SubscriptionResult},
proc_macros::rpc,
server::{PendingSubscriptionSink, ServerBuilder, ServerHandle, SubscriptionMessage},
server::{PendingSubscriptionSink, ServerBuilder, ServerHandle},
types::error::{ErrorObject, ErrorObjectOwned},
};
use tower_http::cors::{AllowOrigin, CorsLayer};
use url::Url;

use helios_common::{
network_spec::NetworkSpec,
Expand All @@ -27,11 +30,24 @@ use crate::client::api::HeliosApi;

pub type Handle = ServerHandle;

#[cfg(not(target_arch = "wasm32"))]
pub struct RpcServerConfig {
pub addr: SocketAddr,
pub allowed_origins: Option<Vec<String>>,
}

pub async fn start<N: NetworkSpec>(
client: Arc<dyn HeliosApi<N>>,
addr: SocketAddr,
config: RpcServerConfig,
) -> Result<ServerHandle> {
let server = ServerBuilder::default().build(addr).await?;
let cors = build_cors_layer(config.allowed_origins)?;
let middleware = tower::ServiceBuilder::new().option_layer(cors);

let server = ServerBuilder::default()
.set_http_middleware(middleware)
.build(config.addr)
.await?;

let rpc = JsonRpc {
client,
phantom: PhantomData,
Expand All @@ -51,6 +67,50 @@ pub async fn start<N: NetworkSpec>(
Ok(server.start(methods))
}

fn build_cors_layer(allowed_origins: Option<Vec<String>>) -> Result<Option<CorsLayer>> {
let origins = match allowed_origins {
Some(origins) if !origins.is_empty() => origins,
_ => return Ok(None),
};

let mut list = Vec::with_capacity(origins.len());
let mut has_wildcard = false;

for origin in origins {
if origin == "*" {
has_wildcard = true;
break;
}
list.push(parse_allowed_origin(&origin)?);
}

let allow_origin = if has_wildcard {
AllowOrigin::any()
} else {
AllowOrigin::list(list)
};

Ok(Some(
CorsLayer::new()
.allow_methods([Method::POST, Method::OPTIONS])
.allow_origin(allow_origin)
.allow_headers([http::header::CONTENT_TYPE]),
))
}

fn parse_allowed_origin(origin: &str) -> Result<http::HeaderValue> {
let url = Url::parse(origin).map_err(|e| eyre!("invalid allowed origin '{origin}': {e}"))?;
if !url.origin().is_tuple() {
return Err(eyre!(
"invalid allowed origin '{origin}': must be a tuple origin (scheme + host)"
));
}

origin
.parse()
.map_err(|e| eyre!("invalid allowed origin header value '{origin}': {e}"))
}

#[derive(Clone)]
struct JsonRpc<N: NetworkSpec> {
client: Arc<dyn HeliosApi<N>>,
Expand Down Expand Up @@ -491,8 +551,8 @@ async fn handle_eth_subscription<N: NetworkSpec>(

tokio::spawn(async move {
while let Ok(message) = stream.recv().await {
let msg = SubscriptionMessage::from_json(&message).unwrap();
if sink.send(msg).await.is_err() {
let raw = serde_json::value::to_raw_value(&message).unwrap();
if sink.send(raw).await.is_err() {
break;
}
}
Expand Down Expand Up @@ -523,8 +583,8 @@ async fn handle_checkpoint_subscription(
tokio::spawn(async move {
while rx.changed().await.is_ok() {
let checkpoint = *rx.borrow_and_update();
let msg = SubscriptionMessage::from_json(&checkpoint).unwrap();
if sink.send(msg).await.is_err() {
let raw = serde_json::value::to_raw_value(&checkpoint).unwrap();
if sink.send(raw).await.is_err() {
break;
}
}
Expand Down
35 changes: 33 additions & 2 deletions ethereum/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use helios_core::execution::providers::block::block_cache::BlockCache;
use helios_core::execution::providers::historical::eip2935::Eip2935Provider;
use helios_core::execution::providers::rpc::RpcExecutionProvider;
use helios_core::execution::providers::verifiable_api::VerifiableApiExecutionProvider;
#[cfg(not(target_arch = "wasm32"))]
use helios_core::jsonrpc::RpcServerConfig;

use crate::config::networks::Network;
use crate::config::Config;
Expand All @@ -34,6 +36,8 @@ pub struct EthereumClientBuilder<DB: Database> {
#[cfg(not(target_arch = "wasm32"))]
rpc_address: Option<SocketAddr>,
#[cfg(not(target_arch = "wasm32"))]
allowed_origins: Option<Vec<String>>,
#[cfg(not(target_arch = "wasm32"))]
data_dir: Option<PathBuf>,
config: Option<Config>,
fallback: Option<Url>,
Expand All @@ -53,6 +57,8 @@ impl<DB: Database> Default for EthereumClientBuilder<DB> {
#[cfg(not(target_arch = "wasm32"))]
rpc_address: None,
#[cfg(not(target_arch = "wasm32"))]
allowed_origins: None,
#[cfg(not(target_arch = "wasm32"))]
data_dir: None,
config: None,
fallback: None,
Expand Down Expand Up @@ -111,6 +117,12 @@ impl<DB: Database> EthereumClientBuilder<DB> {
self
}

#[cfg(not(target_arch = "wasm32"))]
pub fn allowed_origins(mut self, allowed_origins: Vec<String>) -> Self {
self.allowed_origins = Some(allowed_origins);
self
}

#[cfg(not(target_arch = "wasm32"))]
pub fn data_dir(mut self, data_dir: PathBuf) -> Self {
self.data_dir = Some(data_dir);
Expand Down Expand Up @@ -203,6 +215,15 @@ impl<DB: Database> EthereumClientBuilder<DB> {
None
};

#[cfg(not(target_arch = "wasm32"))]
let allowed_origins = if self.allowed_origins.is_some() {
self.allowed_origins
} else if let Some(config) = &self.config {
config.allowed_origins.clone()
} else {
None
};

let fallback = if self.fallback.is_some() {
self.fallback
} else if let Some(config) = &self.config {
Expand Down Expand Up @@ -245,12 +266,22 @@ impl<DB: Database> EthereumClientBuilder<DB> {
forks: base_config.forks,
execution_forks: base_config.execution_forks,
max_checkpoint_age: base_config.max_checkpoint_age,
#[cfg(not(target_arch = "wasm32"))]
allowed_origins: allowed_origins.clone(),
#[cfg(target_arch = "wasm32")]
allowed_origins: None,
fallback,
load_external_fallback,
strict_checkpoint_age,
database_type,
};

#[cfg(not(target_arch = "wasm32"))]
let rpc_config = rpc_address.map(|addr| RpcServerConfig {
addr,
allowed_origins,
});

let config = Arc::new(config);
let consensus = ConsensusClient::<MainnetConsensusSpec, HttpRpc, DB>::new(
&config.consensus_rpc,
Expand All @@ -272,7 +303,7 @@ impl<DB: Database> EthereumClientBuilder<DB> {
execution,
config.execution_forks,
#[cfg(not(target_arch = "wasm32"))]
rpc_address,
rpc_config,
))
} else {
let block_provider = BlockCache::<Ethereum>::new();
Expand All @@ -290,7 +321,7 @@ impl<DB: Database> EthereumClientBuilder<DB> {
execution,
config.execution_forks,
#[cfg(not(target_arch = "wasm32"))]
rpc_address,
rpc_config,
))
}
}
Expand Down
5 changes: 5 additions & 0 deletions ethereum/src/config/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct CliConfig {
pub checkpoint: Option<B256>,
pub rpc_bind_ip: Option<IpAddr>,
pub rpc_port: Option<u16>,
pub allowed_origins: Option<Vec<String>>,
pub data_dir: Option<PathBuf>,
pub fallback: Option<Url>,
pub load_external_fallback: Option<bool>,
Expand Down Expand Up @@ -49,6 +50,10 @@ impl CliConfig {
user_dict.insert("rpc_port", Value::from(port));
}

if let Some(allowed_origins) = &self.allowed_origins {
user_dict.insert("allowed_origins", Value::from(allowed_origins.clone()));
}

if let Some(data_dir) = self.data_dir.as_ref() {
user_dict.insert("data_dir", Value::from(data_dir.to_str().unwrap()));
}
Expand Down
Loading
Loading