diff --git a/breacharbiter.go b/breacharbiter.go index 172a053e..c2d15165 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -579,9 +579,24 @@ func (b *breachArbiter) exactRetribution( return } - // TODO(roasbeef): factor in HTLCs - revokedFunds := breachInfo.revokedOutput.amt - totalFunds := revokedFunds + breachInfo.selfOutput.amt + // Compute both the total value of funds being swept and the + // amount of funds that were revoked from the counter party. + var totalFunds, revokedFunds btcutil.Amount + for _, input := range breachInfo.breachedOutputs { + totalFunds += input.Amount() + + // If the output being revoked is the remote commitment + // output or an offered HTLC output, it's amount + // contributes to the value of funds being revoked from + // the counter party. + switch input.WitnessType() { + case lnwallet.CommitmentRevoke: + revokedFunds += input.Amount() + case lnwallet.HtlcOfferedRevoke: + revokedFunds += input.Amount() + default: + } + } brarLog.Infof("Justice for ChannelPoint(%v) has "+ "been served, %v revoked funds (%v total) "+ @@ -627,7 +642,7 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel, chanPoint := contract.ChannelPoint() - brarLog.Debugf("Breach observer for ChannelPoint(%v) started", + brarLog.Debugf("Breach observer for ChannelPoint(%v) started ", chanPoint) select { @@ -800,6 +815,15 @@ type SpendableOutput interface { // construct the corresponding transaction input. OutPoint() *wire.OutPoint + // WitnessType returns an enum specifying the type of witness that must + // be generated in order to spend this output. + WitnessType() lnwallet.WitnessType + + // SignDesc returns a reference to a spendable output's sign descriptor, + // which is used during signing to compute a valid witness that spends + // this output. + SignDesc() *lnwallet.SignDescriptor + // BuildWitness returns a valid witness allowing this output to be // spent, the witness should be attached to the transaction at the // location determined by the given `txinIdx`. @@ -820,15 +844,15 @@ type breachedOutput struct { witnessFunc lnwallet.WitnessGenerator } -// newBreachedOutput assembles a new breachedOutput that can be used by the +// makeBreachedOutput assembles a new breachedOutput that can be used by the // breach arbiter to construct a justice or sweep transaction. -func newBreachedOutput(outpoint *wire.OutPoint, +func makeBreachedOutput(outpoint *wire.OutPoint, witnessType lnwallet.WitnessType, - signDescriptor *lnwallet.SignDescriptor) *breachedOutput { + signDescriptor *lnwallet.SignDescriptor) breachedOutput { amount := signDescriptor.Output.Value - return &breachedOutput{ + return breachedOutput{ amt: btcutil.Amount(amount), outpoint: *outpoint, witnessType: witnessType, @@ -841,12 +865,24 @@ func (bo *breachedOutput) Amount() btcutil.Amount { return bo.amt } -// OutPoint returns the breached outputs identifier that is to be included as a +// OutPoint returns the breached output's identifier that is to be included as a // transaction input. func (bo *breachedOutput) OutPoint() *wire.OutPoint { return &bo.outpoint } +// WitnessType returns the type of witness that must be generated to spend the +// breached output. +func (bo *breachedOutput) WitnessType() lnwallet.WitnessType { + return bo.witnessType +} + +// SignDesc returns the breached output's SignDescriptor, which is used during +// signing to compute the witness. +func (bo *breachedOutput) SignDesc() *lnwallet.SignDescriptor { + return &bo.signDesc +} + // BuildWitness computes a valid witness that allows us to spend from the // breached output. It does so by first generating and memoizing the witness // generation function, which parameterized primarily by the witness type and @@ -861,7 +897,7 @@ func (bo *breachedOutput) BuildWitness(signer lnwallet.Signer, // been initialized for this breached output. if bo.witnessFunc == nil { bo.witnessFunc = bo.witnessType.GenWitnessFunc( - signer, &bo.signDesc) + signer, bo.SignDesc()) } // Now that we have ensured that the witness generation function has @@ -894,13 +930,7 @@ type retributionInfo struct { capacity btcutil.Amount settledBalance btcutil.Amount - selfOutput *breachedOutput - - revokedOutput *breachedOutput - - htlcOutputs []*breachedOutput - - doneChan chan struct{} + breachedOutputs []breachedOutput } // newRetributionInfo constructs a retributionInfo containing all the @@ -911,31 +941,48 @@ func newRetributionInfo(chanPoint *wire.OutPoint, breachInfo *lnwallet.BreachRetribution, chanInfo *channeldb.ChannelSnapshot) *retributionInfo { - // First, record the breach information and witness type for the local - // channel point. This will allow us to completely generate a valid - // witness in the event of failures, as it will be persisted in the - // retribution store. Here we use CommitmentNoDelay since this output - // belongs to us and has no time-based constraints on spending. - selfOutput := newBreachedOutput(&breachInfo.LocalOutpoint, - lnwallet.CommitmentNoDelay, &breachInfo.LocalOutputSignDesc) - - // Second, record the same information and witness type regarding the - // remote outpoint, which belongs to the party who tried to steal our - // money! Here we set witnessType of the breachedOutput to - // CommitmentRevoke, since we will be using a revoke key, withdrawing - // the funds from the commitment transaction immediately. - revokedOutput := newBreachedOutput(&breachInfo.RemoteOutpoint, - lnwallet.CommitmentRevoke, &breachInfo.RemoteOutputSignDesc) - // Determine the number of second layer HTLCs we will attempt to sweep. nHtlcs := len(breachInfo.HtlcRetributions) - // Lastly, for each of the breached HTLC outputs, assemble the - // information we will persist to disk, such that we will be able to - // deterministically generate a valid witness for each output. This will - // allow the breach arbiter to recover from failures, in the event that - // it must sign and broadcast the justice transaction. - htlcOutputs := make([]*breachedOutput, nHtlcs) + // Initialize a slice to hold the outputs we will attempt to sweep. The + // maximum capacity of the slice is set to 2+nHtlcs to handle the case + // where the local, remote, and all HTLCs are not dust outputs. All + // HTLC outputs provided by the wallet are guaranteed to be non-dust, + // though the commitment outputs are conditionally added depending on + // the nil-ness of their sign descriptors. + breachedOutputs := make([]breachedOutput, 0, nHtlcs+2) + + // First, record the breach information for the local channel point if + // it is not considered dust, which is signaled by a non-nil sign + // descriptor. Here we use CommitmentNoDelay since this output belongs + // to us and has no time-based constraints on spending. + if breachInfo.LocalOutputSignDesc != nil { + localOutput := makeBreachedOutput( + &breachInfo.LocalOutpoint, + lnwallet.CommitmentNoDelay, + breachInfo.LocalOutputSignDesc) + + breachedOutputs = append(breachedOutputs, localOutput) + } + + // Second, record the same information regarding the remote outpoint, + // again if it is not dust, which belongs to the party who tried to + // steal our money! Here we set witnessType of the breachedOutput to + // CommitmentRevoke, since we will be using a revoke key, withdrawing + // the funds from the commitment transaction immediately. + if breachInfo.RemoteOutputSignDesc != nil { + remoteOutput := makeBreachedOutput( + &breachInfo.RemoteOutpoint, + lnwallet.CommitmentRevoke, + breachInfo.RemoteOutputSignDesc) + + breachedOutputs = append(breachedOutputs, remoteOutput) + } + + // Lastly, for each of the breached HTLC outputs, record each as a + // breached output with the appropriate witness type based on its + // directionality. All HTLC outputs provided by the wallet are assumed + // to be non-dust. for i, breachedHtlc := range breachInfo.HtlcRetributions { // Using the breachedHtlc's incoming flag, determine the // appropriate witness type that needs to be generated in order @@ -947,23 +994,24 @@ func newRetributionInfo(chanPoint *wire.OutPoint, htlcWitnessType = lnwallet.HtlcOfferedRevoke } - htlcOutputs[i] = newBreachedOutput( - &breachInfo.HtlcRetributions[i].OutPoint, htlcWitnessType, + htlcOutput := makeBreachedOutput( + &breachInfo.HtlcRetributions[i].OutPoint, + htlcWitnessType, &breachInfo.HtlcRetributions[i].SignDesc) + + breachedOutputs = append(breachedOutputs, htlcOutput) } // TODO(conner): remove dependency on channel snapshot after decoupling // channel closure from the breach arbiter. return &retributionInfo{ - commitHash: breachInfo.BreachTransaction.TxHash(), - chanPoint: *chanPoint, - remoteIdentity: &chanInfo.RemoteIdentity, - capacity: chanInfo.Capacity, - settledBalance: chanInfo.LocalBalance.ToSatoshis(), - selfOutput: selfOutput, - revokedOutput: revokedOutput, - htlcOutputs: htlcOutputs, + commitHash: breachInfo.BreachTransaction.TxHash(), + chanPoint: *chanPoint, + remoteIdentity: &chanInfo.RemoteIdentity, + capacity: chanInfo.Capacity, + settledBalance: chanInfo.LocalBalance.ToSatoshis(), + breachedOutputs: breachedOutputs, } } @@ -974,43 +1022,71 @@ func newRetributionInfo(chanPoint *wire.OutPoint, func (b *breachArbiter) createJusticeTx( r *retributionInfo) (*wire.MsgTx, error) { - // Determine the number of HTLCs to be swept by the justice txn. - nHtlcs := len(r.htlcOutputs) + // We will assemble the breached outputs into a slice of spendable + // outputs, while simultaneously computing the estimated weight of the + // transaction. + var ( + spendableOutputs []SpendableOutput + txWeight uint64 + ) - // Assemble the breached outputs into a slice of spendable outputs, - // starting with the self and revoked outputs, then adding any htlc - // outputs. - breachedOutputs := make([]SpendableOutput, 2+nHtlcs) - breachedOutputs[0] = r.selfOutput - breachedOutputs[1] = r.revokedOutput - for i, htlcOutput := range r.htlcOutputs { - breachedOutputs[2+i] = htlcOutput - } + // Allocate enough space to potentially hold each of the breached + // outputs in the retribution info. + spendableOutputs = make([]SpendableOutput, 0, len(r.breachedOutputs)) - // Compute the transaction weight of the justice transaction, which - // includes 2 + nHtlcs inputs and one output. - var txWeight uint64 - // Begin with a base txn weight, e.g. version, nLockTime, etc. + // The justice transaction we construct will be a segwit transaction + // that pays to a p2wkh output. Components such as the version, + // nLockTime, and output are included in the BaseSweepTxSize, while the + // WitnessHeaderSize accounts for the two bytes that signal this as a + // segwit transaction. txWeight += 4*lnwallet.BaseSweepTxSize + lnwallet.WitnessHeaderSize - // Add to_local revoke script and tx input. - txWeight += 4*lnwallet.InputSize + lnwallet.ToLocalPenaltyWitnessSize - // Add to_remote p2wpkh witness and tx input. - txWeight += 4*lnwallet.InputSize + lnwallet.P2WKHWitnessSize - // Compute the appropriate weight contributed by each revoked accepted - // or offered HTLC witnesses and tx inputs. - for _, htlcOutput := range r.htlcOutputs { - switch htlcOutput.witnessType { + // Next, we iterate over the breached outputs contained in the + // retribution info. For each, we switch over the witness type such + // that we contribute the appropriate weight for each input and witness, + // finally adding to our list of spendable outputs. + for i := range r.breachedOutputs { + // Grab locally scoped reference to breached output. + input := &r.breachedOutputs[i] + + // First, select the appropriate estimated witness weight for + // the give witness type of this breached output. If the witness + // type is unrecognized, we will omit it from the transaction. + var witnessWeight uint64 + switch input.WitnessType() { + case lnwallet.CommitmentNoDelay: + witnessWeight = lnwallet.ToLocalPenaltyWitnessSize + + case lnwallet.CommitmentRevoke: + witnessWeight = lnwallet.P2WKHWitnessSize + case lnwallet.HtlcOfferedRevoke: - txWeight += 4*lnwallet.InputSize + - lnwallet.OfferedHtlcPenaltyWitnessSize + witnessWeight = lnwallet.OfferedHtlcPenaltyWitnessSize + case lnwallet.HtlcAcceptedRevoke: - txWeight += 4*lnwallet.InputSize + - lnwallet.AcceptedHtlcPenaltyWitnessSize + witnessWeight = lnwallet.AcceptedHtlcPenaltyWitnessSize + + default: + brarLog.Warnf("breached output in retribution info "+ + "contains unexpected witness type: %v", + input.WitnessType()) + continue } + + // Next, each of the outputs in the retribution info will be + // used as inputs to the justice transaction. An input is + // considered non-witness data, so it is scaled accordingly. + txWeight += 4 * lnwallet.InputSize + + // Additionally, we contribute the weight of the witness + // directly to the total transaction weight. + txWeight += witnessWeight + + // Finally, append this input to our list of spendable outputs. + spendableOutputs = append(spendableOutputs, input) } - return b.sweepSpendableOutputsTxn(txWeight, breachedOutputs...) + return b.sweepSpendableOutputsTxn(txWeight, spendableOutputs...) } // craftCommitmentSweepTx creates a transaction to sweep the non-delayed output @@ -1024,7 +1100,7 @@ func (b *breachArbiter) createJusticeTx( func (b *breachArbiter) craftCommitSweepTx( closeInfo *lnwallet.UnilateralCloseSummary) (*wire.MsgTx, error) { - selfOutput := newBreachedOutput( + selfOutput := makeBreachedOutput( closeInfo.SelfOutPoint, lnwallet.CommitmentNoDelay, closeInfo.SelfOutputSignDesc, @@ -1038,7 +1114,7 @@ func (b *breachArbiter) craftCommitSweepTx( // Add to_local p2wpkh witness and tx input. txWeight += 4*lnwallet.InputSize + lnwallet.P2WKHWitnessSize - return b.sweepSpendableOutputsTxn(txWeight, selfOutput) + return b.sweepSpendableOutputsTxn(txWeight, &selfOutput) } // sweepSpendableOutputsTxn creates a signed transaction from a sequence of @@ -1272,21 +1348,13 @@ func (ret *retributionInfo) Encode(w io.Writer) error { return err } - if err := ret.selfOutput.Encode(w); err != nil { + nOutputs := len(ret.breachedOutputs) + if err := wire.WriteVarInt(w, 0, uint64(nOutputs)); err != nil { return err } - if err := ret.revokedOutput.Encode(w); err != nil { - return err - } - - numHtlcOutputs := len(ret.htlcOutputs) - if err := wire.WriteVarInt(w, 0, uint64(numHtlcOutputs)); err != nil { - return err - } - - for i := 0; i < numHtlcOutputs; i++ { - if err := ret.htlcOutputs[i].Encode(w); err != nil { + for _, output := range ret.breachedOutputs { + if err := output.Encode(w); err != nil { return err } } @@ -1331,26 +1399,15 @@ func (ret *retributionInfo) Decode(r io.Reader) error { ret.settledBalance = btcutil.Amount( binary.BigEndian.Uint64(scratch[:8])) - ret.selfOutput = &breachedOutput{} - if err := ret.selfOutput.Decode(r); err != nil { - return err - } - - ret.revokedOutput = &breachedOutput{} - if err := ret.revokedOutput.Decode(r); err != nil { - return err - } - - numHtlcOutputsU64, err := wire.ReadVarInt(r, 0) + nOutputsU64, err := wire.ReadVarInt(r, 0) if err != nil { return err } - numHtlcOutputs := int(numHtlcOutputsU64) + nOutputs := int(nOutputsU64) - ret.htlcOutputs = make([]*breachedOutput, numHtlcOutputs) - for i := range ret.htlcOutputs { - ret.htlcOutputs[i] = &breachedOutput{} - if err := ret.htlcOutputs[i].Decode(r); err != nil { + ret.breachedOutputs = make([]breachedOutput, nOutputs) + for i := range ret.breachedOutputs { + if err := ret.breachedOutputs[i].Decode(r); err != nil { return err } } diff --git a/breacharbiter_test.go b/breacharbiter_test.go index fce8999f..a6a44b39 100644 --- a/breacharbiter_test.go +++ b/breacharbiter_test.go @@ -78,116 +78,135 @@ var ( }, } - breachSignDescs = []lnwallet.SignDescriptor{ - { - SingleTweak: []byte{ - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, - }, - WitnessScript: []byte{ - 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde, - 0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, - 0xef, 0xb5, 0x71, 0x48, - }, - Output: &wire.TxOut{ - Value: 5000000000, - PkScript: []byte{ - 0x41, // OP_DATA_65 - 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, - 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, - 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, - 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, - 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, - 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, - 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, - 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, - 0xa6, // 65-byte signature - 0xac, // OP_CHECKSIG - }, - }, - HashType: txscript.SigHashAll, - }, - { - SingleTweak: []byte{ - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, - }, - WitnessScript: []byte{ - 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde, - 0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, - 0xef, 0xb5, 0x71, 0x48, - }, - Output: &wire.TxOut{ - Value: 5000000000, - PkScript: []byte{ - 0x41, // OP_DATA_65 - 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, - 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, - 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, - 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, - 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, - 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, - 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, - 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, - 0xa6, // 65-byte signature - 0xac, // OP_CHECKSIG - }, - }, - HashType: txscript.SigHashAll, - }, - { - SingleTweak: []byte{ - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x02, 0x02, - }, - WitnessScript: []byte{ - 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde, - 0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, - 0xef, 0xb5, 0x71, 0x48, - }, - Output: &wire.TxOut{ - Value: 5000000000, - PkScript: []byte{ - 0x41, // OP_DATA_65 - 0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5, - 0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42, - 0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1, - 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24, - 0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97, - 0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, - 0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20, - 0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63, - 0xa6, // 65-byte signature - 0xac, // OP_CHECKSIG - }, - }, - HashType: txscript.SigHashAll, - }, - } - breachedOutputs = []breachedOutput{ { amt: btcutil.Amount(1e7), outpoint: breachOutPoints[0], witnessType: lnwallet.CommitmentNoDelay, + signDesc: lnwallet.SignDescriptor{ + SingleTweak: []byte{ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, + }, + WitnessScript: []byte{ + 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, + 0x85, 0x6c, 0xde, 0x10, 0xa2, 0x91, + 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, + 0xef, 0xb5, 0x71, 0x48, + }, + Output: &wire.TxOut{ + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, + 0x9e, 0xb1, 0xc5, 0xfe, 0x29, + 0x5a, 0xbd, 0xeb, 0x1d, 0xca, + 0x42, 0x81, 0xbe, 0x98, 0x8e, + 0x2d, 0xa0, 0xb6, 0xc1, 0xc6, + 0xa5, 0x9d, 0xc2, 0x26, 0xc2, + 0x86, 0x24, 0xe1, 0x81, 0x75, + 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, + 0x1f, 0x04, 0x78, 0x34, 0xbc, + 0x06, 0xd6, 0xd6, 0xed, 0xf6, + 0x20, 0xd1, 0x84, 0x24, 0x1a, + 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + HashType: txscript.SigHashAll, + }, }, - { amt: btcutil.Amount(2e9), outpoint: breachOutPoints[1], witnessType: lnwallet.CommitmentRevoke, + signDesc: lnwallet.SignDescriptor{ + SingleTweak: []byte{ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, + }, + WitnessScript: []byte{ + 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, + 0x85, 0x6c, 0xde, 0x10, 0xa2, 0x91, + 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, + 0xef, 0xb5, 0x71, 0x48, + }, + Output: &wire.TxOut{ + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, + 0x9e, 0xb1, 0xc5, 0xfe, 0x29, + 0x5a, 0xbd, 0xeb, 0x1d, 0xca, + 0x42, 0x81, 0xbe, 0x98, 0x8e, + 0x2d, 0xa0, 0xb6, 0xc1, 0xc6, + 0xa5, 0x9d, 0xc2, 0x26, 0xc2, + 0x86, 0x24, 0xe1, 0x81, 0x75, + 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, + 0x1f, 0x04, 0x78, 0x34, 0xbc, + 0x06, 0xd6, 0xd6, 0xed, 0xf6, + 0x20, 0xd1, 0x84, 0x24, 0x1a, + 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + HashType: txscript.SigHashAll, + }, }, - { amt: btcutil.Amount(3e4), outpoint: breachOutPoints[2], witnessType: lnwallet.CommitmentDelayOutput, + signDesc: lnwallet.SignDescriptor{ + SingleTweak: []byte{ + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, + }, + WitnessScript: []byte{ + 0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, + 0x85, 0x6c, 0xde, 0x10, 0xa2, 0x91, + 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2, + 0xef, 0xb5, 0x71, 0x48, + }, + Output: &wire.TxOut{ + Value: 5000000000, + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0xd6, 0x4b, 0xdf, 0xd0, + 0x9e, 0xb1, 0xc5, 0xfe, 0x29, + 0x5a, 0xbd, 0xeb, 0x1d, 0xca, + 0x42, 0x81, 0xbe, 0x98, 0x8e, + 0x2d, 0xa0, 0xb6, 0xc1, 0xc6, + 0xa5, 0x9d, 0xc2, 0x26, 0xc2, + 0x86, 0x24, 0xe1, 0x81, 0x75, + 0xe8, 0x51, 0xc9, 0x6b, 0x97, + 0x3d, 0x81, 0xb0, 0x1c, 0xc3, + 0x1f, 0x04, 0x78, 0x34, 0xbc, + 0x06, 0xd6, 0xd6, 0xed, 0xf6, + 0x20, 0xd1, 0x84, 0x24, 0x1a, + 0x6a, 0xed, 0x8b, 0x63, + 0xa6, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + HashType: txscript.SigHashAll, + }, }, } @@ -203,9 +222,8 @@ var ( chanPoint: breachOutPoints[0], capacity: btcutil.Amount(1e7), settledBalance: btcutil.Amount(1e7), - selfOutput: &breachedOutputs[0], - revokedOutput: &breachedOutputs[1], - htlcOutputs: []*breachedOutput{}, + // Set to breachedOutputs 0 and 1 in init() + breachedOutputs: []breachedOutput{{}, {}}, }, { commitHash: [chainhash.HashSize]byte{ @@ -217,12 +235,8 @@ var ( chanPoint: breachOutPoints[1], capacity: btcutil.Amount(1e7), settledBalance: btcutil.Amount(1e7), - selfOutput: &breachedOutputs[0], - revokedOutput: &breachedOutputs[1], - htlcOutputs: []*breachedOutput{ - &breachedOutputs[1], - &breachedOutputs[2], - }, + // Set to breachedOutputs 1 and 2 in init() + breachedOutputs: []breachedOutput{{}, {}}, }, } ) @@ -238,7 +252,11 @@ func init() { for i := range retributions { retInfo := &retributions[i] retInfo.remoteIdentity = breachedOutputs[i].signDesc.PubKey + retInfo.breachedOutputs[0] = breachedOutputs[i] + retInfo.breachedOutputs[1] = breachedOutputs[i+1] + retributionMap[retInfo.chanPoint] = *retInfo + } } @@ -310,14 +328,12 @@ func initBreachedOutputs() error { bo := &breachedOutputs[i] // Parse the sign descriptor's pubkey. - sd := &breachSignDescs[i] pubkey, err := btcec.ParsePubKey(breachKeys[i], btcec.S256()) if err != nil { return fmt.Errorf("unable to parse pubkey: %v", breachKeys[i]) } - sd.PubKey = pubkey - bo.signDesc = *sd + bo.signDesc.PubKey = pubkey } return nil @@ -325,7 +341,7 @@ func initBreachedOutputs() error { // Test that breachedOutput Encode/Decode works. func TestBreachedOutputSerialization(t *testing.T) { - for i := 0; i < len(breachedOutputs); i++ { + for i := range breachedOutputs { bo := &breachedOutputs[i] var buf bytes.Buffer @@ -353,7 +369,7 @@ func TestBreachedOutputSerialization(t *testing.T) { // Test that retribution Encode/Decode works. func TestRetributionSerialization(t *testing.T) { - for i := 0; i < len(retributions); i++ { + for i := range retributions { ret := &retributions[i] var buf bytes.Buffer @@ -381,21 +397,19 @@ func TestRetributionSerialization(t *testing.T) { // copyRetInfo creates a complete copy of the given retributionInfo. func copyRetInfo(retInfo *retributionInfo) *retributionInfo { - nHtlcs := len(retInfo.htlcOutputs) + nOutputs := len(retInfo.breachedOutputs) ret := &retributionInfo{ - commitHash: retInfo.commitHash, - chanPoint: retInfo.chanPoint, - remoteIdentity: retInfo.remoteIdentity, - capacity: retInfo.capacity, - settledBalance: retInfo.settledBalance, - selfOutput: retInfo.selfOutput, - revokedOutput: retInfo.revokedOutput, - htlcOutputs: make([]*breachedOutput, nHtlcs), + commitHash: retInfo.commitHash, + chanPoint: retInfo.chanPoint, + remoteIdentity: retInfo.remoteIdentity, + capacity: retInfo.capacity, + settledBalance: retInfo.settledBalance, + breachedOutputs: make([]breachedOutput, nOutputs), } - for i, htlco := range retInfo.htlcOutputs { - ret.htlcOutputs[i] = htlco + for i := range retInfo.breachedOutputs { + ret.breachedOutputs[i] = retInfo.breachedOutputs[i] } return ret diff --git a/lnd_test.go b/lnd_test.go index e30d5a93..4b5962f9 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -2010,6 +2010,258 @@ func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) { } } +// testRevokedCloseRetributionZeroValueRemoteOutput tests that Alice is able +// carry out retribution in the event that she fails in state where the remote +// commitment output has zero-value. +func testRevokedCloseRetributionZeroValueRemoteOutput( + net *networkHarness, + t *harnessTest) { + + ctxb := context.Background() + const ( + timeout = time.Duration(time.Second * 10) + chanAmt = maxFundingAmount + paymentAmt = 10000 + numInvoices = 6 + ) + + // Since we'd like to test some multi-hop failure scenarios, we'll + // introduce another node into our test network: Carol. + carol, err := net.NewNode([]string{"--debughtlc", "--hodlhtlc"}) + if err != nil { + t.Fatalf("unable to create new nodes: %v", err) + } + + // We must let Alice have an open channel before she can send a node + // announcement, so we open a channel with Carol, + if err := net.ConnectNodes(ctxb, net.Alice, carol); err != nil { + t.Fatalf("unable to connect alice to carol: %v", err) + } + + // In order to test Alice's response to an uncooperative channel + // closure by Carol, we'll first open up a channel between them with a + // 0.5 BTC value. + ctxt, _ := context.WithTimeout(ctxb, timeout) + chanPoint := openChannelAndAssert(ctxt, t, net, net.Alice, carol, + chanAmt, 0) + + // With the channel open, we'll create a few invoices for Carol that + // Alice will pay to in order to advance the state of the channel. + carolPaymentHashes := make([][]byte, numInvoices) + for i := 0; i < numInvoices; i++ { + preimage := bytes.Repeat([]byte{byte(192 - i)}, 32) + invoice := &lnrpc.Invoice{ + Memo: "testing", + RPreimage: preimage, + Value: paymentAmt, + } + resp, err := carol.AddInvoice(ctxb, invoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + carolPaymentHashes[i] = resp.RHash + } + + // As we'll be querying the state of Carols's channels frequently we'll + // create a closure helper function for the purpose. + getCarolChanInfo := func() (*lnrpc.ActiveChannel, error) { + req := &lnrpc.ListChannelsRequest{} + carolChannelInfo, err := carol.ListChannels(ctxb, req) + if err != nil { + return nil, err + } + if len(carolChannelInfo.Channels) != 1 { + t.Fatalf("carol should only have a single channel, "+ + "instead he has %v", len(carolChannelInfo.Channels)) + } + + return carolChannelInfo.Channels[0], nil + } + + // Wait for Alice to receive the channel edge from the funding manager. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("alice didn't see the alice->carol channel before "+ + "timeout: %v", err) + } + + // Open up a payment stream to Alice that we'll use to send payment to + // Carol. We also create a small helper function to send payments to + // Carol, consuming the payment hashes we generated above. + alicePayStream, err := net.Alice.SendPayment(ctxb) + if err != nil { + t.Fatalf("unable to create payment stream for alice: %v", err) + } + sendPayments := func(start, stop int) error { + for i := start; i < stop; i++ { + sendReq := &lnrpc.SendRequest{ + PaymentHash: carolPaymentHashes[i], + Dest: carol.PubKey[:], + Amt: paymentAmt, + } + if err := alicePayStream.Send(sendReq); err != nil { + return err + } + } + return nil + } + + // Next query for Carol's channel state, as we sent 0 payments, Carol + // should now see her balance as being 0 satoshis. + carolChan, err := getCarolChanInfo() + if err != nil { + t.Fatalf("unable to get carol's channel info: %v", err) + } + if carolChan.LocalBalance != 0 { + t.Fatalf("carol's balance is incorrect, got %v, expected %v", + carolChan.LocalBalance, 0) + } + + // Grab Carol's current commitment height (update number), we'll later + // revert her to this state after additional updates to force him to + // broadcast this soon to be revoked state. + carolStateNumPreCopy := carolChan.NumUpdates + + // Create a temporary file to house Carol's database state at this + // particular point in history. + carolTempDbPath, err := ioutil.TempDir("", "carol-past-state") + if err != nil { + t.Fatalf("unable to create temp db folder: %v", err) + } + carolTempDbFile := filepath.Join(carolTempDbPath, "channel.db") + defer os.Remove(carolTempDbPath) + + // With the temporary file created, copy Carol's current state into the + // temporary file we created above. Later after more updates, we'll + // restore this state. + carolDbPath := filepath.Join(carol.cfg.DataDir, "simnet/bitcoin/channel.db") + if err := copyFile(carolTempDbFile, carolDbPath); err != nil { + t.Fatalf("unable to copy database files: %v", err) + } + + // Finally, send payments from Alice to Carol, consuming Carol's remaining + // payment hashes. + if err := sendPayments(0, numInvoices); err != nil { + t.Fatalf("unable to send payment: %v", err) + } + + time.Sleep(200 * time.Millisecond) + carolChan, err = getCarolChanInfo() + if err != nil { + t.Fatalf("unable to get carol chan info: %v", err) + } + + // Now we shutdown Carol, copying over the his temporary database state + // which has the *prior* channel state over his current most up to date + // state. With this, we essentially force Carol to travel back in time + // within the channel's history. + if err = net.RestartNode(carol, func() error { + return os.Rename(carolTempDbFile, carolDbPath) + }); err != nil { + t.Fatalf("unable to restart node: %v", err) + } + + // Now query for Carol's channel state, it should show that he's at a + // state number in the past, not the *latest* state. + carolChan, err = getCarolChanInfo() + if err != nil { + t.Fatalf("unable to get carol chan info: %v", err) + } + if carolChan.NumUpdates != carolStateNumPreCopy { + t.Fatalf("db copy failed: %v", carolChan.NumUpdates) + } + + // Now force Carol to execute a *force* channel closure by unilaterally + // broadcasting his current channel state. This is actually the + // commitment transaction of a prior *revoked* state, so he'll soon + // feel the wrath of Alice's retribution. + force := true + closeUpdates, _, err := net.CloseChannel(ctxb, carol, chanPoint, force) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } + + // Finally, generate a single block, wait for the final close status + // update, then ensure that the closing transaction was included in the + // block. + block := mineBlocks(t, net, 1)[0] + + // Here, Alice receives a confirmation of Carol's breach transaction. + // We restart Alice to ensure that she is persisting her retribution + // state and continues exacting justice after her node restarts. + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("unable to stop Alice's node: %v", err) + } + + breachTXID, err := net.WaitForChannelClose(ctxb, closeUpdates) + if err != nil { + t.Fatalf("error while waiting for channel close: %v", err) + } + assertTxInBlock(t, block, breachTXID) + + // Query the mempool for Alice's justice transaction, this should be + // broadcast as Carol's contract breaching transaction gets confirmed + // above. + justiceTXID, err := waitForTxInMempool(net.Miner.Node, 5*time.Second) + if err != nil { + t.Fatalf("unable to find Alice's justice tx in mempool: %v", + err) + } + time.Sleep(100 * time.Millisecond) + + // Query for the mempool transaction found above. Then assert that all + // the inputs of this transaction are spending outputs generated by + // Carol's breach transaction above. + justiceTx, err := net.Miner.Node.GetRawTransaction(justiceTXID) + if err != nil { + t.Fatalf("unable to query for justice tx: %v", err) + } + for _, txIn := range justiceTx.MsgTx().TxIn { + if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { + t.Fatalf("justice tx not spending commitment utxo "+ + "instead is: %v", txIn.PreviousOutPoint) + } + } + + // We restart Alice here to ensure that she persists her retribution state + // and successfully continues exacting retribution after restarting. At + // this point, Alice has broadcast the justice transaction, but it hasn't + // been confirmed yet; when Alice restarts, she should start waiting for + // the justice transaction to confirm again. + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("unable to restart Alice's node: %v", err) + } + + // Now mine a block, this transaction should include Alice's justice + // transaction which was just accepted into the mempool. + block = mineBlocks(t, net, 1)[0] + + // The block should have exactly *two* transactions, one of which is + // the justice transaction. + if len(block.Transactions) != 2 { + t.Fatalf("transaction wasn't mined") + } + justiceSha := block.Transactions[1].TxHash() + if !bytes.Equal(justiceTx.Hash()[:], justiceSha[:]) { + t.Fatalf("justice tx wasn't mined") + } + + // Finally, obtain Alice's channel state, she shouldn't report any + // channel as she just successfully brought Carol to justice by sweeping + // all the channel funds. + req := &lnrpc.ListChannelsRequest{} + aliceChanInfo, err := net.Alice.ListChannels(ctxb, req) + if err != nil { + t.Fatalf("unable to query for alice's channels: %v", err) + } + if len(aliceChanInfo.Channels) != 0 { + t.Fatalf("alice shouldn't have a channel: %v", + spew.Sdump(aliceChanInfo.Channels)) + } +} + // testRevokedCloseRetributionRemoteHodl tests that Alice properly responds to a // channel breach made by the remote party, specifically in the case that the // remote party breaches before settling extended HTLCs. @@ -2248,7 +2500,7 @@ func testRevokedCloseRetributionRemoteHodl( } // Query the mempool for Alice's justice transaction, this should be - // broadcast as Bob's contract breaching transaction gets confirmed + // broadcast as Carol's contract breaching transaction gets confirmed // above. _, err = waitForTxInMempool(net.Miner.Node, 5*time.Second) if err != nil { @@ -3520,11 +3772,13 @@ var testsCases = []*testCase{ test: testBidirectionalAsyncPayments, }, { - // TODO(roasbeef): test always needs to be last as Bob's state - // is borked since we trick him into attempting to cheat Alice? name: "revoked uncooperative close retribution", test: testRevokedCloseRetribution, }, + { + name: "revoked uncooperative close retribution zero value remote output", + test: testRevokedCloseRetributionZeroValueRemoteOutput, + }, { name: "revoked uncooperative close retribution remote hodl", test: testRevokedCloseRetributionRemoteHodl, diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 16dfae8f..3f7f0a9a 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1063,7 +1063,9 @@ type BreachRetribution struct { // LocalOutputSignDesc is a SignDescriptor which is capable of // generating the signature necessary to sweep the output within the // BreachTransaction that pays directly us. - LocalOutputSignDesc SignDescriptor + // NOTE: A nil value indicates that the local output is considered dust + // according to the remote party's dust limit. + LocalOutputSignDesc *SignDescriptor // LocalOutpoint is the outpoint of the output paying to us (the local // party) within the breach transaction. @@ -1073,7 +1075,9 @@ type BreachRetribution struct { // generating the signature required to claim the funds as described // within the revocation clause of the remote party's commitment // output. - RemoteOutputSignDesc SignDescriptor + // NOTE: A nil value indicates that the local output is considered dust + // according to the remote party's dust limit. + RemoteOutputSignDesc *SignDescriptor // RemoteOutpoint is the output of the output paying to the remote // party within the breach transaction. @@ -1165,6 +1169,53 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, } } + // Conditionally instantiate a sign descriptor for each of the + // commitment outputs. If either is considered dust using the remote + // party's dust limit, the respective sign descriptor will be nil. + var ( + localSignDesc *SignDescriptor + remoteSignDesc *SignDescriptor + ) + + // Compute the local and remote balances in satoshis. + localAmt := revokedSnapshot.LocalBalance.ToSatoshis() + remoteAmt := revokedSnapshot.RemoteBalance.ToSatoshis() + + // If the local balance exceeds the remote party's dust limit, + // instantiate the local sign descriptor. + if localAmt >= chanState.RemoteChanCfg.DustLimit { + // We'll need to reconstruct the single tweak so we can sweep + // our non-delayed pay-to-self output self. + singleTweak := SingleTweakBytes(commitmentPoint, + chanState.LocalChanCfg.PaymentBasePoint) + + localSignDesc = &SignDescriptor{ + SingleTweak: singleTweak, + PubKey: chanState.LocalChanCfg.PaymentBasePoint, + WitnessScript: localPkScript, + Output: &wire.TxOut{ + PkScript: localWitnessHash, + Value: int64(localAmt), + }, + HashType: txscript.SigHashAll, + } + } + + // Similarly, if the remote balance exceeds the remote party's dust + // limit, assemble the remote sign descriptor. + if remoteAmt >= chanState.RemoteChanCfg.DustLimit { + remoteSignDesc = &SignDescriptor{ + PubKey: chanState.LocalChanCfg.RevocationBasePoint, + DoubleTweak: commitmentSecret, + WitnessScript: remotePkScript, + Output: &wire.TxOut{ + PkScript: remoteWitnessHash, + Value: int64(remoteAmt), + }, + HashType: txscript.SigHashAll, + } + } + // With the commitment outputs located, we'll now generate all the // retribution structs for each of the HTLC transactions active on the // remote commitment transaction. @@ -1216,41 +1267,18 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, } } - // We'll need to reconstruct the single tweak so we can sweep our - // non-delayed pay-to-self output self. - singleTweak := SingleTweakBytes(commitmentPoint, - chanState.LocalChanCfg.PaymentBasePoint) - // Finally, with all the necessary data constructed, we can create the // BreachRetribution struct which houses all the data necessary to // swiftly bring justice to the cheating remote party. return &BreachRetribution{ - BreachTransaction: broadcastCommitment, - RevokedStateNum: stateNum, - PendingHTLCs: revokedSnapshot.Htlcs, - LocalOutpoint: localOutpoint, - LocalOutputSignDesc: SignDescriptor{ - SingleTweak: singleTweak, - PubKey: chanState.LocalChanCfg.PaymentBasePoint, - WitnessScript: localPkScript, - Output: &wire.TxOut{ - PkScript: localWitnessHash, - Value: int64(revokedSnapshot.LocalBalance.ToSatoshis()), - }, - HashType: txscript.SigHashAll, - }, - RemoteOutpoint: remoteOutpoint, - RemoteOutputSignDesc: SignDescriptor{ - PubKey: chanState.LocalChanCfg.RevocationBasePoint, - DoubleTweak: commitmentSecret, - WitnessScript: remotePkScript, - Output: &wire.TxOut{ - PkScript: remoteWitnessHash, - Value: int64(revokedSnapshot.RemoteBalance.ToSatoshis()), - }, - HashType: txscript.SigHashAll, - }, - HtlcRetributions: htlcRetributions, + BreachTransaction: broadcastCommitment, + RevokedStateNum: stateNum, + PendingHTLCs: revokedSnapshot.Htlcs, + LocalOutpoint: localOutpoint, + LocalOutputSignDesc: localSignDesc, + RemoteOutpoint: remoteOutpoint, + RemoteOutputSignDesc: remoteSignDesc, + HtlcRetributions: htlcRetributions, }, nil }