diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index 1de21c19a..40e5a479e 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 63c2c29f2..c4c4d35e3 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 5ff781729..f1b2e2a81 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