Skip to main content

agave_validator/commands/run/
execute.rs

1use {
2    crate::{
3        admin_rpc_service::{self, StakedNodesOverrides, load_staked_nodes_overrides},
4        bootstrap,
5        cli::{self},
6        commands::{FromClapArgMatches, run::args::RunArgs},
7        ledger_lockfile, lock_ledger,
8    },
9    agave_snapshots::{
10        ArchiveFormat, SnapshotInterval, SnapshotVersion,
11        paths::BANK_SNAPSHOTS_DIR,
12        snapshot_config::{SnapshotConfig, SnapshotUsage},
13    },
14    agave_votor::vote_history_storage,
15    agave_votor_transport::MAX_ENDPOINTS,
16    bytesize::ByteSize,
17    clap::{ArgMatches, crate_name, value_t, value_t_or_exit, values_t, values_t_or_exit},
18    crossbeam_channel::unbounded,
19    log::*,
20    rand::{rng, seq::SliceRandom},
21    solana_accounts_db::{
22        accounts_db::{AccountShrinkThreshold, AccountsDbConfig},
23        accounts_file::AccountsFileProvider,
24        accounts_index::{
25            AccountSecondaryIndexes, AccountsIndexConfig, DEFAULT_NUM_ENTRIES_OVERHEAD,
26            DEFAULT_NUM_ENTRIES_TO_EVICT, IndexLimit, IndexLimitThreshold, ScanFilter,
27        },
28        partitioned_rewards::PartitionedEpochRewardsConfig,
29        utils::{
30            create_all_accounts_run_and_snapshot_dirs, create_and_canonicalize_directories,
31            create_and_canonicalize_directory,
32        },
33    },
34    solana_clap_utils::input_parsers::{keypair_of, keypairs_of, pubkey_of, value_of, values_of},
35    solana_clock::{DEFAULT_SLOTS_PER_EPOCH, Slot},
36    solana_core::{
37        banking_stage::transaction_scheduler::scheduler_controller::SchedulerConfig,
38        consensus::tower_storage,
39        repair::repair_handler::RepairHandlerType,
40        resource_limits,
41        snapshot_packager_service::SnapshotPackagerService,
42        system_monitor_service::SystemMonitorService,
43        tpu::MAX_VOTES_PER_SECOND,
44        validator::{
45            BlockProductionMethod, BlockVerificationMethod, SchedulerPacing, Validator,
46            ValidatorConfig, ValidatorLogConfig, ValidatorStartProgress, ValidatorTpuConfig,
47            is_snapshot_config_valid,
48        },
49    },
50    solana_genesis_utils::MAX_GENESIS_ARCHIVE_UNPACKED_SIZE,
51    solana_gossip::{
52        cluster_info::{DEFAULT_CONTACT_SAVE_INTERVAL_MILLIS, NodeConfig},
53        contact_info::ContactInfo,
54        node::Node,
55    },
56    solana_hash::Hash,
57    solana_keypair::Keypair,
58    solana_ledger::{
59        blockstore_options::BlockstoreCleanupStrategy,
60        shred::filter::TurbineMode,
61        use_snapshot_archives_at_startup::{self, UseSnapshotArchivesAtStartup},
62    },
63    solana_net_utils::multihomed_sockets::BindIpAddrs,
64    solana_poh::poh_service,
65    solana_pubkey::Pubkey,
66    solana_runtime::{runtime_config::RuntimeConfig, snapshot_utils},
67    solana_signer::Signer,
68    solana_streamer::{
69        nonblocking::{simple_qos::SimpleQosConfig, swqos::SwQosConfig},
70        quic::{QuicStreamerConfig, SimpleQosQuicStreamerConfig, SwQosQuicStreamerConfig},
71    },
72    solana_tpu_client::tpu_client::DEFAULT_TPU_CONNECTION_POOL_SIZE,
73    solana_turbine::broadcast_stage::BroadcastStageType,
74    solana_validator_exit::Exit,
75    std::{
76        collections::HashSet,
77        env,
78        fs::{self, File},
79        net::{IpAddr, Ipv4Addr, SocketAddr},
80        num::{NonZeroU64, NonZeroUsize},
81        path::{Path, PathBuf},
82        str::{self, FromStr},
83        sync::{Arc, RwLock, atomic::AtomicBool},
84    },
85};
86#[cfg(target_os = "linux")]
87use {
88    agave_cpu_utils::cpu_affinity,
89    agave_xdp::transmitter::{QueueCpuBinding, XdpConfig},
90    solana_clap_utils::input_parsers::parse_cpu_ranges,
91};
92
93#[derive(Debug, PartialEq, Eq)]
94pub enum Operation {
95    Initialize,
96    Run,
97}
98
99pub fn execute(
100    matches: &ArgMatches,
101    solana_version: &str,
102    operation: Operation,
103    config: super::Config,
104) -> Result<(), Box<dyn std::error::Error>> {
105    // Debugging panics is easier with a backtrace
106    if env::var_os("RUST_BACKTRACE").is_none() {
107        // Safety: env update is made before any spawned threads might access the environment
108        unsafe { env::set_var("RUST_BACKTRACE", "1") }
109    }
110
111    let run_args = RunArgs::from_clap_arg_match(matches)?;
112
113    let cli::thread_args::NumThreadConfig {
114        accounts_db_background_threads,
115        accounts_db_foreground_threads,
116        accounts_index_flush_threads,
117        block_production_num_workers,
118        ip_echo_server_threads,
119        rayon_global_threads,
120        replay_forks_threads,
121        replay_transactions_threads,
122        tpu_sigverify_threads,
123        tpu_transaction_forward_receive_threads,
124        tpu_transaction_receive_threads,
125        tpu_vote_transaction_receive_threads,
126        tvu_receive_threads,
127        tvu_retransmit_threads,
128        tvu_sigverify_threads,
129        tvu_bls_sigverify_threads,
130    } = cli::thread_args::parse_num_threads_args(matches);
131
132    let identity_keypair = Arc::new(run_args.identity_keypair);
133
134    let logfile = run_args.logfile;
135    let log_config = if let Some(ref logfile) = logfile {
136        println!("log file: {}", logfile.display());
137        let logrotate_flag = Validator::register_logrotate_signal_handler()?;
138
139        Some(ValidatorLogConfig {
140            logfile: logfile.clone(),
141            logrotate_flag,
142        })
143    } else {
144        None
145    };
146    let use_progress_bar = log_config.is_none();
147    agave_logger::initialize_logging(logfile);
148
149    cli::warn_for_deprecated_arguments(matches);
150
151    info!("{} {}", crate_name!(), solana_version);
152    info!("Starting validator with: {:#?}", std::env::args_os());
153
154    solana_metrics::set_host_id(identity_keypair.pubkey().to_string());
155    solana_metrics::set_panic_hook("validator", Some(String::from(solana_version)));
156
157    let bind_addresses = {
158        let parsed = matches
159            .values_of("bind_address")
160            .expect("bind_address should always be present due to default")
161            .map(solana_net_utils::parse_host)
162            .collect::<Result<Vec<_>, _>>()?;
163        BindIpAddrs::new(parsed).map_err(|err| format!("invalid bind_addresses: {err}"))?
164    };
165
166    let entrypoint_addrs = run_args.entrypoints;
167    for addr in &entrypoint_addrs {
168        if !run_args.socket_addr_space.check(addr) {
169            Err(format!("invalid entrypoint address: {addr}"))?;
170        }
171    }
172    // XDP is not needed for init — it only initializes the ledger and exits.
173    // Also, init drops all Linux capabilities in main() so XDP setup would fail.
174    #[cfg(target_os = "linux")]
175    let xdp_transmit_config: Option<XdpConfig> =
176        build_xdp_config(matches, &operation, &bind_addresses)?;
177
178    let dynamic_port_range =
179        solana_net_utils::parse_port_range(matches.value_of("dynamic_port_range").unwrap())
180            .expect("invalid dynamic_port_range");
181
182    let advertised_ip = matches
183        .value_of("advertised_ip")
184        .map(|advertised_ip| {
185            solana_net_utils::parse_host(advertised_ip)
186                .map_err(|err| format!("failed to parse --advertised-ip: {err}"))
187        })
188        .transpose()?;
189
190    let advertised_ip = if let Some(cli_ip) = advertised_ip {
191        cli_ip
192    } else if !bind_addresses.active().is_unspecified() && !bind_addresses.active().is_loopback() {
193        bind_addresses.active()
194    } else if !entrypoint_addrs.is_empty() {
195        let mut order: Vec<_> = (0..entrypoint_addrs.len()).collect();
196        order.shuffle(&mut rng());
197
198        order
199            .into_iter()
200            .find_map(|i| {
201                let entrypoint_addr = &entrypoint_addrs[i];
202                info!(
203                    "Contacting {entrypoint_addr} to determine the validator's public IP address"
204                );
205                solana_net_utils::get_public_ip_addr_with_binding(
206                    entrypoint_addr,
207                    bind_addresses.active(),
208                )
209                .map_or_else(
210                    |err| {
211                        warn!("Failed to contact cluster entrypoint {entrypoint_addr}: {err}");
212                        None
213                    },
214                    Some,
215                )
216            })
217            .ok_or_else(|| "unable to determine the validator's public IP address".to_string())?
218    } else {
219        IpAddr::V4(Ipv4Addr::LOCALHOST)
220    };
221    let gossip_port = value_t!(matches, "gossip_port", u16).or_else(|_| {
222        solana_net_utils::find_available_port_in_range(bind_addresses.active(), (0, 1))
223            .map_err(|err| format!("unable to find an available gossip port: {err}"))
224    })?;
225
226    let public_tpu_addr = matches
227        .value_of("public_tpu_addr")
228        .map(|public_tpu_addr| {
229            solana_net_utils::parse_host_port(public_tpu_addr)
230                .map_err(|err| format!("failed to parse --public-tpu-address: {err}"))
231        })
232        .transpose()?;
233
234    let public_tpu_forwards_addr = matches
235        .value_of("public_tpu_forwards_addr")
236        .map(|public_tpu_forwards_addr| {
237            solana_net_utils::parse_host_port(public_tpu_forwards_addr)
238                .map_err(|err| format!("failed to parse --public-tpu-forwards-address: {err}"))
239        })
240        .transpose()?;
241
242    let public_tvu_addr = matches
243        .value_of("public_tvu_addr")
244        .map(|public_tvu_addr| {
245            solana_net_utils::parse_host_port(public_tvu_addr)
246                .map_err(|err| format!("failed to parse --public-tvu-address: {err}"))
247        })
248        .transpose()?;
249
250    if bind_addresses.len() > 1 && public_tvu_addr.is_some() {
251        Err(String::from(
252            "--public-tvu-address can not be used in a multihoming context",
253        ))?;
254    }
255
256    let num_quic_endpoints = value_t_or_exit!(matches, "num_quic_endpoints", NonZeroUsize);
257    let num_votor_quic_endpoints = value_t_or_exit!(matches, "num_votor_endpoints", NonZeroUsize);
258    if num_votor_quic_endpoints.get() > MAX_ENDPOINTS {
259        Err(format!(
260            "--num-votor-endpoints must be at most {MAX_ENDPOINTS}"
261        ))?;
262    }
263
264    let node_config = NodeConfig {
265        advertised_ip,
266        gossip_port,
267        port_range: dynamic_port_range,
268        bind_ip_addrs: bind_addresses.clone(),
269        public_tpu_addr,
270        public_tpu_forwards_addr,
271        public_tvu_addr,
272        num_tvu_receive_sockets: tvu_receive_threads,
273        num_tvu_retransmit_sockets: tvu_retransmit_threads,
274        num_quic_endpoints,
275        num_votor_quic_endpoints,
276    };
277
278    let mut node = Node::new_with_external_ip(&identity_keypair.pubkey(), node_config);
279
280    let exit = Arc::new(AtomicBool::new(false));
281
282    #[cfg(not(target_os = "linux"))]
283    let _ = config;
284
285    #[cfg(target_os = "linux")]
286    let (xdp_transmit_setup, xdp_network_config_report) = {
287        use {
288            agave_xdp::transmitter::TransmitterBuilder,
289            caps::{
290                CapSet,
291                Capability::{CAP_BPF, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON, CAP_SYS_NICE},
292            },
293            solana_core::system_monitor_service::XdpNetworkConfigReport,
294        };
295
296        let super::Config { primordial_caps } = config;
297
298        let mut required_caps = HashSet::new();
299        let mut retained_caps = HashSet::new();
300        let mut supported_caps = HashSet::from_iter([
301            CAP_BPF,
302            CAP_NET_ADMIN,
303            CAP_NET_RAW,
304            CAP_PERFMON,
305            CAP_SYS_NICE,
306        ]);
307
308        // make sure we keep any primordial caps
309        supported_caps.extend(primordial_caps.clone());
310        required_caps.extend(primordial_caps.clone());
311        retained_caps.extend(primordial_caps.clone());
312
313        if let Some(xdp_config) = xdp_transmit_config.as_ref() {
314            required_caps.insert(CAP_NET_ADMIN);
315            required_caps.insert(CAP_NET_RAW);
316            if xdp_config.zero_copy {
317                required_caps.insert(CAP_BPF);
318                required_caps.insert(CAP_PERFMON);
319            }
320        }
321
322        let snapshot_packager_niceness_adj =
323            value_t_or_exit!(matches, "snapshot_packager_niceness_adj", i8);
324
325        if snapshot_packager_niceness_adj != 0 || run_args.json_rpc_config.rpc_niceness_adj != 0 {
326            required_caps.insert(CAP_SYS_NICE);
327            retained_caps.insert(CAP_SYS_NICE);
328        }
329
330        // lazy dev check
331        assert!(
332            required_caps.is_subset(&supported_caps),
333            "required_caps contains a cap not in supported_caps",
334        );
335
336        // validate and minimize the permitted set
337        let current_permitted =
338            caps::read(None, CapSet::Permitted).expect("permitted capset to be readable");
339        let missing_caps = required_caps
340            .difference(&current_permitted)
341            .collect::<Vec<_>>();
342        if !missing_caps.is_empty() {
343            error!(
344                "the current configuration requires the following capabilities, which have not \
345                 been permitted to the current process: {missing_caps:?}",
346            );
347            std::process::exit(1);
348        }
349        // warn about extraneous caps that no configuration requires
350        let extra_caps = current_permitted
351            .difference(&supported_caps)
352            .collect::<Vec<_>>();
353        if !extra_caps.is_empty() {
354            warn!(
355                "dropping extraneous capabilities ({extra_caps:?}) from the current process. \
356                 consider removing them from your operational configuration.",
357            );
358        }
359
360        // drop all caps that the current configuration does not require
361        caps::set(None, CapSet::Effective, &required_caps)
362            .expect("linux allows effective capset to be set");
363        caps::set(None, CapSet::Permitted, &required_caps)
364            .expect("linux allows permitted capset to be set");
365
366        // XDP _MUST_ be setup _BEFORE_ the app spawns any threads to ensure linux
367        // capabilities do not leak, leaving the process in a state where it could
368        // potentially be used as a privilege escalation gadget
369        let (xdp_transmit_setup, report) = xdp_transmit_config
370            .clone()
371            .map(|mut xdp_config| {
372                use {
373                    agave_xdp::{device::NetworkDevice, interface_ipv4},
374                    solana_core::validator::XdpTransmitSetup,
375                };
376
377                let device = if let Some(interface) = xdp_config.interface.as_ref() {
378                    NetworkDevice::new(interface).expect("configured interface should exist")
379                } else {
380                    NetworkDevice::new_from_default_route()
381                        .expect("default route device should exist")
382                };
383
384                let xdp_interface = device.name().to_string();
385                // Keep the transmitter and metrics on the selected XDP device. Source IP lookup
386                // uses the same interface name, with bond-master fallback.
387                xdp_config.interface = Some(xdp_interface.clone());
388                let zero_copy = xdp_config.zero_copy;
389                let src_ip = match node.bind_ip_addrs.active() {
390                    IpAddr::V4(ip) if !ip.is_unspecified() => ip,
391                    IpAddr::V4(_unspecified) => interface_ipv4(&xdp_interface).expect(
392                        "selected interface should exist and have an IPv4 address assigned",
393                    ),
394                    _ => panic!("IPv6 not supported"),
395                };
396                (
397                    XdpTransmitSetup {
398                        transmitter_builder: TransmitterBuilder::new(xdp_config, exit.clone())
399                            .expect("failed to create xdp transmitter"),
400                        src_ip,
401                    },
402                    XdpNetworkConfigReport {
403                        zero_copy,
404                        interface: xdp_interface,
405                    },
406                )
407            })
408            .map_or((None, None), |(setup, report)| (Some(setup), Some(report)));
409
410        // we're done with caps needed to init xdp now. remove them from our process
411        caps::set(None, CapSet::Effective, &retained_caps)
412            .expect("linux allows effective capset to be set");
413        caps::set(None, CapSet::Permitted, &retained_caps)
414            .expect("linux allows permitted capset to be set");
415
416        (xdp_transmit_setup, report)
417    };
418
419    #[cfg(not(target_os = "linux"))]
420    let (xdp_transmit_setup, xdp_network_config_report) = (None, None);
421
422    #[cfg(target_os = "linux")]
423    let poh_pinned_cpu_core = value_of(matches, "poh_pinned_cpu_core")
424        .or_else(|| value_of(matches, "experimental_poh_pinned_cpu_core"))
425        .or(poh_service::DEFAULT_PINNED_CPU_CORE);
426
427    #[cfg(not(target_os = "linux"))]
428    let poh_pinned_cpu_core = None;
429
430    solana_core::validator::report_target_features();
431
432    let authorized_voter_keypairs = keypairs_of(matches, "authorized_voter_keypairs")
433        .map(|keypairs| keypairs.into_iter().map(Arc::new).collect())
434        .unwrap_or_else(|| vec![Arc::new(keypair_of(matches, "identity").expect("identity"))]);
435    let authorized_voter_keypairs = Arc::new(RwLock::new(authorized_voter_keypairs));
436
437    let staked_nodes_overrides_path = matches
438        .value_of("staked_nodes_overrides")
439        .map(str::to_string);
440    let staked_nodes_overrides = Arc::new(RwLock::new(
441        match &staked_nodes_overrides_path {
442            None => StakedNodesOverrides::default(),
443            Some(p) => load_staked_nodes_overrides(p).unwrap_or_else(|err| {
444                error!("Failed to load stake-nodes-overrides from {p}: {err}");
445                clap::Error::with_description(
446                    "Failed to load configuration of stake-nodes-overrides argument",
447                    clap::ErrorKind::InvalidValue,
448                )
449                .exit()
450            }),
451        }
452        .staked_map_id,
453    ));
454
455    let init_complete_file = matches.value_of("init_complete_file");
456
457    let private_rpc = matches.is_present("private_rpc");
458    let do_port_check = !matches.is_present("no_port_check");
459
460    let ledger_path = run_args.ledger_path;
461    let blockstore_cleanup_strategy = BlockstoreCleanupStrategy::from_clap_arg_match(matches)?;
462
463    let debug_keys: Option<Arc<HashSet<_>>> = if matches.is_present("debug_key") {
464        Some(Arc::new(
465            values_t_or_exit!(matches, "debug_key", Pubkey)
466                .into_iter()
467                .collect(),
468        ))
469    } else {
470        None
471    };
472
473    let repair_validators = validators_set(
474        &identity_keypair.pubkey(),
475        matches,
476        "repair_validators",
477        "--repair-validator",
478    )?;
479    let repair_whitelist = validators_set(
480        &identity_keypair.pubkey(),
481        matches,
482        "repair_whitelist",
483        "--repair-whitelist",
484    )?;
485    let repair_whitelist = Arc::new(RwLock::new(repair_whitelist.unwrap_or_default()));
486    let gossip_validators = validators_set(
487        &identity_keypair.pubkey(),
488        matches,
489        "gossip_validators",
490        "--gossip-validator",
491    )?;
492
493    if bind_addresses.len() > 1 {
494        for (flag, msg) in [
495            (
496                "advertised_ip",
497                "--advertised-ip cannot be used in a multihoming context. In multihoming, the \
498                 validator will advertise the first --bind-address as this node's public IP \
499                 address.",
500            ),
501            (
502                "public_tpu_addr",
503                "--public-tpu-address can not be used in a multihoming context",
504            ),
505        ] {
506            if matches.is_present(flag) {
507                Err(String::from(msg))?;
508            }
509        }
510    }
511
512    let rpc_bind_address = if matches.is_present("rpc_bind_address") {
513        solana_net_utils::parse_host(matches.value_of("rpc_bind_address").unwrap())
514            .expect("invalid rpc_bind_address")
515    } else if private_rpc {
516        solana_net_utils::parse_host("127.0.0.1").unwrap()
517    } else {
518        bind_addresses.active()
519    };
520
521    let contact_debug_interval = value_t_or_exit!(matches, "contact_debug_interval", u64);
522
523    let account_indexes = AccountSecondaryIndexes::from_clap_arg_match(matches)?;
524
525    let restricted_repair_only_mode = matches.is_present("restricted_repair_only_mode");
526    let accounts_shrink_optimize_total_space =
527        value_t_or_exit!(matches, "accounts_shrink_optimize_total_space", bool);
528    let vote_use_quic = value_t_or_exit!(matches, "vote_use_quic", bool);
529
530    let tpu_connection_pool_size = matches
531        .value_of("tpu_connection_pool_size")
532        .unwrap_or("")
533        .parse()
534        .unwrap_or(DEFAULT_TPU_CONNECTION_POOL_SIZE);
535
536    let shrink_ratio = value_t_or_exit!(matches, "accounts_shrink_ratio", f64);
537    if !(0.0..=1.0).contains(&shrink_ratio) {
538        Err(format!(
539            "the specified account-shrink-ratio is invalid, it must be between 0. and 1.0 \
540             inclusive: {shrink_ratio}"
541        ))?;
542    }
543
544    let shrink_ratio = if accounts_shrink_optimize_total_space {
545        AccountShrinkThreshold::TotalSpace { shrink_ratio }
546    } else {
547        AccountShrinkThreshold::IndividualStore { shrink_ratio }
548    };
549    // TODO: Once entrypoints are updated to return shred-version, this should
550    // abort if it fails to obtain a shred-version, so that nodes always join
551    // gossip with a valid shred-version. The code to adopt entrypoint shred
552    // version can then be deleted from gossip and get_rpc_node above.
553    let expected_shred_version = value_t!(matches, "expected_shred_version", u16)
554        .ok()
555        .or_else(|| get_cluster_shred_version(&entrypoint_addrs, bind_addresses.active()));
556
557    let tower_path = value_t!(matches, "tower", PathBuf)
558        .ok()
559        .unwrap_or_else(|| ledger_path.clone());
560    let tower_storage: Arc<dyn tower_storage::TowerStorage> =
561        Arc::new(tower_storage::FileTowerStorage::new(tower_path));
562
563    let vote_history_storage: Arc<dyn vote_history_storage::VoteHistoryStorage> = Arc::new(
564        vote_history_storage::FileVoteHistoryStorage::new(ledger_path.clone()),
565    );
566
567    let accounts_index_limit =
568        value_t!(matches, "accounts_index_limit", String).unwrap_or_else(|err| err.exit());
569    let index_limit = {
570        enum CliIndexLimit {
571            // deprecated in v4.1.0
572            Minimal,
573            Unlimited,
574            Threshold(u64),
575        }
576        let cli_index_limit = match accounts_index_limit.as_str() {
577            "minimal" => {
578                warn!("Using `minimal` for `--accounts-index-limit` is deprecated.");
579                CliIndexLimit::Minimal
580            }
581            "unlimited" => CliIndexLimit::Unlimited,
582            "25GB" => CliIndexLimit::Threshold(25_000_000_000),
583            "50GB" => CliIndexLimit::Threshold(50_000_000_000),
584            "100GB" => CliIndexLimit::Threshold(100_000_000_000),
585            "200GB" => CliIndexLimit::Threshold(200_000_000_000),
586            "400GB" => CliIndexLimit::Threshold(400_000_000_000),
587            "800GB" => CliIndexLimit::Threshold(800_000_000_000),
588            x => {
589                // clap will enforce only the above values are possible
590                unreachable!("invalid value given to `--accounts-index-limit`: '{x}'")
591            }
592        };
593        match cli_index_limit {
594            CliIndexLimit::Minimal => IndexLimit::Minimal,
595            CliIndexLimit::Unlimited => IndexLimit::InMemOnly,
596            CliIndexLimit::Threshold(num_bytes) => IndexLimit::Threshold(IndexLimitThreshold {
597                num_bytes,
598                num_entries_overhead: DEFAULT_NUM_ENTRIES_OVERHEAD,
599                num_entries_to_evict: DEFAULT_NUM_ENTRIES_TO_EVICT,
600            }),
601        }
602    };
603    // Note: need to still handle --enable-accounts-disk-index until it is removed
604    let index_limit = if matches.is_present("enable_accounts_disk_index") {
605        IndexLimit::Minimal
606    } else {
607        index_limit
608    };
609
610    let mut accounts_index_config = AccountsIndexConfig {
611        num_flush_threads: Some(accounts_index_flush_threads),
612        index_limit,
613        ..AccountsIndexConfig::default()
614    };
615    if let Ok(bins) = value_t!(matches, "accounts_index_bins", usize) {
616        accounts_index_config.bins = Some(bins);
617    }
618    if let Ok(num_initial_accounts) =
619        value_t!(matches, "accounts_index_initial_accounts_count", usize)
620    {
621        accounts_index_config.num_initial_accounts = Some(num_initial_accounts);
622    }
623
624    {
625        let mut accounts_index_paths: Vec<PathBuf> = if matches.is_present("accounts_index_path") {
626            values_t_or_exit!(matches, "accounts_index_path", String)
627                .into_iter()
628                .map(PathBuf::from)
629                .collect()
630        } else {
631            vec![]
632        };
633        if accounts_index_paths.is_empty() {
634            accounts_index_paths = vec![ledger_path.join("accounts_index")];
635        }
636        accounts_index_config.drives = Some(accounts_index_paths);
637    }
638
639    const MB: usize = 1_024 * 1_024;
640
641    let read_cache_limit_bytes = if let Some(limits) =
642        values_of::<ByteSize>(matches, "accounts_db_read_cache_limit")
643    {
644        match limits.as_slice() {
645            [lo, hi] => {
646                let lo = usize::try_from(lo.0)?;
647                let hi = usize::try_from(hi.0)?;
648                if lo > hi {
649                    Err(format!(
650                        "invalid --accounts-db-read-cache-limit: LOW ({lo}) must be <= HIGH ({hi})",
651                    ))?;
652                }
653                Some((lo, hi))
654            }
655            _ => {
656                // clap will enforce two values are given
657                unreachable!("invalid number of values given to accounts-db-read-cache-limit")
658            }
659        }
660    } else {
661        None
662    };
663
664    let write_cache_limit_bytes =
665        value_of::<ByteSize>(matches, "accounts_db_write_cache_limit").map(|limit| limit.0);
666    // accounts-db-write-cache-limit-mb was deprecated in v4.2.0
667    let write_cache_limit_mb = value_t!(matches, "accounts_db_cache_limit_mb", u64)
668        .ok()
669        .map(|mb| mb * MB as u64);
670    // clap will enforce only one cli arg is provided, so pick whichever is Some
671    let write_cache_limit_bytes = write_cache_limit_bytes.or(write_cache_limit_mb);
672
673    let scan_filter_for_shrinking = matches
674        .value_of("accounts_db_scan_filter_for_shrinking")
675        .map(|filter| match filter {
676            "all" => ScanFilter::All,
677            "only-abnormal" => ScanFilter::OnlyAbnormal,
678            "only-abnormal-with-verify" => ScanFilter::OnlyAbnormalWithVerify,
679            _ => {
680                // clap will enforce one of the above values is given
681                unreachable!("invalid value given to accounts_db_scan_filter_for_shrinking")
682            }
683        })
684        .unwrap_or_default();
685
686    let accounts_db_config = AccountsDbConfig {
687        index: Some(accounts_index_config),
688        account_indexes: Some(account_indexes.clone()),
689        bank_hash_details_dir: ledger_path.clone(),
690        shrink_ratio,
691        read_cache_limit_bytes,
692        read_cache_evict_sample_size: None,
693        read_cache_num_shards: None,
694        write_cache_limit_bytes,
695        ancient_append_vec_offset: value_t!(matches, "accounts_db_ancient_append_vecs", i64).ok(),
696        ancient_storage_ideal_size: value_t!(
697            matches,
698            "accounts_db_ancient_storage_ideal_size",
699            u64
700        )
701        .ok(),
702        max_ancient_storages: value_t!(matches, "accounts_db_max_ancient_storages", usize).ok(),
703        skip_initial_hash_calc: false,
704        exhaustively_verify_refcounts: matches.is_present("accounts_db_verify_refcounts"),
705        partitioned_epoch_rewards_config: PartitionedEpochRewardsConfig::default(),
706        scan_filter_for_shrinking,
707        num_background_threads: Some(accounts_db_background_threads),
708        num_foreground_threads: Some(accounts_db_foreground_threads),
709        accounts_file_provider: AccountsFileProvider::AppendVec,
710    };
711
712    let on_start_geyser_plugin_config_files = if matches.is_present("geyser_plugin_config") {
713        Some(
714            values_t_or_exit!(matches, "geyser_plugin_config", String)
715                .into_iter()
716                .map(PathBuf::from)
717                .collect(),
718        )
719    } else {
720        None
721    };
722    let starting_with_geyser_plugins: bool = on_start_geyser_plugin_config_files.is_some()
723        || matches.is_present("geyser_plugin_always_enabled");
724
725    let account_paths: Vec<PathBuf> =
726        if let Ok(account_paths) = values_t!(matches, "account_paths", String) {
727            account_paths
728                .join(",")
729                .split(',')
730                .map(PathBuf::from)
731                .collect()
732        } else {
733            vec![ledger_path.join("accounts")]
734        };
735    let account_paths = create_and_canonicalize_directories(account_paths)
736        .map_err(|err| format!("unable to access account path: {err}"))?;
737
738    // From now on, use run/ paths in the same way as the previous account_paths.
739    let (account_run_paths, account_snapshot_paths) =
740        create_all_accounts_run_and_snapshot_dirs(&account_paths)
741            .map_err(|err| format!("unable to create account directories: {err}"))?;
742
743    let snapshot_config = new_snapshot_config(
744        matches,
745        &ledger_path,
746        &account_paths,
747        run_args.rpc_bootstrap_config.incremental_snapshot_fetch,
748    )?;
749
750    let use_snapshot_archives_at_startup = value_t_or_exit!(
751        matches,
752        use_snapshot_archives_at_startup::cli::NAME,
753        UseSnapshotArchivesAtStartup
754    );
755
756    let skip_transaction_signatures_in_status_cache =
757        !run_args.json_rpc_config.full_api && !snapshot_config.should_generate_snapshots();
758    if skip_transaction_signatures_in_status_cache {
759        info!(
760            "Transaction signatures will not be stored in the status cache because full RPC and \
761             snapshot generation are disabled"
762        );
763    }
764
765    let mut validator_config = ValidatorConfig {
766        log_config,
767        require_tower: matches.is_present("require_tower"),
768        require_vote_history: !matches.is_present("do_not_require_vote_history"),
769        tower_storage,
770        vote_history_storage,
771        max_genesis_archive_unpacked_size: MAX_GENESIS_ARCHIVE_UNPACKED_SIZE,
772        expected_genesis_hash: matches
773            .value_of("expected_genesis_hash")
774            .map(|s| Hash::from_str(s).unwrap()),
775        fixed_leader_schedule: None,
776        expected_bank_hash: matches
777            .value_of("expected_bank_hash")
778            .map(|s| Hash::from_str(s).unwrap()),
779        expected_shred_version,
780        new_hard_forks: hardforks_of(matches, "hard_forks"),
781        runtime_config: RuntimeConfig {
782            log_messages_bytes_limit: value_of(matches, "log_messages_bytes_limit"),
783            skip_transaction_signatures_in_status_cache,
784            ..RuntimeConfig::default()
785        },
786        rpc_config: run_args.json_rpc_config,
787        on_start_geyser_plugin_config_files,
788        geyser_plugin_always_enabled: matches.is_present("geyser_plugin_always_enabled"),
789        rpc_addrs: value_t!(matches, "rpc_port", u16).ok().map(|rpc_port| {
790            (
791                SocketAddr::new(rpc_bind_address, rpc_port),
792                SocketAddr::new(rpc_bind_address, rpc_port + 1),
793                // If additional ports are added, +2 needs to be skipped to avoid a conflict with
794                // the websocket port (which is +2) in web3.js This odd port shifting is tracked at
795                // https://github.com/solana-labs/solana/issues/12250
796            )
797        }),
798        pubsub_config: run_args.pub_sub_config,
799        voting_disabled: matches.is_present("no_voting") || restricted_repair_only_mode,
800        wait_for_supermajority: value_t!(matches, "wait_for_supermajority", Slot).ok(),
801        known_validators: run_args.known_validators,
802        repair_validators,
803        should_check_duplicate_instance: true,
804        repair_whitelist,
805        repair_handler_type: RepairHandlerType::default(),
806        gossip_validators,
807        blockstore_cleanup_strategy,
808        blockstore_options: run_args.blockstore_options,
809        run_verification: !matches.is_present("skip_startup_ledger_verification"),
810        debug_keys,
811        filter_keys: Arc::new(run_args.filter_keys),
812        warp_slot: None,
813        generator_config: None,
814        contact_debug_interval,
815        contact_save_interval: DEFAULT_CONTACT_SAVE_INTERVAL_MILLIS,
816        send_transaction_service_config: run_args.send_transaction_service_config,
817        no_poh_speed_test: matches.is_present("no_poh_speed_test"),
818        no_os_memory_stats_reporting: matches.is_present("no_os_memory_stats_reporting"),
819        no_os_network_stats_reporting: matches.is_present("no_os_network_stats_reporting"),
820        xdp_network_config_report,
821        no_os_cpu_stats_reporting: matches.is_present("no_os_cpu_stats_reporting"),
822        no_os_disk_stats_reporting: matches.is_present("no_os_disk_stats_reporting"),
823        // The validator needs to open many files, check that the process has
824        // permission to do so in order to fail quickly and give a direct error
825        enforce_ulimit_nofile: true,
826        poh_pinned_cpu_core,
827        poh_hashes_per_batch: value_of(matches, "poh_hashes_per_batch")
828            .unwrap_or(poh_service::DEFAULT_HASHES_PER_BATCH),
829        process_ledger_before_services: matches.is_present("process_ledger_before_services"),
830        account_paths: account_run_paths,
831        account_snapshot_paths,
832        accounts_db_config,
833        accounts_db_skip_shrink: true,
834        accounts_db_force_initial_clean: matches.is_present("no_skip_initial_accounts_db_clean"),
835        snapshot_config,
836        no_wait_for_vote_to_start_leader: matches.is_present("no_wait_for_vote_to_start_leader"),
837        wait_to_vote_slot: value_t!(matches, "wait_to_vote_slot", Slot).ok(),
838        staked_nodes_overrides: staked_nodes_overrides.clone(),
839        use_snapshot_archives_at_startup,
840        ip_echo_server_threads,
841        rayon_global_threads,
842        replay_forks_threads,
843        replay_transactions_threads,
844        tvu_shred_sigverify_threads: tvu_sigverify_threads,
845        tvu_bls_sigverify_threads,
846        delay_leader_block_for_pending_fork: !matches
847            .is_present("no_delay_leader_block_for_pending_fork"),
848        turbine_mode: TurbineMode::default(),
849        broadcast_stage_type: BroadcastStageType::Standard,
850        block_verification_method: value_t_or_exit!(
851            matches,
852            "block_verification_method",
853            BlockVerificationMethod
854        ),
855        unified_scheduler_handler_threads: value_t!(
856            matches,
857            "unified_scheduler_handler_threads",
858            usize
859        )
860        .ok(),
861        block_production_method: value_t_or_exit!(
862            matches,
863            "block_production_method",
864            BlockProductionMethod
865        ),
866        block_production_num_workers,
867        block_production_scheduler_config: SchedulerConfig {
868            scheduler_pacing: value_t_or_exit!(
869                matches,
870                "block_production_pacing_fill_time_millis",
871                SchedulerPacing
872            ),
873        },
874        enable_block_production_forwarding: staked_nodes_overrides_path.is_some(),
875        enable_scheduler_bindings: matches.is_present("enable_scheduler_bindings"),
876        banking_trace_dir_byte_limit: value_t_or_exit!(
877            matches,
878            "banking_trace_dir_byte_limit",
879            u64
880        ),
881        validator_exit: Arc::new(RwLock::new(Exit::default())),
882        validator_exit_backpressure: [(
883            SnapshotPackagerService::NAME.to_string(),
884            Arc::new(AtomicBool::new(false)),
885        )]
886        .into(),
887        voting_service_test_override: None,
888        snapshot_packager_niceness_adj: value_t_or_exit!(
889            matches,
890            "snapshot_packager_niceness_adj",
891            i8
892        ),
893    };
894    validator_config
895        .block_production_method
896        .warn_if_deprecated_value();
897
898    let vote_account = pubkey_of(matches, "vote_account").unwrap_or_else(|| {
899        if !validator_config.voting_disabled {
900            warn!("--vote-account not specified, validator will not vote");
901            validator_config.voting_disabled = true;
902        }
903        Keypair::new().pubkey()
904    });
905
906    let maximum_local_snapshot_age = value_t_or_exit!(matches, "maximum_local_snapshot_age", u64);
907    let minimal_snapshot_download_speed =
908        value_t_or_exit!(matches, "minimal_snapshot_download_speed", f32);
909    let maximum_snapshot_download_abort =
910        value_t_or_exit!(matches, "maximum_snapshot_download_abort", u64);
911
912    let public_rpc_addr = matches
913        .value_of("public_rpc_addr")
914        .map(|addr| {
915            solana_net_utils::parse_host_port(addr)
916                .map_err(|err| format!("failed to parse public rpc address: {err}"))
917        })
918        .transpose()?;
919
920    if !matches.is_present("no_os_network_limits_test") {
921        if SystemMonitorService::check_os_network_limits() {
922            info!("OS network limits test passed.");
923        } else {
924            Err("OS network limit test failed. See \
925                https://docs.anza.xyz/operations/guides/validator-start#system-tuning"
926                .to_string())?;
927        }
928    }
929
930    let mut ledger_lock = ledger_lockfile(&ledger_path);
931    let _ledger_write_guard = lock_ledger(&ledger_path, &mut ledger_lock);
932
933    let start_progress = Arc::new(RwLock::new(ValidatorStartProgress::default()));
934    let admin_service_post_init = Arc::new(RwLock::new(None));
935    let (rpc_to_plugin_manager_sender, rpc_to_plugin_manager_receiver) =
936        if starting_with_geyser_plugins {
937            let (sender, receiver) = unbounded();
938            (Some(sender), Some(receiver))
939        } else {
940            (None, None)
941        };
942    admin_rpc_service::run(
943        &ledger_path,
944        admin_rpc_service::AdminRpcRequestMetadata {
945            rpc_addr: validator_config.rpc_addrs.map(|(rpc_addr, _)| rpc_addr),
946            start_time: std::time::SystemTime::now(),
947            validator_exit: validator_config.validator_exit.clone(),
948            validator_exit_backpressure: validator_config.validator_exit_backpressure.clone(),
949            start_progress: start_progress.clone(),
950            authorized_voter_keypairs: authorized_voter_keypairs.clone(),
951            post_init: admin_service_post_init.clone(),
952            tower_storage: validator_config.tower_storage.clone(),
953            vote_history_storage: validator_config.vote_history_storage.clone(),
954            staked_nodes_overrides,
955            rpc_to_plugin_manager_sender,
956        },
957    );
958
959    let tpu_max_connections_per_peer: Option<u64> = matches
960        .value_of("tpu_max_connections_per_peer")
961        .and_then(|v| v.parse().ok());
962    let tpu_max_connections_per_unstaked_peer = tpu_max_connections_per_peer
963        .unwrap_or_else(|| value_t_or_exit!(matches, "tpu_max_connections_per_unstaked_peer", u64));
964    let tpu_max_connections_per_staked_peer = tpu_max_connections_per_peer
965        .unwrap_or_else(|| value_t_or_exit!(matches, "tpu_max_connections_per_staked_peer", u64));
966    let tpu_max_staked_connections = value_t_or_exit!(matches, "tpu_max_staked_connections", u64);
967    let tpu_max_unstaked_connections =
968        value_t_or_exit!(matches, "tpu_max_unstaked_connections", u64);
969
970    let tpu_max_fwd_staked_connections =
971        value_t_or_exit!(matches, "tpu_max_fwd_staked_connections", u64);
972    let tpu_max_fwd_unstaked_connections =
973        value_t_or_exit!(matches, "tpu_max_fwd_unstaked_connections", u64);
974
975    let tpu_max_connections_per_ipaddr_per_minute: u64 =
976        value_t_or_exit!(matches, "tpu_max_connections_per_ipaddr_per_minute", u64);
977    let max_streams_per_ms = value_t_or_exit!(matches, "tpu_max_streams_per_ms", u64);
978
979    let cluster_entrypoints = entrypoint_addrs
980        .iter()
981        .map(ContactInfo::new_gossip_entry_point)
982        .collect::<Vec<_>>();
983
984    if restricted_repair_only_mode {
985        // When in --restricted_repair_only_mode is enabled only the gossip and repair ports
986        // need to be reachable by the entrypoint to respond to gossip pull requests and repair
987        // requests initiated by the node.  All other ports are unused.
988        node.info.remove_tpu();
989        node.info.remove_tpu_forwards();
990        node.info.remove_tvu();
991        node.info.remove_serve_repair();
992        node.info.remove_alpenglow();
993
994        // A node in this configuration shouldn't be an entrypoint to other nodes
995        node.sockets.ip_echo = None;
996    }
997
998    if !private_rpc {
999        macro_rules! set_socket {
1000            ($method:ident, $addr:expr, $name:literal) => {
1001                node.info.$method($addr).expect(&format!(
1002                    "Operator must spin up node with valid {} address",
1003                    $name
1004                ))
1005            };
1006        }
1007        if let Some(public_rpc_addr) = public_rpc_addr {
1008            set_socket!(set_rpc, public_rpc_addr, "RPC");
1009            set_socket!(set_rpc_pubsub, public_rpc_addr, "RPC-pubsub");
1010        } else if let Some((rpc_addr, rpc_pubsub_addr)) = validator_config.rpc_addrs {
1011            let addr = node
1012                .info
1013                .gossip()
1014                .expect("Operator must spin up node with valid gossip address")
1015                .ip();
1016            set_socket!(set_rpc, (addr, rpc_addr.port()), "RPC");
1017            set_socket!(set_rpc_pubsub, (addr, rpc_pubsub_addr.port()), "RPC-pubsub");
1018        }
1019    }
1020
1021    snapshot_utils::remove_tmp_snapshot_archives(
1022        &validator_config.snapshot_config.full_snapshot_archives_dir,
1023    );
1024    snapshot_utils::remove_tmp_snapshot_archives(
1025        &validator_config
1026            .snapshot_config
1027            .incremental_snapshot_archives_dir,
1028    );
1029
1030    if !cluster_entrypoints.is_empty() {
1031        bootstrap::rpc_bootstrap(
1032            &node,
1033            &identity_keypair,
1034            &ledger_path,
1035            &vote_account,
1036            authorized_voter_keypairs.clone(),
1037            &cluster_entrypoints,
1038            &mut validator_config,
1039            run_args.rpc_bootstrap_config,
1040            do_port_check,
1041            use_progress_bar,
1042            maximum_local_snapshot_age,
1043            &start_progress,
1044            minimal_snapshot_download_speed,
1045            maximum_snapshot_download_abort,
1046            run_args.socket_addr_space,
1047        );
1048        *start_progress.write().unwrap() = ValidatorStartProgress::Initializing;
1049    }
1050
1051    if operation == Operation::Initialize {
1052        info!("Validator ledger initialization complete");
1053        return Ok(());
1054    }
1055
1056    // Bootstrap code above pushes a contact-info with more recent timestamp to
1057    // gossip. If the node is staked the contact-info lingers in gossip causing
1058    // false duplicate nodes error.
1059    // Below line refreshes the timestamp on contact-info so that it overrides
1060    // the one pushed by bootstrap.
1061    node.info.hot_swap_pubkey(identity_keypair.pubkey());
1062
1063    let tpu_quic_server_config = SwQosQuicStreamerConfig {
1064        quic_streamer_config: QuicStreamerConfig {
1065            max_connections_per_ipaddr_per_min: tpu_max_connections_per_ipaddr_per_minute,
1066            num_threads: tpu_transaction_receive_threads,
1067            stream_receive_window_size: solana_message::v1::MAX_TRANSACTION_SIZE as u32,
1068            max_stream_data_bytes: solana_message::v1::MAX_TRANSACTION_SIZE as u32,
1069            ..Default::default()
1070        },
1071        qos_config: SwQosConfig {
1072            max_connections_per_unstaked_peer: tpu_max_connections_per_unstaked_peer
1073                .try_into()
1074                .unwrap(),
1075            max_connections_per_staked_peer: tpu_max_connections_per_staked_peer
1076                .try_into()
1077                .unwrap(),
1078            max_staked_connections: tpu_max_staked_connections.try_into().unwrap(),
1079            max_unstaked_connections: tpu_max_unstaked_connections.try_into().unwrap(),
1080            max_streams_per_ms,
1081        },
1082    };
1083
1084    let tpu_fwd_quic_server_config = SwQosQuicStreamerConfig {
1085        quic_streamer_config: QuicStreamerConfig {
1086            max_connections_per_ipaddr_per_min: tpu_max_connections_per_ipaddr_per_minute,
1087            num_threads: tpu_transaction_forward_receive_threads,
1088            stream_receive_window_size: solana_message::v1::MAX_TRANSACTION_SIZE as u32,
1089            max_stream_data_bytes: solana_message::v1::MAX_TRANSACTION_SIZE as u32,
1090            ..Default::default()
1091        },
1092        qos_config: SwQosConfig {
1093            max_connections_per_staked_peer: tpu_max_connections_per_staked_peer
1094                .try_into()
1095                .unwrap(),
1096            max_connections_per_unstaked_peer: tpu_max_connections_per_unstaked_peer
1097                .try_into()
1098                .unwrap(),
1099            max_staked_connections: tpu_max_fwd_staked_connections.try_into().unwrap(),
1100            max_unstaked_connections: tpu_max_fwd_unstaked_connections.try_into().unwrap(),
1101            max_streams_per_ms,
1102        },
1103    };
1104
1105    let vote_quic_server_config = SimpleQosQuicStreamerConfig {
1106        quic_streamer_config: QuicStreamerConfig {
1107            max_connections_per_ipaddr_per_min: tpu_max_connections_per_ipaddr_per_minute,
1108            num_threads: tpu_vote_transaction_receive_threads,
1109            ..Default::default()
1110        },
1111        qos_config: SimpleQosConfig {
1112            max_streams_per_second: MAX_VOTES_PER_SECOND,
1113            ..Default::default()
1114        },
1115    };
1116
1117    let validator = Validator::new_with_exit(
1118        node,
1119        identity_keypair,
1120        &ledger_path,
1121        &vote_account,
1122        authorized_voter_keypairs,
1123        cluster_entrypoints,
1124        &validator_config,
1125        rpc_to_plugin_manager_receiver,
1126        start_progress,
1127        run_args.socket_addr_space,
1128        ValidatorTpuConfig {
1129            vote_use_quic,
1130            tpu_connection_pool_size,
1131            tpu_quic_server_config,
1132            tpu_fwd_quic_server_config,
1133            vote_quic_server_config,
1134            sigverify_threads: tpu_sigverify_threads,
1135        },
1136        admin_service_post_init,
1137        xdp_transmit_setup,
1138        exit,
1139    )
1140    .map_err(|err| format!("{err:?}"))?;
1141
1142    if let Some(filename) = init_complete_file {
1143        File::create(filename).map_err(|err| format!("unable to create {filename}: {err}"))?;
1144    }
1145    info!("Validator initialized");
1146    validator.listen_for_signals()?;
1147    validator.join();
1148    info!("Validator exiting...");
1149
1150    Ok(())
1151}
1152
1153// This function is duplicated in ledger-tool/src/main.rs...
1154fn hardforks_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<Slot>> {
1155    if matches.is_present(name) {
1156        Some(values_t_or_exit!(matches, name, Slot))
1157    } else {
1158        None
1159    }
1160}
1161
1162fn validators_set(
1163    identity_pubkey: &Pubkey,
1164    matches: &ArgMatches<'_>,
1165    matches_name: &str,
1166    arg_name: &str,
1167) -> Result<Option<HashSet<Pubkey>>, String> {
1168    if matches.is_present(matches_name) {
1169        let validators_set: HashSet<_> = values_t_or_exit!(matches, matches_name, Pubkey)
1170            .into_iter()
1171            .collect();
1172        if validators_set.contains(identity_pubkey) {
1173            Err(format!(
1174                "the validator's identity pubkey cannot be a {arg_name}: {identity_pubkey}"
1175            ))?;
1176        }
1177        Ok(Some(validators_set))
1178    } else {
1179        Ok(None)
1180    }
1181}
1182
1183fn get_cluster_shred_version(entrypoints: &[SocketAddr], bind_address: IpAddr) -> Option<u16> {
1184    let entrypoints = {
1185        let mut index: Vec<_> = (0..entrypoints.len()).collect();
1186        index.shuffle(&mut rand::rng());
1187        index.into_iter().map(|i| &entrypoints[i])
1188    };
1189    for entrypoint in entrypoints {
1190        match solana_net_utils::get_cluster_shred_version_with_binding(entrypoint, bind_address) {
1191            Err(err) => eprintln!("get_cluster_shred_version failed: {entrypoint}, {err}"),
1192            Ok(0) => eprintln!("entrypoint {entrypoint} returned shred-version zero"),
1193            Ok(shred_version) => {
1194                info!("obtained shred-version {shred_version} from {entrypoint}");
1195                return Some(shred_version);
1196            }
1197        }
1198    }
1199    None
1200}
1201
1202fn new_snapshot_config(
1203    matches: &ArgMatches,
1204    ledger_path: &Path,
1205    account_paths: &[PathBuf],
1206    incremental_snapshot_fetch: bool,
1207) -> Result<SnapshotConfig, Box<dyn std::error::Error>> {
1208    let (full_snapshot_archive_interval, incremental_snapshot_archive_interval) =
1209        if matches.is_present("no_snapshots") {
1210            // snapshots are disabled
1211            (SnapshotInterval::Disabled, SnapshotInterval::Disabled)
1212        } else {
1213            match (
1214                incremental_snapshot_fetch,
1215                value_t_or_exit!(matches, "snapshot_interval_slots", NonZeroU64),
1216            ) {
1217                (true, incremental_snapshot_interval_slots) => {
1218                    // incremental snapshots are enabled
1219                    // use --snapshot-interval-slots for the incremental snapshot interval
1220                    let full_snapshot_interval_slots =
1221                        value_t_or_exit!(matches, "full_snapshot_interval_slots", NonZeroU64);
1222                    (
1223                        SnapshotInterval::Slots(full_snapshot_interval_slots),
1224                        SnapshotInterval::Slots(incremental_snapshot_interval_slots),
1225                    )
1226                }
1227                (false, full_snapshot_interval_slots) => {
1228                    // incremental snapshots are *disabled*
1229                    // use --snapshot-interval-slots for the *full* snapshot interval
1230                    // also warn if --full-snapshot-interval-slots was specified
1231                    if matches.occurrences_of("full_snapshot_interval_slots") > 0 {
1232                        warn!(
1233                            "Incremental snapshots are disabled, yet \
1234                             --full-snapshot-interval-slots was specified! Note that \
1235                             --full-snapshot-interval-slots is *ignored* when incremental \
1236                             snapshots are disabled. Use --snapshot-interval-slots instead.",
1237                        );
1238                    }
1239                    (
1240                        SnapshotInterval::Slots(full_snapshot_interval_slots),
1241                        SnapshotInterval::Disabled,
1242                    )
1243                }
1244            }
1245        };
1246
1247    info!(
1248        "Snapshot configuration: full snapshot interval: {}, incremental snapshot interval: {}",
1249        match full_snapshot_archive_interval {
1250            SnapshotInterval::Disabled => "disabled".to_string(),
1251            SnapshotInterval::Slots(interval) => format!("{interval} slots"),
1252        },
1253        match incremental_snapshot_archive_interval {
1254            SnapshotInterval::Disabled => "disabled".to_string(),
1255            SnapshotInterval::Slots(interval) => format!("{interval} slots"),
1256        },
1257    );
1258    // It is unlikely that a full snapshot interval greater than an epoch is a good idea.
1259    // Minimally we should warn the user in case this was a mistake.
1260    if let SnapshotInterval::Slots(full_snapshot_interval_slots) = full_snapshot_archive_interval {
1261        let full_snapshot_interval_slots = full_snapshot_interval_slots.get();
1262        if full_snapshot_interval_slots > DEFAULT_SLOTS_PER_EPOCH {
1263            warn!(
1264                "The full snapshot interval is excessively large: {full_snapshot_interval_slots}! \
1265                 This will negatively impact the background cleanup tasks in accounts-db. \
1266                 Consider a smaller value.",
1267            );
1268        }
1269    }
1270
1271    let snapshots_dir = matches
1272        .value_of("snapshots")
1273        .map(Path::new)
1274        .unwrap_or(ledger_path);
1275    let snapshots_dir = create_and_canonicalize_directory(snapshots_dir).map_err(|err| {
1276        format!(
1277            "failed to create snapshots directory '{}': {err}",
1278            snapshots_dir.display(),
1279        )
1280    })?;
1281    if account_paths
1282        .iter()
1283        .any(|account_path| account_path == &snapshots_dir)
1284    {
1285        Err(
1286            "the --accounts and --snapshots paths must be unique since they both create \
1287             'snapshots' subdirectories, otherwise there may be collisions"
1288                .to_string(),
1289        )?;
1290    }
1291
1292    let bank_snapshots_dir = snapshots_dir.join(BANK_SNAPSHOTS_DIR);
1293    fs::create_dir_all(&bank_snapshots_dir).map_err(|err| {
1294        format!(
1295            "failed to create bank snapshots directory '{}': {err}",
1296            bank_snapshots_dir.display(),
1297        )
1298    })?;
1299
1300    let full_snapshot_archives_dir = matches
1301        .value_of("full_snapshot_archive_path")
1302        .map(PathBuf::from)
1303        .unwrap_or_else(|| snapshots_dir.clone());
1304    fs::create_dir_all(&full_snapshot_archives_dir).map_err(|err| {
1305        format!(
1306            "failed to create full snapshot archives directory '{}': {err}",
1307            full_snapshot_archives_dir.display(),
1308        )
1309    })?;
1310
1311    let incremental_snapshot_archives_dir = matches
1312        .value_of("incremental_snapshot_archive_path")
1313        .map(PathBuf::from)
1314        .unwrap_or_else(|| snapshots_dir.clone());
1315    fs::create_dir_all(&incremental_snapshot_archives_dir).map_err(|err| {
1316        format!(
1317            "failed to create incremental snapshot archives directory '{}': {err}",
1318            incremental_snapshot_archives_dir.display(),
1319        )
1320    })?;
1321
1322    let archive_format = {
1323        let archive_format_str = value_t_or_exit!(matches, "snapshot_archive_format", String);
1324        let mut archive_format = ArchiveFormat::from_cli_arg(&archive_format_str)
1325            .unwrap_or_else(|| panic!("Archive format not recognized: {archive_format_str}"));
1326        if let ArchiveFormat::TarZstd { config } = &mut archive_format {
1327            config.compression_level =
1328                value_t_or_exit!(matches, "snapshot_zstd_compression_level", i32);
1329        }
1330        archive_format
1331    };
1332
1333    let snapshot_version = matches
1334        .value_of("snapshot_version")
1335        .map(|value| {
1336            value
1337                .parse::<SnapshotVersion>()
1338                .map_err(|err| format!("unable to parse snapshot version: {err}"))
1339        })
1340        .transpose()?
1341        .unwrap_or(SnapshotVersion::default());
1342
1343    let maximum_full_snapshot_archives_to_retain =
1344        value_t_or_exit!(matches, "maximum_full_snapshots_to_retain", NonZeroUsize);
1345    let maximum_incremental_snapshot_archives_to_retain = value_t_or_exit!(
1346        matches,
1347        "maximum_incremental_snapshots_to_retain",
1348        NonZeroUsize
1349    );
1350
1351    let snapshot_config = SnapshotConfig {
1352        usage: if full_snapshot_archive_interval == SnapshotInterval::Disabled {
1353            SnapshotUsage::LoadOnly
1354        } else {
1355            SnapshotUsage::LoadAndGenerate
1356        },
1357        full_snapshot_archive_interval,
1358        incremental_snapshot_archive_interval,
1359        bank_snapshots_dir,
1360        full_snapshot_archives_dir,
1361        incremental_snapshot_archives_dir,
1362        archive_format,
1363        snapshot_version,
1364        maximum_full_snapshot_archives_to_retain,
1365        maximum_incremental_snapshot_archives_to_retain,
1366        use_registered_io_uring_buffers: resource_limits::check_memlock_limit_for_disk_io(
1367            solana_accounts_db::accounts_db::TOTAL_IO_URING_BUFFERS_SIZE_LIMIT,
1368        ),
1369        use_direct_io: !matches.is_present("no_accounts_db_snapshots_direct_io"),
1370    };
1371
1372    if !is_snapshot_config_valid(&snapshot_config) {
1373        Err(
1374            "invalid snapshot configuration provided: snapshot intervals are incompatible. full \
1375             snapshot interval MUST be larger than incremental snapshot interval (if enabled)"
1376                .to_string(),
1377        )?;
1378    }
1379
1380    Ok(snapshot_config)
1381}
1382
1383#[cfg(target_os = "linux")]
1384fn build_xdp_config(
1385    matches: &ArgMatches,
1386    operation: &Operation,
1387    bind_addresses: &BindIpAddrs,
1388) -> Result<Option<XdpConfig>, String> {
1389    if matches.is_present("no_xdp") || *operation == Operation::Initialize {
1390        return Ok(None);
1391    }
1392    if bind_addresses.len() > 1 {
1393        return Err(
1394            "XDP cannot be used in a multihoming context; pass --no-xdp to disable XDP".to_string(),
1395        );
1396    }
1397    let xdp_interface = matches
1398        .value_of("xdp_interface")
1399        .or_else(|| matches.value_of("experimental_retransmit_xdp_interface"));
1400    let xdp_zero_copy = matches.is_present("xdp_zero_copy")
1401        || matches.is_present("experimental_retransmit_xdp_zero_copy");
1402    let poh_pinned_cpu_core = value_of(matches, "poh_pinned_cpu_core")
1403        .or_else(|| value_of(matches, "experimental_poh_pinned_cpu_core"))
1404        .or(poh_service::DEFAULT_PINNED_CPU_CORE);
1405    let xdp_cpu_cores = matches
1406        .value_of("xdp_cpu_cores")
1407        .or_else(|| matches.value_of("experimental_retransmit_xdp_cpu_cores"));
1408    let cpus = if let Some(cpu_str) = xdp_cpu_cores {
1409        let parsed =
1410            parse_cpu_ranges(cpu_str).expect("clap validator already accepted this CPU list");
1411        if let Some(poh_core) = poh_pinned_cpu_core
1412            && parsed.contains(&poh_core)
1413        {
1414            return Err(format!(
1415                "--xdp-cpu-cores includes PoH core {poh_core}; XDP and PoH must not share a CPU \
1416                 core"
1417            ));
1418        }
1419        Some(parsed)
1420    } else {
1421        // Auto-select a single core, avoiding the PoH core.
1422        match cpu_affinity(None) {
1423            Ok(allowed) => {
1424                match allowed
1425                    .iter()
1426                    .rev()
1427                    .map(|cpu| **cpu)
1428                    .find(|cpu| Some(*cpu) != poh_pinned_cpu_core)
1429                {
1430                    Some(cpu) => Some(vec![cpu]),
1431                    None => {
1432                        return Err(format!(
1433                            "XDP requires a dedicated CPU core separate from PoH (core \
1434                             {poh_pinned_cpu_core:?}), but none is available. Pass --no-xdp to \
1435                             disable XDP."
1436                        ));
1437                    }
1438                }
1439            }
1440            Err(e) => {
1441                return Err(format!(
1442                    "failed to query CPU affinity: {e}. Pass --no-xdp to disable XDP, or provide \
1443                     --xdp-cpu-cores explicitly."
1444                ));
1445            }
1446        }
1447    };
1448    Ok(cpus.map(|cpus| {
1449        info!("XDP enabled on CPU cores: {cpus:?}");
1450        // Map the CPU list onto hardware queues sequentially (queue i -> cpus[i]).
1451        let queues = cpus
1452            .into_iter()
1453            .enumerate()
1454            .map(|(queue, cpu)| QueueCpuBinding {
1455                queue: queue as u32,
1456                cpu,
1457            })
1458            .collect();
1459        XdpConfig::new(xdp_interface, queues, xdp_zero_copy)
1460    }))
1461}
1462
1463#[cfg(all(target_os = "linux", test))]
1464mod xdp_tests {
1465    use {
1466        super::*,
1467        crate::{cli::DefaultArgs, commands::run::args::add_args},
1468        solana_net_utils::multihomed_sockets::BindIpAddrs,
1469        std::net::{IpAddr, Ipv4Addr},
1470    };
1471
1472    fn single_ip_bind() -> BindIpAddrs {
1473        BindIpAddrs::new(vec![Ipv4Addr::UNSPECIFIED.into()]).unwrap()
1474    }
1475
1476    fn multihoming_bind() -> BindIpAddrs {
1477        BindIpAddrs::new(vec![
1478            IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
1479            IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)),
1480        ])
1481        .unwrap()
1482    }
1483
1484    #[test]
1485    fn test_no_xdp_flag_disables_xdp() {
1486        let default_args = DefaultArgs::default();
1487        let app = add_args(clap::App::new("agave-validator"), &default_args);
1488        let matches = app.get_matches_from(vec!["agave-validator", "--no-xdp"]);
1489        let result = build_xdp_config(&matches, &Operation::Run, &single_ip_bind());
1490        assert!(result.unwrap().is_none(), "--no-xdp must disable XDP");
1491    }
1492
1493    #[test]
1494    fn test_init_disables_xdp() {
1495        let default_args = DefaultArgs::default();
1496        let app = add_args(clap::App::new("agave-validator"), &default_args);
1497        let matches = app.get_matches_from(vec!["agave-validator"]);
1498        let result = build_xdp_config(&matches, &Operation::Initialize, &single_ip_bind());
1499        assert!(result.unwrap().is_none(), "init operation must disable XDP");
1500    }
1501
1502    #[test]
1503    fn test_multihoming_is_error() {
1504        let default_args = DefaultArgs::default();
1505        let app = add_args(clap::App::new("agave-validator"), &default_args);
1506        let matches = app.get_matches_from(vec!["agave-validator"]);
1507        let result = build_xdp_config(&matches, &Operation::Run, &multihoming_bind());
1508        assert!(
1509            result.unwrap_err().contains("multihoming"),
1510            "multihoming context must produce an error"
1511        );
1512    }
1513
1514    #[test]
1515    fn test_explicit_xdp_core_conflicts_with_poh_core_is_error() {
1516        let default_args = DefaultArgs::default();
1517        let app = add_args(clap::App::new("agave-validator"), &default_args);
1518        let poh_core = solana_poh::poh_service::DEFAULT_PINNED_CPU_CORE
1519            .unwrap_or(0)
1520            .to_string();
1521        let matches = app.get_matches_from(vec!["agave-validator", "--xdp-cpu-cores", &poh_core]);
1522        let result = build_xdp_config(&matches, &Operation::Run, &single_ip_bind());
1523        assert!(
1524            result.unwrap_err().contains("PoH core"),
1525            "XDP core overlapping PoH core must produce an error"
1526        );
1527    }
1528}