diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..d777fe6e --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +## LiteRpc +LITE_RPC_HTTP_ADDR=http://0.0.0.0:8890 +LITE_RPC_WS_ADDR=[::]:8891 + +## RPC +RPC_ADDR=http://0.0.0.0:8899 +WS_ADDR=ws://0.0.0.0:8900 +# IDENTITY=your_identity_keypair_here + +## Prometheus +# PROMETHEUS_ADDR=your_prometheus_address_here + +## Fanout size and retries configuration +FANOUT_SIZE=18 +MAX_RETRIES=40 +RETRY_TIMEOUT=3 + +## Quic Proxy +# QUIC_PROXY_ADDR=your_quic_proxy_address_here + +## gRPC Configuration +USE_GRPC=false +GRPC_ADDR=http://127.0.0.0:10000 +# GRPC_X_TOKEN=your_grpc_token_here + +## Postgres Configuration +# PG_ENABLED=true +# PG_CONFIG=your_postgres_config_here +# CA_PEM_B64=your_base64_encoded_ca_pem_here +# CLIENT_PKS_B64=your_base64_encoded_client_pks_here +# CLIENT_PKS_PASS=your_client_pks_password_here + +## Gso +# DISABLE_GSO=your_disable_gso_setting_here diff --git a/Cargo.lock b/Cargo.lock index 7da63f13..a6e40117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4325,6 +4325,7 @@ dependencies = [ "log", "native-tls", "postgres-native-tls", + "serde", "solana-lite-rpc-core", "solana-rpc-client", "solana-rpc-client-api", diff --git a/README.md b/README.md index 7eec0695..a56d5423 100644 --- a/README.md +++ b/README.md @@ -58,17 +58,24 @@ Find a new file named `metrics.csv` in the project root. ### Environment Variables -| env | purpose | required? | -|-------------------|------------------------------------------------|---------------------| -| `RPC_URL` | HTTP URL for a full RPC node | yes, for docker | -| `WS_URL` | WS URL for a full RPC node | yes, for docker | -| `IDENTITY` | Staked validator identity keypair | no | -| `PG_ENABLED` | Set to anything but 'false' to enable Postgres | no | -| `PG_CONFIG` | Postgres Connection Config | if postgres enabled | -| `CA_PEM_B64` | Base64 encoded `ca.pem` | if postgres enabled | -| `CLIENT_PKS_B64` | Base64 encoded `client.pks` | if postgres enabled | -| `CLIENT_PKS_PASS` | Password to `client.pks` | if postgres enabled | -| `DISABLE_GSO` | Disable GSO completely | no | +Thank you for providing the default values. Here's the updated table with the default values for the environment variables based on the additional information: + +| Environment Variable | Purpose | Required? | Default Value | +|--------------------------|------------------------------------------------------------------|---------------------|------------------------------------------------| +| `RPC_ADDR` | Address for the RPC node | Replaces default if set | `http://0.0.0.0:8899` (from `DEFAULT_RPC_ADDR`) | +| `WS_ADDR` | WebSocket address for the RPC node | Replaces default if set | `ws://0.0.0.0:8900` (from `DEFAULT_WS_ADDR`) | +| `LITE_RPC_HTTP_ADDR` | HTTP address for the lite RPC node | Replaces default if set | `http://0.0.0.0:8890` (from `DEFAULT_LITE_RPC_ADDR`) | +| `LITE_RPC_WS_ADDR` | WebSocket address for the lite RPC node | Replaces default if set | `[::]:8891` (from `Config::default_lite_rpc_ws_addr`) | +| `FANOUT_SIZE` | Configuration for the fanout size | Replaces default if set | `18` (from `DEFAULT_FANOUT_SIZE`) | +| `IDENTITY` | Identity keypair | Optional, replaces default if set | None | +| `PROMETHEUS_ADDR` | Address for Prometheus monitoring | Replaces default if set | None specified in provided defaults | +| `MAX_RETRIES` | Maximum number of retries per transaction | Replaces default if set | `40` (from `MAX_RETRIES`) | +| `RETRY_TIMEOUT` | Timeout for transaction retries in seconds | Replaces default if set | `3` (from `DEFAULT_RETRY_TIMEOUT`) | +| `QUIC_PROXY_ADDR` | Address for QUIC proxy | Optional | None | +| `USE_GRPC` | Flag to enable or disable gRPC | Enables gRPC if set | `false` | +| `GRPC_ADDR` | gRPC address | Replaces default if set | `http://127.0.0.0:10000` (from `DEFAULT_GRPC_ADDR`) | +| `GRPC_X_TOKEN` | Token for gRPC authentication | Optional | None | +| `PG_*` | Various environment variables for Postgres configuration | Depends on Postgres usage | Based on `PostgresSessionConfig::new_from_env()` | ### Postgres lite-rpc implements an optional postgres service that can write to postgres diff --git a/config.example.json b/config.example.json new file mode 100644 index 00000000..2074c7ea --- /dev/null +++ b/config.example.json @@ -0,0 +1,23 @@ +{ + "rpc_addr": "http://0.0.0.0:8899", + "ws_addr": "ws://0.0.0.0:8900", + "lite_rpc_http_addr": "http://0.0.0.0:8890", + "lite_rpc_ws_addr": "[::]:8891", + "fanout_size": 18, + "identity_keypair": null, + "prometheus_addr": "[::]:9091", + "maximum_retries_per_tx": 40, + "transaction_retry_after_secs": 3, + "quic_proxy_addr": null, + "use_grpc": false, + "grpc_addr": "http://127.0.0.0:10000", + "grpc_x_token": null, + "postgres": { + "pg_config": "your_postgres_config", + "ssl": { + "ca_pem_b64": "your_base64_encoded_ca_pem", + "client_pks_b64": "your_base64_encoded_client_pks", + "client_pks_pass": "your_client_pks_password" + } + } +} diff --git a/core/src/keypair_loader.rs b/core/src/keypair_loader.rs index bc9be5b2..5221e6d6 100644 --- a/core/src/keypair_loader.rs +++ b/core/src/keypair_loader.rs @@ -1,26 +1,25 @@ +use anyhow::Context; use solana_sdk::signature::Keypair; use std::env; // note this is duplicated from lite-rpc module -pub async fn load_identity_keypair(identity_from_cli: &String) -> Option { - if let Ok(identity_env_var) = env::var("IDENTITY") { - if let Ok(identity_bytes) = serde_json::from_str::>(identity_env_var.as_str()) { - Some(Keypair::from_bytes(identity_bytes.as_slice()).unwrap()) - } else { - // must be a file - let identity_file = tokio::fs::read_to_string(identity_env_var.as_str()) - .await - .expect("Cannot find the identity file provided"); - let identity_bytes: Vec = serde_json::from_str(&identity_file).unwrap(); - Some(Keypair::from_bytes(identity_bytes.as_slice()).unwrap()) - } - } else if identity_from_cli.is_empty() { - None - } else { - let identity_file = tokio::fs::read_to_string(identity_from_cli.as_str()) +pub async fn load_identity_keypair( + identity_from_cli: Option, +) -> anyhow::Result> { + let identity_str = if let Some(identity_from_cli) = identity_from_cli { + tokio::fs::read_to_string(identity_from_cli) .await - .expect("Cannot find the identity file provided"); - let identity_bytes: Vec = serde_json::from_str(&identity_file).unwrap(); - Some(Keypair::from_bytes(identity_bytes.as_slice()).unwrap()) - } + .context("Cannot find the identity file provided")? + } else if let Ok(identity_env_var) = env::var("IDENTITY") { + identity_env_var + } else { + return Ok(None); + }; + + let identity_bytes: Vec = + serde_json::from_str(&identity_str).context("Invalid identity format expected Vec")?; + + Ok(Some( + Keypair::from_bytes(identity_bytes.as_slice()).context("Invalid identity")?, + )) } diff --git a/history/Cargo.toml b/history/Cargo.toml index 160dad05..08758b88 100644 --- a/history/Cargo.toml +++ b/history/Cargo.toml @@ -26,4 +26,5 @@ chrono = {workspace = true} bincode = {workspace = true} base64 = {workspace = true} itertools = {workspace = true} -tokio-postgres = { version = "0.7.8", features = ["with-chrono-0_4"] } \ No newline at end of file +tokio-postgres = { version = "0.7.8", features = ["with-chrono-0_4"] } +serde = { workspace = true } diff --git a/history/src/postgres/mod.rs b/history/src/postgres/mod.rs index da7a33f2..dc816d60 100644 --- a/history/src/postgres/mod.rs +++ b/history/src/postgres/mod.rs @@ -1,3 +1,4 @@ pub mod postgres_block; +pub mod postgres_config; pub mod postgres_session; pub mod postgres_transaction; diff --git a/history/src/postgres/postgres_config.rs b/history/src/postgres/postgres_config.rs new file mode 100644 index 00000000..cc5c6bdb --- /dev/null +++ b/history/src/postgres/postgres_config.rs @@ -0,0 +1,52 @@ +use anyhow::Context; +use std::env; +use tokio_postgres::config::SslMode; + +#[derive(serde::Deserialize, Debug, Clone)] +pub struct PostgresSessionConfig { + pub pg_config: String, + pub ssl: Option, +} + +#[derive(serde::Deserialize, Debug, Clone)] +pub struct PostgresSessionSslConfig { + pub ca_pem_b64: String, + pub client_pks_b64: String, + pub client_pks_pass: String, +} + +impl PostgresSessionConfig { + pub fn new_from_env() -> anyhow::Result> { + // pg not enabled + if env::var("PG_ENABLED").is_err() { + return Ok(None); + } + + let env_pg_config = env::var("PG_CONFIG").context("PG_CONFIG not found")?; + + let ssl_config = if env_pg_config + .parse::()? + .get_ssl_mode() + .eq(&SslMode::Disable) + { + None + } else { + let env_ca_pem_b64 = env::var("CA_PEM_B64").context("CA_PEM_B64 not found")?; + let env_client_pks_b64 = + env::var("CLIENT_PKS_B64").context("CLIENT_PKS_B64 not found")?; + let env_client_pks_pass = + env::var("CLIENT_PKS_PASS").context("CLIENT_PKS_PASS not found")?; + + Some(PostgresSessionSslConfig { + ca_pem_b64: env_ca_pem_b64, + client_pks_b64: env_client_pks_b64, + client_pks_pass: env_client_pks_pass, + }) + }; + + Ok(Some(Self { + pg_config: env_pg_config, + ssl: ssl_config, + })) + } +} diff --git a/history/src/postgres/postgres_session.rs b/history/src/postgres/postgres_session.rs index dc97533b..65dbc26a 100644 --- a/history/src/postgres/postgres_session.rs +++ b/history/src/postgres/postgres_session.rs @@ -7,6 +7,8 @@ use solana_lite_rpc_core::encoding::BinaryEncoding; use tokio::sync::RwLock; use tokio_postgres::{config::SslMode, tls::MakeTlsConnect, types::ToSql, Client, NoTls, Socket}; +use super::postgres_config::{PostgresSessionConfig, PostgresSessionSslConfig}; + const MAX_QUERY_SIZE: usize = 200_000; // 0.2 mb pub trait SchemaSize { @@ -36,18 +38,19 @@ pub struct PostgresSession { } impl PostgresSession { - pub async fn new() -> anyhow::Result { - let pg_config = std::env::var("PG_CONFIG").context("env PG_CONFIG not found")?; + pub async fn new( + PostgresSessionConfig { pg_config, ssl }: &PostgresSessionConfig, + ) -> anyhow::Result { let pg_config = pg_config.parse::()?; let client = if let SslMode::Disable = pg_config.get_ssl_mode() { Self::spawn_connection(pg_config, NoTls).await? } else { - let ca_pem_b64 = std::env::var("CA_PEM_B64").context("env CA_PEM_B64 not found")?; - let client_pks_b64 = - std::env::var("CLIENT_PKS_B64").context("env CLIENT_PKS_B64 not found")?; - let client_pks_password = - std::env::var("CLIENT_PKS_PASS").context("env CLIENT_PKS_PASS not found")?; + let PostgresSessionSslConfig { + ca_pem_b64, + client_pks_b64, + client_pks_pass, + } = ssl.as_ref().unwrap(); let ca_pem = BinaryEncoding::Base64 .decode(ca_pem_b64) @@ -58,9 +61,7 @@ impl PostgresSession { let connector = TlsConnector::builder() .add_root_certificate(Certificate::from_pem(&ca_pem)?) - .identity( - Identity::from_pkcs12(&client_pks, &client_pks_password).context("Identity")?, - ) + .identity(Identity::from_pkcs12(&client_pks, client_pks_pass).context("Identity")?) .danger_accept_invalid_hostnames(true) .danger_accept_invalid_certs(true) .build()?; @@ -136,13 +137,15 @@ impl PostgresSession { #[derive(Clone)] pub struct PostgresSessionCache { session: Arc>, + config: PostgresSessionConfig, } impl PostgresSessionCache { - pub async fn new() -> anyhow::Result { - let session = PostgresSession::new().await?; + pub async fn new(config: PostgresSessionConfig) -> anyhow::Result { + let session = PostgresSession::new(&config).await?; Ok(Self { session: Arc::new(RwLock::new(session)), + config, }) } @@ -150,7 +153,7 @@ impl PostgresSessionCache { let session = self.session.read().await; if session.client.is_closed() { drop(session); - let session = PostgresSession::new().await?; + let session = PostgresSession::new(&self.config).await?; *self.session.write().await = session.clone(); Ok(session) } else { diff --git a/lite-rpc/src/cli.rs b/lite-rpc/src/cli.rs index 9e9cfdae..db422530 100644 --- a/lite-rpc/src/cli.rs +++ b/lite-rpc/src/cli.rs @@ -1,42 +1,166 @@ +use std::env; + use crate::{ DEFAULT_FANOUT_SIZE, DEFAULT_GRPC_ADDR, DEFAULT_RETRY_TIMEOUT, DEFAULT_RPC_ADDR, DEFAULT_WS_ADDR, MAX_RETRIES, }; +use anyhow::Context; use clap::Parser; +use dotenv::dotenv; +use solana_lite_rpc_history::postgres::postgres_config::PostgresSessionConfig; #[derive(Parser, Debug, Clone)] #[command(author, version, about, long_about = None)] pub struct Args { - #[arg(short, long, default_value_t = String::from(DEFAULT_RPC_ADDR))] + /// config.json + #[arg(short, long)] + pub config: Option, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Config { + #[serde(default = "Config::default_rpc_addr")] pub rpc_addr: String, - #[arg(short, long, default_value_t = String::from(DEFAULT_WS_ADDR))] + #[serde(default = "Config::default_ws_addr")] pub ws_addr: String, - #[arg(short = 'l', long, default_value_t = String::from("[::]:8890"))] + #[serde(default = "Config::default_lite_rpc_http_addr")] pub lite_rpc_http_addr: String, - #[arg(short = 's', long, default_value_t = String::from("[::]:8891"))] + #[serde(default = "Config::default_lite_rpc_ws_addr")] pub lite_rpc_ws_addr: String, - /// tpu fanout - #[arg(short = 'f', long, default_value_t = DEFAULT_FANOUT_SIZE) ] + #[serde(default = "Config::default_fanout_size")] pub fanout_size: u64, - /// enable logging to postgres - #[arg(short = 'p', long)] - pub enable_postgres: bool, - /// enable metrics to prometheus at addr - #[arg(short = 'm', long, default_value_t = String::from("[::]:9091"))] + #[serde(default)] + pub identity_keypair: Option, + #[serde(default = "Config::default_prometheus_addr")] pub prometheus_addr: String, - #[arg(short = 'k', long, default_value_t = String::new())] - pub identity_keypair: String, - #[arg(long, default_value_t = MAX_RETRIES)] + #[serde(default = "Config::default_maximum_retries_per_tx")] pub maximum_retries_per_tx: usize, - #[arg(long, default_value_t = DEFAULT_RETRY_TIMEOUT)] + #[serde(default = "Config::default_transaction_retry_after_secs")] pub transaction_retry_after_secs: u64, - #[arg(long)] + #[serde(default)] pub quic_proxy_addr: Option, - #[arg(short = 'g', long)] + #[serde(default)] pub use_grpc: bool, - /// grpc address - #[arg(long, default_value_t = String::from(DEFAULT_GRPC_ADDR))] + #[serde(default = "Config::default_grpc_addr")] pub grpc_addr: String, - #[arg(long)] + #[serde(default)] pub grpc_x_token: Option, + /// postgres config + #[serde(default)] + pub postgres: Option, +} + +impl Config { + pub async fn load() -> anyhow::Result { + dotenv().ok(); + + let args = Args::parse(); + + let config_path = if args.config.is_some() { + args.config + } else { + let default_config_path = "config.json"; + + // check if config.json exists in current directory + if tokio::fs::metadata(default_config_path).await.is_err() { + None + } else { + Some(default_config_path.to_string()) + } + }; + + let config = if let Some(config_path) = config_path { + tokio::fs::read_to_string(config_path) + .await + .context("Error reading config file")? + } else { + "{}".to_string() + }; + + let mut config: Config = + serde_json::from_str(&config).context("Error parsing config file")?; + + config.rpc_addr = env::var("RPC_ADDR").unwrap_or(config.rpc_addr); + + config.ws_addr = env::var("WS_ADDR").unwrap_or(config.ws_addr); + + config.lite_rpc_http_addr = + env::var("LITE_RPC_HTTP_ADDR").unwrap_or(config.lite_rpc_http_addr); + + config.lite_rpc_ws_addr = env::var("LITE_RPC_WS_ADDR").unwrap_or(config.lite_rpc_ws_addr); + + config.fanout_size = env::var("FANOUT_SIZE") + .map(|size| size.parse().unwrap()) + .unwrap_or(config.fanout_size); + + config.identity_keypair = env::var("IDENTITY") + .map(Some) + .unwrap_or(config.identity_keypair); + + config.prometheus_addr = env::var("PROMETHEUS_ADDR").unwrap_or(config.prometheus_addr); + + config.maximum_retries_per_tx = env::var("MAX_RETRIES") + .map(|max| max.parse().unwrap()) + .unwrap_or(config.maximum_retries_per_tx); + + config.transaction_retry_after_secs = env::var("RETRY_TIMEOUT") + .map(|secs| secs.parse().unwrap()) + .unwrap_or(config.transaction_retry_after_secs); + + config.quic_proxy_addr = env::var("QUIC_PROXY_ADDR").ok(); + + config.use_grpc = env::var("USE_GRPC") + .map(|_| true) + .unwrap_or(config.use_grpc); + + config.grpc_addr = env::var("GRPC_ADDR").unwrap_or(config.grpc_addr); + + config.grpc_x_token = env::var("GRPC_X_TOKEN") + .map(Some) + .unwrap_or(config.grpc_x_token); + + config.postgres = PostgresSessionConfig::new_from_env()?.or(config.postgres); + + Ok(config) + } + + pub fn lite_rpc_ws_addr() -> String { + "[::]:8891".to_string() + } + + pub fn default_lite_rpc_http_addr() -> String { + "[::]:8890".to_string() + } + + pub fn default_rpc_addr() -> String { + DEFAULT_RPC_ADDR.to_string() + } + + pub fn default_ws_addr() -> String { + DEFAULT_WS_ADDR.to_string() + } + + pub fn default_lite_rpc_ws_addr() -> String { + "[::]:8891".to_string() + } + + pub const fn default_fanout_size() -> u64 { + DEFAULT_FANOUT_SIZE + } + + pub fn default_prometheus_addr() -> String { + "[::]:9091".to_string() + } + + pub const fn default_maximum_retries_per_tx() -> usize { + MAX_RETRIES + } + + pub const fn default_transaction_retry_after_secs() -> u64 { + DEFAULT_RETRY_TIMEOUT + } + + pub fn default_grpc_addr() -> String { + DEFAULT_GRPC_ADDR.to_string() + } } diff --git a/lite-rpc/src/main.rs b/lite-rpc/src/main.rs index e8b6e279..6d5e7666 100644 --- a/lite-rpc/src/main.rs +++ b/lite-rpc/src/main.rs @@ -3,13 +3,13 @@ pub mod rpc_tester; use std::time::Duration; use anyhow::bail; -use clap::Parser; use dashmap::DashMap; -use dotenv::dotenv; +use lite_rpc::bridge::LiteBridge; +use lite_rpc::cli::Config; use lite_rpc::postgres_logger::PostgresLogger; use lite_rpc::service_spawner::ServiceSpawner; -use lite_rpc::{bridge::LiteBridge, cli::Args}; use lite_rpc::{DEFAULT_MAX_NUMBER_OF_TXS_IN_QUEUE, GRPC_VERSION}; +use solana_lite_rpc_history::postgres::postgres_config::PostgresSessionConfig; use crate::rpc_tester::RpcTester; use solana_lite_rpc_cluster_endpoints::endpoint_stremers::EndpointStreaming; @@ -43,7 +43,6 @@ use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer; -use std::env; use std::net::{SocketAddr, ToSocketAddrs}; use std::sync::Arc; use tokio::sync::mpsc; @@ -61,9 +60,9 @@ async fn get_latest_block( } pub async fn start_postgres( - enable: bool, + config: Option, ) -> anyhow::Result<(Option, AnyhowJoinHandle)> { - if !enable { + let Some(config) = config else { return Ok(( None, tokio::spawn(async { @@ -71,22 +70,22 @@ pub async fn start_postgres( unreachable!() }), )); - } + }; let (postgres_send, postgres_recv) = mpsc::unbounded_channel(); - let postgres_session_cache = PostgresSessionCache::new().await?; + let postgres_session_cache = PostgresSessionCache::new(config).await?; let postgres = PostgresLogger::start(postgres_session_cache, postgres_recv); Ok((Some(postgres_send), postgres)) } -pub async fn start_lite_rpc(args: Args, rpc_client: Arc) -> anyhow::Result<()> { - let Args { +pub async fn start_lite_rpc(args: Config, rpc_client: Arc) -> anyhow::Result<()> { + let Config { lite_rpc_ws_addr, lite_rpc_http_addr, fanout_size, - enable_postgres, + postgres, prometheus_addr, identity_keypair, maximum_retries_per_tx, @@ -99,8 +98,8 @@ pub async fn start_lite_rpc(args: Args, rpc_client: Arc) -> anyhow::R } = args; let validator_identity = Arc::new( - load_identity_keypair(&identity_keypair) - .await + load_identity_keypair(identity_keypair) + .await? .unwrap_or_else(Keypair::new), ); @@ -158,7 +157,7 @@ pub async fn start_lite_rpc(args: Args, rpc_client: Arc) -> anyhow::R ); drop(blocks_notifier); - let (notification_channel, postgres) = start_postgres(enable_postgres).await?; + let (notification_channel, postgres) = start_postgres(postgres).await?; let tpu_config = TpuServiceConfig { fanout_slots: fanout_size, @@ -239,34 +238,19 @@ pub async fn start_lite_rpc(args: Args, rpc_client: Arc) -> anyhow::R } } -fn get_args() -> Args { - let mut args = Args::parse(); - - dotenv().ok(); - - args.enable_postgres = args.enable_postgres - || if let Ok(enable_postgres_env_var) = env::var("PG_ENABLED") { - enable_postgres_env_var != "false" - } else { - false - }; - - args -} - #[tokio::main(flavor = "multi_thread", worker_threads = 16)] pub async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); - let args = get_args(); + let config = Config::load().await?; let ctrl_c_signal = tokio::signal::ctrl_c(); - let Args { rpc_addr, .. } = &args; + let Config { rpc_addr, .. } = &config; // rpc client let rpc_client = Arc::new(RpcClient::new(rpc_addr.clone())); let rpc_tester = tokio::spawn(RpcTester::new(rpc_client.clone()).start()); - let main = start_lite_rpc(args.clone(), rpc_client); + let main = start_lite_rpc(config, rpc_client); tokio::select! { err = rpc_tester => { diff --git a/quic-forward-proxy/src/main.rs b/quic-forward-proxy/src/main.rs index 729fbd2c..022ed827 100644 --- a/quic-forward-proxy/src/main.rs +++ b/quic-forward-proxy/src/main.rs @@ -35,7 +35,8 @@ pub async fn main() -> anyhow::Result<()> { dotenv().ok(); let proxy_listener_addr = proxy_listen_addr.parse().unwrap(); - let validator_identity = ValidatorIdentity::new(load_identity_keypair(&identity_keypair).await); + let validator_identity = + ValidatorIdentity::new(load_identity_keypair(Some(identity_keypair)).await?); let tls_config = Arc::new(SelfSignedTlsConfigProvider::new_singleton_self_signed_localhost()); let main_services = QuicForwardProxy::new(proxy_listener_addr, tls_config, validator_identity)