293 lines
10 KiB
Go
293 lines
10 KiB
Go
package wtpolicy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/watchtower/blob"
|
|
)
|
|
|
|
const (
|
|
// RewardScale is the denominator applied when computing the
|
|
// proportional component for a tower's reward output. The current scale
|
|
// is in millionths.
|
|
RewardScale = 1000000
|
|
|
|
// DefaultMaxUpdates specifies the number of encrypted blobs a client
|
|
// can send to the tower in a single session.
|
|
DefaultMaxUpdates = 1024
|
|
|
|
// DefaultRewardRate specifies the fraction of the channel that the
|
|
// tower takes if it successfully sweeps a breach. The value is
|
|
// expressed in millionths of the channel capacity.
|
|
DefaultRewardRate = 10000
|
|
|
|
// DefaultSweepFeeRate specifies the fee rate used to construct justice
|
|
// transactions. The value is expressed in satoshis per kilo-weight.
|
|
DefaultSweepFeeRate = lnwallet.SatPerKWeight(40000)
|
|
|
|
// 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 (
|
|
// ErrFeeExceedsInputs signals that the total input value of breaching
|
|
// commitment txn is insufficient to cover the fees required to sweep
|
|
// it.
|
|
ErrFeeExceedsInputs = errors.New("sweep fee exceeds input value")
|
|
|
|
// ErrRewardExceedsInputs signals that the reward given to the tower (in
|
|
// addition to the transaction fees) is more than the input amount.
|
|
ErrRewardExceedsInputs = errors.New("reward amount exceeds input value")
|
|
|
|
// 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
|
|
// used by clients or servers.
|
|
func DefaultPolicy() Policy {
|
|
return Policy{
|
|
TxPolicy: TxPolicy{
|
|
BlobType: blob.TypeAltruistCommit,
|
|
SweepFeeRate: DefaultSweepFeeRate,
|
|
},
|
|
MaxUpdates: DefaultMaxUpdates,
|
|
}
|
|
}
|
|
|
|
// TxPolicy defines the negotiate parameters that determine the form of the
|
|
// justice transaction for a given breached state. Thus, for any given revoked
|
|
// state, an identical key will result in an identical justice transaction
|
|
// (barring signatures). The parameters specify the format of encrypted blobs
|
|
// sent to the tower, the reward schedule for the tower, and the number of
|
|
// encrypted blobs a client can send in one session.
|
|
type TxPolicy struct {
|
|
// BlobType specifies the blob format that must be used by all updates sent
|
|
// under the session key used to negotiate this session.
|
|
BlobType blob.Type
|
|
|
|
// RewardBase is the fixed amount allocated to the tower when the
|
|
// policy's blob type specifies a reward for the tower. This is taken
|
|
// before adding the proportional reward.
|
|
RewardBase uint32
|
|
|
|
// RewardRate is the fraction of the total balance of the revoked
|
|
// commitment that the watchtower is entitled to. This value is
|
|
// expressed in millionths of the total balance.
|
|
RewardRate uint32
|
|
|
|
// SweepFeeRate expresses the intended fee rate to be used when
|
|
// constructing the justice transaction. All sweep transactions created
|
|
// for this session must use this value during construction, and the
|
|
// signatures must implicitly commit to the resulting output values.
|
|
SweepFeeRate lnwallet.SatPerKWeight
|
|
}
|
|
|
|
// Policy defines the negotiated parameters for a session between a client and
|
|
// server. In addition to the TxPolicy that governs the shape of the justice
|
|
// transaction, the Policy also includes features which only affect the
|
|
// operation of the session.
|
|
type Policy struct {
|
|
TxPolicy
|
|
|
|
// MaxUpdates is the maximum number of updates the watchtower will honor
|
|
// for this session.
|
|
MaxUpdates uint16
|
|
}
|
|
|
|
// String returns a human-readable description of the current policy.
|
|
func (p Policy) String() string {
|
|
return fmt.Sprintf("(blob-type=%b max-updates=%d reward-rate=%d "+
|
|
"sweep-fee-rate=%d)", p.BlobType, p.MaxUpdates, p.RewardRate,
|
|
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
|
|
// policy's fee rate.
|
|
func (p *Policy) ComputeAltruistOutput(totalAmt btcutil.Amount,
|
|
txWeight int64) (btcutil.Amount, error) {
|
|
|
|
txFee := p.SweepFeeRate.FeeForWeight(txWeight)
|
|
if txFee > totalAmt {
|
|
return 0, ErrFeeExceedsInputs
|
|
}
|
|
|
|
sweepAmt := totalAmt - txFee
|
|
|
|
// TODO(conner): replace w/ configurable dust limit
|
|
dustLimit := lnwallet.DefaultDustLimit()
|
|
|
|
// Check that the created outputs won't be dusty.
|
|
if sweepAmt <= dustLimit {
|
|
return 0, ErrCreatesDust
|
|
}
|
|
|
|
return sweepAmt, nil
|
|
}
|
|
|
|
// ComputeRewardOutputs splits the total funds in a breaching commitment
|
|
// transaction between the victim and the tower, according to the sweep fee rate
|
|
// and reward rate. The reward to he tower is subtracted first, before
|
|
// splitting the remaining balance amongst the victim and fees.
|
|
func (p *Policy) ComputeRewardOutputs(totalAmt btcutil.Amount,
|
|
txWeight int64) (btcutil.Amount, btcutil.Amount, error) {
|
|
|
|
txFee := p.SweepFeeRate.FeeForWeight(txWeight)
|
|
if txFee > totalAmt {
|
|
return 0, 0, ErrFeeExceedsInputs
|
|
}
|
|
|
|
// Apply the reward rate to the remaining total, specified in millionths
|
|
// of the available balance.
|
|
rewardAmt := ComputeRewardAmount(totalAmt, p.RewardBase, p.RewardRate)
|
|
if rewardAmt+txFee > totalAmt {
|
|
return 0, 0, ErrRewardExceedsInputs
|
|
}
|
|
|
|
// The sweep amount for the victim constitutes the remainder of the
|
|
// input value.
|
|
sweepAmt := totalAmt - rewardAmt - txFee
|
|
|
|
// TODO(conner): replace w/ configurable dust limit
|
|
dustLimit := lnwallet.DefaultDustLimit()
|
|
|
|
// Check that the created outputs won't be dusty.
|
|
if sweepAmt <= dustLimit {
|
|
return 0, 0, ErrCreatesDust
|
|
}
|
|
|
|
return sweepAmt, rewardAmt, nil
|
|
}
|
|
|
|
// ComputeRewardAmount computes the amount rewarded to the tower using the
|
|
// proportional rate expressed in millionths, e.g. one million is equivalent to
|
|
// one hundred percent of the total amount. The amount is rounded up to the
|
|
// nearest whole satoshi.
|
|
func ComputeRewardAmount(total btcutil.Amount, base, rate uint32) btcutil.Amount {
|
|
rewardBase := btcutil.Amount(base)
|
|
rewardRate := btcutil.Amount(rate)
|
|
|
|
// If the base reward exceeds the total, there is no more funds left
|
|
// from which to derive the proportional fee. We simply return the base,
|
|
// the caller should detect that this exceeds the total amount input.
|
|
if rewardBase > total {
|
|
return rewardBase
|
|
}
|
|
|
|
// Otherwise, subtract the base from the total and compute the
|
|
// proportional reward from the remaining total.
|
|
afterBase := total - rewardBase
|
|
proportional := (afterBase*rewardRate + RewardScale - 1) / RewardScale
|
|
|
|
return rewardBase + proportional
|
|
}
|
|
|
|
// ComputeJusticeTxOuts constructs the justice transaction outputs for the given
|
|
// policy. If the policy specifies a reward for the tower, there will be two
|
|
// outputs paying to the victim and the tower. Otherwise there will be a single
|
|
// output sweeping funds back to the victim. The totalAmt should be the sum of
|
|
// any inputs used in the transaction. The passed txWeight should include the
|
|
// weight of the outputs for the justice transaction, which is dependent on
|
|
// whether the justice transaction has a reward. The sweepPkScript should be the
|
|
// pkScript of the victim to which funds will be recovered. The rewardPkScript
|
|
// is the pkScript of the tower where its reward will be deposited, and will be
|
|
// ignored if the blob type does not specify a reward.
|
|
func (p *Policy) ComputeJusticeTxOuts(totalAmt btcutil.Amount, txWeight int64,
|
|
sweepPkScript, rewardPkScript []byte) ([]*wire.TxOut, error) {
|
|
|
|
var outputs []*wire.TxOut
|
|
|
|
// If the policy specifies a reward for the tower, compute a split of
|
|
// the funds based on the policy's parameters. Otherwise, we will use an
|
|
// the altruist output computation and sweep as much of the funds back
|
|
// to the victim as possible.
|
|
if p.BlobType.Has(blob.FlagReward) {
|
|
// Using the total input amount and the transaction's weight,
|
|
// compute the sweep and reward amounts. This corresponds to the
|
|
// amount returned to the victim and the amount paid to the
|
|
// tower, respectively. To do so, the required transaction fee
|
|
// is subtracted from the total, and the remaining amount is
|
|
// divided according to the prenegotiated reward rate from the
|
|
// client's session info.
|
|
sweepAmt, rewardAmt, err := p.ComputeRewardOutputs(
|
|
totalAmt, txWeight,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add the sweep and reward outputs to the list of txouts.
|
|
outputs = append(outputs, &wire.TxOut{
|
|
PkScript: sweepPkScript,
|
|
Value: int64(sweepAmt),
|
|
})
|
|
outputs = append(outputs, &wire.TxOut{
|
|
PkScript: rewardPkScript,
|
|
Value: int64(rewardAmt),
|
|
})
|
|
} else {
|
|
// Using the total input amount and the transaction's weight,
|
|
// compute the sweep amount, which corresponds to the amount
|
|
// returned to the victim. To do so, the required transaction
|
|
// fee is subtracted from the total input amount.
|
|
sweepAmt, err := p.ComputeAltruistOutput(
|
|
totalAmt, txWeight,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add the sweep output to the list of txouts.
|
|
outputs = append(outputs, &wire.TxOut{
|
|
PkScript: sweepPkScript,
|
|
Value: int64(sweepAmt),
|
|
})
|
|
}
|
|
|
|
return outputs, nil
|
|
}
|