multi: expose peer address sources and offline peers in ListPeers#10973
multi: expose peer address sources and offline peers in ListPeers#10973ZZiigguurraatt wants to merge 1 commit into
Conversation
ListPeers previously returned only currently-connected peers with a single
`address` field, leaving no way to see:
1. Why lnd is maintaining a connection to a given peer.
2. What addresses lnd has stored for reconnecting to the peer.
3. What the peer currently advertises in its NodeAnnouncement.
4. Which peers lnd is auto-reconnecting to but is not currently connected
to.
5. Whether a reconnect attempt is in flight.
This commit fills those gaps by adding to the `Peer` message:
* `is_persistent` — true if lnd is auto-reconnecting to the peer.
* `remembered_addresses` — addresses lnd has stored for the peer (from
the LinkNode record captured at first channel open).
* `gossip_addresses` — addresses from the peer's current
NodeAnnouncement, populated inline instead of requiring a separate
GetNodeInfo call per peer.
* `reconnect_pending` — true if lnd currently has an in-flight reconnect
attempt for the peer.
`ListPeersRequest` gains `include_offline_persistent_peers`. When set, the
response also includes peers that are in lnd's auto-reconnect set but not
currently connected. Per-connection fields (address, bytes_sent, ping_time,
etc.) are empty/zero for those entries.
`lncli listpeers` gains an `--include_offline_persistent_peers` flag.
itest: `listpeers address fields` opens a channel between two nodes,
suspends the peer, and asserts the new field state in both the connected
and offline-persistent cases.
Partial fix for lightningnetwork#10871
e000a9b to
ef81280
Compare
|
PR Severity: CRITICAL 🔴
🔴 Critical (2 files)
🟠 High (3 files)
🟡 Medium (1 file)
🟢 Low (4 files)
Analysis The PR adds new functionality touching No severity bump was applied: after excluding test files ( Given the direct modification of To override, add a |
|
/gateway review |
|
Sorry, |
When lnd completes an outbound connection to a peer that we have an
open channel with, record the address we dialed in that peer's LinkNode
entry (if it is not already listed). Peers without an open channel are
skipped — so casual connections do not grow the on-disk store, and a
lingering entry for a peer whose channels were closed but not yet
reaped by `PruneLinkNodes` will not be extended if we happen to
reconnect to them in the meantime.
Motivation
----------
There are two sources of addresses used to reach a channel peer:
1. LinkNode — the peer's address stored in the channel state DB.
2. Gossip — addresses from the peer's most recent
`NodeAnnouncement`, stored in the on-disk channel graph.
On startup, `establishPersistentConnections` dials the union of these
two sources. That behaviour is unchanged by this commit.
Before this commit:
LinkNode was written once at channel-open and not updated afterward.
Gossip carries subsequent listener changes when a peer re-signs and
re-broadcasts a `NodeAnnouncement`, but there are cases where the
address we can actually reach the peer at is not in their current
gossip entry:
* the peer has since removed the address from their
`NodeAnnouncement` but is still listening on the same host and
port,
* the peer moved to a new host and/or port and we learned the new
address out-of-band faster than the peer's re-broadcast
`NodeAnnouncement` could catch up, or
* the peer never advertised the address in gossip in the first
place (private channel, LSP setup, LAN address).
In any of these cases, lnd could reach the peer at the address at
some point — via automatic reconnect (at startup or after a
mid-session disconnect) while the address was still in gossip, or
via `lncli connect <pub>@<addr>` for addresses that were never in
gossip or where gossip lagged the peer's move. But the address was
not persisted anywhere — so the next restart's startup dial failed
even though a live connection had just worked.
After this commit:
Every successful outbound connection to a channel peer appends the
dialed address to the peer's LinkNode entry (deduped by string). The
next restart's startup dial includes it, so an operator-supplied or
otherwise learned address survives across restarts for channel peers.
Non-channel peers, addresses already listed, and inbound connections
are skipped (see below for the reasoning behind the inbound skip).
Only outbound
-------------
The auto-persist path runs only for outbound connections. For inbound
TCP the peer's `RemoteAddr()` is the ephemeral source port their
kernel picked for the outbound half of their side, not a listener we
could dial back to. Persisting it would inflate the LinkNode address
list with non-dialable entries that fail on every subsequent startup
dial.
There is no equivalent for learning a peer's new listener from an
inbound connection today: BOLT #1 `Init` does not carry a listen
address, and `NodeAnnouncement` gossip is the intended source of
truth. lnd already forwards this: an lnd peer with a confirmed public
channel to us pushes its signed `NodeAnnouncement` on reconnect
(`peer.maybeSendNodeAnn`), and the gossiper writes it to the on-disk
channel graph, which `establishPersistentConnections` reads. That
covers the public-channel-with-lnd-peer case automatically. The gap
this commit narrows is what remains: operator-supplied addresses for
private channels, non-lnd peers, or gossip-lag windows.
Design
------
* `channeldb.LinkNodeDB.AddAddressIfPeerKnown(pub, addr)` appends
`addr` to the peer's LinkNode entry when one already exists and the
address is not already listed. It is a no-op (nil error) when the
peer has no LinkNode entry, which is the invariant we need to gate
the store's growth.
* `server.maybePersistPeerAddress` is called from `peerConnected`
only when `inbound` is false and only after `addPeer`. It checks
`chanStateDB.FetchOpenChannels` for the peer and only then calls
the DB helper. Errors are logged and swallowed.
Since channel peers always have a LinkNode entry (created at channel
open, and `RepairLinkNodes` reinstates missing ones at startup), the
"no-op when no entry" branch of `AddAddressIfPeerKnown` will not fire
in practice for channel peers — the open-channel gate is what keeps the
store from growing.
The new address is surfaced through the existing `remembered_addresses`
field on `Peer` introduced in lightningnetwork#10973; no new RPC surface is required.
itest
-----
`auto persist channel peer address` opens a channel between Alice and
Bob at Bob's initial P2P port, disconnects them, restarts Bob on a new
port, then reconnects Alice to Bob at the new port. The test asserts
that Alice's `remembered_addresses` for Bob then contains both the
channel-open address and the newly-connected address.
Depends on lightningnetwork#10973: the itest reads the `remembered_addresses` field
and uses the `include_offline_persistent_peers` flag added there.
Contributes to lightningnetwork#10870.
When lnd completes an outbound connection to a peer that we have an
open channel with, record the address we dialed in that peer's LinkNode
entry (if it is not already listed). Peers without an open channel are
skipped — so casual connections do not grow the on-disk store, and a
lingering entry for a peer whose channels were closed but not yet
reaped by `PruneLinkNodes` will not be extended if we happen to
reconnect to them in the meantime.
Motivation
----------
There are two sources of addresses used to reach a channel peer:
1. LinkNode — the peer's address stored in the channel state DB.
2. Gossip — addresses from the peer's most recent
`NodeAnnouncement`, stored in the on-disk channel graph.
On startup, `establishPersistentConnections` dials the union of these
two sources. That behaviour is unchanged by this commit.
Before this commit:
LinkNode was written once at channel-open and not updated afterward.
Gossip carries subsequent listener changes when a peer re-signs and
re-broadcasts a `NodeAnnouncement`, but there are cases where the
address we can actually reach the peer at is not in their current
gossip entry:
* the peer has since removed the address from their
`NodeAnnouncement` but is still listening on the same host and
port,
* the peer moved to a new host and/or port and we learned the new
address out-of-band faster than the peer's re-broadcast
`NodeAnnouncement` could catch up, or
* the peer never advertised the address in gossip in the first
place (private channel, LSP setup, LAN address).
In any of these cases, lnd could reach the peer at the address at
some point — via automatic reconnect (at startup or after a
mid-session disconnect) while the address was still in gossip, or
via `lncli connect <pub>@<addr>` for addresses that were never in
gossip or where gossip lagged the peer's move. But the address was
not persisted anywhere — so the next restart's startup dial failed
even though a live connection had just worked.
After this commit:
Every successful outbound connection to a channel peer appends the
dialed address to the peer's LinkNode entry (deduped by string). The
next restart's startup dial includes it, so an operator-supplied or
otherwise learned address survives across restarts for channel peers.
Non-channel peers, addresses already listed, and inbound connections
are skipped (see below for the reasoning behind the inbound skip).
Only outbound
-------------
The auto-persist path runs only for outbound connections. For inbound
TCP the peer's `RemoteAddr()` is the ephemeral source port their
kernel picked for the outbound half of their side, not a listener we
could dial back to. Persisting it would inflate the LinkNode address
list with non-dialable entries that fail on every subsequent startup
dial.
There is no equivalent for learning a peer's new listener from an
inbound connection today: BOLT #1 `Init` does not carry a listen
address, and `NodeAnnouncement` gossip is the intended source of
truth. lnd already forwards this: an lnd peer with a confirmed public
channel to us pushes its signed `NodeAnnouncement` on reconnect
(`peer.maybeSendNodeAnn`), and the gossiper writes it to the on-disk
channel graph, which `establishPersistentConnections` reads. That
covers the public-channel-with-lnd-peer case automatically. The gap
this commit narrows is what remains: operator-supplied addresses for
private channels, non-lnd peers, or gossip-lag windows.
Design
------
* `channeldb.LinkNodeDB.AddAddressIfPeerKnown(pub, addr)` appends
`addr` to the peer's LinkNode entry when one already exists and the
address is not already listed. It is a no-op (nil error) when the
peer has no LinkNode entry, which is the invariant we need to gate
the store's growth.
* `server.maybePersistPeerAddress` is called from `peerConnected`
only when `inbound` is false and only after `addPeer`. It checks
`chanStateDB.FetchOpenChannels` for the peer and only then calls
the DB helper. Errors are logged and swallowed.
Since channel peers always have a LinkNode entry (created at channel
open, and `RepairLinkNodes` reinstates missing ones at startup), the
"no-op when no entry" branch of `AddAddressIfPeerKnown` will not fire
in practice for channel peers — the open-channel gate is what keeps the
store from growing.
The new address is surfaced through the existing `remembered_addresses`
field on `Peer` introduced in lightningnetwork#10973; no new RPC surface is required.
itest
-----
`auto persist channel peer address` opens a channel between Alice and
Bob at Bob's initial P2P port, disconnects them, restarts Bob on a new
port, then reconnects Alice to Bob at the new port. The test asserts
that Alice's `remembered_addresses` for Bob then contains both the
channel-open address and the newly-connected address.
Depends on lightningnetwork#10973: the itest reads the `remembered_addresses` field
and uses the `include_offline_persistent_peers` flag added there.
Contributes to lightningnetwork#10870.
Extends `lncli disconnect` with two ways to prune a peer's stored
LinkNode state:
* `--forget_node` for whole-peer removal.
* `--forget_address` for surgical, per-address removal.
Both operations are gated by a shared `--force` flag when the requested
action would lose meaningful state:
* `--forget_node` drops the live connection (base `disconnect` behavior)
and additionally deletes the peer's LinkNode entry. The LinkNode
delete is refused if open channels still exist with the peer unless
`--force` is also set.
* `--forget_address <host:port>` removes a single stored address from
the peer's LinkNode entry. The live connection is left in place
unless the removed address happens to be the currently-connected
one, in which case the connection is dropped so lnd can re-dial via
the remaining stored/gossip addresses. If the removed address would
be the last one stored for the peer, behaviour depends on whether
open channels exist: with no open channels the LinkNode entry is
deleted automatically; with open channels the request is refused
unless `--force` is also set. Note that after `--forget_address
--force` removes the last stored address for a channel peer, the
peer stays in the persistent-reconnect set — the peer-termination
watcher falls back to the peer's NodeAnnouncement addresses for
reconnect. `--forget_node` is the way to also stop reconnecting
from our side (until lnd next restarts). Even with `--forget_address
--force`, a channel peer is only truly orphaned when it also has
no NodeAnnouncement.
* `--force` on its own is rejected. `--forget_node` and `--forget_address`
are mutually exclusive.
Response Status carries a warning when `--forget_address` triggers the
last-address LinkNode delete.
DB helper:
* new `channeldb.LinkNodeDB.RemoveAddressForPeer(pub, addr, allowLast)`
returning `(deletedEntry bool, err error)`.
* new sentinel errors: `ErrNodeAddressNotFound` (address not stored
for peer), `ErrLastPeerAddress` (refusing to remove last address
without `allowLast`).
Server:
* `DisconnectPeer` signature grows `forgetNode` and `force` parameters.
* new `ForgetPeerAddress` method handles the per-address case,
including the "disconnect if removed address is current" behaviour.
* all internal callers of `DisconnectPeer` updated to pass
`false, false`.
Proto: `DisconnectPeerRequest` gains `forget_node` (2), `force` (3), and
`forget_address` (4). `DisconnectPeerResponse.Status` reuses its
existing type.
itest: `disconnect forget` covers all branches — per-address removal,
last-address gating, `--forget_node` with and without `--force` against a
channel peer, and both error cases (`--force` alone; `--forget_node` +
`--forget_address`).
Depends on lightningnetwork#10975 (which depends on lightningnetwork#10973): the itest seeds a second
stored address by reconnecting Alice on Bob's alternate listener, which
`remembered_addresses` field added in lightningnetwork#10973.
Contributes to lightningnetwork#10870.
Contributes to lightningnetwork#10871.
Extends `lncli disconnect` with two ways to prune a peer's stored
LinkNode state:
* `--forget_node` for whole-peer removal.
* `--forget_address` for surgical, per-address removal.
Both operations are gated by a shared `--force` flag when the requested
action would lose meaningful state:
* `--forget_node` drops the live connection (base `disconnect` behavior)
and additionally deletes the peer's LinkNode entry. The LinkNode
delete is refused if open channels still exist with the peer unless
`--force` is also set.
* `--forget_address <host:port>` removes a single stored address from
the peer's LinkNode entry. The live connection is left in place
unless the removed address happens to be the currently-connected
one, in which case the connection is dropped so lnd can re-dial via
the remaining stored/gossip addresses. If the removed address would
be the last one stored for the peer, behaviour depends on whether
open channels exist: with no open channels the LinkNode entry is
deleted automatically; with open channels the request is refused
unless `--force` is also set. Note that after `--forget_address
--force` removes the last stored address for a channel peer, the
peer stays in the persistent-reconnect set — the peer-termination
watcher falls back to the peer's NodeAnnouncement addresses for
reconnect. `--forget_node` is the way to also stop reconnecting
from our side (until lnd next restarts). Even with `--forget_address
--force`, a channel peer is only truly orphaned when it also has
no NodeAnnouncement.
* `--force` on its own is rejected. `--forget_node` and `--forget_address`
are mutually exclusive.
Response Status carries a warning when `--forget_address` triggers the
last-address LinkNode delete.
DB helper:
* new `channeldb.LinkNodeDB.RemoveAddressForPeer(pub, addr, allowLast)`
returning `(deletedEntry bool, err error)`.
* new sentinel errors: `ErrNodeAddressNotFound` (address not stored
for peer), `ErrLastPeerAddress` (refusing to remove last address
without `allowLast`).
Server:
* `DisconnectPeer` signature grows `forgetNode` and `force` parameters.
* new `ForgetPeerAddress` method handles the per-address case,
including the "disconnect if removed address is current" behaviour.
* all internal callers of `DisconnectPeer` updated to pass
`false, false`.
Proto: `DisconnectPeerRequest` gains `forget_node` (2), `force` (3), and
`forget_address` (4). `DisconnectPeerResponse.Status` reuses its
existing type.
itest: `disconnect forget` covers all branches — per-address removal,
last-address gating, `--forget_node` with and without `--force` against a
channel peer, and both error cases (`--force` alone; `--forget_node` +
`--forget_address`).
Depends on lightningnetwork#10975 (which depends on lightningnetwork#10973): the itest seeds a second
stored address by reconnecting Alice on Bob's alternate listener, which
lightningnetwork#10975's auto-persist captures into LinkNode; it also reads the
`remembered_addresses` field added in lightningnetwork#10973.
Contributes to lightningnetwork#10871.
Extends `lncli disconnect` with two ways to prune a peer's stored
LinkNode state:
* `--forget_node` for whole-peer removal.
* `--forget_address` for surgical, per-address removal.
Both operations are gated by a shared `--force` flag when the requested
action would lose meaningful state:
* `--forget_node` drops the live connection (base `disconnect` behavior)
and additionally deletes the peer's LinkNode entry. The LinkNode
delete is refused if open channels still exist with the peer unless
`--force` is also set.
* `--forget_address <host:port>` removes a single stored address from
the peer's LinkNode entry. The live connection is left in place
unless the removed address happens to be the currently-connected
one, in which case the connection is dropped so lnd can re-dial via
the remaining stored/gossip addresses. If the removed address would
be the last one stored for the peer, behaviour depends on whether
open channels exist: with no open channels the LinkNode entry is
deleted automatically; with open channels the request is refused
unless `--force` is also set. Note that after `--forget_address
--force` removes the last stored address for a channel peer, the
peer stays in the persistent-reconnect set — the peer-termination
watcher falls back to the peer's NodeAnnouncement addresses for
reconnect. `--forget_node` is the way to also stop reconnecting
from our side (until lnd next restarts). Even with `--forget_address
--force`, a channel peer is only truly orphaned when it also has
no NodeAnnouncement.
* `--force` on its own is rejected. `--forget_node` and `--forget_address`
are mutually exclusive.
Response Status carries a warning when `--forget_address` triggers the
last-address LinkNode delete.
DB helper:
* new `channeldb.LinkNodeDB.RemoveAddressForPeer(pub, addr, allowLast)`
returning `(deletedEntry bool, err error)`.
* new sentinel errors: `ErrNodeAddressNotFound` (address not stored
for peer), `ErrLastPeerAddress` (refusing to remove last address
without `allowLast`).
Server:
* `DisconnectPeer` signature grows `forgetNode` and `force` parameters.
* new `ForgetPeerAddress` method handles the per-address case,
including the "disconnect if removed address is current" behaviour.
* all internal callers of `DisconnectPeer` updated to pass
`false, false`.
Proto: `DisconnectPeerRequest` gains `forget_node` (2), `force` (3), and
`forget_address` (4). `DisconnectPeerResponse.Status` reuses its
existing type.
itest: `disconnect forget` covers all branches — per-address removal,
last-address gating, `--forget_node` with and without `--force` against a
channel peer, and both error cases (`--force` alone; `--forget_node` +
`--forget_address`).
Depends on lightningnetwork#10975 (which depends on lightningnetwork#10973): the itest seeds a second
stored address by reconnecting Alice on Bob's alternate listener, which
`remembered_addresses` field added in lightningnetwork#10973.
Contributes to lightningnetwork#10871.
|
/gateway review |
There was a problem hiding this comment.
Gateway review — 6 findings
🔴 0 Blocker · 🟠 2 Major · 🟡 4 Minor · 🔵 0 Nit
Summary
This PR extends ListPeers with peer address-source detail (remembered_addresses, gossip_addresses, is_persistent, reconnect_pending) and an include_offline_persistent_peers request flag. The intent is sound, the proto field numbering is clean, the new flag defaults to false (backward compatible), and the pubkey map-keying is consistent across the connected and offline paths.
The main concern is performance: ListPeers was previously an in-memory read and now issues an unbounded FetchAllLinkNodes() scan plus one v1Graph.FetchNode() per peer on every call, on an endpoint with high caller reach that monitoring tools poll. The second concern is API shape: offline-persistent entries are structurally identical to connected peers but with every per-connection field zeroed, and there is no explicit discriminator, which invites consumers to read zeros as real telemetry.
Bot commands
/gateway re-review— re-run after pushing changes/gateway dismiss <id>— silence a finding (maintainers)/gateway explain <id>— elaborate on a finding (anyone)
| prior channel open or `connect --perm`) or an open channel. Per- | ||
| connection fields (address, bytes_sent, ping_time, etc.) are empty/zero | ||
| for these entries. | ||
| */ |
There was a problem hiding this comment.
🟠 F1 (Major) — Offline-persistent entries have no discriminator flag
When include_offline_persistent_peers=true, the peers list mixes live-connection records and offline placeholders that are structurally identical, with no explicit type tag. An offline entry carries address="" and every per-connection field (ping_time, bytes_sent, bytes_recv, sat_sent, sync_type, features, flap_count, …) zeroed, so a consumer reusing the existing for _, p := range resp.Peers { use(p.PingTime, p.SyncType) } pattern silently treats those zeros as real telemetry.
Why this matters
No field cleanly discriminates the two: empty address is also reachable transiently for a genuinely connected peer, and is_persistent/reconnect_pending can both be true for a live connection. Adding a discriminator to a released proto is a compatibility headache, so it is worth resolving before merge — e.g. an explicit bool connected (or a PeerConnectionState enum) on Peer that is unambiguously false for offline-persistent entries, letting a caller gate per-connection fields on one documented flag rather than inferring liveness from an in-band empty string.
| } | ||
|
|
||
| // Pre-fetch the LinkNode store into a pubkey-keyed map so the | ||
| // per-peer loop becomes a constant lookup rather than a per-peer DB |
There was a problem hiding this comment.
🟠 F2 (Major) — ListPeers now does an unbounded LinkNode scan plus a graph lookup per peer
Each ListPeers call now runs r.server.linkNodeDB.FetchAllLinkNodes() (a full, unpaginated table scan) plus a r.server.v1Graph.FetchNode(ctx, vertex) graph read for every connected peer and every offline persistent peer. This converts a previously cheap in-memory RPC into O(peers) database + graph reads, with no caching or bound.
Why this matters
ListPeers has high caller reach (~22 files, including getdebuginfo, itests, and operator/monitoring tooling that polls it), so the new cost is paid on every invocation and scales with the node's link-node history and connection count — a real latency and DB-contention regression on large nodes. It is macaroon-gated and bounded by the node's own state rather than remotely inflatable, so this is a performance/hardening regression, not a remote DoS. Consider batching or caching the FetchAllLinkNodes result, or gating the graph lookups behind the request so the default hot path stays cheap. (cf. SEC-RULE-120 / SEC-RULE-110.)
| by the Tor project in 2021). v2 entries are preserved on disk so | ||
| that signed NodeAnnouncements still verify; a v2 appearing here is | ||
| a stale-storage signal, not an indication that lnd is attempting to | ||
| reach that address. |
There was a problem hiding this comment.
🟡 F3 (Minor) — remembered_addresses can carry undialable v2 .onion entries
remembered_addresses is reported verbatim from LinkNode storage and may contain deprecated v2 .onion addresses the node will never dial, whereas the adjacent gossip_addresses has v2 filtered out. The dial-safety distinction between these two repeated string fields is load-bearing but invisible in the type; a consumer that feeds the intuitively-named remembered_addresses into a reconnect routine will attempt undialable addresses. The proto comment does document this, but consider filtering v2 out here too, or keep the caveat prominent.
| // Returns an empty slice for any error (e.g. peer not in graph) since | ||
| // missing gossip data is normal. | ||
| gossipAddrsFor := func(pubBytes []byte) []string { | ||
| vertex, vErr := route.NewVertexFromBytes(pubBytes) |
There was a problem hiding this comment.
🟡 F4 (Minor) — gossipAddrsFor silently swallows FetchNode errors
gossipAddrsFor returns nil on any FetchNode error, making a transient graph read failure indistinguishable from "node not in graph / no addresses." (The nil-on-error path does correctly avoid aborting the whole RPC, so there is no fatal-error regression.) Log the error at debug level so operators can tell an empty gossip_addresses from a real read failure.
|
|
||
| rpcPeer.RememberedAddresses = toStrs( | ||
| rememberedAddrsByPub[pubStr], | ||
| ) |
There was a problem hiding this comment.
🟡 F5 (Minor) — is_persistent's headline meaning diverges from its computed value
is_persistent is documented as "true if lnd is maintaining a persistent reconnect attempt," but for connected peers it is computed as inPersistentSet || len(remembered_addresses) > 0 and for offline entries hardcoded true. A peer with a stale LinkNode record (e.g. all channels closed but not yet garbage-collected) will report is_persistent=true even when lnd is not reconnecting to it, and the value is derived by different rules across the two entry types. Either derive it uniformly from the persistent set or align the proto comment with the actual computation.
| // default. | ||
|
|
||
| resp.Peers = append(resp.Peers, rpcPeer) | ||
| } |
There was a problem hiding this comment.
🟡 F6 (Minor) — Offline peers appended in nondeterministic map order
The offline loop ranges over persistentSet (a Go map) and appends to resp.Peers, so the order of offline entries varies between calls while connected peers keep loop order. This can produce unstable output and flaky assertions; sort the offline pubkeys before appending if stable ordering is expected.
|
🤖 gateway audit metadata for this PR — auto-generated, please don't edit. |
ListPeers previously returned only currently-connected peers with a single
addressfield, leaving no way to see:This commit fills those gaps by adding to the
Peermessage:is_persistent— true if lnd is auto-reconnecting to the peer.remembered_addresses— addresses lnd has stored for the peer (from the LinkNode record captured at first channel open).gossip_addresses— addresses from the peer's current NodeAnnouncement, populated inline instead of requiring a separate GetNodeInfo call per peer.reconnect_pending— true if lnd currently has an in-flight reconnect attempt for the peer.ListPeersRequestgainsinclude_offline_persistent_peers. When set, the response also includes peers that are in lnd's auto-reconnect set but not currently connected. Per-connection fields (address, bytes_sent, ping_time, etc.) are empty/zero for those entries.lncli listpeersgains an--include_offline_persistent_peersflag.itest:
listpeers address fieldsopens a channel between two nodes, suspends the peer, and asserts the new field state in both the connected and offline-persistent cases.Partial fix for #10871