Skip to main content

lyquor/
lyquor.rs

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    // Mask SIGINT so Anvil is terminated only when this owner drops it.
27    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    // Install handlers before startup so a signal received while the node is becoming ready is
211    // observed as soon as startup completes.
212    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}