Skip to content
Draft
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
1 change: 1 addition & 0 deletions channeldb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ var dbTopLevelBuckets = [][]byte{
payAddrIndexBucket,
setIDIndexBucket,
peersBucket,
persistentPeersBucket,
nodeInfoBucket,
metaBucket,
closeSummaryBucket,
Expand Down
195 changes: 195 additions & 0 deletions channeldb/persistent_peers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package channeldb

import (
"bytes"
"errors"
"fmt"
"io"
"net"

"github.com/btcsuite/btcd/btcec/v2"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/kvdb"
)

var (
// persistentPeersBucket is the name of a top level bucket in which we
// store the set of peers that the user has explicitly requested to
// remain connected to across LND restarts (e.g. via
// `lncli connect --perm`). Entries are keyed by the peer's compressed
// pubkey and the value is the serialized list of addresses we should
// try when (re)connecting.
persistentPeersBucket = []byte("persistent-peers-bucket")

// ErrPersistentPeerNotFound is returned when no entry exists in the
// persistent peers bucket for the requested pubkey.
ErrPersistentPeerNotFound = errors.New("persistent peer not found")
)

// PersistentPeer represents a peer that the user has explicitly marked as
// persistent. Its addresses are stored so that the connection can be
// re-established when LND restarts.
type PersistentPeer struct {
// PubKey is the identity public key of the peer.
PubKey *btcec.PublicKey

// Addresses is the set of addresses that we will attempt to dial when
// (re)connecting to the peer.
Addresses []net.Addr
}

// AddPersistentPeer unions the given addresses with whatever is already
// stored for the peer, deduping by string. Calling this multiple times for
// the same peer is additive: each call only adds addresses, never removes
// them. Use DeletePersistentPeer to fully forget a peer. The boolean return
// is true if at least one of the given addresses was newly added (i.e. the
// stored set grew); false if everything in addrs was already present.
func (d *DB) AddPersistentPeer(pubKey *btcec.PublicKey,
addrs []net.Addr) (bool, error) {

var addedNew bool
err := kvdb.Update(d, func(tx kvdb.RwTx) error {
bucket, err := tx.CreateTopLevelBucket(persistentPeersBucket)
if err != nil {
return err
}

key := pubKey.SerializeCompressed()

// Read any existing entry so we can union with it.
existing := make([]net.Addr, 0)
if v := bucket.Get(key); v != nil {
existing, err = deserializeAddresses(
bytes.NewReader(v),
)
if err != nil {
return fmt.Errorf("unable to read existing "+
"persistent peer entry: %w", err)
}
}

seen := make(map[string]struct{}, len(existing)+len(addrs))
merged := make([]net.Addr, 0, len(existing)+len(addrs))
for _, a := range existing {
if _, ok := seen[a.String()]; ok {
continue
}
seen[a.String()] = struct{}{}
merged = append(merged, a)
}
addedNew = false
for _, a := range addrs {
if _, ok := seen[a.String()]; ok {
continue
}
seen[a.String()] = struct{}{}
merged = append(merged, a)
addedNew = true
}

var b bytes.Buffer
if err := serializeAddresses(&b, merged); err != nil {
return err
}

return bucket.Put(key, b.Bytes())
}, func() {
addedNew = false
})
return addedNew, err
}

// DeletePersistentPeer removes the persistent peer entry for the given
// pubkey. If no entry exists for the pubkey, this is a no-op.
func (d *DB) DeletePersistentPeer(pubKey *btcec.PublicKey) error {
return kvdb.Update(d, func(tx kvdb.RwTx) error {
bucket := tx.ReadWriteBucket(persistentPeersBucket)
if bucket == nil {
return nil
}

return bucket.Delete(pubKey.SerializeCompressed())
}, func() {})
}

// FetchAllPersistentPeers returns all persistent peers that the user has
// previously asked LND to keep connected to.
func (d *DB) FetchAllPersistentPeers() ([]*PersistentPeer, error) {
var peers []*PersistentPeer
err := kvdb.View(d, func(tx kvdb.RTx) error {
bucket := tx.ReadBucket(persistentPeersBucket)
if bucket == nil {
return nil
}

return bucket.ForEach(func(k, v []byte) error {
if v == nil {
return nil
}

pubKey, err := btcec.ParsePubKey(k)
if err != nil {
return fmt.Errorf("unable to parse "+
"persistent peer pubkey: %w", err)
}

addrs, err := deserializeAddresses(bytes.NewReader(v))
if err != nil {
return fmt.Errorf("unable to deserialize "+
"persistent peer addresses: %w", err)
}

peers = append(peers, &PersistentPeer{
PubKey: pubKey,
Addresses: addrs,
})
return nil
})
}, func() {
peers = nil
})
if err != nil {
return nil, err
}

return peers, nil
}

// serializeAddresses writes the given list of addresses to w using the
// graphdb address serialization helpers.
func serializeAddresses(w io.Writer, addrs []net.Addr) error {
var buf [4]byte
byteOrder.PutUint32(buf[:], uint32(len(addrs)))
if _, err := w.Write(buf[:]); err != nil {
return err
}

for _, addr := range addrs {
if err := graphdb.SerializeAddr(w, addr); err != nil {
return err
}
}

return nil
}

// deserializeAddresses reads a list of addresses from r in the format
// written by serializeAddresses.
func deserializeAddresses(r io.Reader) ([]net.Addr, error) {
var buf [4]byte
if _, err := io.ReadFull(r, buf[:]); err != nil {
return nil, err
}
numAddrs := byteOrder.Uint32(buf[:])

addrs := make([]net.Addr, 0, numAddrs)
for i := uint32(0); i < numAddrs; i++ {
addr, err := graphdb.DeserializeAddr(r)
if err != nil {
return nil, err
}
addrs = append(addrs, addr)
}

return addrs, nil
}
6 changes: 4 additions & 2 deletions chanrestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ func (s *server) ConnectPeer(nodePub *btcec.PublicKey, addrs []net.Addr) error {
// Before we connect to the remote peer, we'll remove any connections
// to ensure the new connection is created after this new link/channel
// is known.
if err := s.DisconnectPeer(nodePub); err != nil {
if err := s.DisconnectPeer(nodePub, false, false); err != nil {
ltndLog.Infof("Peer(%x) is already connected, proceeding "+
"with chan restore", nodePub.SerializeCompressed())
}
Expand All @@ -357,7 +357,9 @@ func (s *server) ConnectPeer(nodePub *btcec.PublicKey, addrs []net.Addr) error {
// Attempt to connect to the peer using this full address. If
// we're unable to connect to them, then we'll try the next
// address in place of it.
err := s.ConnectToPeer(netAddr, true, s.cfg.ConnectionTimeout)
_, err := s.ConnectToPeer(
netAddr, true, false, s.cfg.ConnectionTimeout,
)

// If we're already connected to this peer, then we don't
// consider this an error, so we'll exit here.
Expand Down
101 changes: 91 additions & 10 deletions cmd/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,20 +922,46 @@
the connection request in 30 seconds, use the following:

lncli connect <pubkey>@host --timeout 30s

If you are already connected to the peer:
- Without --perm, the request fails with an "already connected" error.
- With --perm on the same address, the peer is marked as persistent
without re-dialing.
- With --perm on a new address, lnd attempts to swap the active
connection to the new address. The behaviour depends on
--wait_for_dial:
* default (async): the new address is persisted immediately and
lnd dials it in the background. The swap happens if/when the
dial succeeds. If it never succeeds, the existing connection
remains and the conn manager keeps retrying.
* with --wait_for_dial: the RPC waits for the dial. The address
is persisted (and the connection swapped) only on success. On
failure the RPC returns an error, nothing is persisted, and
the existing connection is untouched.
`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "perm",
Usage: "If set, the daemon will attempt to persistently " +
"connect to the target peer.\n" +
" If not, the call will be synchronous.",
"connect to the target peer. If not, the call " +

Check failure on line 946 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)
"will be synchronous.",
},
cli.DurationFlag{
Name: "timeout",
Usage: "The connection timeout value for current request. " +
"Valid uints are {ms, s, m, h}.\n" +
"If not set, the global connection " +
"timeout value (default to 120s) is used.",
Usage: "The connection timeout value for current " +
"request. Valid uints are {ms, s, m, h}. " +
"If not set, the global connection timeout " +
"value (default to 120s) is used. Not " +
"applicable when --perm is used without " +
"--wait_for_dial, since the dial happens " +
"asynchronously in that case.",
},
cli.BoolFlag{
Name: "wait_for_dial",
Usage: "Only meaningful with --perm. Wait for the " +
"initial dial to complete and return an error " +

Check failure on line 962 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)
"if it fails. The address is only added to " +
"the persistent peer set on a successful dial.",
},
},
Action: actionDecorator(connectPeer),
Expand All @@ -958,9 +984,10 @@
Host: splitAddr[1],
}
req := &lnrpc.ConnectPeerRequest{
Addr: addr,
Perm: ctx.Bool("perm"),
Timeout: uint64(ctx.Duration("timeout").Seconds()),
Addr: addr,
Perm: ctx.Bool("perm"),
Timeout: uint64(ctx.Duration("timeout").Seconds()),
WaitForDial: ctx.Bool("wait_for_dial"),
}

lnid, err := client.ConnectPeer(ctxc, req)
Expand All @@ -978,12 +1005,52 @@
Usage: "Disconnect a remote lightning peer identified by " +
"public key.",
ArgsUsage: "<pubkey>",
Description: `
Disconnect a remote lightning peer identified by public key.

By default this only drops the active connection to the peer; the peer
remains in lnd's auto-reconnect set if it was previously marked
permanent via 'lncli connect --perm' or if there are open channels
with it.

With --forget, the peer is also removed from the persistent peer set,
so no automatic reconnect attempt will be made on the next lnd
restart. The --forget operation removes the entire persistent record
for the peer at once; there is no way to remove a single address while
keeping others.

With --force (only meaningful together with --forget), the
channel-peer record is also removed even when open channels still
exist with the peer.

Note: this command only drops the connection from our side. If the
remote peer has us in their persistent set (typical when an open
channel exists between the two nodes), they may re-dial us
immediately and the visible connection state will appear to "stay
connected" even though the disconnect succeeded locally. lnd cannot
prevent the other side from reconnecting.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "node_key",
Usage: "The hex-encoded compressed public key of the peer " +
"to disconnect from",
},
cli.BoolFlag{
Name: "forget",
Usage: "Also remove the peer from the persistent peer " +
"set so it will not be auto-reconnected to on " +
"the next lnd restart. Without this flag, " +
"disconnect only drops the current connection.",
},
cli.BoolFlag{
Name: "force",
Usage: "Only meaningful with '--forget'. By default, " +
"'--forget' will not completely forget a peer " +
"while one or more open channels exist with " +
"them. With '--force', the peer is forgotten " +
"regardless of open channels.",
},
},
Action: actionDecorator(disconnectPeer),
}
Expand All @@ -1005,6 +1072,8 @@

req := &lnrpc.DisconnectPeerRequest{
PubKey: pubKey,
Forget: ctx.Bool("forget"),
Force: ctx.Bool("force"),
}

lnid, err := client.DisconnectPeer(ctxc, req)
Expand Down Expand Up @@ -1633,12 +1702,21 @@
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 " +
"to but is not currently connected to — " +
"either made persistent via 'lncli connect " +
"--perm', or held persistent because of an " +
"open channel",
},
},
Action: actionDecorator(listPeers),
}
Expand All @@ -1652,6 +1730,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
Loading
Loading