From 879486b3673146cdce3b63504631260a4139341e Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Wed, 19 Aug 2026 11:26:41 +0530 Subject: [PATCH] smite-scenarios: sync target chain view via RPC after mining Targets previously learned about new blocks through bitcoind's blocknotify, which fires asynchronously. When multiple blocks are mined, this can queue up many sync calls, adding unnecessary load. Instead, explicitly sync the target once after each MineBlocks operation. Introduce a TargetRpc trait with a chain_sync() method, called by the executor immediately after MineBlocks. CLN and LDK sync explicitly via RPC and signals, respectively, while LND and Eclair use ZMQ and remain no-ops. Dropping -blocknotify also removes the burst of asynchronous SIGUSR1s during initial block generation, so LDK no longer needs the pre_exec SIG_IGN. Signed-off-by: Nishant Bansal --- smite-scenarios/src/executor.rs | 170 ++++++++++++++++++++++---- smite-scenarios/src/scenarios/ir.rs | 10 +- smite-scenarios/src/targets.rs | 21 +++- smite-scenarios/src/targets/cln.rs | 92 ++++++++++++-- smite-scenarios/src/targets/eclair.rs | 17 ++- smite-scenarios/src/targets/ldk.rs | 70 +++++++---- smite-scenarios/src/targets/lnd.rs | 17 ++- workloads/ldk/src/main.rs | 20 +-- 8 files changed, 341 insertions(+), 76 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 1c01b145..fc25ddc2 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -20,6 +20,8 @@ use smite::noise::{ConnectionError, NoiseConnection}; use smite::oracles::{AcceptChannelContext, AcceptChannelOracle, Oracle}; use smite::pending_channel::PendingChannel; use smite::violation::Violation; + +use super::targets::TargetRpc; use smite_ir::operation::AcceptChannelField; use smite_ir::{Operation, Program, Variable}; use std::collections::{HashMap, HashSet}; @@ -225,11 +227,13 @@ pub enum ExecuteError { } /// Executes IR programs against a target over an established connection. -pub struct Executor { +pub struct Executor { /// Connection used to send and receive Lightning messages. conn: C, /// Interface to bitcoind for wallet and chain operations. bitcoin_cli: B, + /// Interface for interacting with the target node through RPC. + rpc: R, /// Immutable state captured during snapshot setup. context: ProgramContext, /// Channel states maintained implicitly across program execution, keyed by @@ -255,13 +259,15 @@ pub struct Executor { mined_txids: HashSet, } -impl Executor { - /// Creates an executor with the given connection, bitcoin-cli handle, and - /// program context. Channel state and negotiations start empty. - pub fn new(conn: C, bitcoin_cli: B, context: ProgramContext) -> Self { +impl Executor { + /// Creates an executor with the given connection, bitcoin-cli handle, + /// program context, and target RPC handle. Channel state and negotiations + /// start empty. + pub fn new(conn: C, bitcoin_cli: B, rpc: R, context: ProgramContext) -> Self { Self { conn, bitcoin_cli, + rpc, context, channel_states: HashMap::new(), negotiations: HashMap::new(), @@ -512,6 +518,7 @@ impl Executor { .map(|(_, hex)| hex) .collect(); self.bitcoin_cli.mine_blocks(*v, &private_mempool); + self.rpc.chain_sync(); self.mined_txids.extend(self.unmined_txids.drain()); log::debug!("[{:?}] MineBlocks: mined {} block(s)", start.elapsed(), v); None @@ -1542,6 +1549,19 @@ mod tests { } } + // Mocking TargetRpc via MockTargetRpc + + #[derive(Default)] + struct MockTargetRpc { + chain_syncs: usize, + } + + impl TargetRpc for MockTargetRpc { + fn chain_sync(&mut self) { + self.chain_syncs += 1; + } + } + // -- Helpers -- fn sample_pubkey(byte: u8) -> PublicKey { @@ -1849,6 +1869,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -1933,6 +1954,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2004,6 +2026,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2096,6 +2119,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2207,6 +2231,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2303,6 +2328,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2352,6 +2378,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2414,6 +2441,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ac_bytes); @@ -2439,6 +2467,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(init_bytes); @@ -2472,6 +2501,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(error_bytes); @@ -2504,6 +2534,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ping_bytes); @@ -2544,6 +2575,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(gossip_bytes); @@ -2580,6 +2612,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ac_bytes); @@ -2620,6 +2653,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ac_bytes); @@ -2662,6 +2696,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ac_bytes); @@ -2708,6 +2743,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ac_bytes.clone()); @@ -2760,6 +2796,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -2809,7 +2846,12 @@ mod tests { instrs.push(instr); } - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .negotiations .insert(temporary_channel_id, sample_funding_negotiation()); @@ -2842,6 +2884,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -2865,6 +2908,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -2882,6 +2926,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -2905,6 +2950,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -2929,6 +2975,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -2952,6 +2999,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -2978,6 +3026,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -3005,6 +3054,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor.conn.queue_recv(ac_bytes); @@ -3024,6 +3074,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -3033,6 +3084,7 @@ mod tests { // Verify that mine_blocks was called with the correct number assert_eq!(executor.bitcoin_cli.mine_blocks_calls, vec![6]); assert!(executor.bitcoin_cli.mined_private_mempool.is_empty()); + assert_eq!(executor.rpc.chain_syncs, 1); } #[test] @@ -3054,6 +3106,7 @@ mod tests { let _ = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ) .execute(&program, std::time::Instant::now()); @@ -3066,7 +3119,12 @@ mod tests { change_spk: sample_change_spk(), ..Default::default() }; - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .execute( &Program { @@ -3082,6 +3140,7 @@ mod tests { broadcast_tx.compute_txid().to_string(), "09b0549b35f14ee862f63bd75811c6c27963c4dea6766ec6836952ec78df1e7e" ); + assert_eq!(executor.rpc.chain_syncs, 0); } // LookupShortChannelId should combine the confirmed block position with @@ -3109,7 +3168,12 @@ mod tests { // Build and send a channel_announcement carrying the looked-up SCID. instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 9)); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .execute( &Program { @@ -3185,7 +3249,12 @@ mod tests { ]; instrs.extend(channel_announcement_from_scid_instructions(instrs.len(), 7)); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .execute( &Program { @@ -3228,7 +3297,12 @@ mod tests { inputs: vec![], }); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .execute( &Program { @@ -3265,7 +3339,13 @@ mod tests { change_spk: sample_change_spk(), ..Default::default() }; - let err = Executor::new(MockConnection::new(), mock_cli, sample_context()) + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); + let err = executor .execute( &Program { instructions: create_and_broadcast_tx_instructions(), @@ -3379,7 +3459,12 @@ mod tests { }) .encode(); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor.conn.queue_recv(fs_bytes); executor .negotiations @@ -3427,6 +3512,7 @@ mod tests { .get(&ChannelId::new([0xbb; 32])) .unwrap(); assert!(pending.funding_built); + assert_eq!(executor.rpc.chain_syncs, 0); } #[test] @@ -3440,7 +3526,12 @@ mod tests { change_spk: sample_change_spk(), ..Default::default() }; - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .negotiations .insert(ChannelId::new([0xbb; 32]), negotiation); @@ -3469,7 +3560,12 @@ mod tests { change_spk: sample_change_spk(), ..Default::default() }; - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .negotiations .insert(ChannelId::new([0xbb; 32]), negotiation); @@ -3500,7 +3596,12 @@ mod tests { let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .execute( &Program { @@ -3539,7 +3640,12 @@ mod tests { let mut instrs = send_funding_created_and_recv_funding_signed_instructions(); instrs.pop(); // Drop the trailing `RecvFundingSigned` instruction. - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor .negotiations .insert(ChannelId::new([0xbb; 32]), negotiation); @@ -3584,7 +3690,12 @@ mod tests { }) .encode(); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor.conn.queue_recv(fs_bytes); executor .negotiations @@ -3624,7 +3735,12 @@ mod tests { }) .encode(); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor.conn.queue_recv(fs_bytes); executor .negotiations @@ -3691,7 +3807,12 @@ mod tests { signature: "304402203dbf3dbf337b042a72576488c1fb019086089d8d790a47f652346cff2511b6e70220395fdf700cb82b0abfcfe8e0b7c822181f2ee72409c82c3ff8e04e36593662c7".parse().unwrap(), }) .encode(); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor.conn.queue_recv(fs_bytes); executor .negotiations @@ -3764,6 +3885,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -3804,6 +3926,7 @@ mod tests { let mut executor = Executor::new( MockConnection::new(), MockBitcoinCli::default(), + MockTargetRpc::default(), sample_context(), ); executor @@ -3820,7 +3943,7 @@ mod tests { } fn recv_channel_ready_executor() -> ( - Executor, + Executor, ChannelId, PublicKey, ) { @@ -3854,7 +3977,12 @@ mod tests { }) .encode(); - let mut executor = Executor::new(MockConnection::new(), mock_cli, sample_context()); + let mut executor = Executor::new( + MockConnection::new(), + mock_cli, + MockTargetRpc::default(), + sample_context(), + ); executor.conn.queue_recv(fs_bytes); executor.conn.queue_recv(cr_bytes); executor diff --git a/smite-scenarios/src/scenarios/ir.rs b/smite-scenarios/src/scenarios/ir.rs index f4926618..5a3d5cd7 100644 --- a/smite-scenarios/src/scenarios/ir.rs +++ b/smite-scenarios/src/scenarios/ir.rs @@ -21,10 +21,10 @@ use crate::targets::Target; /// (out-of-bounds variable refs, type mismatches, `MineBlocks(0)`, etc.). pub struct IrScenario> { target: T, - /// Executes IR programs and owns the connection, bitcoin-cli handle, and - /// program context. Created once before the snapshot and reused across - /// fuzzing runs. - executor: Executor, + /// Executes IR programs and owns the connection, bitcoin-cli handle, + /// program context, and the target's RPC handle. Created once before the + /// snapshot and reused across fuzzing runs. + executor: Executor, // S is only used for static dispatch on S::setup(), not stored. _phantom: PhantomData, } @@ -34,7 +34,7 @@ impl> Scenario for IrScenario { let target = T::start(T::Config::default())?; let (conn, context) = S::setup(&target)?; let bitcoin_cli = target.bitcoin_cli().clone(); - let executor = Executor::new(conn, bitcoin_cli, context); + let executor = Executor::new(conn, bitcoin_cli, target.rpc(), context); Ok(Self { target, executor, diff --git a/smite-scenarios/src/targets.rs b/smite-scenarios/src/targets.rs index 6ee35c9c..98977ecf 100644 --- a/smite-scenarios/src/targets.rs +++ b/smite-scenarios/src/targets.rs @@ -7,10 +7,10 @@ mod ldk; mod lnd; pub use bitcoind::INITIAL_BLOCKS; -pub use cln::{ClnConfig, ClnTarget}; -pub use eclair::{EclairConfig, EclairTarget}; -pub use ldk::{LdkConfig, LdkTarget}; -pub use lnd::{LndConfig, LndTarget}; +pub use cln::{ClnCli, ClnConfig, ClnTarget}; +pub use eclair::{EclairCli, EclairConfig, EclairTarget}; +pub use ldk::{LdkConfig, LdkRpc, LdkTarget}; +pub use lnd::{LndCli, LndConfig, LndTarget}; use smite::bitcoin::BitcoinCli; use smite::scenarios::TargetError; @@ -42,6 +42,13 @@ pub fn check_crash_log() -> Result<(), TargetError> { Ok(()) } +/// Abstraction over target RPC operations for executing commands on a running +/// target, allowing target-specific implementations. +pub trait TargetRpc { + /// Notifies the target of newly mined blocks so it updates its chain view. + fn chain_sync(&mut self); +} + /// A Lightning implementation that can be fuzzed. /// /// This trait abstracts over different Lightning implementations (LND, CLN, LDK, etc.), @@ -50,6 +57,9 @@ pub trait Target: Sized { /// Configuration for this target. type Config: Default; + /// RPC handle for this target. + type Rpc: TargetRpc; + /// Start the target and any dependencies (e.g., bitcoind). /// /// # Errors @@ -63,6 +73,9 @@ pub trait Target: Sized { /// Target's P2P listen address. fn addr(&self) -> SocketAddr; + /// Target's RPC handle for executing commands. + fn rpc(&self) -> Self::Rpc; + /// `bitcoin-cli` wrapper for the regtest `bitcoind` instance. fn bitcoin_cli(&self) -> &BitcoinCli; diff --git a/smite-scenarios/src/targets/cln.rs b/smite-scenarios/src/targets/cln.rs index 0d37ee04..abbd4121 100644 --- a/smite-scenarios/src/targets/cln.rs +++ b/smite-scenarios/src/targets/cln.rs @@ -9,7 +9,9 @@ //! This means checking lightningd's liveness is sufficient for crash detection. use std::fs; +use std::io; use std::net::SocketAddr; +use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::Duration; @@ -20,7 +22,7 @@ use smite::bitcoin::BitcoinCli; use smite::process::ManagedProcess; use super::bitcoind; -use super::{Target, TargetError, check_crash_log}; +use super::{Target, TargetError, TargetRpc, check_crash_log}; /// Configuration for the CLN target. pub struct ClnConfig { @@ -43,19 +45,87 @@ impl Default for ClnConfig { } impl ClnConfig { - fn bitcoind_config(&self, data_dir: &Path) -> bitcoind::BitcoindConfig { + fn bitcoind_config(&self) -> bitcoind::BitcoindConfig { bitcoind::BitcoindConfig { rpc_port: self.bitcoind_rpc_port, p2p_port: self.bitcoind_p2p_port, - extra_args: vec![format!( - "-blocknotify=lightning-cli --lightning-dir='{}' --network=regtest syncblocks", - data_dir.join("cln").display() - )], ..bitcoind::BitcoindConfig::default() } } } +/// RPC handle for interacting with CLN node target. +#[derive(Debug, Clone)] +pub struct ClnCli { + /// Path to the CLN node's unix RPC socket. + pub rpc_socket: PathBuf, +} + +impl ClnCli { + // Bound RPC socket I/O so a stalled lightningd cannot block indefinitely. + const RPC_IO_TIMEOUT: Duration = Duration::from_secs(1); + + /// Sends a JSON-RPC request to CLN over its Unix RPC socket and returns the + /// `result` of the response. + /// + /// # Errors + /// + /// Returns an [`io::Error`] only if the RPC socket cannot be connected to, + /// which means CLN already crashed. That is a symptom of an earlier crash + /// rather than a fault in the call. + /// + /// # Panics + /// + /// If the request cannot be written, the response cannot be read or parsed, + /// or CLN answers with a JSON-RPC `error` object. + fn run(&self, method: &str, params: impl serde::Serialize) -> io::Result { + let mut sock = UnixStream::connect(&self.rpc_socket)?; + sock.set_read_timeout(Some(Self::RPC_IO_TIMEOUT)) + .expect("valid timeout"); + sock.set_write_timeout(Some(Self::RPC_IO_TIMEOUT)) + .expect("valid timeout"); + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": "smite", + "method": method, + "params": params, + }); + serde_json::to_writer(&mut sock, &request) + .unwrap_or_else(|e| panic!("failed to send {method} to lightningd: {e}")); + + let mut response: serde_json::Value = serde_json::Deserializer::from_reader(&mut sock) + .into_iter() + .next() + .unwrap_or_else(|| panic!("lightningd closed the socket without answering {method}")) + .unwrap_or_else(|e| panic!("failed to read {method} response from lightningd: {e}")); + assert!( + response.get("error").is_none(), + "lightningd rejected {method}: {}", + response["error"] + ); + + Ok(response["result"].take()) + } +} + +impl TargetRpc for ClnCli { + /// RPC to make CLN poll for new blocks immediately instead of waiting for + /// its regular poll interval, allowing it to sync faster. + /// + /// # Panics + /// + /// - If lightningd answers with an error, which means the call itself is at + /// fault rather than the target having crashed. + fn chain_sync(&mut self) { + if let Err(e) = self.run("syncblocks", serde_json::json!({})) { + // lightningd is unreachable, indicating that CLN has already + // crashed, check_alive will report the crash at the end. + log::warn!("syncblocks could not reach lightningd: {e}"); + } + } +} + /// CLN (Core Lightning) node target. /// /// Field order matters: `cln` is declared before `bitcoind` so it drops first, @@ -207,12 +277,12 @@ impl Drop for ClnTarget { impl Target for ClnTarget { type Config = ClnConfig; + type Rpc = ClnCli; fn start(config: Self::Config) -> Result { let (data_path, temp_dir) = bitcoind::resolve_data_dir()?; - let bitcoind_config = config.bitcoind_config(&data_path); - let (bitcoind, bitcoin_cli) = bitcoind::start(&bitcoind_config, &data_path)?; + let (bitcoind, bitcoin_cli) = bitcoind::start(&config.bitcoind_config(), &data_path)?; let (cln, pubkey, cln_dir) = Self::start_cln(&config, &data_path)?; let addr = SocketAddr::from(([127, 0, 0, 1], config.cln_p2p_port)); @@ -237,6 +307,12 @@ impl Target for ClnTarget { self.addr } + fn rpc(&self) -> Self::Rpc { + ClnCli { + rpc_socket: self.cln_dir.join("regtest").join("lightning-rpc"), + } + } + fn bitcoin_cli(&self) -> &BitcoinCli { &self.bitcoin_cli } diff --git a/smite-scenarios/src/targets/eclair.rs b/smite-scenarios/src/targets/eclair.rs index c017105a..1aab14e0 100644 --- a/smite-scenarios/src/targets/eclair.rs +++ b/smite-scenarios/src/targets/eclair.rs @@ -16,7 +16,7 @@ use smite::bitcoin::BitcoinCli; use smite::process::ManagedProcess; use super::bitcoind; -use super::{Target, TargetError, check_crash_log}; +use super::{Target, TargetError, TargetRpc, check_crash_log}; /// API password for Eclair's REST API. const API_PASSWORD: &str = "fuzzpass"; @@ -64,6 +64,16 @@ impl EclairConfig { } } +/// RPC handle for interacting with eclair node target. +#[derive(Debug, Clone)] +pub struct EclairCli; + +impl TargetRpc for EclairCli { + /// Eclair receives new blocks directly from bitcoind over ZMQ, so no manual + /// chain synchronization is required. + fn chain_sync(&mut self) {} +} + /// Eclair Lightning node target. /// /// Field order matters: `eclair` is declared before `bitcoind` so it drops first, @@ -197,6 +207,7 @@ impl EclairTarget { impl Target for EclairTarget { type Config = EclairConfig; + type Rpc = EclairCli; fn start(config: Self::Config) -> Result { let (data_path, temp_dir) = bitcoind::resolve_data_dir()?; @@ -225,6 +236,10 @@ impl Target for EclairTarget { self.addr } + fn rpc(&self) -> Self::Rpc { + EclairCli + } + fn bitcoin_cli(&self) -> &BitcoinCli { &self.bitcoin_cli } diff --git a/smite-scenarios/src/targets/ldk.rs b/smite-scenarios/src/targets/ldk.rs index 6d168740..436a02f4 100644 --- a/smite-scenarios/src/targets/ldk.rs +++ b/smite-scenarios/src/targets/ldk.rs @@ -14,7 +14,7 @@ use smite::bitcoin::BitcoinCli; use smite::process::ManagedProcess; use super::bitcoind; -use super::{Target, TargetError, check_crash_log}; +use super::{Target, TargetError, TargetRpc, check_crash_log}; /// Configuration for the LDK target. pub struct LdkConfig { @@ -41,14 +41,49 @@ impl LdkConfig { bitcoind::BitcoindConfig { rpc_port: self.bitcoind_rpc_port, p2p_port: self.bitcoind_p2p_port, - // signals the wrapper (SIGUSR1) to sync on each new block instead - // of waiting for the next poll. - extra_args: vec!["-blocknotify=pkill -USR1 -f ^ldk-node-wrapper".to_string()], ..bitcoind::BitcoindConfig::default() } } } +/// RPC handle for interacting with LDK node target. +/// +/// LDK currently has no RPC socket, so commands are delivered through signals +/// that invoke the corresponding APIs directly. +#[derive(Debug, Clone)] +pub struct LdkRpc; + +impl TargetRpc for LdkRpc { + /// Signals the wrapper (SIGUSR1) to sync on new blocks immediately instead + /// of waiting for its regular poll interval, allowing it to sync faster. + /// + /// # Panics + /// + /// - If `pkill -USR1 ldk-node-wrapper` fails to execute. + /// - If `pkill` fails for any reason other than the wrapper being gone, + /// which means the call itself is at fault rather than the target having + /// crashed. + fn chain_sync(&mut self) { + let out = Command::new("pkill") + .arg("-USR1") + .arg("-f") + .arg("^ldk-node-wrapper") + .output() + .expect("pkill -USR1 ldk-node-wrapper should not fail"); + + match out.status.code() { + Some(0) => {} + // pkill exits 1 when nothing matches, so LDK is gone, check_alive + // will report the crash at the end. + Some(1) => log::warn!("ldk-node-wrapper is not running, skipping chain sync"), + _ => panic!( + "pkill -USR1 ldk-node-wrapper failed: {}", + String::from_utf8_lossy(&out.stderr) + ), + } + } +} + /// LDK Lightning node target. /// /// Field order matters: `ldk` is declared before `bitcoind` so it drops first, @@ -90,28 +125,6 @@ impl LdkTarget { cmd.env("LD_PRELOAD", handler); } - // Ignore SIGUSR1 for the window between exec and the wrapper blocking - // it. Initial-block generation triggers a burst of asynchronous - // `-blocknotify` (`pkill -USR1`), and a stray one landing in that window - // would kill the wrapper, since SIGUSR1 is fatal by default. SIG_IGN - // survives exec; a caught handler would not. - // - // Signals arriving while SIG_IGN is in effect are dropped, but that ends - // once the wrapper calls `pthread_sigmask(SIG_BLOCK)`: blocking wins over - // the disposition, so SIGUSR1 then stays pending for `sigwait()` instead - // of being discarded. SIG_IGN therefore stays in effect for the whole run - // and the wrapper never needs to replace it. - // - // SAFETY: runs in the child after fork, before exec; calls only the - // async-signal-safe `signal`. - unsafe { - use std::os::unix::process::CommandExt; - cmd.pre_exec(|| { - libc::signal(libc::SIGUSR1, libc::SIG_IGN); - Ok(()) - }); - } - let mut ldk = ManagedProcess::spawn(&mut cmd, "ldk-node-wrapper")?; // Parse pubkey from stdout. The wrapper prints: @@ -150,6 +163,7 @@ impl LdkTarget { impl Target for LdkTarget { type Config = LdkConfig; + type Rpc = LdkRpc; fn start(config: Self::Config) -> Result { let (data_path, temp_dir) = bitcoind::resolve_data_dir()?; @@ -178,6 +192,10 @@ impl Target for LdkTarget { self.addr } + fn rpc(&self) -> Self::Rpc { + LdkRpc + } + fn bitcoin_cli(&self) -> &BitcoinCli { &self.bitcoin_cli } diff --git a/smite-scenarios/src/targets/lnd.rs b/smite-scenarios/src/targets/lnd.rs index f42f2a0c..a64faada 100644 --- a/smite-scenarios/src/targets/lnd.rs +++ b/smite-scenarios/src/targets/lnd.rs @@ -14,7 +14,7 @@ use smite::bitcoin::BitcoinCli; use smite::process::ManagedProcess; use super::bitcoind; -use super::{Target, TargetError}; +use super::{Target, TargetError, TargetRpc}; /// Configuration for the LND target. pub struct LndConfig { @@ -81,6 +81,16 @@ impl CoveragePipes { } } +/// RPC handle for interacting with LND node target. +#[derive(Debug, Clone)] +pub struct LndCli; + +impl TargetRpc for LndCli { + /// LND receives new blocks directly from bitcoind over ZMQ, so no manual + /// chain synchronization is required. + fn chain_sync(&mut self) {} +} + /// LND Lightning node target. /// /// Field order matters: `lnd` is declared before `bitcoind` so it drops first, @@ -271,6 +281,7 @@ impl LndTarget { impl Target for LndTarget { type Config = LndConfig; + type Rpc = LndCli; fn start(config: Self::Config) -> Result { let (data_path, temp_dir) = bitcoind::resolve_data_dir()?; @@ -300,6 +311,10 @@ impl Target for LndTarget { self.addr } + fn rpc(&self) -> Self::Rpc { + LndCli + } + fn bitcoin_cli(&self) -> &BitcoinCli { &self.bitcoin_cli } diff --git a/workloads/ldk/src/main.rs b/workloads/ldk/src/main.rs index e179e840..b82cbd9e 100644 --- a/workloads/ldk/src/main.rs +++ b/workloads/ldk/src/main.rs @@ -32,12 +32,12 @@ fn install_panic_hook() { /// with `sigwait()`. /// /// Blocking supersedes the disposition inherited across exec, which for SIGUSR1 -/// is `SIG_IGN` (set by the scenario's pre-exec hook, see `LdkTarget::start`). -/// That distinction is the whole point: an *ignored* signal is discarded the -/// moment it is delivered, while a *blocked* one stays pending until `sigwait()` -/// consumes it, regardless of its disposition. So this call is what makes -/// bitcoind's `-blocknotify` SIGUSR1 observable, and why nothing here calls -/// `sigaction`: the wait loop below is the only consumer these signals need. +/// defaults to terminating the process. That distinction is the whole point: an +/// *ignored* signal is discarded the moment it is delivered, while a *blocked* +/// one stays pending until `sigwait()` consumes it, regardless of its +/// disposition. So this call is what makes target's SIGUSR1 observable, and +/// why nothing here calls `sigaction`: the wait loop below is the only consumer +/// these signals need. /// /// Standard signals do not queue, so a burst of SIGUSR1 collapses into one /// pending instance and thus one wakeup. That is fine here: each wakeup syncs @@ -64,7 +64,7 @@ fn setup_signal_set() -> libc::sigset_t { fn main() { install_panic_hook(); - // bitcoind's -blocknotify sends SIGUSR1 for each new block. + // The target sends SIGUSR1 to request an immediate chain sync. // SIGTERM/SIGINT are used for graceful shutdown. let signal_set = setup_signal_set(); @@ -114,7 +114,7 @@ fn main() { println!("READY"); // Wait for signals. sigwait() blocks here without polling and returns - // immediately when bitcoind sends SIGUSR1 or the process receives + // immediately when the target sends SIGUSR1 or the process receives // SIGTERM/SIGINT. loop { let mut signal = 0; @@ -126,13 +126,13 @@ fn main() { match signal { libc::SIGUSR1 => { - // Sync the wallet whenever bitcoind signals a new block. + // Sync the wallet whenever the target asks for it. // ldk-node's own 2s background poll keeps running, so this is // technically redundant and may race it, but that's safe: // ldk-node coalesces concurrent syncs, so whichever loses the // race just waits on the in-flight sync's result rather than // applying the block twice. We accept the redundancy to sync on - // the block instead of up to 2s later. + // demand instead of up to 2s later. if let Err(e) = node.sync_wallets() { eprintln!("sync_wallets failed: {e}"); }