Skip to content

Commit

Permalink
feat(en): Rate-limit L2 client requests (#1500)
Browse files Browse the repository at this point in the history
## What ❔

Implements rate-limiting for `HttpClient` (HTTP-based JSON-RPC client
that we use in ENs to access main node Web3 APIs).

## Why ❔

Some request patterns made by ENs can be rate-limited (e.g., downloading
L2 blocks during initial syncing).

## Checklist

- [x] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [x] Tests for the changes have been added / updated.
- [x] Documentation comments have been added / updated.
- [x] Code has been formatted via `zk fmt` and `zk lint`.
- [x] Spellcheck has been run via `zk spellcheck`.
- [x] Linkcheck has been run via `zk linkcheck`.
  • Loading branch information
slowli committed Apr 3, 2024
1 parent cc78e1d commit 3f55f1e
Show file tree
Hide file tree
Showing 24 changed files with 1,065 additions and 142 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

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

34 changes: 21 additions & 13 deletions core/bin/external_node/src/config/mod.rs
@@ -1,4 +1,4 @@
use std::{env, time::Duration};
use std::{env, num::NonZeroUsize, time::Duration};

use anyhow::Context;
use serde::Deserialize;
Expand All @@ -15,8 +15,9 @@ use zksync_core::{
};
use zksync_types::{api::BridgeAddresses, fee_model::FeeParams};
use zksync_web3_decl::{
client::L2Client,
error::ClientRpcContext,
jsonrpsee::http_client::{HttpClient, HttpClientBuilder},
jsonrpsee::http_client::HttpClientBuilder,
namespaces::{EnNamespaceClient, EthNamespaceClient, ZksNamespaceClient},
};

Expand All @@ -28,7 +29,7 @@ const BYTES_IN_MEGABYTE: usize = 1_024 * 1_024;

/// This part of the external node config is fetched directly from the main node.
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct RemoteENConfig {
pub(crate) struct RemoteENConfig {
pub bridgehub_proxy_addr: Option<Address>,
pub state_transition_proxy_addr: Option<Address>,
pub transparent_proxy_admin_addr: Option<Address>,
Expand All @@ -46,7 +47,7 @@ pub struct RemoteENConfig {
}

impl RemoteENConfig {
pub async fn fetch(client: &HttpClient) -> anyhow::Result<Self> {
pub async fn fetch(client: &L2Client) -> anyhow::Result<Self> {
let bridges = client
.get_bridge_contracts()
.rpc_context("get_bridge_contracts")
Expand Down Expand Up @@ -110,7 +111,7 @@ impl RemoteENConfig {
}

#[derive(Debug, Deserialize, Clone, PartialEq)]
pub enum BlockFetcher {
pub(crate) enum BlockFetcher {
ServerAPI,
Consensus,
}
Expand All @@ -119,7 +120,7 @@ pub enum BlockFetcher {
/// It can tweak limits of the API, delay intervals of certain components, etc.
/// If any of the fields are not provided, the default values will be used.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct OptionalENConfig {
pub(crate) struct OptionalENConfig {
// User-facing API limits
/// Max possible limit of filters to be in the API state at once.
#[serde(default = "OptionalENConfig::default_filters_limit")]
Expand Down Expand Up @@ -274,6 +275,9 @@ pub struct OptionalENConfig {
// This is intentionally not a part of `RemoteENConfig` because fetching this info from the main node would defeat
// its purpose; the consistency checker assumes that the main node may provide false information.
pub contracts_diamond_proxy_addr: Option<Address>,
/// Number of requests per second allocated for the main node HTTP client. Default is 100 requests.
#[serde(default = "OptionalENConfig::default_main_node_rate_limit_rps")]
pub main_node_rate_limit_rps: NonZeroUsize,

#[serde(default = "OptionalENConfig::default_l1_batch_commit_data_generator_mode")]
pub l1_batch_commit_data_generator_mode: L1BatchCommitDataGeneratorMode,
Expand Down Expand Up @@ -408,6 +412,10 @@ impl OptionalENConfig {
10_000
}

fn default_main_node_rate_limit_rps() -> NonZeroUsize {
NonZeroUsize::new(100).unwrap()
}

const fn default_l1_batch_commit_data_generator_mode() -> L1BatchCommitDataGeneratorMode {
L1BatchCommitDataGeneratorMode::Rollup
}
Expand Down Expand Up @@ -487,7 +495,7 @@ impl OptionalENConfig {

/// This part of the external node config is required for its operation.
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct RequiredENConfig {
pub(crate) struct RequiredENConfig {
/// Port on which the HTTP RPC server is listening.
pub http_port: u16,
/// Port on which the WebSocket RPC server is listening.
Expand Down Expand Up @@ -526,7 +534,7 @@ impl RequiredENConfig {
/// environment variables.
/// Thus it is kept separately for backward compatibility and ease of deserialization.
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct PostgresConfig {
pub(crate) struct PostgresConfig {
pub database_url: String,
pub max_connections: u32,
}
Expand Down Expand Up @@ -563,7 +571,7 @@ pub(crate) fn read_consensus_config() -> anyhow::Result<Option<consensus::Config
/// Configuration for snapshot recovery. Loaded optionally, only if the corresponding command-line argument
/// is supplied to the EN binary.
#[derive(Debug, Clone)]
pub struct SnapshotsRecoveryConfig {
pub(crate) struct SnapshotsRecoveryConfig {
pub snapshots_object_store: ObjectStoreConfig,
}

Expand All @@ -579,7 +587,7 @@ pub(crate) fn read_snapshots_recovery_config() -> anyhow::Result<SnapshotsRecove
/// External Node Config contains all the configuration required for the EN operation.
/// It is split into three parts: required, optional and remote for easier navigation.
#[derive(Debug, Clone)]
pub struct ExternalNodeConfig {
pub(crate) struct ExternalNodeConfig {
pub required: RequiredENConfig,
pub postgres: PostgresConfig,
pub optional: OptionalENConfig,
Expand Down Expand Up @@ -609,9 +617,9 @@ impl ExternalNodeConfig {
.from_env::<TreeComponentConfig>()
.context("could not load external node config")?;

let client = HttpClientBuilder::default()
.build(required.main_node_url()?)
.expect("Unable to build HTTP client for main node");
let client = L2Client::http(&required.main_node_url()?)
.context("Unable to build HTTP client for main node")?
.build();
let remote = RemoteENConfig::fetch(&client)
.await
.context("Unable to fetch required config values from the main node")?;
Expand Down
10 changes: 5 additions & 5 deletions core/bin/external_node/src/helpers.rs
@@ -1,15 +1,15 @@
//! Miscellaneous helpers for the EN.

use zksync_health_check::{async_trait, CheckHealth, Health, HealthStatus};
use zksync_web3_decl::{jsonrpsee::http_client::HttpClient, namespaces::EthNamespaceClient};
use zksync_web3_decl::{client::L2Client, namespaces::EthNamespaceClient};

/// Main node health check.
#[derive(Debug)]
pub(crate) struct MainNodeHealthCheck(HttpClient);
pub(crate) struct MainNodeHealthCheck(L2Client);

impl From<HttpClient> for MainNodeHealthCheck {
fn from(client: HttpClient) -> Self {
Self(client)
impl From<L2Client> for MainNodeHealthCheck {
fn from(client: L2Client) -> Self {
Self(client.for_component("main_node_health_check"))
}
}

Expand Down
20 changes: 14 additions & 6 deletions core/bin/external_node/src/init.rs
Expand Up @@ -7,7 +7,7 @@ use zksync_dal::{ConnectionPool, Core, CoreDal};
use zksync_health_check::AppHealthCheck;
use zksync_object_store::ObjectStoreFactory;
use zksync_snapshots_applier::SnapshotsApplierConfig;
use zksync_web3_decl::jsonrpsee::http_client::HttpClient;
use zksync_web3_decl::client::L2Client;

use crate::config::read_snapshots_recovery_config;

Expand All @@ -21,7 +21,7 @@ enum InitDecision {

pub(crate) async fn ensure_storage_initialized(
pool: &ConnectionPool<Core>,
main_node_client: &HttpClient,
main_node_client: L2Client,
app_health: &AppHealthCheck,
l2_chain_id: L2ChainId,
consider_snapshot_recovery: bool,
Expand Down Expand Up @@ -68,9 +68,13 @@ pub(crate) async fn ensure_storage_initialized(
match decision {
InitDecision::Genesis => {
let mut storage = pool.connection_tagged("en").await?;
perform_genesis_if_needed(&mut storage, l2_chain_id, main_node_client)
.await
.context("performing genesis failed")?;
perform_genesis_if_needed(
&mut storage,
l2_chain_id,
&main_node_client.for_component("genesis"),
)
.await
.context("performing genesis failed")?;
}
InitDecision::SnapshotRecovery => {
anyhow::ensure!(
Expand All @@ -89,7 +93,11 @@ pub(crate) async fn ensure_storage_initialized(
let config = SnapshotsApplierConfig::default();
app_health.insert_component(config.health_check());
config
.run(pool, main_node_client, &blob_store)
.run(
pool,
&main_node_client.for_component("snapshot_recovery"),
&blob_store,
)
.await
.context("snapshot recovery failed")?;
tracing::info!("Snapshot recovery is complete");
Expand Down

0 comments on commit 3f55f1e

Please sign in to comment.