Skip to content

fix(liquidity): update LspNode in place on re-add - #1147

Open
Bartok9 wants to merge 4 commits into
lightningdevkit:mainfrom
Bartok9:fix/ldp-liquidity-lsp-in-place-update
Open

Bartok9 wants to merge 4 commits into
lightningdevkit:mainfrom
Bartok9:fix/ldp-liquidity-lsp-in-place-update

Conversation

@Bartok9

@Bartok9 Bartok9 commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

When an existing LSP node_id is re-added with a new address/token/0conf, update the existing entry in place instead of silently dropping the new values. Extends the peer_store upsert pattern (#1002) to the liqudity config path.

Test: add_liquidity_source_updates_existing_lsp_in_place.

Refs #700.
AI-assisted (Sera).
Agent-Owner: sera
Claim: sera

When an existing LSP node_id is re-added with a new address/token/0conf, update the existing entry in place rather than silently dropping the new values. Previously, duplicates were ignored; now the config is updated and the node is reconnected.

Related to lightningdevkit#700 (peer_store upsert in lightningdevkit#1002; this fixes the LSP config side).

Test: add_liquidity_source_updates_existing_lsp_in_place verifies re-add updates address, token, and 0conf settings.

AI-assisted (Sera).
Signed-off-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
@ldk-reviews-bot

ldk-reviews-bot commented Oct 10, 2026 •

Copy link
Copy Markdown

I've assigned @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ldk-reviews-bot
ldk-reviews-bot requested a review from tnull October 10, 2026 13:14
Comment thread src/liquidity/mod.rs Outdated
log_info!(self.logger, "LSP node {} already added, skipping.", node_id);
return Ok(());
if let Some(existing) = lsp_nodes.iter_mut().find(|n| n.node_id == node_id) {
if existing.address == address {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug: token and trust_peer_0conf updates are silently dropped when the address is unchanged.

The current check only compares the address, so re-adding the same node_id with a new token or trust_peer_0conf returns early and discards the changes. This means token rotation or revoking 0-conf trust at runtime has no effect.

Could we compare all three fields and only reset protocols or reconnect when the address changes?

let addr_changed = existing.address != address;
let changed = addr_changed
    || existing.token != token
    || existing.trust_peer_0conf != trust_peer_0conf;

if !changed {
    return Ok(());
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the early return only compared the address, so a token rotation or a 0conf trust change on the same node_id was dropped.

Pushed 3be1fb5: we now treat the entry as changed if address, token, or trust_peer_0conf differs, and we only reconnect / reset supported_protocols when the address actually changes. Token- or 0conf-only updates are applied in place and left alone if connect/discover is not needed.

Comment thread src/liquidity/mod.rs Outdated
let prev_trust =
std::mem::replace(&mut existing.trust_peer_0conf, trust_peer_0conf);
// Force rediscovery after config/address change.
let prev_protocols = std::mem::take(&mut existing.supported_protocols);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High: mem::take(supported_protocols) makes a healthy LSP "unknown" for the whole connect + discovery window → breaks LSPS2 acceptance / JIT flows

After this take() (L180) supported_protocols is None immediately, before connect + LSPS0 discovery run (up to LIQUIDITY_REQUEST_TIMEOUT_SECS). get_lsp_config / select_lsps_for_protocol (mod.rs L84-100) only match an LSP whose supported_protocols contains the protocol, and get_lsp_config doesn't wait because discovery_done_rx is already true. So during the window:

  • get_lsp_config(..) → None.
  • In event.rs (~L1637), get_lsp_config(&counterparty_node_id, 2).is_some() gates accept_underpaying_htlcs. An inbound JIT channel from this LSP arriving now is accepted without that override, so the LSP's fee-skimmed HTLC is rejected as underpaying: a failed or stuck JIT payment with the LSPS2 lease already consumed.
  • Outgoing LSPS1/LSPS2 requests fail with "no LSP" although the LSP didn't change.

Suggested fix: don't clear supported_protocols up front. Keep the old value until rediscovery succeeds (discover_lsp_protocols already overwrites it). If you want to invalidate on address change, clear it only after a successful reconnect, just before discovery. At minimum, make the event.rs LSPS2 check tolerant of "known LSP, discovery in flight".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — clearing supported_protocols up front made this LSP invisible to get_lsp_config for the whole connect + LSPS0 window.

46f911f keeps the previous protocols until discover_lsp_protocols overwrites them, and only reconnects/rediscovers when the address actually changed. Failure cleanup still restores the prior snapshot.

Also fixes the E0382 from 3be1fb5 (previous moved into the cleanup closure, then borrowed for the reconnect check) that failed check-python / macOS stable.

Re-adding an existing LSP compared only the address, so token rotation
and 0conf trust changes were dropped. Compare all three fields and
reconnect/rediscover only when the address changes.
Do not mem::take supported_protocols before connect+discover, and
decide reconnect from a flag instead of borrowing the moved snapshot.
Comment thread src/liquidity/mod.rs
// Keep supported_protocols until rediscovery overwrites them. Clearing here
// makes get_lsp_config miss this LSP for the whole connect+discover window.
let prev_protocols = existing.supported_protocols.clone();
previous = Some((prev_address, prev_token, prev_trust, prev_protocols));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the take() removal (L192-194) resolves the JIT-window concern.

One thing still open: the snapshot rollback can race with a concurrent update for the same node_id. The lock is released at L205, then cleanup (L209-221) restores the snapshot taken at L195:

  1. A snapshots addr0, sets addr1
  2. B snapshots addr1, sets addr2
  3. A fails → L213-216 write addr0 (and stale supported_protocols) over B's in-flight addr2
  4. B succeeds → Ok(()), but the stored config is addr0

Suggested fix: serialize updates per LSP (lock held across the whole call), or connect + discover first and commit to lsp_nodes only on success, which makes rollback unnecessary. If concurrent re-adds aren't a supported use, a doc note would also be acceptable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the lock is dropped before connect/discover, so a failed older update could write its snapshot over a newer re-add.

0499288 stamps each write with config_generation. Cleanup restores the snapshot (or removes a fresh insert) only when that generation is still current; a later add for the same node_id is left alone. Combined with the disconnect-then-dial fix so an address change actually hits the new endpoint before we treat the update as committed.

Comment thread src/liquidity/mod.rs Outdated
/// `trust_peer_0conf` controls whether the node will accept 0-confirmation channels opened by this
/// LSP. Note this supersedes [`Config::trusted_peers_0conf`] for this peer.
/// Duplicate `node_id`s are ignored.
/// Re-adding an existing `node_id` updates its address/token/0conf settings. A changed address reconnects and rediscovers protocols; token or 0conf-only updates are applied in place.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium: "A changed address reconnects" (L157) / "…and reconnecting" (L178) isn't true for an already-connected LSP

connect_peer_if_necessary (L227) returns Ok(()) immediately if the peer is connected (connection.rs:81-83). When updating a live LSP, the new address is never dialled or validated, and rediscovery (L236) runs over the old session. A wrong address is accepted and only fails at the next disconnect, and the rollback at L229/L238 never fires.

Suggested fix: on address change, disconnect_peer(node_id) then do_connect_peer(node_id, address). Otherwise, reword the doc and log to "applied on next reconnect".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — connect_peer_if_necessary returns immediately while the peer is up, so a live LSP never dialed the new address and the failure path never ran.

0499288 disconnects first, then do_connect_peer with the new address, and only rediscovers after that dial succeeds. Token/0conf-only updates still skip the reconnect. Doc and log now say we disconnect and reconnect, not that a no-op connect counts.

…rollback

connect_peer_if_necessary no-ops while the peer is up, so a live LSP kept
the old session. Disconnect, then dial the new address. Failure cleanup
restores or removes only if this write's generation is still current.

Signed-off-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants