Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ PRECONF_ROUTER_ADDRESS=0x8dDEA87FfA296B951881aDacbAe8011090C54cAC
TAIKO_INBOX_ADDRESS=0xc2DD6e8DC8d0558F00Cc1FA6A16FFF1A62Cc436B
ANCHOR_ADDRESS=0x1670100000000000000000000000000000010001
VALIDATOR_INDEX=1
L2_RPC_URL=ws://127.0.0.1:8546
# Ordinary execution RPC; Shasta and Realtime subscriptions use L2_WS_RPC_URL.
L2_RPC_URL=http://127.0.0.1:8545
L2_WS_RPC_URL=ws://127.0.0.1:8546
L2_AUTH_RPC_URL=http://127.0.0.1:8551
L2_DRIVER_URL=http://127.0.0.1:1235
L1_RPC_URLS=ws://127.0.0.1:32003,wss://123.123.123.123:32001
L1_BEACON_URL=http://127.0.0.1:33001
RUST_LOG=debug,reqwest=info,hyper=info,alloy_transport=info,alloy_rpc_client=info,alloy_provider=info
JWT_SECRET_FILE_PATH=/some/path/jwtsecret.hex
JWT_SECRET_FILE_PATH=/some/path/jwtsecret.hex
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to Catalyst are documented here, organized by release version.

## [Unreleased]

### Features
- Add `L2_WS_RPC_URL` to split WebSocket subscriptions from ordinary L2 execution RPC requests (#2)

---

## [v1.41.0] — 2026-06-25

### Features
Expand Down
184 changes: 184 additions & 0 deletions common/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod config_trait;
pub use config_trait::ConfigTrait;

use crate::shared::alloy_tools::{RpcTransport, parse_rpc_transport};
use alloy::primitives::Address;
use anyhow::Error;
use std::str::FromStr;
Expand All @@ -27,6 +28,7 @@ pub struct Config {
pub preconf_heartbeat_ms: u64,
// L2
pub l2_rpc_url: String,
pub l2_ws_rpc_url: String,
pub l2_auth_rpc_url: String,
pub l2_driver_url: String,
/// jwt secret file path for L2 EL and L2 driver
Expand Down Expand Up @@ -121,6 +123,35 @@ fn get_env_with_deprecation(new_key: &str, deprecated_key: &str) -> Option<Strin
}
}

fn resolve_l2_ws_rpc_url(
l2_rpc_url: &str,
configured_l2_ws_rpc_url: Option<String>,
) -> Result<String, Error> {
let l2_rpc_transport = parse_rpc_transport("L2_RPC_URL", l2_rpc_url)?;

let configured_l2_ws_rpc_url = configured_l2_ws_rpc_url.filter(|url| !url.trim().is_empty());
if let Some(url) = configured_l2_ws_rpc_url {
let parsed = reqwest::Url::parse(&url)
.map_err(|error| anyhow::anyhow!("L2_WS_RPC_URL must be a valid URL: {error}"))?;
match parsed.scheme() {
"ws" | "wss" => return Ok(parsed.to_string()),
scheme => {
return Err(anyhow::anyhow!(
"L2_WS_RPC_URL must use the ws or wss scheme, got '{scheme}'"
));
}
}
}

if let RpcTransport::WebSocket(parsed) = l2_rpc_transport {
return Ok(parsed.to_string());
}

Err(anyhow::anyhow!(
"L2_WS_RPC_URL must be set to a ws:// or wss:// URL when L2_RPC_URL does not use WebSocket"
))
}

impl Config {
pub fn read_env_variables() -> Result<Self, Error> {
// Load environment variables from .env file
Expand Down Expand Up @@ -501,6 +532,9 @@ impl Config {
"ws://127.0.0.1:1234".to_string()
});

let l2_ws_rpc_url =
resolve_l2_ws_rpc_url(&l2_rpc_url, std::env::var("L2_WS_RPC_URL").ok())?;

let l2_auth_rpc_url =
get_env_with_deprecation("L2_AUTH_RPC_URL", "TAIKO_GETH_AUTH_RPC_URL").unwrap_or_else(
|| {
Expand All @@ -518,6 +552,7 @@ impl Config {
let config = Self {
preconfer_address,
l2_rpc_url,
l2_ws_rpc_url,
l2_auth_rpc_url,
l2_driver_url,
catalyst_node_ecdsa_private_key,
Expand Down Expand Up @@ -580,6 +615,7 @@ impl Config {
r#"
Configuration:{}
L2 RPC URL: {},
L2 WebSocket RPC URL: {},
L2 auth RPC URL: {},
L2 driver URL: {},
L1 RPC URL: {},
Expand Down Expand Up @@ -636,6 +672,7 @@ internal server port: {}
"".to_string()
},
config.l2_rpc_url,
config.l2_ws_rpc_url,
config.l2_auth_rpc_url,
config.l2_driver_url,
match config.l1_rpc_urls.split_first() {
Expand Down Expand Up @@ -697,3 +734,150 @@ internal server port: {}
Ok(config)
}
}

#[cfg(test)]
mod tests {
use super::resolve_l2_ws_rpc_url;

#[test]
fn explicit_l2_ws_rpc_url_is_used_with_http_rpc() {
let resolved = resolve_l2_ws_rpc_url(
"http://127.0.0.1:8545",
Some("ws://127.0.0.1:8546".to_string()),
)
.unwrap();

assert_eq!(resolved, "ws://127.0.0.1:8546/");
}

#[test]
fn explicit_l2_ws_rpc_url_trims_surrounding_whitespace() {
let resolved = resolve_l2_ws_rpc_url(
"http://127.0.0.1:8545",
Some(" wss://l2.example ".to_string()),
)
.unwrap();

assert_eq!(resolved, "wss://l2.example/");
}

#[test]
fn explicit_l2_ws_rpc_url_uses_canonical_serialization() {
let resolved = resolve_l2_ws_rpc_url(
"http://127.0.0.1:8545",
Some("wss://l2.example/pa th".to_string()),
)
.unwrap();

assert_eq!(resolved, "wss://l2.example/pa%20th");
}

#[test]
fn legacy_websocket_l2_rpc_url_is_reused() {
let resolved = resolve_l2_ws_rpc_url("wss://l2.example", None).unwrap();

assert_eq!(resolved, "wss://l2.example/");
}

#[test]
fn legacy_websocket_l2_rpc_url_trims_surrounding_whitespace() {
let resolved = resolve_l2_ws_rpc_url(" wss://l2.example ", None).unwrap();

assert_eq!(resolved, "wss://l2.example/");
}

#[test]
fn legacy_websocket_l2_rpc_url_uses_canonical_serialization() {
let resolved = resolve_l2_ws_rpc_url("wss://l2.example/pa th", None).unwrap();

assert_eq!(resolved, "wss://l2.example/pa%20th");
}

#[test]
fn empty_l2_ws_rpc_url_reuses_legacy_websocket_l2_rpc_url() {
let resolved = resolve_l2_ws_rpc_url("wss://l2.example", Some(String::new())).unwrap();

assert_eq!(resolved, "wss://l2.example/");
}

#[test]
fn whitespace_only_l2_ws_rpc_url_reuses_legacy_websocket_l2_rpc_url() {
let resolved =
resolve_l2_ws_rpc_url("wss://l2.example", Some(" \t ".to_string())).unwrap();

assert_eq!(resolved, "wss://l2.example/");
}

#[test]
fn http_l2_rpc_url_requires_explicit_websocket_url() {
let error = resolve_l2_ws_rpc_url("https://l2.example", None)
.unwrap_err()
.to_string();

assert!(error.contains("L2_WS_RPC_URL must be set"));
}

#[test]
fn malformed_l2_rpc_url_is_rejected_with_explicit_websocket_url() {
let error = resolve_l2_ws_rpc_url("not a valid URL", Some("wss://l2.example".to_string()))
.unwrap_err()
.to_string();

assert!(error.contains("L2_RPC_URL must be a valid URL"));
}

#[test]
fn empty_l2_rpc_url_has_a_specific_error() {
let error = resolve_l2_ws_rpc_url("", Some("wss://l2.example".to_string()))
.unwrap_err()
.to_string();

assert_eq!(error, "L2_RPC_URL must not be empty");
}

#[test]
fn l2_rpc_url_rejects_unsupported_scheme() {
let error = resolve_l2_ws_rpc_url("ftp://l2.example", Some("wss://l2.example".to_string()))
.unwrap_err()
.to_string();

assert!(error.contains("L2_RPC_URL must use the http, https, ws or wss scheme"));
}

#[test]
fn explicit_l2_ws_rpc_url_rejects_non_websocket_scheme() {
let error = resolve_l2_ws_rpc_url(
"http://127.0.0.1:8545",
Some("http://127.0.0.1:8546".to_string()),
)
.unwrap_err()
.to_string();

assert!(error.contains("L2_WS_RPC_URL must use the ws or wss scheme"));
}

#[test]
fn explicit_l2_ws_rpc_url_rejects_unsupported_scheme_as_websocket_only() {
let error = resolve_l2_ws_rpc_url(
"http://127.0.0.1:8545",
Some("ftp://l2.example".to_string()),
)
.unwrap_err()
.to_string();

assert_eq!(
error,
"L2_WS_RPC_URL must use the ws or wss scheme, got 'ftp'"
);
}

#[test]
fn explicit_l2_ws_rpc_url_rejects_malformed_url() {
let error =
resolve_l2_ws_rpc_url("http://127.0.0.1:8545", Some("not a valid URL".to_string()))
.unwrap_err()
.to_string();

assert!(error.contains("L2_WS_RPC_URL must be a valid URL"));
}
}
Loading