Merge pull request #7702 from ellemouton/towerClientMux

wtclient: Tower Client Multiplexer
This commit is contained in:
Elle authored and GitHub committed 2023-12-05 12:27:05 +02:00
commit 4fa483f1bc
14 files changed
+1265 -1015

No files matched your search

@@ -129,6 +129,10 @@
In particular, the complexity involved in the lifecycle loop has been In particular, the complexity involved in the lifecycle loop has been
decoupled into logical steps, with each step having its own responsibility, decoupled into logical steps, with each step having its own responsibility,
making it easier to reason about the payment flow. making it easier to reason about the payment flow.
* [Add a watchtower tower client
multiplexer](https://github.com/lightningnetwork/lnd/pull/7702) to manage
tower clients of different types.
## Breaking Changes ## Breaking Changes
## Performance Improvements ## Performance Improvements
+1 -1
View File
@@ -250,7 +250,7 @@ type TowerClient interface {
// parameters within the client. This should be called during link // parameters within the client. This should be called during link
// startup to ensure that the client is able to support the link during // startup to ensure that the client is able to support the link during
// operation. // operation.
RegisterChannel(lnwire.ChannelID) error RegisterChannel(lnwire.ChannelID, channeldb.ChannelType) error
// BackupState initiates a request to back up a particular revoked // BackupState initiates a request to back up a particular revoked
// state. If the method returns nil, the backup is guaranteed to be // state. If the method returns nil, the backup is guaranteed to be
+3 -1
View File
@@ -416,7 +416,9 @@ func (l *channelLink) Start() error {
// If the config supplied watchtower client, ensure the channel is // If the config supplied watchtower client, ensure the channel is
// registered before trying to use it during operation. // registered before trying to use it during operation.
if l.cfg.TowerClient != nil { if l.cfg.TowerClient != nil {
err := l.cfg.TowerClient.RegisterChannel(l.ChanID()) err := l.cfg.TowerClient.RegisterChannel(
l.ChanID(), l.channel.State().ChanType,
)
if err != nil { if err != nil {
return err return err
} }
+3 -7
View File
@@ -15,13 +15,9 @@ type Config struct {
// Active indicates if the watchtower client is enabled. // Active indicates if the watchtower client is enabled.
Active bool Active bool
// Client is the backing watchtower client that we'll interact with // ClientMgr is a tower client manager that manages a set of tower
// through the watchtower RPC subserver. // clients.
Client wtclient.Client ClientMgr wtclient.ClientManager
// AnchorClient is the backing watchtower client for anchor channels that
// we'll interact through the watchtower RPC subserver.
AnchorClient wtclient.Client
// Resolver is a custom resolver that will be used to resolve watchtower // Resolver is a custom resolver that will be used to resolve watchtower
// addresses to ensure we don't leak any information when running over // addresses to ensure we don't leak any information when running over
+68 -90
View File
@@ -16,9 +16,9 @@ import (
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/watchtower" "github.com/lightningnetwork/lnd/watchtower"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtclient" "github.com/lightningnetwork/lnd/watchtower/wtclient"
"github.com/lightningnetwork/lnd/watchtower/wtdb" "github.com/lightningnetwork/lnd/watchtower/wtdb"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
"google.golang.org/grpc" "google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery"
) )
@@ -208,11 +208,7 @@ func (c *WatchtowerClient) AddTower(ctx context.Context,
Address: addr, Address: addr,
} }
// TODO(conner): make atomic via multiplexed client if err := c.cfg.ClientMgr.AddTower(towerAddr); err != nil {
if err := c.cfg.Client.AddTower(towerAddr); err != nil {
return nil, err
}
if err := c.cfg.AnchorClient.AddTower(towerAddr); err != nil {
return nil, err return nil, err
} }
@@ -247,12 +243,7 @@ func (c *WatchtowerClient) RemoveTower(ctx context.Context,
} }
} }
// TODO(conner): make atomic via multiplexed client err = c.cfg.ClientMgr.RemoveTower(pubKey, addr)
err = c.cfg.Client.RemoveTower(pubKey, addr)
if err != nil {
return nil, err
}
err = c.cfg.AnchorClient.RemoveTower(pubKey, addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -272,23 +263,7 @@ func (c *WatchtowerClient) ListTowers(ctx context.Context,
req.IncludeSessions, req.ExcludeExhaustedSessions, req.IncludeSessions, req.ExcludeExhaustedSessions,
) )
anchorTowers, err := c.cfg.AnchorClient.RegisteredTowers(opts...) towersPerBlobType, err := c.cfg.ClientMgr.RegisteredTowers(opts...)
if err != nil {
return nil, err
}
// Collect all the anchor client towers.
rpcTowers := make(map[wtdb.TowerID]*Tower)
for _, tower := range anchorTowers {
rpcTower := marshallTower(
tower, PolicyType_ANCHOR, req.IncludeSessions,
ackCounts, committedUpdateCounts,
)
rpcTowers[tower.ID] = rpcTower
}
legacyTowers, err := c.cfg.Client.RegisteredTowers(opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -296,20 +271,32 @@ func (c *WatchtowerClient) ListTowers(ctx context.Context,
// Collect all the legacy client towers. If it has any of the same // Collect all the legacy client towers. If it has any of the same
// towers that the anchors client has, then just add the session info // towers that the anchors client has, then just add the session info
// for the legacy client to the existing tower. // for the legacy client to the existing tower.
for _, tower := range legacyTowers { rpcTowers := make(map[wtdb.TowerID]*Tower)
rpcTower := marshallTower( for blobType, towers := range towersPerBlobType {
tower, PolicyType_LEGACY, req.IncludeSessions, policyType := PolicyType_LEGACY
ackCounts, committedUpdateCounts, if blobType.IsAnchorChannel() {
) policyType = PolicyType_ANCHOR
t, ok := rpcTowers[tower.ID]
if !ok {
rpcTowers[tower.ID] = rpcTower
continue
} }
t.SessionInfo = append(t.SessionInfo, rpcTower.SessionInfo...) for _, tower := range towers {
t.Sessions = append(t.Sessions, rpcTower.Sessions...) rpcTower := marshallTower(
tower, policyType, req.IncludeSessions,
ackCounts, committedUpdateCounts,
)
t, ok := rpcTowers[tower.ID]
if !ok {
rpcTowers[tower.ID] = rpcTower
continue
}
t.SessionInfo = append(
t.SessionInfo, rpcTower.SessionInfo...,
)
t.Sessions = append(
t.Sessions, rpcTower.Sessions...,
)
}
} }
towers := make([]*Tower, 0, len(rpcTowers)) towers := make([]*Tower, 0, len(rpcTowers))
@@ -337,40 +324,42 @@ func (c *WatchtowerClient) GetTowerInfo(ctx context.Context,
req.IncludeSessions, req.ExcludeExhaustedSessions, req.IncludeSessions, req.ExcludeExhaustedSessions,
) )
// Get the tower and its sessions from anchors client. towersPerBlobType, err := c.cfg.ClientMgr.LookupTower(pubKey, opts...)
tower, err := c.cfg.AnchorClient.LookupTower(pubKey, opts...)
if err != nil {
return nil, err
}
rpcTower := marshallTower(
tower, PolicyType_ANCHOR, req.IncludeSessions, ackCounts,
committedUpdateCounts,
)
// Get the tower and its sessions from legacy client.
tower, err = c.cfg.Client.LookupTower(pubKey, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rpcLegacyTower := marshallTower( var resTower *Tower
tower, PolicyType_LEGACY, req.IncludeSessions, ackCounts, for blobType, tower := range towersPerBlobType {
committedUpdateCounts, policyType := PolicyType_LEGACY
) if blobType.IsAnchorChannel() {
policyType = PolicyType_ANCHOR
}
if !bytes.Equal(rpcTower.Pubkey, rpcLegacyTower.Pubkey) { rpcTower := marshallTower(
return nil, fmt.Errorf("legacy and anchor clients returned " + tower, policyType, req.IncludeSessions,
"inconsistent results for the given tower") ackCounts, committedUpdateCounts,
)
if resTower == nil {
resTower = rpcTower
continue
}
if !bytes.Equal(rpcTower.Pubkey, resTower.Pubkey) {
return nil, fmt.Errorf("tower clients returned " +
"inconsistent results for the given tower")
}
resTower.SessionInfo = append(
resTower.SessionInfo, rpcTower.SessionInfo...,
)
resTower.Sessions = append(
resTower.Sessions, rpcTower.Sessions...,
)
} }
rpcTower.SessionInfo = append( return resTower, nil
rpcTower.SessionInfo, rpcLegacyTower.SessionInfo...,
)
rpcTower.Sessions = append(
rpcTower.Sessions, rpcLegacyTower.Sessions...,
)
return rpcTower, nil
} }
// constructFunctionalOptions is a helper function that constructs a list of // constructFunctionalOptions is a helper function that constructs a list of
@@ -422,30 +411,14 @@ func constructFunctionalOptions(includeSessions,
} }
// Stats returns the in-memory statistics of the client since startup. // Stats returns the in-memory statistics of the client since startup.
func (c *WatchtowerClient) Stats(ctx context.Context, func (c *WatchtowerClient) Stats(_ context.Context,
req *StatsRequest) (*StatsResponse, error) { _ *StatsRequest) (*StatsResponse, error) {
if err := c.isActive(); err != nil { if err := c.isActive(); err != nil {
return nil, err return nil, err
} }
clientStats := []wtclient.ClientStats{ stats := c.cfg.ClientMgr.Stats()
c.cfg.Client.Stats(),
c.cfg.AnchorClient.Stats(),
}
var stats wtclient.ClientStats
for i := range clientStats {
// Grab a reference to the slice index rather than copying bc
// ClientStats contains a lock which cannot be copied by value.
stat := &clientStats[i]
stats.NumTasksAccepted += stat.NumTasksAccepted
stats.NumTasksIneligible += stat.NumTasksIneligible
stats.NumTasksPending += stat.NumTasksPending
stats.NumSessionsAcquired += stat.NumSessionsAcquired
stats.NumSessionsExhausted += stat.NumSessionsExhausted
}
return &StatsResponse{ return &StatsResponse{
NumBackups: uint32(stats.NumTasksAccepted), NumBackups: uint32(stats.NumTasksAccepted),
@@ -464,17 +437,22 @@ func (c *WatchtowerClient) Policy(ctx context.Context,
return nil, err return nil, err
} }
var policy wtpolicy.Policy var blobType blob.Type
switch req.PolicyType { switch req.PolicyType {
case PolicyType_LEGACY: case PolicyType_LEGACY:
policy = c.cfg.Client.Policy() blobType = blob.TypeAltruistCommit
case PolicyType_ANCHOR: case PolicyType_ANCHOR:
policy = c.cfg.AnchorClient.Policy() blobType = blob.TypeAltruistAnchorCommit
default: default:
return nil, fmt.Errorf("unknown policy type: %v", return nil, fmt.Errorf("unknown policy type: %v",
req.PolicyType) req.PolicyType)
} }
policy, err := c.cfg.ClientMgr.Policy(blobType)
if err != nil {
return nil, err
}
return &PolicyResponse{ return &PolicyResponse{
MaxUpdates: uint32(policy.MaxUpdates), MaxUpdates: uint32(policy.MaxUpdates),
SweepSatPerVbyte: uint32(policy.SweepFeeRate.FeePerVByte()), SweepSatPerVbyte: uint32(policy.SweepFeeRate.FeePerVByte()),
+5 -17
View File
@@ -268,12 +268,8 @@ type Config struct {
// HtlcNotifier is used when creating a ChannelLink. // HtlcNotifier is used when creating a ChannelLink.
HtlcNotifier *htlcswitch.HtlcNotifier HtlcNotifier *htlcswitch.HtlcNotifier
// TowerClient is used by legacy channels to backup revoked states. // TowerClient is used to backup revoked states.
TowerClient wtclient.Client TowerClient wtclient.ClientManager
// AnchorTowerClient is used by anchor channels to backup revoked
// states.
AnchorTowerClient wtclient.Client
// DisconnectPeer is used to disconnect this peer if the cooperative close // DisconnectPeer is used to disconnect this peer if the cooperative close
// process fails. // process fails.
@@ -1040,14 +1036,8 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
return p.cfg.ChainArb.NotifyContractUpdate(*chanPoint, update) return p.cfg.ChainArb.NotifyContractUpdate(*chanPoint, update)
} }
chanType := lnChan.State().ChanType var towerClient wtclient.ClientManager
if lnChan.ChanType().IsTaproot() {
// Select the appropriate tower client based on the channel type. It's
// okay if the clients are disabled altogether and these values are nil,
// as the link will check for nilness before using either.
var towerClient htlcswitch.TowerClient
switch {
case chanType.IsTaproot():
// Leave the tower client as nil for now until the tower client // Leave the tower client as nil for now until the tower client
// has support for taproot channels. // has support for taproot channels.
// //
@@ -1060,9 +1050,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
"are not yet taproot channel compatible", "are not yet taproot channel compatible",
chanPoint) chanPoint)
} }
case chanType.HasAnchors(): } else {
towerClient = p.cfg.AnchorTowerClient
default:
towerClient = p.cfg.TowerClient towerClient = p.cfg.TowerClient
} }
+4 -5
View File
@@ -744,11 +744,10 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
r.cfg, s.cc, r.cfg.networkDir, macService, atpl, invoiceRegistry, r.cfg, s.cc, r.cfg.networkDir, macService, atpl, invoiceRegistry,
s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter, s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter,
routerBackend, s.nodeSigner, s.graphDB, s.chanStateDB, routerBackend, s.nodeSigner, s.graphDB, s.chanStateDB,
s.sweeper, tower, s.towerClient, s.anchorTowerClient, s.sweeper, tower, s.towerClientMgr, r.cfg.net.ResolveTCPAddr,
r.cfg.net.ResolveTCPAddr, genInvoiceFeatures, genInvoiceFeatures, genAmpInvoiceFeatures,
genAmpInvoiceFeatures, s.getNodeAnnouncement, s.getNodeAnnouncement, s.updateAndBrodcastSelfNode, parseAddr,
s.updateAndBrodcastSelfNode, parseAddr, rpcsLog, rpcsLog, s.aliasMgr.GetPeerAlias,
s.aliasMgr.GetPeerAlias,
) )
if err != nil { if err != nil {
return err return err
+22 -56
View File
@@ -282,9 +282,7 @@ type server struct {
sphinx *hop.OnionProcessor sphinx *hop.OnionProcessor
towerClient wtclient.Client towerClientMgr *wtclient.Manager
anchorTowerClient wtclient.Client
connMgr *connmgr.ConnManager connMgr *connmgr.ConnManager
@@ -1548,40 +1546,12 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
fetchClosedChannel := s.chanStateDB.FetchClosedChannelForID fetchClosedChannel := s.chanStateDB.FetchClosedChannelForID
s.towerClient, err = wtclient.New(&wtclient.Config{
FetchClosedChannel: fetchClosedChannel,
BuildBreachRetribution: buildBreachRetribution,
SessionCloseRange: cfg.WtClient.SessionCloseRange,
ChainNotifier: s.cc.ChainNotifier,
SubscribeChannelEvents: func() (subscribe.Subscription,
error) {
return s.channelNotifier.
SubscribeChannelEvents()
},
Signer: cc.Wallet.Cfg.Signer,
NewAddress: newSweepPkScriptGen(cc.Wallet),
SecretKeyRing: s.cc.KeyRing,
Dial: cfg.net.Dial,
AuthDial: authDial,
DB: dbs.TowerClientDB,
Policy: policy,
ChainHash: *s.cfg.ActiveNetParams.GenesisHash,
MinBackoff: 10 * time.Second,
MaxBackoff: 5 * time.Minute,
MaxTasksInMemQueue: cfg.WtClient.MaxTasksInMemQueue,
})
if err != nil {
return nil, err
}
// Copy the policy for legacy channels and set the blob flag // Copy the policy for legacy channels and set the blob flag
// signalling support for anchor channels. // signalling support for anchor channels.
anchorPolicy := policy anchorPolicy := policy
anchorPolicy.TxPolicy.BlobType |= anchorPolicy.BlobType |= blob.Type(blob.FlagAnchorChannel)
blob.Type(blob.FlagAnchorChannel)
s.anchorTowerClient, err = wtclient.New(&wtclient.Config{ s.towerClientMgr, err = wtclient.NewManager(&wtclient.Config{
FetchClosedChannel: fetchClosedChannel, FetchClosedChannel: fetchClosedChannel,
BuildBreachRetribution: buildBreachRetribution, BuildBreachRetribution: buildBreachRetribution,
SessionCloseRange: cfg.WtClient.SessionCloseRange, SessionCloseRange: cfg.WtClient.SessionCloseRange,
@@ -1598,12 +1568,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
Dial: cfg.net.Dial, Dial: cfg.net.Dial,
AuthDial: authDial, AuthDial: authDial,
DB: dbs.TowerClientDB, DB: dbs.TowerClientDB,
Policy: anchorPolicy,
ChainHash: *s.cfg.ActiveNetParams.GenesisHash, ChainHash: *s.cfg.ActiveNetParams.GenesisHash,
MinBackoff: 10 * time.Second, MinBackoff: 10 * time.Second,
MaxBackoff: 5 * time.Minute, MaxBackoff: 5 * time.Minute,
MaxTasksInMemQueue: cfg.WtClient.MaxTasksInMemQueue, MaxTasksInMemQueue: cfg.WtClient.MaxTasksInMemQueue,
}) }, policy, anchorPolicy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1925,19 +1894,12 @@ func (s *server) Start() error {
} }
cleanup = cleanup.add(s.htlcNotifier.Stop) cleanup = cleanup.add(s.htlcNotifier.Stop)
if s.towerClient != nil { if s.towerClientMgr != nil {
if err := s.towerClient.Start(); err != nil { if err := s.towerClientMgr.Start(); err != nil {
startErr = err startErr = err
return return
} }
cleanup = cleanup.add(s.towerClient.Stop) cleanup = cleanup.add(s.towerClientMgr.Stop)
}
if s.anchorTowerClient != nil {
if err := s.anchorTowerClient.Start(); err != nil {
startErr = err
return
}
cleanup = cleanup.add(s.anchorTowerClient.Stop)
} }
if err := s.sweeper.Start(); err != nil { if err := s.sweeper.Start(); err != nil {
@@ -2310,16 +2272,10 @@ func (s *server) Stop() error {
// client which will reliably flush all queued states to the // client which will reliably flush all queued states to the
// tower. If this is halted for any reason, the force quit timer // tower. If this is halted for any reason, the force quit timer
// will kick in and abort to allow this method to return. // will kick in and abort to allow this method to return.
if s.towerClient != nil { if s.towerClientMgr != nil {
if err := s.towerClient.Stop(); err != nil { if err := s.towerClientMgr.Stop(); err != nil {
srvrLog.Warnf("Unable to shut down tower "+ srvrLog.Warnf("Unable to shut down tower "+
"client: %v", err) "client manager: %v", err)
}
}
if s.anchorTowerClient != nil {
if err := s.anchorTowerClient.Stop(); err != nil {
srvrLog.Warnf("Unable to shut down anchor "+
"tower client: %v", err)
} }
} }
@@ -3807,6 +3763,17 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
} }
} }
// If we directly set the peer.Config TowerClient member to the
// s.towerClientMgr then in the case that the s.towerClientMgr is nil,
// the peer.Config's TowerClient member will not evaluate to nil even
// though the underlying value is nil. To avoid this gotcha which can
// cause a panic, we need to explicitly pass nil to the peer.Config's
// TowerClient if needed.
var towerClient wtclient.ClientManager
if s.towerClientMgr != nil {
towerClient = s.towerClientMgr
}
// Now that we've established a connection, create a peer, and it to the // Now that we've established a connection, create a peer, and it to the
// set of currently active peers. Configure the peer with the incoming // set of currently active peers. Configure the peer with the incoming
// and outgoing broadcast deltas to prevent htlcs from being accepted or // and outgoing broadcast deltas to prevent htlcs from being accepted or
@@ -3845,8 +3812,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
Invoices: s.invoices, Invoices: s.invoices,
ChannelNotifier: s.channelNotifier, ChannelNotifier: s.channelNotifier,
HtlcNotifier: s.htlcNotifier, HtlcNotifier: s.htlcNotifier,
TowerClient: s.towerClient, TowerClient: towerClient,
AnchorTowerClient: s.anchorTowerClient,
DisconnectPeer: s.DisconnectPeer, DisconnectPeer: s.DisconnectPeer,
GenNodeAnnouncement: func(...netann.NodeAnnModifier) ( GenNodeAnnouncement: func(...netann.NodeAnnModifier) (
lnwire.NodeAnnouncement, error) { lnwire.NodeAnnouncement, error) {
+5 -9
View File
@@ -113,8 +113,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
chanStateDB *channeldb.ChannelStateDB, chanStateDB *channeldb.ChannelStateDB,
sweeper *sweep.UtxoSweeper, sweeper *sweep.UtxoSweeper,
tower *watchtower.Standalone, tower *watchtower.Standalone,
towerClient wtclient.Client, towerClientMgr wtclient.ClientManager,
anchorTowerClient wtclient.Client,
tcpResolver lncfg.TCPResolver, tcpResolver lncfg.TCPResolver,
genInvoiceFeatures func() *lnwire.FeatureVector, genInvoiceFeatures func() *lnwire.FeatureVector,
genAmpInvoiceFeatures func() *lnwire.FeatureVector, genAmpInvoiceFeatures func() *lnwire.FeatureVector,
@@ -287,15 +286,12 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
case *wtclientrpc.Config: case *wtclientrpc.Config:
subCfgValue := extractReflectValue(subCfg) subCfgValue := extractReflectValue(subCfg)
if towerClient != nil && anchorTowerClient != nil { if towerClientMgr != nil {
subCfgValue.FieldByName("Active").Set( subCfgValue.FieldByName("Active").Set(
reflect.ValueOf(towerClient != nil), reflect.ValueOf(towerClientMgr != nil),
) )
subCfgValue.FieldByName("Client").Set( subCfgValue.FieldByName("ClientMgr").Set(
reflect.ValueOf(towerClient), reflect.ValueOf(towerClientMgr),
)
subCfgValue.FieldByName("AnchorClient").Set(
reflect.ValueOf(anchorTowerClient),
) )
} }
subCfgValue.FieldByName("Resolver").Set( subCfgValue.FieldByName("Resolver").Set(
File diff suppressed because it is too large. Load diff
+51 -50
View File
@@ -395,15 +395,16 @@ func (c *mockChannel) getState(
} }
type testHarness struct { type testHarness struct {
t *testing.T t *testing.T
cfg harnessCfg cfg harnessCfg
signer *wtmock.MockSigner signer *wtmock.MockSigner
capacity lnwire.MilliSatoshi capacity lnwire.MilliSatoshi
clientDB *wtdb.ClientDB clientMgr *wtclient.Manager
clientCfg *wtclient.Config clientDB *wtdb.ClientDB
client wtclient.Client clientCfg *wtclient.Config
server *serverHarness clientPolicy wtpolicy.Policy
net *mockNet server *serverHarness
net *mockNet
blockEvents *mockBlockSub blockEvents *mockBlockSub
height int32 height int32
@@ -486,6 +487,7 @@ func newHarness(t *testing.T, cfg harnessCfg) *testHarness {
return &channeldb.ChannelCloseSummary{CloseHeight: height}, nil return &channeldb.ChannelCloseSummary{CloseHeight: height}, nil
} }
h.clientPolicy = cfg.policy
h.clientCfg = &wtclient.Config{ h.clientCfg = &wtclient.Config{
Signer: signer, Signer: signer,
SubscribeChannelEvents: func() (subscribe.Subscription, error) { SubscribeChannelEvents: func() (subscribe.Subscription, error) {
@@ -497,7 +499,6 @@ func newHarness(t *testing.T, cfg harnessCfg) *testHarness {
DB: clientDB, DB: clientDB,
AuthDial: mockNet.AuthDial, AuthDial: mockNet.AuthDial,
SecretKeyRing: wtmock.NewSecretKeyRing(), SecretKeyRing: wtmock.NewSecretKeyRing(),
Policy: cfg.policy,
NewAddress: func() ([]byte, error) { NewAddress: func() ([]byte, error) {
return addrScript, nil return addrScript, nil
}, },
@@ -525,7 +526,7 @@ func newHarness(t *testing.T, cfg harnessCfg) *testHarness {
h.startClient() h.startClient()
t.Cleanup(func() { t.Cleanup(func() {
require.NoError(t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
require.NoError(t, h.clientDB.Close()) require.NoError(t, h.clientDB.Close())
}) })
@@ -559,10 +560,10 @@ func (h *testHarness) startClient() {
Address: towerTCPAddr, Address: towerTCPAddr,
} }
h.client, err = wtclient.New(h.clientCfg) h.clientMgr, err = wtclient.NewManager(h.clientCfg, h.clientPolicy)
require.NoError(h.t, err) require.NoError(h.t, err)
require.NoError(h.t, h.client.Start()) require.NoError(h.t, h.clientMgr.Start())
require.NoError(h.t, h.client.AddTower(towerAddr)) require.NoError(h.t, h.clientMgr.AddTower(towerAddr))
} }
// chanIDFromInt creates a unique channel id given a unique integral id. // chanIDFromInt creates a unique channel id given a unique integral id.
@@ -663,7 +664,7 @@ func (h *testHarness) registerChannel(id uint64) {
h.t.Helper() h.t.Helper()
chanID := chanIDFromInt(id) chanID := chanIDFromInt(id)
err := h.client.RegisterChannel(chanID) err := h.clientMgr.RegisterChannel(chanID, channeldb.SingleFunderBit)
require.NoError(h.t, err) require.NoError(h.t, err)
} }
@@ -704,8 +705,8 @@ func (h *testHarness) backupState(id, i uint64, expErr error) {
chanID := chanIDFromInt(id) chanID := chanIDFromInt(id)
err := h.client.BackupState(&chanID, retribution.RevokedStateNum) err := h.clientMgr.BackupState(&chanID, retribution.RevokedStateNum)
require.ErrorIs(h.t, expErr, err) require.ErrorIs(h.t, err, expErr)
} }
// sendPayments instructs the channel identified by id to send amt to the remote // sendPayments instructs the channel identified by id to send amt to the remote
@@ -752,7 +753,7 @@ func (h *testHarness) recvPayments(id, from, to uint64,
func (h *testHarness) addTower(addr *lnwire.NetAddress) { func (h *testHarness) addTower(addr *lnwire.NetAddress) {
h.t.Helper() h.t.Helper()
err := h.client.AddTower(addr) err := h.clientMgr.AddTower(addr)
require.NoError(h.t, err) require.NoError(h.t, err)
} }
@@ -761,7 +762,7 @@ func (h *testHarness) addTower(addr *lnwire.NetAddress) {
func (h *testHarness) removeTower(pubKey *btcec.PublicKey, addr net.Addr) { func (h *testHarness) removeTower(pubKey *btcec.PublicKey, addr net.Addr) {
h.t.Helper() h.t.Helper()
err := h.client.RemoveTower(pubKey, addr) err := h.clientMgr.RemoveTower(pubKey, addr)
require.NoError(h.t, err) require.NoError(h.t, err)
} }
@@ -1123,7 +1124,7 @@ var clientTests = []clientTest{
) )
// Stop the client, subsequent backups should fail. // Stop the client, subsequent backups should fail.
h.client.Stop() require.NoError(h.t, h.clientMgr.Stop())
// Advance the channel and try to back up the states. We // Advance the channel and try to back up the states. We
// expect ErrClientExiting to be returned from // expect ErrClientExiting to be returned from
@@ -1238,7 +1239,7 @@ var clientTests = []clientTest{
// Stop the client to abort the state updates it has // Stop the client to abort the state updates it has
// queued. // queued.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
// Restart the server and allow it to ack the updates // Restart the server and allow it to ack the updates
// after the client retransmits the unacked update. // after the client retransmits the unacked update.
@@ -1249,6 +1250,7 @@ var clientTests = []clientTest{
// Restart the client and allow it to process the // Restart the client and allow it to process the
// committed update. // committed update.
h.startClient() h.startClient()
h.registerChannel(chanID)
// Wait for the committed update to be accepted by the // Wait for the committed update to be accepted by the
// tower. // tower.
@@ -1433,7 +1435,7 @@ var clientTests = []clientTest{
h.server.waitForUpdates(nil, waitTime) h.server.waitForUpdates(nil, waitTime)
// Stop the client since it has queued backups. // Stop the client since it has queued backups.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
// Restart the server and allow it to ack session // Restart the server and allow it to ack session
// creation. // creation.
@@ -1452,9 +1454,7 @@ var clientTests = []clientTest{
// Assert that the server has updates for the clients // Assert that the server has updates for the clients
// most recent policy. // most recent policy.
h.server.assertUpdatesForPolicy( h.server.assertUpdatesForPolicy(hints, h.clientPolicy)
hints, h.clientCfg.Policy,
)
}, },
}, },
{ {
@@ -1485,7 +1485,7 @@ var clientTests = []clientTest{
h.server.waitForUpdates(nil, waitTime) h.server.waitForUpdates(nil, waitTime)
// Stop the client since it has queued backups. // Stop the client since it has queued backups.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
// Restart the server and allow it to ack session // Restart the server and allow it to ack session
// creation. // creation.
@@ -1496,7 +1496,7 @@ var clientTests = []clientTest{
// Restart the client with a new policy, which will // Restart the client with a new policy, which will
// immediately try to overwrite the prior session with // immediately try to overwrite the prior session with
// the old policy. // the old policy.
h.clientCfg.Policy.SweepFeeRate *= 2 h.clientPolicy.SweepFeeRate *= 2
h.startClient() h.startClient()
// Wait for all the updates to be populated in the // Wait for all the updates to be populated in the
@@ -1505,9 +1505,7 @@ var clientTests = []clientTest{
// Assert that the server has updates for the clients // Assert that the server has updates for the clients
// most recent policy. // most recent policy.
h.server.assertUpdatesForPolicy( h.server.assertUpdatesForPolicy(hints, h.clientPolicy)
hints, h.clientCfg.Policy,
)
}, },
}, },
{ {
@@ -1541,7 +1539,7 @@ var clientTests = []clientTest{
h.server.waitForUpdates(hints[:numUpdates/2], waitTime) h.server.waitForUpdates(hints[:numUpdates/2], waitTime)
// Stop the client, which should have no more backups. // Stop the client, which should have no more backups.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
// Record the policy that the first half was stored // Record the policy that the first half was stored
// under. We'll expect the second half to also be // under. We'll expect the second half to also be
@@ -1549,11 +1547,12 @@ var clientTests = []clientTest{
// adjusting the MaxUpdates. The client should detect // adjusting the MaxUpdates. The client should detect
// that the two policies have equivalent TxPolicies and // that the two policies have equivalent TxPolicies and
// continue using the first. // continue using the first.
expPolicy := h.clientCfg.Policy expPolicy := h.clientPolicy
// Restart the client with a new policy. // Restart the client with a new policy.
h.clientCfg.Policy.MaxUpdates = 20 h.clientPolicy.MaxUpdates = 20
h.startClient() h.startClient()
h.registerChannel(chanID)
// Now, queue the second half of the retributions. // Now, queue the second half of the retributions.
h.backupStates(chanID, numUpdates/2, numUpdates, nil) h.backupStates(chanID, numUpdates/2, numUpdates, nil)
@@ -1602,8 +1601,9 @@ var clientTests = []clientTest{
// Restart the client, so we can ensure the deduping is // Restart the client, so we can ensure the deduping is
// maintained across restarts. // maintained across restarts.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
h.startClient() h.startClient()
h.registerChannel(chanID)
// Try to back up the full range of retributions. Only // Try to back up the full range of retributions. Only
// the second half should actually be sent. // the second half should actually be sent.
@@ -1713,11 +1713,11 @@ var clientTests = []clientTest{
h.server.addr = towerAddr h.server.addr = towerAddr
// Add the new tower address to the client. // Add the new tower address to the client.
err = h.client.AddTower(towerAddr) err = h.clientMgr.AddTower(towerAddr)
require.NoError(h.t, err) require.NoError(h.t, err)
// Remove the old tower address from the client. // Remove the old tower address from the client.
err = h.client.RemoveTower( err = h.clientMgr.RemoveTower(
towerAddr.IdentityKey, oldAddr, towerAddr.IdentityKey, oldAddr,
) )
require.NoError(h.t, err) require.NoError(h.t, err)
@@ -1751,7 +1751,7 @@ var clientTests = []clientTest{
// negotiation with the server will be in progress, so // negotiation with the server will be in progress, so
// the client should be able to remove the server. // the client should be able to remove the server.
err := wait.NoError(func() error { err := wait.NoError(func() error {
return h.client.RemoveTower( return h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, nil, h.server.addr.IdentityKey, nil,
) )
}, waitTime) }, waitTime)
@@ -1794,11 +1794,11 @@ var clientTests = []clientTest{
require.NoError(h.t, h.server.server.Start()) require.NoError(h.t, h.server.server.Start())
// Re-add the server to the client // Re-add the server to the client
err = h.client.AddTower(h.server.addr) err = h.clientMgr.AddTower(h.server.addr)
require.NoError(h.t, err) require.NoError(h.t, err)
// Also add the new tower address. // Also add the new tower address.
err = h.client.AddTower(towerAddr) err = h.clientMgr.AddTower(towerAddr)
require.NoError(h.t, err) require.NoError(h.t, err)
// Assert that if the client attempts to remove the // Assert that if the client attempts to remove the
@@ -1806,7 +1806,7 @@ var clientTests = []clientTest{
// address currently being locked for session // address currently being locked for session
// negotiation. // negotiation.
err = wait.Predicate(func() bool { err = wait.Predicate(func() bool {
err = h.client.RemoveTower( err = h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, h.server.addr.IdentityKey,
h.server.addr.Address, h.server.addr.Address,
) )
@@ -1817,7 +1817,7 @@ var clientTests = []clientTest{
// Assert that the second address can be removed since // Assert that the second address can be removed since
// it is not being used for session negotiation. // it is not being used for session negotiation.
err = wait.NoError(func() error { err = wait.NoError(func() error {
return h.client.RemoveTower( return h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, towerTCPAddr, h.server.addr.IdentityKey, towerTCPAddr,
) )
}, waitTime) }, waitTime)
@@ -1829,7 +1829,7 @@ var clientTests = []clientTest{
// Assert that the client can now remove the first // Assert that the client can now remove the first
// address. // address.
err = wait.NoError(func() error { err = wait.NoError(func() error {
return h.client.RemoveTower( return h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, nil, h.server.addr.IdentityKey, nil,
) )
}, waitTime) }, waitTime)
@@ -1882,7 +1882,7 @@ var clientTests = []clientTest{
require.False(h.t, h.isSessionClosable(sessionIDs[0])) require.False(h.t, h.isSessionClosable(sessionIDs[0]))
// Restart the client. // Restart the client.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
h.startClient() h.startClient()
// The session should now have been marked as closable. // The session should now have been marked as closable.
@@ -2069,7 +2069,7 @@ var clientTests = []clientTest{
h.server.waitForUpdates(hints[:numUpdates/2], waitTime) h.server.waitForUpdates(hints[:numUpdates/2], waitTime)
// Now stop the client and reset its database. // Now stop the client and reset its database.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
db := newClientDB(h.t) db := newClientDB(h.t)
h.clientDB = db h.clientDB = db
@@ -2122,9 +2122,10 @@ var clientTests = []clientTest{
h.backupStates(chanID, 0, numUpdates/2, nil) h.backupStates(chanID, 0, numUpdates/2, nil)
// Restart the Client. And also now start the server. // Restart the Client. And also now start the server.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
h.server.start() h.server.start()
h.startClient() h.startClient()
h.registerChannel(chanID)
// Back up a few more states. // Back up a few more states.
h.backupStates(chanID, numUpdates/2, numUpdates, nil) h.backupStates(chanID, numUpdates/2, numUpdates, nil)
@@ -2222,7 +2223,7 @@ var clientTests = []clientTest{
// Now we can remove the old one. // Now we can remove the old one.
err := wait.Predicate(func() bool { err := wait.Predicate(func() bool {
err := h.client.RemoveTower( err := h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, nil, h.server.addr.IdentityKey, nil,
) )
@@ -2308,7 +2309,7 @@ var clientTests = []clientTest{
require.NoError(h.t, err) require.NoError(h.t, err)
// Now remove the tower. // Now remove the tower.
err = h.client.RemoveTower( err = h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, nil, h.server.addr.IdentityKey, nil,
) )
require.NoError(h.t, err) require.NoError(h.t, err)
@@ -2395,11 +2396,11 @@ var clientTests = []clientTest{
// Now restart the client. This ensures that the // Now restart the client. This ensures that the
// updates are no longer in the pending queue. // updates are no longer in the pending queue.
require.NoError(h.t, h.client.Stop()) require.NoError(h.t, h.clientMgr.Stop())
h.startClient() h.startClient()
// Now remove the tower. // Now remove the tower.
err = h.client.RemoveTower( err = h.clientMgr.RemoveTower(
h.server.addr.IdentityKey, nil, h.server.addr.IdentityKey, nil,
) )
require.NoError(h.t, err) require.NoError(h.t, err)
@@ -2519,7 +2520,7 @@ var clientTests = []clientTest{
// Wait for channel to be "unregistered". // Wait for channel to be "unregistered".
chanID := chanIDFromInt(chanIDInt) chanID := chanIDFromInt(chanIDInt)
err = wait.Predicate(func() bool { err = wait.Predicate(func() bool {
err := h.client.BackupState(&chanID, 0) err := h.clientMgr.BackupState(&chanID, 0)
return errors.Is( return errors.Is(
err, wtclient.ErrUnregisteredChannel, err, wtclient.ErrUnregisteredChannel,
+902
View File
@@ -0,0 +1,902 @@
package wtclient
import (
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channelnotifier"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/subscribe"
"github.com/lightningnetwork/lnd/tor"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtdb"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
)
// ClientManager is the primary interface used by the daemon to control a
// client's lifecycle and backup revoked states.
type ClientManager interface {
// AddTower adds a new watchtower reachable at the given address and
// considers it for new sessions. If the watchtower already exists, then
// any new addresses included will be considered when dialing it for
// session negotiations and backups.
AddTower(*lnwire.NetAddress) error
// RemoveTower removes a watchtower from being considered for future
// session negotiations and from being used for any subsequent backups
// until it's added again. If an address is provided, then this call
// only serves as a way of removing the address from the watchtower
// instead.
RemoveTower(*btcec.PublicKey, net.Addr) error
// Stats returns the in-memory statistics of the client since startup.
Stats() ClientStats
// Policy returns the active client policy configuration.
Policy(blob.Type) (wtpolicy.Policy, error)
// RegisteredTowers retrieves the list of watchtowers registered with
// the client. It returns a set of registered towers per client policy
// type.
RegisteredTowers(opts ...wtdb.ClientSessionListOption) (
map[blob.Type][]*RegisteredTower, error)
// LookupTower retrieves a registered watchtower through its public key.
LookupTower(*btcec.PublicKey, ...wtdb.ClientSessionListOption) (
map[blob.Type]*RegisteredTower, error)
// RegisterChannel persistently initializes any channel-dependent
// parameters within the client. This should be called during link
// startup to ensure that the client is able to support the link during
// operation.
RegisterChannel(lnwire.ChannelID, channeldb.ChannelType) error
// BackupState initiates a request to back up a particular revoked
// state. If the method returns nil, the backup is guaranteed to be
// successful unless the justice transaction would create dust outputs
// when trying to abide by the negotiated policy.
BackupState(chanID *lnwire.ChannelID, stateNum uint64) error
}
// Config provides the client with access to the resources it requires to
// perform its duty. All nillable fields must be non-nil for the tower to be
// initialized properly.
type Config struct {
// Signer provides access to the wallet so that the client can sign
// justice transactions that spend from a remote party's commitment
// transaction.
Signer input.Signer
// SubscribeChannelEvents can be used to subscribe to channel event
// notifications.
SubscribeChannelEvents func() (subscribe.Subscription, error)
// FetchClosedChannel can be used to fetch the info about a closed
// channel. If the channel is not found or not yet closed then
// channeldb.ErrClosedChannelNotFound will be returned.
FetchClosedChannel func(cid lnwire.ChannelID) (
*channeldb.ChannelCloseSummary, error)
// ChainNotifier can be used to subscribe to block notifications.
ChainNotifier chainntnfs.ChainNotifier
// BuildBreachRetribution is a function closure that allows the client
// fetch the breach retribution info for a certain channel at a certain
// revoked commitment height.
BuildBreachRetribution BreachRetributionBuilder
// NewAddress generates a new on-chain sweep pkscript.
NewAddress func() ([]byte, error)
// SecretKeyRing is used to derive the session keys used to communicate
// with the tower. The client only stores the KeyLocators internally so
// that we never store private keys on disk.
SecretKeyRing ECDHKeyRing
// Dial connects to an addr using the specified net and returns the
// connection object.
Dial tor.DialFunc
// AuthDialer establishes a brontide connection over an onion or clear
// network.
AuthDial AuthDialer
// DB provides access to the client's stable storage medium.
DB DB
// ChainHash identifies the chain that the client is on and for which
// the tower must be watching to monitor for breaches.
ChainHash chainhash.Hash
// ReadTimeout is the duration we will wait during a read before
// breaking out of a blocking read. If the value is less than or equal
// to zero, the default will be used instead.
ReadTimeout time.Duration
// WriteTimeout is the duration we will wait during a write before
// breaking out of a blocking write. If the value is less than or equal
// to zero, the default will be used instead.
WriteTimeout time.Duration
// MinBackoff defines the initial backoff applied to connections with
// watchtowers. Subsequent backoff durations will grow exponentially up
// until MaxBackoff.
MinBackoff time.Duration
// MaxBackoff defines the maximum backoff applied to connections with
// watchtowers. If the exponential backoff produces a timeout greater
// than this value, the backoff will be clamped to MaxBackoff.
MaxBackoff time.Duration
// SessionCloseRange is the range over which we will generate a random
// number of blocks to delay closing a session after its last channel
// has been closed.
SessionCloseRange uint32
// MaxTasksInMemQueue is the maximum number of backup tasks that should
// be kept in-memory. Any more tasks will overflow to disk.
MaxTasksInMemQueue uint64
}
// Manager manages the various tower clients that are active. A client is
// required for each different commitment transaction type. The Manager acts as
// a tower client multiplexer.
type Manager struct {
started sync.Once
stopped sync.Once
cfg *Config
clients map[blob.Type]*client
clientsMu sync.Mutex
backupMu sync.Mutex
chanInfos wtdb.ChannelInfos
chanBlobType map[lnwire.ChannelID]blob.Type
closableSessionQueue *sessionCloseMinHeap
wg sync.WaitGroup
quit chan struct{}
}
var _ ClientManager = (*Manager)(nil)
// NewManager constructs a new Manager.
func NewManager(config *Config, policies ...wtpolicy.Policy) (*Manager, error) {
// Copy the config to prevent side effects from modifying both the
// internal and external version of the Config.
cfg := *config
// Set the read timeout to the default if none was provided.
if cfg.ReadTimeout <= 0 {
cfg.ReadTimeout = DefaultReadTimeout
}
// Set the write timeout to the default if none was provided.
if cfg.WriteTimeout <= 0 {
cfg.WriteTimeout = DefaultWriteTimeout
}
chanInfos, err := cfg.DB.FetchChanInfos()
if err != nil {
return nil, err
}
m := &Manager{
cfg: &cfg,
clients: make(map[blob.Type]*client),
chanBlobType: make(map[lnwire.ChannelID]blob.Type),
chanInfos: chanInfos,
closableSessionQueue: newSessionCloseMinHeap(),
quit: make(chan struct{}),
}
for _, policy := range policies {
if err = policy.Validate(); err != nil {
return nil, err
}
if err = m.newClient(policy); err != nil {
return nil, err
}
}
return m, nil
}
// newClient constructs a new client and adds it to the set of clients that
// the Manager is keeping track of.
func (m *Manager) newClient(policy wtpolicy.Policy) error {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
_, ok := m.clients[policy.BlobType]
if ok {
return fmt.Errorf("a client with blob type %s has "+
"already been registered", policy.BlobType)
}
cfg := &clientCfg{
Config: m.cfg,
Policy: policy,
getSweepScript: m.getSweepScript,
}
client, err := newClient(cfg)
if err != nil {
return err
}
m.clients[policy.BlobType] = client
return nil
}
// Start starts all the clients that have been registered with the Manager.
func (m *Manager) Start() error {
var returnErr error
m.started.Do(func() {
chanSub, err := m.cfg.SubscribeChannelEvents()
if err != nil {
returnErr = err
return
}
// Iterate over the list of registered channels and check if any
// of them can be marked as closed.
for id := range m.chanInfos {
isClosed, closedHeight, err := m.isChannelClosed(id)
if err != nil {
returnErr = err
return
}
if !isClosed {
continue
}
_, err = m.cfg.DB.MarkChannelClosed(id, closedHeight)
if err != nil {
log.Errorf("could not mark channel(%s) as "+
"closed: %v", id, err)
continue
}
// Since the channel has been marked as closed, we can
// also remove it from the channel summaries map.
delete(m.chanInfos, id)
}
// Load all closable sessions.
closableSessions, err := m.cfg.DB.ListClosableSessions()
if err != nil {
returnErr = err
return
}
err = m.trackClosableSessions(closableSessions)
if err != nil {
returnErr = err
return
}
m.wg.Add(1)
go m.handleChannelCloses(chanSub)
// Subscribe to new block events.
blockEvents, err := m.cfg.ChainNotifier.RegisterBlockEpochNtfn(
nil,
)
if err != nil {
returnErr = err
return
}
m.wg.Add(1)
go m.handleClosableSessions(blockEvents)
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for _, client := range m.clients {
if err := client.start(); err != nil {
returnErr = err
return
}
}
})
return returnErr
}
// Stop stops all the clients that the Manger is managing.
func (m *Manager) Stop() error {
var returnErr error
m.stopped.Do(func() {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
close(m.quit)
m.wg.Wait()
for _, client := range m.clients {
if err := client.stop(); err != nil {
returnErr = err
}
}
})
return returnErr
}
// AddTower adds a new watchtower reachable at the given address and considers
// it for new sessions. If the watchtower already exists, then any new addresses
// included will be considered when dialing it for session negotiations and
// backups.
func (m *Manager) AddTower(address *lnwire.NetAddress) error {
// We'll start by updating our persisted state, followed by the
// in-memory state of each client, with the new tower. This might not
// actually be a new tower, but it might include a new address at which
// it can be reached.
dbTower, err := m.cfg.DB.CreateTower(address)
if err != nil {
return err
}
tower, err := NewTowerFromDBTower(dbTower)
if err != nil {
return err
}
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for blobType, client := range m.clients {
clientType, err := blobType.Identifier()
if err != nil {
return err
}
if err := client.addTower(tower); err != nil {
return fmt.Errorf("could not add tower(%x) to the %s "+
"tower client: %w",
tower.IdentityKey.SerializeCompressed(),
clientType, err)
}
}
return nil
}
// RemoveTower removes a watchtower from being considered for future session
// negotiations and from being used for any subsequent backups until it's added
// again. If an address is provided, then this call only serves as a way of
// removing the address from the watchtower instead.
func (m *Manager) RemoveTower(key *btcec.PublicKey, addr net.Addr) error {
// We'll load the tower before potentially removing it in order to
// retrieve its ID within the database.
dbTower, err := m.cfg.DB.LoadTower(key)
if err != nil {
return err
}
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for _, client := range m.clients {
err := client.removeTower(dbTower.ID, key, addr)
if err != nil {
return err
}
}
if err := m.cfg.DB.RemoveTower(key, addr); err != nil {
// If the persisted state update fails, re-add the address to
// our client's in-memory state.
tower, newTowerErr := NewTowerFromDBTower(dbTower)
if newTowerErr != nil {
log.Errorf("could not create new in-memory tower: %v",
newTowerErr)
return err
}
for _, client := range m.clients {
addTowerErr := client.addTower(tower)
if addTowerErr != nil {
log.Errorf("could not re-add tower: %v",
addTowerErr)
}
}
return err
}
return nil
}
// Stats returns the in-memory statistics of the clients managed by the Manager
// since startup.
func (m *Manager) Stats() ClientStats {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
var resp ClientStats
for _, client := range m.clients {
stats := client.getStats()
resp.NumTasksAccepted += stats.NumTasksAccepted
resp.NumTasksIneligible += stats.NumTasksIneligible
resp.NumTasksPending += stats.NumTasksPending
resp.NumSessionsAcquired += stats.NumSessionsAcquired
resp.NumSessionsExhausted += stats.NumSessionsExhausted
}
return resp
}
// RegisteredTowers retrieves the list of watchtowers being used by the various
// clients.
func (m *Manager) RegisteredTowers(opts ...wtdb.ClientSessionListOption) (
map[blob.Type][]*RegisteredTower, error) {
towers, err := m.cfg.DB.ListTowers()
if err != nil {
return nil, err
}
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
resp := make(map[blob.Type][]*RegisteredTower)
for _, client := range m.clients {
towers, err := client.registeredTowers(towers, opts...)
if err != nil {
return nil, err
}
resp[client.policy().BlobType] = towers
}
return resp, nil
}
// LookupTower retrieves a registered watchtower through its public key.
func (m *Manager) LookupTower(key *btcec.PublicKey,
opts ...wtdb.ClientSessionListOption) (map[blob.Type]*RegisteredTower,
error) {
tower, err := m.cfg.DB.LoadTower(key)
if err != nil {
return nil, err
}
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
resp := make(map[blob.Type]*RegisteredTower)
for _, client := range m.clients {
tower, err := client.lookupTower(tower, opts...)
if err != nil {
return nil, err
}
resp[client.policy().BlobType] = tower
}
return resp, nil
}
// Policy returns the active client policy configuration for the client using
// the given blob type.
func (m *Manager) Policy(blobType blob.Type) (wtpolicy.Policy, error) {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
var policy wtpolicy.Policy
client, ok := m.clients[blobType]
if !ok {
return policy, fmt.Errorf("no client for the given blob type")
}
return client.policy(), nil
}
// RegisterChannel persistently initializes any channel-dependent parameters
// within the client. This should be called during link startup to ensure that
// the client is able to support the link during operation.
func (m *Manager) RegisterChannel(id lnwire.ChannelID,
chanType channeldb.ChannelType) error {
blobType := blob.TypeAltruistCommit
if chanType.HasAnchors() {
blobType = blob.TypeAltruistAnchorCommit
}
m.clientsMu.Lock()
if _, ok := m.clients[blobType]; !ok {
m.clientsMu.Unlock()
return fmt.Errorf("no client registered for blob type %s",
blobType)
}
m.clientsMu.Unlock()
m.backupMu.Lock()
defer m.backupMu.Unlock()
// If a pkscript for this channel already exists, the channel has been
// previously registered.
if _, ok := m.chanInfos[id]; ok {
// Keep track of which blob type this channel will use for
// updates.
m.chanBlobType[id] = blobType
return nil
}
// Otherwise, generate a new sweep pkscript used to sweep funds for this
// channel.
pkScript, err := m.cfg.NewAddress()
if err != nil {
return err
}
// Persist the sweep pkscript so that restarts will not introduce
// address inflation when the channel is reregistered after a restart.
err = m.cfg.DB.RegisterChannel(id, pkScript)
if err != nil {
return err
}
// Finally, cache the pkscript in our in-memory cache to avoid db
// lookups for the remainder of the daemon's execution.
m.chanInfos[id] = &wtdb.ChannelInfo{
ClientChanSummary: wtdb.ClientChanSummary{
SweepPkScript: pkScript,
},
}
// Keep track of which blob type this channel will use for updates.
m.chanBlobType[id] = blobType
return nil
}
// BackupState initiates a request to back up a particular revoked state. If the
// method returns nil, the backup is guaranteed to be successful unless the
// justice transaction would create dust outputs when trying to abide by the
// negotiated policy.
func (m *Manager) BackupState(chanID *lnwire.ChannelID, stateNum uint64) error {
select {
case <-m.quit:
return ErrClientExiting
default:
}
// Make sure that this channel is registered with the tower client.
m.backupMu.Lock()
info, ok := m.chanInfos[*chanID]
if !ok {
m.backupMu.Unlock()
return ErrUnregisteredChannel
}
// Ignore backups that have already been presented to the client.
var duplicate bool
info.MaxHeight.WhenSome(func(maxHeight uint64) {
if stateNum <= maxHeight {
duplicate = true
}
})
if duplicate {
m.backupMu.Unlock()
log.Debugf("Ignoring duplicate backup for chanid=%v at "+
"height=%d", chanID, stateNum)
return nil
}
// This backup has a higher commit height than any known backup for this
// channel. We'll update our tip so that we won't accept it again if the
// link flaps.
m.chanInfos[*chanID].MaxHeight = fn.Some(stateNum)
blobType, ok := m.chanBlobType[*chanID]
if !ok {
m.backupMu.Unlock()
return ErrUnregisteredChannel
}
m.backupMu.Unlock()
m.clientsMu.Lock()
client, ok := m.clients[blobType]
if !ok {
m.clientsMu.Unlock()
return fmt.Errorf("no client registered for blob type %s",
blobType)
}
m.clientsMu.Unlock()
return client.backupState(chanID, stateNum)
}
// isChanClosed can be used to check if the channel with the given ID has been
// closed. If it has been, the block height in which its closing transaction was
// mined will also be returned.
func (m *Manager) isChannelClosed(id lnwire.ChannelID) (bool, uint32,
error) {
chanSum, err := m.cfg.FetchClosedChannel(id)
if errors.Is(err, channeldb.ErrClosedChannelNotFound) {
return false, 0, nil
} else if err != nil {
return false, 0, err
}
return true, chanSum.CloseHeight, nil
}
// trackClosableSessions takes in a map of session IDs to the earliest block
// height at which the session should be deleted. For each of the sessions,
// a random delay is added to the block height and the session is added to the
// closableSessionQueue.
func (m *Manager) trackClosableSessions(
sessions map[wtdb.SessionID]uint32) error {
// For each closable session, add a random delay to its close
// height and add it to the closableSessionQueue.
for sID, blockHeight := range sessions {
delay, err := newRandomDelay(m.cfg.SessionCloseRange)
if err != nil {
return err
}
deleteHeight := blockHeight + delay
m.closableSessionQueue.Push(&sessionCloseItem{
sessionID: sID,
deleteHeight: deleteHeight,
})
}
return nil
}
// handleChannelCloses listens for channel close events and marks channels as
// closed in the DB.
//
// NOTE: This method MUST be run as a goroutine.
func (m *Manager) handleChannelCloses(chanSub subscribe.Subscription) {
defer m.wg.Done()
log.Debugf("Starting channel close handler")
defer log.Debugf("Stopping channel close handler")
for {
select {
case update, ok := <-chanSub.Updates():
if !ok {
log.Debugf("Channel notifier has exited")
return
}
// We only care about channel-close events.
event, ok := update.(channelnotifier.ClosedChannelEvent)
if !ok {
continue
}
chanID := lnwire.NewChanIDFromOutPoint(
&event.CloseSummary.ChanPoint,
)
log.Debugf("Received ClosedChannelEvent for "+
"channel: %s", chanID)
err := m.handleClosedChannel(
chanID, event.CloseSummary.CloseHeight,
)
if err != nil {
log.Errorf("Could not handle channel close "+
"event for channel(%s): %v", chanID,
err)
}
case <-m.quit:
return
}
}
}
// handleClosedChannel handles the closure of a single channel. It will mark the
// channel as closed in the DB, then it will handle all the sessions that are
// now closable due to the channel closure.
func (m *Manager) handleClosedChannel(chanID lnwire.ChannelID,
closeHeight uint32) error {
m.backupMu.Lock()
defer m.backupMu.Unlock()
// We only care about channels registered with the tower client.
if _, ok := m.chanInfos[chanID]; !ok {
return nil
}
log.Debugf("Marking channel(%s) as closed", chanID)
sessions, err := m.cfg.DB.MarkChannelClosed(chanID, closeHeight)
if err != nil {
return fmt.Errorf("could not mark channel(%s) as closed: %w",
chanID, err)
}
closableSessions := make(map[wtdb.SessionID]uint32, len(sessions))
for _, sess := range sessions {
closableSessions[sess] = closeHeight
}
log.Debugf("Tracking %d new closable sessions as a result of "+
"closing channel %s", len(closableSessions), chanID)
err = m.trackClosableSessions(closableSessions)
if err != nil {
return fmt.Errorf("could not track closable sessions: %w", err)
}
delete(m.chanInfos, chanID)
return nil
}
// handleClosableSessions listens for new block notifications. For each block,
// it checks the closableSessionQueue to see if there is a closable session with
// a delete-height smaller than or equal to the new block, if there is then the
// tower is informed that it can delete the session, and then we also delete it
// from our DB.
func (m *Manager) handleClosableSessions(
blocksChan *chainntnfs.BlockEpochEvent) {
defer m.wg.Done()
log.Debug("Starting closable sessions handler")
defer log.Debug("Stopping closable sessions handler")
for {
select {
case newBlock := <-blocksChan.Epochs:
if newBlock == nil {
return
}
height := uint32(newBlock.Height)
for {
select {
case <-m.quit:
return
default:
}
// If there are no closable sessions that we
// need to handle, then we are done and can
// reevaluate when the next block comes.
item := m.closableSessionQueue.Top()
if item == nil {
break
}
// If there is closable session but the delete
// height we have set for it is after the
// current block height, then our work is done.
if item.deleteHeight > height {
break
}
// Otherwise, we pop this item from the heap
// and handle it.
m.closableSessionQueue.Pop()
// Fetch the session from the DB so that we can
// extract the Tower info.
sess, err := m.cfg.DB.GetClientSession(
item.sessionID,
)
if err != nil {
log.Errorf("error calling "+
"GetClientSession for "+
"session %s: %v",
item.sessionID, err)
continue
}
// get appropriate client.
m.clientsMu.Lock()
client, ok := m.clients[sess.Policy.BlobType]
if !ok {
m.clientsMu.Unlock()
log.Errorf("no client currently " +
"active for the session type")
continue
}
m.clientsMu.Unlock()
clientName, err := client.policy().BlobType.
Identifier()
if err != nil {
log.Errorf("could not get client "+
"identifier: %v", err)
continue
}
// Stop the session and remove it from the
// in-memory set.
err = client.stopAndRemoveSession(
item.sessionID,
)
if err != nil {
log.Errorf("could not remove "+
"session(%s) from in-memory "+
"set of the %s client: %v",
item.sessionID, clientName, err)
continue
}
err = client.deleteSessionFromTower(sess)
if err != nil {
log.Errorf("error deleting "+
"session %s from tower: %v",
sess.ID, err)
continue
}
err = m.cfg.DB.DeleteSession(item.sessionID)
if err != nil {
log.Errorf("could not delete "+
"session(%s) from DB: %w",
sess.ID, err)
continue
}
}
case <-m.quit:
return
}
}
}
func (m *Manager) getSweepScript(id lnwire.ChannelID) ([]byte, bool) {
m.backupMu.Lock()
defer m.backupMu.Unlock()
summary, ok := m.chanInfos[id]
if !ok {
return nil, false
}
return summary.SweepPkScript, true
}
+1 -1
View File
@@ -94,7 +94,7 @@ type sessionQueueConfig struct {
// sessionQueue implements a reliable queue that will encrypt and send accepted // sessionQueue implements a reliable queue that will encrypt and send accepted
// backups to the watchtower specified in the config's ClientSession. Calling // backups to the watchtower specified in the config's ClientSession. Calling
// Stop will attempt to perform a clean shutdown replaying any un-committed // Stop will attempt to perform a clean shutdown replaying any un-committed
// pending updates to the TowerClient's main task pipeline. // pending updates to the client's main task pipeline.
type sessionQueue struct { type sessionQueue struct {
started sync.Once started sync.Once
stopped sync.Once stopped sync.Once
+28 -21
View File
@@ -8,8 +8,6 @@ import (
// ClientStats is a collection of in-memory statistics of the actions the client // ClientStats is a collection of in-memory statistics of the actions the client
// has performed since its creation. // has performed since its creation.
type ClientStats struct { type ClientStats struct {
mu sync.Mutex
// NumTasksPending is the total number of backups that are pending to // NumTasksPending is the total number of backups that are pending to
// be acknowledged by all active and exhausted watchtower sessions. // be acknowledged by all active and exhausted watchtower sessions.
NumTasksPending int NumTasksPending int
@@ -31,68 +29,77 @@ type ClientStats struct {
NumSessionsExhausted int NumSessionsExhausted int
} }
// clientStats wraps ClientStats with a mutex so that it's members can be
// accessed in a thread safe manner.
type clientStats struct {
mu sync.Mutex
ClientStats
}
// taskReceived increments the number of backup requests the client has received // taskReceived increments the number of backup requests the client has received
// from active channels. // from active channels.
func (s *ClientStats) taskReceived() { func (s *clientStats) taskReceived() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.NumTasksPending++ s.NumTasksPending++
} }
// taskAccepted increments the number of tasks that have been assigned to active // taskAccepted increments the number of tasks that have been assigned to active
// session queues, and are awaiting upload to a tower. // session queues, and are awaiting upload to a tower.
func (s *ClientStats) taskAccepted() { func (s *clientStats) taskAccepted() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.NumTasksAccepted++ s.NumTasksAccepted++
s.NumTasksPending-- s.NumTasksPending--
} }
// getStatsCopy returns a copy of the ClientStats.
func (s *clientStats) getStatsCopy() ClientStats {
s.mu.Lock()
defer s.mu.Unlock()
return s.ClientStats
}
// taskIneligible increments the number of tasks that were unable to satisfy the // taskIneligible increments the number of tasks that were unable to satisfy the
// active session queue's policy. These can potentially be retried later, but // active session queue's policy. These can potentially be retried later, but
// typically this means that the balance created dust outputs, so it may not be // typically this means that the balance created dust outputs, so it may not be
// worth backing up at all. // worth backing up at all.
func (s *ClientStats) taskIneligible() { func (s *clientStats) taskIneligible() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.NumTasksIneligible++ s.NumTasksIneligible++
} }
// sessionAcquired increments the number of sessions that have been successfully // sessionAcquired increments the number of sessions that have been successfully
// negotiated by the client during this execution. // negotiated by the client during this execution.
func (s *ClientStats) sessionAcquired() { func (s *clientStats) sessionAcquired() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.NumSessionsAcquired++ s.NumSessionsAcquired++
} }
// sessionExhausted increments the number of session that have become full as a // sessionExhausted increments the number of session that have become full as a
// result of accepting backup tasks. // result of accepting backup tasks.
func (s *ClientStats) sessionExhausted() { func (s *clientStats) sessionExhausted() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.NumSessionsExhausted++ s.NumSessionsExhausted++
} }
// String returns a human-readable summary of the client's metrics. // String returns a human-readable summary of the client's metrics.
func (s *ClientStats) String() string { func (s *clientStats) String() string {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
return fmt.Sprintf("tasks(received=%d accepted=%d ineligible=%d) "+ return fmt.Sprintf("tasks(received=%d accepted=%d ineligible=%d) "+
"sessions(acquired=%d exhausted=%d)", s.NumTasksPending, "sessions(acquired=%d exhausted=%d)", s.NumTasksPending,
s.NumTasksAccepted, s.NumTasksIneligible, s.NumSessionsAcquired, s.NumTasksAccepted, s.NumTasksIneligible, s.NumSessionsAcquired,
s.NumSessionsExhausted) s.NumSessionsExhausted)
} }
// Copy returns a copy of the current stats.
func (s *ClientStats) Copy() ClientStats {
s.mu.Lock()
defer s.mu.Unlock()
return ClientStats{
NumTasksPending: s.NumTasksPending,
NumTasksAccepted: s.NumTasksAccepted,
NumTasksIneligible: s.NumTasksIneligible,
NumSessionsAcquired: s.NumSessionsAcquired,
NumSessionsExhausted: s.NumSessionsExhausted,
}
}