From c0d39ca44d9fa4038341d71b063650dc660f0278 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Mon, 5 Oct 2026 15:39:16 -0300 Subject: [PATCH 1/5] smite-scenarios: add target wallet funding to the harness Targets start with empty wallets, so they cannot fee-bump or sweep, and LDK rejects inbound anchor channels. fund_wallet pays a target four confirmed UTXOs, which moves the snapshot height to FUNDED_HEIGHT. BitcoinCli now names its wallet so a target can own a second one. --- smite-scenarios/src/scenarios/setup.rs | 10 ++-- smite-scenarios/src/targets.rs | 2 +- smite-scenarios/src/targets/bitcoind.rs | 66 ++++++++++++++----------- smite/src/bitcoin.rs | 23 +++++++++ 4 files changed, 65 insertions(+), 36 deletions(-) diff --git a/smite-scenarios/src/scenarios/setup.rs b/smite-scenarios/src/scenarios/setup.rs index 2cfe5773..fbaef707 100644 --- a/smite-scenarios/src/scenarios/setup.rs +++ b/smite-scenarios/src/scenarios/setup.rs @@ -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); @@ -76,10 +76,10 @@ impl SnapshotSetup 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 diff --git a/smite-scenarios/src/targets.rs b/smite-scenarios/src/targets.rs index ce35c0b0..8b76db6a 100644 --- a/smite-scenarios/src/targets.rs +++ b/smite-scenarios/src/targets.rs @@ -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}; diff --git a/smite-scenarios/src/targets/bitcoind.rs b/smite-scenarios/src/targets/bitcoind.rs index 0cff647c..ff1dcdc5 100644 --- a/smite-scenarios/src/targets/bitcoind.rs +++ b/smite-scenarios/src/targets/bitcoind.rs @@ -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). @@ -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 @@ -130,38 +151,23 @@ 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. +#[expect(dead_code, reason = "targets call it in the following commits")] +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(()) } diff --git a/smite/src/bitcoin.rs b/smite/src/bitcoin.rs index 2aa0a1cf..a41d1b0f 100644 --- a/smite/src/bitcoin.rs +++ b/smite/src/bitcoin.rs @@ -2,6 +2,7 @@ //! `bitcoind` instances via `bitcoin-cli`. use std::cmp::Ordering; +use std::io; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; @@ -68,6 +69,9 @@ pub struct BitcoinCli { pub rpc_port: u16, /// Path passed to `bitcoin-cli -datadir`. pub bitcoind_dir: PathBuf, + /// Wallet that wallet RPCs act on, required once `bitcoind` has more than + /// one wallet loaded. + pub wallet: String, } impl BitcoinCli { @@ -79,11 +83,30 @@ impl BitcoinCli { cmd.arg("-regtest") .arg(format!("-datadir={}", self.bitcoind_dir.display())) .arg(format!("-rpcport={}", self.rpc_port)) + .arg(format!("-rpcwallet={}", self.wallet)) .arg("-rpcuser=rpcuser") .arg("-rpcpassword=rpcpass"); cmd } + /// Runs `bitcoin-cli` with `args` and returns its trimmed stdout. + /// + /// # Errors + /// + /// Returns an error if `bitcoin-cli` fails to execute or exits non-zero, + /// carrying its stderr in the latter case. + pub fn call(&self, args: &[&str]) -> io::Result { + let output = self.run().args(args).output()?; + if !output.status.success() { + return Err(io::Error::other(format!( + "bitcoin-cli {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr).trim() + ))); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) + } + /// Mines the given number of blocks. /// /// Any transactions stored in `private_mempool` are included in the first From e3c053befb070f6acce6615d4c147c2877085262 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Mon, 5 Oct 2026 15:39:26 -0300 Subject: [PATCH 2/5] smite-scenarios: fund LND's wallet at startup --- smite-scenarios/src/targets/bitcoind.rs | 1 - smite-scenarios/src/targets/lnd.rs | 91 +++++++++++++++++++------ 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/smite-scenarios/src/targets/bitcoind.rs b/smite-scenarios/src/targets/bitcoind.rs index ff1dcdc5..75c93123 100644 --- a/smite-scenarios/src/targets/bitcoind.rs +++ b/smite-scenarios/src/targets/bitcoind.rs @@ -161,7 +161,6 @@ fn setup_wallet(cli: &BitcoinCli) -> Result<(), TargetError> { /// Pays [`FUNDING_UTXOS`] outputs to a target's `address` from the fuzzer's /// wallet and mines [`FUNDING_BLOCKS`] to confirm them. -#[expect(dead_code, reason = "targets call it in the following commits")] 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. diff --git a/smite-scenarios/src/targets/lnd.rs b/smite-scenarios/src/targets/lnd.rs index 2c76090a..f8b7e9f6 100644 --- a/smite-scenarios/src/targets/lnd.rs +++ b/smite-scenarios/src/targets/lnd.rs @@ -8,6 +8,7 @@ use std::time::Duration; use bitcoin::secp256k1; use serde::Deserialize; +use serde::de::DeserializeOwned; use smite::bitcoin::BitcoinCli; use smite::process::ManagedProcess; @@ -85,12 +86,11 @@ impl LndTarget { /// identity pubkey. fn start_lnd( config: &LndConfig, - data_dir: &Path, + lnd_dir: &Path, ) -> Result<(ManagedProcess, secp256k1::PublicKey), TargetError> { log::info!("Starting lnd..."); - let lnd_dir = data_dir.join("lnd"); - fs::create_dir_all(&lnd_dir)?; + fs::create_dir_all(lnd_dir)?; let mut cmd = Command::new("lnd"); cmd.arg("--noseedbackup") @@ -136,7 +136,7 @@ impl LndTarget { if !lnd.is_running() { return Err(TargetError::StartFailed("lnd exited during startup".into())); } - if let Ok((pubkey, blockheight, synced_to_chain)) = Self::query_info(config, &lnd_dir) { + if let Ok((pubkey, blockheight, synced_to_chain)) = Self::query_info(config, lnd_dir) { if blockheight >= bitcoind::INITIAL_BLOCKS && synced_to_chain { log::info!("lnd synced (blockheight={blockheight})"); return Ok((lnd, pubkey)); @@ -163,22 +163,7 @@ impl LndTarget { synced_to_chain: bool, } - let output = Command::new("lncli") - .arg(format!("--lnddir={}", lnd_dir.display())) - .arg(format!("--rpcserver=127.0.0.1:{}", config.lnd_rpc_port)) - .arg("--network=regtest") - .arg("getinfo") - .output()?; - - if !output.status.success() { - return Err(TargetError::StartFailed(format!( - "lncli getinfo failed: {}", - String::from_utf8_lossy(&output.stderr) - ))); - } - - let info: GetInfoResponse = serde_json::from_slice(&output.stdout) - .map_err(|e| TargetError::StartFailed(format!("failed to parse lncli output: {e}")))?; + let info: GetInfoResponse = Self::lncli(config, lnd_dir, &["getinfo"])?; log::info!( "LND identity pubkey: {}, blockheight: {}, synced_to_chain: {}", @@ -195,6 +180,68 @@ impl LndTarget { Ok((pubkey, info.block_height, info.synced_to_chain)) } + + /// Funds LND's on-chain wallet and waits until LND sees the funds confirmed. + fn fund_wallet( + config: &LndConfig, + lnd_dir: &Path, + bitcoin_cli: &BitcoinCli, + ) -> Result<(), TargetError> { + #[derive(Deserialize)] + struct NewAddressResponse { + address: String, + } + + // lncli prints 64-bit integers as JSON strings. + #[derive(Deserialize)] + struct WalletBalanceResponse { + confirmed_balance: String, + } + + let NewAddressResponse { address } = + Self::lncli(config, lnd_dir, &["newaddress", "p2wkh"])?; + bitcoind::fund_wallet(bitcoin_cli, &address)?; + + log::info!("Waiting for lnd to confirm its wallet funding..."); + for _ in 0..120 { + let balance: WalletBalanceResponse = Self::lncli(config, lnd_dir, &["walletbalance"])?; + if balance.confirmed_balance != "0" { + log::info!("lnd confirmed balance: {} sat", balance.confirmed_balance); + return Ok(()); + } + std::thread::sleep(Duration::from_secs(1)); + } + + Err(TargetError::StartFailed( + "lnd did not confirm its wallet funding".into(), + )) + } + + /// Runs an lncli command against LND and parses its JSON output. + fn lncli( + config: &LndConfig, + lnd_dir: &Path, + args: &[&str], + ) -> Result { + let output = Command::new("lncli") + .arg(format!("--lnddir={}", lnd_dir.display())) + .arg(format!("--rpcserver=127.0.0.1:{}", config.lnd_rpc_port)) + .arg("--network=regtest") + .args(args) + .output()?; + + let command = args.join(" "); + if !output.status.success() { + return Err(TargetError::StartFailed(format!( + "lncli {command} failed: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + + serde_json::from_slice(&output.stdout).map_err(|e| { + TargetError::StartFailed(format!("failed to parse lncli {command} output: {e}")) + }) + } } impl Target for LndTarget { @@ -205,7 +252,9 @@ impl Target for LndTarget { let (data_path, temp_dir) = bitcoind::resolve_data_dir()?; let (bitcoind, bitcoin_cli) = bitcoind::start(&config.bitcoind_config(), &data_path)?; - let (lnd, pubkey) = Self::start_lnd(&config, &data_path)?; + let lnd_dir = data_path.join("lnd"); + let (lnd, pubkey) = Self::start_lnd(&config, &lnd_dir)?; + Self::fund_wallet(&config, &lnd_dir, &bitcoin_cli)?; let addr = SocketAddr::from(([127, 0, 0, 1], config.lnd_p2p_port)); log::info!("Both daemons are running, ready to fuzz"); From f3ca6ee1caef6dbfe61f4cf2ac58077ab19e749c Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Mon, 5 Oct 2026 15:39:26 -0300 Subject: [PATCH 3/5] smite-scenarios: fund CLN's wallet at startup --- smite-scenarios/src/targets/cln.rs | 72 ++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/smite-scenarios/src/targets/cln.rs b/smite-scenarios/src/targets/cln.rs index d898ec98..2d3668ad 100644 --- a/smite-scenarios/src/targets/cln.rs +++ b/smite-scenarios/src/targets/cln.rs @@ -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; @@ -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. /// @@ -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, + } + + #[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( + rpc: &ClnRpc, + method: &str, + params: serde_json::Value, + ) -> Result { + 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 { @@ -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"); @@ -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 { From 3f0a5bde93e8930e7a1a0377f5bb1d41a441c741 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Mon, 5 Oct 2026 15:39:26 -0300 Subject: [PATCH 4/5] smite-scenarios: give Eclair its own funded wallet Eclair spent from bitcoind's default wallet, which is also the fuzzer's, so the two competed for the same coins. --- smite-scenarios/src/targets/eclair.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/smite-scenarios/src/targets/eclair.rs b/smite-scenarios/src/targets/eclair.rs index c38cec77..8110d26c 100644 --- a/smite-scenarios/src/targets/eclair.rs +++ b/smite-scenarios/src/targets/eclair.rs @@ -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). @@ -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, @@ -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( @@ -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)); } @@ -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)); From d48a301941fbf1fea79399331e5e74a53de0bce5 Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Mon, 5 Oct 2026 15:39:26 -0300 Subject: [PATCH 5/5] smite-scenarios: fund LDK's wallet at startup --- smite-scenarios/src/targets/ldk.rs | 15 ++++++++++----- workloads/ldk/src/main.rs | 12 +++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/smite-scenarios/src/targets/ldk.rs b/smite-scenarios/src/targets/ldk.rs index 70d7bf60..8952d6e4 100644 --- a/smite-scenarios/src/targets/ldk.rs +++ b/smite-scenarios/src/targets/ldk.rs @@ -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..."); @@ -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: + // ADDRESS:
// READY let stdout = ldk.inner().stdout.take().ok_or_else(|| { TargetError::StartFailed("ldk-node-wrapper stdout not captured".into()) @@ -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; } @@ -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)) } } @@ -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"); diff --git a/workloads/ldk/src/main.rs b/workloads/ldk/src/main.rs index 8cf79050..97d4f589 100644 --- a/workloads/ldk/src/main.rs +++ b/workloads/ldk/src/main.rs @@ -107,8 +107,18 @@ fn main() { std::thread::sleep(Duration::from_secs(1)); } - // Output for LdkTarget to parse. + // Output for LdkTarget to parse. LdkTarget funds ADDRESS, and READY + // follows once the funds confirm, so the node can fee-bump, sweep, and + // accept inbound anchor channels, which require an on-chain reserve. println!("PUBKEY:{}", node.node_id()); + let address = node + .onchain_payment() + .new_address() + .expect("new on-chain address"); + println!("ADDRESS:{address}"); + while node.list_balances().spendable_onchain_balance_sats == 0 { + std::thread::sleep(Duration::from_secs(1)); + } println!("READY"); // Wait for signals. sigwait() blocks here without polling and returns