276c384455
This commit fixes s bug in the breachArbiter which was introduced during the change that added the live channel hand off from an active peer. Rather than closing the settle channel of the _older_ breachObserver, the settle channel of the newer beachObserver was being closed. This would result in a panic once the channel itself was actually settled as the channel would be closed again. To fix this bug, we now properly close the channel of the old settle signal instead of the new one.
512 lines
17 KiB
Go
512 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/roasbeef/btcd/txscript"
|
|
"github.com/roasbeef/btcd/wire"
|
|
"github.com/roasbeef/btcutil"
|
|
)
|
|
|
|
// breachArbiter is a special sub-system which is responsible for watching and
|
|
// acting on the detection of any attempted uncooperative channel breaches by
|
|
// channel counter-parties. This file essentially acts as deterrence code for
|
|
// those attempting to launch attacks against the daemon. In practice it's
|
|
// expected that the logic in this file never gets executed, but it is
|
|
// important to have it in place just in case we encounter cheating channel
|
|
// counter-parties.
|
|
// TODO(roasbeef): closures in config for sub-system pointers to decouple?
|
|
type breachArbiter struct {
|
|
wallet *lnwallet.LightningWallet
|
|
db *channeldb.DB
|
|
notifier chainntnfs.ChainNotifier
|
|
htlcSwitch *htlcSwitch
|
|
|
|
// breachObservers is a map which tracks all the active breach
|
|
// observers we're currently managing. The key of the map is the
|
|
// funding outpoint of the channel, and the value is a channel which
|
|
// will be closed once we detect that the channel has been
|
|
// cooperatively closed, there by killing the goroutine and freeing up
|
|
// resource.
|
|
breachObservers map[wire.OutPoint]chan struct{}
|
|
|
|
// breachedContracts is a channel which is used internally within the
|
|
// struct to send the necessary information required to punish a
|
|
// counter-party once a channel breach is detected. Breach observers
|
|
// use this to communicate with the main contractObserver goroutine.
|
|
breachedContracts chan *retributionInfo
|
|
|
|
// newContracts is a channel which is used by outside sub-systems to
|
|
// notify the breachArbiter of a new contract (a channel) that should
|
|
// be watched.
|
|
newContracts chan *lnwallet.LightningChannel
|
|
|
|
// settledContracts is a channel by outside sub-subsystems to notify
|
|
// the breachArbiter that a channel has peacefully been closed. Once a
|
|
// channel has been closed the arbiter no longer needs to watch for
|
|
// breach closes.
|
|
settledContracts chan *wire.OutPoint
|
|
|
|
started uint32
|
|
stopped uint32
|
|
quit chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// newBreachArbiter creates a new instance of a breachArbiter initialize with
|
|
// its dependant objects.
|
|
func newBreachArbiter(wallet *lnwallet.LightningWallet, db *channeldb.DB,
|
|
notifier chainntnfs.ChainNotifier, h *htlcSwitch) *breachArbiter {
|
|
|
|
return &breachArbiter{
|
|
wallet: wallet,
|
|
db: db,
|
|
notifier: notifier,
|
|
htlcSwitch: h,
|
|
|
|
breachObservers: make(map[wire.OutPoint]chan struct{}),
|
|
breachedContracts: make(chan *retributionInfo),
|
|
newContracts: make(chan *lnwallet.LightningChannel),
|
|
settledContracts: make(chan *wire.OutPoint),
|
|
quit: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start is an idempotent method that officially starts the breachArbiter along
|
|
// with all other goroutines it needs to perform its functions.
|
|
func (b *breachArbiter) Start() error {
|
|
if !atomic.CompareAndSwapUint32(&b.started, 0, 1) {
|
|
return nil
|
|
}
|
|
|
|
brarLog.Tracef("Starting breach aribter")
|
|
|
|
b.wg.Add(1)
|
|
go b.contractObserver()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop is an idempotent method that signals the breachArbiter to execute a
|
|
// graceful shutdown. This function will block until all goroutines spawned by
|
|
// the breachArbiter have gracefully exited.
|
|
func (b *breachArbiter) Stop() error {
|
|
if !atomic.CompareAndSwapUint32(&b.stopped, 0, 1) {
|
|
return nil
|
|
}
|
|
|
|
brarLog.Infof("Breach arbiter shutting down")
|
|
|
|
close(b.quit)
|
|
b.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// contractObserver is the primary goroutine for the breachArbiter. This
|
|
// goroutine is responsible for managing goroutines that watch for breaches for
|
|
// all current active and newly created channels. If a channel breach is
|
|
// detected by a spawned child goroutine, then the contractObserver will
|
|
// execute the retribution logic required to sweep ALL outputs from a contested
|
|
// channel into the daemon's wallet.
|
|
//
|
|
// NOTE: This MUST be run as a goroutine.
|
|
func (b *breachArbiter) contractObserver() {
|
|
defer b.wg.Done()
|
|
|
|
// First we need to query that database state for all currently active
|
|
// channels, each of these channels will need a goroutine assigned to
|
|
// it to watch for channel breaches.
|
|
activeChannels, err := b.db.FetchAllChannels()
|
|
if err != nil {
|
|
// TODO(roasbeef): this is a fatal error...
|
|
brarLog.Errorf("unable to fetch active channels: %v", err)
|
|
}
|
|
|
|
brarLog.Infof("Retrieved %v channels from database, watching with "+
|
|
"vigilance!", len(activeChannels))
|
|
|
|
// For each active channel found within the database, we launch a
|
|
// detected breachObserver goroutine for that channel and also track
|
|
// the new goroutine within the breachObservers map so we can cancel it
|
|
// later if necessary.
|
|
for _, chanState := range activeChannels {
|
|
channel, err := lnwallet.NewLightningChannel(nil, nil,
|
|
b.notifier, chanState)
|
|
if err != nil {
|
|
brarLog.Errorf("unable to load channel: %v", err)
|
|
}
|
|
|
|
settleSignal := make(chan struct{})
|
|
chanPoint := channel.ChannelPoint()
|
|
b.breachObservers[*chanPoint] = settleSignal
|
|
|
|
b.wg.Add(1)
|
|
go b.breachObserver(channel, settleSignal)
|
|
}
|
|
|
|
out:
|
|
for {
|
|
select {
|
|
case breachInfo := <-b.breachedContracts:
|
|
// A new channel contract has just been breached! We
|
|
// first register for a notification to be dispatched
|
|
// once the breach transaction (the revoked commitment
|
|
// transaction) has been confirmed in the chain to
|
|
// ensure we're not dealing with a moving target.
|
|
breachTXID := &breachInfo.commitHash
|
|
confChan, err := b.notifier.RegisterConfirmationsNtfn(breachTXID, 1)
|
|
if err != nil {
|
|
brarLog.Errorf("unable to register for conf for txid: ",
|
|
breachTXID)
|
|
continue
|
|
}
|
|
|
|
brarLog.Warnf("A channel has been breached with tx: %v. "+
|
|
"Waiting for confirmation, then justice will be served!",
|
|
breachTXID)
|
|
|
|
// With the notification registered, we launch a new
|
|
// goroutine which will finalize the channel
|
|
// retribution after the breach transaction has been
|
|
// confirmed.
|
|
b.wg.Add(1)
|
|
go b.exactRetribution(confChan, breachInfo)
|
|
|
|
delete(b.breachObservers, breachInfo.chanPoint)
|
|
case contract := <-b.newContracts:
|
|
// A new channel has just been opened within the
|
|
// daemon, so we launch a new breachObserver to handle
|
|
// the detection of attempted contract breaches.
|
|
settleSignal := make(chan struct{})
|
|
chanPoint := contract.ChannelPoint()
|
|
|
|
// If the contract is already being watched, then an
|
|
// additional send indicates we have a stale version of
|
|
// the contract. So we'll cancel active watcher
|
|
// goroutine to create a new instance with the latest
|
|
// contract reference.
|
|
if oldSignal, ok := b.breachObservers[*chanPoint]; ok {
|
|
brarLog.Infof("ChannelPoint(%v) is now live, "+
|
|
"abandoning state contract for live "+
|
|
"version", chanPoint)
|
|
close(oldSignal)
|
|
}
|
|
|
|
b.breachObservers[*chanPoint] = settleSignal
|
|
|
|
brarLog.Debugf("New contract detected, launching " +
|
|
"breachObserver")
|
|
|
|
b.wg.Add(1)
|
|
go b.breachObserver(contract, settleSignal)
|
|
|
|
// TODO(roasbeef): add doneChan to signal to peer continue
|
|
// * peer send over to us on loadActiveChanenls, sync
|
|
// until we're aware so no state transitions
|
|
case chanPoint := <-b.settledContracts:
|
|
// A new channel has been closed either unilaterally or
|
|
// cooperatively, as a result we no longer need a
|
|
// breachObserver detected to the channel.
|
|
killSignal, ok := b.breachObservers[*chanPoint]
|
|
if !ok {
|
|
brarLog.Errorf("Unable to find contract: %v",
|
|
chanPoint)
|
|
continue
|
|
}
|
|
|
|
brarLog.Debugf("ChannelPoint(%v) has been settled, "+
|
|
"cancelling breachObserver", chanPoint)
|
|
|
|
// If we had a breachObserver active, then we signal it
|
|
// for exit and also delete its state from our tracking
|
|
// map.
|
|
close(killSignal)
|
|
delete(b.breachObservers, *chanPoint)
|
|
case <-b.quit:
|
|
break out
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// exactRetribution is a goroutine which is executed once a contract breach has
|
|
// been detected by a breachObserver. This function is responsible for
|
|
// punishing a counter-party 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,
|
|
breachInfo *retributionInfo) {
|
|
|
|
defer b.wg.Done()
|
|
|
|
// TODO(roasbeef): state needs to be check-pointed here
|
|
|
|
select {
|
|
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
|
|
}
|
|
|
|
// Otherwise, if this is a real confirmation notification, then
|
|
// we fall through to complete out duty.
|
|
case <-b.quit:
|
|
return
|
|
}
|
|
|
|
brarLog.Debugf("Breach transaction %v has been confirmed, sweeping "+
|
|
"revoked funds", breachInfo.commitHash)
|
|
|
|
// With the breach transaction confirmed, we now create the justice tx
|
|
// which will claim ALL the funds within the channel.
|
|
justiceTx, err := b.createJusticeTx(breachInfo)
|
|
if err != nil {
|
|
brarLog.Errorf("unable to create justice tx: %v", err)
|
|
return
|
|
}
|
|
|
|
brarLog.Debugf("Broadcasting justice tx: %v", newLogClosure(func() string {
|
|
return spew.Sdump(justiceTx)
|
|
}))
|
|
|
|
// Finally, broadcast the transaction, finalizing the channels'
|
|
// retribution against the cheating counter-party.
|
|
if err := b.wallet.PublishTransaction(justiceTx); err != nil {
|
|
brarLog.Errorf("unable to broadcast "+
|
|
"justice tx: %v", err)
|
|
return
|
|
}
|
|
|
|
// 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 work low that the
|
|
// deed has been done.
|
|
justiceTXID := justiceTx.TxSha()
|
|
confChan, err = b.notifier.RegisterConfirmationsNtfn(&justiceTXID, 1)
|
|
if err != nil {
|
|
brarLog.Errorf("unable to register for conf for txid: %v",
|
|
justiceTXID)
|
|
return
|
|
}
|
|
|
|
select {
|
|
case _, ok := <-confChan.Confirmed:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// TODO(roasbeef): factor in HTLC's
|
|
revokedFunds := breachInfo.revokedOutput.amt
|
|
totalFunds := revokedFunds + breachInfo.selfOutput.amt
|
|
|
|
brarLog.Infof("Justice for ChannelPoint(%v) has "+
|
|
"been served, %v revoked funds (%v total) "+
|
|
"have been claimed", breachInfo.chanPoint,
|
|
revokedFunds, totalFunds)
|
|
|
|
// TODO(roasbeef): add peer to blacklist?
|
|
|
|
// TODO(roasbeef): close other active channels with offending peer
|
|
|
|
close(breachInfo.doneChan)
|
|
|
|
return
|
|
case <-b.quit:
|
|
return
|
|
}
|
|
}
|
|
|
|
// breachObserver notifies the breachArbiter contract observer goroutine that a
|
|
// channel's contract has been breached by the prior counter party. Once
|
|
// notified the breachArbiter will attempt to sweep ALL funds within the
|
|
// channel using the information provided within the BreachRetribution
|
|
// generated due to the breach of channel contract. The funds will be swept
|
|
// only after the breaching transaction receives a necessary number of
|
|
// confirmations.
|
|
func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
|
settleSignal chan struct{}) {
|
|
|
|
defer b.wg.Done()
|
|
|
|
chanPoint := contract.ChannelPoint()
|
|
|
|
brarLog.Debugf("Breach observer for ChannelPoint(%v) started", chanPoint)
|
|
|
|
select {
|
|
// A read from this channel indicates that the contract has been
|
|
// settled cooperatively so we exit as our duties are no longer needed.
|
|
case <-settleSignal:
|
|
return
|
|
|
|
// A read from this channel indicates that a channel breach has been
|
|
// detected! So we notify the main coordination goroutine with the
|
|
// information needed to bring the counter-party to justice.
|
|
case breachInfo := <-contract.ContractBreach:
|
|
brarLog.Warnf("REVOKED STATE #%v FOR ChannelPoint(%v) "+
|
|
"broadcast, REMOTE PEER IS DOING SOMETHING "+
|
|
"SKETCHY!!!", breachInfo.RevokedStateNum,
|
|
chanPoint)
|
|
|
|
// Immediately notify the HTLC switch that this link has been
|
|
// breached in order to ensure any incoming or outgoing
|
|
// multi-hop HTLC's aren't sent over this link, nor any other
|
|
// links associated with this peer.
|
|
b.htlcSwitch.CloseLink(chanPoint, CloseBreach)
|
|
if err := contract.DeleteState(); err != nil {
|
|
brarLog.Errorf("unable to delete channel state: %v", err)
|
|
}
|
|
|
|
// TODO(roasbeef): need to handle case of remote broadcast
|
|
// mid-local initiated state-transition, possible false-positive?
|
|
|
|
// First we generate the witness generation function which will
|
|
// be used to sweep the output only we can satisfy on the
|
|
// commitment transaction. This output is just a regular p2wkh
|
|
// output.
|
|
localSignDesc := breachInfo.LocalOutputSignDesc
|
|
localWitness := func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
|
inputIndex int) ([][]byte, error) {
|
|
|
|
desc := localSignDesc
|
|
desc.SigHashes = hc
|
|
desc.InputIndex = inputIndex
|
|
|
|
return lnwallet.CommitSpendNoDelay(b.wallet.Signer, desc, tx)
|
|
}
|
|
|
|
// Next we create the witness generation function that will be
|
|
// used to sweep the cheating counter party's output by taking
|
|
// advantage of the revocation clause within the output's
|
|
// witness script.
|
|
remoteSignDesc := breachInfo.RemoteOutputSignDesc
|
|
remoteWitness := func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
|
inputIndex int) ([][]byte, error) {
|
|
|
|
desc := breachInfo.RemoteOutputSignDesc
|
|
desc.SigHashes = hc
|
|
desc.InputIndex = inputIndex
|
|
|
|
return lnwallet.CommitSpendRevoke(b.wallet.Signer, desc, tx)
|
|
}
|
|
|
|
// Finally, with the two witness generation funcs created, we
|
|
// send the retribution information to the utxo nursery.
|
|
// TODO(roasbeef): populate htlc breaches
|
|
b.breachedContracts <- &retributionInfo{
|
|
commitHash: breachInfo.BreachTransaction.TxSha(),
|
|
chanPoint: *chanPoint,
|
|
|
|
selfOutput: &breachedOutput{
|
|
amt: btcutil.Amount(localSignDesc.Output.Value),
|
|
outpoint: breachInfo.LocalOutpoint,
|
|
witnessFunc: localWitness,
|
|
},
|
|
|
|
revokedOutput: &breachedOutput{
|
|
amt: btcutil.Amount(remoteSignDesc.Output.Value),
|
|
outpoint: breachInfo.RemoteOutpoint,
|
|
witnessFunc: remoteWitness,
|
|
},
|
|
|
|
doneChan: make(chan struct{}),
|
|
}
|
|
// TODO(roasbeef): delete chan state on unilateral close also?
|
|
case <-b.quit:
|
|
return
|
|
}
|
|
}
|
|
|
|
// breachedOutput contains all the information needed to sweep a breached
|
|
// output. A breach output is an output that were now entitled to due to a
|
|
// revoked commitment transaction being broadcast.
|
|
type breachedOutput struct {
|
|
amt btcutil.Amount
|
|
outpoint wire.OutPoint
|
|
witnessFunc witnessGenerator
|
|
|
|
twoStageClaim bool
|
|
}
|
|
|
|
// retributionInfo encapsulates all the data needed to sweep all the contested
|
|
// funds within a channel whose contract has been breached by the prior
|
|
// counter-party. This struct is used by the utxoNursery to create the justice
|
|
// transaction which spends all outputs of the commitment transaction into an
|
|
// output controlled by the wallet.
|
|
type retributionInfo struct {
|
|
commitHash wire.ShaHash
|
|
chanPoint wire.OutPoint
|
|
|
|
selfOutput *breachedOutput
|
|
|
|
revokedOutput *breachedOutput
|
|
|
|
htlcOutputs *[]breachedOutput
|
|
|
|
doneChan chan struct{}
|
|
}
|
|
|
|
// createJusticeTx creates a transaction 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 counter-party. 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) {
|
|
// First, we obtain a new public key script from the wallet which we'll
|
|
// sweep the funds to.
|
|
// TODO(roasbeef): possibly create many outputs to minimize change in
|
|
// the future?
|
|
pkScriptOfJustice, err := newSweepPkScript(b.wallet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Before creating the actual TxOut, we'll need to calculate proper fee
|
|
// to attach to the transaction to ensure a timely confirmation.
|
|
// TODO(roasbeef): remove hard-coded fee
|
|
totalAmt := r.selfOutput.amt + r.revokedOutput.amt
|
|
sweepedAmt := int64(totalAmt - 5000)
|
|
|
|
// With the fee calculate, we can now create the justice transaction
|
|
// using the information gathered above.
|
|
justiceTx := wire.NewMsgTx()
|
|
justiceTx.AddTxOut(&wire.TxOut{
|
|
PkScript: pkScriptOfJustice,
|
|
Value: sweepedAmt,
|
|
})
|
|
justiceTx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: r.selfOutput.outpoint,
|
|
})
|
|
justiceTx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: r.revokedOutput.outpoint,
|
|
})
|
|
|
|
hashCache := txscript.NewTxSigHashes(justiceTx)
|
|
|
|
// Finally, using the witness generation functions attached to the
|
|
// retribution information, we'll populate the inputs with fully valid
|
|
// witnesses for both commitment outputs, and all the pending HTLC's at
|
|
// this state in the channel's history.
|
|
// TODO(roasbeef): handle the 2-layer HTLC's
|
|
localWitness, err := r.selfOutput.witnessFunc(justiceTx, hashCache, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
justiceTx.TxIn[0].Witness = localWitness
|
|
|
|
remoteWitness, err := r.revokedOutput.witnessFunc(justiceTx, hashCache, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
justiceTx.TxIn[1].Witness = remoteWitness
|
|
|
|
return justiceTx, nil
|
|
}
|