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}"); }