Merge pull request #341 from cfromknecht/breach-filter-commit-dust

Breach Arbiter Ignore Dust Commitment Outputs
This commit is contained in:
Olaoluwa Osuntokun 2017-09-26 17:10:12 -07:00 committed by GitHub
commit 1d487ea78b
4 changed files with 616 additions and 263 deletions

View File

@ -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
}
}

View File

@ -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

View File

@ -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,

View File

@ -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
}