diff --git a/cmd/lncli/wtclient.go b/cmd/lncli/wtclient.go index 5f887fc9..f831403a 100644 --- a/cmd/lncli/wtclient.go +++ b/cmd/lncli/wtclient.go @@ -250,19 +250,44 @@ var policyCommand = cli.Command{ Name: "policy", Usage: "Display the active watchtower client policy configuration.", Action: actionDecorator(policy), + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "legacy", + Usage: "Retrieve the legacy tower client's current " + + "policy. (default)", + }, + cli.BoolFlag{ + Name: "anchor", + Usage: "Retrieve the anchor tower client's current policy.", + }, + }, } func policy(ctx *cli.Context) error { // Display the command's help message if the number of arguments/flags // is not what we expect. - if ctx.NArg() > 0 || ctx.NumFlags() > 0 { + if ctx.NArg() > 0 || ctx.NumFlags() > 1 { return cli.ShowCommandHelp(ctx, "policy") } + var policyType wtclientrpc.PolicyType + switch { + case ctx.Bool("anchor"): + policyType = wtclientrpc.PolicyType_ANCHOR + case ctx.Bool("legacy"): + policyType = wtclientrpc.PolicyType_LEGACY + + // For backwards compatibility with original rpc behavior. + default: + policyType = wtclientrpc.PolicyType_LEGACY + } + client, cleanUp := getWtclient(ctx) defer cleanUp() - req := &wtclientrpc.PolicyRequest{} + req := &wtclientrpc.PolicyRequest{ + PolicyType: policyType, + } resp, err := client.Policy(context.Background(), req) if err != nil { return err diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index c881341f..e121e061 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -183,7 +183,8 @@ type TowerClient interface { // abide by the negotiated policy. If the channel we're trying to back // up doesn't have a tweak for the remote party's output, then // isTweakless should be true. - BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution, bool) error + BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution, + channeldb.ChannelType) error } // InterceptableHtlcForwarder is the interface to set the interceptor diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 70fff3db..6a7c3128 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -256,7 +256,7 @@ type ChannelLinkConfig struct { // TowerClient is an optional engine that manages the signing, // encrypting, and uploading of justice transactions to the daemon's - // configured set of watchtowers. + // configured set of watchtowers for legacy channels. TowerClient TowerClient // MaxOutgoingCltvExpiry is the maximum outgoing timelock that the link @@ -435,12 +435,7 @@ func (l *channelLink) Start() error { // If the config supplied watchtower client, ensure the channel is // registered before trying to use it during operation. - // TODO(halseth): support anchor types for watchtower. - state := l.channel.State() - if l.cfg.TowerClient != nil && state.ChanType.HasAnchors() { - l.log.Warnf("Skipping tower registration for anchor " + - "channel type") - } else if l.cfg.TowerClient != nil && !state.ChanType.HasAnchors() { + if l.cfg.TowerClient != nil { err := l.cfg.TowerClient.RegisterChannel(l.ChanID()) if err != nil { return err @@ -1835,14 +1830,9 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { return } - // If we have a tower client, we'll proceed in backing up the - // state that was just revoked. - // TODO(halseth): support anchor types for watchtower. - state := l.channel.State() - if l.cfg.TowerClient != nil && state.ChanType.HasAnchors() { - l.log.Warnf("Skipping tower backup for anchor " + - "channel type") - } else if l.cfg.TowerClient != nil && !state.ChanType.HasAnchors() { + // If we have a tower client for this channel type, we'll + if l.cfg.TowerClient != nil { + state := l.channel.State() breachInfo, err := lnwallet.NewBreachRetribution( state, state.RemoteCommitment.CommitHeight-1, 0, ) @@ -1852,10 +1842,9 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { return } - chanType := l.channel.State().ChanType chanID := l.ChanID() err = l.cfg.TowerClient.BackupState( - &chanID, breachInfo, chanType.IsTweakless(), + &chanID, breachInfo, state.ChanType, ) if err != nil { l.fail(LinkFailureError{code: ErrInternalError}, diff --git a/lnrpc/wtclientrpc/config.go b/lnrpc/wtclientrpc/config.go index 8796bd05..9127c084 100644 --- a/lnrpc/wtclientrpc/config.go +++ b/lnrpc/wtclientrpc/config.go @@ -19,6 +19,10 @@ type Config struct { // through the watchtower RPC subserver. Client wtclient.Client + // 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 // addresses to ensure we don't leak any information when running over // non-clear networks, e.g. Tor, etc. diff --git a/lnrpc/wtclientrpc/wtclient.go b/lnrpc/wtclientrpc/wtclient.go index acd136ef..4d55e468 100644 --- a/lnrpc/wtclientrpc/wtclient.go +++ b/lnrpc/wtclientrpc/wtclient.go @@ -14,6 +14,8 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/watchtower" "github.com/lightningnetwork/lnd/watchtower/wtclient" + "github.com/lightningnetwork/lnd/watchtower/wtdb" + "github.com/lightningnetwork/lnd/watchtower/wtpolicy" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" ) @@ -176,9 +178,14 @@ func (c *WatchtowerClient) AddTower(ctx context.Context, IdentityKey: pubKey, Address: addr, } + + // TODO(conner): make atomic via multiplexed client 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 &AddTowerResponse{}, nil } @@ -211,7 +218,13 @@ func (c *WatchtowerClient) RemoveTower(ctx context.Context, } } - if err := c.cfg.Client.RemoveTower(pubKey, addr); err != nil { + // TODO(conner): make atomic via multiplexed client + err = c.cfg.Client.RemoveTower(pubKey, addr) + if err != nil { + return nil, err + } + err = c.cfg.AnchorClient.RemoveTower(pubKey, addr) + if err != nil { return nil, err } @@ -226,11 +239,25 @@ func (c *WatchtowerClient) ListTowers(ctx context.Context, return nil, err } - towers, err := c.cfg.Client.RegisteredTowers() + anchorTowers, err := c.cfg.AnchorClient.RegisteredTowers() if err != nil { return nil, err } + legacyTowers, err := c.cfg.Client.RegisteredTowers() + if err != nil { + return nil, err + } + + // Filter duplicates. + towers := make(map[wtdb.TowerID]*wtclient.RegisteredTower) + for _, tower := range anchorTowers { + towers[tower.Tower.ID] = tower + } + for _, tower := range legacyTowers { + towers[tower.Tower.ID] = tower + } + rpcTowers := make([]*Tower, 0, len(towers)) for _, tower := range towers { rpcTower := marshallTower(tower, req.IncludeSessions) @@ -253,7 +280,11 @@ func (c *WatchtowerClient) GetTowerInfo(ctx context.Context, return nil, err } - tower, err := c.cfg.Client.LookupTower(pubKey) + var tower *wtclient.RegisteredTower + tower, err = c.cfg.Client.LookupTower(pubKey) + if err == wtdb.ErrTowerNotFound { + tower, err = c.cfg.AnchorClient.LookupTower(pubKey) + } if err != nil { return nil, err } @@ -269,7 +300,24 @@ func (c *WatchtowerClient) Stats(ctx context.Context, return nil, err } - stats := c.cfg.Client.Stats() + clientStats := []wtclient.ClientStats{ + 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.NumTasksReceived += stat.NumTasksReceived + stats.NumSessionsAcquired += stat.NumSessionsAcquired + stats.NumSessionsExhausted += stat.NumSessionsExhausted + } + return &StatsResponse{ NumBackups: uint32(stats.NumTasksAccepted), NumFailedBackups: uint32(stats.NumTasksIneligible), @@ -287,7 +335,17 @@ func (c *WatchtowerClient) Policy(ctx context.Context, return nil, err } - policy := c.cfg.Client.Policy() + var policy wtpolicy.Policy + switch req.PolicyType { + case PolicyType_LEGACY: + policy = c.cfg.Client.Policy() + case PolicyType_ANCHOR: + policy = c.cfg.AnchorClient.Policy() + default: + return nil, fmt.Errorf("unknown policy type: %v", + req.PolicyType) + } + return &PolicyResponse{ MaxUpdates: uint32(policy.MaxUpdates), SweepSatPerByte: uint32(policy.SweepFeeRate.FeePerKVByte() / 1000), diff --git a/lnrpc/wtclientrpc/wtclient.pb.go b/lnrpc/wtclientrpc/wtclient.pb.go index abd6c450..2a657b78 100644 --- a/lnrpc/wtclientrpc/wtclient.pb.go +++ b/lnrpc/wtclientrpc/wtclient.pb.go @@ -24,6 +24,33 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +type PolicyType int32 + +const ( + // Selects the policy from the legacy tower client. + PolicyType_LEGACY PolicyType = 0 + // Selects the policy from the anchor tower client. + PolicyType_ANCHOR PolicyType = 1 +) + +var PolicyType_name = map[int32]string{ + 0: "LEGACY", + 1: "ANCHOR", +} + +var PolicyType_value = map[string]int32{ + "LEGACY": 0, + "ANCHOR": 1, +} + +func (x PolicyType) String() string { + return proto.EnumName(PolicyType_name, int32(x)) +} + +func (PolicyType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_b5f4e7d95a641af2, []int{0} +} + type AddTowerRequest struct { // The identifying public key of the watchtower to add. Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` @@ -579,9 +606,12 @@ func (m *StatsResponse) GetNumSessionsExhausted() uint32 { } type PolicyRequest struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + // + //The client type from which to retrieve the active offering policy. + PolicyType PolicyType `protobuf:"varint,1,opt,name=policy_type,json=policyType,proto3,enum=wtclientrpc.PolicyType" json:"policy_type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *PolicyRequest) Reset() { *m = PolicyRequest{} } @@ -609,6 +639,13 @@ func (m *PolicyRequest) XXX_DiscardUnknown() { var xxx_messageInfo_PolicyRequest proto.InternalMessageInfo +func (m *PolicyRequest) GetPolicyType() PolicyType { + if m != nil { + return m.PolicyType + } + return PolicyType_LEGACY +} + type PolicyResponse struct { // //The maximum number of updates each session we negotiate with watchtowers @@ -663,6 +700,7 @@ func (m *PolicyResponse) GetSweepSatPerByte() uint32 { } func init() { + proto.RegisterEnum("wtclientrpc.PolicyType", PolicyType_name, PolicyType_value) proto.RegisterType((*AddTowerRequest)(nil), "wtclientrpc.AddTowerRequest") proto.RegisterType((*AddTowerResponse)(nil), "wtclientrpc.AddTowerResponse") proto.RegisterType((*RemoveTowerRequest)(nil), "wtclientrpc.RemoveTowerRequest") @@ -681,50 +719,54 @@ func init() { func init() { proto.RegisterFile("wtclientrpc/wtclient.proto", fileDescriptor_b5f4e7d95a641af2) } var fileDescriptor_b5f4e7d95a641af2 = []byte{ - // 682 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0x4d, 0x6f, 0xd3, 0x40, - 0x10, 0x95, 0x1b, 0x12, 0xd2, 0x49, 0xda, 0xa4, 0x1b, 0x5a, 0x19, 0x53, 0x48, 0xf0, 0x29, 0x7c, - 0x28, 0x11, 0x2d, 0x48, 0x9c, 0x2a, 0xda, 0x42, 0x2b, 0x24, 0x90, 0x22, 0x17, 0x04, 0xe2, 0x80, - 0xb5, 0xb1, 0xb7, 0x89, 0x55, 0x7b, 0xed, 0x7a, 0xd7, 0x4d, 0xf2, 0xa3, 0xf8, 0x19, 0xfc, 0x00, - 0xfe, 0x0d, 0x47, 0xe4, 0xf5, 0xda, 0xb1, 0x1b, 0x47, 0x1c, 0xe0, 0x16, 0xcf, 0x7b, 0xfb, 0x3c, - 0x7e, 0xf3, 0x32, 0x0b, 0xda, 0x8c, 0x5b, 0xae, 0x43, 0x28, 0x0f, 0x03, 0x6b, 0x98, 0xfe, 0x1e, - 0x04, 0xa1, 0xcf, 0x7d, 0xd4, 0xc8, 0x61, 0xfa, 0x29, 0xb4, 0x8e, 0x6d, 0xfb, 0x93, 0x3f, 0x23, - 0xa1, 0x41, 0xae, 0x23, 0xc2, 0x38, 0xda, 0x83, 0x5a, 0x10, 0x8d, 0xaf, 0xc8, 0x42, 0x55, 0x7a, - 0x4a, 0xbf, 0x69, 0xc8, 0x27, 0xa4, 0xc2, 0x5d, 0x6c, 0xdb, 0x21, 0x61, 0x4c, 0xdd, 0xe8, 0x29, - 0xfd, 0x4d, 0x23, 0x7d, 0xd4, 0x11, 0xb4, 0x97, 0x22, 0x2c, 0xf0, 0x29, 0x23, 0xfa, 0x19, 0x20, - 0x83, 0x78, 0xfe, 0x0d, 0xf9, 0x47, 0xed, 0x5d, 0xe8, 0x14, 0x74, 0xa4, 0xfc, 0x57, 0xe8, 0x9c, - 0x13, 0x2e, 0x6a, 0xef, 0xe9, 0xa5, 0xff, 0x37, 0xfd, 0x27, 0xd0, 0x76, 0xa8, 0xe5, 0x46, 0x36, - 0x31, 0x19, 0x61, 0xcc, 0xf1, 0x69, 0xf2, 0xa2, 0xba, 0xd1, 0x92, 0xf5, 0x0b, 0x59, 0xd6, 0x7f, - 0x28, 0xd0, 0x14, 0xba, 0xb2, 0x82, 0xba, 0xd0, 0xa0, 0x91, 0x67, 0x8e, 0xb1, 0x75, 0x15, 0x05, - 0x4c, 0x08, 0x6f, 0x19, 0x40, 0x23, 0xef, 0x24, 0xa9, 0xa0, 0x01, 0x74, 0x62, 0x42, 0x40, 0xa8, - 0xed, 0xd0, 0x49, 0x46, 0xdc, 0x10, 0xc4, 0x1d, 0x1a, 0x79, 0xa3, 0x04, 0x49, 0xf9, 0x5d, 0x68, - 0x78, 0x78, 0x9e, 0xf1, 0x2a, 0x89, 0xa0, 0x87, 0xe7, 0x29, 0xe1, 0x19, 0x20, 0x36, 0x23, 0x24, - 0x30, 0x19, 0xe6, 0x66, 0x40, 0x42, 0x73, 0xbc, 0xe0, 0x44, 0xbd, 0x23, 0x78, 0x2d, 0x81, 0x5c, - 0x60, 0x3e, 0x22, 0xe1, 0xc9, 0x82, 0x13, 0xfd, 0x97, 0x02, 0x55, 0xd1, 0xef, 0xda, 0x8f, 0xdf, - 0x87, 0x4d, 0xe9, 0x26, 0x89, 0xbb, 0xaa, 0xf4, 0x37, 0x8d, 0x65, 0x01, 0xbd, 0x06, 0x15, 0x5b, - 0xdc, 0xb9, 0xc9, 0x9c, 0x31, 0x2d, 0x4c, 0x6d, 0xc7, 0xc6, 0x9c, 0x88, 0xd6, 0xea, 0xc6, 0x5e, - 0x82, 0x4b, 0x3f, 0x4e, 0x53, 0x14, 0x3d, 0x86, 0x66, 0xfc, 0xdd, 0x99, 0xa1, 0x49, 0x83, 0xb1, - 0x59, 0xa9, 0x99, 0xe8, 0x15, 0xd4, 0x33, 0xb8, 0xda, 0xab, 0xf4, 0x1b, 0x07, 0xf7, 0x07, 0xb9, - 0xf8, 0x0d, 0xf2, 0x46, 0x1b, 0x19, 0x55, 0x3f, 0x82, 0x9d, 0x0f, 0x0e, 0x4b, 0xc6, 0xcb, 0xd2, - 0xd9, 0x96, 0xcd, 0x50, 0x29, 0x9f, 0xe1, 0x1b, 0x40, 0xf9, 0xf3, 0x49, 0x66, 0xd0, 0x53, 0xa8, - 0x71, 0x51, 0x51, 0x15, 0xd1, 0x0a, 0x5a, 0x6d, 0xc5, 0x90, 0x0c, 0x7d, 0x1b, 0x9a, 0x17, 0x1c, - 0xf3, 0xf4, 0xe5, 0xfa, 0x6f, 0x05, 0xb6, 0x64, 0x41, 0xaa, 0xfd, 0xf7, 0x58, 0x3c, 0x07, 0x14, - 0xf3, 0x2f, 0xb1, 0xe3, 0x12, 0xfb, 0x56, 0x3a, 0xda, 0x34, 0xf2, 0xce, 0x04, 0x90, 0xb2, 0x0f, - 0x60, 0x37, 0x6f, 0xbe, 0x89, 0xad, 0xeb, 0xc8, 0x09, 0x89, 0x2d, 0xa7, 0xd0, 0xc9, 0x4d, 0xe1, - 0x58, 0x42, 0xe8, 0x25, 0xec, 0x15, 0xce, 0x90, 0xf9, 0x14, 0x47, 0x8c, 0x13, 0x5b, 0xad, 0x8a, - 0x43, 0xf7, 0x72, 0x87, 0xde, 0xa5, 0x98, 0xde, 0x82, 0xad, 0x91, 0xef, 0x3a, 0xd6, 0x22, 0xf5, - 0xe2, 0x3b, 0x6c, 0xa7, 0x85, 0xa5, 0x17, 0x71, 0xa2, 0xa3, 0x20, 0xce, 0x45, 0xe6, 0x85, 0x87, - 0xe7, 0x9f, 0x93, 0xca, 0x9a, 0x44, 0x6f, 0x94, 0x26, 0xfa, 0xe0, 0x67, 0x05, 0xda, 0x5f, 0x30, - 0xb7, 0xa6, 0x62, 0x16, 0xa7, 0x62, 0x42, 0xe8, 0x1c, 0xea, 0xe9, 0x8e, 0x41, 0xfb, 0x85, 0xc1, - 0xdd, 0xda, 0x5f, 0xda, 0xc3, 0x35, 0xa8, 0xec, 0x75, 0x04, 0x8d, 0xdc, 0x42, 0x41, 0xdd, 0x02, - 0x7b, 0x75, 0x65, 0x69, 0xbd, 0xf5, 0x04, 0xa9, 0xf8, 0x11, 0x60, 0x99, 0x36, 0xf4, 0xa8, 0xc0, - 0x5f, 0x89, 0xb1, 0xd6, 0x5d, 0x8b, 0x4b, 0xb9, 0xb7, 0xd0, 0xcc, 0xaf, 0x36, 0x54, 0x6c, 0xa0, - 0x64, 0xeb, 0x69, 0x25, 0x41, 0x46, 0x47, 0x50, 0x15, 0x79, 0x45, 0xc5, 0x3f, 0x5c, 0x3e, 0xd4, - 0x9a, 0x56, 0x06, 0xc9, 0x2e, 0x8e, 0xa1, 0x96, 0x0c, 0x19, 0x15, 0x59, 0x85, 0x28, 0x68, 0x0f, - 0x4a, 0xb1, 0x44, 0xe2, 0xe4, 0xf0, 0xdb, 0x8b, 0x89, 0xc3, 0xa7, 0xd1, 0x78, 0x60, 0xf9, 0xde, - 0xd0, 0x75, 0x26, 0x53, 0x4e, 0x1d, 0x3a, 0xa1, 0x84, 0xcf, 0xfc, 0xf0, 0x6a, 0xe8, 0x52, 0x7b, - 0xe8, 0xd2, 0xfc, 0x05, 0x15, 0x06, 0xd6, 0xb8, 0x26, 0x2e, 0xa9, 0xc3, 0x3f, 0x01, 0x00, 0x00, - 0xff, 0xff, 0x5d, 0xba, 0x03, 0x17, 0xc2, 0x06, 0x00, 0x00, + // 739 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xcd, 0x6e, 0xd3, 0x4a, + 0x14, 0xbe, 0x6e, 0x6e, 0x72, 0xd3, 0x93, 0xb4, 0x4d, 0x27, 0xb7, 0xbd, 0xb9, 0xa6, 0x90, 0x60, + 0xb1, 0x08, 0x05, 0x25, 0x22, 0x05, 0xa9, 0xab, 0x8a, 0x34, 0xb4, 0xa5, 0x52, 0x81, 0xc8, 0x2d, + 0xe2, 0x67, 0x81, 0x35, 0xb1, 0xa7, 0x89, 0x55, 0x7b, 0xec, 0xda, 0xe3, 0x26, 0x79, 0x28, 0x1e, + 0x83, 0x07, 0xe0, 0x6d, 0x58, 0x22, 0x8f, 0xc7, 0x8e, 0xdd, 0x3a, 0x62, 0x01, 0x3b, 0xfb, 0x7c, + 0xdf, 0x7c, 0x3e, 0xf3, 0x9d, 0x1f, 0x83, 0x3c, 0x65, 0xba, 0x65, 0x12, 0xca, 0x3c, 0x57, 0xef, + 0xc6, 0xcf, 0x1d, 0xd7, 0x73, 0x98, 0x83, 0x2a, 0x29, 0x4c, 0x19, 0xc0, 0x46, 0xdf, 0x30, 0x2e, + 0x9c, 0x29, 0xf1, 0x54, 0x72, 0x1d, 0x10, 0x9f, 0xa1, 0x6d, 0x28, 0xb9, 0xc1, 0xe8, 0x8a, 0xcc, + 0x1b, 0x52, 0x4b, 0x6a, 0x57, 0x55, 0xf1, 0x86, 0x1a, 0xf0, 0x0f, 0x36, 0x0c, 0x8f, 0xf8, 0x7e, + 0x63, 0xa5, 0x25, 0xb5, 0x57, 0xd5, 0xf8, 0x55, 0x41, 0x50, 0x5b, 0x88, 0xf8, 0xae, 0x43, 0x7d, + 0xa2, 0x1c, 0x03, 0x52, 0x89, 0xed, 0xdc, 0x90, 0xdf, 0xd4, 0xde, 0x82, 0x7a, 0x46, 0x47, 0xc8, + 0x7f, 0x84, 0xfa, 0x09, 0x61, 0x3c, 0x76, 0x4a, 0x2f, 0x9d, 0x5f, 0xe9, 0x3f, 0x86, 0x9a, 0x49, + 0x75, 0x2b, 0x30, 0x88, 0xe6, 0x13, 0xdf, 0x37, 0x1d, 0x1a, 0x7d, 0xa8, 0xac, 0x6e, 0x88, 0xf8, + 0xb9, 0x08, 0x2b, 0x5f, 0x25, 0xa8, 0x72, 0x5d, 0x11, 0x41, 0x4d, 0xa8, 0xd0, 0xc0, 0xd6, 0x46, + 0x58, 0xbf, 0x0a, 0x5c, 0x9f, 0x0b, 0xaf, 0xa9, 0x40, 0x03, 0xfb, 0x30, 0x8a, 0xa0, 0x0e, 0xd4, + 0x43, 0x82, 0x4b, 0xa8, 0x61, 0xd2, 0x71, 0x42, 0x5c, 0xe1, 0xc4, 0x4d, 0x1a, 0xd8, 0xc3, 0x08, + 0x89, 0xf9, 0x4d, 0xa8, 0xd8, 0x78, 0x96, 0xf0, 0x0a, 0x91, 0xa0, 0x8d, 0x67, 0x31, 0xe1, 0x09, + 0x20, 0x7f, 0x4a, 0x88, 0xab, 0xf9, 0x98, 0x69, 0x2e, 0xf1, 0xb4, 0xd1, 0x9c, 0x91, 0xc6, 0xdf, + 0x9c, 0xb7, 0xc1, 0x91, 0x73, 0xcc, 0x86, 0xc4, 0x3b, 0x9c, 0x33, 0xa2, 0x7c, 0x97, 0xa0, 0xc8, + 0xf3, 0x5d, 0x7a, 0xf9, 0x1d, 0x58, 0x15, 0x6e, 0x92, 0x30, 0xab, 0x42, 0x7b, 0x55, 0x5d, 0x04, + 0xd0, 0x3e, 0x34, 0xb0, 0xce, 0xcc, 0x9b, 0xc4, 0x19, 0x4d, 0xc7, 0xd4, 0x30, 0x0d, 0xcc, 0x08, + 0x4f, 0xad, 0xac, 0x6e, 0x47, 0xb8, 0xf0, 0x63, 0x10, 0xa3, 0xe8, 0x21, 0x54, 0xc3, 0x7b, 0x27, + 0x86, 0x46, 0x09, 0x86, 0x66, 0xc5, 0x66, 0xa2, 0x17, 0x50, 0x4e, 0xe0, 0x62, 0xab, 0xd0, 0xae, + 0xf4, 0xfe, 0xef, 0xa4, 0xda, 0xaf, 0x93, 0x36, 0x5a, 0x4d, 0xa8, 0xca, 0x01, 0x6c, 0x9e, 0x99, + 0x7e, 0x54, 0x5e, 0x3f, 0xae, 0x6d, 0x5e, 0x0d, 0xa5, 0xfc, 0x1a, 0xbe, 0x04, 0x94, 0x3e, 0x1f, + 0xf5, 0x0c, 0xda, 0x85, 0x12, 0xe3, 0x91, 0x86, 0xc4, 0x53, 0x41, 0x77, 0x53, 0x51, 0x05, 0x43, + 0x59, 0x87, 0xea, 0x39, 0xc3, 0x2c, 0xfe, 0xb8, 0xf2, 0x43, 0x82, 0x35, 0x11, 0x10, 0x6a, 0x7f, + 0xbc, 0x2d, 0x9e, 0x02, 0x0a, 0xf9, 0x97, 0xd8, 0xb4, 0x88, 0x71, 0xab, 0x3b, 0x6a, 0x34, 0xb0, + 0x8f, 0x39, 0x10, 0xb3, 0x7b, 0xb0, 0x95, 0x36, 0x5f, 0xc3, 0xfa, 0x75, 0x60, 0x7a, 0xc4, 0x10, + 0x55, 0xa8, 0xa7, 0xaa, 0xd0, 0x17, 0x10, 0x7a, 0x0e, 0xdb, 0x99, 0x33, 0x64, 0x36, 0xc1, 0x81, + 0xcf, 0x88, 0xd1, 0x28, 0xf2, 0x43, 0xff, 0xa6, 0x0e, 0x1d, 0xc5, 0x98, 0x72, 0x0a, 0x6b, 0x43, + 0xc7, 0x32, 0xf5, 0x79, 0x5c, 0x88, 0x7d, 0xa8, 0xb8, 0x3c, 0xa0, 0xb1, 0xb9, 0x4b, 0xf8, 0xcd, + 0xd7, 0x7b, 0xff, 0x65, 0xcc, 0x8c, 0x0e, 0x5c, 0xcc, 0x5d, 0xa2, 0x82, 0x9b, 0x3c, 0x2b, 0x5f, + 0x60, 0x3d, 0x96, 0x5a, 0xb8, 0x18, 0xce, 0x42, 0xe0, 0x86, 0x1d, 0x95, 0xb8, 0x68, 0xe3, 0xd9, + 0xfb, 0x28, 0xb2, 0x64, 0x16, 0x56, 0x72, 0x67, 0x61, 0xf7, 0x11, 0xc0, 0xe2, 0xcb, 0x08, 0xa0, + 0x74, 0x76, 0x74, 0xd2, 0x1f, 0x7c, 0xaa, 0xfd, 0x15, 0x3e, 0xf7, 0xdf, 0x0e, 0x5e, 0xbf, 0x53, + 0x6b, 0x52, 0xef, 0x5b, 0x01, 0x6a, 0x1f, 0x30, 0xd3, 0x27, 0xbc, 0xd6, 0x03, 0x9e, 0x34, 0x3a, + 0x81, 0x72, 0xbc, 0xc3, 0xd0, 0x4e, 0xe6, 0x2e, 0xb7, 0xf6, 0xa3, 0x7c, 0x7f, 0x09, 0x2a, 0x6e, + 0x34, 0x84, 0x4a, 0x6a, 0x61, 0xa1, 0x66, 0x86, 0x7d, 0x77, 0x25, 0xca, 0xad, 0xe5, 0x04, 0xa1, + 0xf8, 0x06, 0x60, 0xd1, 0xcd, 0xe8, 0x41, 0x86, 0x7f, 0x67, 0x4c, 0xe4, 0xe6, 0x52, 0x5c, 0xc8, + 0xbd, 0x82, 0x6a, 0x7a, 0x75, 0xa2, 0x6c, 0x02, 0x39, 0x5b, 0x55, 0xce, 0x19, 0x14, 0x74, 0x00, + 0x45, 0x3e, 0x0f, 0x28, 0x3b, 0xd0, 0xe9, 0xa1, 0x91, 0xe5, 0x3c, 0x48, 0x64, 0xd1, 0x87, 0x52, + 0x54, 0x2a, 0x24, 0xe7, 0x74, 0x4e, 0xac, 0x70, 0x2f, 0x17, 0x8b, 0x24, 0x0e, 0xf7, 0x3e, 0x3f, + 0x1b, 0x9b, 0x6c, 0x12, 0x8c, 0x3a, 0xba, 0x63, 0x77, 0x2d, 0x73, 0x3c, 0x61, 0xd4, 0xa4, 0x63, + 0x4a, 0xd8, 0xd4, 0xf1, 0xae, 0xba, 0x16, 0x35, 0xba, 0x16, 0x4d, 0xff, 0x00, 0x3d, 0x57, 0x1f, + 0x95, 0xf8, 0x4f, 0x70, 0xef, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0x99, 0x5f, 0xe1, 0x8e, 0x22, + 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/lnrpc/wtclientrpc/wtclient.pb.gw.go b/lnrpc/wtclientrpc/wtclient.pb.gw.go index 533bd9f7..c650cfec 100644 --- a/lnrpc/wtclientrpc/wtclient.pb.gw.go +++ b/lnrpc/wtclientrpc/wtclient.pb.gw.go @@ -254,10 +254,21 @@ func local_request_WatchtowerClient_Stats_0(ctx context.Context, marshaler runti } +var ( + filter_WatchtowerClient_Policy_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + func request_WatchtowerClient_Policy_0(ctx context.Context, marshaler runtime.Marshaler, client WatchtowerClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq PolicyRequest var metadata runtime.ServerMetadata + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WatchtowerClient_Policy_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.Policy(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err @@ -267,6 +278,10 @@ func local_request_WatchtowerClient_Policy_0(ctx context.Context, marshaler runt var protoReq PolicyRequest var metadata runtime.ServerMetadata + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_WatchtowerClient_Policy_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.Policy(ctx, &protoReq) return msg, metadata, err diff --git a/lnrpc/wtclientrpc/wtclient.proto b/lnrpc/wtclientrpc/wtclient.proto index f7304b3c..15584cd5 100644 --- a/lnrpc/wtclientrpc/wtclient.proto +++ b/lnrpc/wtclientrpc/wtclient.proto @@ -149,7 +149,19 @@ message StatsResponse { uint32 num_sessions_exhausted = 5; } +enum PolicyType { + // Selects the policy from the legacy tower client. + LEGACY = 0; + + // Selects the policy from the anchor tower client. + ANCHOR = 1; +} + message PolicyRequest { + /* + The client type from which to retrieve the active offering policy. + */ + PolicyType policy_type = 1; } message PolicyResponse { diff --git a/lnrpc/wtclientrpc/wtclient.swagger.json b/lnrpc/wtclientrpc/wtclient.swagger.json index 5f7cc8c3..90ac613d 100644 --- a/lnrpc/wtclientrpc/wtclient.swagger.json +++ b/lnrpc/wtclientrpc/wtclient.swagger.json @@ -134,6 +134,20 @@ } } }, + "parameters": [ + { + "name": "policy_type", + "description": "The client type from which to retrieve the active offering policy.\n\n - LEGACY: Selects the policy from the legacy tower client.\n - ANCHOR: Selects the policy from the anchor tower client.", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "LEGACY", + "ANCHOR" + ], + "default": "LEGACY" + } + ], "tags": [ "WatchtowerClient" ] @@ -281,6 +295,15 @@ } } }, + "wtclientrpcPolicyType": { + "type": "string", + "enum": [ + "LEGACY", + "ANCHOR" + ], + "default": "LEGACY", + "description": " - LEGACY: Selects the policy from the legacy tower client.\n - ANCHOR: Selects the policy from the anchor tower client." + }, "wtclientrpcRemoveTowerResponse": { "type": "object" }, diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index aaa28df8..95208413 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -9070,6 +9070,19 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, t *harnessTest) { + t.t.Run("anchors", func(tt *testing.T) { + ht := newHarnessTest(tt, net) + testRevokedCloseRetributionAltruistWatchtowerCase(net, ht, true) + }) + t.t.Run("legacy", func(tt *testing.T) { + ht := newHarnessTest(tt, net) + testRevokedCloseRetributionAltruistWatchtowerCase(net, ht, false) + }) +} + +func testRevokedCloseRetributionAltruistWatchtowerCase( + net *lntest.NetworkHarness, t *harnessTest, anchors bool) { + ctxb := context.Background() const ( chanAmt = lnd.MaxBtcFundingAmount @@ -9080,7 +9093,11 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. - carol, err := net.NewNode("Carol", []string{"--hodl.exit-settle"}) + carolArgs := []string{"--hodl.exit-settle"} + if anchors { + carolArgs = append(carolArgs, "--protocol.anchors") + } + carol, err := net.NewNode("Carol", carolArgs) if err != nil { t.Fatalf("unable to create new nodes: %v", err) } @@ -9133,10 +9150,14 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, // Dave will be the breached party. We set --nolisten to ensure Carol // won't be able to connect to him and trigger the channel data // protection logic automatically. - dave, err := net.NewNode("Dave", []string{ + daveArgs := []string{ "--nolisten", "--wtclient.active", - }) + } + if anchors { + daveArgs = append(daveArgs, "--protocol.anchors") + } + dave, err := net.NewNode("Dave", daveArgs) if err != nil { t.Fatalf("unable to create new node: %v", err) } @@ -9249,7 +9270,9 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, err = wait.NoError(func() error { ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) defer cancel() - bkpStats, err := dave.WatchtowerClient.Stats(ctxt, &wtclientrpc.StatsRequest{}) + bkpStats, err := dave.WatchtowerClient.Stats(ctxt, + &wtclientrpc.StatsRequest{}, + ) if err != nil { return err diff --git a/peer/brontide.go b/peer/brontide.go index 164bbdf4..578d7133 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -248,9 +248,13 @@ type Config struct { // HtlcNotifier is used when creating a ChannelLink. HtlcNotifier *htlcswitch.HtlcNotifier - // TowerClient is used when creating a ChannelLink. + // TowerClient is used by legacy channels to backup revoked states. TowerClient wtclient.Client + // AnchorTowerClient is used by anchor channels to backup revoked + // states. + AnchorTowerClient wtclient.Client + // DisconnectPeer is used to disconnect this peer if the cooperative close // process fails. DisconnectPeer func(*btcec.PublicKey) error @@ -757,6 +761,18 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, return p.cfg.ChainArb.UpdateContractSignals(*chanPoint, signals) } + chanType := lnChan.State().ChanType + + // 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 + if chanType.HasAnchors() { + towerClient = p.cfg.AnchorTowerClient + } else { + towerClient = p.cfg.TowerClient + } + linkCfg := htlcswitch.ChannelLinkConfig{ Peer: p, DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators, @@ -782,7 +798,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, MinFeeUpdateTimeout: htlcswitch.DefaultMinLinkFeeUpdateTimeout, MaxFeeUpdateTimeout: htlcswitch.DefaultMaxLinkFeeUpdateTimeout, OutgoingCltvRejectDelta: p.cfg.OutgoingCltvRejectDelta, - TowerClient: p.cfg.TowerClient, + TowerClient: towerClient, MaxOutgoingCltvExpiry: p.cfg.MaxOutgoingCltvExpiry, MaxFeeAllocation: p.cfg.MaxChannelFeeAllocation, NotifyActiveLink: p.cfg.ChannelNotifier.NotifyActiveLinkEvent, diff --git a/rpcserver.go b/rpcserver.go index a7c265f9..bb5318e4 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -619,8 +619,8 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, cfg, s.cc, cfg.networkDir, macService, atpl, invoiceRegistry, s.htlcSwitch, cfg.ActiveNetParams.Params, s.chanRouter, routerBackend, s.nodeSigner, s.remoteChanDB, s.sweeper, tower, - s.towerClient, cfg.net.ResolveTCPAddr, genInvoiceFeatures, - rpcsLog, + s.towerClient, s.anchorTowerClient, cfg.net.ResolveTCPAddr, + genInvoiceFeatures, rpcsLog, ) if err != nil { return nil, err diff --git a/server.go b/server.go index 13e1462b..c7d2f350 100644 --- a/server.go +++ b/server.go @@ -66,6 +66,7 @@ import ( "github.com/lightningnetwork/lnd/ticker" "github.com/lightningnetwork/lnd/tor" "github.com/lightningnetwork/lnd/walletunlocker" + "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/wtclient" "github.com/lightningnetwork/lnd/watchtower/wtpolicy" "github.com/lightningnetwork/lnd/watchtower/wtserver" @@ -247,6 +248,8 @@ type server struct { towerClient wtclient.Client + anchorTowerClient wtclient.Client + connMgr *connmgr.ConnManager sigPool *lnwallet.SigPool @@ -1265,6 +1268,29 @@ func newServer(cfg *Config, listenAddrs []net.Addr, if err != nil { return nil, err } + + // Copy the policy for legacy channels and set the blob flag + // signalling support for anchor channels. + anchorPolicy := policy + anchorPolicy.TxPolicy.BlobType |= + blob.Type(blob.FlagAnchorChannel) + + s.anchorTowerClient, err = wtclient.New(&wtclient.Config{ + Signer: cc.Wallet.Cfg.Signer, + NewAddress: newSweepPkScriptGen(cc.Wallet), + SecretKeyRing: s.cc.KeyRing, + Dial: cfg.net.Dial, + AuthDial: authDial, + DB: towerClientDB, + Policy: anchorPolicy, + ChainHash: *s.cfg.ActiveNetParams.GenesisHash, + MinBackoff: 10 * time.Second, + MaxBackoff: 5 * time.Minute, + ForceQuitDelay: wtclient.DefaultForceQuitDelay, + }) + if err != nil { + return nil, err + } } if len(cfg.ExternalHosts) != 0 { @@ -1438,6 +1464,12 @@ func (s *server) Start() error { return } } + if s.anchorTowerClient != nil { + if err := s.anchorTowerClient.Start(); err != nil { + startErr = err + return + } + } if err := s.htlcSwitch.Start(); err != nil { startErr = err return @@ -1675,7 +1707,16 @@ func (s *server) Stop() error { // tower. If this is halted for any reason, the force quit timer // will kick in and abort to allow this method to return. if s.towerClient != nil { - s.towerClient.Stop() + if err := s.towerClient.Stop(); err != nil { + srvrLog.Warnf("Unable to shut down tower "+ + "client: %v", err) + } + } + if s.anchorTowerClient != nil { + if err := s.anchorTowerClient.Stop(); err != nil { + srvrLog.Warnf("Unable to shut down anchor "+ + "tower client: %v", err) + } } if s.hostAnn != nil { @@ -3036,6 +3077,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, ChannelNotifier: s.channelNotifier, HtlcNotifier: s.htlcNotifier, TowerClient: s.towerClient, + AnchorTowerClient: s.anchorTowerClient, DisconnectPeer: s.DisconnectPeer, GenNodeAnnouncement: s.genNodeAnnouncement, diff --git a/subrpcserver_config.go b/subrpcserver_config.go index aa1a9ce0..433f7413 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -96,6 +96,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, sweeper *sweep.UtxoSweeper, tower *watchtower.Standalone, towerClient wtclient.Client, + anchorTowerClient wtclient.Client, tcpResolver lncfg.TCPResolver, genInvoiceFeatures func() *lnwire.FeatureVector, rpcLogger btclog.Logger) error { @@ -243,13 +244,16 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, case *wtclientrpc.Config: subCfgValue := extractReflectValue(subCfg) - if towerClient != nil { + if towerClient != nil && anchorTowerClient != nil { subCfgValue.FieldByName("Active").Set( reflect.ValueOf(towerClient != nil), ) subCfgValue.FieldByName("Client").Set( reflect.ValueOf(towerClient), ) + subCfgValue.FieldByName("AnchorClient").Set( + reflect.ValueOf(anchorTowerClient), + ) } subCfgValue.FieldByName("Resolver").Set( reflect.ValueOf(tcpResolver), diff --git a/watchtower/blob/type.go b/watchtower/blob/type.go index f24b41f3..73d963c8 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -138,8 +138,9 @@ func (t Type) String() string { // supportedTypes is the set of all configurations known to be supported by the // package. var supportedTypes = map[Type]struct{}{ - TypeAltruistCommit: {}, - TypeRewardCommit: {}, + TypeAltruistCommit: {}, + TypeRewardCommit: {}, + TypeAltruistAnchorCommit: {}, } // IsSupportedType returns true if the given type is supported by the package. diff --git a/watchtower/blob/type_test.go b/watchtower/blob/type_test.go index a88965b2..026263df 100644 --- a/watchtower/blob/type_test.go +++ b/watchtower/blob/type_test.go @@ -123,6 +123,12 @@ func TestSupportedTypes(t *testing.T) { t.Fatalf("default type %s is not supported", blob.TypeAltruistCommit) } + // Assert that the altruist anchor commit types are supported. + if !blob.IsSupportedType(blob.TypeAltruistAnchorCommit) { + t.Fatalf("default type %s is not supported", + blob.TypeAltruistAnchorCommit) + } + // Assert that all claimed supported types are actually supported. for _, supType := range blob.SupportedTypes() { if blob.IsSupportedType(supType) { diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go index 9994f7d0..adc000a8 100644 --- a/watchtower/wtclient/backup_task.go +++ b/watchtower/wtclient/backup_task.go @@ -1,12 +1,15 @@ package wtclient import ( + "fmt" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/txsort" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -36,6 +39,7 @@ import ( type backupTask struct { id wtdb.BackupID breachInfo *lnwallet.BreachRetribution + chanType channeldb.ChannelType // state-dependent variables @@ -54,7 +58,7 @@ type backupTask struct { // variables. func newBackupTask(chanID *lnwire.ChannelID, breachInfo *lnwallet.BreachRetribution, - sweepPkScript []byte, isTweakless bool) *backupTask { + sweepPkScript []byte, chanType channeldb.ChannelType) *backupTask { // Parse the non-dust outputs from the breach transaction, // simultaneously computing the total amount contained in the inputs @@ -85,17 +89,35 @@ func newBackupTask(chanID *lnwire.ChannelID, totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value } if breachInfo.LocalOutputSignDesc != nil { - witnessType := input.CommitmentNoDelay - if isTweakless { + var witnessType input.WitnessType + switch { + case chanType.HasAnchors(): + witnessType = input.CommitmentToRemoteConfirmed + case chanType.IsTweakless(): witnessType = input.CommitSpendNoDelayTweakless + default: + witnessType = input.CommitmentNoDelay } - toRemoteInput = input.NewBaseInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, - ) + // Anchor channels have a CSV-encumbered to-remote output. We'll + // construct a CSV input in that case and assign the proper CSV + // delay of 1, otherwise we fallback to the a regular P2WKH + // to-remote output for tweaked or tweakless channels. + if chanType.HasAnchors() { + toRemoteInput = input.NewCsvInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, 1, + ) + } else { + toRemoteInput = input.NewBaseInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, + ) + } totalAmt += breachInfo.LocalOutputSignDesc.Output.Value } @@ -106,6 +128,7 @@ func newBackupTask(chanID *lnwire.ChannelID, CommitHeight: breachInfo.RevokedStateNum, }, breachInfo: breachInfo, + chanType: chanType, toLocalInput: toLocalInput, toRemoteInput: toRemoteInput, totalAmt: btcutil.Amount(totalAmt), @@ -145,13 +168,28 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody) error { // underestimate the size by one byte. The diferrence in weight // can cause different output values on the sweep transaction, // so we mimic the original bug and create signatures using the - // original weight estimate. - weightEstimate.AddWitnessInput( - input.ToLocalPenaltyWitnessSize - 1, - ) + // original weight estimate. For anchor channels we'll go ahead + // an use the correct penalty witness when signing our justice + // transactions. + if t.chanType.HasAnchors() { + weightEstimate.AddWitnessInput( + input.ToLocalPenaltyWitnessSize, + ) + } else { + weightEstimate.AddWitnessInput( + input.ToLocalPenaltyWitnessSize - 1, + ) + } } if t.toRemoteInput != nil { - weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + // Legacy channels (both tweaked and non-tweaked) spend from + // P2WKH output. Anchor channels spend a to-remote confirmed + // P2WSH output. + if t.chanType.HasAnchors() { + weightEstimate.AddWitnessInput(input.ToRemoteConfirmedWitnessSize) + } else { + weightEstimate.AddWitnessInput(input.P2WKHWitnessSize) + } } // All justice transactions have a p2wkh output paying to the victim. @@ -163,6 +201,12 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody) error { weightEstimate.AddP2WKHOutput() } + if t.chanType.HasAnchors() != session.Policy.IsAnchorChannel() { + log.Criticalf("Invalid task (has_anchors=%t) for session "+ + "(has_anchors=%t)", t.chanType.HasAnchors(), + session.Policy.IsAnchorChannel()) + } + // Now, compute the output values depending on whether FlagReward is set // in the current session's policy. outputs, err := session.Policy.ComputeJusticeTxOuts( @@ -219,9 +263,10 @@ func (t *backupTask) craftSessionPayload( // information. This will either be contain both the to-local and // to-remote outputs, or only be the to-local output. inputs := t.inputs() - for prevOutPoint := range inputs { + for prevOutPoint, input := range inputs { justiceTxn.AddTxIn(&wire.TxIn{ PreviousOutPoint: prevOutPoint, + Sequence: input.BlocksToMaturity(), }) } @@ -288,7 +333,12 @@ func (t *backupTask) craftSessionPayload( case input.CommitSpendNoDelayTweakless: fallthrough case input.CommitmentNoDelay: + fallthrough + case input.CommitmentToRemoteConfirmed: copy(justiceKit.CommitToRemoteSig[:], signature[:]) + default: + return hint, nil, fmt.Errorf("invalid witness type: %v", + inp.WitnessType()) } } diff --git a/watchtower/wtclient/backup_task_internal_test.go b/watchtower/wtclient/backup_task_internal_test.go index 3c8ed17f..db85472d 100644 --- a/watchtower/wtclient/backup_task_internal_test.go +++ b/watchtower/wtclient/backup_task_internal_test.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -74,7 +75,7 @@ type backupTaskTest struct { bindErr error expSweepScript []byte signer input.Signer - tweakless bool + chanType channeldb.ChannelType } // genTaskTest creates a instance of a backupTaskTest using the passed @@ -92,7 +93,13 @@ func genTaskTest( expSweepAmt int64, expRewardAmt int64, bindErr error, - tweakless bool) backupTaskTest { + chanType channeldb.ChannelType) backupTaskTest { + + // Set the anchor flag in the blob type if the session needs to support + // anchor channels. + if chanType.HasAnchors() { + blobType |= blob.Type(blob.FlagAnchorChannel) + } // Parse the key pairs for all keys used in the test. revSK, revPK := btcec.PrivKeyFromBytes( @@ -192,17 +199,31 @@ func genTaskTest( Index: index, } - witnessType := input.CommitmentNoDelay - if tweakless { + var witnessType input.WitnessType + switch { + case chanType.HasAnchors(): + witnessType = input.CommitmentToRemoteConfirmed + case chanType.IsTweakless(): witnessType = input.CommitSpendNoDelayTweakless + default: + witnessType = input.CommitmentNoDelay } - toRemoteInput = input.NewBaseInput( - &breachInfo.LocalOutpoint, - witnessType, - breachInfo.LocalOutputSignDesc, - 0, - ) + if chanType.HasAnchors() { + toRemoteInput = input.NewCsvInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, 1, + ) + } else { + toRemoteInput = input.NewBaseInput( + &breachInfo.LocalOutpoint, + witnessType, + breachInfo.LocalOutputSignDesc, + 0, + ) + } } return backupTaskTest{ @@ -227,7 +248,7 @@ func genTaskTest( bindErr: bindErr, expSweepScript: makeAddrSlice(22), signer: signer, - tweakless: tweakless, + chanType: chanType, } } @@ -253,60 +274,97 @@ var ( func TestBackupTask(t *testing.T) { t.Parallel() + chanTypes := []channeldb.ChannelType{ + channeldb.SingleFunderBit, + channeldb.SingleFunderTweaklessBit, + channeldb.AnchorOutputsBit, + } + var backupTaskTests []backupTaskTest - for _, tweakless := range []bool{true, false} { + for _, chanType := range chanTypes { + // Depending on whether the test is for anchor channels or + // legacy (tweaked and non-tweaked) channels, adjust the + // expected sweep amount to accommodate. These are different for + // several reasons: + // - anchor to-remote outputs require a P2WSH sweep rather + // than a P2WKH sweep. + // - the to-local weight estimate fixes an off-by-one. + // In tests related to the dust threshold, the size difference + // between the channel types makes it so that the threshold fee + // rate is slightly lower (since the transactions are heavier). + var ( + expSweepCommitNoRewardBoth int64 = 299241 + expSweepCommitNoRewardLocal int64 = 199514 + expSweepCommitNoRewardRemote int64 = 99561 + expSweepCommitRewardBoth int64 = 296117 + expSweepCommitRewardLocal int64 = 197390 + expSweepCommitRewardRemote int64 = 98437 + sweepFeeRateNoRewardRemoteDust chainfee.SatPerKWeight = 227500 + sweepFeeRateRewardRemoteDust chainfee.SatPerKWeight = 175000 + ) + if chanType.HasAnchors() { + expSweepCommitNoRewardBoth = 299236 + expSweepCommitNoRewardLocal = 199513 + expSweepCommitNoRewardRemote = 99557 + expSweepCommitRewardBoth = 296112 + expSweepCommitRewardLocal = 197389 + expSweepCommitRewardRemote = 98433 + sweepFeeRateNoRewardRemoteDust = 225000 + sweepFeeRateRewardRemoteDust = 173750 + } + backupTaskTests = append(backupTaskTests, []backupTaskTest{ genTaskTest( "commit no-reward, both outputs", - 100, // stateNum - 200000, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 1000, // sweepFeeRate - nil, // rewardScript - 299241, // expSweepAmt - 0, // expRewardAmt - nil, // bindErr - tweakless, + 100, // stateNum + 200000, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitNoReward, // blobType + 1000, // sweepFeeRate + nil, // rewardScript + expSweepCommitNoRewardBoth, // expSweepAmt + 0, // expRewardAmt + nil, // bindErr + chanType, ), genTaskTest( "commit no-reward, to-local output only", - 1000, // stateNum - 200000, // toLocalAmt - 0, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 1000, // sweepFeeRate - nil, // rewardScript - 199514, // expSweepAmt - 0, // expRewardAmt - nil, // bindErr - tweakless, + 1000, // stateNum + 200000, // toLocalAmt + 0, // toRemoteAmt + blobTypeCommitNoReward, // blobType + 1000, // sweepFeeRate + nil, // rewardScript + expSweepCommitNoRewardLocal, // expSweepAmt + 0, // expRewardAmt + nil, // bindErr + chanType, ), genTaskTest( "commit no-reward, to-remote output only", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 1000, // sweepFeeRate - nil, // rewardScript - 99561, // expSweepAmt - 0, // expRewardAmt - nil, // bindErr - tweakless, + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitNoReward, // blobType + 1000, // sweepFeeRate + nil, // rewardScript + expSweepCommitNoRewardRemote, // expSweepAmt + 0, // expRewardAmt + nil, // bindErr + chanType, ), genTaskTest( "commit no-reward, to-remote output only, creates dust", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitNoReward, // blobType - 227500, // sweepFeeRate - nil, // rewardScript - 0, // expSweepAmt - 0, // expRewardAmt - wtpolicy.ErrCreatesDust, // bindErr - tweakless, + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitNoReward, // blobType + sweepFeeRateNoRewardRemoteDust, // sweepFeeRate + nil, // rewardScript + 0, // expSweepAmt + 0, // expRewardAmt + wtpolicy.ErrCreatesDust, // bindErr + chanType, ), genTaskTest( "commit no-reward, no outputs, fee rate exceeds inputs", @@ -319,7 +377,7 @@ func TestBackupTask(t *testing.T) { 0, // expSweepAmt 0, // expRewardAmt wtpolicy.ErrFeeExceedsInputs, // bindErr - tweakless, + chanType, ), genTaskTest( "commit no-reward, no outputs, fee rate of 0 creates dust", @@ -332,59 +390,59 @@ func TestBackupTask(t *testing.T) { 0, // expSweepAmt 0, // expRewardAmt wtpolicy.ErrCreatesDust, // bindErr - tweakless, + chanType, ), genTaskTest( "commit reward, both outputs", - 100, // stateNum - 200000, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitReward, // blobType - 1000, // sweepFeeRate - addrScript, // rewardScript - 296117, // expSweepAmt - 3000, // expRewardAmt - nil, // bindErr - tweakless, + 100, // stateNum + 200000, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitReward, // blobType + 1000, // sweepFeeRate + addrScript, // rewardScript + expSweepCommitRewardBoth, // expSweepAmt + 3000, // expRewardAmt + nil, // bindErr + chanType, ), genTaskTest( "commit reward, to-local output only", - 1000, // stateNum - 200000, // toLocalAmt - 0, // toRemoteAmt - blobTypeCommitReward, // blobType - 1000, // sweepFeeRate - addrScript, // rewardScript - 197390, // expSweepAmt - 2000, // expRewardAmt - nil, // bindErr - tweakless, + 1000, // stateNum + 200000, // toLocalAmt + 0, // toRemoteAmt + blobTypeCommitReward, // blobType + 1000, // sweepFeeRate + addrScript, // rewardScript + expSweepCommitRewardLocal, // expSweepAmt + 2000, // expRewardAmt + nil, // bindErr + chanType, ), genTaskTest( "commit reward, to-remote output only", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitReward, // blobType - 1000, // sweepFeeRate - addrScript, // rewardScript - 98437, // expSweepAmt - 1000, // expRewardAmt - nil, // bindErr - tweakless, + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitReward, // blobType + 1000, // sweepFeeRate + addrScript, // rewardScript + expSweepCommitRewardRemote, // expSweepAmt + 1000, // expRewardAmt + nil, // bindErr + chanType, ), genTaskTest( "commit reward, to-remote output only, creates dust", - 1, // stateNum - 0, // toLocalAmt - 100000, // toRemoteAmt - blobTypeCommitReward, // blobType - 175000, // sweepFeeRate - addrScript, // rewardScript - 0, // expSweepAmt - 0, // expRewardAmt - wtpolicy.ErrCreatesDust, // bindErr - tweakless, + 1, // stateNum + 0, // toLocalAmt + 100000, // toRemoteAmt + blobTypeCommitReward, // blobType + sweepFeeRateRewardRemoteDust, // sweepFeeRate + addrScript, // rewardScript + 0, // expSweepAmt + 0, // expRewardAmt + wtpolicy.ErrCreatesDust, // bindErr + chanType, ), genTaskTest( "commit reward, no outputs, fee rate exceeds inputs", @@ -397,7 +455,7 @@ func TestBackupTask(t *testing.T) { 0, // expSweepAmt 0, // expRewardAmt wtpolicy.ErrFeeExceedsInputs, // bindErr - tweakless, + chanType, ), genTaskTest( "commit reward, no outputs, fee rate of 0 creates dust", @@ -410,7 +468,7 @@ func TestBackupTask(t *testing.T) { 0, // expSweepAmt 0, // expRewardAmt wtpolicy.ErrCreatesDust, // bindErr - tweakless, + chanType, ), }...) } @@ -430,7 +488,7 @@ func testBackupTask(t *testing.T, test backupTaskTest) { // Create a new backupTask from the channel id and breach info. task := newBackupTask( &test.chanID, test.breachInfo, test.expSweepScript, - test.tweakless, + test.chanType, ) // Assert that all parameters set during initialization are properly diff --git a/watchtower/wtclient/client.go b/watchtower/wtclient/client.go index 62e4ac67..239b29b3 100644 --- a/watchtower/wtclient/client.go +++ b/watchtower/wtclient/client.go @@ -10,6 +10,9 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -40,13 +43,14 @@ const ( DefaultForceQuitDelay = 10 * time.Second ) -var ( - // activeSessionFilter is a filter that ignored any sessions which are - // not active. - activeSessionFilter = func(s *wtdb.ClientSession) bool { - return s.Status == wtdb.CSessionActive +// genActiveSessionFilter generates a filter that selects active sessions that +// also match the desired channel type, either legacy or anchor. +func genActiveSessionFilter(anchor bool) func(*wtdb.ClientSession) bool { + return func(s *wtdb.ClientSession) bool { + return s.Status == wtdb.CSessionActive && + anchor == s.Policy.IsAnchorChannel() } -) +} // RegisteredTower encompasses information about a registered watchtower with // the client. @@ -103,7 +107,8 @@ type Client interface { // negotiated policy. If the channel we're trying to back up doesn't // have a tweak for the remote party's output, then isTweakless should // be true. - BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution, bool) error + BackupState(*lnwire.ChannelID, *lnwallet.BreachRetribution, + channeldb.ChannelType) error // Start initializes the watchtower client, allowing it process requests // to backup revoked channel states. @@ -228,6 +233,8 @@ type TowerClient struct { cfg *Config + log btclog.Logger + pipeline *taskPipeline negotiator SessionNegotiator @@ -274,10 +281,18 @@ func New(config *Config) (*TowerClient, error) { cfg.WriteTimeout = DefaultWriteTimeout } + prefix := "(legacy)" + if cfg.Policy.IsAnchorChannel() { + prefix = "(anchor)" + } + plog := build.NewPrefixLog(prefix, log) + // Next, load all candidate sessions and towers from the database into // the client. We will use any of these session if their policies match // the current policy of the client, otherwise they will be ignored and // new sessions will be requested. + isAnchorClient := cfg.Policy.IsAnchorChannel() + activeSessionFilter := genActiveSessionFilter(isAnchorClient) candidateSessions, err := getClientSessions( cfg.DB, cfg.SecretKeyRing, nil, activeSessionFilter, ) @@ -287,7 +302,7 @@ func New(config *Config) (*TowerClient, error) { var candidateTowers []*wtdb.Tower for _, s := range candidateSessions { - log.Infof("Using private watchtower %s, offering policy %s", + plog.Infof("Using private watchtower %s, offering policy %s", s.Tower, cfg.Policy) candidateTowers = append(candidateTowers, s.Tower) } @@ -301,6 +316,7 @@ func New(config *Config) (*TowerClient, error) { c := &TowerClient{ cfg: cfg, + log: plog, pipeline: newTaskPipeline(), candidateTowers: newTowerListIterator(candidateTowers...), candidateSessions: candidateSessions, @@ -422,7 +438,7 @@ func (c *TowerClient) buildHighestCommitHeights() { func (c *TowerClient) Start() error { var err error c.started.Do(func() { - log.Infof("Starting watchtower client") + c.log.Infof("Starting watchtower client") // First, restart a session queue for any sessions that have // committed but unacked state updates. This ensures that these @@ -430,7 +446,7 @@ func (c *TowerClient) Start() error { // restart. for _, session := range c.candidateSessions { if len(session.CommittedUpdates) > 0 { - log.Infof("Starting session=%s to process "+ + c.log.Infof("Starting session=%s to process "+ "%d committed backups", session.ID, len(session.CommittedUpdates)) c.initActiveQueue(session) @@ -460,7 +476,7 @@ func (c *TowerClient) Start() error { // Stop idempotently initiates a graceful shutdown of the watchtower client. func (c *TowerClient) Stop() error { c.stopped.Do(func() { - log.Debugf("Stopping watchtower client") + c.log.Debugf("Stopping watchtower client") // 1. To ensure we don't hang forever on shutdown due to // unintended failures, we'll delay a call to force quit the @@ -493,7 +509,7 @@ func (c *TowerClient) Stop() error { // queues, we no longer need to negotiate sessions. c.negotiator.Stop() - log.Debugf("Waiting for active session queues to finish "+ + c.log.Debugf("Waiting for active session queues to finish "+ "draining, stats: %s", c.stats) // 5. Shutdown all active session queues in parallel. These will @@ -509,7 +525,7 @@ func (c *TowerClient) Stop() error { default: } - log.Debugf("Client successfully stopped, stats: %s", c.stats) + c.log.Debugf("Client successfully stopped, stats: %s", c.stats) }) return nil } @@ -518,7 +534,7 @@ func (c *TowerClient) Stop() error { // client. This should only be executed if Stop is unable to exit cleanly. func (c *TowerClient) ForceQuit() { c.forced.Do(func() { - log.Infof("Force quitting watchtower client") + c.log.Infof("Force quitting watchtower client") // 1. Shutdown the backup queue, which will prevent any further // updates from being accepted. In practice, the links should be @@ -543,7 +559,7 @@ func (c *TowerClient) ForceQuit() { return s.ForceQuit }) - log.Infof("Watchtower client unclean shutdown complete, "+ + c.log.Infof("Watchtower client unclean shutdown complete, "+ "stats: %s", c.stats) }) } @@ -592,7 +608,8 @@ func (c *TowerClient) RegisterChannel(chanID lnwire.ChannelID) error { // - breached outputs contain too little value to sweep at the target sweep fee // rate. func (c *TowerClient) BackupState(chanID *lnwire.ChannelID, - breachInfo *lnwallet.BreachRetribution, isTweakless bool) error { + breachInfo *lnwallet.BreachRetribution, + chanType channeldb.ChannelType) error { // Retrieve the cached sweep pkscript used for this channel. c.backupMu.Lock() @@ -606,7 +623,7 @@ func (c *TowerClient) BackupState(chanID *lnwire.ChannelID, height, ok := c.chanCommitHeights[*chanID] if ok && breachInfo.RevokedStateNum <= height { c.backupMu.Unlock() - log.Debugf("Ignoring duplicate backup for chanid=%v at height=%d", + c.log.Debugf("Ignoring duplicate backup for chanid=%v at height=%d", chanID, breachInfo.RevokedStateNum) return nil } @@ -618,7 +635,7 @@ func (c *TowerClient) BackupState(chanID *lnwire.ChannelID, c.backupMu.Unlock() task := newBackupTask( - chanID, breachInfo, summary.SweepPkScript, isTweakless, + chanID, breachInfo, summary.SweepPkScript, chanType, ) return c.pipeline.QueueBackupTask(task) @@ -669,15 +686,15 @@ func (c *TowerClient) nextSessionQueue() *sessionQueue { func (c *TowerClient) backupDispatcher() { defer c.wg.Done() - log.Tracef("Starting backup dispatcher") - defer log.Tracef("Stopping backup dispatcher") + c.log.Tracef("Starting backup dispatcher") + defer c.log.Tracef("Stopping backup dispatcher") for { switch { // No active session queue and no additional sessions. case c.sessionQueue == nil && len(c.candidateSessions) == 0: - log.Infof("Requesting new session.") + c.log.Infof("Requesting new session.") // Immediately request a new session. c.negotiator.RequestSession() @@ -688,7 +705,7 @@ func (c *TowerClient) backupDispatcher() { awaitSession: select { case session := <-c.negotiator.NewSessions(): - log.Infof("Acquired new session with id=%s", + c.log.Infof("Acquired new session with id=%s", session.ID) c.candidateSessions[session.ID] = session c.stats.sessionAcquired() @@ -698,7 +715,7 @@ func (c *TowerClient) backupDispatcher() { continue case <-c.statTicker.C: - log.Infof("Client stats: %s", c.stats) + c.log.Infof("Client stats: %s", c.stats) // A new tower has been requested to be added. We'll // update our persisted and in-memory state and consider @@ -732,7 +749,7 @@ func (c *TowerClient) backupDispatcher() { // backup tasks. c.sessionQueue = c.nextSessionQueue() if c.sessionQueue != nil { - log.Debugf("Loaded next candidate session "+ + c.log.Debugf("Loaded next candidate session "+ "queue id=%s", c.sessionQueue.ID()) } @@ -759,13 +776,13 @@ func (c *TowerClient) backupDispatcher() { // we can request new sessions before the session is // fully empty, which this case would handle. case session := <-c.negotiator.NewSessions(): - log.Warnf("Acquired new session with id=%s "+ + c.log.Warnf("Acquired new session with id=%s "+ "while processing tasks", session.ID) c.candidateSessions[session.ID] = session c.stats.sessionAcquired() case <-c.statTicker.C: - log.Infof("Client stats: %s", c.stats) + c.log.Infof("Client stats: %s", c.stats) // Process each backup task serially from the queue of // revoked states. @@ -776,7 +793,7 @@ func (c *TowerClient) backupDispatcher() { return } - log.Debugf("Processing %v", task.id) + c.log.Debugf("Processing %v", task.id) c.stats.taskReceived() c.processTask(task) @@ -821,7 +838,7 @@ func (c *TowerClient) processTask(task *backupTask) { // sessionQueue will be removed if accepting the task left the sessionQueue in // an exhausted state. func (c *TowerClient) taskAccepted(task *backupTask, newStatus reserveStatus) { - log.Infof("Queued %v successfully for session %v", + c.log.Infof("Queued %v successfully for session %v", task.id, c.sessionQueue.ID()) c.stats.taskAccepted() @@ -840,7 +857,7 @@ func (c *TowerClient) taskAccepted(task *backupTask, newStatus reserveStatus) { case reserveExhausted: c.stats.sessionExhausted() - log.Debugf("Session %s exhausted", c.sessionQueue.ID()) + c.log.Debugf("Session %s exhausted", c.sessionQueue.ID()) // This task left the session exhausted, set it to nil and // proceed to the next loop so we can consume another @@ -863,13 +880,13 @@ func (c *TowerClient) taskRejected(task *backupTask, curStatus reserveStatus) { case reserveAvailable: c.stats.taskIneligible() - log.Infof("Ignoring ineligible %v", task.id) + c.log.Infof("Ignoring ineligible %v", task.id) err := c.cfg.DB.MarkBackupIneligible( task.id.ChanID, task.id.CommitHeight, ) if err != nil { - log.Errorf("Unable to mark %v ineligible: %v", + c.log.Errorf("Unable to mark %v ineligible: %v", task.id, err) // It is safe to not handle this error, even if we could @@ -889,7 +906,7 @@ func (c *TowerClient) taskRejected(task *backupTask, curStatus reserveStatus) { case reserveExhausted: c.stats.sessionExhausted() - log.Debugf("Session %v exhausted, %v queued for next session", + c.log.Debugf("Session %v exhausted, %v queued for next session", c.sessionQueue.ID(), task.id) // Cache the task that we pulled off, so that we can process it @@ -918,7 +935,7 @@ func (c *TowerClient) readMessage(peer wtserver.Peer) (wtwire.Message, error) { err := peer.SetReadDeadline(time.Now().Add(c.cfg.ReadTimeout)) if err != nil { err = fmt.Errorf("unable to set read deadline: %v", err) - log.Errorf("Unable to read msg: %v", err) + c.log.Errorf("Unable to read msg: %v", err) return nil, err } @@ -926,7 +943,7 @@ func (c *TowerClient) readMessage(peer wtserver.Peer) (wtwire.Message, error) { rawMsg, err := peer.ReadNextMessage() if err != nil { err = fmt.Errorf("unable to read message: %v", err) - log.Errorf("Unable to read msg: %v", err) + c.log.Errorf("Unable to read msg: %v", err) return nil, err } @@ -936,11 +953,11 @@ func (c *TowerClient) readMessage(peer wtserver.Peer) (wtwire.Message, error) { msg, err := wtwire.ReadMessage(msgReader, 0) if err != nil { err = fmt.Errorf("unable to parse message: %v", err) - log.Errorf("Unable to read msg: %v", err) + c.log.Errorf("Unable to read msg: %v", err) return nil, err } - logMessage(peer, msg, true) + c.logMessage(peer, msg, true) return msg, nil } @@ -953,7 +970,7 @@ func (c *TowerClient) sendMessage(peer wtserver.Peer, msg wtwire.Message) error _, err := wtwire.WriteMessage(&b, msg, 0) if err != nil { err = fmt.Errorf("Unable to encode msg: %v", err) - log.Errorf("Unable to send msg: %v", err) + c.log.Errorf("Unable to send msg: %v", err) return err } @@ -962,16 +979,16 @@ func (c *TowerClient) sendMessage(peer wtserver.Peer, msg wtwire.Message) error err = peer.SetWriteDeadline(time.Now().Add(c.cfg.WriteTimeout)) if err != nil { err = fmt.Errorf("unable to set write deadline: %v", err) - log.Errorf("Unable to send msg: %v", err) + c.log.Errorf("Unable to send msg: %v", err) return err } - logMessage(peer, msg, false) + c.logMessage(peer, msg, false) // Write out the full message to the remote peer. _, err = peer.Write(b.Bytes()) if err != nil { - log.Errorf("Unable to send msg: %v", err) + c.log.Errorf("Unable to send msg: %v", err) } return err } @@ -1065,6 +1082,8 @@ func (c *TowerClient) handleNewTower(msg *newTowerMsg) error { c.candidateTowers.AddCandidate(tower) // Include all of its corresponding sessions to our set of candidates. + isAnchorClient := c.cfg.Policy.IsAnchorChannel() + activeSessionFilter := genActiveSessionFilter(isAnchorClient) sessions, err := getClientSessions( c.cfg.DB, c.cfg.SecretKeyRing, &tower.ID, activeSessionFilter, ) @@ -1232,7 +1251,9 @@ func (c *TowerClient) Policy() wtpolicy.Policy { // logMessage writes information about a message received from a remote peer, // using directional prepositions to signal whether the message was sent or // received. -func logMessage(peer wtserver.Peer, msg wtwire.Message, read bool) { +func (c *TowerClient) logMessage( + peer wtserver.Peer, msg wtwire.Message, read bool) { + var action = "Received" var preposition = "from" if !read { @@ -1245,7 +1266,7 @@ func logMessage(peer wtserver.Peer, msg wtwire.Message, read bool) { summary = "(" + summary + ")" } - log.Debugf("%s %s%v %s %x@%s", action, msg.MsgType(), summary, + c.log.Debugf("%s %s%v %s %x@%s", action, msg.MsgType(), summary, preposition, peer.RemotePub().SerializeCompressed(), peer.RemoteAddr()) } diff --git a/watchtower/wtclient/client_test.go b/watchtower/wtclient/client_test.go index 41e01b61..f20e5286 100644 --- a/watchtower/wtclient/client_test.go +++ b/watchtower/wtclient/client_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -633,7 +634,7 @@ func (h *testHarness) backupState(id, i uint64, expErr error) { _, retribution := h.channel(id).getState(i) chanID := chanIDFromInt(id) - err := h.client.BackupState(&chanID, retribution, false) + err := h.client.BackupState(&chanID, retribution, channeldb.SingleFunderBit) if err != expErr { h.t.Fatalf("back error mismatch, want: %v, got: %v", expErr, err) diff --git a/watchtower/wtclient/session_negotiator.go b/watchtower/wtclient/session_negotiator.go index c782deee..8ab4521f 100644 --- a/watchtower/wtclient/session_negotiator.go +++ b/watchtower/wtclient/session_negotiator.go @@ -112,8 +112,19 @@ var _ SessionNegotiator = (*sessionNegotiator)(nil) // newSessionNegotiator initializes a fresh sessionNegotiator instance. func newSessionNegotiator(cfg *NegotiatorConfig) *sessionNegotiator { + // Generate the set of features the negitator will present to the tower + // upon connection. For anchor channels, we'll conditionally signal that + // we require support for anchor channels depdening on the requested + // policy. + features := []lnwire.FeatureBit{ + wtwire.AltruistSessionsRequired, + } + if cfg.Policy.IsAnchorChannel() { + features = append(features, wtwire.AnchorCommitRequired) + } + localInit := wtwire.NewInitMessage( - lnwire.NewRawFeatureVector(wtwire.AltruistSessionsRequired), + lnwire.NewRawFeatureVector(features...), cfg.ChainHash, ) diff --git a/watchtower/wtpolicy/policy.go b/watchtower/wtpolicy/policy.go index 9c7f5e64..40eb001b 100644 --- a/watchtower/wtpolicy/policy.go +++ b/watchtower/wtpolicy/policy.go @@ -120,6 +120,11 @@ func (p Policy) String() string { p.SweepFeeRate) } +// IsAnchorChannel returns true if the session policy requires anchor channels. +func (p Policy) IsAnchorChannel() bool { + return p.TxPolicy.BlobType.IsAnchorChannel() +} + // Validate ensures that the policy satisfies some minimal correctness // constraints. func (p Policy) Validate() error { diff --git a/watchtower/wtpolicy/policy_test.go b/watchtower/wtpolicy/policy_test.go index 4182a0de..b73c4845 100644 --- a/watchtower/wtpolicy/policy_test.go +++ b/watchtower/wtpolicy/policy_test.go @@ -5,6 +5,7 @@ import ( "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/wtpolicy" + "github.com/stretchr/testify/require" ) var validationTests = []struct { @@ -91,3 +92,21 @@ func TestPolicyValidate(t *testing.T) { }) } } + +// TestPolicyIsAnchorChannel asserts that the IsAnchorChannel helper properly +// reflects the anchor bit of the policy's blob type. +func TestPolicyIsAnchorChannel(t *testing.T) { + policyNoAnchor := wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + }, + } + require.Equal(t, false, policyNoAnchor.IsAnchorChannel()) + + policyAnchor := wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistAnchorCommit, + }, + } + require.Equal(t, true, policyAnchor.IsAnchorChannel()) +} diff --git a/watchtower/wtserver/server.go b/watchtower/wtserver/server.go index faf39819..c46447b2 100644 --- a/watchtower/wtserver/server.go +++ b/watchtower/wtserver/server.go @@ -96,7 +96,10 @@ type Server struct { // sessions and send state updates. func New(cfg *Config) (*Server, error) { localInit := wtwire.NewInitMessage( - lnwire.NewRawFeatureVector(wtwire.AltruistSessionsOptional), + lnwire.NewRawFeatureVector( + wtwire.AltruistSessionsOptional, + wtwire.AnchorCommitOptional, + ), cfg.ChainHash, ) diff --git a/watchtower/wtserver/server_test.go b/watchtower/wtserver/server_test.go index 0bb5806d..5915d2d4 100644 --- a/watchtower/wtserver/server_test.go +++ b/watchtower/wtserver/server_test.go @@ -161,6 +161,28 @@ type createSessionTestCase struct { } var createSessionTests = []createSessionTestCase{ + { + name: "duplicate session create altruist anchor commit", + initMsg: wtwire.NewInitMessage( + lnwire.NewRawFeatureVector(), + testnetChainHash, + ), + createMsg: &wtwire.CreateSession{ + BlobType: blob.TypeAltruistAnchorCommit, + MaxUpdates: 1000, + RewardBase: 0, + RewardRate: 0, + SweepFeeRate: 10000, + }, + expReply: &wtwire.CreateSessionReply{ + Code: wtwire.CodeOK, + Data: []byte{}, + }, + expDupReply: &wtwire.CreateSessionReply{ + Code: wtwire.CodeOK, + Data: []byte{}, + }, + }, { name: "duplicate session create", initMsg: wtwire.NewInitMessage( diff --git a/watchtower/wtwire/features.go b/watchtower/wtwire/features.go index 7ba298e0..83ab207f 100644 --- a/watchtower/wtwire/features.go +++ b/watchtower/wtwire/features.go @@ -7,6 +7,8 @@ import "github.com/lightningnetwork/lnd/lnwire" var FeatureNames = map[lnwire.FeatureBit]string{ AltruistSessionsRequired: "altruist-sessions", AltruistSessionsOptional: "altruist-sessions", + AnchorCommitRequired: "anchor-commit", + AnchorCommitOptional: "anchor-commit", } const ( @@ -19,4 +21,13 @@ const ( // support a remote party who understand the protocol for creating and // updating watchtower sessions. AltruistSessionsOptional lnwire.FeatureBit = 1 + + // AnchorCommitRequired specifies that the advertising tower requires + // the remote party to negotiate sessions for protecting anchor + // channels. + AnchorCommitRequired lnwire.FeatureBit = 2 + + // AnchorCommitOptional specifies that the advertising tower allows the + // remote party to negotiate sessions for protecting anchor channels. + AnchorCommitOptional lnwire.FeatureBit = 3 )