diff --git a/config.go b/config.go index 657db7d..ff3737f 100644 --- a/config.go +++ b/config.go @@ -112,6 +112,11 @@ type closeChanPolicyCfg struct { MinWalletBalance float64 `long:"minwalletbalance" description:"The minimum wallet balance below which channels will start to be closed"` } +type ignorePolicyCfg struct { + Channels []string `long:"channels" description:"List of channels to ignore for management purposes"` + Nodes []string `long:"nodes" description:"List of nodes to ignore for management purposes"` +} + type config struct { ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` @@ -148,8 +153,9 @@ type config struct { LNNodeAddrs []string `long:"lnnodeaddr" description:"Public address of the underlying LN node in the P2P network"` // Policy Config. - OpenPolicy openChanPolicyCfg `group:"Open Channel Policy" namespace:"openpolicy"` - ClosePolicy closeChanPolicyCfg `group:"Close Channel Policy" namespace:"closepolicy"` + OpenPolicy openChanPolicyCfg `group:"Open Channel Policy" namespace:"openpolicy"` + ClosePolicy closeChanPolicyCfg `group:"Close Channel Policy" namespace:"closepolicy"` + IgnorePolicy ignorePolicyCfg `group:"Ignore Policy" namespace:"ignorepolicy"` // The rest of the members of this struct are filled by loadConfig(). @@ -216,6 +222,15 @@ func (c *config) serverConfig() (*server.Config, error) { createKey = []byte(c.OpenPolicy.Key) } + ignoreNodes := make(map[string]struct{}, len(c.IgnorePolicy.Nodes)) + for _, s := range c.IgnorePolicy.Nodes { + ignoreNodes[s] = struct{}{} + } + ignoreChannels := make(map[string]struct{}, len(c.IgnorePolicy.Channels)) + for _, s := range c.IgnorePolicy.Channels { + ignoreChannels[s] = struct{}{} + } + return &server.Config{ ChainParams: c.activeNet.chainParams(), LNRPCHost: c.LNRPCHost, @@ -235,6 +250,8 @@ func (c *config) serverConfig() (*server.Config, error) { CreateKey: createKey, CloseCheckInterval: c.ClosePolicy.CheckInterval, MinChanLifetime: c.ClosePolicy.MinChanLifetime, + IgnoreNodes: ignoreNodes, + IgnoreChannels: ignoreChannels, }, nil } diff --git a/dcrlnpd.conf b/dcrlnpd.conf index dd0eb68..69c71db 100644 --- a/dcrlnpd.conf +++ b/dcrlnpd.conf @@ -112,3 +112,21 @@ ; Minimum wallet balance, below which channels will begin to be closed. ; closepolicy.minwalletbalance = 1.0 + + +[Ignore Policy] + +; List of channel points to ignore for channel management purposes. Even if +; these are outbound, they will NOT be closed if they don't conform to the +; policy. +; +; ignorepolicy.channels = abcd...:1 +; ignorepolicy.channels = ef01...:3 + +; List of nodes to ignore for channel management purposes. All channels that +; were opened to one of these will be ignored. Note that this also makes LPD +; reject requests to open channels to these, because the goal of this setting +; is to mark these nodes as manually managed. +; +; ignorepolicy.nodes = abcd... +; ignorepolicy.nodes = ef01... diff --git a/server/server.go b/server/server.go index e6f07c3..b600cba 100644 --- a/server/server.go +++ b/server/server.go @@ -31,6 +31,7 @@ var ( errPeerUnconnected = fmt.Errorf("%w: peer not connected to LP node", errPolicy) errNotEnoughUtxos = fmt.Errorf("%w: not enough utxos to create channel", errPolicy) errTooManyPendingChans = fmt.Errorf("%w: node already has too many pending channels", errPolicy) + errIgnoredNode = fmt.Errorf("%w: node is in list of ignored nodes", errPolicy) ) // Config holds the server config. @@ -58,6 +59,8 @@ type Config struct { MinChanLifetime time.Duration CreateKey []byte InvoiceExpiration time.Duration + IgnoreChannels map[string]struct{} + IgnoreNodes map[string]struct{} // ChanInvoiceFeeRate is the fee rate to charge for creating a channel. // in atoms/channel-size-atoms. @@ -156,6 +159,12 @@ func (s *Server) canCreateChannel(ctx context.Context, wv waitingInvoice) error return errTooLargeChan } + // Check this node is not on the ignored list. Ignored nodes are + // manually managed. + if _, ok := s.cfg.IgnoreNodes[wv.TargetNode.String()]; ok { + return errIgnoredNode + } + // Verify if there are already opened channels to the target node. Note // that this does NOT differentiate between LP-initiated vs remote node // initiated channels. @@ -541,6 +550,31 @@ type ManagementInfo struct { Reclaimed dcrutil.Amount } +// isChannelIgnored returns true if the channel should be ignored for +// management purposes (c is not outbound or one of the channels configured to +// be ignored). +func (s *Server) isChannelIgnored(c *lnrpc.Channel) bool { + if !c.Initiator { + s.log.Debugf("Ignoring management of %s due to not outbound", + c.ChannelPoint) + return true + } + + if _, ok := s.cfg.IgnoreChannels[c.ChannelPoint]; ok { + s.log.Debugf("Ignoring management of %s due to ignored channel cfg", + c.ChannelPoint) + return true + } + + if _, ok := s.cfg.IgnoreNodes[c.RemotePubkey]; ok { + s.log.Debugf("Ignoring management of %s due to ignored node cfg", + c.ChannelPoint) + return true + } + + return false +} + // FetchManagedChannels returns the list of channels managed by the server. func (s *Server) FetchManagedChannels(ctx context.Context) (res ManagementInfo, err error) { // Fetch the current wallet balance and see if we actually need to @@ -584,8 +618,8 @@ func (s *Server) FetchManagedChannels(ctx context.Context) (res ManagementInfo, cid := lnwire.NewShortChanIDFromInt(c.ChanId) cp := c.ChannelPoint - // Ignore channels where we are not the initiator. - if !c.Initiator { + // Skip if this channel is ignored for management. + if s.isChannelIgnored(c) { res.UnmanagedChannels = append(res.UnmanagedChannels, UnmanagedChannel{ ChanPoint: cp, diff --git a/server/server_test.go b/server/server_test.go index 673af64..6f253a4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -20,6 +20,9 @@ func TestManageChannels(t *testing.T) { chanPoint0 := "0000000000000000000000000000000000000000000000000000000000000000:0" chanPoint1 := "1111111111111111111111111111111111111111111111111111111111111111:1" chanPoint2 := "2222222222222222222222222222222222222222222222222222222222222222:2" + chanPointIgnored := "3333333333333333333333333333333333333333333333333333333333333333:3" + ignoredRemoteId := "ignored-remote" + var nextCidTxIndex uint32 cidForLifetime := func(d time.Duration) uint64 { blocks := uint64(d / chainParams.TargetTimePerBlock) @@ -28,6 +31,7 @@ func TestManageChannels(t *testing.T) { nextCidTxIndex += 1 return cid.ToUint64() } + tests := []struct { name string wbal dcrutil.Amount @@ -143,6 +147,30 @@ func TestManageChannels(t *testing.T) { chanPoint0: {}, chanPoint2: {}, }, + }, { + name: "ignored channel is not closed", + wbal: minWalletBal - 1, + chans: []*lnrpc.Channel{{ + ChannelPoint: chanPointIgnored, + RemotePubkey: "remote", + ChanId: cidForLifetime(minLifetime + 1), + Capacity: int64(dcr), + LocalBalance: int64(dcr), + Initiator: true, + }}, + wantClosed: map[string]struct{}{}, + }, { + name: "channel from ignored node is not closed", + wbal: minWalletBal - 1, + chans: []*lnrpc.Channel{{ + ChannelPoint: chanPoint0, + RemotePubkey: ignoredRemoteId, + ChanId: cidForLifetime(minLifetime + 1), + Capacity: int64(dcr), + LocalBalance: int64(dcr), + Initiator: true, + }}, + wantClosed: map[string]struct{}{}, }} for _, tc := range tests { @@ -182,6 +210,8 @@ func TestManageChannels(t *testing.T) { ChainParams: chainParams, MinWalletBalance: minWalletBal, MinChanLifetime: minLifetime, + IgnoreChannels: map[string]struct{}{chanPointIgnored: {}}, + IgnoreNodes: map[string]struct{}{ignoredRemoteId: {}}, }, }