Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions channeldb/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,46 @@ func deleteLinkNode(tx kvdb.RwTx, identity *btcec.PublicKey) error {
return nodeMetaBucket.Delete(pubKey)
}

// AddAddressIfPeerKnown appends addr to the address list of the LinkNode entry
// for identity, but only if an entry already exists. If no LinkNode is stored
// for the peer this is a no-op and returns (false, nil). Duplicate addresses
// are ignored (matched by String()). Returns true if the address was newly
// stored.
func (l *LinkNodeDB) AddAddressIfPeerKnown(identity *btcec.PublicKey,
addr net.Addr) (bool, error) {

var added bool
err := kvdb.Update(l.backend, func(tx kvdb.RwTx) error {
nodeMetaBucket := tx.ReadWriteBucket(nodeInfoBucket)
if nodeMetaBucket == nil {
return nil
}

node, err := fetchLinkNode(tx, identity)
if errors.Is(err, ErrNodeNotFound) {
return nil
}
if err != nil {
return err
}

for _, a := range node.Addresses {
if a.String() == addr.String() {
return nil
}
}

node.Addresses = append(node.Addresses, addr)
added = true

return putLinkNode(nodeMetaBucket, node)
}, func() {
added = false
})

return added, err
}

// FetchLinkNode attempts to lookup the data for a LinkNode based on a target
// identity public key. If a particular LinkNode for the passed identity public
// key cannot be found, then ErrNodeNotFound if returned.
Expand Down
13 changes: 12 additions & 1 deletion cmd/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1633,12 +1633,20 @@
var listPeersCommand = cli.Command{
Name: "listpeers",
Category: "Peers",
Usage: "List all active, currently connected peers.",
Usage: "List currently connected peers, and optionally also " +
"offline peers that lnd is auto-reconnecting to.",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "list_errors",
Usage: "list a full set of most recent errors for the peer",
},
cli.BoolFlag{
Name: "include_offline_persistent_peers",
Usage: "also list peers that lnd is auto-reconnecting " +

Check failure on line 1645 in cmd/commands/commands.go

View workflow job for this annotation

GitHub Actions / Lint code

the line is 81 characters long, which exceeds the maximum of 80 characters. (ll)
"to but is not currently connected to — " +
"either via a stored LinkNode record or " +
"because of an open channel",
},
},
Action: actionDecorator(listPeers),
}
Expand All @@ -1652,6 +1660,9 @@
// specifically requests a full error set, then we will provide it.
req := &lnrpc.ListPeersRequest{
LatestError: !ctx.IsSet("list_errors"),
IncludeOfflinePersistentPeers: ctx.Bool(
"include_offline_persistent_peers",
),
}
resp, err := client.ListPeers(ctxc, req)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,26 @@
the chain backend via bitcoind's `submitpackage`, allowing a zero-fee v3/TRUC
parent to be accepted together with a fee-paying CPFP child.

* [`ListPeers` gains address-source
detail](https://github.com/lightningnetwork/lnd/pull/10973). Each `Peer`
now carries `remembered_addresses` (addresses lnd has stored for the peer),
`gossip_addresses` (from the peer's current `NodeAnnouncement`),
`is_persistent` (true if lnd is auto-reconnecting to the peer), and
`reconnect_pending` (true if a retry is in flight). A new
`ListPeersRequest.include_offline_persistent_peers` flag opts into
surfacing peers in the reconnect set we are not currently connected to.

## lncli Additions

* The `estimateroutefee` command now supports [restricting fee estimates to
specific first-hop outgoing
channels](https://github.com/lightningnetwork/lnd/pull/10501) via the new
`--outgoing_chan_id` flag.

* `lncli listpeers` gains
[`--include_offline_persistent_peers`](https://github.com/lightningnetwork/lnd/pull/10973)
(see the `ListPeers` RPC entry above).

* A new
[`wallet submitpackage`](https://github.com/lightningnetwork/lnd/pull/10900)
command submits a package of hex-encoded transactions via the new
Expand All @@ -88,6 +101,24 @@

## Functional Updates

* [Peer addresses now auto-persist on
connect](https://github.com/lightningnetwork/lnd/pull/10975) for peers we
have an open channel with. When lnd completes an outbound connection to
a channel peer, it records the dialed address in the peer's `LinkNode`
entry if it isn't already listed. On restart lnd attempts this address
in addition to those from the peer's current `NodeAnnouncement`, so a
channel peer remains reachable across restarts even when the address we
last used to reach them isn't in their gossip entry — because they have
since removed it from their `NodeAnnouncement` but are still listening
on the same host and port, because they moved to a new host and/or port
and we learned the new address out-of-band faster than their
re-broadcast `NodeAnnouncement` could catch up, or because they never
advertised it in gossip in the first place. Only outbound connections
trigger this — for inbound TCP the peer's remote address is an
ephemeral source port rather than a dialable listener. Non-channel
peers are also skipped so casual connections do not grow the on-disk
address list.

## RPC Updates

## lncli Updates
Expand Down Expand Up @@ -148,3 +179,4 @@
* Boris Nagaev
* Erick Cestari
* Jared Tobin
* ZZiigguurraatt
8 changes: 8 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ var allTestCases = []*lntest.TestCase{
Name: "reconnect after ip change",
TestFunc: testReconnectAfterIPChange,
},
{
Name: "listpeers address fields",
TestFunc: testListPeersAddressFields,
},
{
Name: "auto persist channel peer address",
TestFunc: testAutoPersistChannelPeerAddress,
},
{
Name: "addpeer config",
TestFunc: testAddPeerConfig,
Expand Down
190 changes: 190 additions & 0 deletions itest/lnd_network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,193 @@
// Check existing connection.
ht.AssertConnected(alice, bob)
}

// testListPeersAddressFields verifies the address-source fields that
// ListPeers exposes on the Peer message: is_persistent, remembered_addresses
// (from the LinkNode record), gossip_addresses (from NodeAnnouncement),
// reconnect_pending, and the include_offline_persistent_peers request flag.
func testListPeersAddressFields(ht *lntest.HarnessTest) {
alice := ht.NewNodeWithCoins("Alice", nil)
bob := ht.NewNode("Bob", nil)
bobInfo := bob.RPC.GetInfo()

// Opening a channel writes a LinkNode record for the peer (this is
// what remembered_addresses reads from), and puts the peer in the
// persistent-reconnect set.
ht.ConnectNodes(alice, bob)
ht.OpenChannel(alice, bob, lntest.OpenChannelParams{Amt: 100_000})

// Helper: pull Bob's entry out of Alice's ListPeers response.
bobPeer := func(req *lnrpc.ListPeersRequest) *lnrpc.Peer {
resp := alice.RPC.ListPeersReq(req)
for _, p := range resp.Peers {
if p.PubKey == bobInfo.IdentityPubkey {
return p
}
}
return nil
}

// While connected, the new fields should be populated.
require.NoError(ht, wait.NoError(func() error {
p := bobPeer(&lnrpc.ListPeersRequest{})
if p == nil {
return fmt.Errorf("bob not yet in listpeers")
}
if !p.IsPersistent {
return fmt.Errorf("is_persistent should be true for " +
"a channel peer")
}
if len(p.RememberedAddresses) == 0 {
return fmt.Errorf("remembered_addresses should be " +
"populated for a channel peer")
}
if p.ReconnectPending {
return fmt.Errorf("reconnect_pending should be " +
"false while connected")
}
return nil
}, defaultTimeout))

// Stop Bob so Alice's connection to him drops.
restartBob := ht.SuspendNode(bob)

// Without include_offline_persistent_peers, Bob should not appear
// in the response once his connection is gone.
require.NoError(ht, wait.NoError(func() error {
p := bobPeer(&lnrpc.ListPeersRequest{})
if p != nil {
return fmt.Errorf("bob still visible without " +
"include_offline_persistent_peers")
}
return nil
}, defaultTimeout))

// With include_offline_persistent_peers, Bob should appear as an
// offline persistent entry: address empty, remembered_addresses
// populated, is_persistent true.
p := bobPeer(&lnrpc.ListPeersRequest{
IncludeOfflinePersistentPeers: true,
})
require.NotNil(ht, p,
"bob should appear when include_offline_persistent_peers=true")
require.Empty(ht, p.Address,
"offline entry should have empty address")
require.True(ht, p.IsPersistent,
"offline channel peer should still be is_persistent")
require.NotEmpty(ht, p.RememberedAddresses,
"offline channel peer should still expose remembered_addresses")

// Bring Bob back online so the test doesn't leave a hung retry.
require.NoError(ht, restartBob())
}

// testAutoPersistChannelPeerAddress verifies that when a peer we have an
// open channel with connects on an address that isn't already in its
// LinkNode entry, that address is appended to the entry so a future
// restart can dial it. The scenario: open a channel between Alice and Bob
// at port A, disconnect them, move Bob to port B, reconnect Alice to Bob
// at port B, then assert that Alice's remembered_addresses for Bob now
// contains both A and B.
func testAutoPersistChannelPeerAddress(ht *lntest.HarnessTest) {
// Alice is set up with --nolisten so Bob physically cannot dial
// back to her at any point during the test. Combined with the
// long backoff on Alice's own persistent-reconnect logic, this
// guarantees the only peer connection to Bob we ever observe
// during the test is the outbound one we initiate explicitly.
aliceArgs := []string{
"--nolisten",
"--minbackoff=1h",
"--maxbackoff=1h",
}
alice := ht.NewNodeWithCoins("Alice", aliceArgs)
bob := ht.NewNode("Bob", nil)
bobInfo := bob.RPC.GetInfo()

portA := bob.Cfg.P2PPort
addrA := fmt.Sprintf("127.0.0.1:%d", portA)

// Open a channel — this seeds Alice's LinkNode entry for Bob with
// Bob's address at channel-open time, which is port A.
ht.ConnectNodes(alice, bob)
ht.OpenChannel(alice, bob, lntest.OpenChannelParams{Amt: 100_000})

// bobPeer pulls Bob's entry out of Alice's ListPeers response,
// including offline persistent entries so the assertion still works
// while Alice is disconnected from Bob.
bobPeer := func() *lnrpc.Peer {
resp := alice.RPC.ListPeersReq(&lnrpc.ListPeersRequest{
IncludeOfflinePersistentPeers: true,
})
for _, p := range resp.Peers {
if p.PubKey == bobInfo.IdentityPubkey {
return p
}
}
return nil
}

// Sanity: port A appears in remembered_addresses now.
require.NoError(ht, wait.NoError(func() error {
p := bobPeer()
if p == nil {
return fmt.Errorf("bob not yet in listpeers")
}
for _, a := range p.RememberedAddresses {
if a == addrA {
return nil
}
}
return fmt.Errorf("remembered_addresses %v missing %s",
p.RememberedAddresses, addrA)
}, defaultTimeout))

// Move Bob to port B.
portB := port.NextAvailablePort()
addrB := fmt.Sprintf("127.0.0.1:%d", portB)

ht.DisconnectNodes(alice, bob)
bob.Cfg.P2PPort = portB
ht.RestartNode(bob)

// Reconnect Alice to Bob explicitly at port B. This is a plain
// (non-perm) connect — the auto-persist behaviour is what we're
// exercising.
alice.RPC.ConnectPeer(&lnrpc.ConnectPeerRequest{
Addr: &lnrpc.LightningAddress{
Pubkey: bobInfo.IdentityPubkey,
Host: addrB,
},
})
ht.AssertConnected(alice, bob)

// Alice's remembered_addresses for Bob should now list both A
// (from channel open) and B (auto-persisted on this connect).
require.NoError(ht, wait.NoError(func() error {
p := bobPeer()
if p == nil {
return fmt.Errorf("bob missing from listpeers")
}
haveA, haveB := false, false
for _, a := range p.RememberedAddresses {
switch a {
case addrA:
haveA = true
case addrB:
haveB = true
}
}
if !haveA {
return fmt.Errorf("remembered_addresses %v missing "+
"channel-open address %s",
p.RememberedAddresses, addrA)
}
if !haveB {
return fmt.Errorf("remembered_addresses %v missing "+
"auto-persisted address %s",
p.RememberedAddresses, addrB)
}
return nil
}, defaultTimeout))
}

Check failure on line 525 in itest/lnd_network_test.go

View workflow job for this annotation

GitHub Actions / Lint code

File is not properly formatted (gci)
Loading
Loading