Merge pull request #5036 from halseth/breacharbiter-justice-splitting

[breacharbiter] Split justice tx in case of delayed confirmation.
This commit is contained in:
Olaoluwa Osuntokun 2021-05-12 13:42:42 -07:00 committed by GitHub
commit c0acdd8082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 898 additions and 325 deletions

@ -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)
}
// 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.
func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo,
spendNtfns map[wire.OutPoint]*chainntnfs.SpendEvent) error {
inputs := breachInfo.breachedOutputs
// spend is used to wrap the index of the output that gets spent
// together with the spend details.
// 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
// 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) ([]spend, error) {
inputs := breachInfo.breachedOutputs
// 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,21 +451,63 @@ 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)
spends = append(spends, s)
}
return spends, nil
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
}
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)
breachedOutput.outpoint, breachInfo.chanPoint)
// In this case we'll morph our initial revoke
// spend to instead point to the second level
@ -465,6 +520,22 @@ func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo,
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 {
// 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()
}
totalFunds += breachedOutput.Amount()
brarLog.Infof("Spend on %s(%v) for ChannelPoint(%v) "+
"transitions output to terminal state, "+
"removing input from justice transaction",
@ -488,12 +559,7 @@ func (b *breachArbiter) waitForSpendEvent(breachInfo *retributionInfo,
// Update our remaining set of outputs before continuing with
// another attempt at publication.
breachInfo.breachedOutputs = inputs[:nextIndex]
case <-b.quit:
return errBrarShuttingDown
}
return nil
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)
if err != nil {
brarLog.Errorf("Unable to get finalized txn for"+
"chanid=%v: %v", &breachInfo.chanPoint, err)
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
// 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)
justiceTxs, err := b.createJusticeTx(breachInfo.breachedOutputs)
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
err := b.waitForSpendEvent(breachInfo, spendNtfns)
if err != nil {
if err != errBrarShuttingDown {
brarLog.Errorf("error waiting for "+
"spend event: %v", err)
}
// 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
)
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,80 +685,103 @@ 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:
// On every new block, we check whether we should republish the
// transactions.
case epoch, ok := <-newBlockChan.Epochs:
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:
}
// 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.Infof("Justice for ChannelPoint(%v) has "+
"been served, %v revoked funds (%v total) "+
"have been claimed", breachInfo.chanPoint,
revokedFunds, totalFunds)
brarLog.Warnf("Block height %v arrived without "+
"justice tx confirming (breached at "+
"height %v), splitting justice tx.",
epoch.Height, breachInfo.breachHeight)
err = b.cleanupBreach(&breachInfo.chanPoint)
// 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.Errorf("Failed to cleanup breached "+
"ChannelPoint(%v): %v", breachInfo.chanPoint,
err)
brarLog.Warnf("Unable to broadcast "+
"commit out spending justice "+
"tx: %v", err)
}
}
// TODO(roasbeef): add peer to blacklist?
if justiceTxs.spendHTLCs != nil {
tx := justiceTxs.spendHTLCs
// TODO(roasbeef): close other active channels with offending
// peer
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
return
case <-b.quit:
return
break Loop
}
}
// Wait for our go routine to exit.
wg.Wait()
}
// cleanupBreach marks the given channel point as fully resolved and removes the
// retribution for that the channel from the retribution store.
func (b *breachArbiter) cleanupBreach(chanPoint *wire.OutPoint) error {
@ -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.