diff --git a/breacharbiter.go b/breacharbiter.go index 16d4282e9..172a053ef 100644 --- a/breacharbiter.go +++ b/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 } diff --git a/breacharbiter_test.go b/breacharbiter_test.go index 6affbe144..fce8999f6 100644 --- a/breacharbiter_test.go +++ b/breacharbiter_test.go @@ -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 diff --git a/lnd_test.go b/lnd_test.go index 9f1b89348..f14119ee1 100644 --- a/lnd_test.go +++ b/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, }, } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index d5f0ed1b5..0bf4dc536 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -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, } } diff --git a/lnwallet/script_utils.go b/lnwallet/script_utils.go index 873961a42..509a69d1b 100644 --- a/lnwallet/script_utils.go +++ b/lnwallet/script_utils.go @@ -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 diff --git a/lnwallet/signdescriptor.go b/lnwallet/signdescriptor.go index 56eca39dd..cba39ec22 100644 --- a/lnwallet/signdescriptor.go +++ b/lnwallet/signdescriptor.go @@ -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 } diff --git a/lnwallet/size.go b/lnwallet/size.go index 74037e9ae..55c956c3c 100644 --- a/lnwallet/size.go +++ b/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 diff --git a/lnwallet/witnessgen.go b/lnwallet/witnessgen.go index 42e92fc37..434bff4e7 100644 --- a/lnwallet/witnessgen.go +++ b/lnwallet/witnessgen.go @@ -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) } diff --git a/server.go b/server.go index be542ac93..e04afd367 100644 --- a/server.go +++ b/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 diff --git a/utxonursery.go b/utxonursery.go index 8ccd9aca1..12a7999ed 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -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",