Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 149 additions & 21 deletions smite-scenarios/src/executor.rs

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions smite-scenarios/src/scenarios/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ use crate::targets::Target;
/// (out-of-bounds variable refs, type mismatches, `MineBlocks(0)`, etc.).
pub struct IrScenario<T: Target, S: SnapshotSetup<T>> {
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<NoiseConnection, BitcoinCli>,
/// 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<NoiseConnection, BitcoinCli, T::Rpc>,
// S is only used for static dispatch on S::setup(), not stored.
_phantom: PhantomData<S>,
}
Expand All @@ -34,7 +34,7 @@ impl<T: Target, S: SnapshotSetup<T>> Scenario for IrScenario<T, S> {
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,
Expand Down
21 changes: 17 additions & 4 deletions smite-scenarios/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.),
Expand All @@ -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
Expand All @@ -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;

Expand Down
92 changes: 84 additions & 8 deletions smite-scenarios/src/targets/cln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized this should now be renamed to ClnRpc. I'll update it

/// 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<serde_json::Value> {
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,
Expand Down Expand Up @@ -207,12 +277,12 @@ impl Drop for ClnTarget {

impl Target for ClnTarget {
type Config = ClnConfig;
type Rpc = ClnCli;

fn start(config: Self::Config) -> Result<Self, TargetError> {
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));

Expand All @@ -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
}
Expand Down
17 changes: 16 additions & 1 deletion smite-scenarios/src/targets/eclair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -197,6 +207,7 @@ impl EclairTarget {

impl Target for EclairTarget {
type Config = EclairConfig;
type Rpc = EclairCli;

fn start(config: Self::Config) -> Result<Self, TargetError> {
let (data_path, temp_dir) = bitcoind::resolve_data_dir()?;
Expand Down Expand Up @@ -225,6 +236,10 @@ impl Target for EclairTarget {
self.addr
}

fn rpc(&self) -> Self::Rpc {
EclairCli
}

fn bitcoin_cli(&self) -> &BitcoinCli {
&self.bitcoin_cli
}
Expand Down
70 changes: 44 additions & 26 deletions smite-scenarios/src/targets/ldk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -90,28 +125,6 @@ impl LdkTarget {
cmd.env("LD_PRELOAD", handler);
}

// Ignore SIGUSR1 for the window between exec and the wrapper blocking

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments at workloads/ldk/src/main.rs also need to be updated after the remove of SIG_IGN.

// 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:
Expand Down Expand Up @@ -150,6 +163,7 @@ impl LdkTarget {

impl Target for LdkTarget {
type Config = LdkConfig;
type Rpc = LdkRpc;

fn start(config: Self::Config) -> Result<Self, TargetError> {
let (data_path, temp_dir) = bitcoind::resolve_data_dir()?;
Expand Down Expand Up @@ -178,6 +192,10 @@ impl Target for LdkTarget {
self.addr
}

fn rpc(&self) -> Self::Rpc {
LdkRpc
}

fn bitcoin_cli(&self) -> &BitcoinCli {
&self.bitcoin_cli
}
Expand Down
Loading