Skip to content

Commit a2c86f9

Browse files
committed
Consolidate LSPS4 usability checks to execute time
Channel usability (is_usable) was checked at four separate points: htlc_intercepted, peer_connected, process_pending_htlcs, and calculate_htlc_actions_for_peer. Each had its own deferral logic, and they had to coordinate (the timer skipped channel opens assuming peer_connected already handled them). This coordination broke: PR #9 made peer_connected call process_htlcs_for_peer during reestablish, which saw an empty capacity map because non-usable channels were filtered out, and emitted a spurious OpenChannel on every reconnect with a pending HTLC. Move the usability check to execute_htlc_actions, right before forward_intercepted_htlc. If no usable channel exists, the forward is skipped and the HTLC stays in store for the timer to retry. htlc_intercepted, peer_connected, and process_pending_htlcs now all call process_htlcs_for_peer unconditionally. Change the pre-forward guard from is_peer_connected to has_usable_channel, which covers the disconnect+reconnect race where the peer is connected but the channel has not finished reestablishing.
1 parent b89fc71 commit a2c86f9

3 files changed

Lines changed: 232 additions & 63 deletions

File tree

flake.lock

Lines changed: 96 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{
2+
description = "Rust Lightning Development Environment";
3+
4+
inputs = {
5+
# Nixpkgs channel. New channels are released every 6 months.
6+
# See: https://github.com/NixOS/nixpkgs/tags
7+
nixpkgs.url = "github:nixos/nixpkgs/25.11";
8+
9+
# This makes it easy for the flake to be multi-platform.
10+
# See: https://github.com/numtide/flake-utils
11+
flake-utils.url = "github:numtide/flake-utils";
12+
13+
# Provides Rust toolchains.
14+
# See: https://github.com/oxalica/rust-overlay
15+
rust-overlay.url = "github:oxalica/rust-overlay";
16+
};
17+
18+
outputs =
19+
{
20+
self,
21+
nixpkgs,
22+
flake-utils,
23+
rust-overlay,
24+
}:
25+
flake-utils.lib.eachDefaultSystem (
26+
system:
27+
let
28+
# Overlays provide additional packages not available in the channels.
29+
overlays = [
30+
# Provides the rust-bin package; a set of pre-built Rust toolchains.
31+
(import rust-overlay)
32+
];
33+
34+
# The final set of packages.
35+
pkgs = import nixpkgs {
36+
# Inheriting from system is what makes this multi-platform.
37+
# We also inherit the overlays that we want to use.
38+
inherit system overlays;
39+
};
40+
41+
# The specific Rust toolchain that we use in development shells.
42+
rust-toolchain = pkgs.rust-bin.stable."1.85.1".default.override {
43+
extensions = [ "rust-src" ]; # Needed for the rust-analyzer extension to work.
44+
};
45+
46+
# Common packages that are required to build and test the software.
47+
commonPackages = with pkgs; [
48+
rust-toolchain # Rust toolchain
49+
nodejs # JavaScript runtime, required for MCP tools
50+
mold # Fast linker for Rust/C/C++
51+
pnpm # Package manager for JavaScript, required for MCP tools
52+
stdenv.cc.cc.lib # C++ standard library for runtime
53+
];
54+
55+
# Common environment variables that are required.
56+
commonEnv = {
57+
# OpenSSL configuration for Nix
58+
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
59+
# C++ standard library path for runtime
60+
LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib";
61+
};
62+
63+
# The script to be executed upon entering the development shell.
64+
# Useful for configuration and validation of the environment.
65+
commonShellHook = ''
66+
echo "=========================================="
67+
echo " Rust Lightning Development Shell"
68+
echo "=========================================="
69+
70+
# Verify Rust version
71+
rustc --version
72+
cargo --version
73+
74+
echo "=========================================="
75+
echo "Terminal ready. All systems operational."
76+
echo "=========================================="
77+
'';
78+
79+
# Helper function to create a development shell that is pre-configured
80+
# with common packages, environment variables, and shell hook.
81+
mkDevShell =
82+
{
83+
name ? "",
84+
packages ? [ ],
85+
env ? { },
86+
shellHook ? "",
87+
}:
88+
pkgs.mkShell {
89+
inherit name;
90+
packages = commonPackages ++ packages;
91+
shellHook = commonShellHook + shellHook;
92+
env = commonEnv // env;
93+
};
94+
in
95+
{
96+
# The development shells.
97+
devShells = {
98+
# The default development shell for humans.
99+
# Use `nix develop` or direnv to enter the shell.
100+
default = mkDevShell {
101+
name = "rust-lightning-dev-shell";
102+
};
103+
};
104+
}
105+
);
106+
}
107+

lightning-liquidity/src/lsps4/service.rs

Lines changed: 29 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ where
218218
.get_cm()
219219
.list_channels_with_counterparty(&counterparty_node_id);
220220
let any_usable = channels.iter().any(|ch| ch.is_usable);
221-
let has_channels = !channels.is_empty();
222221

223222
log_info!(
224223
self.logger,
@@ -240,40 +239,23 @@ where
240239
);
241240
}
242241

243-
if has_channels && !any_usable {
244-
// Channels exist but none usable yet (channel_reestablish in progress).
245-
// Defer until process_pending_htlcs picks them up.
246-
log_info!(
247-
self.logger,
248-
"[LSPS4] htlc_intercepted: peer {} has {} channels but none usable, \
249-
deferring HTLC until channel_reestablish completes. payment_hash: {}",
250-
counterparty_node_id,
251-
channels.len(),
252-
payment_hash
253-
);
254-
self.htlc_store.insert(htlc).unwrap();
255-
} else {
256-
// Either channels are usable, or no channels exist (need JIT open).
257-
// calculate_htlc_actions_for_peer handles both: forward through usable
258-
// channels, or emit OpenChannel event when no capacity exists.
259-
let actions = self.calculate_htlc_actions_for_peer(
260-
counterparty_node_id,
261-
vec![htlc.clone()],
262-
);
242+
let actions = self.calculate_htlc_actions_for_peer(
243+
counterparty_node_id,
244+
vec![htlc.clone()],
245+
);
263246

264-
if actions.needs_liquidity_action() {
265-
self.htlc_store.insert(htlc).unwrap();
266-
}
247+
if actions.needs_liquidity_action() {
248+
self.htlc_store.insert(htlc).unwrap();
249+
}
267250

268-
log_debug!(
269-
self.logger,
270-
"[LSPS4] htlc_intercepted: calculated actions for peer {}: {:?}",
271-
counterparty_node_id,
272-
actions
273-
);
251+
log_debug!(
252+
self.logger,
253+
"[LSPS4] htlc_intercepted: calculated actions for peer {}: {:?}",
254+
counterparty_node_id,
255+
actions
256+
);
274257

275-
self.execute_htlc_actions(actions, counterparty_node_id.clone());
276-
}
258+
self.execute_htlc_actions(actions, counterparty_node_id.clone());
277259
}
278260
} else {
279261
log_error!(
@@ -414,28 +396,7 @@ where
414396
}
415397
}
416398

417-
if self.has_usable_channel(&counterparty_node_id) || channels.is_empty() {
418-
// Either channels are usable (forward immediately) or no channels exist
419-
// at all (process_htlcs_for_peer will trigger OpenChannel for JIT).
420-
self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs);
421-
} else {
422-
// Channels exist but none usable yet (reestablish in progress).
423-
// We still call process_htlcs_for_peer because calculate_htlc_actions
424-
// skips non-usable channels. If existing capacity is insufficient, this
425-
// will emit OpenChannel now rather than deferring to process_pending_htlcs
426-
// (which would never open a channel). Forwards through existing channels
427-
// will be empty since none are usable, so no premature forwarding occurs.
428-
// The actual forwards happen later via channel_ready or process_pending_htlcs
429-
// once reestablish completes.
430-
log_info!(
431-
self.logger,
432-
"[LSPS4] peer_connected: {} has {} pending HTLCs, channels not yet usable \
433-
(reestablish in progress) - checking if new channel needed",
434-
counterparty_node_id,
435-
htlcs.len()
436-
);
437-
self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs);
438-
}
399+
self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs);
439400

440401
log_info!(
441402
self.logger,
@@ -589,11 +550,14 @@ where
589550
}
590551

591552
if self.has_usable_channel(&node_id) {
592-
let actions = self.calculate_htlc_actions_for_peer(node_id, htlcs);
593-
if actions.is_empty() {
594-
continue;
595-
}
596-
self.execute_htlc_actions(actions, node_id);
553+
// Channel reestablish completed — attempt to forward the deferred HTLCs.
554+
log_info!(
555+
self.logger,
556+
"[LSPS4] process_pending_htlcs: forwarding {} HTLCs for peer {} (channel now usable)",
557+
htlcs.len(),
558+
node_id
559+
);
560+
self.process_htlcs_for_peer(node_id, htlcs);
597561
}
598562
}
599563
}
@@ -835,12 +799,14 @@ where
835799

836800
// Execute forwards
837801
for forward_action in forwards {
838-
// Re-check peer liveness right before forwarding to narrow the
839-
// TOCTOU window between the usability check and the actual forward.
840-
if !self.is_peer_connected(&their_node_id) {
802+
// Re-check channel usability right before forwarding to narrow the
803+
// TOCTOU window. Covers both peer disconnect and disconnect+reconnect
804+
// (where the peer is connected but the channel is still reestablishing).
805+
if !self.has_usable_channel(&their_node_id) {
841806
log_info!(
842807
self.logger,
843-
"[LSPS4] execute_htlc_actions: peer {} disconnected before forward, skipping HTLC {:?} (will retry on next timer tick). payment_hash: {}",
808+
"[LSPS4] execute_htlc_actions: no usable channel for peer {}, \
809+
skipping HTLC {:?} (will retry on next timer tick). payment_hash: {}",
844810
their_node_id,
845811
forward_action.htlc.id(),
846812
forward_action.htlc.payment_hash()

0 commit comments

Comments
 (0)