breacharbiter: properly account for second-level spends during breach remedy

In this commit, we address an un accounted for case during the breach
remedy process. If the remote node actually went directly to the second
layer during a channel breach attempt, then we wouldn’t properly be
able to sweep with out justice transaction, as some HTLC inputs may
actually be spent at that point.

In order to address this case, we’ll now catch the transaction
rejection, then check to see which input was spent, promote that to a
second level spend, and repeat as necessary. At the end of this loop,
any inputs which have been spent to the second level will have had the
prevouts and witnesses updated.

In order to perform this transition, we now also store the second level
witness script in the database. This allow us to modify the sign desc
with the proper input value, as well as witness script.
This commit is contained in:
Olaoluwa Osuntokun 2018-01-22 17:11:02 -08:00
parent 4e6c816d11
commit 2faafbcd93
No known key found for this signature in database
GPG Key ID: 964EA263DD637C21
5 changed files with 259 additions and 86 deletions

View File

@ -410,20 +410,56 @@ out:
return
}
// convertToSecondLevelRevoke takes a breached output, and a transaction that
// spends it to the second level, and mutates the breach output into one that
// is able to properly sweep that second level output. We'll use this function
// when we go to sweep a breached commitment transaction, but the cheating
// party has already attempted to take it to the second level
func convertToSecondLevelRevoke(bo *breachedOutput, breachInfo *retributionInfo,
spendDetails *chainntnfs.SpendDetail) {
// In this case, we'll modify the witness type of this output to
// actually prepare for a second level revoke.
bo.witnessType = lnwallet.HtlcSecondLevelRevoke
// We'll also redirect the outpoint to this second level output, so the
// spending transaction updates it inputs accordingly.
spendingTx := spendDetails.SpendingTx
oldOp := bo.outpoint
bo.outpoint = wire.OutPoint{
Hash: spendingTx.TxHash(),
Index: 0,
}
// Next, we need to update the amount so we can do fee estimation
// properly, and also so we can generate a valid signature as we need
// to know the new input value (the second level transactions shaves
// off some funds to fees).
newAmt := spendingTx.TxOut[0].Value
bo.amt = btcutil.Amount(newAmt)
bo.signDesc.Output.Value = newAmt
// Finally, we'll need to adjust the witness program in the
// SignDescriptor.
bo.signDesc.WitnessScript = bo.secondLevelWitnessScript
brarLog.Warnf("HTLC(%v) for ChannelPoint(%v) has been spent to the "+
"second-level, adjusting -> %v", oldOp, breachInfo.chanPoint,
bo.outpoint)
}
// exactRetribution is a goroutine which is executed once a contract breach has
// been detected by a breachObserver. This function is responsible for
// punishing a counterparty for violating the channel contract by sweeping ALL
// the lingering funds within the channel into the daemon's wallet.
//
// NOTE: This MUST be run as a goroutine.
func (b *breachArbiter) exactRetribution(
confChan *chainntnfs.ConfirmationEvent,
func (b *breachArbiter) exactRetribution(confChan *chainntnfs.ConfirmationEvent,
breachInfo *retributionInfo) {
defer b.wg.Done()
// TODO(roasbeef): state needs to be checkpointed here
var breachConfHeight uint32
select {
case breachConf, ok := <-confChan.Confirmed:
@ -455,9 +491,60 @@ func (b *breachArbiter) exactRetribution(
// construct a sweep transaction and write it to disk. This will allow
// the breach arbiter to re-register for notifications for the justice
// txid.
secondLevelCheck:
if finalTx == nil {
// Before we create the justice tx, we need to check to see if
// any of the active HTLC's on the commitment transactions has
// been spent. In this case, we'll need to go to the second
// level to sweep them before the remote party can.
for i := 0; i < len(breachInfo.breachedOutputs); i++ {
breachedOutput := &breachInfo.breachedOutputs[i]
// If this isn't an HTLC output, then we can skip it.
if breachedOutput.witnessType != lnwallet.HtlcAcceptedRevoke &&
breachedOutput.witnessType != lnwallet.HtlcOfferedRevoke {
continue
}
brarLog.Debugf("Checking for second-level attempt on "+
"HTLC(%v) for ChannelPoint(%v)",
breachedOutput.outpoint, breachInfo.chanPoint)
// Now that we have an HTLC output, we'll quickly check
// to see if it has been spent or not.
spendNtfn, err := b.cfg.Notifier.RegisterSpendNtfn(
&breachedOutput.outpoint, breachInfo.breachHeight,
)
if err != nil {
brarLog.Errorf("unable to check for spentness "+
"of out_point=%v: %v",
breachedOutput.outpoint, err)
continue
}
select {
// The output has been taken to the second level!
case spendDetails, ok := <-spendNtfn.Spend:
if !ok {
return
}
// 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, spendDetails,
)
// It hasn't been spent so we'll continue.
default:
}
}
// With the breach transaction confirmed, we now create the
// justice tx which will claim ALL the funds within the channel.
// 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)
@ -474,16 +561,22 @@ func (b *breachArbiter) exactRetribution(
}
}
brarLog.Debugf("Broadcasting justice tx: %v",
newLogClosure(func() string {
return spew.Sdump(finalTx)
}))
brarLog.Debugf("Broadcasting justice tx: %v", newLogClosure(func() string {
return spew.Sdump(finalTx)
}))
// Finally, broadcast the transaction, finalizing the channels'
// retribution against the cheating counterparty.
if err := b.cfg.PublishTransaction(finalTx); err != nil {
// We'll now attempt to broadcast the transaction which finalized the
// channel's retribution against the cheating counter party.
err = b.cfg.PublishTransaction(finalTx)
if err != nil {
brarLog.Errorf("unable to broadcast "+
"justice tx: %v", err)
if strings.Contains(err.Error(), "already been spent") {
brarLog.Infof("Attempting to transfer HTLC revocations " +
"to the second level")
finalTx = nil
goto secondLevelCheck
}
}
// As a conclusionary step, we register for a notification to be
@ -709,6 +802,8 @@ type breachedOutput struct {
witnessType lnwallet.WitnessType
signDesc lnwallet.SignDescriptor
secondLevelWitnessScript []byte
witnessFunc lnwallet.WitnessGenerator
}
@ -716,15 +811,17 @@ type breachedOutput struct {
// breach arbiter to construct a justice or sweep transaction.
func makeBreachedOutput(outpoint *wire.OutPoint,
witnessType lnwallet.WitnessType,
secondLevelScript []byte,
signDescriptor *lnwallet.SignDescriptor) breachedOutput {
amount := signDescriptor.Output.Value
return breachedOutput{
amt: btcutil.Amount(amount),
outpoint: *outpoint,
witnessType: witnessType,
signDesc: *signDescriptor,
amt: btcutil.Amount(amount),
outpoint: *outpoint,
secondLevelWitnessScript: secondLevelScript,
witnessType: witnessType,
signDesc: *signDescriptor,
}
}
@ -756,17 +853,14 @@ func (bo *breachedOutput) SignDesc() *lnwallet.SignDescriptor {
// generation function, which parameterized primarily by the witness type and
// sign descriptor. The method then returns the witness computed by invoking
// this function on the first and subsequent calls.
func (bo *breachedOutput) BuildWitness(signer lnwallet.Signer,
txn *wire.MsgTx,
hashCache *txscript.TxSigHashes,
txinIdx int) ([][]byte, error) {
func (bo *breachedOutput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx,
hashCache *txscript.TxSigHashes, txinIdx int) ([][]byte, error) {
// First, we ensure that the witness generation function has
// been initialized for this breached output.
if bo.witnessFunc == nil {
bo.witnessFunc = bo.witnessType.GenWitnessFunc(
signer, bo.SignDesc())
}
// First, we ensure that the witness generation function has been
// initialized for this breached output.
bo.witnessFunc = bo.witnessType.GenWitnessFunc(
signer, bo.SignDesc(),
)
// Now that we have ensured that the witness generation function has
// been initialized, we can proceed to execute it and generate the
@ -818,6 +912,9 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
localOutput := makeBreachedOutput(
&breachInfo.LocalOutpoint,
lnwallet.CommitmentNoDelay,
// No second level script as this is a commitment
// output.
nil,
breachInfo.LocalOutputSignDesc)
breachedOutputs = append(breachedOutputs, localOutput)
@ -832,6 +929,9 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
remoteOutput := makeBreachedOutput(
&breachInfo.RemoteOutpoint,
lnwallet.CommitmentRevoke,
// No second level script as this is a commitment
// output.
nil,
breachInfo.RemoteOutputSignDesc)
breachedOutputs = append(breachedOutputs, remoteOutput)
@ -855,6 +955,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
htlcOutput := makeBreachedOutput(
&breachInfo.HtlcRetributions[i].OutPoint,
htlcWitnessType,
breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
&breachInfo.HtlcRetributions[i].SignDesc)
breachedOutputs = append(breachedOutputs, htlcOutput)
@ -865,6 +966,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
chainHash: breachInfo.ChainHash,
chanPoint: *chanPoint,
breachedOutputs: breachedOutputs,
breachHeight: breachInfo.BreachHeight,
}
}
@ -917,6 +1019,9 @@ func (b *breachArbiter) createJusticeTx(
case lnwallet.HtlcAcceptedRevoke:
witnessWeight = lnwallet.AcceptedHtlcPenaltyWitnessSize
case lnwallet.HtlcSecondLevelRevoke:
witnessWeight = lnwallet.SecondLevelHtlcPenaltyWitnessSize
default:
brarLog.Warnf("breached output in retribution info "+
"contains unexpected witness type: %v",
@ -961,6 +1066,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight uint64,
}
txFee := btcutil.Amount(txWeight * uint64(feePerWeight))
// TODO(roasbeef): already start to siphon their funds into fees
sweepAmt := int64(totalAmt - txFee)
// With the fee calculated, we can now create the transaction using the
@ -1353,7 +1459,13 @@ func (bo *breachedOutput) Encode(w io.Writer) error {
return err
}
if err := lnwallet.WriteSignDescriptor(w, &bo.signDesc); err != nil {
err := lnwallet.WriteSignDescriptor(w, &bo.signDesc)
if err != nil {
return err
}
err = wire.WriteVarBytes(w, 0, bo.secondLevelWitnessScript)
if err != nil {
return err
}
@ -1382,11 +1494,18 @@ func (bo *breachedOutput) Decode(r io.Reader) error {
return err
}
wScript, err := wire.ReadVarBytes(r, 0, 1000, "witness script")
if err != nil {
return err
}
bo.secondLevelWitnessScript = wScript
if _, err := io.ReadFull(r, scratch[:2]); err != nil {
return err
}
bo.witnessType = lnwallet.WitnessType(
binary.BigEndian.Uint16(scratch[:2]))
binary.BigEndian.Uint16(scratch[:2]),
)
return nil
}

View File

@ -8,15 +8,17 @@ import (
"fmt"
"io/ioutil"
"math/rand"
"net"
"os"
"reflect"
"sync"
"testing"
"time"
"github.com/btcsuite/btclog"
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
@ -128,6 +130,7 @@ var (
},
HashType: txscript.SigHashAll,
},
secondLevelWitnessScript: breachKeys[0],
},
{
amt: btcutil.Amount(2e9),
@ -171,6 +174,7 @@ var (
},
HashType: txscript.SigHashAll,
},
secondLevelWitnessScript: breachKeys[0],
},
{
amt: btcutil.Amount(3e4),
@ -214,6 +218,7 @@ var (
},
HashType: txscript.SigHashAll,
},
secondLevelWitnessScript: breachKeys[0],
},
}
@ -933,16 +938,26 @@ func TestBreachHandoffSuccess(t *testing.T) {
// Create a pair of channels using a notifier that allows us to signal
// a spend of the funding transaction. Alice's channel will be the on
// observing a breach.
notifier := makeMockSpendNotifier()
alice, bob, cleanUpChans, err := createInitChannelsWithNotifier(
1, notifier)
alice, bob, cleanUpChans, err := createInitChannels(1)
if err != nil {
t.Fatalf("unable to create test channels: %v", err)
}
defer cleanUpChans()
// Instantiate a breach arbiter to handle the breach of alice's channel.
brar, cleanUpArb, err := createTestArbiter(t, notifier, alice.State().Db)
alicePoint := alice.ChannelPoint()
spendEvents := contractcourt.ChainEventSubscription{
UnilateralClosure: make(chan *lnwallet.UnilateralCloseSummary, 1),
CooperativeClosure: make(chan struct{}, 1),
ContractBreach: make(chan *lnwallet.BreachRetribution, 1),
ProcessACK: make(chan error, 1),
ChanPoint: *alicePoint,
Cancel: func() {
},
}
brar, cleanUpArb, err := createTestArbiter(
t, &spendEvents, alice.State().Db,
)
if err != nil {
t.Fatalf("unable to initialize test breach arbiter: %v", err)
}
@ -963,7 +978,7 @@ func TestBreachHandoffSuccess(t *testing.T) {
// Generate the force close summary at this point in time, this will
// serve as the old state bob will broadcast.
forceCloseSummary, err := bob.ForceClose()
bobClose, err := bob.ForceClose()
if err != nil {
t.Fatalf("unable to force close bob's channel: %v", err)
}
@ -982,18 +997,24 @@ func TestBreachHandoffSuccess(t *testing.T) {
}
chanPoint := alice.ChanPoint
breachTxn := forceCloseSummary.CloseTx
// Signal a spend of the funding transaction and wait for the close
// observer to exit.
notifier.Spend(chanPoint, 100, breachTxn)
alice.WaitForClose()
spendEvents.ContractBreach <- &lnwallet.BreachRetribution{
BreachTransaction: bobClose.CloseTx,
}
// We'll also wait to consume the ACK back from the breach arbiter.
select {
case <-spendEvents.ProcessACK:
case <-time.After(time.Second * 15):
t.Fatalf("breach arbiter didn't send ack back")
}
// 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)
assertPendingClosed(t, alice)
}
// TestBreachHandoffFail tests that a channel's close observer properly
@ -1005,16 +1026,26 @@ func TestBreachHandoffFail(t *testing.T) {
// Create a pair of channels using a notifier that allows us to signal
// a spend of the funding transaction. Alice's channel will be the on
// observing a breach.
notifier := makeMockSpendNotifier()
alice, bob, cleanUpChans, err := createInitChannelsWithNotifier(
1, notifier)
alice, bob, cleanUpChans, err := createInitChannels(1)
if err != nil {
t.Fatalf("unable to create test channels: %v", err)
}
defer cleanUpChans()
// Instantiate a breach arbiter to handle the breach of alice's channel.
brar, cleanUpArb, err := createTestArbiter(t, notifier, alice.State().Db)
alicePoint := alice.ChannelPoint()
spendEvents := contractcourt.ChainEventSubscription{
UnilateralClosure: make(chan *lnwallet.UnilateralCloseSummary, 1),
CooperativeClosure: make(chan struct{}, 1),
ContractBreach: make(chan *lnwallet.BreachRetribution, 1),
ProcessACK: make(chan error, 1),
ChanPoint: *alicePoint,
Cancel: func() {
},
}
brar, cleanUpArb, err := createTestArbiter(
t, &spendEvents, alice.State().Db,
)
if err != nil {
t.Fatalf("unable to initialize test breach arbiter: %v", err)
}
@ -1035,7 +1066,7 @@ func TestBreachHandoffFail(t *testing.T) {
// Generate the force close summary at this point in time, this will
// serve as the old state bob will broadcast.
forceCloseSummary, err := bob.ForceClose()
bobClose, err := bob.ForceClose()
if err != nil {
t.Fatalf("unable to force close bob's channel: %v", err)
}
@ -1062,12 +1093,17 @@ func TestBreachHandoffFail(t *testing.T) {
// Signal the notifier to dispatch spend notifications of the funding
// transaction using the transaction from bob's closing summary.
chanPoint := alice.ChanPoint
breachTxn := forceCloseSummary.CloseTx
notifier.Spend(chanPoint, 100, breachTxn)
// Wait for the close observer to exit, all persistent effects should be
// observable after this point.
alice.WaitForClose()
spendEvents.ContractBreach <- &lnwallet.BreachRetribution{
BreachTransaction: bobClose.CloseTx,
}
select {
case err := <-spendEvents.ProcessACK:
if err == nil {
t.Fatalf("breach write should have failed")
}
case <-time.After(time.Second * 15):
t.Fatalf("breach arbiter didn't send ack back")
}
// Since the handoff failed, the breach arbiter should not show the
// channel as breached, and the channel should also not have been marked
@ -1075,6 +1111,14 @@ func TestBreachHandoffFail(t *testing.T) {
assertNoArbiterBreach(t, brar, chanPoint)
assertNotPendingClosed(t, alice)
brar, cleanUpArb, err = createTestArbiter(
t, &spendEvents, alice.State().Db,
)
if err != nil {
t.Fatalf("unable to initialize test breach arbiter: %v", err)
}
defer cleanUpArb()
// Instantiate a second lightning channel for alice, using the state of
// her last channel.
aliceKeyPriv, _ := btcec.PrivKeyFromBytes(btcec.S256(),
@ -1089,14 +1133,19 @@ func TestBreachHandoffFail(t *testing.T) {
// Signal a spend of the funding transaction and wait for the close
// observer to exit. This time we are allowing the handoff to succeed.
notifier.Spend(chanPoint, 100, breachTxn)
alice2.WaitForClose()
spendEvents.ContractBreach <- &lnwallet.BreachRetribution{
BreachTransaction: bobClose.CloseTx,
}
select {
case <-spendEvents.ProcessACK:
case <-time.After(time.Second * 15):
t.Fatalf("breach arbiter didn't send ack back")
}
// Check that the breach was properly recorded in the breach arbiter,
// and that the close observer marked the channel as pending closed
// before exiting.
assertArbiterBreach(t, brar, chanPoint)
assertPendingClosed(t, alice)
}
// assertArbiterBreach checks that the breach arbiter has persisted the breach
@ -1134,24 +1183,6 @@ func assertNoArbiterBreach(t *testing.T, brar *breachArbiter,
}
}
// assertPendingClosed checks that the channel has been marked pending closed in
// the channel database.
func assertPendingClosed(t *testing.T, c *lnwallet.LightningChannel) {
closedChans, err := c.State().Db.FetchClosedChannels(true)
if err != nil {
t.Fatalf("unable to load pending closed channels: %v", err)
}
for _, chanSummary := range closedChans {
if chanSummary.ChanPoint == *c.ChanPoint {
return
}
}
t.Fatalf("channel %v was not marked pending closed",
c.ChanPoint)
}
// assertNotPendingClosed checks that the channel has not been marked pending
// closed in the channel database.
func assertNotPendingClosed(t *testing.T, c *lnwallet.LightningChannel) {
@ -1170,7 +1201,7 @@ func assertNotPendingClosed(t *testing.T, c *lnwallet.LightningChannel) {
// createTestArbiter instantiates a breach arbiter with a failing retribution
// store, so that controlled failures can be tested.
func createTestArbiter(t *testing.T, notifier chainntnfs.ChainNotifier,
func createTestArbiter(t *testing.T, chainEvents *contractcourt.ChainEventSubscription,
db *channeldb.DB) (*breachArbiter, func(), error) {
// Create a failing retribution store, that wraps a normal one.
@ -1183,13 +1214,17 @@ func createTestArbiter(t *testing.T, notifier chainntnfs.ChainNotifier,
signer := &mockSigner{key: aliceKeyPriv}
// Assemble our test arbiter.
notifier := makeMockSpendNotifier()
ba := newBreachArbiter(&BreachConfig{
CloseLink: func(_ *wire.OutPoint, _ htlcswitch.ChannelCloseType) {},
DB: db,
Estimator: &lnwallet.StaticFeeEstimator{FeeRate: 50},
GenSweepScript: func() ([]byte, error) { return nil, nil },
Notifier: notifier,
CloseLink: func(_ *wire.OutPoint, _ htlcswitch.ChannelCloseType) {},
DB: db,
Estimator: &lnwallet.StaticFeeEstimator{FeeRate: 50},
GenSweepScript: func() ([]byte, error) { return nil, nil },
SubscribeChannelEvents: func(_ wire.OutPoint) (*contractcourt.ChainEventSubscription, error) {
return chainEvents, nil
},
Signer: signer,
Notifier: notifier,
PublishTransaction: func(_ *wire.MsgTx) error { return nil },
Store: store,
})
@ -1206,12 +1241,10 @@ func createTestArbiter(t *testing.T, notifier chainntnfs.ChainNotifier,
return ba, cleanUp, nil
}
// createInitChannelsWithNotifier creates two initialized test channels funded
// with 10 BTC, with 5 BTC allocated to each side. Within the channel, Alice is
// the initiator.
func createInitChannelsWithNotifier(revocationWindow int,
notifier chainntnfs.ChainNotifier) (*lnwallet.LightningChannel,
*lnwallet.LightningChannel, func(), error) {
// createInitChannels creates two initialized test channels funded with 10 BTC,
// with 5 BTC allocated to each side. Within the channel, Alice is the
// initiator.
func createInitChannels(revocationWindow int) (*lnwallet.LightningChannel, *lnwallet.LightningChannel, func(), error) {
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(),
alicesPrivKey)
@ -1376,9 +1409,24 @@ func createInitChannelsWithNotifier(revocationWindow int,
return nil, nil, nil, err
}
addr := &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 18556,
}
if err := channelAlice.State().SyncPending(addr, 101); err != nil {
return nil, nil, nil, err
}
if err := channelAlice.State().FullSync(); err != nil {
return nil, nil, nil, err
}
addr = &net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 18555,
}
if err := channelBob.State().SyncPending(addr, 101); err != nil {
return nil, nil, nil, err
}
if err := channelBob.State().FullSync(); err != nil {
return nil, nil, nil, err
}

View File

@ -90,9 +90,11 @@ func (m *mockNotfier) RegisterConfirmationsNtfn(txid *chainhash.Hash, numConfs,
Confirmed: m.confChannel,
}, nil
}
func (m *mockNotfier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent,
error) {
return nil, nil
func (m *mockNotfier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, error) {
return &chainntnfs.BlockEpochEvent{
Epochs: make(chan *chainntnfs.BlockEpoch),
Cancel: func() {},
}, nil
}
func (m *mockNotfier) Start() error {

View File

@ -251,8 +251,12 @@ func createTestPeer(notifier chainntnfs.ChainNotifier,
breachArbiter := &breachArbiter{}
chainArb := contractcourt.NewChainArbitrator(
contractcourt.ChainArbitratorConfig{}, nil,
contractcourt.ChainArbitratorConfig{
Notifier: notifier,
ChainIO: chainIO,
}, dbAlice,
)
chainArb.WatchNewChannel(aliceChannelState)
s := &server{
chanDB: dbAlice,

View File

@ -1732,7 +1732,7 @@ func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
return kidOutput{
breachedOutput: makeBreachedOutput(
outpoint, witnessType, signDescriptor,
outpoint, witnessType, nil, signDescriptor,
),
isHtlc: isHtlc,
originChanPoint: *originChanPoint,