Merge pull request #4782 from cfromknecht/anchor-wtserver

watchtower: anchor channel support
This commit is contained in:
Olaoluwa Osuntokun 2020-11-30 17:49:32 -08:00 committed by GitHub
commit 94f8311667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 703 additions and 241 deletions

@ -250,19 +250,44 @@ var policyCommand = cli.Command{
Name: "policy", Name: "policy",
Usage: "Display the active watchtower client policy configuration.", Usage: "Display the active watchtower client policy configuration.",
Action: actionDecorator(policy), 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 { func policy(ctx *cli.Context) error {
// Display the command's help message if the number of arguments/flags // Display the command's help message if the number of arguments/flags
// is not what we expect. // is not what we expect.
if ctx.NArg() > 0 || ctx.NumFlags() > 0 { if ctx.NArg() > 0 || ctx.NumFlags() > 1 {
return cli.ShowCommandHelp(ctx, "policy") 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) client, cleanUp := getWtclient(ctx)
defer cleanUp() defer cleanUp()
req := &wtclientrpc.PolicyRequest{} req := &wtclientrpc.PolicyRequest{
PolicyType: policyType,
}
resp, err := client.Policy(context.Background(), req) resp, err := client.Policy(context.Background(), req)
if err != nil { if err != nil {
return err return err

@ -183,7 +183,8 @@ type TowerClient interface {
// abide by the negotiated policy. If the channel we're trying to back // 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 // up doesn't have a tweak for the remote party's output, then
// isTweakless should be true. // 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 // InterceptableHtlcForwarder is the interface to set the interceptor

@ -256,7 +256,7 @@ type ChannelLinkConfig struct {
// TowerClient is an optional engine that manages the signing, // TowerClient is an optional engine that manages the signing,
// encrypting, and uploading of justice transactions to the daemon's // encrypting, and uploading of justice transactions to the daemon's
// configured set of watchtowers. // configured set of watchtowers for legacy channels.
TowerClient TowerClient TowerClient TowerClient
// MaxOutgoingCltvExpiry is the maximum outgoing timelock that the link // 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 // 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.
// TODO(halseth): support anchor types for watchtower. if l.cfg.TowerClient != nil {
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() {
err := l.cfg.TowerClient.RegisterChannel(l.ChanID()) err := l.cfg.TowerClient.RegisterChannel(l.ChanID())
if err != nil { if err != nil {
return err return err
@ -1835,14 +1830,9 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
return return
} }
// If we have a tower client, we'll proceed in backing up the // If we have a tower client for this channel type, we'll
// state that was just revoked. if l.cfg.TowerClient != nil {
// TODO(halseth): support anchor types for watchtower.
state := l.channel.State() 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() {
breachInfo, err := lnwallet.NewBreachRetribution( breachInfo, err := lnwallet.NewBreachRetribution(
state, state.RemoteCommitment.CommitHeight-1, 0, state, state.RemoteCommitment.CommitHeight-1, 0,
) )
@ -1852,10 +1842,9 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
return return
} }
chanType := l.channel.State().ChanType
chanID := l.ChanID() chanID := l.ChanID()
err = l.cfg.TowerClient.BackupState( err = l.cfg.TowerClient.BackupState(
&chanID, breachInfo, chanType.IsTweakless(), &chanID, breachInfo, state.ChanType,
) )
if err != nil { if err != nil {
l.fail(LinkFailureError{code: ErrInternalError}, l.fail(LinkFailureError{code: ErrInternalError},

@ -19,6 +19,10 @@ type Config struct {
// through the watchtower RPC subserver. // through the watchtower RPC subserver.
Client wtclient.Client 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 // 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
// non-clear networks, e.g. Tor, etc. // non-clear networks, e.g. Tor, etc.

@ -14,6 +14,8 @@ import (
"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/wtclient" "github.com/lightningnetwork/lnd/watchtower/wtclient"
"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"
) )
@ -176,9 +178,14 @@ func (c *WatchtowerClient) AddTower(ctx context.Context,
IdentityKey: pubKey, IdentityKey: pubKey,
Address: addr, Address: addr,
} }
// TODO(conner): make atomic via multiplexed client
if err := c.cfg.Client.AddTower(towerAddr); err != nil { if err := c.cfg.Client.AddTower(towerAddr); err != nil {
return nil, err return nil, err
} }
if err := c.cfg.AnchorClient.AddTower(towerAddr); err != nil {
return nil, err
}
return &AddTowerResponse{}, nil 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 return nil, err
} }
@ -226,11 +239,25 @@ func (c *WatchtowerClient) ListTowers(ctx context.Context,
return nil, err return nil, err
} }
towers, err := c.cfg.Client.RegisteredTowers() anchorTowers, err := c.cfg.AnchorClient.RegisteredTowers()
if err != nil { if err != nil {
return nil, err 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)) rpcTowers := make([]*Tower, 0, len(towers))
for _, tower := range towers { for _, tower := range towers {
rpcTower := marshallTower(tower, req.IncludeSessions) rpcTower := marshallTower(tower, req.IncludeSessions)
@ -253,7 +280,11 @@ func (c *WatchtowerClient) GetTowerInfo(ctx context.Context,
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -269,7 +300,24 @@ func (c *WatchtowerClient) Stats(ctx context.Context,
return nil, err 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{ return &StatsResponse{
NumBackups: uint32(stats.NumTasksAccepted), NumBackups: uint32(stats.NumTasksAccepted),
NumFailedBackups: uint32(stats.NumTasksIneligible), NumFailedBackups: uint32(stats.NumTasksIneligible),
@ -287,7 +335,17 @@ func (c *WatchtowerClient) Policy(ctx context.Context,
return nil, err 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{ return &PolicyResponse{
MaxUpdates: uint32(policy.MaxUpdates), MaxUpdates: uint32(policy.MaxUpdates),
SweepSatPerByte: uint32(policy.SweepFeeRate.FeePerKVByte() / 1000), SweepSatPerByte: uint32(policy.SweepFeeRate.FeePerKVByte() / 1000),

@ -24,6 +24,33 @@ var _ = math.Inf
// proto package needs to be updated. // proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 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 { type AddTowerRequest struct {
// The identifying public key of the watchtower to add. // The identifying public key of the watchtower to add.
Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"`
@ -579,6 +606,9 @@ func (m *StatsResponse) GetNumSessionsExhausted() uint32 {
} }
type PolicyRequest struct { type PolicyRequest struct {
//
//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_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -609,6 +639,13 @@ func (m *PolicyRequest) XXX_DiscardUnknown() {
var xxx_messageInfo_PolicyRequest proto.InternalMessageInfo var xxx_messageInfo_PolicyRequest proto.InternalMessageInfo
func (m *PolicyRequest) GetPolicyType() PolicyType {
if m != nil {
return m.PolicyType
}
return PolicyType_LEGACY
}
type PolicyResponse struct { type PolicyResponse struct {
// //
//The maximum number of updates each session we negotiate with watchtowers //The maximum number of updates each session we negotiate with watchtowers
@ -663,6 +700,7 @@ func (m *PolicyResponse) GetSweepSatPerByte() uint32 {
} }
func init() { func init() {
proto.RegisterEnum("wtclientrpc.PolicyType", PolicyType_name, PolicyType_value)
proto.RegisterType((*AddTowerRequest)(nil), "wtclientrpc.AddTowerRequest") proto.RegisterType((*AddTowerRequest)(nil), "wtclientrpc.AddTowerRequest")
proto.RegisterType((*AddTowerResponse)(nil), "wtclientrpc.AddTowerResponse") proto.RegisterType((*AddTowerResponse)(nil), "wtclientrpc.AddTowerResponse")
proto.RegisterType((*RemoveTowerRequest)(nil), "wtclientrpc.RemoveTowerRequest") proto.RegisterType((*RemoveTowerRequest)(nil), "wtclientrpc.RemoveTowerRequest")
@ -681,50 +719,54 @@ func init() {
func init() { proto.RegisterFile("wtclientrpc/wtclient.proto", fileDescriptor_b5f4e7d95a641af2) } func init() { proto.RegisterFile("wtclientrpc/wtclient.proto", fileDescriptor_b5f4e7d95a641af2) }
var fileDescriptor_b5f4e7d95a641af2 = []byte{ var fileDescriptor_b5f4e7d95a641af2 = []byte{
// 682 bytes of a gzipped FileDescriptorProto // 739 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0x4d, 0x6f, 0xd3, 0x40, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xcd, 0x6e, 0xd3, 0x4a,
0x10, 0x95, 0x1b, 0x12, 0xd2, 0x49, 0xda, 0xa4, 0x1b, 0x5a, 0x19, 0x53, 0x48, 0xf0, 0x29, 0x7c, 0x14, 0xbe, 0x6e, 0x6e, 0x72, 0xd3, 0x93, 0xb4, 0x4d, 0x27, 0xb7, 0xbd, 0xb9, 0xa6, 0x90, 0x60,
0x28, 0x11, 0x2d, 0x48, 0x9c, 0x2a, 0xda, 0x42, 0x2b, 0x24, 0x90, 0x22, 0x17, 0x04, 0xe2, 0x80, 0xb1, 0x08, 0x05, 0x25, 0x22, 0x05, 0xa9, 0xab, 0x8a, 0x34, 0xb4, 0xa5, 0x52, 0x81, 0xc8, 0x2d,
0xb5, 0xb1, 0xb7, 0x89, 0x55, 0x7b, 0xed, 0x7a, 0xd7, 0x4d, 0xf2, 0xa3, 0xf8, 0x19, 0xfc, 0x00, 0xe2, 0x67, 0x81, 0x35, 0xb1, 0xa7, 0x89, 0x55, 0x7b, 0xec, 0xda, 0xe3, 0x26, 0x79, 0x28, 0x1e,
0xfe, 0x0d, 0x47, 0xe4, 0xf5, 0xda, 0xb1, 0x1b, 0x47, 0x1c, 0xe0, 0x16, 0xcf, 0x7b, 0xfb, 0x3c, 0x83, 0x07, 0xe0, 0x6d, 0x58, 0x22, 0x8f, 0xc7, 0x8e, 0xdd, 0x3a, 0x62, 0x01, 0x3b, 0xfb, 0x7c,
0x7e, 0xf3, 0x32, 0x0b, 0xda, 0x8c, 0x5b, 0xae, 0x43, 0x28, 0x0f, 0x03, 0x6b, 0x98, 0xfe, 0x1e, 0xdf, 0x7c, 0x3e, 0xf3, 0x9d, 0x1f, 0x83, 0x3c, 0x65, 0xba, 0x65, 0x12, 0xca, 0x3c, 0x57, 0xef,
0x04, 0xa1, 0xcf, 0x7d, 0xd4, 0xc8, 0x61, 0xfa, 0x29, 0xb4, 0x8e, 0x6d, 0xfb, 0x93, 0x3f, 0x23, 0xc6, 0xcf, 0x1d, 0xd7, 0x73, 0x98, 0x83, 0x2a, 0x29, 0x4c, 0x19, 0xc0, 0x46, 0xdf, 0x30, 0x2e,
0xa1, 0x41, 0xae, 0x23, 0xc2, 0x38, 0xda, 0x83, 0x5a, 0x10, 0x8d, 0xaf, 0xc8, 0x42, 0x55, 0x7a, 0x9c, 0x29, 0xf1, 0x54, 0x72, 0x1d, 0x10, 0x9f, 0xa1, 0x6d, 0x28, 0xb9, 0xc1, 0xe8, 0x8a, 0xcc,
0x4a, 0xbf, 0x69, 0xc8, 0x27, 0xa4, 0xc2, 0x5d, 0x6c, 0xdb, 0x21, 0x61, 0x4c, 0xdd, 0xe8, 0x29, 0x1b, 0x52, 0x4b, 0x6a, 0x57, 0x55, 0xf1, 0x86, 0x1a, 0xf0, 0x0f, 0x36, 0x0c, 0x8f, 0xf8, 0x7e,
0xfd, 0x4d, 0x23, 0x7d, 0xd4, 0x11, 0xb4, 0x97, 0x22, 0x2c, 0xf0, 0x29, 0x23, 0xfa, 0x19, 0x20, 0x63, 0xa5, 0x25, 0xb5, 0x57, 0xd5, 0xf8, 0x55, 0x41, 0x50, 0x5b, 0x88, 0xf8, 0xae, 0x43, 0x7d,
0x83, 0x78, 0xfe, 0x0d, 0xf9, 0x47, 0xed, 0x5d, 0xe8, 0x14, 0x74, 0xa4, 0xfc, 0x57, 0xe8, 0x9c, 0xa2, 0x1c, 0x03, 0x52, 0x89, 0xed, 0xdc, 0x90, 0xdf, 0xd4, 0xde, 0x82, 0x7a, 0x46, 0x47, 0xc8,
0x13, 0x2e, 0x6a, 0xef, 0xe9, 0xa5, 0xff, 0x37, 0xfd, 0x27, 0xd0, 0x76, 0xa8, 0xe5, 0x46, 0x36, 0x7f, 0x84, 0xfa, 0x09, 0x61, 0x3c, 0x76, 0x4a, 0x2f, 0x9d, 0x5f, 0xe9, 0x3f, 0x86, 0x9a, 0x49,
0x31, 0x19, 0x61, 0xcc, 0xf1, 0x69, 0xf2, 0xa2, 0xba, 0xd1, 0x92, 0xf5, 0x0b, 0x59, 0xd6, 0x7f, 0x75, 0x2b, 0x30, 0x88, 0xe6, 0x13, 0xdf, 0x37, 0x1d, 0x1a, 0x7d, 0xa8, 0xac, 0x6e, 0x88, 0xf8,
0x28, 0xd0, 0x14, 0xba, 0xb2, 0x82, 0xba, 0xd0, 0xa0, 0x91, 0x67, 0x8e, 0xb1, 0x75, 0x15, 0x05, 0xb9, 0x08, 0x2b, 0x5f, 0x25, 0xa8, 0x72, 0x5d, 0x11, 0x41, 0x4d, 0xa8, 0xd0, 0xc0, 0xd6, 0x46,
0x4c, 0x08, 0x6f, 0x19, 0x40, 0x23, 0xef, 0x24, 0xa9, 0xa0, 0x01, 0x74, 0x62, 0x42, 0x40, 0xa8, 0x58, 0xbf, 0x0a, 0x5c, 0x9f, 0x0b, 0xaf, 0xa9, 0x40, 0x03, 0xfb, 0x30, 0x8a, 0xa0, 0x0e, 0xd4,
0xed, 0xd0, 0x49, 0x46, 0xdc, 0x10, 0xc4, 0x1d, 0x1a, 0x79, 0xa3, 0x04, 0x49, 0xf9, 0x5d, 0x68, 0x43, 0x82, 0x4b, 0xa8, 0x61, 0xd2, 0x71, 0x42, 0x5c, 0xe1, 0xc4, 0x4d, 0x1a, 0xd8, 0xc3, 0x08,
0x78, 0x78, 0x9e, 0xf1, 0x2a, 0x89, 0xa0, 0x87, 0xe7, 0x29, 0xe1, 0x19, 0x20, 0x36, 0x23, 0x24, 0x89, 0xf9, 0x4d, 0xa8, 0xd8, 0x78, 0x96, 0xf0, 0x0a, 0x91, 0xa0, 0x8d, 0x67, 0x31, 0xe1, 0x09,
0x30, 0x19, 0xe6, 0x66, 0x40, 0x42, 0x73, 0xbc, 0xe0, 0x44, 0xbd, 0x23, 0x78, 0x2d, 0x81, 0x5c, 0x20, 0x7f, 0x4a, 0x88, 0xab, 0xf9, 0x98, 0x69, 0x2e, 0xf1, 0xb4, 0xd1, 0x9c, 0x91, 0xc6, 0xdf,
0x60, 0x3e, 0x22, 0xe1, 0xc9, 0x82, 0x13, 0xfd, 0x97, 0x02, 0x55, 0xd1, 0xef, 0xda, 0x8f, 0xdf, 0x9c, 0xb7, 0xc1, 0x91, 0x73, 0xcc, 0x86, 0xc4, 0x3b, 0x9c, 0x33, 0xa2, 0x7c, 0x97, 0xa0, 0xc8,
0x87, 0x4d, 0xe9, 0x26, 0x89, 0xbb, 0xaa, 0xf4, 0x37, 0x8d, 0x65, 0x01, 0xbd, 0x06, 0x15, 0x5b, 0xf3, 0x5d, 0x7a, 0xf9, 0x1d, 0x58, 0x15, 0x6e, 0x92, 0x30, 0xab, 0x42, 0x7b, 0x55, 0x5d, 0x04,
0xdc, 0xb9, 0xc9, 0x9c, 0x31, 0x2d, 0x4c, 0x6d, 0xc7, 0xc6, 0x9c, 0x88, 0xd6, 0xea, 0xc6, 0x5e, 0xd0, 0x3e, 0x34, 0xb0, 0xce, 0xcc, 0x9b, 0xc4, 0x19, 0x4d, 0xc7, 0xd4, 0x30, 0x0d, 0xcc, 0x08,
0x82, 0x4b, 0x3f, 0x4e, 0x53, 0x14, 0x3d, 0x86, 0x66, 0xfc, 0xdd, 0x99, 0xa1, 0x49, 0x83, 0xb1, 0x4f, 0xad, 0xac, 0x6e, 0x47, 0xb8, 0xf0, 0x63, 0x10, 0xa3, 0xe8, 0x21, 0x54, 0xc3, 0x7b, 0x27,
0x59, 0xa9, 0x99, 0xe8, 0x15, 0xd4, 0x33, 0xb8, 0xda, 0xab, 0xf4, 0x1b, 0x07, 0xf7, 0x07, 0xb9, 0x86, 0x46, 0x09, 0x86, 0x66, 0xc5, 0x66, 0xa2, 0x17, 0x50, 0x4e, 0xe0, 0x62, 0xab, 0xd0, 0xae,
0xf8, 0x0d, 0xf2, 0x46, 0x1b, 0x19, 0x55, 0x3f, 0x82, 0x9d, 0x0f, 0x0e, 0x4b, 0xc6, 0xcb, 0xd2, 0xf4, 0xfe, 0xef, 0xa4, 0xda, 0xaf, 0x93, 0x36, 0x5a, 0x4d, 0xa8, 0xca, 0x01, 0x6c, 0x9e, 0x99,
0xd9, 0x96, 0xcd, 0x50, 0x29, 0x9f, 0xe1, 0x1b, 0x40, 0xf9, 0xf3, 0x49, 0x66, 0xd0, 0x53, 0xa8, 0x7e, 0x54, 0x5e, 0x3f, 0xae, 0x6d, 0x5e, 0x0d, 0xa5, 0xfc, 0x1a, 0xbe, 0x04, 0x94, 0x3e, 0x1f,
0x71, 0x51, 0x51, 0x15, 0xd1, 0x0a, 0x5a, 0x6d, 0xc5, 0x90, 0x0c, 0x7d, 0x1b, 0x9a, 0x17, 0x1c, 0xf5, 0x0c, 0xda, 0x85, 0x12, 0xe3, 0x91, 0x86, 0xc4, 0x53, 0x41, 0x77, 0x53, 0x51, 0x05, 0x43,
0xf3, 0xf4, 0xe5, 0xfa, 0x6f, 0x05, 0xb6, 0x64, 0x41, 0xaa, 0xfd, 0xf7, 0x58, 0x3c, 0x07, 0x14, 0x59, 0x87, 0xea, 0x39, 0xc3, 0x2c, 0xfe, 0xb8, 0xf2, 0x43, 0x82, 0x35, 0x11, 0x10, 0x6a, 0x7f,
0xf3, 0x2f, 0xb1, 0xe3, 0x12, 0xfb, 0x56, 0x3a, 0xda, 0x34, 0xf2, 0xce, 0x04, 0x90, 0xb2, 0x0f, 0xbc, 0x2d, 0x9e, 0x02, 0x0a, 0xf9, 0x97, 0xd8, 0xb4, 0x88, 0x71, 0xab, 0x3b, 0x6a, 0x34, 0xb0,
0x60, 0x37, 0x6f, 0xbe, 0x89, 0xad, 0xeb, 0xc8, 0x09, 0x89, 0x2d, 0xa7, 0xd0, 0xc9, 0x4d, 0xe1, 0x8f, 0x39, 0x10, 0xb3, 0x7b, 0xb0, 0x95, 0x36, 0x5f, 0xc3, 0xfa, 0x75, 0x60, 0x7a, 0xc4, 0x10,
0x58, 0x42, 0xe8, 0x25, 0xec, 0x15, 0xce, 0x90, 0xf9, 0x14, 0x47, 0x8c, 0x13, 0x5b, 0xad, 0x8a, 0x55, 0xa8, 0xa7, 0xaa, 0xd0, 0x17, 0x10, 0x7a, 0x0e, 0xdb, 0x99, 0x33, 0x64, 0x36, 0xc1, 0x81,
0x43, 0xf7, 0x72, 0x87, 0xde, 0xa5, 0x98, 0xde, 0x82, 0xad, 0x91, 0xef, 0x3a, 0xd6, 0x22, 0xf5, 0xcf, 0x88, 0xd1, 0x28, 0xf2, 0x43, 0xff, 0xa6, 0x0e, 0x1d, 0xc5, 0x98, 0x72, 0x0a, 0x6b, 0x43,
0xe2, 0x3b, 0x6c, 0xa7, 0x85, 0xa5, 0x17, 0x71, 0xa2, 0xa3, 0x20, 0xce, 0x45, 0xe6, 0x85, 0x87, 0xc7, 0x32, 0xf5, 0x79, 0x5c, 0x88, 0x7d, 0xa8, 0xb8, 0x3c, 0xa0, 0xb1, 0xb9, 0x4b, 0xf8, 0xcd,
0xe7, 0x9f, 0x93, 0xca, 0x9a, 0x44, 0x6f, 0x94, 0x26, 0xfa, 0xe0, 0x67, 0x05, 0xda, 0x5f, 0x30, 0xd7, 0x7b, 0xff, 0x65, 0xcc, 0x8c, 0x0e, 0x5c, 0xcc, 0x5d, 0xa2, 0x82, 0x9b, 0x3c, 0x2b, 0x5f,
0xb7, 0xa6, 0x62, 0x16, 0xa7, 0x62, 0x42, 0xe8, 0x1c, 0xea, 0xe9, 0x8e, 0x41, 0xfb, 0x85, 0xc1, 0x60, 0x3d, 0x96, 0x5a, 0xb8, 0x18, 0xce, 0x42, 0xe0, 0x86, 0x1d, 0x95, 0xb8, 0x68, 0xe3, 0xd9,
0xdd, 0xda, 0x5f, 0xda, 0xc3, 0x35, 0xa8, 0xec, 0x75, 0x04, 0x8d, 0xdc, 0x42, 0x41, 0xdd, 0x02, 0xfb, 0x28, 0xb2, 0x64, 0x16, 0x56, 0x72, 0x67, 0x61, 0xf7, 0x11, 0xc0, 0xe2, 0xcb, 0x08, 0xa0,
0x7b, 0x75, 0x65, 0x69, 0xbd, 0xf5, 0x04, 0xa9, 0xf8, 0x11, 0x60, 0x99, 0x36, 0xf4, 0xa8, 0xc0, 0x74, 0x76, 0x74, 0xd2, 0x1f, 0x7c, 0xaa, 0xfd, 0x15, 0x3e, 0xf7, 0xdf, 0x0e, 0x5e, 0xbf, 0x53,
0x5f, 0x89, 0xb1, 0xd6, 0x5d, 0x8b, 0x4b, 0xb9, 0xb7, 0xd0, 0xcc, 0xaf, 0x36, 0x54, 0x6c, 0xa0, 0x6b, 0x52, 0xef, 0x5b, 0x01, 0x6a, 0x1f, 0x30, 0xd3, 0x27, 0xbc, 0xd6, 0x03, 0x9e, 0x34, 0x3a,
0x64, 0xeb, 0x69, 0x25, 0x41, 0x46, 0x47, 0x50, 0x15, 0x79, 0x45, 0xc5, 0x3f, 0x5c, 0x3e, 0xd4, 0x81, 0x72, 0xbc, 0xc3, 0xd0, 0x4e, 0xe6, 0x2e, 0xb7, 0xf6, 0xa3, 0x7c, 0x7f, 0x09, 0x2a, 0x6e,
0x9a, 0x56, 0x06, 0xc9, 0x2e, 0x8e, 0xa1, 0x96, 0x0c, 0x19, 0x15, 0x59, 0x85, 0x28, 0x68, 0x0f, 0x34, 0x84, 0x4a, 0x6a, 0x61, 0xa1, 0x66, 0x86, 0x7d, 0x77, 0x25, 0xca, 0xad, 0xe5, 0x04, 0xa1,
0x4a, 0xb1, 0x44, 0xe2, 0xe4, 0xf0, 0xdb, 0x8b, 0x89, 0xc3, 0xa7, 0xd1, 0x78, 0x60, 0xf9, 0xde, 0xf8, 0x06, 0x60, 0xd1, 0xcd, 0xe8, 0x41, 0x86, 0x7f, 0x67, 0x4c, 0xe4, 0xe6, 0x52, 0x5c, 0xc8,
0xd0, 0x75, 0x26, 0x53, 0x4e, 0x1d, 0x3a, 0xa1, 0x84, 0xcf, 0xfc, 0xf0, 0x6a, 0xe8, 0x52, 0x7b, 0xbd, 0x82, 0x6a, 0x7a, 0x75, 0xa2, 0x6c, 0x02, 0x39, 0x5b, 0x55, 0xce, 0x19, 0x14, 0x74, 0x00,
0xe8, 0xd2, 0xfc, 0x05, 0x15, 0x06, 0xd6, 0xb8, 0x26, 0x2e, 0xa9, 0xc3, 0x3f, 0x01, 0x00, 0x00, 0x45, 0x3e, 0x0f, 0x28, 0x3b, 0xd0, 0xe9, 0xa1, 0x91, 0xe5, 0x3c, 0x48, 0x64, 0xd1, 0x87, 0x52,
0xff, 0xff, 0x5d, 0xba, 0x03, 0x17, 0xc2, 0x06, 0x00, 0x00, 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. // Reference imports to suppress errors if they are not otherwise used.

@ -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) { 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 protoReq PolicyRequest
var metadata runtime.ServerMetadata 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)) msg, err := client.Policy(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err return msg, metadata, err
@ -267,6 +278,10 @@ func local_request_WatchtowerClient_Policy_0(ctx context.Context, marshaler runt
var protoReq PolicyRequest var protoReq PolicyRequest
var metadata runtime.ServerMetadata 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) msg, err := server.Policy(ctx, &protoReq)
return msg, metadata, err return msg, metadata, err

@ -149,7 +149,19 @@ message StatsResponse {
uint32 num_sessions_exhausted = 5; 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 { message PolicyRequest {
/*
The client type from which to retrieve the active offering policy.
*/
PolicyType policy_type = 1;
} }
message PolicyResponse { message PolicyResponse {

@ -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": [ "tags": [
"WatchtowerClient" "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": { "wtclientrpcRemoveTowerResponse": {
"type": "object" "type": "object"
}, },

@ -9070,6 +9070,19 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness,
func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness,
t *harnessTest) { 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() ctxb := context.Background()
const ( const (
chanAmt = lnd.MaxBtcFundingAmount 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 // Since we'd like to test some multi-hop failure scenarios, we'll
// introduce another node into our test network: Carol. // 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 { if err != nil {
t.Fatalf("unable to create new nodes: %v", err) 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 // 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 // won't be able to connect to him and trigger the channel data
// protection logic automatically. // protection logic automatically.
dave, err := net.NewNode("Dave", []string{ daveArgs := []string{
"--nolisten", "--nolisten",
"--wtclient.active", "--wtclient.active",
}) }
if anchors {
daveArgs = append(daveArgs, "--protocol.anchors")
}
dave, err := net.NewNode("Dave", daveArgs)
if err != nil { if err != nil {
t.Fatalf("unable to create new node: %v", err) t.Fatalf("unable to create new node: %v", err)
} }
@ -9249,7 +9270,9 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness,
err = wait.NoError(func() error { err = wait.NoError(func() error {
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel() defer cancel()
bkpStats, err := dave.WatchtowerClient.Stats(ctxt, &wtclientrpc.StatsRequest{}) bkpStats, err := dave.WatchtowerClient.Stats(ctxt,
&wtclientrpc.StatsRequest{},
)
if err != nil { if err != nil {
return err return err

@ -248,9 +248,13 @@ 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 when creating a ChannelLink. // TowerClient is used by legacy channels to backup revoked states.
TowerClient wtclient.Client 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 // DisconnectPeer is used to disconnect this peer if the cooperative close
// process fails. // process fails.
DisconnectPeer func(*btcec.PublicKey) error DisconnectPeer func(*btcec.PublicKey) error
@ -757,6 +761,18 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
return p.cfg.ChainArb.UpdateContractSignals(*chanPoint, signals) 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{ linkCfg := htlcswitch.ChannelLinkConfig{
Peer: p, Peer: p,
DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators, DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators,
@ -782,7 +798,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
MinFeeUpdateTimeout: htlcswitch.DefaultMinLinkFeeUpdateTimeout, MinFeeUpdateTimeout: htlcswitch.DefaultMinLinkFeeUpdateTimeout,
MaxFeeUpdateTimeout: htlcswitch.DefaultMaxLinkFeeUpdateTimeout, MaxFeeUpdateTimeout: htlcswitch.DefaultMaxLinkFeeUpdateTimeout,
OutgoingCltvRejectDelta: p.cfg.OutgoingCltvRejectDelta, OutgoingCltvRejectDelta: p.cfg.OutgoingCltvRejectDelta,
TowerClient: p.cfg.TowerClient, TowerClient: towerClient,
MaxOutgoingCltvExpiry: p.cfg.MaxOutgoingCltvExpiry, MaxOutgoingCltvExpiry: p.cfg.MaxOutgoingCltvExpiry,
MaxFeeAllocation: p.cfg.MaxChannelFeeAllocation, MaxFeeAllocation: p.cfg.MaxChannelFeeAllocation,
NotifyActiveLink: p.cfg.ChannelNotifier.NotifyActiveLinkEvent, NotifyActiveLink: p.cfg.ChannelNotifier.NotifyActiveLinkEvent,

@ -619,8 +619,8 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service,
cfg, s.cc, cfg.networkDir, macService, atpl, invoiceRegistry, cfg, s.cc, cfg.networkDir, macService, atpl, invoiceRegistry,
s.htlcSwitch, cfg.ActiveNetParams.Params, s.chanRouter, s.htlcSwitch, cfg.ActiveNetParams.Params, s.chanRouter,
routerBackend, s.nodeSigner, s.remoteChanDB, s.sweeper, tower, routerBackend, s.nodeSigner, s.remoteChanDB, s.sweeper, tower,
s.towerClient, cfg.net.ResolveTCPAddr, genInvoiceFeatures, s.towerClient, s.anchorTowerClient, cfg.net.ResolveTCPAddr,
rpcsLog, genInvoiceFeatures, rpcsLog,
) )
if err != nil { if err != nil {
return nil, err return nil, err

@ -66,6 +66,7 @@ import (
"github.com/lightningnetwork/lnd/ticker" "github.com/lightningnetwork/lnd/ticker"
"github.com/lightningnetwork/lnd/tor" "github.com/lightningnetwork/lnd/tor"
"github.com/lightningnetwork/lnd/walletunlocker" "github.com/lightningnetwork/lnd/walletunlocker"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtclient" "github.com/lightningnetwork/lnd/watchtower/wtclient"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy" "github.com/lightningnetwork/lnd/watchtower/wtpolicy"
"github.com/lightningnetwork/lnd/watchtower/wtserver" "github.com/lightningnetwork/lnd/watchtower/wtserver"
@ -247,6 +248,8 @@ type server struct {
towerClient wtclient.Client towerClient wtclient.Client
anchorTowerClient wtclient.Client
connMgr *connmgr.ConnManager connMgr *connmgr.ConnManager
sigPool *lnwallet.SigPool sigPool *lnwallet.SigPool
@ -1265,6 +1268,29 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
if err != nil { if err != nil {
return nil, err 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 { if len(cfg.ExternalHosts) != 0 {
@ -1438,6 +1464,12 @@ func (s *server) Start() error {
return return
} }
} }
if s.anchorTowerClient != nil {
if err := s.anchorTowerClient.Start(); err != nil {
startErr = err
return
}
}
if err := s.htlcSwitch.Start(); err != nil { if err := s.htlcSwitch.Start(); err != nil {
startErr = err startErr = err
return return
@ -1675,7 +1707,16 @@ func (s *server) Stop() error {
// 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.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 { if s.hostAnn != nil {
@ -3036,6 +3077,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
ChannelNotifier: s.channelNotifier, ChannelNotifier: s.channelNotifier,
HtlcNotifier: s.htlcNotifier, HtlcNotifier: s.htlcNotifier,
TowerClient: s.towerClient, TowerClient: s.towerClient,
AnchorTowerClient: s.anchorTowerClient,
DisconnectPeer: s.DisconnectPeer, DisconnectPeer: s.DisconnectPeer,
GenNodeAnnouncement: s.genNodeAnnouncement, GenNodeAnnouncement: s.genNodeAnnouncement,

@ -96,6 +96,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
sweeper *sweep.UtxoSweeper, sweeper *sweep.UtxoSweeper,
tower *watchtower.Standalone, tower *watchtower.Standalone,
towerClient wtclient.Client, towerClient wtclient.Client,
anchorTowerClient wtclient.Client,
tcpResolver lncfg.TCPResolver, tcpResolver lncfg.TCPResolver,
genInvoiceFeatures func() *lnwire.FeatureVector, genInvoiceFeatures func() *lnwire.FeatureVector,
rpcLogger btclog.Logger) error { rpcLogger btclog.Logger) error {
@ -243,13 +244,16 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
case *wtclientrpc.Config: case *wtclientrpc.Config:
subCfgValue := extractReflectValue(subCfg) subCfgValue := extractReflectValue(subCfg)
if towerClient != nil { if towerClient != nil && anchorTowerClient != nil {
subCfgValue.FieldByName("Active").Set( subCfgValue.FieldByName("Active").Set(
reflect.ValueOf(towerClient != nil), reflect.ValueOf(towerClient != nil),
) )
subCfgValue.FieldByName("Client").Set( subCfgValue.FieldByName("Client").Set(
reflect.ValueOf(towerClient), reflect.ValueOf(towerClient),
) )
subCfgValue.FieldByName("AnchorClient").Set(
reflect.ValueOf(anchorTowerClient),
)
} }
subCfgValue.FieldByName("Resolver").Set( subCfgValue.FieldByName("Resolver").Set(
reflect.ValueOf(tcpResolver), reflect.ValueOf(tcpResolver),

@ -140,6 +140,7 @@ func (t Type) String() string {
var supportedTypes = map[Type]struct{}{ var supportedTypes = map[Type]struct{}{
TypeAltruistCommit: {}, TypeAltruistCommit: {},
TypeRewardCommit: {}, TypeRewardCommit: {},
TypeAltruistAnchorCommit: {},
} }
// IsSupportedType returns true if the given type is supported by the package. // IsSupportedType returns true if the given type is supported by the package.

@ -123,6 +123,12 @@ func TestSupportedTypes(t *testing.T) {
t.Fatalf("default type %s is not supported", blob.TypeAltruistCommit) 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. // Assert that all claimed supported types are actually supported.
for _, supType := range blob.SupportedTypes() { for _, supType := range blob.SupportedTypes() {
if blob.IsSupportedType(supType) { if blob.IsSupportedType(supType) {

@ -1,12 +1,15 @@
package wtclient package wtclient
import ( import (
"fmt"
"github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/txsort" "github.com/btcsuite/btcutil/txsort"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -36,6 +39,7 @@ import (
type backupTask struct { type backupTask struct {
id wtdb.BackupID id wtdb.BackupID
breachInfo *lnwallet.BreachRetribution breachInfo *lnwallet.BreachRetribution
chanType channeldb.ChannelType
// state-dependent variables // state-dependent variables
@ -54,7 +58,7 @@ type backupTask struct {
// variables. // variables.
func newBackupTask(chanID *lnwire.ChannelID, func newBackupTask(chanID *lnwire.ChannelID,
breachInfo *lnwallet.BreachRetribution, breachInfo *lnwallet.BreachRetribution,
sweepPkScript []byte, isTweakless bool) *backupTask { sweepPkScript []byte, chanType channeldb.ChannelType) *backupTask {
// Parse the non-dust outputs from the breach transaction, // Parse the non-dust outputs from the breach transaction,
// simultaneously computing the total amount contained in the inputs // simultaneously computing the total amount contained in the inputs
@ -85,17 +89,35 @@ func newBackupTask(chanID *lnwire.ChannelID,
totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value totalAmt += breachInfo.RemoteOutputSignDesc.Output.Value
} }
if breachInfo.LocalOutputSignDesc != nil { if breachInfo.LocalOutputSignDesc != nil {
witnessType := input.CommitmentNoDelay var witnessType input.WitnessType
if isTweakless { switch {
case chanType.HasAnchors():
witnessType = input.CommitmentToRemoteConfirmed
case chanType.IsTweakless():
witnessType = input.CommitSpendNoDelayTweakless witnessType = input.CommitSpendNoDelayTweakless
default:
witnessType = input.CommitmentNoDelay
} }
// 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( toRemoteInput = input.NewBaseInput(
&breachInfo.LocalOutpoint, &breachInfo.LocalOutpoint,
witnessType, witnessType,
breachInfo.LocalOutputSignDesc, breachInfo.LocalOutputSignDesc,
0, 0,
) )
}
totalAmt += breachInfo.LocalOutputSignDesc.Output.Value totalAmt += breachInfo.LocalOutputSignDesc.Output.Value
} }
@ -106,6 +128,7 @@ func newBackupTask(chanID *lnwire.ChannelID,
CommitHeight: breachInfo.RevokedStateNum, CommitHeight: breachInfo.RevokedStateNum,
}, },
breachInfo: breachInfo, breachInfo: breachInfo,
chanType: chanType,
toLocalInput: toLocalInput, toLocalInput: toLocalInput,
toRemoteInput: toRemoteInput, toRemoteInput: toRemoteInput,
totalAmt: btcutil.Amount(totalAmt), totalAmt: btcutil.Amount(totalAmt),
@ -145,14 +168,29 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody) error {
// underestimate the size by one byte. The diferrence in weight // underestimate the size by one byte. The diferrence in weight
// can cause different output values on the sweep transaction, // can cause different output values on the sweep transaction,
// so we mimic the original bug and create signatures using the // so we mimic the original bug and create signatures using the
// original weight estimate. // 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( weightEstimate.AddWitnessInput(
input.ToLocalPenaltyWitnessSize - 1, input.ToLocalPenaltyWitnessSize - 1,
) )
} }
}
if t.toRemoteInput != nil { if t.toRemoteInput != nil {
// 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) weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
} }
}
// All justice transactions have a p2wkh output paying to the victim. // All justice transactions have a p2wkh output paying to the victim.
weightEstimate.AddP2WKHOutput() weightEstimate.AddP2WKHOutput()
@ -163,6 +201,12 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody) error {
weightEstimate.AddP2WKHOutput() 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 // Now, compute the output values depending on whether FlagReward is set
// in the current session's policy. // in the current session's policy.
outputs, err := session.Policy.ComputeJusticeTxOuts( outputs, err := session.Policy.ComputeJusticeTxOuts(
@ -219,9 +263,10 @@ func (t *backupTask) craftSessionPayload(
// information. This will either be contain both the to-local and // information. This will either be contain both the to-local and
// to-remote outputs, or only be the to-local output. // to-remote outputs, or only be the to-local output.
inputs := t.inputs() inputs := t.inputs()
for prevOutPoint := range inputs { for prevOutPoint, input := range inputs {
justiceTxn.AddTxIn(&wire.TxIn{ justiceTxn.AddTxIn(&wire.TxIn{
PreviousOutPoint: prevOutPoint, PreviousOutPoint: prevOutPoint,
Sequence: input.BlocksToMaturity(),
}) })
} }
@ -288,7 +333,12 @@ func (t *backupTask) craftSessionPayload(
case input.CommitSpendNoDelayTweakless: case input.CommitSpendNoDelayTweakless:
fallthrough fallthrough
case input.CommitmentNoDelay: case input.CommitmentNoDelay:
fallthrough
case input.CommitmentToRemoteConfirmed:
copy(justiceKit.CommitToRemoteSig[:], signature[:]) copy(justiceKit.CommitToRemoteSig[:], signature[:])
default:
return hint, nil, fmt.Errorf("invalid witness type: %v",
inp.WitnessType())
} }
} }

@ -13,6 +13,7 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@ -74,7 +75,7 @@ type backupTaskTest struct {
bindErr error bindErr error
expSweepScript []byte expSweepScript []byte
signer input.Signer signer input.Signer
tweakless bool chanType channeldb.ChannelType
} }
// genTaskTest creates a instance of a backupTaskTest using the passed // genTaskTest creates a instance of a backupTaskTest using the passed
@ -92,7 +93,13 @@ func genTaskTest(
expSweepAmt int64, expSweepAmt int64,
expRewardAmt int64, expRewardAmt int64,
bindErr error, 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. // Parse the key pairs for all keys used in the test.
revSK, revPK := btcec.PrivKeyFromBytes( revSK, revPK := btcec.PrivKeyFromBytes(
@ -192,11 +199,24 @@ func genTaskTest(
Index: index, Index: index,
} }
witnessType := input.CommitmentNoDelay var witnessType input.WitnessType
if tweakless { switch {
case chanType.HasAnchors():
witnessType = input.CommitmentToRemoteConfirmed
case chanType.IsTweakless():
witnessType = input.CommitSpendNoDelayTweakless witnessType = input.CommitSpendNoDelayTweakless
default:
witnessType = input.CommitmentNoDelay
} }
if chanType.HasAnchors() {
toRemoteInput = input.NewCsvInput(
&breachInfo.LocalOutpoint,
witnessType,
breachInfo.LocalOutputSignDesc,
0, 1,
)
} else {
toRemoteInput = input.NewBaseInput( toRemoteInput = input.NewBaseInput(
&breachInfo.LocalOutpoint, &breachInfo.LocalOutpoint,
witnessType, witnessType,
@ -204,6 +224,7 @@ func genTaskTest(
0, 0,
) )
} }
}
return backupTaskTest{ return backupTaskTest{
name: name, name: name,
@ -227,7 +248,7 @@ func genTaskTest(
bindErr: bindErr, bindErr: bindErr,
expSweepScript: makeAddrSlice(22), expSweepScript: makeAddrSlice(22),
signer: signer, signer: signer,
tweakless: tweakless, chanType: chanType,
} }
} }
@ -253,8 +274,45 @@ var (
func TestBackupTask(t *testing.T) { func TestBackupTask(t *testing.T) {
t.Parallel() t.Parallel()
chanTypes := []channeldb.ChannelType{
channeldb.SingleFunderBit,
channeldb.SingleFunderTweaklessBit,
channeldb.AnchorOutputsBit,
}
var backupTaskTests []backupTaskTest 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{ backupTaskTests = append(backupTaskTests, []backupTaskTest{
genTaskTest( genTaskTest(
"commit no-reward, both outputs", "commit no-reward, both outputs",
@ -264,10 +322,10 @@ func TestBackupTask(t *testing.T) {
blobTypeCommitNoReward, // blobType blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate 1000, // sweepFeeRate
nil, // rewardScript nil, // rewardScript
299241, // expSweepAmt expSweepCommitNoRewardBoth, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
nil, // bindErr nil, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit no-reward, to-local output only", "commit no-reward, to-local output only",
@ -277,10 +335,10 @@ func TestBackupTask(t *testing.T) {
blobTypeCommitNoReward, // blobType blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate 1000, // sweepFeeRate
nil, // rewardScript nil, // rewardScript
199514, // expSweepAmt expSweepCommitNoRewardLocal, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
nil, // bindErr nil, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit no-reward, to-remote output only", "commit no-reward, to-remote output only",
@ -290,10 +348,10 @@ func TestBackupTask(t *testing.T) {
blobTypeCommitNoReward, // blobType blobTypeCommitNoReward, // blobType
1000, // sweepFeeRate 1000, // sweepFeeRate
nil, // rewardScript nil, // rewardScript
99561, // expSweepAmt expSweepCommitNoRewardRemote, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
nil, // bindErr nil, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit no-reward, to-remote output only, creates dust", "commit no-reward, to-remote output only, creates dust",
@ -301,12 +359,12 @@ func TestBackupTask(t *testing.T) {
0, // toLocalAmt 0, // toLocalAmt
100000, // toRemoteAmt 100000, // toRemoteAmt
blobTypeCommitNoReward, // blobType blobTypeCommitNoReward, // blobType
227500, // sweepFeeRate sweepFeeRateNoRewardRemoteDust, // sweepFeeRate
nil, // rewardScript nil, // rewardScript
0, // expSweepAmt 0, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr wtpolicy.ErrCreatesDust, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit no-reward, no outputs, fee rate exceeds inputs", "commit no-reward, no outputs, fee rate exceeds inputs",
@ -319,7 +377,7 @@ func TestBackupTask(t *testing.T) {
0, // expSweepAmt 0, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
wtpolicy.ErrFeeExceedsInputs, // bindErr wtpolicy.ErrFeeExceedsInputs, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit no-reward, no outputs, fee rate of 0 creates dust", "commit no-reward, no outputs, fee rate of 0 creates dust",
@ -332,7 +390,7 @@ func TestBackupTask(t *testing.T) {
0, // expSweepAmt 0, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr wtpolicy.ErrCreatesDust, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit reward, both outputs", "commit reward, both outputs",
@ -342,10 +400,10 @@ func TestBackupTask(t *testing.T) {
blobTypeCommitReward, // blobType blobTypeCommitReward, // blobType
1000, // sweepFeeRate 1000, // sweepFeeRate
addrScript, // rewardScript addrScript, // rewardScript
296117, // expSweepAmt expSweepCommitRewardBoth, // expSweepAmt
3000, // expRewardAmt 3000, // expRewardAmt
nil, // bindErr nil, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit reward, to-local output only", "commit reward, to-local output only",
@ -355,10 +413,10 @@ func TestBackupTask(t *testing.T) {
blobTypeCommitReward, // blobType blobTypeCommitReward, // blobType
1000, // sweepFeeRate 1000, // sweepFeeRate
addrScript, // rewardScript addrScript, // rewardScript
197390, // expSweepAmt expSweepCommitRewardLocal, // expSweepAmt
2000, // expRewardAmt 2000, // expRewardAmt
nil, // bindErr nil, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit reward, to-remote output only", "commit reward, to-remote output only",
@ -368,10 +426,10 @@ func TestBackupTask(t *testing.T) {
blobTypeCommitReward, // blobType blobTypeCommitReward, // blobType
1000, // sweepFeeRate 1000, // sweepFeeRate
addrScript, // rewardScript addrScript, // rewardScript
98437, // expSweepAmt expSweepCommitRewardRemote, // expSweepAmt
1000, // expRewardAmt 1000, // expRewardAmt
nil, // bindErr nil, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit reward, to-remote output only, creates dust", "commit reward, to-remote output only, creates dust",
@ -379,12 +437,12 @@ func TestBackupTask(t *testing.T) {
0, // toLocalAmt 0, // toLocalAmt
100000, // toRemoteAmt 100000, // toRemoteAmt
blobTypeCommitReward, // blobType blobTypeCommitReward, // blobType
175000, // sweepFeeRate sweepFeeRateRewardRemoteDust, // sweepFeeRate
addrScript, // rewardScript addrScript, // rewardScript
0, // expSweepAmt 0, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr wtpolicy.ErrCreatesDust, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit reward, no outputs, fee rate exceeds inputs", "commit reward, no outputs, fee rate exceeds inputs",
@ -397,7 +455,7 @@ func TestBackupTask(t *testing.T) {
0, // expSweepAmt 0, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
wtpolicy.ErrFeeExceedsInputs, // bindErr wtpolicy.ErrFeeExceedsInputs, // bindErr
tweakless, chanType,
), ),
genTaskTest( genTaskTest(
"commit reward, no outputs, fee rate of 0 creates dust", "commit reward, no outputs, fee rate of 0 creates dust",
@ -410,7 +468,7 @@ func TestBackupTask(t *testing.T) {
0, // expSweepAmt 0, // expSweepAmt
0, // expRewardAmt 0, // expRewardAmt
wtpolicy.ErrCreatesDust, // bindErr 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. // Create a new backupTask from the channel id and breach info.
task := newBackupTask( task := newBackupTask(
&test.chanID, test.breachInfo, test.expSweepScript, &test.chanID, test.breachInfo, test.expSweepScript,
test.tweakless, test.chanType,
) )
// Assert that all parameters set during initialization are properly // Assert that all parameters set during initialization are properly

@ -10,6 +10,9 @@ import (
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash" "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/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@ -40,13 +43,14 @@ const (
DefaultForceQuitDelay = 10 * time.Second DefaultForceQuitDelay = 10 * time.Second
) )
var ( // genActiveSessionFilter generates a filter that selects active sessions that
// activeSessionFilter is a filter that ignored any sessions which are // also match the desired channel type, either legacy or anchor.
// not active. func genActiveSessionFilter(anchor bool) func(*wtdb.ClientSession) bool {
activeSessionFilter = func(s *wtdb.ClientSession) bool { return func(s *wtdb.ClientSession) bool {
return s.Status == wtdb.CSessionActive return s.Status == wtdb.CSessionActive &&
anchor == s.Policy.IsAnchorChannel()
}
} }
)
// RegisteredTower encompasses information about a registered watchtower with // RegisteredTower encompasses information about a registered watchtower with
// the client. // the client.
@ -103,7 +107,8 @@ type Client interface {
// negotiated policy. If the channel we're trying to back up doesn't // 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 // have a tweak for the remote party's output, then isTweakless should
// be true. // 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 // Start initializes the watchtower client, allowing it process requests
// to backup revoked channel states. // to backup revoked channel states.
@ -228,6 +233,8 @@ type TowerClient struct {
cfg *Config cfg *Config
log btclog.Logger
pipeline *taskPipeline pipeline *taskPipeline
negotiator SessionNegotiator negotiator SessionNegotiator
@ -274,10 +281,18 @@ func New(config *Config) (*TowerClient, error) {
cfg.WriteTimeout = DefaultWriteTimeout 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 // 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 client. We will use any of these session if their policies match
// the current policy of the client, otherwise they will be ignored and // the current policy of the client, otherwise they will be ignored and
// new sessions will be requested. // new sessions will be requested.
isAnchorClient := cfg.Policy.IsAnchorChannel()
activeSessionFilter := genActiveSessionFilter(isAnchorClient)
candidateSessions, err := getClientSessions( candidateSessions, err := getClientSessions(
cfg.DB, cfg.SecretKeyRing, nil, activeSessionFilter, cfg.DB, cfg.SecretKeyRing, nil, activeSessionFilter,
) )
@ -287,7 +302,7 @@ func New(config *Config) (*TowerClient, error) {
var candidateTowers []*wtdb.Tower var candidateTowers []*wtdb.Tower
for _, s := range candidateSessions { 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) s.Tower, cfg.Policy)
candidateTowers = append(candidateTowers, s.Tower) candidateTowers = append(candidateTowers, s.Tower)
} }
@ -301,6 +316,7 @@ func New(config *Config) (*TowerClient, error) {
c := &TowerClient{ c := &TowerClient{
cfg: cfg, cfg: cfg,
log: plog,
pipeline: newTaskPipeline(), pipeline: newTaskPipeline(),
candidateTowers: newTowerListIterator(candidateTowers...), candidateTowers: newTowerListIterator(candidateTowers...),
candidateSessions: candidateSessions, candidateSessions: candidateSessions,
@ -422,7 +438,7 @@ func (c *TowerClient) buildHighestCommitHeights() {
func (c *TowerClient) Start() error { func (c *TowerClient) Start() error {
var err error var err error
c.started.Do(func() { 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 // First, restart a session queue for any sessions that have
// committed but unacked state updates. This ensures that these // committed but unacked state updates. This ensures that these
@ -430,7 +446,7 @@ func (c *TowerClient) Start() error {
// restart. // restart.
for _, session := range c.candidateSessions { for _, session := range c.candidateSessions {
if len(session.CommittedUpdates) > 0 { 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, "%d committed backups", session.ID,
len(session.CommittedUpdates)) len(session.CommittedUpdates))
c.initActiveQueue(session) c.initActiveQueue(session)
@ -460,7 +476,7 @@ func (c *TowerClient) Start() error {
// Stop idempotently initiates a graceful shutdown of the watchtower client. // Stop idempotently initiates a graceful shutdown of the watchtower client.
func (c *TowerClient) Stop() error { func (c *TowerClient) Stop() error {
c.stopped.Do(func() { 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 // 1. To ensure we don't hang forever on shutdown due to
// unintended failures, we'll delay a call to force quit the // 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. // queues, we no longer need to negotiate sessions.
c.negotiator.Stop() 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) "draining, stats: %s", c.stats)
// 5. Shutdown all active session queues in parallel. These will // 5. Shutdown all active session queues in parallel. These will
@ -509,7 +525,7 @@ func (c *TowerClient) Stop() error {
default: default:
} }
log.Debugf("Client successfully stopped, stats: %s", c.stats) c.log.Debugf("Client successfully stopped, stats: %s", c.stats)
}) })
return nil return nil
} }
@ -518,7 +534,7 @@ func (c *TowerClient) Stop() error {
// client. This should only be executed if Stop is unable to exit cleanly. // client. This should only be executed if Stop is unable to exit cleanly.
func (c *TowerClient) ForceQuit() { func (c *TowerClient) ForceQuit() {
c.forced.Do(func() { 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 // 1. Shutdown the backup queue, which will prevent any further
// updates from being accepted. In practice, the links should be // updates from being accepted. In practice, the links should be
@ -543,7 +559,7 @@ func (c *TowerClient) ForceQuit() {
return s.ForceQuit return s.ForceQuit
}) })
log.Infof("Watchtower client unclean shutdown complete, "+ c.log.Infof("Watchtower client unclean shutdown complete, "+
"stats: %s", c.stats) "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 // - breached outputs contain too little value to sweep at the target sweep fee
// rate. // rate.
func (c *TowerClient) BackupState(chanID *lnwire.ChannelID, 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. // Retrieve the cached sweep pkscript used for this channel.
c.backupMu.Lock() c.backupMu.Lock()
@ -606,7 +623,7 @@ func (c *TowerClient) BackupState(chanID *lnwire.ChannelID,
height, ok := c.chanCommitHeights[*chanID] height, ok := c.chanCommitHeights[*chanID]
if ok && breachInfo.RevokedStateNum <= height { if ok && breachInfo.RevokedStateNum <= height {
c.backupMu.Unlock() 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) chanID, breachInfo.RevokedStateNum)
return nil return nil
} }
@ -618,7 +635,7 @@ func (c *TowerClient) BackupState(chanID *lnwire.ChannelID,
c.backupMu.Unlock() c.backupMu.Unlock()
task := newBackupTask( task := newBackupTask(
chanID, breachInfo, summary.SweepPkScript, isTweakless, chanID, breachInfo, summary.SweepPkScript, chanType,
) )
return c.pipeline.QueueBackupTask(task) return c.pipeline.QueueBackupTask(task)
@ -669,15 +686,15 @@ func (c *TowerClient) nextSessionQueue() *sessionQueue {
func (c *TowerClient) backupDispatcher() { func (c *TowerClient) backupDispatcher() {
defer c.wg.Done() defer c.wg.Done()
log.Tracef("Starting backup dispatcher") c.log.Tracef("Starting backup dispatcher")
defer log.Tracef("Stopping backup dispatcher") defer c.log.Tracef("Stopping backup dispatcher")
for { for {
switch { switch {
// No active session queue and no additional sessions. // No active session queue and no additional sessions.
case c.sessionQueue == nil && len(c.candidateSessions) == 0: case c.sessionQueue == nil && len(c.candidateSessions) == 0:
log.Infof("Requesting new session.") c.log.Infof("Requesting new session.")
// Immediately request a new session. // Immediately request a new session.
c.negotiator.RequestSession() c.negotiator.RequestSession()
@ -688,7 +705,7 @@ func (c *TowerClient) backupDispatcher() {
awaitSession: awaitSession:
select { select {
case session := <-c.negotiator.NewSessions(): case session := <-c.negotiator.NewSessions():
log.Infof("Acquired new session with id=%s", c.log.Infof("Acquired new session with id=%s",
session.ID) session.ID)
c.candidateSessions[session.ID] = session c.candidateSessions[session.ID] = session
c.stats.sessionAcquired() c.stats.sessionAcquired()
@ -698,7 +715,7 @@ func (c *TowerClient) backupDispatcher() {
continue continue
case <-c.statTicker.C: 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 // A new tower has been requested to be added. We'll
// update our persisted and in-memory state and consider // update our persisted and in-memory state and consider
@ -732,7 +749,7 @@ func (c *TowerClient) backupDispatcher() {
// backup tasks. // backup tasks.
c.sessionQueue = c.nextSessionQueue() c.sessionQueue = c.nextSessionQueue()
if c.sessionQueue != nil { if c.sessionQueue != nil {
log.Debugf("Loaded next candidate session "+ c.log.Debugf("Loaded next candidate session "+
"queue id=%s", c.sessionQueue.ID()) "queue id=%s", c.sessionQueue.ID())
} }
@ -759,13 +776,13 @@ func (c *TowerClient) backupDispatcher() {
// we can request new sessions before the session is // we can request new sessions before the session is
// fully empty, which this case would handle. // fully empty, which this case would handle.
case session := <-c.negotiator.NewSessions(): 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) "while processing tasks", session.ID)
c.candidateSessions[session.ID] = session c.candidateSessions[session.ID] = session
c.stats.sessionAcquired() c.stats.sessionAcquired()
case <-c.statTicker.C: 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 // Process each backup task serially from the queue of
// revoked states. // revoked states.
@ -776,7 +793,7 @@ func (c *TowerClient) backupDispatcher() {
return return
} }
log.Debugf("Processing %v", task.id) c.log.Debugf("Processing %v", task.id)
c.stats.taskReceived() c.stats.taskReceived()
c.processTask(task) 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 // sessionQueue will be removed if accepting the task left the sessionQueue in
// an exhausted state. // an exhausted state.
func (c *TowerClient) taskAccepted(task *backupTask, newStatus reserveStatus) { 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()) task.id, c.sessionQueue.ID())
c.stats.taskAccepted() c.stats.taskAccepted()
@ -840,7 +857,7 @@ func (c *TowerClient) taskAccepted(task *backupTask, newStatus reserveStatus) {
case reserveExhausted: case reserveExhausted:
c.stats.sessionExhausted() 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 // This task left the session exhausted, set it to nil and
// proceed to the next loop so we can consume another // proceed to the next loop so we can consume another
@ -863,13 +880,13 @@ func (c *TowerClient) taskRejected(task *backupTask, curStatus reserveStatus) {
case reserveAvailable: case reserveAvailable:
c.stats.taskIneligible() c.stats.taskIneligible()
log.Infof("Ignoring ineligible %v", task.id) c.log.Infof("Ignoring ineligible %v", task.id)
err := c.cfg.DB.MarkBackupIneligible( err := c.cfg.DB.MarkBackupIneligible(
task.id.ChanID, task.id.CommitHeight, task.id.ChanID, task.id.CommitHeight,
) )
if err != nil { if err != nil {
log.Errorf("Unable to mark %v ineligible: %v", c.log.Errorf("Unable to mark %v ineligible: %v",
task.id, err) task.id, err)
// It is safe to not handle this error, even if we could // 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: case reserveExhausted:
c.stats.sessionExhausted() 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) c.sessionQueue.ID(), task.id)
// Cache the task that we pulled off, so that we can process it // 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)) err := peer.SetReadDeadline(time.Now().Add(c.cfg.ReadTimeout))
if err != nil { if err != nil {
err = fmt.Errorf("unable to set read deadline: %v", err) 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 return nil, err
} }
@ -926,7 +943,7 @@ func (c *TowerClient) readMessage(peer wtserver.Peer) (wtwire.Message, error) {
rawMsg, err := peer.ReadNextMessage() rawMsg, err := peer.ReadNextMessage()
if err != nil { if err != nil {
err = fmt.Errorf("unable to read message: %v", err) 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 return nil, err
} }
@ -936,11 +953,11 @@ func (c *TowerClient) readMessage(peer wtserver.Peer) (wtwire.Message, error) {
msg, err := wtwire.ReadMessage(msgReader, 0) msg, err := wtwire.ReadMessage(msgReader, 0)
if err != nil { if err != nil {
err = fmt.Errorf("unable to parse message: %v", err) 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 return nil, err
} }
logMessage(peer, msg, true) c.logMessage(peer, msg, true)
return msg, nil return msg, nil
} }
@ -953,7 +970,7 @@ func (c *TowerClient) sendMessage(peer wtserver.Peer, msg wtwire.Message) error
_, err := wtwire.WriteMessage(&b, msg, 0) _, err := wtwire.WriteMessage(&b, msg, 0)
if err != nil { if err != nil {
err = fmt.Errorf("Unable to encode msg: %v", err) 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 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)) err = peer.SetWriteDeadline(time.Now().Add(c.cfg.WriteTimeout))
if err != nil { if err != nil {
err = fmt.Errorf("unable to set write deadline: %v", err) 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 return err
} }
logMessage(peer, msg, false) c.logMessage(peer, msg, false)
// Write out the full message to the remote peer. // Write out the full message to the remote peer.
_, err = peer.Write(b.Bytes()) _, err = peer.Write(b.Bytes())
if err != nil { if err != nil {
log.Errorf("Unable to send msg: %v", err) c.log.Errorf("Unable to send msg: %v", err)
} }
return err return err
} }
@ -1065,6 +1082,8 @@ func (c *TowerClient) handleNewTower(msg *newTowerMsg) error {
c.candidateTowers.AddCandidate(tower) c.candidateTowers.AddCandidate(tower)
// Include all of its corresponding sessions to our set of candidates. // Include all of its corresponding sessions to our set of candidates.
isAnchorClient := c.cfg.Policy.IsAnchorChannel()
activeSessionFilter := genActiveSessionFilter(isAnchorClient)
sessions, err := getClientSessions( sessions, err := getClientSessions(
c.cfg.DB, c.cfg.SecretKeyRing, &tower.ID, activeSessionFilter, 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, // logMessage writes information about a message received from a remote peer,
// using directional prepositions to signal whether the message was sent or // using directional prepositions to signal whether the message was sent or
// received. // 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 action = "Received"
var preposition = "from" var preposition = "from"
if !read { if !read {
@ -1245,7 +1266,7 @@ func logMessage(peer wtserver.Peer, msg wtwire.Message, read bool) {
summary = "(" + summary + ")" 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(), preposition, peer.RemotePub().SerializeCompressed(),
peer.RemoteAddr()) peer.RemoteAddr())
} }

@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@ -633,7 +634,7 @@ func (h *testHarness) backupState(id, i uint64, expErr error) {
_, retribution := h.channel(id).getState(i) _, retribution := h.channel(id).getState(i)
chanID := chanIDFromInt(id) chanID := chanIDFromInt(id)
err := h.client.BackupState(&chanID, retribution, false) err := h.client.BackupState(&chanID, retribution, channeldb.SingleFunderBit)
if err != expErr { if err != expErr {
h.t.Fatalf("back error mismatch, want: %v, got: %v", h.t.Fatalf("back error mismatch, want: %v, got: %v",
expErr, err) expErr, err)

@ -112,8 +112,19 @@ var _ SessionNegotiator = (*sessionNegotiator)(nil)
// newSessionNegotiator initializes a fresh sessionNegotiator instance. // newSessionNegotiator initializes a fresh sessionNegotiator instance.
func newSessionNegotiator(cfg *NegotiatorConfig) *sessionNegotiator { 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( localInit := wtwire.NewInitMessage(
lnwire.NewRawFeatureVector(wtwire.AltruistSessionsRequired), lnwire.NewRawFeatureVector(features...),
cfg.ChainHash, cfg.ChainHash,
) )

@ -120,6 +120,11 @@ func (p Policy) String() string {
p.SweepFeeRate) 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 // Validate ensures that the policy satisfies some minimal correctness
// constraints. // constraints.
func (p Policy) Validate() error { func (p Policy) Validate() error {

@ -5,6 +5,7 @@ import (
"github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtpolicy" "github.com/lightningnetwork/lnd/watchtower/wtpolicy"
"github.com/stretchr/testify/require"
) )
var validationTests = []struct { 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())
}

@ -96,7 +96,10 @@ type Server struct {
// sessions and send state updates. // sessions and send state updates.
func New(cfg *Config) (*Server, error) { func New(cfg *Config) (*Server, error) {
localInit := wtwire.NewInitMessage( localInit := wtwire.NewInitMessage(
lnwire.NewRawFeatureVector(wtwire.AltruistSessionsOptional), lnwire.NewRawFeatureVector(
wtwire.AltruistSessionsOptional,
wtwire.AnchorCommitOptional,
),
cfg.ChainHash, cfg.ChainHash,
) )

@ -161,6 +161,28 @@ type createSessionTestCase struct {
} }
var createSessionTests = []createSessionTestCase{ 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", name: "duplicate session create",
initMsg: wtwire.NewInitMessage( initMsg: wtwire.NewInitMessage(

@ -7,6 +7,8 @@ import "github.com/lightningnetwork/lnd/lnwire"
var FeatureNames = map[lnwire.FeatureBit]string{ var FeatureNames = map[lnwire.FeatureBit]string{
AltruistSessionsRequired: "altruist-sessions", AltruistSessionsRequired: "altruist-sessions",
AltruistSessionsOptional: "altruist-sessions", AltruistSessionsOptional: "altruist-sessions",
AnchorCommitRequired: "anchor-commit",
AnchorCommitOptional: "anchor-commit",
} }
const ( const (
@ -19,4 +21,13 @@ const (
// support a remote party who understand the protocol for creating and // support a remote party who understand the protocol for creating and
// updating watchtower sessions. // updating watchtower sessions.
AltruistSessionsOptional lnwire.FeatureBit = 1 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
) )