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
10 changes: 5 additions & 5 deletions smite-scenarios/src/scenarios/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use smite::scenarios::ScenarioError;

use super::{handshake_with_target, ping_pong};
use crate::executor::ProgramContext;
use crate::targets::{INITIAL_BLOCKS, Target};
use crate::targets::{FUNDED_HEIGHT, Target};

const TIMEOUT: Duration = Duration::from_secs(5);

Expand Down Expand Up @@ -76,10 +76,10 @@ impl<T: Target> SnapshotSetup<T> for PostInitSetup {
let context = ProgramContext {
target_pubkey: *target.pubkey(),
chain_hash: REGTEST_CHAIN_HASH,
// All targets gate startup on `INITIAL_BLOCKS` being mined, so
// this is the floor. Dynamic per-target queries can replace it
// later.
block_height: u32::try_from(INITIAL_BLOCKS).expect("fits in u32"),
// All targets gate startup on their wallet funding confirming at
// `FUNDED_HEIGHT`, so this is the floor. Dynamic per-target
// queries can replace it later.
block_height: u32::try_from(FUNDED_HEIGHT).expect("fits in u32"),
// Since we echo the same features the target sent, but strip both
// required and optional bits to exercise only the single funded
// flow and avoid unrelated noise, negotiated features are just the
Expand Down
2 changes: 1 addition & 1 deletion smite-scenarios/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod eclair;
mod ldk;
mod lnd;

pub use bitcoind::INITIAL_BLOCKS;
pub use bitcoind::FUNDED_HEIGHT;
pub use cln::{ClnConfig, ClnRpc, ClnTarget};
pub use eclair::{EclairConfig, EclairRpc, EclairTarget};
pub use ldk::{LdkConfig, LdkRpc, LdkTarget};
Expand Down
65 changes: 35 additions & 30 deletions smite-scenarios/src/targets/bitcoind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ use super::TargetError;
/// Number of blocks to generate at startup for coinbase maturity.
pub const INITIAL_BLOCKS: u64 = 101;

/// Blocks [`fund_wallet`] mines to confirm a target's funding.
const FUNDING_BLOCKS: u64 = 1;

/// Chain height once the target's wallet is funded.
pub const FUNDED_HEIGHT: u64 = INITIAL_BLOCKS + FUNDING_BLOCKS;

/// Wallet the fuzzer funds its own transactions from.
const FUZZER_WALLET: &str = "default";

/// Confirmed UTXOs each target's wallet starts with.
///
/// Targets need them to fee-bump and sweep on chain, and LDK rejects inbound
/// anchor channels without an on-chain reserve. Separate coins let concurrent
/// claims (e.g. anchor CPFP and HTLC claims) each find a confirmed input.
const FUNDING_UTXOS: usize = 4;

/// Value of each funding UTXO in BTC, well above LDK's default 25k sat
/// per-channel anchor reserve.
const FUNDING_UTXO_BTC: &str = "0.1";

/// Bitcoind configuration.
pub struct BitcoindConfig {
/// Bitcoin RPC port (default: 18443 for regtest).
Expand Down Expand Up @@ -104,6 +124,7 @@ pub fn start(
let cli = BitcoinCli {
rpc_port: config.rpc_port,
bitcoind_dir,
wallet: FUZZER_WALLET.into(),
};

// Wait for bitcoind to be ready
Expand All @@ -130,38 +151,22 @@ pub fn start(
))
}

/// Creates wallet and generates initial blocks.
/// Creates the fuzzer's wallet and generates initial blocks.
fn setup_wallet(cli: &BitcoinCli) -> Result<(), TargetError> {
// Create wallet
let status = cli
.run()
.arg("createwallet")
.arg("default")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;

// command fails if wallet already exists (i.e. SMITE_DATA_DIR was mounted)
if !status.success() {
return Err(TargetError::StartFailed(
"failed to create wallet (does it already exist?)".into(),
));
}

// Generate initial blocks
let status = cli
.run()
.arg("-generate")
.arg(INITIAL_BLOCKS.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
// Fails if the wallet already exists (i.e. SMITE_DATA_DIR was mounted).
cli.call(&["createwallet", &cli.wallet])?;
cli.call(&["-generate", &INITIAL_BLOCKS.to_string()])?;
Ok(())
}

if !status.success() {
return Err(TargetError::StartFailed(
"failed to generate initial blocks".into(),
));
/// Pays [`FUNDING_UTXOS`] outputs to a target's `address` from the fuzzer's
/// wallet and mines [`FUNDING_BLOCKS`] to confirm them.
pub fn fund_wallet(cli: &BitcoinCli, address: &str) -> Result<(), TargetError> {
log::info!("Funding target wallet at {address}");
// One payment per UTXO, as `sendmany` rejects repeated addresses.
for _ in 0..FUNDING_UTXOS {
cli.call(&["sendtoaddress", address, FUNDING_UTXO_BTC])?;
}

cli.call(&["-generate", &FUNDING_BLOCKS.to_string()])?;
Ok(())
}
72 changes: 69 additions & 3 deletions smite-scenarios/src/targets/cln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::time::Duration;

use bitcoin::secp256k1;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use smite::bitcoin::BitcoinCli;
use smite::process::ManagedProcess;

Expand Down Expand Up @@ -65,6 +66,12 @@ impl ClnRpc {
/// Bound RPC socket I/O so a stalled lightningd cannot block indefinitely.
const RPC_IO_TIMEOUT: Duration = Duration::from_secs(1);

fn new(cln_dir: &Path) -> Self {
Self {
rpc_socket: cln_dir.join("regtest").join("lightning-rpc"),
}
}

/// Sends a JSON-RPC request to CLN over its Unix RPC socket and returns the
/// `result` of the response.
///
Expand Down Expand Up @@ -276,6 +283,66 @@ impl ClnTarget {

Ok((pubkey, info.blockheight))
}

/// Funds CLN's on-chain wallet and waits until CLN sees the funds confirmed.
fn fund_wallet(cln_dir: &Path, bitcoin_cli: &BitcoinCli) -> Result<(), TargetError> {
#[derive(Deserialize)]
struct NewAddrResponse {
bech32: String,
}

#[derive(Deserialize)]
struct ListFundsResponse {
outputs: Vec<Output>,
}

#[derive(Deserialize)]
struct Output {
status: String,
}

let mut rpc = ClnRpc::new(cln_dir);
let NewAddrResponse { bech32 } = Self::rpc_json(
&rpc,
"newaddr",
serde_json::json!({"addresstype": "bech32"}),
)?;
bitcoind::fund_wallet(bitcoin_cli, &bech32)?;

log::info!("Waiting for lightningd to confirm its wallet funding...");
for _ in 0..120 {
// lightningd polls bitcoind only every 30s, so ask it to sync now.
rpc.chain_sync();
let funds: ListFundsResponse =
Self::rpc_json(&rpc, "listfunds", serde_json::json!({}))?;
if funds.outputs.iter().any(|o| o.status == "confirmed") {
log::info!(
"lightningd confirmed {} funding outputs",
funds.outputs.len()
);
return Ok(());
}
std::thread::sleep(Duration::from_secs(1));
}

Err(TargetError::StartFailed(
"lightningd did not confirm its wallet funding".into(),
))
}

/// Calls a lightningd RPC method and parses its result.
fn rpc_json<T: DeserializeOwned>(
rpc: &ClnRpc,
method: &str,
params: serde_json::Value,
) -> Result<T, TargetError> {
let result = rpc
.run(method, params)
.map_err(|e| TargetError::StartFailed(format!("lightningd {method} failed: {e}")))?;
serde_json::from_value(result).map_err(|e| {
TargetError::StartFailed(format!("failed to parse lightningd {method} result: {e}"))
})
}
}

impl Drop for ClnTarget {
Expand Down Expand Up @@ -321,6 +388,7 @@ impl Target for ClnTarget {

let (bitcoind, bitcoin_cli) = bitcoind::start(&config.bitcoind_config(), &data_path)?;
let (cln, pubkey, cln_dir) = Self::start_cln(&config, &data_path)?;
Self::fund_wallet(&cln_dir, &bitcoin_cli)?;
let addr = SocketAddr::from(([127, 0, 0, 1], config.cln_p2p_port));

log::info!("Both daemons are running, ready to fuzz");
Expand All @@ -345,9 +413,7 @@ impl Target for ClnTarget {
}

fn rpc(&self) -> Self::Rpc {
ClnRpc {
rpc_socket: self.cln_dir.join("regtest").join("lightning-rpc"),
}
ClnRpc::new(&self.cln_dir)
}

fn bitcoin_cli(&self) -> &BitcoinCli {
Expand Down
21 changes: 19 additions & 2 deletions smite-scenarios/src/targets/eclair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ use super::{Target, TargetError, TargetRpc, check_crash_log};
/// API password for Eclair's REST API.
const API_PASSWORD: &str = "fuzzpass";

/// bitcoind wallet Eclair spends from, kept apart from the fuzzer's wallet.
const ECLAIR_WALLET: &str = "eclair";

/// Configuration for the Eclair target.
pub struct EclairConfig {
/// Bitcoin RPC port (default: 18443 for regtest).
Expand Down Expand Up @@ -101,6 +104,7 @@ impl EclairTarget {
eclair.bitcoind.rpcuser=rpcuser\n\
eclair.bitcoind.rpcpassword=rpcpass\n\
eclair.bitcoind.rpcport={bitcoind_rpc_port}\n\
eclair.bitcoind.wallet={ECLAIR_WALLET}\n\
eclair.bitcoind.zmqblock=\"tcp://127.0.0.1:{zmq_block_port}\"\n\
eclair.bitcoind.zmqtx=\"tcp://127.0.0.1:{zmq_tx_port}\"\n",
eclair_p2p_port = config.eclair_p2p_port,
Expand All @@ -114,6 +118,18 @@ impl EclairTarget {
Ok(())
}

/// Creates and funds Eclair's bitcoind wallet. Eclair keeps its keys in
/// bitcoind, so this can run before Eclair starts.
fn fund_wallet(bitcoin_cli: &BitcoinCli) -> Result<(), TargetError> {
let wallet = BitcoinCli {
wallet: ECLAIR_WALLET.into(),
..bitcoin_cli.clone()
};
wallet.call(&["createwallet", ECLAIR_WALLET])?;
let address = wallet.call(&["getnewaddress"])?;
bitcoind::fund_wallet(bitcoin_cli, &address)
}

/// Starts Eclair and waits for it to be ready and synced.
/// Returns the process and Eclair's identity pubkey.
fn start_eclair(
Expand Down Expand Up @@ -142,11 +158,11 @@ impl EclairTarget {
let eclair = ManagedProcess::spawn(&mut cmd, "eclair")?;

// Wait for Eclair to be ready and fully synced. We poll the REST API
// until blockHeight matches the initial blocks we generated.
// until blockHeight matches the block that confirmed its funding.
log::info!("Waiting for eclair to be ready and synced...");
for _ in 0..120 {
if let Ok((pubkey, blockheight)) = Self::query_info(config) {
if blockheight >= bitcoind::INITIAL_BLOCKS {
if blockheight >= bitcoind::FUNDED_HEIGHT {
log::info!("eclair synced (blockheight={blockheight})");
return Ok((eclair, pubkey));
}
Expand Down Expand Up @@ -213,6 +229,7 @@ impl Target for EclairTarget {
let (data_path, temp_dir) = bitcoind::resolve_data_dir()?;

let (bitcoind, bitcoin_cli) = bitcoind::start(&config.bitcoind_config(), &data_path)?;
Self::fund_wallet(&bitcoin_cli)?;
let (eclair, pubkey) = Self::start_eclair(&config, &data_path)?;
let addr = SocketAddr::from(([127, 0, 0, 1], config.eclair_p2p_port));

Expand Down
15 changes: 10 additions & 5 deletions smite-scenarios/src/targets/ldk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ pub struct LdkTarget {
}

impl LdkTarget {
/// Starts ldk-node-wrapper and waits for it to be ready.
/// Returns the process and LDK's identity pubkey.
/// Starts ldk-node-wrapper, funds its on-chain wallet, and waits for it to
/// be ready. Returns the process and LDK's identity pubkey.
fn start_ldk(
config: &LdkConfig,
data_dir: &Path,
bitcoin_cli: &BitcoinCli,
) -> Result<(ManagedProcess, secp256k1::PublicKey), TargetError> {
log::info!("Starting ldk-node-wrapper...");

Expand All @@ -121,8 +122,10 @@ impl LdkTarget {

let mut ldk = ManagedProcess::spawn(&mut cmd, "ldk-node-wrapper")?;

// Parse pubkey from stdout. The wrapper prints:
// Parse pubkey from stdout and fund the wallet address. The wrapper
// prints READY once the funding confirms:
// PUBKEY:<hex>
// ADDRESS:<address>
// READY
let stdout = ldk.inner().stdout.take().ok_or_else(|| {
TargetError::StartFailed("ldk-node-wrapper stdout not captured".into())
Expand All @@ -142,6 +145,8 @@ impl LdkTarget {
TargetError::StartFailed(format!("failed to parse pubkey: {e}"))
})?);
log::info!("LDK identity pubkey: {hex}");
} else if let Some(address) = line.strip_prefix("ADDRESS:") {
bitcoind::fund_wallet(bitcoin_cli, address)?;
} else if line == "READY" {
break;
}
Expand All @@ -150,7 +155,7 @@ impl LdkTarget {
let pubkey =
pubkey.ok_or_else(|| TargetError::StartFailed("no PUBKEY line received".into()))?;

log::info!("ldk-node-wrapper is ready and synced");
log::info!("ldk-node-wrapper is ready, synced, and funded");
Ok((ldk, pubkey))
}
}
Expand All @@ -163,7 +168,7 @@ impl Target for LdkTarget {
let (data_path, temp_dir) = bitcoind::resolve_data_dir()?;

let (bitcoind, bitcoin_cli) = bitcoind::start(&config.bitcoind_config(), &data_path)?;
let (ldk, pubkey) = Self::start_ldk(&config, &data_path)?;
let (ldk, pubkey) = Self::start_ldk(&config, &data_path, &bitcoin_cli)?;
let addr = SocketAddr::from(([127, 0, 0, 1], config.ldk_p2p_port));

log::info!("Both daemons are running, ready to fuzz");
Expand Down
Loading
Loading