Merge pull request #2515 from cfromknecht/altruist-output-calc
watchtower/wtpolicy: altruist output calculation
This commit is contained in:
commit
bcc9e30396
@ -8,6 +8,7 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/txsort"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/watchtower/blob"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtdb"
|
||||
@ -166,40 +167,41 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64,
|
||||
})
|
||||
}
|
||||
|
||||
// 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.SessionInfo.ComputeSweepOutputs(
|
||||
totalAmt, txWeight,
|
||||
// Using the session's policy, compute the outputs that should be added
|
||||
// to the justice transaction. In the case of an altruist sweep, there
|
||||
// will be a single output paying back to the victim. Otherwise for a
|
||||
// reward sweep, there will be two outputs, one of which pays back to
|
||||
// the victim while the other gives a cut to the tower.
|
||||
outputs, err := p.SessionInfo.Policy.ComputeJusticeTxOuts(
|
||||
totalAmt, txWeight, p.JusticeKit.SweepAddress[:],
|
||||
p.SessionInfo.RewardAddress,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(conner): abort/don't add if outputs are dusty
|
||||
// Attach the computed txouts to the justice transaction.
|
||||
justiceTxn.TxOut = outputs
|
||||
|
||||
// Add the sweep and reward outputs to the justice transaction.
|
||||
justiceTxn.AddTxOut(&wire.TxOut{
|
||||
PkScript: p.JusticeKit.SweepAddress[:],
|
||||
Value: int64(sweepAmt),
|
||||
})
|
||||
justiceTxn.AddTxOut(&wire.TxOut{
|
||||
PkScript: p.SessionInfo.RewardAddress,
|
||||
Value: int64(rewardAmt),
|
||||
})
|
||||
|
||||
// TODO(conner): apply and handle BIP69 sort
|
||||
// Apply a BIP69 sort to the resulting transaction.
|
||||
txsort.InPlaceSort(justiceTxn)
|
||||
|
||||
btx := btcutil.NewTx(justiceTxn)
|
||||
if err := blockchain.CheckTransactionSanity(btx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Since the transaction inputs could have been reordered as a result of the
|
||||
// BIP69 sort, create an index mapping each prevout to it's new index.
|
||||
inputIndex := make(map[wire.OutPoint]int)
|
||||
for i, txIn := range justiceTxn.TxIn {
|
||||
inputIndex[txIn.PreviousOutPoint] = i
|
||||
}
|
||||
|
||||
// Attach each of the provided witnesses to the transaction.
|
||||
for i, input := range inputs {
|
||||
for _, input := range inputs {
|
||||
// Lookup the input's new post-sort position.
|
||||
i := inputIndex[input.outPoint]
|
||||
justiceTxn.TxIn[i].Witness = input.witness
|
||||
|
||||
// Validate the reconstructed witnesses to ensure they are valid
|
||||
@ -229,9 +231,6 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
|
||||
weightEstimate input.TxWeightEstimator
|
||||
)
|
||||
|
||||
// Add our reward address to the weight estimate.
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
|
||||
// Add the sweep address's contribution, depending on whether it is a
|
||||
// p2wkh or p2wsh output.
|
||||
switch len(p.JusticeKit.SweepAddress) {
|
||||
@ -245,6 +244,12 @@ func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) {
|
||||
return nil, ErrUnknownSweepAddrType
|
||||
}
|
||||
|
||||
// Add our reward address to the weight estimate if the policy's blob
|
||||
// type specifies a reward output.
|
||||
if p.SessionInfo.Policy.BlobType.Has(blob.FlagReward) {
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
}
|
||||
|
||||
// Assemble the breached to-local output from the justice descriptor and
|
||||
// add it to our weight estimate.
|
||||
toLocalInput, err := p.commitToLocalInput()
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/txsort"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
@ -46,9 +47,39 @@ var (
|
||||
0x70, 0x4c, 0xff, 0x1e, 0x9c, 0x00, 0x93, 0xbe,
|
||||
0xe2, 0x2e, 0x68, 0x08, 0x4c, 0xb4, 0x0f, 0x4f,
|
||||
}
|
||||
|
||||
rewardCommitType = blob.TypeFromFlags(
|
||||
blob.FlagReward, blob.FlagCommitOutputs,
|
||||
)
|
||||
|
||||
altruistCommitType = blob.FlagCommitOutputs.Type()
|
||||
)
|
||||
|
||||
// TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the
|
||||
// correct justice transaction for different blob types.
|
||||
func TestJusticeDescriptor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blobType blob.Type
|
||||
}{
|
||||
{
|
||||
name: "reward and commit type",
|
||||
blobType: rewardCommitType,
|
||||
},
|
||||
{
|
||||
name: "altruist and commit type",
|
||||
blobType: altruistCommitType,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testJusticeDescriptor(t, test.blobType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
|
||||
const (
|
||||
localAmount = btcutil.Amount(100000)
|
||||
remoteAmount = btcutil.Amount(200000)
|
||||
@ -113,31 +144,25 @@ func TestJusticeDescriptor(t *testing.T) {
|
||||
|
||||
// Compute the weight estimate for our justice transaction.
|
||||
var weightEstimate input.TxWeightEstimator
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
weightEstimate.AddWitnessInput(input.ToLocalPenaltyWitnessSize)
|
||||
weightEstimate.AddWitnessInput(input.P2WKHWitnessSize)
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
if blobType.Has(blob.FlagReward) {
|
||||
weightEstimate.AddP2WKHOutput()
|
||||
}
|
||||
txWeight := weightEstimate.Weight()
|
||||
|
||||
// Create a session info so that simulate agreement of the sweep
|
||||
// parameters that should be used in constructing the justice
|
||||
// transaction.
|
||||
sessionInfo := &wtdb.SessionInfo{
|
||||
Policy: wtpolicy.Policy{
|
||||
policy := wtpolicy.Policy{
|
||||
BlobType: blobType,
|
||||
SweepFeeRate: 2000,
|
||||
RewardRate: 900000,
|
||||
},
|
||||
RewardAddress: makeAddrSlice(22),
|
||||
}
|
||||
|
||||
// Given the total input amount and the weight estimate, compute the
|
||||
// amount that should be swept for the victim and the amount taken as a
|
||||
// reward by the watchtower.
|
||||
sweepAmt, rewardAmt, err := sessionInfo.ComputeSweepOutputs(
|
||||
totalAmount, int64(txWeight),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute sweep outputs: %v", err)
|
||||
sessionInfo := &wtdb.SessionInfo{
|
||||
Policy: policy,
|
||||
RewardAddress: makeAddrSlice(22),
|
||||
}
|
||||
|
||||
// Begin to assemble the justice kit, starting with the sweep address,
|
||||
@ -170,20 +195,20 @@ func TestJusticeDescriptor(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
|
||||
Value: int64(sweepAmt),
|
||||
PkScript: justiceKit.SweepAddress,
|
||||
},
|
||||
{
|
||||
|
||||
Value: int64(rewardAmt),
|
||||
PkScript: sessionInfo.RewardAddress,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
outputs, err := policy.ComputeJusticeTxOuts(
|
||||
totalAmount, int64(txWeight), justiceKit.SweepAddress,
|
||||
sessionInfo.RewardAddress,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to compute justice txouts: %v", err)
|
||||
}
|
||||
|
||||
// Attach the txouts and BIP69 sort the resulting transaction.
|
||||
justiceTxn.TxOut = outputs
|
||||
txsort.InPlaceSort(justiceTxn)
|
||||
|
||||
hashCache := txscript.NewTxSigHashes(justiceTxn)
|
||||
|
||||
// Create the sign descriptor used to sign for the to-local input.
|
||||
|
@ -3,7 +3,6 @@ package wtdb
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtpolicy"
|
||||
)
|
||||
|
||||
@ -35,11 +34,6 @@ var (
|
||||
// number larger than the session's max number of updates.
|
||||
ErrSessionConsumed = errors.New("all session updates have been " +
|
||||
"consumed")
|
||||
|
||||
// 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 values")
|
||||
)
|
||||
|
||||
// SessionInfo holds the negotiated session parameters for single session id,
|
||||
@ -98,31 +92,6 @@ func (s *SessionInfo) AcceptUpdateSequence(seqNum, lastApplied uint16) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComputeSweepOutputs 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 fees are first subtracted from the overall total, before
|
||||
// splitting the remaining balance amongst the victim and tower.
|
||||
func (s *SessionInfo) ComputeSweepOutputs(totalAmt btcutil.Amount,
|
||||
txVSize int64) (btcutil.Amount, btcutil.Amount, error) {
|
||||
|
||||
txFee := s.Policy.SweepFeeRate.FeeForWeight(txVSize)
|
||||
if txFee > totalAmt {
|
||||
return 0, 0, ErrFeeExceedsInputs
|
||||
}
|
||||
|
||||
totalAmt -= txFee
|
||||
|
||||
// Apply the reward rate to the remaining total, specified in millionths
|
||||
// of the available balance.
|
||||
rewardRate := btcutil.Amount(s.Policy.RewardRate)
|
||||
rewardAmt := (totalAmt*rewardRate + 999999) / 1000000
|
||||
sweepAmt := totalAmt - rewardAmt
|
||||
|
||||
// TODO(conner): check dustiness
|
||||
|
||||
return sweepAmt, rewardAmt, nil
|
||||
}
|
||||
|
||||
// Match is returned in response to a database query for a breach hints
|
||||
// contained in a particular block. The match encapsulates all data required to
|
||||
// properly decrypt a client's encrypted blob, and pursue action on behalf of
|
||||
|
@ -1,13 +1,21 @@
|
||||
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
|
||||
@ -22,6 +30,21 @@ const (
|
||||
DefaultSweepFeeRate = 3000
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// DefaultPolicy returns a Policy containing the default parameters that can be
|
||||
// used by clients or servers.
|
||||
func DefaultPolicy() Policy {
|
||||
@ -71,3 +94,150 @@ func (p Policy) String() string {
|
||||
"sweep-fee-rate=%d)", p.BlobType, p.MaxUpdates, p.RewardRate,
|
||||
p.SweepFeeRate)
|
||||
}
|
||||
|
||||
// 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 substracted 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user