From a29cd563f6cc77693b21ca65e0a48783cd33d955 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 21 Nov 2016 13:27:50 -0600 Subject: [PATCH] utxonursery: expand duties to channel contract breach retribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit expands the duties of the utxoNursery (which should maybe be renamed), to handle carrying out retribution against a counter-party who breaches the channel contract by broadcasting a prior revoked state on-chain. As part of the retribution, once the breach transaction (the revoked commitment transaction) has been confirmed within a block, the nursery then sweep ALL funds pending within the channel to the daemon’s wallet. This new section of the code has been implemented without full persistence logic similar to time-locked output sweeping workflow of the nursery. In a later commit, this section will gain full persistence logic so the workflows can survive restarts of the daemon. --- utxonursery.go | 284 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 259 insertions(+), 25 deletions(-) diff --git a/utxonursery.go b/utxonursery.go index 317c75e4..1be28760 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -17,9 +17,13 @@ import ( // peer. The nursery accepts outputs and "incubates" them until they've reached // maturity, then sweep the outputs into the source wallet. An output is // considered mature after the relative time-lock within the pkScript has -// passed. As outputs reach their maturity age, they're sweeped in batches into +// passed. As outputs reach their maturity age, they're swept in batches into // the source wallet, returning the outputs so they can be used within future // channels, or regular Bitcoin transactions. +// +// On a part-time basis, the utxoNursery also acts as an adjudicator in the +// scenario that we detect a peer breaching the contract of a channel by +// broadcasting a prior revoked state. type utxoNursery struct { sync.RWMutex @@ -34,6 +38,8 @@ type utxoNursery struct { unstagedOutputs map[wire.OutPoint]*immatureOutput stagedOutputs map[uint32][]*immatureOutput + breachedContracts chan *retributionInfo + started uint32 stopped uint32 quit chan struct{} @@ -46,12 +52,13 @@ func newUtxoNursery(notifier chainntnfs.ChainNotifier, wallet *lnwallet.LightningWallet) *utxoNursery { return &utxoNursery{ - notifier: notifier, - wallet: wallet, - requests: make(chan *incubationRequest), - unstagedOutputs: make(map[wire.OutPoint]*immatureOutput), - stagedOutputs: make(map[uint32][]*immatureOutput), - quit: make(chan struct{}), + notifier: notifier, + wallet: wallet, + requests: make(chan *incubationRequest), + breachedContracts: make(chan *retributionInfo), + unstagedOutputs: make(map[wire.OutPoint]*immatureOutput), + stagedOutputs: make(map[uint32][]*immatureOutput), + quit: make(chan struct{}), } } @@ -69,18 +76,19 @@ func (u *utxoNursery) Start() error { func (u *utxoNursery) Stop() error { close(u.quit) u.wg.Wait() + return nil } // incubator is tasked with watching over all immature outputs until they've -// reached "maturity", after which they'll be sweeped into the underlying -// wallet in batches within a single transaction. Immature outputs can be -// divided into three stages: early stage, mid stage, and final stage. During -// the early stage, the transaction containing the output has not yet been -// confirmed. Once the txn creating the output is confirmed, then output moves -// to the mid stage wherein a dedicated goroutine waits until it has reached -// "maturity". Once an output is mature, it will be sweeped into the wallet at -// the earlier possible height. +// reached "maturity", after which they'll be swept into the underlying wallet +// in batches within a single transaction. Immature outputs can be divided into +// three stages: early stage, mid stage, and final stage. During the early +// stage, the transaction containing the output has not yet been confirmed. +// Once the txn creating the output is confirmed, then output moves to the mid +// stage wherein a dedicated goroutine waits until it has reached "maturity". +// Once an output is mature, it will be swept into the wallet at the earlier +// possible height. func (u *utxoNursery) incubator() { // Register with the notifier to receive notifications for each newly // connected block. @@ -125,6 +133,7 @@ out: if !ok { utxnLog.Errorf("notification chan "+ "closed, can't advance output %v", outpoint) + return } utxnLog.Infof("Outpoint %v confirmed in "+ @@ -191,6 +200,86 @@ out: continue } delete(u.stagedOutputs, newHeight) + case breachInfo := <-u.breachedContracts: + // A new channel contract has just been breached! We + // first register for a notification to be dispatched + // once the breach transaction (the revoked commitment + // transaction) has been confirmed in the chain to + // ensure we're not dealing with a moving target. + breachTXID := &breachInfo.commitHash + confChan, err := u.notifier.RegisterConfirmationsNtfn(breachTXID, 1) + if err != nil { + utxnLog.Errorf("unable to register for conf for txid: ", + breachTXID) + continue + } + + utxnLog.Infof("A channel has been breached with tx: %v. "+ + "Waiting for confirmation, then justice will be served!", + breachTXID) + + // With the notification registered, we launch a new + // goroutine which will finalize the channel + // retribution after the breach transaction has been + // confirmed. + go func() { + // If the second value is !ok, then the channel + // has been closed signifying a daemon + // shutdown, so we exit. + if _, ok := <-confChan.Confirmed; !ok { + // TODO(roasbeef): should check-point + // state above + return + } + + utxnLog.Infof("Breach transaction %v has been "+ + "confirmed, sweeping revoked funds", breachTXID) + + // With the breach transaction confirmed, we + // now create the justice tx which will claim + // ALL the funds within the channel. + justiceTx, err := u.createJusticeTx(breachInfo) + if err != nil { + utxnLog.Errorf("unable to create "+ + "justice tx: %v", err) + return + } + + utxnLog.Infof("Broadcasting justice tx: %v", + newLogClosure(func() string { + return spew.Sdump(justiceTx) + })) + + // Finally, broadcast the transaction, + // finalizing the channels' retribution against + // the cheating counter-party. + err = u.wallet.PublishTransaction(justiceTx) + if err != nil { + utxnLog.Errorf("unable to broadcast "+ + "justice tx: %v", err) + return + } + + // As a conclusionary step, we register for a + // notification to be dispatched once the + // justice tx is confirmed. After confirmation + // we notify the caller that initiated the + // retribution work low that the deed has been + // done. + justiceTXID := justiceTx.TxSha() + confChan, err := u.notifier.RegisterConfirmationsNtfn(&justiceTXID, 1) + if err != nil { + utxnLog.Errorf("unable to register for conf for txid: ", + justiceTXID) + return + } + + if _, ok := <-confChan.Confirmed; !ok { + return + } + + close(breachInfo.doneChan) + }() case <-u.quit: break out } @@ -199,15 +288,24 @@ out: u.wg.Done() } -// createSweepTx creates a final sweeping transaction with all witnesses -// inplace for all inputs. The created transaction has a single output sending -// all the funds back to the source wallet. -func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgTx, error) { +// newSweepPkScript creates a new public key script which should be used to +// sweep any time-locked, or contested channel funds into the wallet. +// Specifically, the script generted is a version 0, pay-to-witness-pubkey-hash +// (p2wkh) output. +func (u *utxoNursery) newSweepPkScript() ([]byte, error) { sweepAddr, err := u.wallet.NewAddress(lnwallet.WitnessPubKey, false) if err != nil { return nil, err } - pkScript, err := txscript.PayToAddrScript(sweepAddr) + + return txscript.PayToAddrScript(sweepAddr) +} + +// createSweepTx creates a final sweeping transaction with all witnesses +// inplace for all inputs. The created transaction has a single output sending +// all the funds back to the source wallet. +func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgTx, error) { + pkScript, err := u.newSweepPkScript() if err != nil { return nil, err } @@ -221,7 +319,7 @@ func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgT sweepTx.Version = 2 sweepTx.AddTxOut(&wire.TxOut{ PkScript: pkScript, - Value: int64(totalSum - 1000), + Value: int64(totalSum - 5000), }) for _, utxo := range matureOutputs { sweepTx.AddTxIn(&wire.TxIn{ @@ -249,9 +347,9 @@ func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgT return sweepTx, nil } -// witnessFunc represents a function which is able to generate the final +// witnessGenerator represents a function which is able to generate the final // witness for a particular public key script. This function acts as an -// abstraction layer, hidiing the details of the underlying script from the +// abstraction layer, hiding the details of the underlying script from the // utxoNursery. type witnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) @@ -279,10 +377,11 @@ type incubationRequest struct { } // incubateOutputs sends a request to utxoNursery to incubate the outputs -// defined within the summary of a closed channel. Induvidually, as all outputs -// reach maturity they'll be sweeped back into the wallet. +// defined within the summary of a closed channel. Individually, as all outputs +// reach maturity they'll be swept back into the wallet. func (u *utxoNursery) incubateOutputs(closeSummary *lnwallet.ForceCloseSummary) { // TODO(roasbeef): should use factory func here based on an interface + // * interface type stored on disk next to record // * spend here also assumes delay is blocked bsaed, and in range witnessFunc := func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) { desc := closeSummary.SelfOutputSignDesc @@ -304,3 +403,138 @@ func (u *utxoNursery) incubateOutputs(closeSummary *lnwallet.ForceCloseSummary) outputs: []*immatureOutput{selfOutput}, } } + +// retributionInfo encapsulates all the data needed to sweep all the contested +// funds within a channel whose contract has been breached by the prior +// counter-party. This struct is used by the utxoNursery to create the justice +// transaction which spends all outputs of the commitment transaction into an +// output controlled by the wallet. +type retributionInfo struct { + commitHash wire.ShaHash + + localAmt btcutil.Amount + localOutpoint wire.OutPoint + localWitnessFunc witnessGenerator + + remoteAmt btcutil.Amount + remoteOutpoint wire.OutPoint + remotWitnessFunc witnessGenerator + + htlcWitnessFuncs []witnessGenerator + + doneChan chan struct{} +} + +// createJusticeTx creates a transaction which exacts "justice" by sweeping ALL +// the funds within the channel which we are now entitled to due to a breach of +// the channel's contract by the counter-party. This function returns a *fully* +// signed transaction with the witness for each input fully in place. +func (u *utxoNursery) 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 := u.newSweepPkScript() + if err != nil { + return nil, err + } + + // Before creating the actual TxOut, we'll need to calculate proper fee + // to attach to the transaction to ensure a timely confirmation. + // TODO(roasbeef): remove hard-coded fee + totalAmt := r.localAmt + r.remoteAmt + sweepedAmt := int64(totalAmt - 5000) + + // With the fee calculate, we can now create the justice transaction + // using the information gathered above. + justiceTx := wire.NewMsgTx() + justiceTx.AddTxOut(&wire.TxOut{ + PkScript: pkScriptOfJustice, + Value: sweepedAmt, + }) + justiceTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: r.localOutpoint, + }) + justiceTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: r.remoteOutpoint, + }) + + hashCache := txscript.NewTxSigHashes(justiceTx) + + // Finally, using the witness generation functions attached to the + // retribution information, we'll populate the inputs with fully valid + // witnesses for both commitment outputs, and all the pending HTLC's at + // this state in the channel's history. + // TODO(roasbeef): handle the 2-layer HTLC's + localWitness, err := r.localWitnessFunc(justiceTx, hashCache, 0) + if err != nil { + return nil, err + } + justiceTx.TxIn[0].Witness = localWitness + + remoteWitness, err := r.remotWitnessFunc(justiceTx, hashCache, 1) + if err != nil { + return nil, err + } + justiceTx.TxIn[1].Witness = remoteWitness + + return justiceTx, nil +} + +// sweepRevokedFunds notifies the utxoNursery that a channel's contract has +// been breached by the prior counter party. Once notified the utxoNursery will +// attempt to sweep ALL funds within the channel using the information provided +// within the BreachRetribution generated due to the breach of channel +// contract. The funds will be swept only after the breaching transaction +// receives a necessary number of confirmations. A channel is immediately +// returned which will be closed once the funds have been successful swept into +// the wallet. +func (u *utxoNursery) sweepRevokedFunds(breachInfo *lnwallet.BreachRetribution) chan struct{} { + // First we generate the witness generation function which will be used + // to sweep the output only we can satisfy on the commitment + // transaction. This output is just a regular p2wkh output. + localSignDesc := breachInfo.LocalOutputSignDesc + localWitness := func(tx *wire.MsgTx, hc *txscript.TxSigHashes, + inputIndex int) ([][]byte, error) { + + desc := localSignDesc + desc.SigHashes = hc + desc.InputIndex = inputIndex + + return lnwallet.CommitSpendNoDelay(u.wallet.Signer, desc, tx) + } + + // Next we create the witness generation function that will be used to + // sweep the cheating counter party's output by taking advantage of the + // revocation clause within the output's witness script. + remoteSignDesc := breachInfo.RemoteOutputSignDesc + remoteWitness := func(tx *wire.MsgTx, hc *txscript.TxSigHashes, + inputIndex int) ([][]byte, error) { + + desc := breachInfo.RemoteOutputSignDesc + desc.SigHashes = hc + desc.InputIndex = inputIndex + + return lnwallet.CommitSpendRevoke(u.wallet.Signer, desc, tx) + } + + // Finally, with the two witness generation funcs created, we send the + // retribution information to the utxo nursery. The created doneChan + // will be closed once the nursery sweeps all outputs inti the wallet. + doneChan := make(chan struct{}) + u.breachedContracts <- &retributionInfo{ + commitHash: breachInfo.BreachTransaction.TxSha(), + + localAmt: btcutil.Amount(localSignDesc.Output.Value), + localOutpoint: breachInfo.LocalOutpoint, + localWitnessFunc: localWitness, + + remoteAmt: btcutil.Amount(remoteSignDesc.Output.Value), + remoteOutpoint: breachInfo.RemoteOutpoint, + remotWitnessFunc: remoteWitness, + + doneChan: doneChan, + } + + return doneChan +}