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