Merge pull request #264 from cfromknecht/sweep-2nd-layer-htlcs
Breach Arbiter Sweep 2nd layer HTLCs
This commit is contained in:
commit
94b54f0243
676
breacharbiter.go
676
breacharbiter.go
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@ -15,6 +14,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/roasbeef/btcd/blockchain"
|
||||
"github.com/roasbeef/btcd/btcec"
|
||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||
"github.com/roasbeef/btcd/txscript"
|
||||
@ -30,6 +30,52 @@ import (
|
||||
// continue from the persisted state.
|
||||
var retributionBucket = []byte("retribution")
|
||||
|
||||
// BreachConfig bundles the required subsystems used by the breach arbiter. An
|
||||
// instance of BreachConfig is passed to newBreachArbiter during instantiation.
|
||||
type BreachConfig struct {
|
||||
// ChainIO is used by the breach arbiter to determine the current height
|
||||
// of the blockchain, which is required to subscribe for spend
|
||||
// notifications from Notifier.
|
||||
ChainIO lnwallet.BlockChainIO
|
||||
|
||||
// CloseLink allows the breach arbiter to shutdown any channel links for
|
||||
// which it detects a breach, ensuring now further activity will
|
||||
// continue across the link. The method accepts link's channel point and a
|
||||
// close type to be included in the channel close summary.
|
||||
CloseLink func(*wire.OutPoint, htlcswitch.ChannelCloseType)
|
||||
|
||||
// DB provides access to the user's channels, allowing the breach
|
||||
// arbiter to determine the current state of a user's channels, and how
|
||||
// it should respond to channel closure.
|
||||
DB *channeldb.DB
|
||||
|
||||
// Estimator is used by the breach arbiter to determine an appropriate
|
||||
// fee level when generating, signing, and broadcasting sweep
|
||||
// transactions.
|
||||
Estimator lnwallet.FeeEstimator
|
||||
|
||||
// GenSweepScript generates the receiving scripts for swept outputs.
|
||||
GenSweepScript func() ([]byte, error)
|
||||
|
||||
// Notifier provides a publish/subscribe interface for event driven
|
||||
// notifications regarding the confirmation of txids.
|
||||
Notifier chainntnfs.ChainNotifier
|
||||
|
||||
// PublishTransaction facilitates the process of broadcasting a
|
||||
// transaction to the network.
|
||||
PublishTransaction func(*wire.MsgTx) error
|
||||
|
||||
// Signer is used by the breach arbiter to generate sweep transactions,
|
||||
// which move coins from previously open channels back to the user's
|
||||
// wallet.
|
||||
Signer lnwallet.Signer
|
||||
|
||||
// Store is a persistent resource that maintains information regarding
|
||||
// breached channels. This is used in conjunction with DB to recover
|
||||
// from crashes, restarts, or other failures.
|
||||
Store RetributionStore
|
||||
}
|
||||
|
||||
// breachArbiter is a special subsystem which is responsible for watching and
|
||||
// acting on the detection of any attempted uncooperative channel breaches by
|
||||
// channel counterparties. This file essentially acts as deterrence code for
|
||||
@ -39,14 +85,7 @@ var retributionBucket = []byte("retribution")
|
||||
// counterparties.
|
||||
// TODO(roasbeef): closures in config for subsystem pointers to decouple?
|
||||
type breachArbiter struct {
|
||||
wallet *lnwallet.LightningWallet
|
||||
db *channeldb.DB
|
||||
notifier chainntnfs.ChainNotifier
|
||||
chainIO lnwallet.BlockChainIO
|
||||
estimator lnwallet.FeeEstimator
|
||||
htlcSwitch *htlcswitch.Switch
|
||||
|
||||
retributionStore RetributionStore
|
||||
cfg *BreachConfig
|
||||
|
||||
// breachObservers is a map which tracks all the active breach
|
||||
// observers we're currently managing. The key of the map is the
|
||||
@ -81,19 +120,9 @@ type breachArbiter struct {
|
||||
|
||||
// newBreachArbiter creates a new instance of a breachArbiter initialized with
|
||||
// its dependent objects.
|
||||
func newBreachArbiter(wallet *lnwallet.LightningWallet, db *channeldb.DB,
|
||||
notifier chainntnfs.ChainNotifier, h *htlcswitch.Switch,
|
||||
chain lnwallet.BlockChainIO, fe lnwallet.FeeEstimator) *breachArbiter {
|
||||
|
||||
func newBreachArbiter(cfg *BreachConfig) *breachArbiter {
|
||||
return &breachArbiter{
|
||||
wallet: wallet,
|
||||
db: db,
|
||||
notifier: notifier,
|
||||
chainIO: chain,
|
||||
htlcSwitch: h,
|
||||
estimator: fe,
|
||||
|
||||
retributionStore: newRetributionStore(db),
|
||||
cfg: cfg,
|
||||
|
||||
breachObservers: make(map[wire.OutPoint]chan struct{}),
|
||||
breachedContracts: make(chan *retributionInfo),
|
||||
@ -119,7 +148,7 @@ func (b *breachArbiter) Start() error {
|
||||
// breach is reflected in channeldb.
|
||||
breachRetInfos := make(map[wire.OutPoint]retributionInfo)
|
||||
closeSummaries := make(map[wire.OutPoint]channeldb.ChannelCloseSummary)
|
||||
err := b.retributionStore.ForAll(func(ret *retributionInfo) error {
|
||||
err := b.cfg.Store.ForAll(func(ret *retributionInfo) error {
|
||||
// Extract emitted retribution information.
|
||||
breachRetInfos[ret.chanPoint] = *ret
|
||||
|
||||
@ -129,7 +158,7 @@ func (b *breachArbiter) Start() error {
|
||||
closeSummary := channeldb.ChannelCloseSummary{
|
||||
ChanPoint: ret.chanPoint,
|
||||
ClosingTXID: ret.commitHash,
|
||||
RemotePub: &ret.remoteIdentity,
|
||||
RemotePub: ret.remoteIdentity,
|
||||
Capacity: ret.capacity,
|
||||
SettledBalance: ret.settledBalance,
|
||||
CloseType: channeldb.BreachClose,
|
||||
@ -146,7 +175,7 @@ func (b *breachArbiter) Start() error {
|
||||
// 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()
|
||||
activeChannels, err := b.cfg.DB.FetchAllChannels()
|
||||
if err != nil && err != channeldb.ErrNoActiveChannels {
|
||||
brarLog.Errorf("unable to fetch active channels: %v", err)
|
||||
return err
|
||||
@ -167,11 +196,11 @@ func (b *breachArbiter) Start() error {
|
||||
// channels can be discarded, as their fate will be placed in the hands
|
||||
// of an exactRetribution task spawned later.
|
||||
//
|
||||
// NOTE Spawning of the exactRetribution task is intentionally postponed
|
||||
// until after this step in order to ensure that the all breached
|
||||
// channels are reflected as closed in channeldb and consistent with
|
||||
// what is checkpointed by the breach arbiter. Instead of treating the
|
||||
// breached-and-closed and breached-but-still-active channels as
|
||||
// NOTE: Spawning of the exactRetribution task is intentionally
|
||||
// postponed until after this step in order to ensure that the all
|
||||
// breached channels are reflected as closed in channeldb and consistent
|
||||
// with what is checkpointed by the breach arbiter. Instead of treating
|
||||
// the breached-and-closed and breached-but-still-active channels as
|
||||
// separate sets of channels, we first ensure that all
|
||||
// breached-but-still-active channels are promoted to
|
||||
// breached-and-closed during restart, allowing us to treat them as a
|
||||
@ -183,8 +212,8 @@ func (b *breachArbiter) Start() error {
|
||||
channelsToWatch := make([]*lnwallet.LightningChannel, 0, nActive)
|
||||
for _, chanState := range activeChannels {
|
||||
// Initialize active channel from persisted channel state.
|
||||
channel, err := lnwallet.NewLightningChannel(nil, b.notifier,
|
||||
b.estimator, chanState)
|
||||
channel, err := lnwallet.NewLightningChannel(nil,
|
||||
b.cfg.Notifier, b.cfg.Estimator, chanState)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to load channel from "+
|
||||
"disk: %v", err)
|
||||
@ -203,10 +232,8 @@ func (b *breachArbiter) Start() error {
|
||||
// notify the HTLC switch that this link should be
|
||||
// closed, and that all activity on the link should
|
||||
// cease.
|
||||
b.htlcSwitch.CloseLink(
|
||||
&chanState.FundingOutpoint,
|
||||
htlcswitch.CloseBreach,
|
||||
)
|
||||
b.cfg.CloseLink(&chanState.FundingOutpoint,
|
||||
htlcswitch.CloseBreach)
|
||||
|
||||
// Ensure channeldb is consistent with the persisted
|
||||
// breach.
|
||||
@ -229,18 +256,25 @@ func (b *breachArbiter) Start() error {
|
||||
}
|
||||
|
||||
// TODO(roasbeef): instead use closure height of channel
|
||||
_, currentHeight, err := b.chainIO.GetBestBlock()
|
||||
_, currentHeight, err := b.cfg.ChainIO.GetBestBlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Additionally, we'll also want to watch any pending close or force
|
||||
// close transactions to we can properly mark them as resolved in the
|
||||
// database.
|
||||
if err := b.watchForPendingCloseConfs(currentHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Spawn the exactRetribution tasks to monitor and resolve any breaches
|
||||
// that were loaded from the retribution store.
|
||||
for chanPoint, closeSummary := range closeSummaries {
|
||||
// Register for a notification when the breach transaction is
|
||||
// confirmed on chain.
|
||||
breachTXID := closeSummary.ClosingTXID
|
||||
confChan, err := b.notifier.RegisterConfirmationsNtfn(
|
||||
confChan, err := b.cfg.Notifier.RegisterConfirmationsNtfn(
|
||||
&breachTXID, 1, uint32(currentHeight))
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to register for conf updates "+
|
||||
@ -259,10 +293,13 @@ func (b *breachArbiter) Start() error {
|
||||
b.wg.Add(1)
|
||||
go b.contractObserver(channelsToWatch)
|
||||
|
||||
// Additionally, we'll also want to retrieve any pending close or force
|
||||
// close transactions to we can properly mark them as resolved in the
|
||||
// database.
|
||||
pendingCloseChans, err := b.db.FetchClosedChannels(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// watchForPendingCloseConfs dispatches confirmation notification subscribers
|
||||
// that mark any pending channels as fully closed when signaled.
|
||||
func (b *breachArbiter) watchForPendingCloseConfs(currentHeight int32) error {
|
||||
pendingCloseChans, err := b.cfg.DB.FetchClosedChannels(true)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to fetch closing channels: %v", err)
|
||||
return err
|
||||
@ -281,9 +318,8 @@ func (b *breachArbiter) Start() error {
|
||||
pendingClose.ChanPoint)
|
||||
|
||||
closeTXID := pendingClose.ClosingTXID
|
||||
confNtfn, err := b.notifier.RegisterConfirmationsNtfn(
|
||||
&closeTXID, 1, uint32(currentHeight),
|
||||
)
|
||||
confNtfn, err := b.cfg.Notifier.RegisterConfirmationsNtfn(
|
||||
&closeTXID, 1, uint32(currentHeight))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -309,10 +345,10 @@ func (b *breachArbiter) Start() error {
|
||||
// UnilateralCloseSummary on disk so can
|
||||
// possibly sweep output here
|
||||
|
||||
err := b.db.MarkChanFullyClosed(&chanPoint)
|
||||
err := b.cfg.DB.MarkChanFullyClosed(&chanPoint)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to mark chan "+
|
||||
"as closed: %v", err)
|
||||
brarLog.Errorf("unable to mark channel"+
|
||||
" as closed: %v", err)
|
||||
}
|
||||
|
||||
case <-b.quit:
|
||||
@ -373,10 +409,10 @@ out:
|
||||
for {
|
||||
select {
|
||||
case breachInfo := <-b.breachedContracts:
|
||||
_, currentHeight, err := b.chainIO.GetBestBlock()
|
||||
_, currentHeight, err := b.cfg.ChainIO.GetBestBlock()
|
||||
if err != nil {
|
||||
brarLog.Errorf(
|
||||
"unable to get best height: %v", err)
|
||||
brarLog.Errorf("unable to get best height: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
// A new channel contract has just been breached! We
|
||||
@ -385,9 +421,8 @@ out:
|
||||
// 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, uint32(currentHeight),
|
||||
)
|
||||
cfChan, err := b.cfg.Notifier.RegisterConfirmationsNtfn(
|
||||
breachTXID, 1, uint32(currentHeight))
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to register for conf "+
|
||||
"updates for txid: %v, err: %v",
|
||||
@ -405,7 +440,7 @@ out:
|
||||
// retribution after the breach transaction has been
|
||||
// confirmed.
|
||||
b.wg.Add(1)
|
||||
go b.exactRetribution(confChan, breachInfo)
|
||||
go b.exactRetribution(cfChan, breachInfo)
|
||||
|
||||
delete(b.breachObservers, breachInfo.chanPoint)
|
||||
|
||||
@ -511,7 +546,7 @@ func (b *breachArbiter) exactRetribution(
|
||||
return spew.Sdump(justiceTx)
|
||||
}))
|
||||
|
||||
_, currentHeight, err := b.chainIO.GetBestBlock()
|
||||
_, currentHeight, err := b.cfg.ChainIO.GetBestBlock()
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to get current height: %v", err)
|
||||
return
|
||||
@ -519,7 +554,7 @@ func (b *breachArbiter) exactRetribution(
|
||||
|
||||
// Finally, broadcast the transaction, finalizing the channels'
|
||||
// retribution against the cheating counterparty.
|
||||
if err := b.wallet.PublishTransaction(justiceTx); err != nil {
|
||||
if err := b.cfg.PublishTransaction(justiceTx); err != nil {
|
||||
brarLog.Errorf("unable to broadcast "+
|
||||
"justice tx: %v", err)
|
||||
return
|
||||
@ -530,8 +565,8 @@ func (b *breachArbiter) exactRetribution(
|
||||
// notify the caller that initiated the retribution workflow that the
|
||||
// deed has been done.
|
||||
justiceTXID := justiceTx.TxHash()
|
||||
confChan, err = b.notifier.RegisterConfirmationsNtfn(&justiceTXID, 1,
|
||||
uint32(currentHeight))
|
||||
confChan, err = b.cfg.Notifier.RegisterConfirmationsNtfn(
|
||||
&justiceTXID, 1, uint32(currentHeight))
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to register for conf for txid: %v",
|
||||
justiceTXID)
|
||||
@ -554,14 +589,14 @@ func (b *breachArbiter) exactRetribution(
|
||||
revokedFunds, totalFunds)
|
||||
|
||||
// With the channel closed, mark it in the database as such.
|
||||
err := b.db.MarkChanFullyClosed(&breachInfo.chanPoint)
|
||||
err := b.cfg.DB.MarkChanFullyClosed(&breachInfo.chanPoint)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to mark chan as closed: %v", err)
|
||||
}
|
||||
|
||||
// Justice has been carried out; we can safely delete the
|
||||
// retribution info from the database.
|
||||
err = b.retributionStore.Remove(&breachInfo.chanPoint)
|
||||
err = b.cfg.Store.Remove(&breachInfo.chanPoint)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to remove retribution "+
|
||||
"from the db: %v", err)
|
||||
@ -572,8 +607,6 @@ func (b *breachArbiter) exactRetribution(
|
||||
// TODO(roasbeef): close other active channels with offending
|
||||
// peer
|
||||
|
||||
close(breachInfo.doneChan)
|
||||
|
||||
return
|
||||
case <-b.quit:
|
||||
return
|
||||
@ -621,15 +654,16 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
||||
|
||||
// Next, we'll launch a goroutine to wait until the closing
|
||||
// transaction has been confirmed so we can mark the contract
|
||||
// as resolved in the database. This go routine is _not_
|
||||
// tracked by the breach aribter's wait group since the callback
|
||||
// may not be executed before shutdown, potentially leading to
|
||||
// a deadlock.
|
||||
// as resolved in the database. This go routine is _not_ tracked
|
||||
// by the breach arbiter's wait group since the callback may not
|
||||
// be executed before shutdown, potentially leading to a
|
||||
// deadlocks as the arbiter may not be able to finish shutting
|
||||
// down.
|
||||
//
|
||||
// TODO(roasbeef): also notify utxoNursery, might've had
|
||||
// outbound HTLC's in flight
|
||||
go waitForChanToClose(uint32(closeInfo.SpendingHeight),
|
||||
b.notifier, nil, chanPoint, closeInfo.SpenderTxHash,
|
||||
b.cfg.Notifier, nil, chanPoint, closeInfo.SpenderTxHash,
|
||||
func() {
|
||||
// As we just detected a channel was closed via
|
||||
// a unilateral commitment broadcast by the
|
||||
@ -650,9 +684,11 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
||||
goto close
|
||||
}
|
||||
|
||||
err = b.wallet.PublishTransaction(
|
||||
sweepTx,
|
||||
)
|
||||
brarLog.Infof("Sweeping breached "+
|
||||
"outputs with: %v",
|
||||
spew.Sdump(sweepTx))
|
||||
|
||||
err = b.cfg.PublishTransaction(sweepTx)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to "+
|
||||
"broadcast tx: %v", err)
|
||||
@ -664,7 +700,7 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
||||
"is fully closed, updating DB",
|
||||
chanPoint)
|
||||
|
||||
err := b.db.MarkChanFullyClosed(chanPoint)
|
||||
err := b.cfg.DB.MarkChanFullyClosed(chanPoint)
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to mark chan "+
|
||||
"as closed: %v", err)
|
||||
@ -684,83 +720,46 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
||||
// breached in order to ensure any incoming or outgoing
|
||||
// multi-hop HTLCs aren't sent over this link, nor any other
|
||||
// links associated with this peer.
|
||||
b.htlcSwitch.CloseLink(chanPoint, htlcswitch.CloseBreach)
|
||||
chanInfo := contract.StateSnapshot()
|
||||
b.cfg.CloseLink(chanPoint, htlcswitch.CloseBreach)
|
||||
|
||||
// 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) {
|
||||
// Obtain a snapshot of the final channel state, which can be
|
||||
// used to reclose a breached channel in the event of a failure.
|
||||
chanInfo := contract.StateSnapshot()
|
||||
|
||||
desc := localSignDesc
|
||||
desc.SigHashes = hc
|
||||
desc.InputIndex = inputIndex
|
||||
|
||||
return lnwallet.CommitSpendNoDelay(
|
||||
b.wallet.Cfg.Signer, &desc, tx)
|
||||
}
|
||||
|
||||
// Next we create the witness generation function that will be
|
||||
// used to sweep the cheating counterparty'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.Cfg.Signer, &desc, tx)
|
||||
}
|
||||
|
||||
// Assemble the retribution information that parameterizes the
|
||||
// construction of transactions required to correct the breach.
|
||||
// TODO(roasbeef): populate htlc breaches
|
||||
retInfo := &retributionInfo{
|
||||
commitHash: breachInfo.BreachTransaction.TxHash(),
|
||||
chanPoint: *chanPoint,
|
||||
|
||||
remoteIdentity: chanInfo.RemoteIdentity,
|
||||
capacity: chanInfo.Capacity,
|
||||
settledBalance: chanInfo.LocalBalance.ToSatoshis(),
|
||||
|
||||
selfOutput: &breachedOutput{
|
||||
amt: btcutil.Amount(localSignDesc.Output.Value),
|
||||
outpoint: breachInfo.LocalOutpoint,
|
||||
signDescriptor: localSignDesc,
|
||||
witnessType: lnwallet.CommitmentNoDelay,
|
||||
witnessFunc: localWitness,
|
||||
},
|
||||
|
||||
revokedOutput: &breachedOutput{
|
||||
amt: btcutil.Amount(remoteSignDesc.Output.Value),
|
||||
outpoint: breachInfo.RemoteOutpoint,
|
||||
signDescriptor: remoteSignDesc,
|
||||
witnessType: lnwallet.CommitmentRevoke,
|
||||
witnessFunc: remoteWitness,
|
||||
},
|
||||
|
||||
htlcOutputs: []*breachedOutput{},
|
||||
|
||||
doneChan: make(chan struct{}),
|
||||
}
|
||||
// Using the breach information provided by the wallet and the
|
||||
// channel snapshot, construct the retribution information that
|
||||
// will be persisted to disk.
|
||||
retInfo := newRetributionInfo(chanPoint, breachInfo, chanInfo)
|
||||
|
||||
// Persist the pending retribution state to disk.
|
||||
if err := b.retributionStore.Add(retInfo); err != nil {
|
||||
brarLog.Errorf("unable to persist "+
|
||||
"retribution info to db: %v", err)
|
||||
if err := b.cfg.Store.Add(retInfo); err != nil {
|
||||
brarLog.Errorf("unable to persist retribution info "+
|
||||
"to db: %v", err)
|
||||
}
|
||||
|
||||
// TODO(conner): move responsibility of channel closure into
|
||||
// lnwallet. Have breach arbiter ACK after writing to disk, then
|
||||
// have wallet mark channel as closed. This allows the wallet to
|
||||
// attempt to retransmit the breach info if the either arbiter
|
||||
// or the wallet goes down before completing the hand off.
|
||||
|
||||
// Now that the breach arbiter has persisted the information,
|
||||
// we can go ahead and mark the channel as closed in the
|
||||
// channeldb. This step is done after persisting the
|
||||
// retribution information so that a failure between these steps
|
||||
// will cause an attempt to monitor the still-open channel.
|
||||
// However, since the retribution information was persisted
|
||||
// before, the arbiter will recognize that the channel should be
|
||||
// closed, and proceed to mark it as such after a restart, and
|
||||
// forgo monitoring it for breaches.
|
||||
|
||||
// Construct the breached channel's close summary marking the
|
||||
// channel using the snapshot from before, and marking this as a
|
||||
// BreachClose.
|
||||
closeInfo := &channeldb.ChannelCloseSummary{
|
||||
ChanPoint: *chanPoint,
|
||||
ClosingTXID: breachInfo.BreachTransaction.TxHash(),
|
||||
@ -770,6 +769,10 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
||||
CloseType: channeldb.BreachClose,
|
||||
IsPending: true,
|
||||
}
|
||||
|
||||
// Next, persist the channel close to disk. Upon restart, the
|
||||
// arbiter will recognize that this channel has been breached
|
||||
// and marked close, and fast track its path to justice.
|
||||
if err := contract.DeleteState(closeInfo); err != nil {
|
||||
brarLog.Errorf("unable to delete channel state: %v",
|
||||
err)
|
||||
@ -787,20 +790,90 @@ func (b *breachArbiter) breachObserver(contract *lnwallet.LightningChannel,
|
||||
}
|
||||
}
|
||||
|
||||
// SpendableOutput an interface which can be used by the breach arbiter to
|
||||
// construct a transaction spending from outputs we control.
|
||||
type SpendableOutput interface {
|
||||
// Amount returns the number of satoshis contained within the output.
|
||||
Amount() btcutil.Amount
|
||||
|
||||
// Outpoint returns the reference to the output being spent, used to
|
||||
// construct the corresponding transaction input.
|
||||
OutPoint() *wire.OutPoint
|
||||
|
||||
// BuildWitness returns a valid witness allowing this output to be
|
||||
// spent, the witness should be attached to the transaction at the
|
||||
// location determined by the given `txinIdx`.
|
||||
BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx,
|
||||
hashCache *txscript.TxSigHashes,
|
||||
txinIdx int) ([][]byte, error)
|
||||
}
|
||||
|
||||
// breachedOutput contains all the information needed to sweep a breached
|
||||
// output. A breached output is an output that we are now entitled to due to a
|
||||
// revoked commitment transaction being broadcast.
|
||||
type breachedOutput struct {
|
||||
amt btcutil.Amount
|
||||
outpoint wire.OutPoint
|
||||
amt btcutil.Amount
|
||||
outpoint wire.OutPoint
|
||||
witnessType lnwallet.WitnessType
|
||||
signDesc lnwallet.SignDescriptor
|
||||
|
||||
signDescriptor lnwallet.SignDescriptor
|
||||
witnessType lnwallet.WitnessType
|
||||
witnessFunc lnwallet.WitnessGenerator
|
||||
|
||||
twoStageClaim bool
|
||||
witnessFunc lnwallet.WitnessGenerator
|
||||
}
|
||||
|
||||
// newBreachedOutput assembles a new breachedOutput that can be used by the
|
||||
// breach arbiter to construct a justice or sweep transaction.
|
||||
func newBreachedOutput(outpoint *wire.OutPoint,
|
||||
witnessType lnwallet.WitnessType,
|
||||
signDescriptor *lnwallet.SignDescriptor) *breachedOutput {
|
||||
|
||||
amount := signDescriptor.Output.Value
|
||||
|
||||
return &breachedOutput{
|
||||
amt: btcutil.Amount(amount),
|
||||
outpoint: *outpoint,
|
||||
witnessType: witnessType,
|
||||
signDesc: *signDescriptor,
|
||||
}
|
||||
}
|
||||
|
||||
// Amount returns the number of satoshis contained in the breached output.
|
||||
func (bo *breachedOutput) Amount() btcutil.Amount {
|
||||
return bo.amt
|
||||
}
|
||||
|
||||
// OutPoint returns the breached outputs identifier that is to be included as a
|
||||
// transaction input.
|
||||
func (bo *breachedOutput) OutPoint() *wire.OutPoint {
|
||||
return &bo.outpoint
|
||||
}
|
||||
|
||||
// BuildWitness computes a valid witness that allows us to spend from the
|
||||
// breached output. It does so by first generating and memoizing the witness
|
||||
// generation function, which parameterized primarily by the witness type and
|
||||
// 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) {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Now that we have ensured that the witness generation function has
|
||||
// been initialized, we can proceed to execute it and generate the
|
||||
// witness for this particular breached output.
|
||||
return bo.witnessFunc(txn, hashCache, txinIdx)
|
||||
}
|
||||
|
||||
// Add compile-time constraint ensuring breachedOutput implements
|
||||
// SpendableOutput.
|
||||
var _ SpendableOutput = (*breachedOutput)(nil)
|
||||
|
||||
// retributionInfo encapsulates all the data needed to sweep all the contested
|
||||
// funds within a channel whose contract has been breached by the prior
|
||||
// counterparty. This struct is used to create the justice transaction which
|
||||
@ -810,11 +883,14 @@ type retributionInfo struct {
|
||||
commitHash chainhash.Hash
|
||||
chanPoint wire.OutPoint
|
||||
|
||||
// TODO(conner): remove the following group of fields after decoupling
|
||||
// the breach arbiter from the wallet.
|
||||
|
||||
// Fields copied from channel snapshot when a breach is detected. This
|
||||
// is necessary for deterministically constructing the channel close
|
||||
// summary in the event that the breach arbiter crashes before closing
|
||||
// the channel.
|
||||
remoteIdentity btcec.PublicKey
|
||||
remoteIdentity *btcec.PublicKey
|
||||
capacity btcutil.Amount
|
||||
settledBalance btcutil.Amount
|
||||
|
||||
@ -827,6 +903,70 @@ type retributionInfo struct {
|
||||
doneChan chan struct{}
|
||||
}
|
||||
|
||||
// newRetributionInfo constructs a retributionInfo containing all the
|
||||
// information required by the breach arbiter to recover funds from breached
|
||||
// channels. The information is primarily populated using the BreachRetribution
|
||||
// delivered by the wallet when it detects a channel breach.
|
||||
func newRetributionInfo(chanPoint *wire.OutPoint,
|
||||
breachInfo *lnwallet.BreachRetribution,
|
||||
chanInfo *channeldb.ChannelSnapshot) *retributionInfo {
|
||||
|
||||
// First, record the breach information and witness type for the local
|
||||
// channel point. This will allow us to completely generate a valid
|
||||
// witness in the event of failures, as it will be persisted in the
|
||||
// retribution store. Here we use CommitmentNoDelay since this output
|
||||
// belongs to us and has no time-based constraints on spending.
|
||||
selfOutput := newBreachedOutput(&breachInfo.LocalOutpoint,
|
||||
lnwallet.CommitmentNoDelay, &breachInfo.LocalOutputSignDesc)
|
||||
|
||||
// Second, record the same information and witness type regarding the
|
||||
// remote outpoint, which belongs to the party who tried to steal our
|
||||
// money! Here we set witnessType of the breachedOutput to
|
||||
// CommitmentRevoke, since we will be using a revoke key, withdrawing
|
||||
// the funds from the commitment transaction immediately.
|
||||
revokedOutput := newBreachedOutput(&breachInfo.RemoteOutpoint,
|
||||
lnwallet.CommitmentRevoke, &breachInfo.RemoteOutputSignDesc)
|
||||
|
||||
// Determine the number of second layer HTLCs we will attempt to sweep.
|
||||
nHtlcs := len(breachInfo.HtlcRetributions)
|
||||
|
||||
// Lastly, for each of the breached HTLC outputs, assemble the
|
||||
// information we will persist to disk, such that we will be able to
|
||||
// deterministically generate a valid witness for each output. This will
|
||||
// allow the breach arbiter to recover from failures, in the event that
|
||||
// it must sign and broadcast the justice transaction.
|
||||
htlcOutputs := make([]*breachedOutput, nHtlcs)
|
||||
for i, breachedHtlc := range breachInfo.HtlcRetributions {
|
||||
// Using the breachedHtlc's incoming flag, determine the
|
||||
// appropriate witness type that needs to be generated in order
|
||||
// to sweep the HTLC output.
|
||||
var htlcWitnessType lnwallet.WitnessType
|
||||
if breachedHtlc.IsIncoming {
|
||||
htlcWitnessType = lnwallet.HtlcAcceptedRevoke
|
||||
} else {
|
||||
htlcWitnessType = lnwallet.HtlcOfferedRevoke
|
||||
}
|
||||
|
||||
htlcOutputs[i] = newBreachedOutput(
|
||||
&breachInfo.HtlcRetributions[i].OutPoint, htlcWitnessType,
|
||||
&breachInfo.HtlcRetributions[i].SignDesc)
|
||||
}
|
||||
|
||||
// TODO(conner): remove dependency on channel snapshot after decoupling
|
||||
// channel closure from the breach arbiter.
|
||||
|
||||
return &retributionInfo{
|
||||
commitHash: breachInfo.BreachTransaction.TxHash(),
|
||||
chanPoint: *chanPoint,
|
||||
remoteIdentity: &chanInfo.RemoteIdentity,
|
||||
capacity: chanInfo.Capacity,
|
||||
settledBalance: chanInfo.LocalBalance.ToSatoshis(),
|
||||
selfOutput: selfOutput,
|
||||
revokedOutput: revokedOutput,
|
||||
htlcOutputs: htlcOutputs,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 counterparty. This function returns a *fully*
|
||||
@ -834,66 +974,43 @@ type retributionInfo struct {
|
||||
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
|
||||
// Determine the number of HTLCs to be swept by the justice txn.
|
||||
nHtlcs := len(r.htlcOutputs)
|
||||
|
||||
// Assemble the breached outputs into a slice of spendable outputs,
|
||||
// starting with the self and revoked outputs, then adding any htlc
|
||||
// outputs.
|
||||
breachedOutputs := make([]SpendableOutput, 2+nHtlcs)
|
||||
breachedOutputs[0] = r.selfOutput
|
||||
breachedOutputs[1] = r.revokedOutput
|
||||
for i, htlcOutput := range r.htlcOutputs {
|
||||
breachedOutputs[2+i] = htlcOutput
|
||||
}
|
||||
|
||||
r.selfOutput.witnessFunc = r.selfOutput.witnessType.GenWitnessFunc(
|
||||
&b.wallet.Cfg.Signer, &r.selfOutput.signDescriptor)
|
||||
// Compute the transaction weight of the justice transaction, which
|
||||
// includes 2 + nHtlcs inputs and one output.
|
||||
var txWeight uint64
|
||||
// Begin with a base txn weight, e.g. version, nLockTime, etc.
|
||||
txWeight += 4*lnwallet.BaseSweepTxSize + lnwallet.WitnessHeaderSize
|
||||
// Add to_local revoke script and tx input.
|
||||
txWeight += 4*lnwallet.InputSize + lnwallet.ToLocalPenaltyWitnessSize
|
||||
// Add to_remote p2wpkh witness and tx input.
|
||||
txWeight += 4*lnwallet.InputSize + lnwallet.P2WKHWitnessSize
|
||||
|
||||
r.revokedOutput.witnessFunc = r.revokedOutput.witnessType.GenWitnessFunc(
|
||||
&b.wallet.Cfg.Signer, &r.revokedOutput.signDescriptor)
|
||||
|
||||
for i := range r.htlcOutputs {
|
||||
r.htlcOutputs[i].witnessFunc = r.htlcOutputs[i].witnessType.GenWitnessFunc(
|
||||
&b.wallet.Cfg.Signer, &r.htlcOutputs[i].signDescriptor)
|
||||
// Compute the appropriate weight contributed by each revoked accepted
|
||||
// or offered HTLC witnesses and tx inputs.
|
||||
for _, htlcOutput := range r.htlcOutputs {
|
||||
switch htlcOutput.witnessType {
|
||||
case lnwallet.HtlcOfferedRevoke:
|
||||
txWeight += 4*lnwallet.InputSize +
|
||||
lnwallet.OfferedHtlcPenaltyWitnessSize
|
||||
case lnwallet.HtlcAcceptedRevoke:
|
||||
txWeight += 4*lnwallet.InputSize +
|
||||
lnwallet.AcceptedHtlcPenaltyWitnessSize
|
||||
}
|
||||
}
|
||||
|
||||
// Before creating the actual TxOut, we'll need to calculate the 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 calculated, we can now create the justice transaction
|
||||
// using the information gathered above.
|
||||
justiceTx := wire.NewMsgTx(2)
|
||||
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 HTLCs at
|
||||
// this state in the channel's history.
|
||||
// TODO(roasbeef): handle the 2-layer HTLCs
|
||||
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
|
||||
return b.sweepSpendableOutputsTxn(txWeight, breachedOutputs...)
|
||||
}
|
||||
|
||||
// craftCommitmentSweepTx creates a transaction to sweep the non-delayed output
|
||||
@ -907,61 +1024,108 @@ func (b *breachArbiter) createJusticeTx(
|
||||
func (b *breachArbiter) craftCommitSweepTx(
|
||||
closeInfo *lnwallet.UnilateralCloseSummary) (*wire.MsgTx, error) {
|
||||
|
||||
// First, we'll fetch a fresh script that we can use to sweep the funds
|
||||
// under the control of the wallet.
|
||||
sweepPkScript, err := newSweepPkScript(b.wallet)
|
||||
selfOutput := newBreachedOutput(
|
||||
closeInfo.SelfOutPoint,
|
||||
lnwallet.CommitmentNoDelay,
|
||||
closeInfo.SelfOutputSignDesc,
|
||||
)
|
||||
|
||||
// Compute the transaction weight of the commit sweep transaction, which
|
||||
// includes a single input and output.
|
||||
var txWeight uint64
|
||||
// Begin with a base txn weight, e.g. version, nLockTime, etc.
|
||||
txWeight += 4*lnwallet.BaseSweepTxSize + lnwallet.WitnessHeaderSize
|
||||
// Add to_local p2wpkh witness and tx input.
|
||||
txWeight += 4*lnwallet.InputSize + lnwallet.P2WKHWitnessSize
|
||||
|
||||
return b.sweepSpendableOutputsTxn(txWeight, selfOutput)
|
||||
}
|
||||
|
||||
// sweepSpendableOutputsTxn creates a signed transaction from a sequence of
|
||||
// spendable outputs by sweeping the funds into a single p2wkh output.
|
||||
func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight uint64,
|
||||
inputs ...SpendableOutput) (*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?
|
||||
pkScript, err := b.cfg.GenSweepScript()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(roasbeef): use proper fees
|
||||
outputAmt := closeInfo.SelfOutputSignDesc.Output.Value
|
||||
sweepAmt := int64(outputAmt - 5000)
|
||||
|
||||
if sweepAmt <= 0 {
|
||||
// TODO(roasbeef): add output to special pool, can be swept
|
||||
// when: funding a channel, sweeping time locked outputs, or
|
||||
// delivering
|
||||
// justice after a channel breach
|
||||
return nil, fmt.Errorf("output to small to sweep in isolation")
|
||||
// Compute the total amount contained in the inputs.
|
||||
var totalAmt btcutil.Amount
|
||||
for _, input := range inputs {
|
||||
totalAmt += input.Amount()
|
||||
}
|
||||
|
||||
// With the amount we're sweeping computed, we can now creating the
|
||||
// sweep transaction itself.
|
||||
sweepTx := wire.NewMsgTx(1)
|
||||
sweepTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *closeInfo.SelfOutPoint,
|
||||
})
|
||||
sweepTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: sweepPkScript,
|
||||
Value: int64(sweepAmt),
|
||||
feePerWeight := b.cfg.Estimator.EstimateFeePerWeight(1)
|
||||
txFee := btcutil.Amount(txWeight * feePerWeight)
|
||||
|
||||
sweepAmt := int64(totalAmt - txFee)
|
||||
|
||||
// With the fee calculated, we can now create the transaction using the
|
||||
// information gathered above and the provided retribution information.
|
||||
txn := wire.NewMsgTx(2)
|
||||
|
||||
// We begin by adding the output to which our funds will be deposited.
|
||||
txn.AddTxOut(&wire.TxOut{
|
||||
PkScript: pkScript,
|
||||
Value: sweepAmt,
|
||||
})
|
||||
|
||||
// Next, we'll generate the signature required to satisfy the p2wkh
|
||||
// witness program.
|
||||
signDesc := closeInfo.SelfOutputSignDesc
|
||||
signDesc.SigHashes = txscript.NewTxSigHashes(sweepTx)
|
||||
signDesc.InputIndex = 0
|
||||
sweepSig, err := b.wallet.Cfg.Signer.SignOutputRaw(sweepTx, signDesc)
|
||||
if err != nil {
|
||||
// Next, we add all of the spendable outputs as inputs to the
|
||||
// transaction.
|
||||
for _, input := range inputs {
|
||||
txn.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *input.OutPoint(),
|
||||
})
|
||||
}
|
||||
|
||||
// Before signing the transaction, check to ensure that it meets some
|
||||
// basic validity requirements.
|
||||
btx := btcutil.NewTx(txn)
|
||||
if err := blockchain.CheckTransactionSanity(btx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally, we'll manually craft the witness. The witness here is the
|
||||
// exact same as a regular p2wkh witness, but we'll need to ensure that
|
||||
// we use the tweaked public key as the last item in the witness stack
|
||||
// which was originally used to created the pkScript we're spending.
|
||||
witness := make([][]byte, 2)
|
||||
witness[0] = append(sweepSig, byte(txscript.SigHashAll))
|
||||
witness[1] = lnwallet.TweakPubKeyWithTweak(
|
||||
signDesc.PubKey, signDesc.SingleTweak,
|
||||
).SerializeCompressed()
|
||||
// Create a sighash cache to improve the performance of hashing and
|
||||
// signing SigHashAll inputs.
|
||||
hashCache := txscript.NewTxSigHashes(txn)
|
||||
|
||||
sweepTx.TxIn[0].Witness = witness
|
||||
// Create a closure that encapsulates the process of initializing a
|
||||
// particular output's witness generation function, computing the
|
||||
// witness, and attaching it to the transaction. This function accepts
|
||||
// an integer index representing the intended txin index, and the
|
||||
// breached output from which it will spend.
|
||||
addWitness := func(idx int, so SpendableOutput) error {
|
||||
// First, we construct a valid witness for this outpoint and
|
||||
// transaction using the SpendableOutput's witness generation
|
||||
// function.
|
||||
witness, err := so.BuildWitness(b.cfg.Signer, txn, hashCache,
|
||||
idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
brarLog.Infof("Sweeping commitment output with: %v", spew.Sdump(sweepTx))
|
||||
// Then, we add the witness to the transaction at the
|
||||
// appropriate txin index.
|
||||
txn.TxIn[idx].Witness = witness
|
||||
|
||||
return sweepTx, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finally, generate a witness for each output and attach it to the
|
||||
// transaction.
|
||||
for i, input := range inputs {
|
||||
if err := addWitness(i, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return txn, nil
|
||||
}
|
||||
|
||||
// RetributionStore provides an interface for managing a persistent map from
|
||||
@ -1154,7 +1318,7 @@ func (ret *retributionInfo) Decode(r io.Reader) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret.remoteIdentity = *remoteIdentity
|
||||
ret.remoteIdentity = remoteIdentity
|
||||
|
||||
if _, err := io.ReadFull(r, scratch[:8]); err != nil {
|
||||
return err
|
||||
@ -1184,7 +1348,7 @@ func (ret *retributionInfo) Decode(r io.Reader) error {
|
||||
numHtlcOutputs := int(numHtlcOutputsU64)
|
||||
|
||||
ret.htlcOutputs = make([]*breachedOutput, numHtlcOutputs)
|
||||
for i := 0; i < numHtlcOutputs; i++ {
|
||||
for i := range ret.htlcOutputs {
|
||||
ret.htlcOutputs[i] = &breachedOutput{}
|
||||
if err := ret.htlcOutputs[i].Decode(r); err != nil {
|
||||
return err
|
||||
@ -1207,8 +1371,7 @@ func (bo *breachedOutput) Encode(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := lnwallet.WriteSignDescriptor(
|
||||
w, &bo.signDescriptor); err != nil {
|
||||
if err := lnwallet.WriteSignDescriptor(w, &bo.signDesc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1217,15 +1380,6 @@ func (bo *breachedOutput) Encode(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if bo.twoStageClaim {
|
||||
scratch[0] = 1
|
||||
} else {
|
||||
scratch[0] = 0
|
||||
}
|
||||
if _, err := w.Write(scratch[:1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1242,8 +1396,7 @@ func (bo *breachedOutput) Decode(r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := lnwallet.ReadSignDescriptor(
|
||||
r, &bo.signDescriptor); err != nil {
|
||||
if err := lnwallet.ReadSignDescriptor(r, &bo.signDesc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1253,14 +1406,5 @@ func (bo *breachedOutput) Decode(r io.Reader) error {
|
||||
bo.witnessType = lnwallet.WitnessType(
|
||||
binary.BigEndian.Uint16(scratch[:2]))
|
||||
|
||||
if _, err := io.ReadFull(r, scratch[:1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if scratch[0] == 1 {
|
||||
bo.twoStageClaim = true
|
||||
} else {
|
||||
bo.twoStageClaim = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -173,24 +173,21 @@ var (
|
||||
|
||||
breachedOutputs = []breachedOutput{
|
||||
{
|
||||
amt: btcutil.Amount(1e7),
|
||||
outpoint: breachOutPoints[0],
|
||||
witnessType: lnwallet.CommitmentNoDelay,
|
||||
twoStageClaim: true,
|
||||
amt: btcutil.Amount(1e7),
|
||||
outpoint: breachOutPoints[0],
|
||||
witnessType: lnwallet.CommitmentNoDelay,
|
||||
},
|
||||
|
||||
{
|
||||
amt: btcutil.Amount(2e9),
|
||||
outpoint: breachOutPoints[1],
|
||||
witnessType: lnwallet.CommitmentRevoke,
|
||||
twoStageClaim: false,
|
||||
amt: btcutil.Amount(2e9),
|
||||
outpoint: breachOutPoints[1],
|
||||
witnessType: lnwallet.CommitmentRevoke,
|
||||
},
|
||||
|
||||
{
|
||||
amt: btcutil.Amount(3e4),
|
||||
outpoint: breachOutPoints[2],
|
||||
witnessType: lnwallet.CommitmentDelayOutput,
|
||||
twoStageClaim: false,
|
||||
amt: btcutil.Amount(3e4),
|
||||
outpoint: breachOutPoints[2],
|
||||
witnessType: lnwallet.CommitmentDelayOutput,
|
||||
},
|
||||
}
|
||||
|
||||
@ -240,7 +237,7 @@ func init() {
|
||||
// channel point.
|
||||
for i := range retributions {
|
||||
retInfo := &retributions[i]
|
||||
retInfo.remoteIdentity = *breachedOutputs[i].signDescriptor.PubKey
|
||||
retInfo.remoteIdentity = breachedOutputs[i].signDesc.PubKey
|
||||
retributionMap[retInfo.chanPoint] = *retInfo
|
||||
}
|
||||
}
|
||||
@ -320,7 +317,7 @@ func initBreachedOutputs() error {
|
||||
breachKeys[i])
|
||||
}
|
||||
sd.PubKey = pubkey
|
||||
bo.signDescriptor = *sd
|
||||
bo.signDesc = *sd
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -395,7 +392,6 @@ func copyRetInfo(retInfo *retributionInfo) *retributionInfo {
|
||||
selfOutput: retInfo.selfOutput,
|
||||
revokedOutput: retInfo.revokedOutput,
|
||||
htlcOutputs: make([]*breachedOutput, nHtlcs),
|
||||
doneChan: retInfo.doneChan,
|
||||
}
|
||||
|
||||
for i, htlco := range retInfo.htlcOutputs {
|
||||
@ -776,8 +772,8 @@ restartCheck:
|
||||
foundSet[ret.chanPoint] = struct{}{}
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("unkwown retribution "+
|
||||
"retrieved from db: %v", ret)
|
||||
return fmt.Errorf("unkwown retribution retrieved "+
|
||||
"from db: %v", ret)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
227
lnd_test.go
227
lnd_test.go
@ -2010,10 +2010,10 @@ func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) {
|
||||
}
|
||||
}
|
||||
|
||||
// testRevokedCloseRetributinPostBreachConf tests that Alice is able carry out
|
||||
// retribution in the event that she fails immediately after receiving a
|
||||
// confirmation of Carol's breach txn.
|
||||
func testRevokedCloseRetributionPostBreachConf(
|
||||
// testRevokedCloseRetributionRemoteHodl tests that Alice properly responds to a
|
||||
// channel breach made by the remote party, specifically in the case that the
|
||||
// remote party breaches before settling extended HTLCs.
|
||||
func testRevokedCloseRetributionRemoteHodl(
|
||||
net *networkHarness,
|
||||
t *harnessTest) {
|
||||
|
||||
@ -2021,33 +2021,34 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
const (
|
||||
timeout = time.Duration(time.Second * 10)
|
||||
chanAmt = maxFundingAmount
|
||||
pushAmt = 20000
|
||||
paymentAmt = 10000
|
||||
numInvoices = 6
|
||||
)
|
||||
|
||||
// Since we'd like to test some multi-hop failure scenarios, we'll
|
||||
// introduce another node into our test network: Carol.
|
||||
carol, err := net.NewNode(nil)
|
||||
// Since this test will result in the counterparty being left in a weird
|
||||
// state, we will introduce another node into our test network: Carol.
|
||||
carol, err := net.NewNode([]string{"--debughtlc", "--hodlhtlc"})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create new nodes: %v", err)
|
||||
}
|
||||
|
||||
// We must let Dave have an open channel before he can send a node
|
||||
// announcement, so we open a channel with Carol,
|
||||
// We must let Alice communicate with Carol before they are able to
|
||||
// open channel, so we connect Alice and Carol,
|
||||
if err := net.ConnectNodes(ctxb, net.Alice, carol); err != nil {
|
||||
t.Fatalf("unable to connect alice to carol: %v", err)
|
||||
}
|
||||
|
||||
// In order to test Alice's response to an uncooperative channel
|
||||
// closure by Carol, we'll first open up a channel between them with a
|
||||
// 0.5 BTC value.
|
||||
// maxFundingAmount (2^24) satoshis value.
|
||||
ctxt, _ := context.WithTimeout(ctxb, timeout)
|
||||
chanPoint := openChannelAndAssert(ctxt, t, net, net.Alice, carol,
|
||||
chanAmt, 0)
|
||||
chanAmt, pushAmt)
|
||||
|
||||
// With the channel open, we'll create a few invoices for Caro that
|
||||
// With the channel open, we'll create a few invoices for Carol that
|
||||
// Alice will pay to in order to advance the state of the channel.
|
||||
bobPaymentHashes := make([][]byte, numInvoices)
|
||||
carolPaymentHashes := make([][]byte, numInvoices)
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
preimage := bytes.Repeat([]byte{byte(192 - i)}, 32)
|
||||
invoice := &lnrpc.Invoice{
|
||||
@ -2060,30 +2061,57 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
t.Fatalf("unable to add invoice: %v", err)
|
||||
}
|
||||
|
||||
bobPaymentHashes[i] = resp.RHash
|
||||
carolPaymentHashes[i] = resp.RHash
|
||||
}
|
||||
|
||||
// As we'll be querying the state of bob's channels frequently we'll
|
||||
// As we'll be querying the state of Carol's channels frequently we'll
|
||||
// create a closure helper function for the purpose.
|
||||
getCarolChanInfo := func() (*lnrpc.ActiveChannel, error) {
|
||||
req := &lnrpc.ListChannelsRequest{}
|
||||
bobChannelInfo, err := carol.ListChannels(ctxb, req)
|
||||
carolChannelInfo, err := carol.ListChannels(ctxb, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(bobChannelInfo.Channels) != 1 {
|
||||
t.Fatalf("bob should only have a single channel, instead he has %v",
|
||||
len(bobChannelInfo.Channels))
|
||||
if len(carolChannelInfo.Channels) != 1 {
|
||||
t.Fatalf("carol should only have a single channel, instead he has %v",
|
||||
len(carolChannelInfo.Channels))
|
||||
}
|
||||
|
||||
return bobChannelInfo.Channels[0], nil
|
||||
return carolChannelInfo.Channels[0], nil
|
||||
}
|
||||
// We'll introduce a closure to validate that Carol's current balance
|
||||
// matches the given expected amount.
|
||||
checkCarolBalance := func(expectedAmt int64) {
|
||||
carolChan, err := getCarolChanInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get carol's channel info: %v", err)
|
||||
}
|
||||
if carolChan.LocalBalance != expectedAmt {
|
||||
t.Fatalf("carol's balance is incorrect, "+
|
||||
"got %v, expected %v", carolChan.LocalBalance,
|
||||
expectedAmt)
|
||||
}
|
||||
}
|
||||
// We'll introduce another closure to validate that Carol's current
|
||||
// number of updates is at least as large as the provided minimum
|
||||
// number.
|
||||
checkCarolNumUpdatesAtleast := func(minimum uint64) {
|
||||
carolChan, err := getCarolChanInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get carol's channel info: %v", err)
|
||||
}
|
||||
if carolChan.NumUpdates < minimum {
|
||||
t.Fatalf("carol's numupdates is incorrect, want %v "+
|
||||
"to be atleast %v", carolChan.NumUpdates,
|
||||
minimum)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for Alice to receive the channel edge from the funding manager.
|
||||
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||
if err != nil {
|
||||
t.Fatalf("alice didn't see the alice->bob channel before "+
|
||||
t.Fatalf("alice didn't see the alice->carol channel before "+
|
||||
"timeout: %v", err)
|
||||
}
|
||||
|
||||
@ -2094,16 +2122,26 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create payment stream for alice: %v", err)
|
||||
}
|
||||
sendPayments := func(start, stop int) error {
|
||||
sendPayments := func(start, stop int, isHodl bool) error {
|
||||
for i := start; i < stop; i++ {
|
||||
sendReq := &lnrpc.SendRequest{
|
||||
PaymentHash: bobPaymentHashes[i],
|
||||
PaymentHash: carolPaymentHashes[i],
|
||||
Dest: carol.PubKey[:],
|
||||
Amt: paymentAmt,
|
||||
}
|
||||
if err := alicePayStream.Send(sendReq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the remote peer is in hodl mode, we should not
|
||||
// attempt to receive a message, otherwise the test will
|
||||
// block.
|
||||
if isHodl {
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, the peer is not in hodl mode, and we will
|
||||
// expect a response.
|
||||
if resp, err := alicePayStream.Recv(); err != nil {
|
||||
t.Fatalf("payment stream has been closed: %v", err)
|
||||
} else if resp.PaymentError != "" {
|
||||
@ -2114,80 +2152,94 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that carol's balance starts with the amount we pushed to her.
|
||||
checkCarolBalance(pushAmt)
|
||||
|
||||
// Send payments from Alice to Carol using 3 of Carol's payment hashes
|
||||
// generated above.
|
||||
if err := sendPayments(0, numInvoices/2); err != nil {
|
||||
if err := sendPayments(0, numInvoices/2, true); err != nil {
|
||||
t.Fatalf("unable to send payment: %v", err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
|
||||
// Next query for Carol's channel state, as we sent 3 payments of 10k
|
||||
// satoshis each, Carol should now see his balance as being 30k satoshis.
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
bobChan, err := getCarolChanInfo()
|
||||
// satoshis each, however Carol should now see her balance as being
|
||||
// equal to the push amount in satoshis since she has not settled.
|
||||
carolChan, err := getCarolChanInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get bob's channel info: %v", err)
|
||||
t.Fatalf("unable to get carol's channel info: %v", err)
|
||||
}
|
||||
if bobChan.LocalBalance != 30000 {
|
||||
t.Fatalf("bob's balance is incorrect, got %v, expected %v",
|
||||
bobChan.LocalBalance, 30000)
|
||||
}
|
||||
|
||||
// Grab Carol's current commitment height (update number), we'll later
|
||||
// revert him to this state after additional updates to force him to
|
||||
// revert her to this state after additional updates to force her to
|
||||
// broadcast this soon to be revoked state.
|
||||
bobStateNumPreCopy := bobChan.NumUpdates
|
||||
carolStateNumPreCopy := carolChan.NumUpdates
|
||||
|
||||
// Ensure that carol's balance still reflects the original amount we
|
||||
// pushed to her.
|
||||
checkCarolBalance(pushAmt)
|
||||
// Since Carol has not settled, she should only see at least one update
|
||||
// to her channel.
|
||||
checkCarolNumUpdatesAtleast(1)
|
||||
|
||||
// Create a temporary file to house Carol's database state at this
|
||||
// particular point in history.
|
||||
bobTempDbPath, err := ioutil.TempDir("", "bob-past-state")
|
||||
carolTempDbPath, err := ioutil.TempDir("", "carol-past-state")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create temp db folder: %v", err)
|
||||
}
|
||||
bobTempDbFile := filepath.Join(bobTempDbPath, "channel.db")
|
||||
defer os.Remove(bobTempDbPath)
|
||||
carolTempDbFile := filepath.Join(carolTempDbPath, "channel.db")
|
||||
defer os.Remove(carolTempDbPath)
|
||||
|
||||
// With the temporary file created, copy Carol's current state into the
|
||||
// temporary file we created above. Later after more updates, we'll
|
||||
// restore this state.
|
||||
bobDbPath := filepath.Join(carol.cfg.DataDir, "simnet/bitcoin/channel.db")
|
||||
if err := copyFile(bobTempDbFile, bobDbPath); err != nil {
|
||||
carolDbPath := filepath.Join(carol.cfg.DataDir, "simnet/bitcoin/channel.db")
|
||||
if err := copyFile(carolTempDbFile, carolDbPath); err != nil {
|
||||
t.Fatalf("unable to copy database files: %v", err)
|
||||
}
|
||||
|
||||
// Finally, send payments from Alice to Carol, consuming Carol's remaining
|
||||
// payment hashes.
|
||||
if err := sendPayments(numInvoices/2, numInvoices); err != nil {
|
||||
if err := sendPayments(numInvoices/2, numInvoices, true); err != nil {
|
||||
t.Fatalf("unable to send payment: %v", err)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
bobChan, err = getCarolChanInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get bob chan info: %v", err)
|
||||
}
|
||||
// Ensure that carol's balance still shows the amount we originally
|
||||
// pushed to her, and that at least one more update has occurred.
|
||||
checkCarolBalance(pushAmt)
|
||||
checkCarolNumUpdatesAtleast(carolStateNumPreCopy + 1)
|
||||
|
||||
// Now we shutdown Carol, copying over the his temporary database state
|
||||
// which has the *prior* channel state over his current most up to date
|
||||
// Now we shutdown Carol, copying over the her temporary database state
|
||||
// which has the *prior* channel state over her current most up to date
|
||||
// state. With this, we essentially force Carol to travel back in time
|
||||
// within the channel's history.
|
||||
if err = net.RestartNode(carol, func() error {
|
||||
return os.Rename(bobTempDbFile, bobDbPath)
|
||||
return os.Rename(carolTempDbFile, carolDbPath)
|
||||
}); err != nil {
|
||||
t.Fatalf("unable to restart node: %v", err)
|
||||
}
|
||||
|
||||
// Now query for Carol's channel state, it should show that he's at a
|
||||
// state number in the past, not the *latest* state.
|
||||
bobChan, err = getCarolChanInfo()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Ensure that Carol's view of the channel is consistent with the
|
||||
// state of the channel just before it was snapshotted.
|
||||
checkCarolBalance(pushAmt)
|
||||
checkCarolNumUpdatesAtleast(1)
|
||||
|
||||
// Now query for Carol's channel state, it should show that she's at a
|
||||
// state number in the past, *not* the latest state.
|
||||
carolChan, err = getCarolChanInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get bob chan info: %v", err)
|
||||
t.Fatalf("unable to get carol chan info: %v", err)
|
||||
}
|
||||
if bobChan.NumUpdates != bobStateNumPreCopy {
|
||||
t.Fatalf("db copy failed: %v", bobChan.NumUpdates)
|
||||
if carolChan.NumUpdates != carolStateNumPreCopy {
|
||||
t.Fatalf("db copy failed: %v", carolChan.NumUpdates)
|
||||
}
|
||||
|
||||
// Now force Carol to execute a *force* channel closure by unilaterally
|
||||
// broadcasting his current channel state. This is actually the
|
||||
// commitment transaction of a prior *revoked* state, so he'll soon
|
||||
// broadcasting her current channel state. This is actually the
|
||||
// commitment transaction of a prior *revoked* state, so she'll soon
|
||||
// feel the wrath of Alice's retribution.
|
||||
force := true
|
||||
closeUpdates, _, err := net.CloseChannel(ctxb, carol, chanPoint, force)
|
||||
@ -2195,18 +2247,29 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
t.Fatalf("unable to close channel: %v", err)
|
||||
}
|
||||
|
||||
// Finally, generate a single block, wait for the final close status
|
||||
// update, then ensure that the closing transaction was included in the
|
||||
// block.
|
||||
// Query the mempool for Alice's justice transaction, this should be
|
||||
// broadcast as Bob's contract breaching transaction gets confirmed
|
||||
// above.
|
||||
_, err = waitForTxInMempool(net.Miner.Node, 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find Alice's justice tx in mempool: %v", err)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Generate a single block to mine the breach transaction.
|
||||
block := mineBlocks(t, net, 1)[0]
|
||||
|
||||
// Here, Alice receives a confirmation of Carol's breach transaction. We
|
||||
// restart Alice to ensure that she is persisting her retribution state and
|
||||
// continues exacting justice after her node restarts.
|
||||
// Wait so Alice receives a confirmation of Carol's breach transaction.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// We restart Alice to ensure that she is persisting her retribution
|
||||
// state and continues exacting justice after her node restarts.
|
||||
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||
t.Fatalf("unable to stop Alice's node: %v", err)
|
||||
}
|
||||
|
||||
// Finally, Wait for the final close status update, then ensure that the
|
||||
// closing transaction was included in the block.
|
||||
breachTXID, err := net.WaitForChannelClose(ctxb, closeUpdates)
|
||||
if err != nil {
|
||||
t.Fatalf("error while waiting for channel close: %v", err)
|
||||
@ -2222,20 +2285,6 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Query for the mempool transaction found above. Then assert that all
|
||||
// the inputs of this transaction are spending outputs generated by
|
||||
// Carol's breach transaction above.
|
||||
justiceTx, err := net.Miner.Node.GetRawTransaction(justiceTXID)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query for justice tx: %v", err)
|
||||
}
|
||||
for _, txIn := range justiceTx.MsgTx().TxIn {
|
||||
if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) {
|
||||
t.Fatalf("justice tx not spending commitment utxo "+
|
||||
"instead is: %v", txIn.PreviousOutPoint)
|
||||
}
|
||||
}
|
||||
|
||||
// We restart Alice here to ensure that she persists her retribution state
|
||||
// and successfully continues exacting retribution after restarting. At
|
||||
// this point, Alice has broadcast the justice transaction, but it hasn't
|
||||
@ -2245,6 +2294,28 @@ func testRevokedCloseRetributionPostBreachConf(
|
||||
t.Fatalf("unable to restart Alice's node: %v", err)
|
||||
}
|
||||
|
||||
// Query for the mempool transaction found above. Then assert that (1)
|
||||
// the justice tx has the appropriate number of inputs, and (2) all
|
||||
// the inputs of this transaction are spending outputs generated by
|
||||
// Carol's breach transaction above.
|
||||
justiceTx, err := net.Miner.Node.GetRawTransaction(justiceTXID)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query for justice tx: %v", err)
|
||||
}
|
||||
exNumInputs := 2 + numInvoices/2
|
||||
if len(justiceTx.MsgTx().TxIn) != exNumInputs {
|
||||
t.Fatalf("justice tx should have exactly 2 commitment inputs"+
|
||||
"and %v htlc inputs, expected %v in total, got %v",
|
||||
numInvoices/2, exNumInputs,
|
||||
len(justiceTx.MsgTx().TxIn))
|
||||
}
|
||||
for _, txIn := range justiceTx.MsgTx().TxIn {
|
||||
if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) {
|
||||
t.Fatalf("justice tx not spending commitment utxo "+
|
||||
"instead is: %v", txIn.PreviousOutPoint)
|
||||
}
|
||||
}
|
||||
|
||||
// Now mine a block, this transaction should include Alice's justice
|
||||
// transaction which was just accepted into the mempool.
|
||||
block = mineBlocks(t, net, 1)[0]
|
||||
@ -3459,8 +3530,8 @@ var testsCases = []*testCase{
|
||||
test: testRevokedCloseRetribution,
|
||||
},
|
||||
{
|
||||
name: "revoked uncooperative close retribution post breach conf",
|
||||
test: testRevokedCloseRetributionPostBreachConf,
|
||||
name: "revoked uncooperative close retribution remote hodl",
|
||||
test: testRevokedCloseRetributionRemoteHodl,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1032,6 +1032,12 @@ type HtlcRetribution struct {
|
||||
// OutPoint is the target outpoint of this HTLC pointing to the
|
||||
// breached commitment transaction.
|
||||
OutPoint wire.OutPoint
|
||||
|
||||
// IsIncoming is a boolean flag that indicates whether or not this
|
||||
// HTLC was accepted from the counterparty. A false value indicates that
|
||||
// this HTLC was offered by us. This flag is used determine the exact
|
||||
// witness type should be used to sweep the output.
|
||||
IsIncoming bool
|
||||
}
|
||||
|
||||
// BreachRetribution contains all the data necessary to bring a channel
|
||||
@ -1162,7 +1168,7 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
|
||||
// With the commitment outputs located, we'll now generate all the
|
||||
// retribution structs for each of the HTLC transactions active on the
|
||||
// remote commitment transaction.
|
||||
htlcRetributions := make([]HtlcRetribution, len(chanState.Htlcs))
|
||||
htlcRetributions := make([]HtlcRetribution, len(revokedSnapshot.Htlcs))
|
||||
for i, htlc := range revokedSnapshot.Htlcs {
|
||||
var (
|
||||
htlcScript []byte
|
||||
@ -1206,6 +1212,7 @@ func newBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
|
||||
Hash: commitHash,
|
||||
Index: uint32(htlc.OutputIndex),
|
||||
},
|
||||
IsIncoming: htlc.Incoming,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,6 +297,24 @@ func senderHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor,
|
||||
return witnessStack, nil
|
||||
}
|
||||
|
||||
// SenderHtlcSpendRevoke constructs a valid witness allowing the receiver of an
|
||||
// HTLC to claim the output with knowledge of the revocation private key in the
|
||||
// scenario that the sender of the HTLC broadcasts a previously 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 SenderHtlcSpendRevoke(signer Signer, signDesc *SignDescriptor,
|
||||
sweepTx *wire.MsgTx) (wire.TxWitness, error) {
|
||||
|
||||
// Derive the revocation key using the local revocation base point and
|
||||
// commitment point.
|
||||
revokeKey := DeriveRevocationPubkey(signDesc.PubKey,
|
||||
signDesc.DoubleTweak.PubKey())
|
||||
|
||||
return senderHtlcSpendRevoke(signer, signDesc, revokeKey, sweepTx)
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -528,6 +546,24 @@ func receiverHtlcSpendRevoke(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) {
|
||||
|
||||
// Derive the revocation key using the local revocation base point and
|
||||
// commitment point.
|
||||
revokeKey := DeriveRevocationPubkey(signDesc.PubKey,
|
||||
signDesc.DoubleTweak.PubKey())
|
||||
|
||||
return receiverHtlcSpendRevoke(signer, signDesc, revokeKey, sweepTx)
|
||||
}
|
||||
|
||||
// receiverHtlcSpendTimeout constructs a valid witness allowing the sender of
|
||||
// an HTLC to recover the pending funds after an absolute timeout in the
|
||||
// scenario that the receiver of the HTLC broadcasts their version of the
|
||||
|
@ -166,7 +166,7 @@ func ReadSignDescriptor(r io.Reader, sd *SignDescriptor) error {
|
||||
return ErrTweakOverdose
|
||||
}
|
||||
|
||||
witnessScript, err := wire.ReadVarBytes(r, 0, 100, "witnessScript")
|
||||
witnessScript, err := wire.ReadVarBytes(r, 0, 500, "witnessScript")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
171
lnwallet/size.go
171
lnwallet/size.go
@ -18,12 +18,31 @@ const (
|
||||
// - WitnessScriptSHA256: 32 bytes
|
||||
P2WSHSize = 1 + 1 + 32
|
||||
|
||||
// P2WKHOutputSize 31 bytes
|
||||
// - value: 8 bytes
|
||||
// - var_int: 1 byte (pkscript_length)
|
||||
// - pkscript (p2wpkh): 22 bytes
|
||||
P2WKHOutputSize = 8 + 1 + 22
|
||||
|
||||
// P2WSHOutputSize 43 bytes
|
||||
// - value: 8 bytes
|
||||
// - var_int: 1 byte (pkscript_length)
|
||||
// - pkscript (p2wsh): 34 bytes
|
||||
P2WSHOutputSize = 8 + 1 + 34
|
||||
|
||||
// P2WPKHSize 22 bytes
|
||||
// - OP_0: 1 byte
|
||||
// - OP_DATA: 1 byte (PublicKeyHASH160 length)
|
||||
// - PublicKeyHASH160: 20 bytes
|
||||
P2WPKHSize = 1 + 1 + 20
|
||||
|
||||
// P2WKHWitnessSize 108 bytes
|
||||
// - OP_DATA: 1 byte (signature length)
|
||||
// - signature
|
||||
// - OP_DATA: 1 byte (pubkey length)
|
||||
// - pubkey
|
||||
P2WKHWitnessSize = 1 + 73 + 1 + 33
|
||||
|
||||
// MultiSigSize 71 bytes
|
||||
// - OP_2: 1 byte
|
||||
// - OP_DATA: 1 byte (pubKeyAlice length)
|
||||
@ -45,7 +64,7 @@ const (
|
||||
// - WitnessScript (MultiSig)
|
||||
WitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + MultiSigSize
|
||||
|
||||
// FundingInputSize 41 bytes
|
||||
// InputSize 41 bytes
|
||||
// - PreviousOutPoint:
|
||||
// - Hash: 32 bytes
|
||||
// - Index: 4 bytes
|
||||
@ -57,7 +76,12 @@ const (
|
||||
// we separate the calculation of ordinary data
|
||||
// from witness data.
|
||||
// - Sequence: 4 bytes
|
||||
FundingInputSize = 32 + 4 + 1 + 4
|
||||
InputSize = 32 + 4 + 1 + 4
|
||||
|
||||
// FundingInputSize represents the size of an input to a funding
|
||||
// transaction, and is equivalent to the size of a standard segwit input
|
||||
// as calculated above.
|
||||
FundingInputSize = InputSize
|
||||
|
||||
// CommitmentDelayOutput 43 bytes
|
||||
// - Value: 8 bytes
|
||||
@ -82,7 +106,19 @@ const (
|
||||
// - Marker: 1 byte
|
||||
WitnessHeaderSize = 1 + 1
|
||||
|
||||
// BaseCommitmentTxSize 125 43 * num-htlc-outputs bytes
|
||||
// BaseSweepTxSize 42 + 41 * num-swept-inputs bytes
|
||||
// - Version: 4 bytes
|
||||
// - WitnessHeader <---- part of the witness data
|
||||
// - CountTxIn: 2 byte
|
||||
// - TxIn: 41 * num-swept-inputs bytes
|
||||
// ....SweptInputs....
|
||||
// - CountTxOut: 1 byte
|
||||
// - TxOut: 31 bytes
|
||||
// P2WPKHOutput: 31 bytes
|
||||
// - LockTime: 4 bytes
|
||||
BaseSweepTxSize = 4 + 2 + 1 + P2WKHOutputSize + 4
|
||||
|
||||
// BaseCommitmentTxSize 125 + 43 * num-htlc-outputs bytes
|
||||
// - Version: 4 bytes
|
||||
// - WitnessHeader <---- part of the witness data
|
||||
// - CountTxIn: 1 byte
|
||||
@ -119,7 +155,134 @@ const (
|
||||
// of a contract breach, the punishment transaction is able to sweep
|
||||
// all the HTLC's yet still remain below the widely used standard
|
||||
// weight limits.
|
||||
MaxHTLCNumber = 967
|
||||
MaxHTLCNumber = 966
|
||||
|
||||
// ToLocalPenaltyScriptSize 83 bytes
|
||||
// - OP_IF: 1 byte
|
||||
// - OP_DATA: 1 byte (revocationkey length)
|
||||
// - revocationkey: 33 bytes
|
||||
// - OP_CHECKSIG: 1 byte
|
||||
// - OP_ELSE: 1 byte
|
||||
// - OP_DATA: 1 byte (localkey length)
|
||||
// - localkey: 33 bytes
|
||||
// - OP_CHECKSIG_VERIFY: 1 byte
|
||||
// - OP_DATA: 1 byte (delay length)
|
||||
// - delay: 8 bytes
|
||||
// -OP_CHECKSEQUENCEVERIFY: 1 byte
|
||||
// - OP_ENDIF: 1 byte
|
||||
ToLocalPenaltyScriptSize = 1 + 1 + 33 + 1 + 1 + 1 + 33 + 1 + 1 + 8 + 1 + 1
|
||||
|
||||
// ToLocalPenaltyWitnessSize 160 bytes
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - revocation_sig_length: 1 byte
|
||||
// - revocation_sig: 73 bytes
|
||||
// - one_length: 1 byte
|
||||
// - witness_script_length: 1 byte
|
||||
// - witness_script (to_local_script)
|
||||
ToLocalPenaltyWitnessSize = 1 + 1 + 73 + 1 + 1 + ToLocalPenaltyScriptSize
|
||||
|
||||
// AcceptedHtlcPenaltyScriptSize 139 bytes
|
||||
// - OP_DUP: 1 byte
|
||||
// - OP_HASH160: 1 byte
|
||||
// - OP_DATA: 1 byte (RIPEMD160(SHA256(revocationkey)) length)
|
||||
// - RIPEMD160(SHA256(revocationkey)): 20 bytes
|
||||
// - OP_EQUAL: 1 byte
|
||||
// - OP_IF: 1 byte
|
||||
// - OP_CHECKSIG: 1 byte
|
||||
// - OP_ELSE: 1 byte
|
||||
// - OP_DATA: 1 byte (remotekey length)
|
||||
// - remotekey: 33 bytes
|
||||
// - OP_SWAP: 1 byte
|
||||
// - OP_SIZE: 1 byte
|
||||
// - 32: 1 byte
|
||||
// - OP_EQUAL: 1 byte
|
||||
// - OP_IF: 1 byte
|
||||
// - OP_HASH160: 1 byte
|
||||
// - OP_DATA: 1 byte (RIPEMD160(payment_hash) length)
|
||||
// - RIPEMD160(payment_hash): 20 bytes
|
||||
// - OP_EQUALVERIFY: 1 byte
|
||||
// - 2: 1 byte
|
||||
// - OP_SWAP: 1 byte
|
||||
// - OP_DATA: 1 byte (localkey length)
|
||||
// - localkey: 33 bytes
|
||||
// - 2: 1 byte
|
||||
// - OP_CHECKMULTISIG: 1 byte
|
||||
// - OP_ELSE: 1 byte
|
||||
// - OP_DROP: 1 byte
|
||||
// - OP_DATA: 1 byte (cltv_expiry length)
|
||||
// - cltv_expiry: 4 bytes
|
||||
// - OP_CHECKLOCKTIMEVERIFY: 1 byte
|
||||
// - OP_DROP: 1 byte
|
||||
// - OP_CHECKSIG: 1 byte
|
||||
// - OP_ENDIF: 1 byte
|
||||
// - OP_ENDIF: 1 byte
|
||||
AcceptedHtlcPenaltyScriptSize = 3*1 + 20 + 5*1 + 33 + 7*1 + 20 + 4*1 +
|
||||
33 + 5*1 + 4 + 5*1
|
||||
|
||||
// AcceptedHtlcPenaltyWitnessSize 249 bytes
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - revocation_sig_length: 1 byte
|
||||
// - revocation_sig: 73 bytes
|
||||
// - revocation_key_length: 1 byte
|
||||
// - revocation_key: 33 bytes
|
||||
// - witness_script_length: 1 byte
|
||||
// - witness_script (accepted_htlc_script)
|
||||
AcceptedHtlcPenaltyWitnessSize = 1 + 1 + 73 + 1 + 33 + 1 +
|
||||
AcceptedHtlcPenaltyScriptSize
|
||||
|
||||
// OfferedHtlcScriptSize 133 bytes
|
||||
// - OP_DUP: 1 byte
|
||||
// - OP_HASH160: 1 byte
|
||||
// - OP_DATA: 1 byte (RIPEMD160(SHA256(revocationkey)) length)
|
||||
// - RIPEMD160(SHA256(revocationkey)): 20 bytes
|
||||
// - OP_EQUAL: 1 byte
|
||||
// - OP_IF: 1 byte
|
||||
// - OP_CHECKSIG: 1 byte
|
||||
// - OP_ELSE: 1 byte
|
||||
// - OP_DATA: 1 byte (remotekey length)
|
||||
// - remotekey: 33 bytes
|
||||
// - OP_SWAP: 1 byte
|
||||
// - OP_SIZE: 1 byte
|
||||
// - OP_DATA: 1 byte (32 length)
|
||||
// - 32: 1 byte
|
||||
// - OP_EQUAL: 1 byte
|
||||
// - OP_NOTIF: 1 byte
|
||||
// - OP_DROP: 1 byte
|
||||
// - 2: 1 byte
|
||||
// - OP_SWAP: 1 byte
|
||||
// - OP_DATA: 1 byte (localkey length)
|
||||
// - localkey: 33 bytes
|
||||
// - 2: 1 byte
|
||||
// - OP_CHECKMULTISIG: 1 byte
|
||||
// - OP_ELSE: 1 byte
|
||||
// - OP_HASH160: 1 byte
|
||||
// - OP_DATA: 1 byte (RIPEMD160(payment_hash) length)
|
||||
// - RIPEMD160(payment_hash): 20 bytes
|
||||
// - OP_EQUALVERIFY: 1 byte
|
||||
// - OP_CHECKSIG: 1 byte
|
||||
// - OP_ENDIF: 1 byte
|
||||
// - OP_ENDIF: 1 byte
|
||||
OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 4*1
|
||||
|
||||
// OfferedHtlcWitnessSize 243 bytes
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - revocation_sig_length: 1 byte
|
||||
// - revocation_sig: 73 bytes
|
||||
// - revocation_key_length: 1 byte
|
||||
// - revocation_key: 33 bytes
|
||||
// - witness_script_length: 1 byte
|
||||
// - witness_script (offered_htlc_script)
|
||||
OfferedHtlcWitnessSize = 1 + 1 + 73 + 1 + 33 + 1 + OfferedHtlcScriptSize
|
||||
|
||||
// OfferedHtlcPenaltyWitnessSize 243 bytes
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - revocation_sig_length: 1 byte
|
||||
// - revocation_sig: 73 bytes
|
||||
// - revocation_key_length: 1 byte
|
||||
// - revocation_key: 33 bytes
|
||||
// - witness_script_length: 1 byte
|
||||
// - witness_script (offered_htlc_script)
|
||||
OfferedHtlcPenaltyWitnessSize = 1 + 1 + 73 + 1 + 1 + OfferedHtlcScriptSize
|
||||
)
|
||||
|
||||
// estimateCommitTxWeight estimate commitment transaction weight depending on
|
||||
|
@ -25,6 +25,14 @@ const (
|
||||
// of a malicious counterparty's who broadcasts a revoked commitment
|
||||
// transaction.
|
||||
CommitmentRevoke WitnessType = 2
|
||||
|
||||
// HtlcOfferedRevoke is a witness that allows us to sweep an HTLC
|
||||
// output that we offered to the counterparty.
|
||||
HtlcOfferedRevoke WitnessType = 3
|
||||
|
||||
// HtlcAcceptedRevoke is a witness that allows us to sweep an HTLC
|
||||
// output that we accepted from the counterparty.
|
||||
HtlcAcceptedRevoke WitnessType = 4
|
||||
)
|
||||
|
||||
// WitnessGenerator represents a function which is able to generate the final
|
||||
@ -35,7 +43,7 @@ type WitnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
||||
|
||||
// GenWitnessFunc will return a WitnessGenerator function that an output
|
||||
// uses to generate the witness for a sweep transaction.
|
||||
func (wt WitnessType) GenWitnessFunc(signer *Signer,
|
||||
func (wt WitnessType) GenWitnessFunc(signer Signer,
|
||||
descriptor *SignDescriptor) WitnessGenerator {
|
||||
|
||||
return func(tx *wire.MsgTx, hc *txscript.TxSigHashes,
|
||||
@ -47,11 +55,15 @@ func (wt WitnessType) GenWitnessFunc(signer *Signer,
|
||||
|
||||
switch wt {
|
||||
case CommitmentTimeLock:
|
||||
return CommitSpendTimeout(*signer, desc, tx)
|
||||
return CommitSpendTimeout(signer, desc, tx)
|
||||
case CommitmentNoDelay:
|
||||
return CommitSpendNoDelay(*signer, desc, tx)
|
||||
return CommitSpendNoDelay(signer, desc, tx)
|
||||
case CommitmentRevoke:
|
||||
return CommitSpendRevoke(*signer, desc, tx)
|
||||
return CommitSpendRevoke(signer, desc, tx)
|
||||
case HtlcOfferedRevoke:
|
||||
return ReceiverHtlcSpendRevoke(signer, desc, tx)
|
||||
case HtlcAcceptedRevoke:
|
||||
return SenderHtlcSpendRevoke(signer, desc, tx)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown witness type: %v", wt)
|
||||
}
|
||||
|
24
server.go
24
server.go
@ -23,6 +23,7 @@ import (
|
||||
"github.com/roasbeef/btcd/btcec"
|
||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||
"github.com/roasbeef/btcd/connmgr"
|
||||
"github.com/roasbeef/btcd/wire"
|
||||
"github.com/roasbeef/btcutil"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@ -288,8 +289,27 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.breachArbiter = newBreachArbiter(cc.wallet, chanDB, cc.chainNotifier,
|
||||
s.htlcSwitch, s.cc.chainIO, s.cc.feeEstimator)
|
||||
// Construct a closure that wraps the htlcswitch's CloseLink method.
|
||||
closeLink := func(chanPoint *wire.OutPoint,
|
||||
closureType htlcswitch.ChannelCloseType) {
|
||||
// TODO(conner): Properly respect the update and error channels
|
||||
// returned by CloseLink.
|
||||
s.htlcSwitch.CloseLink(chanPoint, closureType)
|
||||
}
|
||||
|
||||
s.breachArbiter = newBreachArbiter(&BreachConfig{
|
||||
Signer: cc.wallet.Cfg.Signer,
|
||||
DB: chanDB,
|
||||
PublishTransaction: cc.wallet.PublishTransaction,
|
||||
Notifier: cc.chainNotifier,
|
||||
ChainIO: s.cc.chainIO,
|
||||
Estimator: s.cc.feeEstimator,
|
||||
CloseLink: closeLink,
|
||||
Store: newRetributionStore(chanDB),
|
||||
GenSweepScript: func() ([]byte, error) {
|
||||
return newSweepPkScript(cc.wallet)
|
||||
},
|
||||
})
|
||||
|
||||
// Create the connection manager which will be responsible for
|
||||
// maintaining persistent outbound connections and also accepting new
|
||||
|
@ -786,8 +786,7 @@ func fetchGraduatingOutputs(db *channeldb.DB, wallet *lnwallet.LightningWallet,
|
||||
// output or not.
|
||||
for _, kgtnOutput := range kgtnOutputs {
|
||||
kgtnOutput.witnessFunc = kgtnOutput.witnessType.GenWitnessFunc(
|
||||
&wallet.Cfg.Signer, kgtnOutput.signDescriptor,
|
||||
)
|
||||
wallet.Cfg.Signer, kgtnOutput.signDescriptor)
|
||||
}
|
||||
|
||||
utxnLog.Infof("New block: height=%v, sweeping %v mature outputs",
|
||||
|
Loading…
Reference in New Issue
Block a user