diff --git a/Cargo.lock b/Cargo.lock index 42088f0..b1b0ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "aquamarine" @@ -7132,11 +7132,11 @@ dependencies = [ "kube", "log", "rand 0.8.5", - "rayon", "reqwest", "rustc_version", "rustls", "solana-core", + "solana-ledger", "solana-logger", "solana-sdk", "strum 0.26.2", diff --git a/Cargo.toml b/Cargo.toml index 5484393..1443be6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,10 @@ k8s-openapi ={ version = "0.20.0", features = ["v1_28"] } kube = "0.87.2" log = "0.4.21" rand = "0.8.5" -rayon = "1.9.0" reqwest = { version = "0.11.23", features = ["blocking", "brotli", "deflate", "gzip", "rustls-tls", "json"] } rustls = { version = "0.21.11", default-features = false, features = ["quic"] } solana-core = "1.18.8" +solana-ledger = "1.18.8" solana-logger = "1.18.8" solana-sdk = "1.18.8" strum = "0.26.2" diff --git a/src/library.rs b/src/cluster_images.rs similarity index 55% rename from src/library.rs rename to src/cluster_images.rs index 3b66931..4c779ee 100644 --- a/src/library.rs +++ b/src/cluster_images.rs @@ -9,36 +9,33 @@ use { // 3) RPC Node -> One image for each RPC node (not implemented yet) // 4) Clients -> Each client has its own image (not implemented yet) -pub struct Library { - validators: Vec>, - _clients: Option>, +#[derive(Default)] +pub struct ClusterImages { + bootstrap: Option, + _validator: Option, + _rpc: Option, + _clients: Vec, } -impl Default for Library { - fn default() -> Self { - Self { - validators: vec![None; 1], - _clients: None, - } - } -} - -impl Library { +impl ClusterImages { pub fn set_item(&mut self, item: Validator, validator_type: ValidatorType) { match validator_type { - ValidatorType::Bootstrap => self.validators[0] = Some(item), + ValidatorType::Bootstrap => self.bootstrap = Some(item), _ => panic!("{validator_type} not implemented yet!"), } } pub fn bootstrap(&mut self) -> Result<&mut Validator, Box> { - self.validators - .get_mut(0) - .and_then(Option::as_mut) - .ok_or_else(|| "No Bootstrap validator found.".to_string().into()) + self.bootstrap + .as_mut() + .ok_or_else(|| "Bootstrap validator is not available".into()) } pub fn get_validators(&self) -> impl Iterator { - self.validators.iter().filter_map(Option::as_ref) + self.bootstrap + .iter() + .chain(self._validator.iter()) + .chain(self._rpc.iter()) + .filter_map(Some) } } diff --git a/src/docker.rs b/src/docker.rs index 8469032..1bbc502 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -1,16 +1,15 @@ use { crate::{ new_spinner_progress_bar, release::DeployMethod, startup_scripts::StartupScripts, - validator::Validator, DockerPushThreadError, ValidatorType, BUILD, ROCKET, + validator::Validator, ValidatorType, BUILD, ROCKET, }, log::*, - rayon::prelude::*, std::{ error::Error, fmt::{self, Display, Formatter}, fs, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::{Child, Command, Stdio}, }, }; @@ -198,40 +197,38 @@ WORKDIR /home/solana Ok(()) } - pub fn push_image(docker_image: &DockerImage) -> Result<(), Box> { - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message(format!( - "{ROCKET}Pushing {docker_image} image to registry...", - )); + pub fn push_image(docker_image: &DockerImage) -> Result> { let command = format!("docker push '{docker_image}'"); - let output = Command::new("sh") + let child = Command::new("sh") .arg("-c") .arg(&command) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn() - .expect("Failed to execute command") - .wait_with_output() - .expect("Failed to push image"); + .spawn()?; - if !output.status.success() { - return Err(Box::new(DockerPushThreadError::from( - output.status.to_string(), - ))); - } - progress_bar.finish_and_clear(); - Ok(()) + Ok(child) } - pub fn push_images<'a, I>(&self, validators: I) -> Result<(), Box> + pub fn push_images<'a, I>(&self, validators: I) -> Result<(), Box> where I: IntoIterator, { info!("Pushing images..."); - validators + let children: Result, _> = validators .into_iter() - .collect::>() // Collect into Vec and thread push - .par_iter() - .try_for_each(|validator| Self::push_image(validator.image())) + .map(|validator| Self::push_image(validator.image())) + .collect(); + + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(format!("{ROCKET}Pushing images to registry...")); + for child in children? { + let output = child.wait_with_output()?; + if !output.status.success() { + return Err(output.status.to_string().into()); + } + } + progress_bar.finish_and_clear(); + + Ok(()) } } diff --git a/src/k8s_helpers.rs b/src/k8s_helpers.rs index 9041678..950ea97 100644 --- a/src/k8s_helpers.rs +++ b/src/k8s_helpers.rs @@ -1,11 +1,18 @@ use { - k8s_openapi::{api::core::v1::Secret, ByteString}, - kube::api::ObjectMeta, - std::{ - collections::{BTreeMap, HashMap}, - error::Error, - path::PathBuf, + crate::{docker::DockerImage, ValidatorType}, + k8s_openapi::{ + api::{ + apps::v1::{ReplicaSet, ReplicaSetSpec}, + core::v1::{ + Container, EnvVar, PodSecurityContext, PodSpec, PodTemplateSpec, Probe, + ResourceRequirements, Secret, Volume, VolumeMount, + }, + }, + apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::LabelSelector}, + ByteString, }, + kube::api::ObjectMeta, + std::{collections::BTreeMap, error::Error, path::PathBuf}, }; pub enum SecretType { @@ -26,21 +33,20 @@ fn build_secret(name: &str, data: BTreeMap) -> Secret { pub fn create_secret( secret_name: &str, - secrets: HashMap, + secrets: BTreeMap, ) -> Result> { - let mut data: BTreeMap = BTreeMap::new(); - for (label, value) in secrets { - match value { - SecretType::Value { v } => { - data.insert(label, ByteString(v.into_bytes())); - } + let data = secrets + .into_iter() + .map(|(label, value)| match value { + SecretType::Value { v } => Ok((label, ByteString(v.into_bytes()))), SecretType::File { path } => { - let file_content = std::fs::read(&path) + let content = std::fs::read(&path) .map_err(|err| format!("Failed to read file '{:?}': {}", path, err))?; - data.insert(label, ByteString(file_content)); + Ok((label, ByteString(content))) } - } - } + }) + .collect::, Box>>()?; + Ok(build_secret(secret_name, data)) } @@ -49,3 +55,67 @@ pub fn create_selector(key: &str, value: &str) -> BTreeMap { btree.insert(key.to_string(), value.to_string()); btree } + +#[allow(clippy::too_many_arguments)] +pub fn create_replica_set( + name: ValidatorType, + namespace: String, + label_selector: BTreeMap, + image_name: DockerImage, + environment_variables: Vec, + command: Vec, + volumes: Option>, + volume_mounts: Option>, + pod_requests: BTreeMap, + readiness_probe: Option, +) -> Result> { + let pod_spec = PodTemplateSpec { + metadata: Some(ObjectMeta { + labels: Some(label_selector.clone()), + ..Default::default() + }), + spec: Some(PodSpec { + containers: vec![Container { + name: format!("{}-container", image_name.validator_type()), + image: Some(image_name.to_string()), + image_pull_policy: Some("Always".to_string()), + env: Some(environment_variables), + command: Some(command), + volume_mounts, + readiness_probe, + resources: Some(ResourceRequirements { + requests: Some(pod_requests), + ..Default::default() + }), + ..Default::default() + }], + volumes, + security_context: Some(PodSecurityContext { + run_as_user: Some(1000), + run_as_group: Some(1000), + ..Default::default() + }), + ..Default::default() + }), + }; + + let replicas_set_spec = ReplicaSetSpec { + replicas: Some(1), + selector: LabelSelector { + match_labels: Some(label_selector), + ..Default::default() + }, + template: Some(pod_spec), + ..Default::default() + }; + + Ok(ReplicaSet { + metadata: ObjectMeta { + name: Some(format!("{name}-replicaset")), + namespace: Some(namespace), + ..Default::default() + }, + spec: Some(replicas_set_spec), + ..Default::default() + }) +} diff --git a/src/kubernetes.rs b/src/kubernetes.rs index f15fcb5..4c43c18 100644 --- a/src/kubernetes.rs +++ b/src/kubernetes.rs @@ -1,27 +1,63 @@ use { - crate::k8s_helpers::{self, SecretType}, - k8s_openapi::api::core::v1::{Namespace, Secret}, + crate::{ + docker::DockerImage, + k8s_helpers::{self, SecretType}, + validator_config::ValidatorConfig, + ValidatorType, + }, + k8s_openapi::{ + api::{ + apps::v1::ReplicaSet, + core::v1::{ + EnvVar, EnvVarSource, Namespace, ObjectFieldSelector, Secret, SecretVolumeSource, + Volume, VolumeMount, + }, + }, + apimachinery::pkg::api::resource::Quantity, + }, kube::{ api::{Api, ListParams, PostParams}, Client, }, - std::{ - collections::{BTreeMap, HashMap}, - error::Error, - path::Path, - }, + log::*, + solana_sdk::{pubkey::Pubkey, signature::keypair::read_keypair_file, signer::Signer}, + std::{collections::BTreeMap, error::Error, path::Path}, }; -pub struct Kubernetes { +#[derive(Debug, Clone)] +pub struct PodRequests { + requests: BTreeMap, +} + +impl PodRequests { + pub fn new(cpu_requests: String, memory_requests: String) -> PodRequests { + PodRequests { + requests: BTreeMap::from([ + ("cpu".to_string(), Quantity(cpu_requests)), + ("memory".to_string(), Quantity(memory_requests)), + ]), + } + } +} + +pub struct Kubernetes<'a> { k8s_client: Client, namespace: String, + validator_config: &'a mut ValidatorConfig, + pod_requests: PodRequests, } -impl Kubernetes { - pub async fn new(namespace: &str) -> Kubernetes { +impl<'a> Kubernetes<'a> { + pub async fn new( + namespace: &str, + validator_config: &'a mut ValidatorConfig, + pod_requests: PodRequests, + ) -> Kubernetes<'a> { Self { k8s_client: Client::try_default().await.unwrap(), namespace: namespace.to_owned(), + validator_config, + pod_requests, } } @@ -38,7 +74,7 @@ impl Kubernetes { } pub fn create_bootstrap_secret( - &self, + &mut self, secret_name: &str, config_dir: &Path, ) -> Result> { @@ -47,7 +83,11 @@ impl Kubernetes { let vote_key_path = config_dir.join("bootstrap-validator/vote-account.json"); let stake_key_path = config_dir.join("bootstrap-validator/stake-account.json"); - let mut secrets = HashMap::new(); + let bootstrap_keypair = read_keypair_file(&identity_key_path) + .expect("Failed to read bootstrap validator keypair file"); + self.add_known_validator(bootstrap_keypair.pubkey()); + + let mut secrets = BTreeMap::new(); secrets.insert( "faucet".to_string(), SecretType::File { @@ -76,12 +116,96 @@ impl Kubernetes { k8s_helpers::create_secret(secret_name, secrets) } + fn add_known_validator(&mut self, pubkey: Pubkey) { + self.validator_config.known_validators.push(pubkey); + info!("pubkey added to known validators: {:?}", pubkey); + } + pub async fn deploy_secret(&self, secret: &Secret) -> Result { let secrets_api: Api = Api::namespaced(self.k8s_client.clone(), self.namespace.as_str()); secrets_api.create(&PostParams::default(), secret).await } + pub fn create_bootstrap_validator_replica_set( + &mut self, + image_name: &DockerImage, + secret_name: Option, + label_selector: &BTreeMap, + ) -> Result> { + let env_vars = vec![EnvVar { + name: "MY_POD_IP".to_string(), + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + field_path: "status.podIP".to_string(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }]; + + let accounts_volume = Some(vec![Volume { + name: "bootstrap-accounts-volume".into(), + secret: Some(SecretVolumeSource { + secret_name, + ..Default::default() + }), + ..Default::default() + }]); + + let accounts_volume_mount = Some(vec![VolumeMount { + name: "bootstrap-accounts-volume".to_string(), + mount_path: "/home/solana/bootstrap-accounts".to_string(), + ..Default::default() + }]); + + let mut command = + vec!["/home/solana/k8s-cluster-scripts/bootstrap-startup-script.sh".to_string()]; + command.extend(self.generate_bootstrap_command_flags()); + + k8s_helpers::create_replica_set( + ValidatorType::Bootstrap, + self.namespace.clone(), + label_selector.clone(), + image_name.clone(), + env_vars, + command.clone(), + accounts_volume, + accounts_volume_mount, + self.pod_requests.requests.clone(), + None, + ) + } + + fn generate_command_flags(&self, flags: &mut Vec) { + if self.validator_config.skip_poh_verify { + flags.push("--skip-poh-verify".to_string()); + } + if self.validator_config.no_snapshot_fetch { + flags.push("--no-snapshot-fetch".to_string()); + } + if self.validator_config.require_tower { + flags.push("--require-tower".to_string()); + } + if self.validator_config.enable_full_rpc { + flags.push("--enable-rpc-transaction-history".to_string()); + flags.push("--enable-extended-tx-metadata-storage".to_string()); + } + + if let Some(limit_ledger_size) = self.validator_config.max_ledger_size { + flags.push("--limit-ledger-size".to_string()); + flags.push(limit_ledger_size.to_string()); + } + } + + fn generate_bootstrap_command_flags(&self) -> Vec { + let mut flags: Vec = Vec::new(); + self.generate_command_flags(&mut flags); + + flags + } + pub fn create_selector(&self, key: &str, value: &str) -> BTreeMap { k8s_helpers::create_selector(key, value) } diff --git a/src/lib.rs b/src/lib.rs index f1f1059..101aa1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,6 @@ use { log::*, reqwest::Client, std::{ - error::Error, - fmt::{self, Display, Formatter}, fs::File, io::{BufReader, Cursor, Read, Write}, path::{Path, PathBuf}, @@ -58,41 +56,15 @@ pub enum ValidatorType { Client, } -#[derive(Debug)] -struct DockerPushThreadError { - message: String, -} - -impl Display for DockerPushThreadError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -impl Error for DockerPushThreadError {} - -impl From for DockerPushThreadError { - fn from(message: String) -> Self { - DockerPushThreadError { message } - } -} - -impl From<&str> for DockerPushThreadError { - fn from(message: &str) -> Self { - DockerPushThreadError { - message: message.to_string(), - } - } -} - +pub mod cluster_images; pub mod docker; pub mod genesis; pub mod k8s_helpers; pub mod kubernetes; -pub mod library; pub mod release; pub mod startup_scripts; pub mod validator; +pub mod validator_config; static BUILD: Emoji = Emoji("👷 ", ""); static PACKAGE: Emoji = Emoji("📦 ", ""); @@ -177,18 +149,19 @@ async fn fetch_program( version: &str, solana_root_path: &Path, ) -> Result<(), Box> { - let so_filename = format!("spl_{}-{}.so", name.replace('-', "_"), version); - let so_path = solana_root_path.join(&so_filename); - let so_name = format!("spl_{}.so", name.replace('-', "_")); + let name_with_underscores = name.replace('-', "_"); + let so_filename = format!("spl_{name_with_underscores}-{version}.so"); + let download_path = solana_root_path.join(&so_filename); + let so_name = format!("spl_{name_with_underscores}.so"); - if !so_path.exists() { - info!("Downloading {} {}", name, version); + if !download_path.exists() { + info!("Downloading {name} {version}"); let url = format!( "https://github.com/solana-labs/solana-program-library/releases/download/{}-v{}/{}", name, version, so_name ); - download_to_temp(&url, &so_path) + download_to_temp(&url, &download_path) .await .map_err(|err| format!("Unable to download {url}. Error: {err}"))?; } @@ -208,7 +181,7 @@ pub async fn fetch_spl(solana_root_path: &Path) -> Result<(), Box clap::ArgMatches { .default_value("latest") .help("Docker image tag."), ) + // Bootstrap/Validator Config + .arg( + Arg::with_name("limit_ledger_size") + .long("limit-ledger-size") + .takes_value(true) + .default_value(&DEFAULT_MAX_LEDGER_SHREDS.to_string()) + .help("Validator Config. The `--limit-ledger-size` parameter allows you to specify how many ledger + shreds your node retains on disk. If you do not + include this parameter, the validator will keep the entire ledger until it runs + out of disk space. The default value attempts to keep the ledger disk usage + under 500GB. More or less disk usage may be requested by adding an argument to + `--limit-ledger-size` if desired. Check `agave-validator --help` for the + default limit value used by `--limit-ledger-size`. More information about + selecting a custom limit value is at : https://github.com/solana-labs/solana/blob/583cec922b6107e0f85c7e14cb5e642bc7dfb340/core/src/ledger_cleanup_service.rs#L15-L26"), + ) + .arg( + Arg::with_name("skip_poh_verify") + .long("skip-poh-verify") + .help("Validator config. If set, validators will skip verifying + the ledger they already have saved to disk at + boot (results in a much faster boot)"), + ) + .arg( + Arg::with_name("no_snapshot_fetch") + .long("no-snapshot-fetch") + .help("Validator config. If set, disables booting validators from a snapshot"), + ) + .arg( + Arg::with_name("require_tower") + .long("require-tower") + .help("Validator config. Refuse to start if saved tower state is not found. + Off by default since validator won't restart if the pod restarts"), + ) + .arg( + Arg::with_name("enable_full_rpc") + .long("full-rpc") + .help("Validator config. Support full RPC services on all nodes"), + ) + // kubernetes config + .arg( + Arg::with_name("cpu_requests") + .long("cpu-requests") + .takes_value(true) + .default_value("20") // 20 cores + .help("Kubernetes pod config. Specify minimum CPUs required for deploying validator. + can use millicore notation as well. e.g. 500m (500 millicores) == 0.5 and is equivalent to half a core. + [default: 20]"), + ) + .arg( + Arg::with_name("memory_requests") + .long("memory-requests") + .takes_value(true) + .default_value("70Gi") // 70 Gibibytes + .help("Kubernetes pod config. Specify minimum memory required for deploying validator. + Can specify unit here (B, Ki, Mi, Gi, Ti) for bytes, kilobytes, etc (2^N notation) + e.g. 1Gi == 1024Mi == 1024Ki == 1,047,576B. [default: 70Gi]"), + ) .get_matches() } @@ -214,22 +275,6 @@ async fn main() { ); } - let kub_controller = Kubernetes::new(environment_config.namespace).await; - match kub_controller.namespace_exists().await { - Ok(true) => (), - Ok(false) => { - error!( - "Namespace: '{}' doesn't exist. Exiting...", - environment_config.namespace - ); - return; - } - Err(err) => { - error!("Error: {err}"); - return; - } - } - let build_config = BuildConfig::new( deploy_method.clone(), build_type, @@ -287,6 +332,50 @@ async fn main() { ), }; + let limit_ledger_size = value_t_or_exit!(matches, "limit_ledger_size", u64); + let mut validator_config = ValidatorConfig { + max_ledger_size: if limit_ledger_size < DEFAULT_MIN_MAX_LEDGER_SHREDS { + clap::Error::with_description( + format!("The provided --limit-ledger-size value was too small, the minimum value is {DEFAULT_MIN_MAX_LEDGER_SHREDS}"), + clap::ErrorKind::ArgumentNotFound, + ) + .exit(); + } else { + Some(limit_ledger_size) + }, + skip_poh_verify: matches.is_present("skip_poh_verify"), + no_snapshot_fetch: matches.is_present("no_snapshot_fetch"), + require_tower: matches.is_present("require_tower"), + enable_full_rpc: matches.is_present("enable_full_rpc"), + known_validators: vec![], + }; + + let pod_requests = PodRequests::new( + matches.value_of("cpu_requests").unwrap().to_string(), + matches.value_of("memory_requests").unwrap().to_string(), + ); + + let mut kub_controller = Kubernetes::new( + environment_config.namespace, + &mut validator_config, + pod_requests, + ) + .await; + match kub_controller.namespace_exists().await { + Ok(true) => (), + Ok(false) => { + error!( + "Namespace: '{}' doesn't exist. Exiting...", + environment_config.namespace + ); + return; + } + Err(err) => { + error!("Error: {err}"); + return; + } + } + match build_config.prepare().await { Ok(_) => info!("Validator setup prepared successfully"), Err(err) => { @@ -339,7 +428,7 @@ async fn main() { .unwrap_or_default() .to_string(); - let mut validator_library = Library::default(); + let mut cluster_images = ClusterImages::default(); let bootstrap_validator = Validator::new(DockerImage::new( registry_name.clone(), @@ -347,10 +436,10 @@ async fn main() { image_name.clone(), image_tag.clone(), )); - validator_library.set_item(bootstrap_validator, ValidatorType::Bootstrap); + cluster_images.set_item(bootstrap_validator, ValidatorType::Bootstrap); if build_config.docker_build() { - for v in validator_library.get_validators() { + for v in cluster_images.get_validators() { match docker.build_image(solana_root.get_root_path(), v.image()) { Ok(_) => info!("{} image built successfully", v.validator_type()), Err(err) => { @@ -360,7 +449,7 @@ async fn main() { } } - match docker.push_images(validator_library.get_validators()) { + match docker.push_images(cluster_images.get_validators()) { Ok(_) => info!("Validator images pushed successfully"), Err(err) => { error!("Failed to push Validator docker image {err}"); @@ -369,7 +458,7 @@ async fn main() { } } - let bootstrap_validator = validator_library.bootstrap().expect("should be bootstrap"); + let bootstrap_validator = cluster_images.bootstrap().expect("should be bootstrap"); match kub_controller.create_bootstrap_secret("bootstrap-accounts-secret", &config_directory) { Ok(secret) => bootstrap_validator.set_secret(secret), Err(err) => { @@ -389,12 +478,12 @@ async fn main() { } } - // Create bootstrap labels + // Create Bootstrap labels + // Bootstrap needs two labels, one for each service. + // One for Load Balancer, one direct let identity_path = config_directory.join("bootstrap-validator/identity.json"); let bootstrap_keypair = read_keypair_file(identity_path).expect("Failed to read bootstrap keypair file"); - // Bootstrap needs two labels. It will have two services. - // One for Load Balancer, one direct bootstrap_validator.add_label( "load-balancer/name", "load-balancer-selector", @@ -411,4 +500,17 @@ async fn main() { bootstrap_keypair.pubkey().to_string(), LabelType::Info, ); + + // create bootstrap replica set + match kub_controller.create_bootstrap_validator_replica_set( + bootstrap_validator.image(), + bootstrap_validator.secret().metadata.name.clone(), + bootstrap_validator.replica_set_labels(), + ) { + Ok(replica_set) => bootstrap_validator.set_replica_set(replica_set), + Err(err) => { + error!("Error creating bootstrap validator replicas_set: {err}"); + return; + } + }; } diff --git a/src/startup_scripts.rs b/src/startup_scripts.rs index b00b1bb..b6e599f 100644 --- a/src/startup_scripts.rs +++ b/src/startup_scripts.rs @@ -75,12 +75,6 @@ while [[ -n $1 ]]; do elif [[ $1 = --enable-rpc-bigtable-ledger-storage ]]; then args+=("$1") shift - elif [[ $1 = --tpu-disable-quic ]]; then - args+=("$1") - shift - elif [[ $1 = --tpu-enable-udp ]]; then - args+=("$1") - shift elif [[ $1 = --rpc-send-batch-ms ]]; then # not enabled in net.sh args+=("$1" "$2") shift 2 diff --git a/src/validator_config.rs b/src/validator_config.rs new file mode 100644 index 0000000..900a32b --- /dev/null +++ b/src/validator_config.rs @@ -0,0 +1,11 @@ +use solana_sdk::pubkey::Pubkey; + +#[derive(Debug)] +pub struct ValidatorConfig { + pub max_ledger_size: Option, + pub skip_poh_verify: bool, + pub no_snapshot_fetch: bool, + pub require_tower: bool, + pub enable_full_rpc: bool, + pub known_validators: Vec, +}