276 lines
11 KiB
Rust
276 lines
11 KiB
Rust
use std::path::PathBuf;
|
||
use std::process::ExitCode;
|
||
|
||
use montana_node::commands::{init, inspect, start, status, time};
|
||
use montana_node::NodeError;
|
||
|
||
fn main() -> ExitCode {
|
||
let argv: Vec<String> = std::env::args().collect();
|
||
if argv.len() < 2 {
|
||
eprintln!("{}", help_text());
|
||
return ExitCode::from(2);
|
||
}
|
||
|
||
let result = match argv[1].as_str() {
|
||
"init" => parse_init(&argv[2..]).and_then(init::run),
|
||
"inspect" => parse_inspect(&argv[2..]).and_then(inspect::run),
|
||
"status" => parse_status(&argv[2..]).and_then(status::run),
|
||
"time" => parse_time(&argv[2..]).and_then(time::run),
|
||
"start" => parse_start(&argv[2..]).and_then(start::run),
|
||
"help" | "-h" | "--help" => {
|
||
println!("{}", help_text());
|
||
return ExitCode::SUCCESS;
|
||
},
|
||
other => {
|
||
eprintln!("неизвестная команда: {other}");
|
||
eprintln!();
|
||
eprintln!("{}", help_text());
|
||
return ExitCode::from(2);
|
||
},
|
||
};
|
||
|
||
match result {
|
||
Ok(()) => ExitCode::SUCCESS,
|
||
Err(e) => {
|
||
eprintln!("ошибка: {e}");
|
||
ExitCode::from(1)
|
||
},
|
||
}
|
||
}
|
||
|
||
fn help_text() -> String {
|
||
String::from(
|
||
"montana-node — узел Montana (singleton либо cross-machine M8 mode)\n\
|
||
\n\
|
||
Использование:\n\
|
||
\n\
|
||
montana-node init [--data-dir <PATH>] [--mnemonic \"<24 слова>\"]\n\
|
||
[--entropy <hex32>] [--force]\n\
|
||
\n\
|
||
montana-node inspect [--data-dir <PATH>] [--reveal-master-seed]\n\
|
||
\n\
|
||
montana-node status [--data-dir <PATH>]\n\
|
||
\n\
|
||
montana-node time [--data-dir <PATH>]\n\
|
||
\n\
|
||
montana-node start [--data-dir <PATH>] [--max-windows <N>]\n\
|
||
[--d-test-override <N>]\n\
|
||
[--listen <multiaddr>] [--genesis-manifest <PATH>]\n\
|
||
\n\
|
||
Команды:\n\
|
||
\n\
|
||
init Создать identity (мнемоника + ключи) и сохранить в identity.bin.\n\
|
||
Если ни --mnemonic, ни --entropy не указаны — генерируется\n\
|
||
случайная энтропия через системный ГСЧ.\n\
|
||
\n\
|
||
inspect Прочитать identity.bin и вывести account_id / node_id /\n\
|
||
fingerprint. Секретные ключи на экран не выводятся.\n\
|
||
\n\
|
||
status Показать содержимое локального state: AccountTable,\n\
|
||
NodeTable, CandidatePool, статус узла, current_window,\n\
|
||
phase lifecycle (Bootstrap/CandidateVdf/Registered/Active),\n\
|
||
supply, балансы.\n\
|
||
\n\
|
||
time Показать current_window локального узла, ближайшее\n\
|
||
selection-окно, эпоху τ₂.\n\
|
||
\n\
|
||
start БОЕВОЙ РЕЖИМ — запуск узла Montana через canonical\n\
|
||
apply_proposal pipeline. Узел проходит lifecycle:\n\
|
||
Bootstrap → CandidateVdf (тикает VDF до vdf_chain_length\n\
|
||
≥ τ₂ = 20160 окон, ~10 часов\n\
|
||
wall-clock на M-class Mac)\n\
|
||
CandidateVdf → Registered (формирует NodeRegistration\n\
|
||
через apply_noderegistrations_batch)\n\
|
||
Registered → Active (через apply_selection_event на\n\
|
||
следующем W % 336 == 0)\n\
|
||
Active: per окно VdfReveal + BundledConfirmation +\n\
|
||
ProposalHeader + apply_proposal + archive_proposal,\n\
|
||
state_root self-verify, эмиссия 13 Ɉ оператору\n\
|
||
Ctrl-C — корректная остановка с сохранением state.\n\
|
||
\n\
|
||
Опции:\n\
|
||
\n\
|
||
--data-dir <PATH> Каталог данных узла. По умолчанию на macOS:\n\
|
||
$HOME/Library/Application Support/Montana/node\n\
|
||
--mnemonic \"...\" Восстановить identity из 24-словной фразы.\n\
|
||
--entropy <hex32> Использовать 32-байтную энтропию (64 hex).\n\
|
||
--force Перезаписать существующий identity.bin.\n\
|
||
--reveal-master-seed В inspect: показать полный master_seed.\n\
|
||
--max-windows <N> В start: остановиться после N окон.\n\
|
||
--d-test-override <N> В start: TEST-ONLY override D = N итераций.\n\
|
||
Production использует params.d0 = 300_000_000.\n\
|
||
Override используется в тестах для скорости.\n",
|
||
)
|
||
}
|
||
|
||
fn parse_init(args: &[String]) -> Result<init::InitArgs, NodeError> {
|
||
let mut data_dir: Option<PathBuf> = None;
|
||
let mut mnemonic: Option<String> = None;
|
||
let mut entropy_hex: Option<String> = None;
|
||
let mut force = false;
|
||
|
||
let mut i = 0;
|
||
while i < args.len() {
|
||
match args[i].as_str() {
|
||
"--data-dir" => {
|
||
data_dir = Some(PathBuf::from(expect_value(args, i, "--data-dir")?));
|
||
i += 2;
|
||
},
|
||
"--mnemonic" => {
|
||
mnemonic = Some(expect_value(args, i, "--mnemonic")?.to_string());
|
||
i += 2;
|
||
},
|
||
"--entropy" => {
|
||
entropy_hex = Some(expect_value(args, i, "--entropy")?.to_string());
|
||
i += 2;
|
||
},
|
||
"--force" => {
|
||
force = true;
|
||
i += 1;
|
||
},
|
||
other => {
|
||
return Err(NodeError::InvalidArguments(format!(
|
||
"неизвестный флаг для init: {other}"
|
||
)))
|
||
},
|
||
}
|
||
}
|
||
Ok(init::InitArgs {
|
||
data_dir,
|
||
mnemonic,
|
||
entropy_hex,
|
||
force,
|
||
})
|
||
}
|
||
|
||
fn parse_inspect(args: &[String]) -> Result<inspect::InspectArgs, NodeError> {
|
||
let mut data_dir: Option<PathBuf> = None;
|
||
let mut reveal_master_seed = false;
|
||
let mut i = 0;
|
||
while i < args.len() {
|
||
match args[i].as_str() {
|
||
"--data-dir" => {
|
||
data_dir = Some(PathBuf::from(expect_value(args, i, "--data-dir")?));
|
||
i += 2;
|
||
},
|
||
"--reveal-master-seed" => {
|
||
reveal_master_seed = true;
|
||
i += 1;
|
||
},
|
||
other => {
|
||
return Err(NodeError::InvalidArguments(format!(
|
||
"неизвестный флаг для inspect: {other}"
|
||
)))
|
||
},
|
||
}
|
||
}
|
||
Ok(inspect::InspectArgs {
|
||
data_dir,
|
||
reveal_master_seed,
|
||
})
|
||
}
|
||
|
||
fn parse_status(args: &[String]) -> Result<status::StatusArgs, NodeError> {
|
||
let mut data_dir: Option<PathBuf> = None;
|
||
let mut i = 0;
|
||
while i < args.len() {
|
||
match args[i].as_str() {
|
||
"--data-dir" => {
|
||
data_dir = Some(PathBuf::from(expect_value(args, i, "--data-dir")?));
|
||
i += 2;
|
||
},
|
||
other => {
|
||
return Err(NodeError::InvalidArguments(format!(
|
||
"неизвестный флаг для status: {other}"
|
||
)))
|
||
},
|
||
}
|
||
}
|
||
Ok(status::StatusArgs { data_dir })
|
||
}
|
||
|
||
fn parse_time(args: &[String]) -> Result<time::TimeArgs, NodeError> {
|
||
let mut data_dir: Option<PathBuf> = None;
|
||
let mut i = 0;
|
||
while i < args.len() {
|
||
match args[i].as_str() {
|
||
"--data-dir" => {
|
||
data_dir = Some(PathBuf::from(expect_value(args, i, "--data-dir")?));
|
||
i += 2;
|
||
},
|
||
other => {
|
||
return Err(NodeError::InvalidArguments(format!(
|
||
"неизвестный флаг для time: {other}"
|
||
)))
|
||
},
|
||
}
|
||
}
|
||
Ok(time::TimeArgs { data_dir })
|
||
}
|
||
|
||
fn parse_start(args: &[String]) -> Result<start::StartArgs, NodeError> {
|
||
let mut data_dir: Option<PathBuf> = None;
|
||
let mut max_windows: Option<u64> = None;
|
||
let mut d_test_override: Option<u64> = None;
|
||
let mut listen_multiaddr: Option<String> = None;
|
||
let mut genesis_manifest: Option<PathBuf> = None;
|
||
let mut i = 0;
|
||
while i < args.len() {
|
||
match args[i].as_str() {
|
||
"--data-dir" => {
|
||
data_dir = Some(PathBuf::from(expect_value(args, i, "--data-dir")?));
|
||
i += 2;
|
||
},
|
||
"--max-windows" => {
|
||
max_windows = Some(expect_value(args, i, "--max-windows")?.parse().map_err(
|
||
|_| NodeError::InvalidArguments("--max-windows должен быть u64".into()),
|
||
)?);
|
||
i += 2;
|
||
},
|
||
"--d-test-override" => {
|
||
d_test_override = Some(
|
||
expect_value(args, i, "--d-test-override")?
|
||
.parse()
|
||
.map_err(|_| {
|
||
NodeError::InvalidArguments("--d-test-override должен быть u64".into())
|
||
})?,
|
||
);
|
||
i += 2;
|
||
},
|
||
"--listen" => {
|
||
listen_multiaddr = Some(expect_value(args, i, "--listen")?.to_string());
|
||
i += 2;
|
||
},
|
||
"--genesis-manifest" => {
|
||
genesis_manifest =
|
||
Some(PathBuf::from(expect_value(args, i, "--genesis-manifest")?));
|
||
i += 2;
|
||
},
|
||
other => {
|
||
return Err(NodeError::InvalidArguments(format!(
|
||
"неизвестный флаг для start: {other}"
|
||
)))
|
||
},
|
||
}
|
||
}
|
||
if listen_multiaddr.is_some() != genesis_manifest.is_some() {
|
||
return Err(NodeError::InvalidArguments(
|
||
"--listen и --genesis-manifest должны указываться вместе (cross-machine mode) либо оба отсутствовать (singleton mode)"
|
||
.into(),
|
||
));
|
||
}
|
||
Ok(start::StartArgs {
|
||
data_dir,
|
||
max_windows,
|
||
d_test_override,
|
||
listen_multiaddr,
|
||
genesis_manifest,
|
||
})
|
||
}
|
||
|
||
fn expect_value<'a>(args: &'a [String], i: usize, flag: &str) -> Result<&'a str, NodeError> {
|
||
args.get(i + 1)
|
||
.map(|s| s.as_str())
|
||
.ok_or_else(|| NodeError::InvalidArguments(format!("флаг {flag} требует значения")))
|
||
}
|