diff --git a/integration_tests/tests/config.rs b/integration_tests/tests/config.rs index 9501551d5..5f0d27a18 100644 --- a/integration_tests/tests/config.rs +++ b/integration_tests/tests/config.rs @@ -6,8 +6,12 @@ use anyhow::Result; use integration_tests::TestContext; +use test_fixtures::{ + MultiZoneTestContextBuilder, ZoneTestContextBuilder, + config::{MultiNodeTestContextConfig, bedrock_channel_id}, +}; use tokio::test; -use wallet::cli::{Command, config::ConfigSubcommand}; +use wallet::cli::{Command, config::ConfigSubcommand, statistics::StatisticsSubcommand}; #[test] async fn modify_config_field() -> Result<()> { @@ -36,3 +40,62 @@ async fn modify_config_field() -> Result<()> { Ok(()) } + +#[test] +async fn modify_config_field_multiseq() -> Result<()> { + let mut ctx = MultiZoneTestContextBuilder::default() + .with_zone(ZoneTestContextBuilder::new(MultiNodeTestContextConfig { + num_nodes: 2, + bedrock_channel: bedrock_channel_id(), + })) + .build() + .await?; + + // Default config have callibration limit and distribution limit as 1 + // Modifying them + let wallet_mut = ctx.wallet_mut(); + + let command = Command::Config(ConfigSubcommand::Set { + key: "distribution_limit".to_owned(), + value: "2".to_owned(), + }); + wallet::cli::execute_subcommand(wallet_mut, command).await?; + + let command = Command::Config(ConfigSubcommand::Set { + key: "calibration_limit".to_owned(), + value: "10".to_owned(), + }); + wallet::cli::execute_subcommand(wallet_mut, command).await?; + + // Check config correctness + assert_eq!( + wallet_mut + .config() + .multi_sequencer_client_config + .calibration_limit, + 10 + ); + assert_eq!( + wallet_mut + .config() + .multi_sequencer_client_config + .distribution_limit, + 2 + ); + + // Rotate clients to callibrate the other one + let command = Command::Statistics(StatisticsSubcommand::ExecuteRotation); + wallet::cli::execute_subcommand(wallet_mut, command).await?; + + // After that, there must be two leaders + let leaders = wallet_mut.leaders(); + assert_eq!(leaders.len(), 2); + + // And both of them must have similar statistics + let first_stat = wallet_mut.get_statistics(&leaders[0].1).unwrap(); + let second_stat = wallet_mut.get_statistics(&leaders[1].1).unwrap(); + + assert_eq!(first_stat.latest_block_id, second_stat.latest_block_id); + + Ok(()) +} diff --git a/lez/wallet/src/cli/config.rs b/lez/wallet/src/cli/config.rs index 8c8c4b5c7..8b7a192b5 100644 --- a/lez/wallet/src/cli/config.rs +++ b/lez/wallet/src/cli/config.rs @@ -1,9 +1,11 @@ use anyhow::Result; use clap::Subcommand; +use common::config::BasicAuth; use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, + config::SequencerConnectionData, }; /// Represents generic config CLI subcommand. @@ -21,6 +23,14 @@ pub enum ConfigSubcommand { Set { key: String, value: String }, /// Prints description of corresponding field. Description { key: String }, + /// Adds a new sequencer to the list. + AddSequencer { + addr: String, + user: Option, + password: Option, + }, + /// Remove sequencer from a list. + RemoveSequencer { addr: String }, } impl ConfigSubcommand { @@ -51,6 +61,15 @@ impl ConfigSubcommand { "seq_block_poll_max_amount" => { println!("{}", config.seq_block_poll_max_amount); } + "distribution_limit" => { + println!( + "{}", + config.multi_sequencer_client_config.distribution_limit + ); + } + "calibration_limit" => { + println!("{}", config.multi_sequencer_client_config.calibration_limit); + } _ => { println!("Unknown field"); } @@ -69,6 +88,9 @@ impl ConfigSubcommand { ) -> Result { let mut config = wallet_core.config().clone(); match key.as_str() { + "sequencers" => { + anyhow::bail!("Not settable via this method, use add-sequencer subcommand"); + } "seq_poll_timeout" => { config.seq_poll_timeout = humantime::parse_duration(&value) .map_err(|e| anyhow::anyhow!("Invalid duration: {e}"))?; @@ -82,8 +104,11 @@ impl ConfigSubcommand { "seq_block_poll_max_amount" => { config.seq_block_poll_max_amount = value.parse()?; } - "initial_accounts" => { - anyhow::bail!("Setting this field from wallet is not supported"); + "distribution_limit" => { + config.multi_sequencer_client_config.distribution_limit = value.parse()?; + } + "calibration_limit" => { + config.multi_sequencer_client_config.calibration_limit = value.parse()?; } _ => { anyhow::bail!("Unknown field"); @@ -101,8 +126,8 @@ impl ConfigSubcommand { "override_rust_log" => { println!("Value of variable RUST_LOG to override, affects logging"); } - "sequencer_addr" => { - println!("HTTP V4 account_id of sequencer"); + "sequencer" => { + println!("A list of HTTP V4 addresses of sequencer, with authorization"); } "seq_poll_timeout" => { println!( @@ -124,11 +149,15 @@ impl ConfigSubcommand { "Sequencer client polling variable: max number of blocks to request in one polling call" ); } - "initial_accounts" => { - println!("List of initial accounts' keys(both public and private)"); + "distribution_limit" => { + println!( + "Sequencer multi node variable: max number of nodes to distribute transaction(can not be zero)" + ); } - "basic_auth" => { - println!("Basic authentication credentials for sequencer HTTP requests"); + "calibration_limit" => { + println!( + "Sequencer multi node variable: max number of callibration runs before the end of handshake(can not be zero)" + ); } _ => { println!("Unknown field"); @@ -148,6 +177,50 @@ impl WalletSubcommand for ConfigSubcommand { Self::Get { all, key } => Self::handle_get(all, key, wallet_core), Self::Set { key, value } => Self::handle_set(key, value, wallet_core).await, Self::Description { key } => Ok(Self::handle_description(&key, wallet_core)), + Self::AddSequencer { + addr, + user, + password, + } => { + let url_addr = addr.parse()?; + + let basic_auth = user.map(|user| { + let mut basic_auth = BasicAuth { + username: user, + password: None, + }; + + if password.is_some() { + basic_auth.password = password; + } + + basic_auth + }); + + let seq_connection_data = SequencerConnectionData { + sequencer_addr: url_addr, + basic_auth, + }; + + wallet_core.config.sequencers.push(seq_connection_data); + + Ok(SubcommandReturnValue::Empty) + } + Self::RemoveSequencer { addr } => { + let url_addr = addr.parse()?; + + let (idx, _) = wallet_core + .config + .sequencers + .iter() + .enumerate() + .find(|(_, conn_data)| conn_data.sequencer_addr == url_addr) + .ok_or_else(|| anyhow::anyhow!("Sequencer with this addr is not found"))?; + + wallet_core.config.sequencers.remove(idx); + + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/lez/wallet/src/cli/mod.rs b/lez/wallet/src/cli/mod.rs index 26653b61b..191db9001 100644 --- a/lez/wallet/src/cli/mod.rs +++ b/lez/wallet/src/cli/mod.rs @@ -25,6 +25,7 @@ use crate::{ native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand, vault::VaultSubcommand, }, + statistics::StatisticsSubcommand, }, config::SequencerConnectionData, storage::Storage, @@ -37,6 +38,7 @@ pub mod group; pub mod keycard; pub mod network; pub mod programs; +pub mod statistics; pub(crate) trait WalletSubcommand { async fn handle_subcommand(self, wallet_core: &mut WalletCore) @@ -101,6 +103,9 @@ pub enum Command { /// Keycard hardware wallet management. #[command(subcommand)] Keycard(KeycardSubcommand), + /// Metrics management. + #[command(subcommand)] + Statistics(StatisticsSubcommand), } /// To execute commands, env var `LEE_WALLET_HOME_DIR` must be set into directory with config. @@ -320,6 +325,9 @@ pub async fn execute_subcommand( .await .context("Transaction finalization error")? } + Command::Statistics(statistics_subcommand) => { + statistics_subcommand.handle_subcommand(wallet_core).await? + } }; // Kind of a sledgehammer solution, but it is not clear if there is the case to not store diff --git a/lez/wallet/src/cli/statistics.rs b/lez/wallet/src/cli/statistics.rs new file mode 100644 index 000000000..02f1ccecd --- /dev/null +++ b/lez/wallet/src/cli/statistics.rs @@ -0,0 +1,85 @@ +use anyhow::Result; +use clap::Subcommand; + +use crate::{ + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, + config::SequencerConnectionData, + multi_client::{calibrate_client, make_subclient}, +}; + +/// Represents generic config CLI subcommand. +#[derive(Subcommand, Debug, Clone)] +pub enum StatisticsSubcommand { + /// Show the list of the current leaders. + ShowLeaders, + /// Execute client list rotation, applies all statistics, the re-chooses the leaders. + ExecuteRotation, + /// (Re)callibrate the client. + Callibrate { addr: String }, + /// Shpw the statistics of the client. + ShowStatistics { addr: String }, +} + +impl WalletSubcommand for StatisticsSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + Self::ShowLeaders => { + let leader_urls = wallet_core + .leaders() + .iter() + .map(|(_, url)| url) + .collect::>(); + + println!("Leader URLs is {leader_urls:?}"); + + Ok(SubcommandReturnValue::Empty) + } + Self::ExecuteRotation => { + wallet_core.client_rotation().await?; + + Ok(SubcommandReturnValue::Empty) + } + Self::Callibrate { addr } => { + let url_addr = addr.parse()?; + let calibration_limit = wallet_core + .config() + .multi_sequencer_client_config + .calibration_limit; + let SequencerConnectionData { + sequencer_addr, + basic_auth, + } = wallet_core + .config() + .sequencers + .iter() + .find(|conn_data| conn_data.sequencer_addr == url_addr) + .ok_or_else(|| { + anyhow::anyhow!("Sequencer with this addr was not found in config") + })?; + let client = make_subclient(sequencer_addr, basic_auth)?; + + let statistics = calibrate_client(client, calibration_limit) + .await + .ok_or_else(|| anyhow::anyhow!("Failed to callibrate the sequencer"))?; + + wallet_core.statistics.insert(url_addr, statistics); + + Ok(SubcommandReturnValue::Empty) + } + Self::ShowStatistics { addr } => { + let url_addr = addr.parse()?; + + println!( + "Statistics of a {url_addr:?} is {:?}", + wallet_core.get_statistics(&url_addr) + ); + + Ok(SubcommandReturnValue::Empty) + } + } + } +} diff --git a/lez/wallet/src/multi_client.rs b/lez/wallet/src/multi_client.rs index 49c6bbda1..ad0cb0930 100644 --- a/lez/wallet/src/multi_client.rs +++ b/lez/wallet/src/multi_client.rs @@ -10,7 +10,7 @@ use std::{collections::HashMap, path::Path, sync::Arc}; use anyhow::{Context as _, Result}; -use common::{HashType, transaction::LeeTransaction}; +use common::{HashType, config::BasicAuth, transaction::LeeTransaction}; use itertools::Itertools as _; use lee_core::BlockId; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; @@ -115,23 +115,7 @@ impl MultiSequencerClient { basic_auth, } in conn_data { - let sequencer_client = { - let mut builder = SequencerClientBuilder::default(); - if let Some(basic_auth) = &basic_auth { - builder = builder.set_headers( - std::iter::once(( - "Authorization".parse().expect("Header name is valid"), - format!("Basic {basic_auth}") - .parse() - .context("Invalid basic auth format")?, - )) - .collect(), - ); - } - builder - .build(sequencer_addr) - .context("Failed to create sequencer client")? - }; + let sequencer_client = make_subclient(sequencer_addr, basic_auth)?; if statistics.contains_key(sequencer_addr) { actualization_list.push((sequencer_addr.clone(), sequencer_client.clone())); @@ -472,9 +456,33 @@ async fn measure_request_duration(client: &SequencerClient) -> (u128, Option, +) -> Result { + let mut builder = SequencerClientBuilder::default(); + if let Some(basic_auth) = &basic_auth { + builder = builder.set_headers( + std::iter::once(( + "Authorization".parse().expect("Header name is valid"), + format!("Basic {basic_auth}") + .parse() + .context("Invalid basic auth format")?, + )) + .collect(), + ); + } + builder + .build(sequencer_addr) + .context("Failed to create sequencer client") +} + /// Calibrate statistics for one client. Takes `client` by value deliberately, cloning /// `SequencerClient` is cheap. -async fn calibrate_client(client: SequencerClient, calibration_limit: usize) -> Option { +pub async fn calibrate_client( + client: SequencerClient, + calibration_limit: usize, +) -> Option { let mut latencies = vec![]; let mut latest_block_id = 0; let mut errors: u64 = 0;