From f2a94548ab2d988c35d0751dc35cd50303a88fbb Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 14 Jul 2026 07:31:12 -0400 Subject: [PATCH] fix(bitcoin/lnd): rpcbind=0.0.0.0 in generated bitcoin.conf + RPC readiness gate for dependent installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the fresh-install triple failure (LND needing ~5 install attempts, Bitcoin UI bitcoin-rpc 502, backend getblockchaininfo errors): the legacy conf writer set rpcallowip but never rpcbind, so bitcoind bound RPC to the container's loopback only — every dial over the container network (LND, bitcoin-ui) and the rootless port-forward was refused. LND exited instantly, the 60s post-start poll read that as INSTALL CRASH, and the wallet (and its seed backup) never got created. - write_bitcoin_conf + ensure_bitcoin_rpc_config now emit/ensure rpcbind=0.0.0.0 (host exposure unchanged: publish stays 127.0.0.1) - new install gate: bitcoin-dependent apps (lnd/electrs/electrumx/ mempool-electrs/btcpay) wait up to 3 min for an authenticated getblockchaininfo to answer before their container starts, with INSTALL WAIT progress lines; on timeout the install fails with an actionable message instead of a crash-looping container Verified live on the fresh-ISO framework node: after adding rpcbind and restarting bitcoin-knots, LND connected, created its wallet, baked macaroons, and settled into 'Waiting for chain backend to finish sync'. Co-Authored-By: Claude Fable 5 --- .../src/api/rpc/package/install.rs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index b0bc972f..eef2e000 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -816,6 +816,15 @@ impl RpcHandler { }; run_args.push(&effective_image); + // Bitcoin-dependent apps (LND, electrs, BTCPay…) exit immediately if + // bitcoind's RPC isn't answering when they start; the 60s post-start + // poll then reads that exit as INSTALL CRASH and the whole install + // fails — the "LND took 5 attempts" failure mode on fresh installs. + // Gate the container start on the RPC actually responding (IBD is + // fine — getblockchaininfo answers during sync) with a generous wait, + // and fail with an actionable message instead of a crash-looping app. + wait_for_bitcoin_rpc_gate(package_id).await?; + install_log(&format!( "INSTALL RUN: {} — podman run {} (image: {})", package_id, container_name, effective_image @@ -1380,11 +1389,18 @@ impl RpcHandler { // (self-shrunk on restart); duplicating it to stdout pushed every IBD // "UpdateTip" line through conmon into journald (>1 GB/day). Deep // debugging uses /var/lib/archipelago/bitcoin/debug.log. + // rpcbind=0.0.0.0 is REQUIRED inside a container: with rpcallowip set + // but no rpcbind, bitcoind binds RPC to 127.0.0.1 in the container + // netns only — LND / the Bitcoin UI dialing bitcoin-knots:8332 over + // the bridge get connection refused (fresh-install LND crash-loop + + // bitcoin-rpc 502, seen on the 1.7.99 ISO). The port publish stays + // 127.0.0.1-only on the host, so exposure is unchanged. let bitcoin_conf = format!( "\ # rpcauth: salted hash only - no plaintext password in config or CLI\n\ {}\n\ server=1\n\ +rpcbind=0.0.0.0\n\ rpcallowip=0.0.0.0/0\n\ listen=1\n\ rpcthreads=16\n\ @@ -2475,6 +2491,82 @@ async fn wait_for_adopted_container(package_id: &str, container_name: &str) -> R )) } +/// One-shot probe: does bitcoind answer an authenticated getblockchaininfo? +/// Works during IBD (the call answers with progress while syncing). Goes via +/// the host-published RPC port, which fails in exactly the same conditions +/// as the container-network path (bitcoind down, still binding, bad rpcbind). +async fn bitcoin_rpc_answering() -> bool { + let (user, pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await; + let client = match reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + { + Ok(c) => c, + Err(_) => return false, + }; + let body = serde_json::json!({ + "jsonrpc": "1.0", + "id": "install-gate", + "method": "getblockchaininfo", + "params": [], + }); + match client + .post(crate::constants::BITCOIN_RPC_URL) + .basic_auth(&user, Some(&pass)) + .json(&body) + .send() + .await + { + Ok(resp) => resp.status().is_success(), + Err(_) => false, + } +} + +/// Hold the install of a bitcoin-dependent app until bitcoind's RPC answers, +/// up to 3 minutes. No-op for apps that don't need bitcoin at start. +async fn wait_for_bitcoin_rpc_gate(package_id: &str) -> Result<()> { + if !matches!( + package_id, + "lnd" | "electrumx" | "electrs" | "mempool-electrs" | "btcpay-server" | "btcpayserver" + ) { + return Ok(()); + } + let deadline = tokio::time::Instant::now() + Duration::from_secs(180); + let mut announced = false; + while !bitcoin_rpc_answering().await { + if tokio::time::Instant::now() >= deadline { + install_log(&format!( + "INSTALL FAIL: {} — Bitcoin RPC not answering after 180s; refusing to start a container that would crash-loop", + package_id + )) + .await; + anyhow::bail!( + "Bitcoin's RPC is not responding, and {} needs it to start. \ + Bitcoin may still be starting up — wait a minute and try again. \ + If this persists, check the Bitcoin app logs.", + package_id + ); + } + if !announced { + install_log(&format!( + "INSTALL WAIT: {} — waiting for Bitcoin RPC to become ready (up to 3 min)", + package_id + )) + .await; + announced = true; + } + tokio::time::sleep(Duration::from_secs(5)).await; + } + if announced { + install_log(&format!( + "INSTALL WAIT OK: {} — Bitcoin RPC is answering, starting container", + package_id + )) + .await; + } + Ok(()) +} + async fn ensure_bitcoin_rpc_config() -> Result { let script = r#" set -eu @@ -2502,6 +2594,7 @@ ensure_line() { fi } ensure_line server=1 +ensure_line rpcbind=0.0.0.0 ensure_line rpcallowip=0.0.0.0/0 ensure_line listen=1 ensure_line rpcthreads=16