package lookout import ( "errors" "fmt" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/watchtower/blob" "github.com/lightningnetwork/lnd/watchtower/wtdb" ) var ( // ErrOutputNotFound signals that the breached output could not be found // on the commitment transaction. ErrOutputNotFound = errors.New("unable to find output on commit tx") // ErrUnknownSweepAddrType signals that client provided an output that // was not p2wkh or p2wsh. ErrUnknownSweepAddrType = errors.New("sweep addr is not p2wkh or p2wsh") ) // JusticeDescriptor contains the information required to sweep a breached // channel on behalf of a victim. It supports the ability to create the justice // transaction that sweeps the commitments and recover a cut of the channel for // the watcher's eternal vigilance. type JusticeDescriptor struct { // BreachedCommitTx is the commitment transaction that caused the breach // to be detected. BreachedCommitTx *wire.MsgTx // SessionInfo contains the contract with the watchtower client and // the prenegotiated terms they agreed to. SessionInfo *wtdb.SessionInfo // JusticeKit contains the decrypted blob and information required to // construct the transaction scripts and witnesses. JusticeKit blob.JusticeKit } // breachedInput contains the required information to construct and spend // breached outputs on a commitment transaction. type breachedInput struct { txOut *wire.TxOut outPoint wire.OutPoint witness [][]byte sequence uint32 } // commitToLocalInput extracts the information required to spend the commit // to-local output. func (p *JusticeDescriptor) commitToLocalInput() (*breachedInput, error) { kit := p.JusticeKit // Retrieve the to-local output script and witness from the justice kit. toLocalPkScript, witness, err := kit.ToLocalOutputSpendInfo() if err != nil { return nil, err } // Locate the to-local output on the breaching commitment transaction. toLocalIndex, toLocalTxOut, err := findTxOutByPkScript( p.BreachedCommitTx, toLocalPkScript, ) if err != nil { return nil, err } // Construct the to-local outpoint that will be spent in the justice // transaction. toLocalOutPoint := wire.OutPoint{ Hash: p.BreachedCommitTx.TxHash(), Index: toLocalIndex, } return &breachedInput{ txOut: toLocalTxOut, outPoint: toLocalOutPoint, witness: witness, }, nil } // commitToRemoteInput extracts the information required to spend the commit // to-remote output. func (p *JusticeDescriptor) commitToRemoteInput() (*breachedInput, error) { kit := p.JusticeKit // Retrieve the to-remote output script, witness script and sequence // from the justice kit. toRemotePkScript, witness, seq, err := kit.ToRemoteOutputSpendInfo() if err != nil { return nil, err } // Locate the to-remote output on the breaching commitment transaction. toRemoteIndex, toRemoteTxOut, err := findTxOutByPkScript( p.BreachedCommitTx, toRemotePkScript, ) if err != nil { return nil, err } // Construct the to-remote outpoint which will be spent in the justice // transaction. toRemoteOutPoint := wire.OutPoint{ Hash: p.BreachedCommitTx.TxHash(), Index: toRemoteIndex, } return &breachedInput{ txOut: toRemoteTxOut, outPoint: toRemoteOutPoint, witness: witness, sequence: seq, }, nil } // assembleJusticeTxn accepts the breached inputs recovered from state update // and attempts to construct the justice transaction that sweeps the victims // funds to their wallet and claims the watchtower's reward. func (p *JusticeDescriptor) assembleJusticeTxn(txWeight lntypes.WeightUnit, inputs ...*breachedInput) (*wire.MsgTx, error) { justiceTxn := wire.NewMsgTx(2) // First, construct add the breached inputs to our justice transaction // and compute the total amount that will be swept. var totalAmt btcutil.Amount for _, inp := range inputs { totalAmt += btcutil.Amount(inp.txOut.Value) justiceTxn.AddTxIn(&wire.TxIn{ PreviousOutPoint: inp.outPoint, Sequence: inp.sequence, }) } // Using the session's policy, compute the outputs that should be added // to the justice transaction. In the case of an altruist sweep, there // will be a single output paying back to the victim. Otherwise for a // reward sweep, there will be two outputs, one of which pays back to // the victim while the other gives a cut to the tower. outputs, err := p.SessionInfo.Policy.ComputeJusticeTxOuts( totalAmt, txWeight, p.JusticeKit.SweepAddress(), p.SessionInfo.RewardAddress, ) if err != nil { return nil, err } // Attach the computed txouts to the justice transaction. justiceTxn.TxOut = outputs // Apply a BIP69 sort to the resulting transaction. txsort.InPlaceSort(justiceTxn) btx := btcutil.NewTx(justiceTxn) if err := blockchain.CheckTransactionSanity(btx); err != nil { return nil, err } // Since the transaction inputs could have been reordered as a result of the // BIP69 sort, create an index mapping each prevout to it's new index. inputIndex := make(map[wire.OutPoint]int) for i, txIn := range justiceTxn.TxIn { inputIndex[txIn.PreviousOutPoint] = i } // Attach each of the provided witnesses to the transaction. prevOutFetcher, err := prevOutFetcher(inputs) if err != nil { return nil, fmt.Errorf("error creating previous output "+ "fetcher: %v", err) } hashes := txscript.NewTxSigHashes(justiceTxn, prevOutFetcher) for _, inp := range inputs { // Lookup the input's new post-sort position. i := inputIndex[inp.outPoint] justiceTxn.TxIn[i].Witness = inp.witness // Validate the reconstructed witnesses to ensure they are // valid for the breached inputs. vm, err := txscript.NewEngine( inp.txOut.PkScript, justiceTxn, i, txscript.StandardVerifyFlags, nil, hashes, inp.txOut.Value, prevOutFetcher, ) if err != nil { return nil, err } if err := vm.Execute(); err != nil { log.Debugf("Failed to validate justice transaction: %s", spew.Sdump(justiceTxn)) return nil, fmt.Errorf("error validating TX: %w", err) } } return justiceTxn, nil } // CreateJusticeTxn computes the justice transaction that sweeps a breaching // commitment transaction. The justice transaction is constructed by assembling // the witnesses using data provided by the client in a prior state update. // // NOTE: An older version of ToLocalPenaltyWitnessSize underestimated the size // of the witness by one byte, which could cause the signature(s) to break if // the tower is reconstructing with the newer constant because the output values // might differ. This method retains that original behavior to not invalidate // historical signatures. func (p *JusticeDescriptor) CreateJusticeTxn() (*wire.MsgTx, error) { var ( sweepInputs = make([]*breachedInput, 0, 2) weightEstimate input.TxWeightEstimator ) commitmentType, err := p.SessionInfo.Policy.BlobType.CommitmentType(nil) if err != nil { return nil, err } // Add the sweep address's contribution, depending on whether it is a // p2wkh or p2wsh output. switch len(p.JusticeKit.SweepAddress()) { case input.P2WPKHSize: weightEstimate.AddP2WKHOutput() case input.P2WSHSize: weightEstimate.AddP2WSHOutput() default: return nil, ErrUnknownSweepAddrType } // Add our reward address to the weight estimate if the policy's blob // type specifies a reward output. if p.SessionInfo.Policy.BlobType.Has(blob.FlagReward) { weightEstimate.AddP2WKHOutput() } // Assemble the breached to-local output from the justice descriptor and // add it to our weight estimate. toLocalInput, err := p.commitToLocalInput() if err != nil { return nil, err } // Get the weight for the to-local witness and add that to the // estimator. toLocalWitnessSize, err := commitmentType.ToLocalWitnessSize() if err != nil { return nil, err } weightEstimate.AddWitnessInput(toLocalWitnessSize) sweepInputs = append(sweepInputs, toLocalInput) log.Debugf("Found to local witness output=%#v, stack=%v", toLocalInput.txOut, toLocalInput.witness) // If the justice kit specifies that we have to sweep the to-remote // output, we'll also try to assemble the output and add it to weight // estimate if successful. if p.JusticeKit.HasCommitToRemoteOutput() { toRemoteInput, err := p.commitToRemoteInput() if err != nil { return nil, err } sweepInputs = append(sweepInputs, toRemoteInput) log.Debugf("Found to remote witness output=%#v, stack=%v", toRemoteInput.txOut, toRemoteInput.witness) // Get the weight for the to-remote witness and add that to the // estimator. toRemoteWitnessSize, err := commitmentType.ToRemoteWitnessSize() if err != nil { return nil, err } weightEstimate.AddWitnessInput(toRemoteWitnessSize) } // TODO(conner): sweep htlc outputs txWeight := weightEstimate.Weight() return p.assembleJusticeTxn(txWeight, sweepInputs...) } // findTxOutByPkScript searches the given transaction for an output whose // pkscript matches the query. If one is found, the TxOut is returned along with // the index. // // NOTE: The search stops after the first match is found. func findTxOutByPkScript(txn *wire.MsgTx, pkScript *txscript.PkScript) (uint32, *wire.TxOut, error) { found, index := input.FindScriptOutputIndex(txn, pkScript.Script()) if !found { return 0, nil, ErrOutputNotFound } return index, txn.TxOut[index], nil } // prevOutFetcher returns a txscript.MultiPrevOutFetcher for the given set // of inputs. func prevOutFetcher(inputs []*breachedInput) (*txscript.MultiPrevOutFetcher, error) { fetcher := txscript.NewMultiPrevOutFetcher(nil) for _, inp := range inputs { if inp.txOut == nil { return nil, fmt.Errorf("missing input utxo information") } fetcher.AddPrevOut(inp.outPoint, inp.txOut) } return fetcher, nil }