From a9cefb832611fea7fa9960cf8e53784a94c21027 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 8 Aug 2026 08:14:11 -0400 Subject: [PATCH] fix(lnd): give the rotation's verify step its own deadline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mint wait and the post-rotation verify shared one 15-minute budget. A rotation that legitimately spent 14 of those minutes waiting for LND to mint a fresh macaroon — normal on a loaded node, where opening channel.db/graph.db/ wallet.db alone has been measured at 2m38s — then had 60 seconds to confirm the node identity and channel census came back, and would report FAILURE on a wallet that was completely healthy. That is the most alarming possible way to be wrong about someone's Lightning node: it names a backup directory and tells them to investigate before retrying, at the exact moment nothing is actually broken. Each wait now gets its own budget. Waiting longer costs nothing here — the failure this step exists to catch (changed identity, missing channels) is not time-sensitive. Co-Authored-By: Claude Opus 5 (1M context) --- core/archipelago/src/api/rpc/lnd/macaroons.rs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/core/archipelago/src/api/rpc/lnd/macaroons.rs b/core/archipelago/src/api/rpc/lnd/macaroons.rs index 1cf6657a..5c293575 100644 --- a/core/archipelago/src/api/rpc/lnd/macaroons.rs +++ b/core/archipelago/src/api/rpc/lnd/macaroons.rs @@ -616,10 +616,11 @@ async fn run_rotation( .await .with_context(|| format!("unlocking the wallet — old material is in {backup}"))?; - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(MACAROON_WAIT_SECS); + let mint_deadline = + std::time::Instant::now() + std::time::Duration::from_secs(MACAROON_WAIT_SECS); let admin_path = format!("{LND_MAINNET_DIR}/admin.macaroon"); let mut new_digest = None; - while std::time::Instant::now() < deadline { + while std::time::Instant::now() < mint_deadline { if let Some(d) = digest_as_root(&admin_path).await { new_digest = Some(d); break; @@ -644,10 +645,19 @@ async fn run_rotation( }); // 6. Verify the things that must NOT have changed. + // + // Its own budget, deliberately not the mint deadline. Sharing one would mean + // a rotation that legitimately spent 14 of its 15 minutes waiting for LND to + // mint gets 1 minute to prove the channels came back, and then reports + // FAILURE on a node that is perfectly healthy — the most alarming possible + // way to be wrong about someone's Lightning wallet. with_progress(|p| p.set("verify", StepState::Running, None)); - let (pubkey_after, census_after) = wait_for_serving(deadline).await.with_context(|| { - format!("verifying the node after rotation — old material is in {backup}") - })?; + let verify_deadline = + std::time::Instant::now() + std::time::Duration::from_secs(MACAROON_WAIT_SECS); + let (pubkey_after, census_after) = + wait_for_serving(verify_deadline).await.with_context(|| { + format!("verifying the node after rotation — old material is in {backup}") + })?; with_progress(|p| p.channels_after = Some(census_after.channels_open)); if pubkey_after != pubkey_before { with_progress(|p| p.set("verify", StepState::Failed, None));