diff --git a/server.go b/server.go index 73a0ef6e..b0c2350f 100644 --- a/server.go +++ b/server.go @@ -1069,6 +1069,10 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, policy.SweepFeeRate = sweepRateSatPerByte.FeePerKWeight() } + if err := policy.Validate(); err != nil { + return nil, err + } + s.towerClient, err = wtclient.New(&wtclient.Config{ Signer: cc.wallet.Cfg.Signer, NewAddress: newSweepPkScriptGen(cc.wallet), diff --git a/watchtower/wtpolicy/policy.go b/watchtower/wtpolicy/policy.go index ca1debd3..163c1f1e 100644 --- a/watchtower/wtpolicy/policy.go +++ b/watchtower/wtpolicy/policy.go @@ -27,7 +27,11 @@ const ( // DefaultSweepFeeRate specifies the fee rate used to construct justice // transactions. The value is expressed in satoshis per kilo-weight. - DefaultSweepFeeRate = 3000 + DefaultSweepFeeRate = lnwallet.SatPerKWeight(12000) + + // MinSweepFeeRate is the minimum sweep fee rate a client may use in its + // policy, the current value is 4 sat/kw. + MinSweepFeeRate = lnwallet.SatPerKWeight(4000) ) var ( @@ -43,6 +47,17 @@ var ( // ErrCreatesDust signals that the session's policy would create a dust // output for the victim. ErrCreatesDust = errors.New("justice transaction creates dust at fee rate") + + // ErrAltruistReward signals that the policy is invalid because it + // contains a non-zero RewardBase or RewardRate on an altruist policy. + ErrAltruistReward = errors.New("altruist policy has reward params") + + // ErrNoMaxUpdates signals that the policy specified zero MaxUpdates. + ErrNoMaxUpdates = errors.New("max updates must be positive") + + // ErrSweepFeeRateTooLow signals that the policy's fee rate is too low + // to get into the mempool during low congestion. + ErrSweepFeeRateTooLow = errors.New("sweep fee rate too low") ) // DefaultPolicy returns a Policy containing the default parameters that can be @@ -50,11 +65,8 @@ var ( func DefaultPolicy() Policy { return Policy{ TxPolicy: TxPolicy{ - BlobType: blob.TypeAltruistCommit, - RewardRate: DefaultRewardRate, - SweepFeeRate: lnwallet.SatPerKWeight( - DefaultSweepFeeRate, - ), + BlobType: blob.TypeAltruistCommit, + SweepFeeRate: DefaultSweepFeeRate, }, MaxUpdates: DefaultMaxUpdates, } @@ -107,6 +119,31 @@ func (p Policy) String() string { p.SweepFeeRate) } +// Validate ensures that the policy satisfies some minimal correctness +// constraints. +func (p Policy) Validate() error { + // RewardBase and RewardRate should not be set if the policy doesn't + // have a reward. + if !p.BlobType.Has(blob.FlagReward) && + (p.RewardBase != 0 || p.RewardRate != 0) { + + return ErrAltruistReward + } + + // MaxUpdates must be positive. + if p.MaxUpdates == 0 { + return ErrNoMaxUpdates + } + + // SweepFeeRate must be sane enough to get in the mempool during low + // congestion. + if p.SweepFeeRate < MinSweepFeeRate { + return ErrSweepFeeRateTooLow + } + + return nil +} + // ComputeAltruistOutput computes the lone output value of a justice transaction // that pays no reward to the tower. The value is computed using the weight of // of the justice transaction and subtracting an amount that satisfies the diff --git a/watchtower/wtpolicy/policy_test.go b/watchtower/wtpolicy/policy_test.go new file mode 100644 index 00000000..4182a0de --- /dev/null +++ b/watchtower/wtpolicy/policy_test.go @@ -0,0 +1,93 @@ +package wtpolicy_test + +import ( + "testing" + + "github.com/lightningnetwork/lnd/watchtower/blob" + "github.com/lightningnetwork/lnd/watchtower/wtpolicy" +) + +var validationTests = []struct { + name string + policy wtpolicy.Policy + expErr error +}{ + { + name: "fail no maxupdates", + policy: wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + }, + }, + expErr: wtpolicy.ErrNoMaxUpdates, + }, + { + name: "fail altruist with reward base", + policy: wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + RewardBase: 1, + }, + }, + expErr: wtpolicy.ErrAltruistReward, + }, + { + name: "fail altruist with reward rate", + policy: wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + RewardRate: 1, + }, + }, + expErr: wtpolicy.ErrAltruistReward, + }, + { + name: "fail sweep fee rate too low", + policy: wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + }, + MaxUpdates: 1, + }, + expErr: wtpolicy.ErrSweepFeeRateTooLow, + }, + { + name: "minimal valid altruist policy", + policy: wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + SweepFeeRate: wtpolicy.MinSweepFeeRate, + }, + MaxUpdates: 1, + }, + }, + { + name: "valid altruist policy with default sweep rate", + policy: wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistCommit, + SweepFeeRate: wtpolicy.DefaultSweepFeeRate, + }, + MaxUpdates: 1, + }, + }, + { + name: "valid default policy", + policy: wtpolicy.DefaultPolicy(), + }, +} + +// TestPolicyValidate asserts that the sanity checks for policies behave as +// expected. +func TestPolicyValidate(t *testing.T) { + for i := range validationTests { + test := validationTests[i] + t.Run(test.name, func(t *testing.T) { + err := test.policy.Validate() + if err != test.expErr { + t.Fatalf("validation error mismatch, "+ + "want: %v, got: %v", test.expErr, err) + } + }) + } +}