1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use alloy_node_bindings::{Anvil, AnvilInstance};
5use serde_json::{Map, Value};
6use tokio::signal::unix::{SignalKind, signal};
7
8use lyquor_api::anyhow::{self, Context as _};
9use lyquor_api::log::LogClass;
10use lyquor_config::{
11 config::{DB, NodeConfig},
12 profile::NetworkType,
13};
14use lyquor_lib::node::{LyquorNode, NodeArgs, anvil_state_path};
15
16static ANVIL_PID: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
17
18fn generate_unused_port() -> anyhow::Result<u16> {
19 let listener = std::net::TcpListener::bind("0.0.0.0:0").context("Failed to bind to local address.")?;
20 let port = listener.local_addr().context("Failed to get local address.")?.port();
21 Ok(port)
22}
23
24fn start_devnet_anvil(state_file: Option<&Path>, state_interval_secs: u64) -> anyhow::Result<AnvilInstance> {
25 let anvil_port = generate_unused_port()?;
26 use nix::sys::signal::{self, SigSet, SigmaskHow, Signal};
28 let mut sigset = SigSet::empty();
29 sigset.add(Signal::SIGINT);
30 let mut old_mask = SigSet::empty();
31 signal::sigprocmask(SigmaskHow::SIG_BLOCK, Some(&sigset), Some(&mut old_mask))
32 .context("Failed to mask SIGINT for anvil.")?;
33
34 let mut anvil_builder = Anvil::new().port(anvil_port).keep_stdout();
35 if let Some(state_file) = state_file {
36 if let Some(state_dir) = state_file.parent() {
37 std::fs::create_dir_all(state_dir)?;
38 }
39 anvil_builder = anvil_builder.arg("--state").arg(state_file.as_os_str());
40 if state_interval_secs > 0 {
41 anvil_builder = anvil_builder
42 .arg("--state-interval")
43 .arg(state_interval_secs.to_string());
44 }
45 }
46 let mut anvil = anvil_builder.try_spawn()?;
47
48 let stdout_reader = std::io::BufReader::new(
49 anvil
50 .child_mut()
51 .stdout
52 .take()
53 .context("Failed to read from anvil stdout.")?,
54 );
55 tokio::task::spawn_blocking(move || {
56 use std::io::BufRead;
57 for line in stdout_reader.lines() {
58 tracing::debug!(
59 target: "lyquor_anvil",
60 line = %line.unwrap_or_else(|_| String::new()),
61 "Anvil output"
62 );
63 }
64 });
65
66 tracing::info!(anvil_port, "Anvil started");
67 signal::sigprocmask(SigmaskHow::SIG_SETMASK, Some(&old_mask), None)
68 .context("Failed to restore old signal mask for anvil.")?;
69 Ok(anvil)
70}
71
72fn parse_override_value(raw: &str) -> Value {
73 if let Ok(value) = raw.parse::<bool>() {
74 return Value::from(value);
75 }
76 if let Ok(value) = raw.parse::<u64>() {
77 return Value::from(value);
78 }
79 if let Ok(value) = raw.parse::<i64>() {
80 return Value::from(value);
81 }
82 if let Ok(value) = raw.parse::<f64>() {
83 return Value::from(value);
84 }
85 Value::from(raw.to_string())
86}
87
88fn insert_override_path(overrides: &mut Map<String, Value>, key: &str, value: Value) -> anyhow::Result<()> {
89 let mut segments = key.split('.').peekable();
90 let mut current = overrides;
91
92 while let Some(segment) = segments.next() {
93 if segments.peek().is_none() {
94 current.insert(segment.to_string(), value);
95 return Ok(());
96 }
97
98 let entry = current
99 .entry(segment.to_string())
100 .or_insert_with(|| Value::Object(Map::<String, Value>::new()));
101 match entry {
102 Value::Object(dict) => current = dict,
103 _ => anyhow::bail!("Override path '{key}' conflicts with non-table value."),
104 }
105 }
106
107 Ok(())
108}
109
110fn build_config_overrides(matches: &clap::ArgMatches) -> anyhow::Result<Map<String, Value>> {
111 let mut overrides = Map::new();
112 if let Some(entries) = matches.get_many::<String>("config-override") {
113 for entry in entries {
114 let (key, value) = entry
115 .split_once('=')
116 .ok_or_else(|| anyhow::anyhow!("Invalid --config-override '{entry}', expected key=value"))?;
117 insert_override_path(&mut overrides, key, parse_override_value(value))?;
118 }
119 }
120 Ok(overrides)
121}
122
123const NODE_RUNTIME_THREAD_NAME: &str = "lyquor-node";
124const VM_RUNTIME_THREAD_NAME: &str = "lyquor-vm";
125
126fn build_node_runtime(worker_threads: usize) -> std::io::Result<tokio::runtime::Runtime> {
127 tokio::runtime::Builder::new_multi_thread()
128 .enable_all()
129 .worker_threads(worker_threads)
130 .thread_name(NODE_RUNTIME_THREAD_NAME)
131 .build()
132}
133
134fn build_vm_runtime(worker_threads: usize) -> std::io::Result<tokio::runtime::Runtime> {
135 tokio::runtime::Builder::new_multi_thread()
136 .enable_all()
137 .worker_threads(worker_threads)
138 .thread_name(VM_RUNTIME_THREAD_NAME)
139 .build()
140}
141
142fn load_config_from_args() -> anyhow::Result<NodeConfig> {
143 let matches = clap::command!()
144 .version(lyquor_cli::build_version!())
145 .propagate_version(true)
146 .arg(clap::arg!(--config <PATH> "Path to the Lyquor config file (toml, yaml, json)."))
147 .arg(
148 clap::arg!(--"config-override" <KEY_VALUE> "Override config with key=value (repeatable).")
149 .action(clap::ArgAction::Append)
150 .value_parser(clap::builder::NonEmptyStringValueParser::new()),
151 )
152 .get_matches();
153
154 let config_path = matches.get_one::<String>("config").map(PathBuf::from);
155 NodeConfig::load(config_path, build_config_overrides(&matches)?)
156}
157
158fn main() -> anyhow::Result<()> {
159 std::panic::set_hook(Box::new(|_| {
160 if let Some(&pid) = ANVIL_PID.get() {
161 tracing::warn!(
162 process_id = pid,
163 class = %LogClass::CoreRuntime,
164 "panic detected; stopping Anvil"
165 );
166 use nix::sys::signal::{Signal, kill};
167 let _ = kill(nix::unistd::Pid::from_raw(pid as i32), Signal::SIGTERM);
168 }
169 }));
170
171 lyquor_cli::setup_tracing()?;
172 println!("{}", lyquor_cli::format_logo_banner(lyquor_cli::build_version!()));
173
174 let config = load_config_from_args()?;
175 let runtime = build_node_runtime(config.runtime.node_threads).context("Failed to build node Tokio runtime")?;
176 let vm_runtime = build_vm_runtime(config.runtime.execution_threads).context("Failed to build VM Tokio runtime")?;
177 let vm_runtime_handle = vm_runtime.handle().clone();
178
179 let result = runtime.block_on(run_node(config, vm_runtime_handle));
180 vm_runtime.shutdown_background();
181 result
182}
183
184async fn run_node(config: NodeConfig, vm_runtime: tokio::runtime::Handle) -> anyhow::Result<()> {
185 let mut anvil: Option<AnvilInstance> = None;
186 let seq_eth_url = match config.profile.base {
187 NetworkType::Devnet => match config.profile.sequencer.as_deref() {
188 None => {
189 let state_file = match &config.storage.db {
190 DB::RocksDb { .. } => Some(anvil_state_path(
191 &config.resolved_db_dir(),
192 config.profile.base.as_str(),
193 )),
194 DB::MemDb => None,
195 };
196 let instance = start_devnet_anvil(state_file.as_deref(), config.flush_interval_secs)?;
197 ANVIL_PID.set(instance.child().id()).ok();
198 let endpoint = instance.ws_endpoint();
199 anvil = Some(instance);
200 endpoint
201 }
202 Some(url) => url.to_string(),
203 },
204 _ => anyhow::bail!("Network is not supported."),
205 };
206 let profile = Arc::new(config.profile.resolve(seq_eth_url.clone())?);
207 let node_seed = config.node_key.seed_bytes()?;
208 let (_, tls_config) = lyquor_tls::generator::single_node_config_with_seed(&node_seed, profile.network_domain());
209
210 let mut sigint = signal(SignalKind::interrupt()).context("Failed to register SIGINT handler")?;
213 let mut sigterm = signal(SignalKind::terminate()).context("Failed to register SIGTERM handler")?;
214
215 let node = LyquorNode::start(NodeArgs {
216 config: Arc::new(config),
217 profile,
218 seq_eth_url,
219 tls_config,
220 started_anvil: anvil.is_some(),
221 vm_runtime,
222 })
223 .await?;
224
225 tokio::select! {
226 _ = sigint.recv() => {
227 tracing::info!("received SIGINT");
228 }
229 _ = sigterm.recv() => {
230 tracing::info!("received SIGTERM");
231 }
232 () = node.finished() => {
233 tracing::info!("all node subsystems finished");
234 }
235 }
236 node.shutdown().await;
237 Ok(())
238}