Merge pull request #5036 from halseth/breacharbiter-justice-splitting
[breacharbiter] Split justice tx in case of delayed confirmation.
This commit is contained in:
commit
c0acdd8082
599
breacharbiter.go
599
breacharbiter.go
@ -25,6 +25,21 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
const (
|
||||
// justiceTxConfTarget is the number of blocks we'll use as a
|
||||
// confirmation target when creating the justice transaction. We'll
|
||||
// choose an aggressive target, since we want to be sure it confirms
|
||||
// quickly.
|
||||
justiceTxConfTarget = 2
|
||||
|
||||
// blocksPassedSplitPublish is the number of blocks without
|
||||
// confirmation of the justice tx we'll wait before starting to publish
|
||||
// smaller variants of the justice tx. We do this to mitigate an attack
|
||||
// the channel peer can do by pinning the HTLC outputs of the
|
||||
// commitment with low-fee HTLC transactions.
|
||||
blocksPassedSplitPublish = 4
|
||||
)
|
||||
|
||||
var (
|
||||
// retributionBucket stores retribution state on disk between detecting
|
||||
// a contract breach, broadcasting a justice transaction that sweeps the
|
||||
@ -318,24 +333,22 @@ func convertToSecondLevelRevoke(bo *breachedOutput, breachInfo *retributionInfo,
|
||||
bo.outpoint)
|
||||
}
|
||||
|
||||
// spend is used to wrap the index of the retributionInfo output that gets
|
||||
// spent together with the spend details.
|
||||
type spend struct {
|
||||
index int
|
||||
detail *chainntnfs.SpendDetail
|
||||
}
|
||||
|
||||
// waitForSpendEvent waits for any of the breached outputs to get spent, and
|
||||
// mutates the breachInfo to be able to sweep it. This method should be used
|
||||
// when we fail to publish the justice tx because of a double spend, indicating
|
||||
// that the counter party has taken one of the breached outputs to the second
|
||||
// level. The spendNtfns map is a cache used to store registered spend
|
||||
// subscriptions, in case we must call this method multiple times.
|
||||
// returns the spend details for those outputs. The spendNtfns map is a cache
|
||||
// used to store registered spend subscriptions, in case we must call this
|
||||
// method multiple times.
|
||||
func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo,
|
||||
spendNtfns map[wire.OutPoint]*chainntnfs.SpendEvent) error {
|
||||
spendNtfns map[wire.OutPoint]*chainntnfs.SpendEvent) ([]spend, error) {
|
||||
|
||||
inputs := breachInfo.breachedOutputs
|
||||
|
||||
// spend is used to wrap the index of the output that gets spent
|
||||
// together with the spend details.
|
||||
type spend struct {
|
||||
index int
|
||||
detail *chainntnfs.SpendDetail
|
||||
}
|
||||
|
||||
// We create a channel the first goroutine that gets a spend event can
|
||||
// signal. We make it buffered in case multiple spend events come in at
|
||||
// the same time.
|
||||
@ -378,7 +391,7 @@ func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo,
|
||||
// to avoid entering an infinite loop.
|
||||
select {
|
||||
case <-b.quit:
|
||||
return errBrarShuttingDown
|
||||
return nil, errBrarShuttingDown
|
||||
default:
|
||||
continue
|
||||
}
|
||||
@ -438,62 +451,115 @@ func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo,
|
||||
// channel before ranging over its content.
|
||||
close(allSpends)
|
||||
|
||||
doneOutputs := make(map[int]struct{})
|
||||
// Gather all detected spends and return them.
|
||||
var spends []spend
|
||||
for s := range allSpends {
|
||||
breachedOutput := &inputs[s.index]
|
||||
delete(spendNtfns, breachedOutput.outpoint)
|
||||
|
||||
switch breachedOutput.witnessType {
|
||||
case input.HtlcAcceptedRevoke:
|
||||
fallthrough
|
||||
case input.HtlcOfferedRevoke:
|
||||
brarLog.Infof("Spend on second-level"+
|
||||
"%s(%v) for ChannelPoint(%v) "+
|
||||
"transitions to second-level output",
|
||||
breachedOutput.witnessType,
|
||||
breachedOutput.outpoint,
|
||||
breachInfo.chanPoint)
|
||||
spends = append(spends, s)
|
||||
}
|
||||
|
||||
// In this case we'll morph our initial revoke
|
||||
// spend to instead point to the second level
|
||||
// output, and update the sign descriptor in the
|
||||
// process.
|
||||
convertToSecondLevelRevoke(
|
||||
breachedOutput, breachInfo, s.detail,
|
||||
)
|
||||
return spends, nil
|
||||
|
||||
continue
|
||||
case <-b.quit:
|
||||
return nil, errBrarShuttingDown
|
||||
}
|
||||
}
|
||||
|
||||
// updateBreachInfo mutates the passed breachInfo by removing or converting any
|
||||
// outputs among the spends. It also counts the total and revoked funds swept
|
||||
// by our justice spends.
|
||||
func updateBreachInfo(breachInfo *retributionInfo, spends []spend) (
|
||||
btcutil.Amount, btcutil.Amount) {
|
||||
|
||||
inputs := breachInfo.breachedOutputs
|
||||
doneOutputs := make(map[int]struct{})
|
||||
|
||||
var totalFunds, revokedFunds btcutil.Amount
|
||||
for _, s := range spends {
|
||||
breachedOutput := &inputs[s.index]
|
||||
txIn := s.detail.SpendingTx.TxIn[s.detail.SpenderInputIndex]
|
||||
|
||||
switch breachedOutput.witnessType {
|
||||
case input.HtlcAcceptedRevoke:
|
||||
fallthrough
|
||||
case input.HtlcOfferedRevoke:
|
||||
// If the HTLC output was spent using the revocation
|
||||
// key, it is our own spend, and we can forget the
|
||||
// output. Otherwise it has been taken to the second
|
||||
// level.
|
||||
signDesc := &breachedOutput.signDesc
|
||||
ok, err := input.IsHtlcSpendRevoke(txIn, signDesc)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to determine if "+
|
||||
"revoke spend: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
brarLog.Infof("Spend on %s(%v) for ChannelPoint(%v) "+
|
||||
"transitions output to terminal state, "+
|
||||
"removing input from justice transaction",
|
||||
if ok {
|
||||
brarLog.Debugf("HTLC spend was our own " +
|
||||
"revocation spend")
|
||||
break
|
||||
}
|
||||
|
||||
brarLog.Infof("Spend on second-level "+
|
||||
"%s(%v) for ChannelPoint(%v) "+
|
||||
"transitions to second-level output",
|
||||
breachedOutput.witnessType,
|
||||
breachedOutput.outpoint, breachInfo.chanPoint)
|
||||
|
||||
doneOutputs[s.index] = struct{}{}
|
||||
// In this case we'll morph our initial revoke
|
||||
// spend to instead point to the second level
|
||||
// output, and update the sign descriptor in the
|
||||
// process.
|
||||
convertToSecondLevelRevoke(
|
||||
breachedOutput, breachInfo, s.detail,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter the inputs for which we can no longer proceed.
|
||||
var nextIndex int
|
||||
for i := range inputs {
|
||||
if _, ok := doneOutputs[i]; ok {
|
||||
continue
|
||||
}
|
||||
// Now that we have determined the spend is done by us, we
|
||||
// count the total and revoked funds swept depending on the
|
||||
// input type.
|
||||
switch breachedOutput.witnessType {
|
||||
|
||||
inputs[nextIndex] = inputs[i]
|
||||
nextIndex++
|
||||
// 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.
|
||||
case input.CommitmentRevoke, input.HtlcSecondLevelRevoke,
|
||||
input.HtlcOfferedRevoke:
|
||||
|
||||
revokedFunds += breachedOutput.Amount()
|
||||
}
|
||||
|
||||
// Update our remaining set of outputs before continuing with
|
||||
// another attempt at publication.
|
||||
breachInfo.breachedOutputs = inputs[:nextIndex]
|
||||
totalFunds += breachedOutput.Amount()
|
||||
brarLog.Infof("Spend on %s(%v) for ChannelPoint(%v) "+
|
||||
"transitions output to terminal state, "+
|
||||
"removing input from justice transaction",
|
||||
breachedOutput.witnessType,
|
||||
breachedOutput.outpoint, breachInfo.chanPoint)
|
||||
|
||||
case <-b.quit:
|
||||
return errBrarShuttingDown
|
||||
doneOutputs[s.index] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
// Filter the inputs for which we can no longer proceed.
|
||||
var nextIndex int
|
||||
for i := range inputs {
|
||||
if _, ok := doneOutputs[i]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
inputs[nextIndex] = inputs[i]
|
||||
nextIndex++
|
||||
}
|
||||
|
||||
// Update our remaining set of outputs before continuing with
|
||||
// another attempt at publication.
|
||||
breachInfo.breachedOutputs = inputs[:nextIndex]
|
||||
return totalFunds, revokedFunds
|
||||
}
|
||||
|
||||
// exactRetribution is a goroutine which is executed once a contract breach has
|
||||
@ -508,17 +574,14 @@ func (b *breachArbiter) exactRetribution(confChan *chainntnfs.ConfirmationEvent,
|
||||
defer b.wg.Done()
|
||||
|
||||
// TODO(roasbeef): state needs to be checkpointed here
|
||||
var breachConfHeight uint32
|
||||
select {
|
||||
case breachConf, ok := <-confChan.Confirmed:
|
||||
case _, ok := <-confChan.Confirmed:
|
||||
// If the second value is !ok, then the channel has been closed
|
||||
// signifying a daemon shutdown, so we exit.
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
breachConfHeight = breachConf.BlockHeight
|
||||
|
||||
// Otherwise, if this is a real confirmation notification, then
|
||||
// we fall through to complete our duty.
|
||||
case <-b.quit:
|
||||
@ -533,37 +596,20 @@ func (b *breachArbiter) exactRetribution(confChan *chainntnfs.ConfirmationEvent,
|
||||
// SpendEvents between each attempt to not re-register uneccessarily.
|
||||
spendNtfns := make(map[wire.OutPoint]*chainntnfs.SpendEvent)
|
||||
|
||||
finalTx, err := b.cfg.Store.GetFinalizedTxn(&breachInfo.chanPoint)
|
||||
// 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
|
||||
|
||||
justiceTxBroadcast:
|
||||
// With the breach transaction confirmed, we now create the
|
||||
// justice tx which will claim ALL the funds within the
|
||||
// channel.
|
||||
justiceTxs, err := b.createJusticeTx(breachInfo.breachedOutputs)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to get finalized txn for"+
|
||||
"chanid=%v: %v", &breachInfo.chanPoint, err)
|
||||
brarLog.Errorf("Unable to create justice tx: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If this retribution has not been finalized before, we will first
|
||||
// construct a sweep transaction and write it to disk. This will allow
|
||||
// the breach arbiter to re-register for notifications for the justice
|
||||
// txid.
|
||||
justiceTxBroadcast:
|
||||
if finalTx == nil {
|
||||
// With the breach transaction confirmed, we now create the
|
||||
// justice tx which will claim ALL the funds within the
|
||||
// channel.
|
||||
finalTx, err = b.createJusticeTx(breachInfo)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to create justice tx: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Persist our finalized justice transaction before making an
|
||||
// attempt to broadcast.
|
||||
err := b.cfg.Store.Finalize(&breachInfo.chanPoint, finalTx)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to finalize justice tx for "+
|
||||
"chanid=%v: %v", &breachInfo.chanPoint, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
finalTx := justiceTxs.spendAll
|
||||
|
||||
brarLog.Debugf("Broadcasting justice tx: %v", newLogClosure(func() string {
|
||||
return spew.Sdump(finalTx)
|
||||
@ -575,31 +621,63 @@ justiceTxBroadcast:
|
||||
err = b.cfg.PublishTransaction(finalTx, label)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to broadcast justice tx: %v", err)
|
||||
}
|
||||
|
||||
if err == lnwallet.ErrDoubleSpend {
|
||||
// Broadcasting the transaction failed because of a
|
||||
// conflict either in the mempool or in chain. We'll
|
||||
// now create spend subscriptions for all HTLC outputs
|
||||
// on the commitment transaction that could possibly
|
||||
// have been spent, and wait for any of them to
|
||||
// trigger.
|
||||
brarLog.Infof("Waiting for a spend event before " +
|
||||
"attempting to craft new justice tx.")
|
||||
finalTx = nil
|
||||
// Regardless of publication succeeded or not, we now wait for any of
|
||||
// the inputs to be spent. If any input got spent by the remote, we
|
||||
// must recreate our justice transaction.
|
||||
var (
|
||||
spendChan = make(chan []spend, 1)
|
||||
errChan = make(chan error, 1)
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
err := b.waitForSpendEvent(breachInfo, spendNtfns)
|
||||
if err != nil {
|
||||
if err != errBrarShuttingDown {
|
||||
brarLog.Errorf("error waiting for "+
|
||||
"spend event: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
spends, err := b.waitForSpendEvent(breachInfo, spendNtfns)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
spendChan <- spends
|
||||
}()
|
||||
|
||||
// We'll also register for block notifications, such that in case our
|
||||
// justice tx doesn't confirm within a reasonable timeframe, we can
|
||||
// start to more aggressively sweep the time sensitive outputs.
|
||||
newBlockChan, err := b.cfg.Notifier.RegisterBlockEpochNtfn(nil)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to register for block notifications: %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
defer newBlockChan.Cancel()
|
||||
|
||||
Loop:
|
||||
for {
|
||||
|
||||
select {
|
||||
case spends := <-spendChan:
|
||||
// Update the breach info with the new spends.
|
||||
t, r := updateBreachInfo(breachInfo, spends)
|
||||
totalFunds += t
|
||||
revokedFunds += r
|
||||
|
||||
brarLog.Infof("%v spends from breach tx for "+
|
||||
"ChannelPoint(%v) has been detected, %v "+
|
||||
"revoked funds (%v total) have been claimed",
|
||||
len(spends), breachInfo.chanPoint,
|
||||
revokedFunds, totalFunds)
|
||||
|
||||
if len(breachInfo.breachedOutputs) == 0 {
|
||||
brarLog.Debugf("No more outputs to sweep for "+
|
||||
"breach, marking ChannelPoint(%v) "+
|
||||
"fully resolved", breachInfo.chanPoint)
|
||||
brarLog.Infof("Justice for ChannelPoint(%v) "+
|
||||
"has been served, %v revoked funds "+
|
||||
"(%v total) have been claimed. No "+
|
||||
"more outputs to sweep, marking fully "+
|
||||
"resolved", breachInfo.chanPoint,
|
||||
revokedFunds, totalFunds)
|
||||
|
||||
err = b.cleanupBreach(&breachInfo.chanPoint)
|
||||
if err != nil {
|
||||
@ -607,78 +685,101 @@ justiceTxBroadcast:
|
||||
"breached ChannelPoint(%v): %v",
|
||||
breachInfo.chanPoint, err)
|
||||
}
|
||||
return
|
||||
|
||||
// TODO(roasbeef): add peer to blacklist?
|
||||
|
||||
// TODO(roasbeef): close other active channels
|
||||
// with offending peer
|
||||
break Loop
|
||||
}
|
||||
|
||||
brarLog.Infof("Attempting another justice tx "+
|
||||
"with %d inputs",
|
||||
len(breachInfo.breachedOutputs))
|
||||
|
||||
wg.Wait()
|
||||
goto justiceTxBroadcast
|
||||
}
|
||||
}
|
||||
|
||||
// As a conclusionary step, we register for a notification to be
|
||||
// dispatched once the justice tx is confirmed. After confirmation we
|
||||
// notify the caller that initiated the retribution workflow that the
|
||||
// deed has been done.
|
||||
justiceTXID := finalTx.TxHash()
|
||||
justiceScript := finalTx.TxOut[0].PkScript
|
||||
confChan, err = b.cfg.Notifier.RegisterConfirmationsNtfn(
|
||||
&justiceTXID, justiceScript, 1, breachConfHeight,
|
||||
)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to register for conf for txid(%v): %v",
|
||||
justiceTXID, err)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case _, ok := <-confChan.Confirmed:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 _, inp := range breachInfo.breachedOutputs {
|
||||
totalFunds += inp.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 inp.WitnessType() {
|
||||
case input.CommitmentRevoke:
|
||||
revokedFunds += inp.Amount()
|
||||
case input.HtlcOfferedRevoke:
|
||||
revokedFunds += inp.Amount()
|
||||
default:
|
||||
// On every new block, we check whether we should republish the
|
||||
// transactions.
|
||||
case epoch, ok := <-newBlockChan.Epochs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// If less than four blocks have passed since the
|
||||
// breach confirmed, we'll continue waiting. It was
|
||||
// published with a 2-block fee estimate, so it's not
|
||||
// unexpected that four blocks without confirmation can
|
||||
// pass.
|
||||
splitHeight := breachInfo.breachHeight +
|
||||
blocksPassedSplitPublish
|
||||
if uint32(epoch.Height) < splitHeight {
|
||||
continue Loop
|
||||
}
|
||||
|
||||
brarLog.Warnf("Block height %v arrived without "+
|
||||
"justice tx confirming (breached at "+
|
||||
"height %v), splitting justice tx.",
|
||||
epoch.Height, breachInfo.breachHeight)
|
||||
|
||||
// Otherwise we'll attempt to publish the two separate
|
||||
// justice transactions that sweeps the commitment
|
||||
// outputs and the HTLC outputs separately. This is to
|
||||
// mitigate the case where our "spend all" justice TX
|
||||
// doesn't propagate because the HTLC outputs have been
|
||||
// pinned by low fee HTLC txs.
|
||||
label := labels.MakeLabel(
|
||||
labels.LabelTypeJusticeTransaction, nil,
|
||||
)
|
||||
if justiceTxs.spendCommitOuts != nil {
|
||||
tx := justiceTxs.spendCommitOuts
|
||||
|
||||
brarLog.Debugf("Broadcasting justice tx "+
|
||||
"spending commitment outs: %v",
|
||||
newLogClosure(func() string {
|
||||
return spew.Sdump(tx)
|
||||
}))
|
||||
|
||||
err = b.cfg.PublishTransaction(tx, label)
|
||||
if err != nil {
|
||||
brarLog.Warnf("Unable to broadcast "+
|
||||
"commit out spending justice "+
|
||||
"tx: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if justiceTxs.spendHTLCs != nil {
|
||||
tx := justiceTxs.spendHTLCs
|
||||
|
||||
brarLog.Debugf("Broadcasting justice tx "+
|
||||
"spending HTLC outs: %v",
|
||||
newLogClosure(func() string {
|
||||
return spew.Sdump(tx)
|
||||
}))
|
||||
|
||||
err = b.cfg.PublishTransaction(tx, label)
|
||||
if err != nil {
|
||||
brarLog.Warnf("Unable to broadcast "+
|
||||
"HTLC out spending justice "+
|
||||
"tx: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case err := <-errChan:
|
||||
if err != errBrarShuttingDown {
|
||||
brarLog.Errorf("error waiting for "+
|
||||
"spend event: %v", err)
|
||||
}
|
||||
break Loop
|
||||
|
||||
case <-b.quit:
|
||||
break Loop
|
||||
}
|
||||
|
||||
brarLog.Infof("Justice for ChannelPoint(%v) has "+
|
||||
"been served, %v revoked funds (%v total) "+
|
||||
"have been claimed", breachInfo.chanPoint,
|
||||
revokedFunds, totalFunds)
|
||||
|
||||
err = b.cleanupBreach(&breachInfo.chanPoint)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Failed to cleanup breached "+
|
||||
"ChannelPoint(%v): %v", breachInfo.chanPoint,
|
||||
err)
|
||||
}
|
||||
|
||||
// TODO(roasbeef): add peer to blacklist?
|
||||
|
||||
// TODO(roasbeef): close other active channels with offending
|
||||
// peer
|
||||
|
||||
return
|
||||
case <-b.quit:
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for our go routine to exit.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// cleanupBreach marks the given channel point as fully resolved and removes the
|
||||
@ -1045,12 +1146,87 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
||||
}
|
||||
}
|
||||
|
||||
// createJusticeTx creates a transaction which exacts "justice" by sweeping ALL
|
||||
// justiceTxVariants is a struct that holds transactions which exacts "justice"
|
||||
// by sweeping ALL the funds within the channel which we are now entitled to
|
||||
// due to a breach of the channel's contract by the counterparty. There are
|
||||
// three variants of the justice transaction:
|
||||
//
|
||||
// 1. The "normal" justice tx that spends all breached outputs
|
||||
// 2. A tx that spends only the breached to_local output and to_remote output
|
||||
// (can be nil if none of these exist)
|
||||
// 3. A tx that spends all the breached HTLC outputs, and second-level HTLC
|
||||
// outputs (can be nil if no HTLC outputs exist).
|
||||
//
|
||||
// The reason we create these three variants, is that in certain cases (like
|
||||
// with the anchor output HTLC malleability), the channel counter party can pin
|
||||
// the HTLC outputs with low fee children, hindering our normal justice tx that
|
||||
// attempts to spend these outputs from propagating. In this case we want to
|
||||
// spend the to_local output separately, before the CSV lock expires.
|
||||
type justiceTxVariants struct {
|
||||
spendAll *wire.MsgTx
|
||||
spendCommitOuts *wire.MsgTx
|
||||
spendHTLCs *wire.MsgTx
|
||||
}
|
||||
|
||||
// createJusticeTx creates transactions which exacts "justice" by sweeping ALL
|
||||
// the funds within the channel which we are now entitled to due to a breach of
|
||||
// the channel's contract by the counterparty. This function returns a *fully*
|
||||
// signed transaction with the witness for each input fully in place.
|
||||
func (b *breachArbiter) createJusticeTx(
|
||||
r *retributionInfo) (*wire.MsgTx, error) {
|
||||
breachedOutputs []breachedOutput) (*justiceTxVariants, error) {
|
||||
|
||||
var (
|
||||
allInputs []input.Input
|
||||
commitInputs []input.Input
|
||||
htlcInputs []input.Input
|
||||
)
|
||||
|
||||
for i := range breachedOutputs {
|
||||
// Grab locally scoped reference to breached output.
|
||||
inp := &breachedOutputs[i]
|
||||
allInputs = append(allInputs, inp)
|
||||
|
||||
// Check if the input is from an HTLC or a commitment output.
|
||||
if inp.WitnessType() == input.HtlcAcceptedRevoke ||
|
||||
inp.WitnessType() == input.HtlcOfferedRevoke ||
|
||||
inp.WitnessType() == input.HtlcSecondLevelRevoke {
|
||||
|
||||
htlcInputs = append(htlcInputs, inp)
|
||||
} else {
|
||||
commitInputs = append(commitInputs, inp)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
txs = &justiceTxVariants{}
|
||||
err error
|
||||
)
|
||||
|
||||
// For each group of inputs, create a tx that spends them.
|
||||
txs.spendAll, err = b.createSweepTx(allInputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txs.spendCommitOuts, err = b.createSweepTx(commitInputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txs.spendHTLCs, err = b.createSweepTx(htlcInputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txs, nil
|
||||
}
|
||||
|
||||
// createSweepTx creates a tx that sweeps the passed inputs back to our wallet.
|
||||
func (b *breachArbiter) createSweepTx(inputs []input.Input) (*wire.MsgTx,
|
||||
error) {
|
||||
if len(inputs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We will assemble the breached outputs into a slice of spendable
|
||||
// outputs, while simultaneously computing the estimated weight of the
|
||||
@ -1062,7 +1238,7 @@ func (b *breachArbiter) createJusticeTx(
|
||||
|
||||
// Allocate enough space to potentially hold each of the breached
|
||||
// outputs in the retribution info.
|
||||
spendableOutputs = make([]input.Input, 0, len(r.breachedOutputs))
|
||||
spendableOutputs = make([]input.Input, 0, len(inputs))
|
||||
|
||||
// The justice transaction we construct will be a segwit transaction
|
||||
// that pays to a p2wkh output. Components such as the version,
|
||||
@ -1071,15 +1247,15 @@ func (b *breachArbiter) createJusticeTx(
|
||||
|
||||
// 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 {
|
||||
// that we contribute the appropriate weight for each input and
|
||||
// witness, finally adding to our list of spendable outputs.
|
||||
for i := range inputs {
|
||||
// Grab locally scoped reference to breached output.
|
||||
inp := &r.breachedOutputs[i]
|
||||
inp := inputs[i]
|
||||
|
||||
// First, determine the appropriate estimated witness weight for
|
||||
// the give witness type of this breached output. If the witness
|
||||
// weight cannot be estimated, we will omit it from the
|
||||
// First, determine the appropriate estimated witness weight
|
||||
// for the give witness type of this breached output. If the
|
||||
// witness weight cannot be estimated, we will omit it from the
|
||||
// transaction.
|
||||
witnessWeight, _, err := inp.WitnessType().SizeUpperBound()
|
||||
if err != nil {
|
||||
@ -1120,7 +1296,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64,
|
||||
|
||||
// We'll actually attempt to target inclusion within the next two
|
||||
// blocks as we'd like to sweep these funds back into our wallet ASAP.
|
||||
feePerKw, err := b.cfg.Estimator.EstimateFeePerKW(2)
|
||||
feePerKw, err := b.cfg.Estimator.EstimateFeePerKW(justiceTxConfTarget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1214,15 +1390,6 @@ type RetributionStore interface {
|
||||
// is aware of any breaches for the provided channel point.
|
||||
IsBreached(chanPoint *wire.OutPoint) (bool, error)
|
||||
|
||||
// Finalize persists the finalized justice transaction for a particular
|
||||
// channel.
|
||||
Finalize(chanPoint *wire.OutPoint, finalTx *wire.MsgTx) error
|
||||
|
||||
// GetFinalizedTxn loads the finalized justice transaction, if any, from
|
||||
// the retribution store. The finalized transaction will be nil if
|
||||
// Finalize has not yet been called for this channel point.
|
||||
GetFinalizedTxn(chanPoint *wire.OutPoint) (*wire.MsgTx, error)
|
||||
|
||||
// Remove deletes the retributionInfo from disk, if any exists, under
|
||||
// the given key. An error should be re raised if the removal fails.
|
||||
Remove(key *wire.OutPoint) error
|
||||
@ -1273,68 +1440,6 @@ func (rs *retributionStore) Add(ret *retributionInfo) error {
|
||||
}, func() {})
|
||||
}
|
||||
|
||||
// Finalize writes a signed justice transaction to the retribution store. This
|
||||
// is done before publishing the transaction, so that we can recover the txid on
|
||||
// startup and re-register for confirmation notifications.
|
||||
func (rs *retributionStore) Finalize(chanPoint *wire.OutPoint,
|
||||
finalTx *wire.MsgTx) error {
|
||||
return kvdb.Update(rs.db, func(tx kvdb.RwTx) error {
|
||||
justiceBkt, err := tx.CreateTopLevelBucket(justiceTxnBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var chanBuf bytes.Buffer
|
||||
if err := writeOutpoint(&chanBuf, chanPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var txBuf bytes.Buffer
|
||||
if err := finalTx.Serialize(&txBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return justiceBkt.Put(chanBuf.Bytes(), txBuf.Bytes())
|
||||
}, func() {})
|
||||
}
|
||||
|
||||
// GetFinalizedTxn loads the finalized justice transaction for the provided
|
||||
// channel point. The finalized transaction will be nil if Finalize has yet to
|
||||
// be called for this channel point.
|
||||
func (rs *retributionStore) GetFinalizedTxn(
|
||||
chanPoint *wire.OutPoint) (*wire.MsgTx, error) {
|
||||
|
||||
var finalTxBytes []byte
|
||||
if err := kvdb.View(rs.db, func(tx kvdb.RTx) error {
|
||||
justiceBkt := tx.ReadBucket(justiceTxnBucket)
|
||||
if justiceBkt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chanBuf bytes.Buffer
|
||||
if err := writeOutpoint(&chanBuf, chanPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
finalTxBytes = justiceBkt.Get(chanBuf.Bytes())
|
||||
|
||||
return nil
|
||||
}, func() {
|
||||
finalTxBytes = nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if finalTxBytes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
finalTx := &wire.MsgTx{}
|
||||
err := finalTx.Deserialize(bytes.NewReader(finalTxBytes))
|
||||
|
||||
return finalTx, err
|
||||
}
|
||||
|
||||
// IsBreached queries the retribution store to discern if this channel was
|
||||
// previously breached. This is used when connecting to a peer to determine if
|
||||
// it is safe to add a link to the htlcswitch, as we should never add a channel
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -407,24 +408,6 @@ func (frs *failingRetributionStore) IsBreached(chanPoint *wire.OutPoint) (bool,
|
||||
return frs.rs.IsBreached(chanPoint)
|
||||
}
|
||||
|
||||
func (frs *failingRetributionStore) Finalize(chanPoint *wire.OutPoint,
|
||||
finalTx *wire.MsgTx) error {
|
||||
|
||||
frs.mu.Lock()
|
||||
defer frs.mu.Unlock()
|
||||
|
||||
return frs.rs.Finalize(chanPoint, finalTx)
|
||||
}
|
||||
|
||||
func (frs *failingRetributionStore) GetFinalizedTxn(
|
||||
chanPoint *wire.OutPoint) (*wire.MsgTx, error) {
|
||||
|
||||
frs.mu.Lock()
|
||||
defer frs.mu.Unlock()
|
||||
|
||||
return frs.rs.GetFinalizedTxn(chanPoint)
|
||||
}
|
||||
|
||||
func (frs *failingRetributionStore) Remove(key *wire.OutPoint) error {
|
||||
frs.mu.Lock()
|
||||
defer frs.mu.Unlock()
|
||||
@ -1220,8 +1203,74 @@ func TestBreachHandoffFail(t *testing.T) {
|
||||
assertArbiterBreach(t, brar, chanPoint)
|
||||
}
|
||||
|
||||
type publAssertion func(*testing.T, map[wire.OutPoint]*wire.MsgTx,
|
||||
chan *wire.MsgTx)
|
||||
// TestBreachCreateJusticeTx tests that we create three different variants of
|
||||
// the justice tx.
|
||||
func TestBreachCreateJusticeTx(t *testing.T) {
|
||||
brar, _, _, _, _, cleanUpChans, cleanUpArb := initBreachedState(t)
|
||||
defer cleanUpChans()
|
||||
defer cleanUpArb()
|
||||
|
||||
// In this test we just want to check that the correct inputs are added
|
||||
// to the justice tx, not that we create a valid spend, so we just set
|
||||
// some params making the script generation succeed.
|
||||
aliceKeyPriv, _ := btcec.PrivKeyFromBytes(
|
||||
btcec.S256(), channels.AlicesPrivKey,
|
||||
)
|
||||
alicePubKey := aliceKeyPriv.PubKey()
|
||||
|
||||
signDesc := &breachedOutputs[0].signDesc
|
||||
signDesc.KeyDesc.PubKey = alicePubKey
|
||||
signDesc.DoubleTweak = aliceKeyPriv
|
||||
|
||||
// We'll test all the different types of outputs we'll sweep with the
|
||||
// justice tx.
|
||||
outputTypes := []input.StandardWitnessType{
|
||||
input.CommitmentNoDelay,
|
||||
input.CommitSpendNoDelayTweakless,
|
||||
input.CommitmentToRemoteConfirmed,
|
||||
input.CommitmentRevoke,
|
||||
input.HtlcAcceptedRevoke,
|
||||
input.HtlcOfferedRevoke,
|
||||
input.HtlcSecondLevelRevoke,
|
||||
}
|
||||
|
||||
breachedOutputs := make([]breachedOutput, len(outputTypes))
|
||||
for i, wt := range outputTypes {
|
||||
// Create a fake breached output for each type, ensuring they
|
||||
// have different outpoints for our logic to accept them.
|
||||
op := breachedOutputs[0].outpoint
|
||||
op.Index = uint32(i)
|
||||
breachedOutputs[i] = makeBreachedOutput(
|
||||
&op,
|
||||
wt,
|
||||
// Second level scripts doesn't matter in this test.
|
||||
nil,
|
||||
signDesc,
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
// Create the justice transactions.
|
||||
justiceTxs, err := brar.createJusticeTx(breachedOutputs)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, justiceTxs)
|
||||
|
||||
// The spendAll tx should be spending all the outputs. This is the
|
||||
// "regular" justice transaction type.
|
||||
require.Len(t, justiceTxs.spendAll.TxIn, len(breachedOutputs))
|
||||
|
||||
// The spendCommitOuts tx should be spending the 4 typed of commit outs
|
||||
// (note that in practice there will be at most two commit outputs per
|
||||
// commmit, but we test all 4 types here).
|
||||
require.Len(t, justiceTxs.spendCommitOuts.TxIn, 4)
|
||||
|
||||
// Finally check that the spendHTLCs tx are spending the two revoked
|
||||
// HTLC types, and the second level type.
|
||||
require.Len(t, justiceTxs.spendHTLCs.TxIn, 3)
|
||||
}
|
||||
|
||||
type publAssertion func(*testing.T, map[wire.OutPoint]struct{},
|
||||
chan *wire.MsgTx, chainhash.Hash) *wire.MsgTx
|
||||
|
||||
type breachTest struct {
|
||||
name string
|
||||
@ -1231,6 +1280,10 @@ type breachTest struct {
|
||||
// htlc is in effect "readded" to the set of inputs.
|
||||
spend2ndLevel bool
|
||||
|
||||
// sweepHtlc tests that the HTLC output is swept using the revocation
|
||||
// path in a separate tx.
|
||||
sweepHtlc bool
|
||||
|
||||
// sendFinalConf informs the test to send a confirmation for the justice
|
||||
// transaction before asserting the arbiter is cleaned up.
|
||||
sendFinalConf bool
|
||||
@ -1244,35 +1297,119 @@ type breachTest struct {
|
||||
whenZeroInputs publAssertion
|
||||
}
|
||||
|
||||
var (
|
||||
type spendTxs struct {
|
||||
commitSpendTx *wire.MsgTx
|
||||
htlc2ndLevlTx *wire.MsgTx
|
||||
htlc2ndLevlSpend *wire.MsgTx
|
||||
htlcSweep *wire.MsgTx
|
||||
}
|
||||
|
||||
func getSpendTransactions(signer input.Signer, chanPoint *wire.OutPoint,
|
||||
retribution *lnwallet.BreachRetribution) (*spendTxs, error) {
|
||||
|
||||
localOutpoint := retribution.LocalOutpoint
|
||||
remoteOutpoint := retribution.RemoteOutpoint
|
||||
htlcOutpoint := retribution.HtlcRetributions[0].OutPoint
|
||||
|
||||
// commitSpendTx is used to spend commitment outputs.
|
||||
commitSpendTx = &wire.MsgTx{
|
||||
commitSpendTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: localOutpoint,
|
||||
},
|
||||
{
|
||||
PreviousOutPoint: remoteOutpoint,
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{Value: 500000000},
|
||||
},
|
||||
}
|
||||
|
||||
// htlc2ndLevlTx is used to transition an htlc output on the commitment
|
||||
// transaction to a second level htlc.
|
||||
htlc2ndLevlTx = &wire.MsgTx{
|
||||
htlc2ndLevlTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{Value: 20000},
|
||||
},
|
||||
}
|
||||
|
||||
secondLvlOp := wire.OutPoint{
|
||||
Hash: htlc2ndLevlTx.TxHash(),
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
// htlcSpendTx is used to spend from a second level htlc.
|
||||
htlcSpendTx = &wire.MsgTx{
|
||||
htlcSpendTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: secondLvlOp,
|
||||
},
|
||||
},
|
||||
|
||||
TxOut: []*wire.TxOut{
|
||||
{Value: 10000},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// htlcSweep is used to spend the HTLC output directly using the
|
||||
// revocation key.
|
||||
htlcSweep := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{Value: 21000},
|
||||
},
|
||||
}
|
||||
|
||||
// In order for the breacharbiter to detect that it is being spent
|
||||
// using the revocation key, it will inspect the witness. Therefore
|
||||
// sign and add the witness to the HTLC sweep.
|
||||
retInfo := newRetributionInfo(chanPoint, retribution)
|
||||
|
||||
hashCache := txscript.NewTxSigHashes(htlcSweep)
|
||||
for i := range retInfo.breachedOutputs {
|
||||
inp := &retInfo.breachedOutputs[i]
|
||||
|
||||
// Find the HTLC output. so we can add the witness.
|
||||
switch inp.witnessType {
|
||||
case input.HtlcAcceptedRevoke:
|
||||
fallthrough
|
||||
case input.HtlcOfferedRevoke:
|
||||
inputScript, err := inp.CraftInputScript(
|
||||
signer, htlcSweep, hashCache, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
htlcSweep.TxIn[0].Witness = inputScript.Witness
|
||||
}
|
||||
}
|
||||
|
||||
return &spendTxs{
|
||||
commitSpendTx: commitSpendTx,
|
||||
htlc2ndLevlTx: htlc2ndLevlTx,
|
||||
htlc2ndLevlSpend: htlcSpendTx,
|
||||
htlcSweep: htlcSweep,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var breachTests = []breachTest{
|
||||
{
|
||||
name: "all spends",
|
||||
spend2ndLevel: true,
|
||||
whenNonZeroInputs: func(t *testing.T,
|
||||
inputs map[wire.OutPoint]*wire.MsgTx,
|
||||
publTx chan *wire.MsgTx) {
|
||||
inputs map[wire.OutPoint]struct{},
|
||||
publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx {
|
||||
|
||||
var tx *wire.MsgTx
|
||||
select {
|
||||
@ -1281,7 +1418,7 @@ var breachTests = []breachTest{
|
||||
t.Fatalf("tx was not published")
|
||||
}
|
||||
|
||||
// The justice transaction should have thee same number
|
||||
// The justice transaction should have the same number
|
||||
// of inputs as we are tracking in the test.
|
||||
if len(tx.TxIn) != len(inputs) {
|
||||
t.Fatalf("expected justice txn to have %d "+
|
||||
@ -1295,10 +1432,11 @@ var breachTests = []breachTest{
|
||||
findInputIndex(t, in, tx)
|
||||
}
|
||||
|
||||
return tx
|
||||
},
|
||||
whenZeroInputs: func(t *testing.T,
|
||||
inputs map[wire.OutPoint]*wire.MsgTx,
|
||||
publTx chan *wire.MsgTx) {
|
||||
inputs map[wire.OutPoint]struct{},
|
||||
publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx {
|
||||
|
||||
// Sanity check to ensure the brar doesn't try to
|
||||
// broadcast another sweep, since all outputs have been
|
||||
@ -1308,6 +1446,8 @@ var breachTests = []breachTest{
|
||||
t.Fatalf("tx published unexpectedly")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1315,18 +1455,36 @@ var breachTests = []breachTest{
|
||||
spend2ndLevel: false,
|
||||
sendFinalConf: true,
|
||||
whenNonZeroInputs: func(t *testing.T,
|
||||
inputs map[wire.OutPoint]*wire.MsgTx,
|
||||
publTx chan *wire.MsgTx) {
|
||||
inputs map[wire.OutPoint]struct{},
|
||||
publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx {
|
||||
|
||||
var tx *wire.MsgTx
|
||||
select {
|
||||
case <-publTx:
|
||||
case tx = <-publTx:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("tx was not published")
|
||||
}
|
||||
|
||||
// The justice transaction should have the same number
|
||||
// of inputs as we are tracking in the test.
|
||||
if len(tx.TxIn) != len(inputs) {
|
||||
t.Fatalf("expected justice txn to have %d "+
|
||||
"inputs, found %d", len(inputs),
|
||||
len(tx.TxIn))
|
||||
}
|
||||
|
||||
// Ensure that each input exists on the justice
|
||||
// transaction.
|
||||
for in := range inputs {
|
||||
findInputIndex(t, in, tx)
|
||||
}
|
||||
|
||||
return tx
|
||||
},
|
||||
whenZeroInputs: func(t *testing.T,
|
||||
inputs map[wire.OutPoint]*wire.MsgTx,
|
||||
publTx chan *wire.MsgTx) {
|
||||
inputs map[wire.OutPoint]struct{},
|
||||
publTx chan *wire.MsgTx,
|
||||
htlc2ndLevlTxHash chainhash.Hash) *wire.MsgTx {
|
||||
|
||||
// Now a transaction attempting to spend from the second
|
||||
// level tx should be published instead. Let this
|
||||
@ -1355,10 +1513,60 @@ var breachTests = []breachTest{
|
||||
// ensuring we aren't mistaking this for a different
|
||||
// output type.
|
||||
onlyInput := tx.TxIn[0].PreviousOutPoint.Hash
|
||||
if onlyInput != htlc2ndLevlTx.TxHash() {
|
||||
if onlyInput != htlc2ndLevlTxHash {
|
||||
t.Fatalf("tx not attempting to spend second "+
|
||||
"level tx, %v", tx.TxIn[0])
|
||||
}
|
||||
|
||||
return tx
|
||||
},
|
||||
},
|
||||
{ // nolint: dupl
|
||||
// Test that if the HTLC output is swept via the revoke path
|
||||
// (by us) in a separate tx, it will be handled correctly.
|
||||
name: "sweep htlc",
|
||||
sweepHtlc: true,
|
||||
whenNonZeroInputs: func(t *testing.T,
|
||||
inputs map[wire.OutPoint]struct{},
|
||||
publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx {
|
||||
|
||||
var tx *wire.MsgTx
|
||||
select {
|
||||
case tx = <-publTx:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("tx was not published")
|
||||
}
|
||||
|
||||
// The justice transaction should have the same number
|
||||
// of inputs as we are tracking in the test.
|
||||
if len(tx.TxIn) != len(inputs) {
|
||||
t.Fatalf("expected justice txn to have %d "+
|
||||
"inputs, found %d", len(inputs),
|
||||
len(tx.TxIn))
|
||||
}
|
||||
|
||||
// Ensure that each input exists on the justice
|
||||
// transaction.
|
||||
for in := range inputs {
|
||||
findInputIndex(t, in, tx)
|
||||
}
|
||||
|
||||
return tx
|
||||
},
|
||||
whenZeroInputs: func(t *testing.T,
|
||||
inputs map[wire.OutPoint]struct{},
|
||||
publTx chan *wire.MsgTx, _ chainhash.Hash) *wire.MsgTx {
|
||||
|
||||
// Sanity check to ensure the brar doesn't try to
|
||||
// broadcast another sweep, since all outputs have been
|
||||
// spent externally.
|
||||
select {
|
||||
case <-publTx:
|
||||
t.Fatalf("tx published unexpectedly")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1417,7 +1625,11 @@ func testBreachSpends(t *testing.T, test breachTest) {
|
||||
},
|
||||
BreachRetribution: retribution,
|
||||
}
|
||||
contractBreaches <- breach
|
||||
select {
|
||||
case contractBreaches <- breach:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("breach not delivered")
|
||||
}
|
||||
|
||||
// We'll also wait to consume the ACK back from the breach arbiter.
|
||||
select {
|
||||
@ -1458,7 +1670,12 @@ func testBreachSpends(t *testing.T, test breachTest) {
|
||||
// Notify that the breaching transaction is confirmed, to trigger the
|
||||
// retribution logic.
|
||||
notifier := brar.cfg.Notifier.(*mock.SpendNotifier)
|
||||
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
|
||||
|
||||
select {
|
||||
case notifier.ConfChan <- &chainntnfs.TxConfirmation{}:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("conf not delivered")
|
||||
}
|
||||
|
||||
// The breach arbiter should attempt to sweep all outputs on the
|
||||
// breached commitment. We'll pretend that the HTLC output has been
|
||||
@ -1482,59 +1699,283 @@ func testBreachSpends(t *testing.T, test breachTest) {
|
||||
remoteOutpoint := retribution.RemoteOutpoint
|
||||
htlcOutpoint := retribution.HtlcRetributions[0].OutPoint
|
||||
|
||||
spendTxs, err := getSpendTransactions(
|
||||
brar.cfg.Signer, chanPoint, retribution,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Construct a map from outpoint on the force close to the transaction
|
||||
// we want it to be spent by. As the test progresses, this map will be
|
||||
// updated to contain only the set of commitment or second level
|
||||
// outpoints that remain to be spent.
|
||||
inputs := map[wire.OutPoint]*wire.MsgTx{
|
||||
htlcOutpoint: htlc2ndLevlTx,
|
||||
localOutpoint: commitSpendTx,
|
||||
remoteOutpoint: commitSpendTx,
|
||||
spentBy := map[wire.OutPoint]*wire.MsgTx{
|
||||
htlcOutpoint: spendTxs.htlc2ndLevlTx,
|
||||
localOutpoint: spendTxs.commitSpendTx,
|
||||
remoteOutpoint: spendTxs.commitSpendTx,
|
||||
}
|
||||
|
||||
// We also keep a map of those remaining outputs we expect the
|
||||
// breacharbiter to try and sweep.
|
||||
inputsToSweep := map[wire.OutPoint]struct{}{
|
||||
htlcOutpoint: {},
|
||||
localOutpoint: {},
|
||||
remoteOutpoint: {},
|
||||
}
|
||||
|
||||
htlc2ndLevlTx := spendTxs.htlc2ndLevlTx
|
||||
htlcSpendTx := spendTxs.htlc2ndLevlSpend
|
||||
|
||||
// If the test is checking sweep of the HTLC directly without the
|
||||
// second level, insert the sweep tx instead.
|
||||
if test.sweepHtlc {
|
||||
spentBy[htlcOutpoint] = spendTxs.htlcSweep
|
||||
}
|
||||
|
||||
// Until no more inputs to spend remain, deliver the spend events and
|
||||
// process the assertions prescribed by the test case.
|
||||
for len(inputs) > 0 {
|
||||
var justiceTx *wire.MsgTx
|
||||
for len(spentBy) > 0 {
|
||||
var (
|
||||
op wire.OutPoint
|
||||
spendTx *wire.MsgTx
|
||||
)
|
||||
|
||||
// Pick an outpoint at random from the set of inputs.
|
||||
for op, spendTx = range inputs {
|
||||
delete(inputs, op)
|
||||
for op, spendTx = range spentBy {
|
||||
delete(spentBy, op)
|
||||
break
|
||||
}
|
||||
|
||||
// Deliver the spend notification for the chosen transaction.
|
||||
notifier.Spend(&op, 2, spendTx)
|
||||
|
||||
// When the second layer transfer is detected, add back the
|
||||
// outpoint of the second layer tx so that we can spend it
|
||||
// again. Only do so if the test requests this behavior.
|
||||
// Since the remote just swept this input, we expect our next
|
||||
// justice transaction to not include them.
|
||||
delete(inputsToSweep, op)
|
||||
|
||||
// If this is the second-level spend, we must add the new
|
||||
// outpoint to our expected sweeps.
|
||||
spendTxID := spendTx.TxHash()
|
||||
if test.spend2ndLevel && spendTxID == htlc2ndLevlTx.TxHash() {
|
||||
// Create the second level outpoint that will be spent,
|
||||
// the index is always zero for these 1-in-1-out txns.
|
||||
if spendTxID == htlc2ndLevlTx.TxHash() {
|
||||
// Create the second level outpoint that will
|
||||
// be spent, the index is always zero for these
|
||||
// 1-in-1-out txns.
|
||||
spendOp := wire.OutPoint{Hash: spendTxID}
|
||||
inputs[spendOp] = htlcSpendTx
|
||||
inputsToSweep[spendOp] = struct{}{}
|
||||
|
||||
// When the second layer transfer is detected, add back
|
||||
// the outpoint of the second layer tx so that we can
|
||||
// spend it again. Only do so if the test requests this
|
||||
// behavior.
|
||||
if test.spend2ndLevel {
|
||||
spentBy[spendOp] = htlcSpendTx
|
||||
}
|
||||
}
|
||||
|
||||
if len(inputs) > 0 {
|
||||
test.whenNonZeroInputs(t, inputs, publTx)
|
||||
if len(spentBy) > 0 {
|
||||
justiceTx = test.whenNonZeroInputs(t, inputsToSweep, publTx, htlc2ndLevlTx.TxHash())
|
||||
} else {
|
||||
// Reset the publishing error so that any publication,
|
||||
// made by the breach arbiter, if any, will succeed.
|
||||
publMtx.Lock()
|
||||
publErr = nil
|
||||
publMtx.Unlock()
|
||||
test.whenZeroInputs(t, inputs, publTx)
|
||||
justiceTx = test.whenZeroInputs(t, inputsToSweep, publTx, htlc2ndLevlTx.TxHash())
|
||||
}
|
||||
}
|
||||
|
||||
// Deliver confirmation of sweep if the test expects it.
|
||||
// Deliver confirmation of sweep if the test expects it. Since we are
|
||||
// looking for the final justice tx to confirme, we deliver a spend of
|
||||
// all its inputs.
|
||||
if test.sendFinalConf {
|
||||
notifier.ConfChan <- &chainntnfs.TxConfirmation{}
|
||||
for _, txin := range justiceTx.TxIn {
|
||||
op := txin.PreviousOutPoint
|
||||
notifier.Spend(&op, 3, justiceTx)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the channel is fully resolved.
|
||||
assertBrarCleanup(t, brar, alice.ChanPoint, alice.State().Db)
|
||||
}
|
||||
|
||||
// TestBreachDelayedJusticeConfirmation tests that the breach arbiter will
|
||||
// "split" the justice tx in case the first justice tx doesn't confirm within
|
||||
// a reasonable time.
|
||||
func TestBreachDelayedJusticeConfirmation(t *testing.T) {
|
||||
brar, alice, _, bobClose, contractBreaches,
|
||||
cleanUpChans, cleanUpArb := initBreachedState(t)
|
||||
defer cleanUpChans()
|
||||
defer cleanUpArb()
|
||||
|
||||
var (
|
||||
height = bobClose.ChanSnapshot.CommitHeight
|
||||
blockHeight = int32(10)
|
||||
forceCloseTx = bobClose.CloseTx
|
||||
chanPoint = alice.ChanPoint
|
||||
publTx = make(chan *wire.MsgTx)
|
||||
)
|
||||
|
||||
// Make PublishTransaction always return succeed.
|
||||
brar.cfg.PublishTransaction = func(tx *wire.MsgTx, _ string) error {
|
||||
publTx <- tx
|
||||
return nil
|
||||
}
|
||||
|
||||
// Notify the breach arbiter about the breach.
|
||||
retribution, err := lnwallet.NewBreachRetribution(
|
||||
alice.State(), height, uint32(blockHeight),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create breach retribution: %v", err)
|
||||
}
|
||||
|
||||
processACK := make(chan error, 1)
|
||||
breach := &ContractBreachEvent{
|
||||
ChanPoint: *chanPoint,
|
||||
ProcessACK: func(brarErr error) {
|
||||
processACK <- brarErr
|
||||
},
|
||||
BreachRetribution: retribution,
|
||||
}
|
||||
|
||||
select {
|
||||
case contractBreaches <- breach:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("breach not delivered")
|
||||
}
|
||||
|
||||
// We'll also wait to consume the ACK back from the breach arbiter.
|
||||
select {
|
||||
case err := <-processACK:
|
||||
if err != nil {
|
||||
t.Fatalf("handoff failed: %v", err)
|
||||
}
|
||||
case <-time.After(time.Second * 15):
|
||||
t.Fatalf("breach arbiter didn't send ack back")
|
||||
}
|
||||
|
||||
state := alice.State()
|
||||
err = state.CloseChannel(&channeldb.ChannelCloseSummary{
|
||||
ChanPoint: state.FundingOutpoint,
|
||||
ChainHash: state.ChainHash,
|
||||
RemotePub: state.IdentityPub,
|
||||
CloseType: channeldb.BreachClose,
|
||||
Capacity: state.Capacity,
|
||||
IsPending: true,
|
||||
ShortChanID: state.ShortChanID(),
|
||||
RemoteCurrentRevocation: state.RemoteCurrentRevocation,
|
||||
RemoteNextRevocation: state.RemoteNextRevocation,
|
||||
LocalChanConfig: state.LocalChanCfg,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to close channel: %v", err)
|
||||
}
|
||||
|
||||
// After exiting, the breach arbiter should have persisted the
|
||||
// retribution information and the channel should be shown as pending
|
||||
// force closed.
|
||||
assertArbiterBreach(t, brar, chanPoint)
|
||||
|
||||
// Assert that the database sees the channel as pending close, otherwise
|
||||
// the breach arbiter won't be able to fully close it.
|
||||
assertPendingClosed(t, alice)
|
||||
|
||||
// Notify that the breaching transaction is confirmed, to trigger the
|
||||
// retribution logic.
|
||||
notifier := brar.cfg.Notifier.(*mock.SpendNotifier)
|
||||
|
||||
select {
|
||||
case notifier.ConfChan <- &chainntnfs.TxConfirmation{}:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatalf("conf not delivered")
|
||||
}
|
||||
|
||||
// The breach arbiter should attempt to sweep all outputs on the
|
||||
// breached commitment.
|
||||
var justiceTx *wire.MsgTx
|
||||
select {
|
||||
case justiceTx = <-publTx:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("tx was not published")
|
||||
}
|
||||
|
||||
require.Len(t, justiceTx.TxIn, 3)
|
||||
|
||||
// All outputs should initially spend from the force closed txn.
|
||||
forceTxID := forceCloseTx.TxHash()
|
||||
for _, txIn := range justiceTx.TxIn {
|
||||
if txIn.PreviousOutPoint.Hash != forceTxID {
|
||||
t.Fatalf("og justice tx not spending commitment")
|
||||
}
|
||||
}
|
||||
|
||||
// Now we'll pretend some blocks pass without the justice tx
|
||||
// confirming.
|
||||
for i := int32(0); i <= 3; i++ {
|
||||
notifier.EpochChan <- &chainntnfs.BlockEpoch{
|
||||
Height: blockHeight + i,
|
||||
}
|
||||
|
||||
// On every epoch, check that no new tx is published.
|
||||
select {
|
||||
case <-publTx:
|
||||
t.Fatalf("tx was published")
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
// Now mine another block without the justice tx confirming. This
|
||||
// should lead to the breacharbiter publishing the split justice tx
|
||||
// variants.
|
||||
notifier.EpochChan <- &chainntnfs.BlockEpoch{
|
||||
Height: blockHeight + 4,
|
||||
}
|
||||
|
||||
var (
|
||||
splits []*wire.MsgTx
|
||||
spending = make(map[wire.OutPoint]struct{})
|
||||
maxIndex = uint32(len(forceCloseTx.TxOut)) - 1
|
||||
)
|
||||
for i := 0; i < 2; i++ {
|
||||
|
||||
var tx *wire.MsgTx
|
||||
select {
|
||||
case tx = <-publTx:
|
||||
splits = append(splits, tx)
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("tx not published")
|
||||
}
|
||||
|
||||
// Check that every input is from the breached tx and that
|
||||
// there are no duplicates.
|
||||
for _, in := range tx.TxIn {
|
||||
op := in.PreviousOutPoint
|
||||
_, ok := spending[op]
|
||||
if ok {
|
||||
t.Fatal("already spent")
|
||||
}
|
||||
|
||||
if op.Hash != forceTxID || op.Index > maxIndex {
|
||||
t.Fatalf("not spending breach")
|
||||
}
|
||||
|
||||
spending[op] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// All the inputs from the original justice transaction should have
|
||||
// been spent by the 2 splits.
|
||||
require.Len(t, spending, len(justiceTx.TxIn))
|
||||
require.Len(t, splits, 2)
|
||||
|
||||
// Finally notify that they confirm, making the breach arbiter clean
|
||||
// up.
|
||||
for _, tx := range splits {
|
||||
for _, in := range tx.TxIn {
|
||||
op := &in.PreviousOutPoint
|
||||
notifier.Spend(op, blockHeight+5, tx)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the channel is fully resolved.
|
||||
|
@ -313,21 +313,32 @@ func SenderHtlcSpendRevokeWithKey(signer Signer, signDesc *SignDescriptor,
|
||||
func SenderHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor,
|
||||
sweepTx *wire.MsgTx) (wire.TxWitness, error) {
|
||||
|
||||
if signDesc.KeyDesc.PubKey == nil {
|
||||
return nil, fmt.Errorf("cannot generate witness with nil " +
|
||||
"KeyDesc pubkey")
|
||||
revokeKey, err := deriveRevokePubKey(signDesc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Derive the revocation key using the local revocation base point and
|
||||
// commitment point.
|
||||
revokeKey := DeriveRevocationPubkey(
|
||||
signDesc.KeyDesc.PubKey,
|
||||
signDesc.DoubleTweak.PubKey(),
|
||||
)
|
||||
|
||||
return SenderHtlcSpendRevokeWithKey(signer, signDesc, revokeKey, sweepTx)
|
||||
}
|
||||
|
||||
// IsHtlcSpendRevoke is used to determine if the passed spend is spending a
|
||||
// HTLC output using the revocation key.
|
||||
func IsHtlcSpendRevoke(txIn *wire.TxIn, signDesc *SignDescriptor) (
|
||||
bool, error) {
|
||||
|
||||
revokeKey, err := deriveRevokePubKey(signDesc)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(txIn.Witness) == 3 &&
|
||||
bytes.Equal(txIn.Witness[1], revokeKey.SerializeCompressed()) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SenderHtlcSpendRedeem constructs a valid witness allowing the receiver of an
|
||||
// HTLC to redeem the pending output in the scenario that the sender broadcasts
|
||||
// their version of the commitment transaction. A valid spend requires
|
||||
@ -575,16 +586,7 @@ func ReceiverHtlcSpendRevokeWithKey(signer Signer, signDesc *SignDescriptor,
|
||||
return witnessStack, nil
|
||||
}
|
||||
|
||||
// ReceiverHtlcSpendRevoke constructs a valid witness allowing the sender of an
|
||||
// HTLC within a previously revoked commitment transaction to re-claim the
|
||||
// pending funds in the case that the receiver broadcasts this revoked
|
||||
// commitment transaction. This method first derives the appropriate revocation
|
||||
// key, and requires that the provided SignDescriptor has a local revocation
|
||||
// basepoint and commitment secret in the PubKey and DoubleTweak fields,
|
||||
// respectively.
|
||||
func ReceiverHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor,
|
||||
sweepTx *wire.MsgTx) (wire.TxWitness, error) {
|
||||
|
||||
func deriveRevokePubKey(signDesc *SignDescriptor) (*btcec.PublicKey, error) {
|
||||
if signDesc.KeyDesc.PubKey == nil {
|
||||
return nil, fmt.Errorf("cannot generate witness with nil " +
|
||||
"KeyDesc pubkey")
|
||||
@ -597,6 +599,24 @@ func ReceiverHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor,
|
||||
signDesc.DoubleTweak.PubKey(),
|
||||
)
|
||||
|
||||
return revokeKey, nil
|
||||
}
|
||||
|
||||
// ReceiverHtlcSpendRevoke constructs a valid witness allowing the sender of an
|
||||
// HTLC within a previously revoked commitment transaction to re-claim the
|
||||
// pending funds in the case that the receiver broadcasts this revoked
|
||||
// commitment transaction. This method first derives the appropriate revocation
|
||||
// key, and requires that the provided SignDescriptor has a local revocation
|
||||
// basepoint and commitment secret in the PubKey and DoubleTweak fields,
|
||||
// respectively.
|
||||
func ReceiverHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor,
|
||||
sweepTx *wire.MsgTx) (wire.TxWitness, error) {
|
||||
|
||||
revokeKey, err := deriveRevokePubKey(signDesc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ReceiverHtlcSpendRevokeWithKey(signer, signDesc, revokeKey, sweepTx)
|
||||
}
|
||||
|
||||
|
@ -67,13 +67,20 @@ func (s *SpendNotifier) Spend(outpoint *wire.OutPoint, height int32,
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
var inputIndex uint32
|
||||
for i, in := range txn.TxIn {
|
||||
if in.PreviousOutPoint == *outpoint {
|
||||
inputIndex = uint32(i)
|
||||
}
|
||||
}
|
||||
|
||||
txnHash := txn.TxHash()
|
||||
details := &chainntnfs.SpendDetail{
|
||||
SpentOutPoint: outpoint,
|
||||
SpendingHeight: height,
|
||||
SpendingTx: txn,
|
||||
SpenderTxHash: &txnHash,
|
||||
SpenderInputIndex: outpoint.Index,
|
||||
SpenderInputIndex: inputIndex,
|
||||
}
|
||||
|
||||
// Cache details in case of late registration.
|
||||
|
Loading…
Reference in New Issue
Block a user