diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index 1de21c19..40e5a479 100644 --- a/watchtower/lookout/justice_descriptor.go +++ b/watchtower/lookout/justice_descriptor.go @@ -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() diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 63c2c29f..c4c4d35e 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -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{ - SweepFeeRate: 2000, - RewardRate: 900000, - }, - RewardAddress: makeAddrSlice(22), + policy := wtpolicy.Policy{ + BlobType: blobType, + SweepFeeRate: 2000, + RewardRate: 900000, } - - // 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. diff --git a/watchtower/wtdb/session_info.go b/watchtower/wtdb/session_info.go index 5ff78172..f1b2e2a8 100644 --- a/watchtower/wtdb/session_info.go +++ b/watchtower/wtdb/session_info.go @@ -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 diff --git a/watchtower/wtpolicy/policy.go b/watchtower/wtpolicy/policy.go index 3336b07c..eb0f60e6 100644 --- a/watchtower/wtpolicy/policy.go +++ b/watchtower/wtpolicy/policy.go @@ -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 +}