Skip to content

Commit

Permalink
L1 client failover
Browse files Browse the repository at this point in the history
Allow the L1 client to be configured with multiple RPC provider URLs.
Fail over to the next provider in the list if
* two requests fail in a given time period
* N requests fail in a row

Handle rate limit errors specially by preventing requests to the
provider until the rate limit period expires.

Closes #2360
  • Loading branch information
jbearer committed Jan 3, 2025
1 parent a54f777 commit 03b2c6a
Show file tree
Hide file tree
Showing 15 changed files with 487 additions and 94 deletions.
22 changes: 20 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions builder/src/bin/permissionless-builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ struct NonPermissionedBuilderOptions {
eth_account_index: u32,

/// Url we will use for RPC communication with L1.
#[clap(long, env = "ESPRESSO_BUILDER_L1_PROVIDER")]
l1_provider_url: Url,
#[clap(
long,
env = "ESPRESSO_BUILDER_L1_PROVIDER",
value_delimiter = ',',
num_args = 1..,
)]
l1_provider_url: Vec<Url>,

/// Peer nodes use to fetch missing state
#[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
Expand Down Expand Up @@ -133,7 +138,7 @@ async fn run<V: Versions>(
opt: NonPermissionedBuilderOptions,
) -> anyhow::Result<()> {
let l1_params = L1Params {
url: opt.l1_provider_url,
urls: opt.l1_provider_url,
options: Default::default(),
};

Expand Down
2 changes: 1 addition & 1 deletion builder/src/non_permissioned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn build_instance_state<V: Versions>(
l1_params: L1Params,
state_peers: Vec<Url>,
) -> NodeState {
let l1_client = l1_params.options.connect(l1_params.url);
let l1_client = l1_params.options.connect(l1_params.urls);
NodeState::new(
u64::MAX, // dummy node ID, only used for debugging
chain_config,
Expand Down
11 changes: 8 additions & 3 deletions marketplace-builder/src/bin/marketplace-builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ struct NonPermissionedBuilderOptions {
eth_account_index: u32,

/// Url we will use for RPC communication with L1.
#[clap(long, env = "ESPRESSO_BUILDER_L1_PROVIDER")]
l1_provider_url: Url,
#[clap(
long,
env = "ESPRESSO_BUILDER_L1_PROVIDER",
value_delimiter = ',',
num_args = 1..,
)]
l1_provider_url: Vec<Url>,

/// Peer nodes use to fetch missing state
#[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
Expand Down Expand Up @@ -140,7 +145,7 @@ async fn run<V: Versions>(
opt: NonPermissionedBuilderOptions,
) -> anyhow::Result<()> {
let l1_params = L1Params {
url: opt.l1_provider_url,
urls: opt.l1_provider_url,
options: Default::default(),
};

Expand Down
2 changes: 1 addition & 1 deletion marketplace-builder/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub fn build_instance_state<V: Versions>(
l1_params: L1Params,
state_peers: Vec<Url>,
) -> NodeState {
let l1_client = l1_params.options.connect(l1_params.url);
let l1_client = l1_params.options.connect(l1_params.urls);
NodeState::new(
u64::MAX, // dummy node ID, only used for debugging
chain_config,
Expand Down
20 changes: 19 additions & 1 deletion sequencer-sqlite/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 21 additions & 8 deletions sequencer/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use espresso_types::{
use ethers::types::H160;
use sequencer_utils::deployer::is_proxy_contract;
use serde::{Deserialize, Serialize};
use url::Url;
use vbs::version::Version;

/// Initial configuration of an Espresso stake table.
Expand Down Expand Up @@ -85,8 +86,8 @@ impl Genesis {
}

impl Genesis {
pub async fn validate_fee_contract(&self, l1_rpc_url: String) -> anyhow::Result<()> {
let l1 = L1Client::new(l1_rpc_url.parse().context("invalid url")?);
pub async fn validate_fee_contract(&self, l1_rpc_url: Url) -> anyhow::Result<()> {
let l1 = L1Client::new(l1_rpc_url);

if let Some(fee_contract_address) = self.chain_config.fee_contract {
tracing::info!("validating fee contract at {fee_contract_address:x}");
Expand Down Expand Up @@ -593,7 +594,9 @@ mod test {
let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));

// validate the fee_contract address
let result = genesis.validate_fee_contract(anvil.endpoint()).await;
let result = genesis
.validate_fee_contract(anvil.endpoint().parse().unwrap())
.await;

// check if the result from the validation is an error
if let Err(e) = result {
Expand Down Expand Up @@ -639,7 +642,9 @@ mod test {
let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));

// Call the validation logic for the fee_contract address
let result = genesis.validate_fee_contract(anvil.endpoint()).await;
let result = genesis
.validate_fee_contract(anvil.endpoint().parse().unwrap())
.await;

assert!(
result.is_ok(),
Expand Down Expand Up @@ -711,7 +716,9 @@ mod test {
let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));

// Call the validation logic for the fee_contract address
let result = genesis.validate_fee_contract(anvil.endpoint()).await;
let result = genesis
.validate_fee_contract(anvil.endpoint().parse().unwrap())
.await;

assert!(
result.is_ok(),
Expand Down Expand Up @@ -783,7 +790,9 @@ mod test {
let genesis: Genesis = toml::from_str(&toml).unwrap_or_else(|err| panic!("{err:#}"));

// Call the validation logic for the fee_contract address
let result = genesis.validate_fee_contract(anvil.endpoint()).await;
let result = genesis
.validate_fee_contract(anvil.endpoint().parse().unwrap())
.await;

// check if the result from the validation is an error
if let Err(e) = result {
Expand Down Expand Up @@ -851,7 +860,9 @@ mod test {
let rpc_url = "https://ethereum-sepolia.publicnode.com";

// validate the fee_contract address
let result = genesis.validate_fee_contract(rpc_url.to_string()).await;
let result = genesis
.validate_fee_contract(rpc_url.parse().unwrap())
.await;

// check if the result from the validation is an error
if let Err(e) = result {
Expand Down Expand Up @@ -904,7 +915,9 @@ mod test {
let rpc_url = "https://ethereum-sepolia.publicnode.com";

// validate the fee_contract address
let result = genesis.validate_fee_contract(rpc_url.to_string()).await;
let result = genesis
.validate_fee_contract(rpc_url.parse().unwrap())
.await;

// check if the result from the validation is an error
if let Err(e) = result {
Expand Down
4 changes: 2 additions & 2 deletions sequencer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ pub struct NetworkParams {
}

pub struct L1Params {
pub url: Url,
pub urls: Vec<Url>,
pub options: L1ClientOptions,
}

Expand Down Expand Up @@ -491,7 +491,7 @@ pub async fn init_node<P: SequencerPersistence, V: Versions>(
let l1_client = l1_params
.options
.with_metrics(metrics)
.connect(l1_params.url);
.connect(l1_params.urls);
l1_client.spawn_tasks().await;
let l1_genesis = match genesis.l1_finalized {
L1Finalized::Block(b) => b,
Expand Down
8 changes: 5 additions & 3 deletions sequencer/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,12 @@ pub struct Options {
#[clap(
long,
env = "ESPRESSO_SEQUENCER_L1_PROVIDER",
default_value = "http://localhost:8545"
default_value = "http://localhost:8545",
value_delimiter = ',',
num_args = 1..,
)]
#[derivative(Debug(format_with = "Display::fmt"))]
pub l1_provider_url: Url,
#[derivative(Debug = "ignore")]
pub l1_provider_url: Vec<Url>,

/// Configuration for the L1 client.
#[clap(flatten)]
Expand Down
4 changes: 2 additions & 2 deletions sequencer/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub async fn main() -> anyhow::Result<()> {

// validate that the fee contract is a proxy and panic otherwise
genesis
.validate_fee_contract(opt.l1_provider_url.to_string())
.validate_fee_contract(opt.l1_provider_url[0].clone())
.await
.unwrap();

Expand Down Expand Up @@ -130,7 +130,7 @@ where
{
let (private_staking_key, private_state_key) = opt.private_keys()?;
let l1_params = L1Params {
url: opt.l1_provider_url,
urls: opt.l1_provider_url,
options: opt.l1_options,
};

Expand Down
2 changes: 2 additions & 0 deletions types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ testing = []
anyhow = { workspace = true }
ark-serialize = { workspace = true }
async-broadcast = { workspace = true }
async-lock = { workspace = true }
async-trait = { workspace = true }
base64-bytes = { workspace = true }
bincode = { workspace = true }
Expand Down Expand Up @@ -39,6 +40,7 @@ num-traits = { workspace = true }
paste = { workspace = true }
pretty_assertions = { workspace = true }
rand = { workspace = true }
reqwest = "0.11" # Same version used by ethers
sequencer-utils = { path = "../utils" }
serde = { workspace = true }
serde_json = { workspace = true }
Expand Down
Loading

0 comments on commit 03b2c6a

Please sign in to comment.