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