Files
lighthouse/lcli/src/main.rs
Michael Sproul 42f6d7b02d Yeet env_logger into the sun (#7872)
- Remove explicit `env_logger` usage from `state_processing` tests and `lcli`.
- Set up tracing correctly for `lcli` (I've checked that we can see logs after this change).
- I didn't do anything to set up logging for the `state_processing` tests, as these are rarely run manually (they never fail). We could add `test_logger` in there on an as-needed basis.
2025-08-15 03:17:26 +00:00

767 lines
32 KiB
Rust

mod block_root;
mod check_deposit_data;
mod generate_bootnode_enr;
mod http_sync;
mod indexed_attestations;
mod mnemonic_validators;
mod mock_el;
mod parse_ssz;
mod skip_slots;
mod state_root;
mod transition_blocks;
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::{FLAG_HEADER, parse_optional};
use environment::{EnvironmentBuilder, LoggerConfig};
use eth2_network_config::Eth2NetworkConfig;
use parse_ssz::run_parse_ssz;
use std::path::PathBuf;
use std::process;
use std::str::FromStr;
use tracing_subscriber::{filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt};
use types::{EthSpec, EthSpecId};
fn main() {
let matches = Command::new("Lighthouse CLI Tool")
.version(lighthouse_version::VERSION)
.display_order(0)
.about("Performs various testing-related tasks, including defining testnets.")
.arg(
Arg::new("spec")
.short('s')
.long("spec")
.value_name("STRING")
.action(ArgAction::Set)
.value_parser(["minimal", "mainnet", "gnosis"])
.default_value("mainnet")
.global(true)
.display_order(0)
)
.arg(
Arg::new("testnet-dir")
.short('d')
.long("testnet-dir")
.value_name("PATH")
.action(ArgAction::Set)
.global(true)
.help("The testnet dir.")
.display_order(0)
)
.arg(
Arg::new("network")
.long("network")
.value_name("NAME")
.action(ArgAction::Set)
.global(true)
.help("The network to use. Defaults to mainnet.")
.conflicts_with("testnet-dir")
.display_order(0)
)
.subcommand(
Command::new("skip-slots")
.about(
"Performs a state transition from some state across some number of skip slots",
)
.arg(
Arg::new("output-path")
.long("output-path")
.value_name("PATH")
.action(ArgAction::Set)
.help("Path to output a SSZ file.")
.display_order(0)
)
.arg(
Arg::new("pre-state-path")
.long("pre-state-path")
.value_name("PATH")
.action(ArgAction::Set)
.conflicts_with("beacon-url")
.help("Path to a SSZ file of the pre-state.")
.display_order(0)
)
.arg(
Arg::new("beacon-url")
.long("beacon-url")
.value_name("URL")
.action(ArgAction::Set)
.help("URL to a beacon-API provider.")
.display_order(0)
)
.arg(
Arg::new("state-id")
.long("state-id")
.value_name("STATE_ID")
.action(ArgAction::Set)
.requires("beacon-url")
.help("Identifier for a state as per beacon-API standards (slot, root, etc.)")
.display_order(0)
)
.arg(
Arg::new("runs")
.long("runs")
.value_name("INTEGER")
.action(ArgAction::Set)
.default_value("1")
.help("Number of repeat runs, useful for benchmarking.")
.display_order(0)
)
.arg(
Arg::new("state-root")
.long("state-root")
.value_name("HASH256")
.action(ArgAction::Set)
.help("Tree hash root of the provided state, to avoid computing it.")
.display_order(0)
)
.arg(
Arg::new("slots")
.long("slots")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("Number of slots to skip forward.")
.display_order(0)
)
.arg(
Arg::new("partial-state-advance")
.long("partial-state-advance")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help("If present, don't compute state roots when skipping forward.")
.display_order(0)
)
)
.subcommand(
Command::new("transition-blocks")
.about("Performs a state transition given a pre-state and block")
.arg(
Arg::new("pre-state-path")
.long("pre-state-path")
.value_name("PATH")
.action(ArgAction::Set)
.conflicts_with("beacon-url")
.requires("block-path")
.help("Path to load a BeaconState from as SSZ.")
.display_order(0)
)
.arg(
Arg::new("block-path")
.long("block-path")
.value_name("PATH")
.action(ArgAction::Set)
.conflicts_with("beacon-url")
.requires("pre-state-path")
.help("Path to load a SignedBeaconBlock from as SSZ.")
.display_order(0)
)
.arg(
Arg::new("post-state-output-path")
.long("post-state-output-path")
.value_name("PATH")
.action(ArgAction::Set)
.help("Path to output the post-state.")
.display_order(0)
)
.arg(
Arg::new("pre-state-output-path")
.long("pre-state-output-path")
.value_name("PATH")
.action(ArgAction::Set)
.help("Path to output the pre-state, useful when used with --beacon-url.")
.display_order(0)
)
.arg(
Arg::new("block-output-path")
.long("block-output-path")
.value_name("PATH")
.action(ArgAction::Set)
.help("Path to output the block, useful when used with --beacon-url.")
.display_order(0)
)
.arg(
Arg::new("beacon-url")
.long("beacon-url")
.value_name("URL")
.action(ArgAction::Set)
.help("URL to a beacon-API provider.")
.display_order(0)
)
.arg(
Arg::new("block-id")
.long("block-id")
.value_name("BLOCK_ID")
.action(ArgAction::Set)
.requires("beacon-url")
.help("Identifier for a block as per beacon-API standards (slot, root, etc.)")
.display_order(0)
)
.arg(
Arg::new("runs")
.long("runs")
.value_name("INTEGER")
.action(ArgAction::Set)
.default_value("1")
.help("Number of repeat runs, useful for benchmarking.")
.display_order(0)
)
.arg(
Arg::new("no-signature-verification")
.long("no-signature-verification")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help("Disable signature verification.")
.display_order(0)
)
.arg(
Arg::new("exclude-cache-builds")
.long("exclude-cache-builds")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help("If present, pre-build the committee and tree-hash caches without \
including them in the timings.")
.display_order(0)
)
.arg(
Arg::new("exclude-post-block-thc")
.long("exclude-post-block-thc")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help("If present, don't rebuild the tree-hash-cache after applying \
the block.")
.display_order(0)
)
)
.subcommand(
Command::new("pretty-ssz")
.about("Parses SSZ-encoded data from a file")
.arg(
Arg::new("format")
.short('f')
.long("format")
.value_name("FORMAT")
.action(ArgAction::Set)
.required(false)
.default_value("json")
.value_parser(["json", "yaml"])
.help("Output format to use")
.display_order(0)
)
.arg(
Arg::new("type")
.value_name("TYPE")
.action(ArgAction::Set)
.required(true)
.help("Type to decode")
.display_order(0)
)
.arg(
Arg::new("ssz-file")
.value_name("FILE")
.action(ArgAction::Set)
.required(true)
.help("Path to SSZ bytes")
.display_order(0)
)
)
.subcommand(
Command::new("check-deposit-data")
.about("Checks the integrity of some deposit data.")
.arg(
Arg::new("deposit-amount")
.index(1)
.value_name("GWEI")
.action(ArgAction::Set)
.required(true)
.help("The amount (in Gwei) that was deposited")
.display_order(0)
)
.arg(
Arg::new("deposit-data")
.index(2)
.value_name("HEX")
.action(ArgAction::Set)
.required(true)
.help(
"A 0x-prefixed hex string of the deposit data. Should include the
function signature.",
)
.display_order(0)
),
)
.subcommand(
Command::new("generate-bootnode-enr")
.about("Generates an ENR address to be used as a pre-genesis boot node.")
.arg(
Arg::new("ip")
.long("ip")
.value_name("IP_ADDRESS")
.action(ArgAction::Set)
.required(true)
.help("The IP address to be included in the ENR and used for discovery")
.display_order(0)
)
.arg(
Arg::new("udp-port")
.long("udp-port")
.value_name("UDP_PORT")
.action(ArgAction::Set)
.required(true)
.help("The UDP port to be included in the ENR and used for discovery")
.display_order(0)
)
.arg(
Arg::new("tcp-port")
.long("tcp-port")
.value_name("TCP_PORT")
.action(ArgAction::Set)
.required(true)
.help(
"The TCP port to be included in the ENR and used for application comms",
)
.display_order(0)
)
.arg(
Arg::new("output-dir")
.long("output-dir")
.value_name("OUTPUT_DIRECTORY")
.action(ArgAction::Set)
.required(true)
.help("The directory in which to create the network dir")
.display_order(0)
)
.arg(
Arg::new("genesis-fork-version")
.long("genesis-fork-version")
.value_name("HEX")
.action(ArgAction::Set)
.required(true)
.help(
"Used to avoid reply attacks between testnets. Recommended to set to
non-default.",
)
.display_order(0)
),
)
.subcommand(
Command::new("mnemonic-validators")
.about("Produces validator directories by deriving the keys from \
a mnemonic. For testing purposes only, DO NOT USE IN \
PRODUCTION!")
.arg(
Arg::new("count")
.long("count")
.value_name("COUNT")
.action(ArgAction::Set)
.required(true)
.help("Produces validators in the range of 0..count.")
.display_order(0)
)
.arg(
Arg::new("base-dir")
.long("base-dir")
.value_name("BASE_DIR")
.action(ArgAction::Set)
.required(true)
.help("The base directory where validator keypairs and secrets are stored")
.display_order(0)
)
.arg(
Arg::new("node-count")
.long("node-count")
.value_name("NODE_COUNT")
.action(ArgAction::Set)
.help("The number of nodes to divide the validator keys to")
.display_order(0)
)
.arg(
Arg::new("mnemonic-phrase")
.long("mnemonic-phrase")
.value_name("MNEMONIC_PHRASE")
.action(ArgAction::Set)
.required(true)
.help("The mnemonic with which we generate the validator keys")
.display_order(0)
)
)
.subcommand(
Command::new("indexed-attestations")
.about("Convert attestations to indexed form, using the committees from a state.")
.arg(
Arg::new("state")
.long("state")
.value_name("SSZ_STATE")
.action(ArgAction::Set)
.required(true)
.help("BeaconState to generate committees from (SSZ)")
.display_order(0)
)
.arg(
Arg::new("attestations")
.long("attestations")
.value_name("JSON_ATTESTATIONS")
.action(ArgAction::Set)
.required(true)
.help("List of Attestations to convert to indexed form (JSON)")
.display_order(0)
)
)
.subcommand(
Command::new("block-root")
.about("Computes the block root of some block.")
.arg(
Arg::new("block-path")
.long("block-path")
.value_name("PATH")
.action(ArgAction::Set)
.conflicts_with("beacon-url")
.help("Path to load a SignedBeaconBlock from as SSZ.")
.display_order(0)
)
.arg(
Arg::new("beacon-url")
.long("beacon-url")
.value_name("URL")
.action(ArgAction::Set)
.help("URL to a beacon-API provider.")
.display_order(0)
)
.arg(
Arg::new("block-id")
.long("block-id")
.value_name("BLOCK_ID")
.action(ArgAction::Set)
.requires("beacon-url")
.help("Identifier for a block as per beacon-API standards (slot, root, etc.)")
.display_order(0)
)
.arg(
Arg::new("runs")
.long("runs")
.value_name("INTEGER")
.action(ArgAction::Set)
.default_value("1")
.help("Number of repeat runs, useful for benchmarking.")
.display_order(0)
)
)
.subcommand(
Command::new("state-root")
.about("Computes the state root of some state.")
.arg(
Arg::new("state-path")
.long("state-path")
.value_name("PATH")
.action(ArgAction::Set)
.conflicts_with("beacon-url")
.help("Path to load a BeaconState from as SSZ.")
.display_order(0)
)
.arg(
Arg::new("beacon-url")
.long("beacon-url")
.value_name("URL")
.action(ArgAction::Set)
.help("URL to a beacon-API provider.")
.display_order(0)
)
.arg(
Arg::new("state-id")
.long("state-id")
.value_name("BLOCK_ID")
.action(ArgAction::Set)
.requires("beacon-url")
.help("Identifier for a state as per beacon-API standards (slot, root, etc.)")
.display_order(0)
)
.arg(
Arg::new("runs")
.long("runs")
.value_name("INTEGER")
.action(ArgAction::Set)
.default_value("1")
.help("Number of repeat runs, useful for benchmarking.")
.display_order(0)
)
)
.subcommand(
Command::new("mock-el")
.about("Creates a mock execution layer server. This is NOT SAFE and should only \
be used for testing and development on testnets. Do not use in production. Do not \
use on mainnet. It cannot perform validator duties.")
.arg(
Arg::new("jwt-output-path")
.long("jwt-output-path")
.value_name("PATH")
.action(ArgAction::Set)
.required(true)
.help("Path to write the JWT secret.")
.display_order(0)
)
.arg(
Arg::new("listen-address")
.long("listen-address")
.value_name("IP_ADDRESS")
.action(ArgAction::Set)
.help("The server will listen on this address.")
.default_value("127.0.0.1")
.display_order(0)
)
.arg(
Arg::new("listen-port")
.long("listen-port")
.value_name("PORT")
.action(ArgAction::Set)
.help("The server will listen on this port.")
.default_value("8551")
.display_order(0)
)
.arg(
Arg::new("all-payloads-valid")
.long("all-payloads-valid")
.action(ArgAction::Set)
.help("Controls the response to newPayload and forkchoiceUpdated. \
Set to 'true' to return VALID. Set to 'false' to return SYNCING.")
.default_value("false")
.hide(true)
.display_order(0)
)
.arg(
Arg::new("shanghai-time")
.long("shanghai-time")
.value_name("UNIX_TIMESTAMP")
.action(ArgAction::Set)
.help("The payload timestamp that enables Shanghai. Defaults to the mainnet value.")
.default_value("1681338479")
.display_order(0)
)
.arg(
Arg::new("cancun-time")
.long("cancun-time")
.value_name("UNIX_TIMESTAMP")
.action(ArgAction::Set)
.help("The payload timestamp that enables Cancun. No default is provided \
until Cancun is triggered on mainnet.")
.display_order(0)
)
.arg(
Arg::new("prague-time")
.long("prague-time")
.value_name("UNIX_TIMESTAMP")
.action(ArgAction::Set)
.help("The payload timestamp that enables Prague. No default is provided \
until Prague is triggered on mainnet.")
.display_order(0)
)
.arg(
Arg::new("osaka-time")
.long("osaka-time")
.value_name("UNIX_TIMESTAMP")
.action(ArgAction::Set)
.help("The payload timestamp that enables Osaka. No default is provided \
until Osaka is triggered on mainnet.")
.display_order(0)
)
)
.subcommand(
Command::new("http-sync")
.about("Manual sync")
.arg(
Arg::new("start-block")
.long("start-block")
.value_name("BLOCK_ID")
.action(ArgAction::Set)
.help("Block ID of source's head")
.default_value("head")
.required(true)
.display_order(0)
)
.arg(
Arg::new("source-url")
.long("source-url")
.value_name("URL")
.action(ArgAction::Set)
.help("URL to a synced beacon-API provider")
.required(true)
.display_order(0)
)
.arg(
Arg::new("target-url")
.long("target-url")
.value_name("URL")
.action(ArgAction::Set)
.help("URL to an unsynced beacon-API provider")
.required(true)
.display_order(0)
)
.arg(
Arg::new("testnet-dir")
.short('d')
.long("testnet-dir")
.value_name("PATH")
.action(ArgAction::Set)
.global(true)
.help("The testnet dir.")
.display_order(0)
)
.arg(
Arg::new("network")
.long("network")
.value_name("NAME")
.action(ArgAction::Set)
.global(true)
.help("The network to use. Defaults to mainnet.")
.conflicts_with("testnet-dir")
.display_order(0)
)
.arg(
Arg::new("known-common-ancestor")
.long("known-common-ancestor")
.value_name("BLOCK_ID")
.action(ArgAction::Set)
.help("Block ID of common ancestor, if known.")
.display_order(0)
)
.arg(
Arg::new("block-cache-dir")
.long("block-cache-dir")
.value_name("PATH")
.action(ArgAction::Set)
.help("Directory to keep a cache of the downloaded SSZ blocks.")
.display_order(0)
)
)
.get_matches();
let result = matches
.get_one::<String>("spec")
.ok_or_else(|| "Missing --spec flag".to_string())
.and_then(|s| FromStr::from_str(s))
.and_then(|eth_spec_id| match eth_spec_id {
EthSpecId::Minimal => run(EnvironmentBuilder::minimal(), &matches),
EthSpecId::Mainnet => run(EnvironmentBuilder::mainnet(), &matches),
EthSpecId::Gnosis => run(EnvironmentBuilder::gnosis(), &matches),
});
match result {
Ok(()) => process::exit(0),
Err(e) => {
println!("Failed to run lcli: {}", e);
process::exit(1)
}
}
}
fn run<E: EthSpec>(env_builder: EnvironmentBuilder<E>, matches: &ArgMatches) -> Result<(), String> {
let (env_builder, file_logging_layer, stdout_logging_layer, _sse_logging_layer_opt) =
env_builder
.multi_threaded_tokio_runtime()
.map_err(|e| format!("should start tokio runtime: {:?}", e))?
.init_tracing(
LoggerConfig {
path: None,
debug_level: LevelFilter::TRACE,
logfile_debug_level: LevelFilter::TRACE,
log_format: None,
logfile_format: None,
log_color: true,
logfile_color: false,
disable_log_timestamp: false,
max_log_size: 0,
max_log_number: 0,
compression: false,
is_restricted: true,
sse_logging: false, // No SSE Logging in LCLI
extra_info: false,
},
"",
0o600,
);
let env = env_builder
.build()
.map_err(|e| format!("should build env: {:?}", e))?;
let mut logging_layers = vec![file_logging_layer];
if let Some(stdout) = stdout_logging_layer {
logging_layers.push(stdout);
}
let logging_result = tracing_subscriber::registry()
.with(logging_layers)
.try_init();
if let Err(e) = logging_result {
eprintln!("Failed to initialize logger: {e}");
}
// Determine testnet-dir path or network name depending on CLI flags.
let (testnet_dir, network_name) =
if let Some(testnet_dir) = parse_optional::<PathBuf>(matches, "testnet-dir")? {
(Some(testnet_dir), None)
} else {
let network_name =
parse_optional(matches, "network")?.unwrap_or_else(|| "mainnet".to_string());
(None, Some(network_name))
};
let get_network_config = || {
if let Some(testnet_dir) = &testnet_dir {
Eth2NetworkConfig::load(testnet_dir.clone()).map_err(|e| {
format!(
"Unable to open testnet dir at {}: {}",
testnet_dir.display(),
e
)
})
} else {
let network_name = network_name.ok_or("no network name or testnet-dir provided")?;
Eth2NetworkConfig::constant(&network_name)?.ok_or("invalid network name".into())
}
};
match matches.subcommand() {
Some(("transition-blocks", matches)) => {
let network_config = get_network_config()?;
transition_blocks::run::<E>(env, network_config, matches)
.map_err(|e| format!("Failed to transition blocks: {}", e))
}
Some(("skip-slots", matches)) => {
let network_config = get_network_config()?;
skip_slots::run::<E>(env, network_config, matches)
.map_err(|e| format!("Failed to skip slots: {}", e))
}
Some(("pretty-ssz", matches)) => {
let network_config = get_network_config()?;
run_parse_ssz::<E>(network_config, matches)
.map_err(|e| format!("Failed to pretty print hex: {}", e))
}
Some(("check-deposit-data", matches)) => check_deposit_data::run(matches)
.map_err(|e| format!("Failed to run check-deposit-data command: {}", e)),
Some(("generate-bootnode-enr", matches)) => {
generate_bootnode_enr::run::<E>(matches, &env.eth2_config.spec)
.map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e))
}
Some(("mnemonic-validators", matches)) => mnemonic_validators::run(matches)
.map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)),
Some(("indexed-attestations", matches)) => indexed_attestations::run::<E>(matches)
.map_err(|e| format!("Failed to run indexed-attestations command: {}", e)),
Some(("block-root", matches)) => {
let network_config = get_network_config()?;
block_root::run::<E>(env, network_config, matches)
.map_err(|e| format!("Failed to run block-root command: {}", e))
}
Some(("state-root", matches)) => {
let network_config = get_network_config()?;
state_root::run::<E>(env, network_config, matches)
.map_err(|e| format!("Failed to run state-root command: {}", e))
}
Some(("mock-el", matches)) => mock_el::run::<E>(env, matches)
.map_err(|e| format!("Failed to run mock-el command: {}", e)),
Some(("http-sync", matches)) => {
let network_config = get_network_config()?;
http_sync::run::<E>(env, network_config, matches)
.map_err(|e| format!("Failed to run http-sync command: {}", e))
}
Some((other, _)) => Err(format!("Unknown subcommand {}. See --help.", other)),
_ => Err("No subcommand provided. See --help.".to_string()),
}
}