fix(bitcoin/lnd): rpcbind=0.0.0.0 in generated bitcoin.conf + RPC readiness gate for dependent installs

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 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-14 07:31:12 -04:00
parent b48ea85961
commit f2a94548ab

View File

@ -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<bool> {
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