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",
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

@ -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

@ -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},

@ -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.

@ -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),

@ -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.

@ -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

@ -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 {

@ -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"
},

@ -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

@ -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,

@ -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

@ -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,

@ -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),

@ -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.

@ -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) {

@ -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())
}
}

@ -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

@ -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())
}

@ -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)

@ -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,
)

@ -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 {

@ -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())
}

@ -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,
)

@ -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(

@ -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
)