package sweep import ( "errors" "fmt" "sort" "strings" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) var ( // DefaultMaxInputsPerTx specifies the default maximum number of inputs // allowed in a single sweep tx. If more need to be swept, multiple txes // are created and published. DefaultMaxInputsPerTx = uint32(100) // ErrLocktimeConflict is returned when inputs with different // transaction nLockTime values are included in the same transaction. // // NOTE: due the SINGLE|ANYONECANPAY sighash flag, which is used in the // second level success/timeout txns, only the txns sharing the same // nLockTime can exist in the same tx. ErrLocktimeConflict = errors.New("incompatible locktime") ) // createSweepTx builds a signed tx spending the inputs to the given outputs, // sending any leftover change to the change script. func createSweepTx(inputs []input.Input, outputs []*wire.TxOut, changePkScript []byte, currentBlockHeight uint32, feeRate, maxFeeRate chainfee.SatPerKWeight, signer input.Signer) (*wire.MsgTx, btcutil.Amount, error) { inputs, estimator, err := getWeightEstimate( inputs, outputs, feeRate, maxFeeRate, [][]byte{changePkScript}, ) if err != nil { return nil, 0, err } txFee := estimator.feeWithParent() var ( // Create the sweep transaction that we will be building. We // use version 2 as it is required for CSV. sweepTx = wire.NewMsgTx(2) // Track whether any of the inputs require a certain locktime. locktime = int32(-1) // We keep track of total input amount, and required output // amount to use for calculating the change amount below. totalInput btcutil.Amount requiredOutput btcutil.Amount // We'll add the inputs as we go so we know the final ordering // of inputs to sign. idxs []input.Input ) // We start by adding all inputs that commit to an output. We do this // since the input and output index must stay the same for the // signatures to be valid. for _, o := range inputs { if o.RequiredTxOut() == nil { continue } idxs = append(idxs, o) sweepTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: o.OutPoint(), Sequence: o.BlocksToMaturity(), }) sweepTx.AddTxOut(o.RequiredTxOut()) if lt, ok := o.RequiredLockTime(); ok { // If another input commits to a different locktime, // they cannot be combined in the same transaction. if locktime != -1 && locktime != int32(lt) { return nil, 0, ErrLocktimeConflict } locktime = int32(lt) } totalInput += btcutil.Amount(o.SignDesc().Output.Value) requiredOutput += btcutil.Amount(o.RequiredTxOut().Value) } // Sum up the value contained in the remaining inputs, and add them to // the sweep transaction. for _, o := range inputs { if o.RequiredTxOut() != nil { continue } idxs = append(idxs, o) sweepTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: o.OutPoint(), Sequence: o.BlocksToMaturity(), }) if lt, ok := o.RequiredLockTime(); ok { if locktime != -1 && locktime != int32(lt) { return nil, 0, ErrLocktimeConflict } locktime = int32(lt) } totalInput += btcutil.Amount(o.SignDesc().Output.Value) } // Add the outputs given, if any. for _, o := range outputs { sweepTx.AddTxOut(o) requiredOutput += btcutil.Amount(o.Value) } if requiredOutput+txFee > totalInput { return nil, 0, fmt.Errorf("insufficient input to create sweep "+ "tx: input_sum=%v, output_sum=%v", totalInput, requiredOutput+txFee) } // The value remaining after the required output and fees, go to // change. Not that this fee is what we would have to pay in case the // sweep tx has a change output. changeAmt := totalInput - requiredOutput - txFee // We'll calculate the dust limit for the given changePkScript since it // is variable. changeLimit := lnwallet.DustLimitForSize(len(changePkScript)) // The txn will sweep the amount after fees to the pkscript generated // above. if changeAmt >= changeLimit { sweepTx.AddTxOut(&wire.TxOut{ PkScript: changePkScript, Value: int64(changeAmt), }) } else { log.Infof("Change amt %v below dustlimit %v, not adding "+ "change output", changeAmt, changeLimit) // The dust amount is added to the fee as the miner will // collect it. txFee += changeAmt } // We'll default to using the current block height as locktime, if none // of the inputs commits to a different locktime. sweepTx.LockTime = currentBlockHeight if locktime != -1 { sweepTx.LockTime = uint32(locktime) } // Before signing the transaction, check to ensure that it meets some // basic validity requirements. // // TODO(conner): add more control to sanity checks, allowing us to // delay spending "problem" outputs, e.g. possibly batching with other // classes if fees are too low. btx := btcutil.NewTx(sweepTx) if err := blockchain.CheckTransactionSanity(btx); err != nil { return nil, 0, err } prevInputFetcher, err := input.MultiPrevOutFetcher(inputs) if err != nil { return nil, 0, fmt.Errorf("error creating prev input fetcher "+ "for hash cache: %v", err) } hashCache := txscript.NewTxSigHashes(sweepTx, prevInputFetcher) // With all the inputs in place, use each output's unique input script // function to generate the final witness required for spending. addInputScript := func(idx int, tso input.Input) error { inputScript, err := tso.CraftInputScript( signer, sweepTx, hashCache, prevInputFetcher, idx, ) if err != nil { return err } sweepTx.TxIn[idx].Witness = inputScript.Witness if len(inputScript.SigScript) != 0 { sweepTx.TxIn[idx].SignatureScript = inputScript.SigScript } return nil } for idx, inp := range idxs { if err := addInputScript(idx, inp); err != nil { return nil, 0, err } } log.Debugf("Creating sweep transaction %v for %v inputs (%s) "+ "using %v, tx_weight=%v, tx_fee=%v, parents_count=%v, "+ "parents_fee=%v, parents_weight=%v, current_height=%v", sweepTx.TxHash(), len(inputs), inputTypeSummary(inputs), feeRate, estimator.weight(), txFee, len(estimator.parents), estimator.parentsFee, estimator.parentsWeight, currentBlockHeight) return sweepTx, txFee, nil } // getWeightEstimate returns a weight estimate for the given inputs. // Additionally, it returns counts for the number of csv and cltv inputs. func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut, feeRate, maxFeeRate chainfee.SatPerKWeight, outputPkScripts [][]byte) ([]input.Input, *weightEstimator, error) { // We initialize a weight estimator so we can accurately asses the // amount of fees we need to pay for this sweep transaction. // // TODO(roasbeef): can be more intelligent about buffering outputs to // be more efficient on-chain. weightEstimate := newWeightEstimator(feeRate, maxFeeRate) // Our sweep transaction will always pay to the given set of outputs. for _, o := range outputs { weightEstimate.addOutput(o) } // If there is any leftover change after paying to the given outputs // and required outputs, it will go to a single segwit p2wkh or p2tr // address. This will be our change address, so ensure it contributes // to our weight estimate. Note that if we have other outputs, we might // end up creating a sweep tx without a change output. It is okay to // add the change output to the weight estimate regardless, since the // estimated fee will just be subtracted from this already dust output, // and trimmed. for _, outputPkScript := range outputPkScripts { switch { case txscript.IsPayToTaproot(outputPkScript): weightEstimate.addP2TROutput() case txscript.IsPayToWitnessScriptHash(outputPkScript): weightEstimate.addP2WSHOutput() case txscript.IsPayToWitnessPubKeyHash(outputPkScript): weightEstimate.addP2WKHOutput() case txscript.IsPayToPubKeyHash(outputPkScript): weightEstimate.estimator.AddP2PKHOutput() case txscript.IsPayToScriptHash(outputPkScript): weightEstimate.estimator.AddP2SHOutput() default: // Unknown script type. return nil, nil, fmt.Errorf("unknown script "+ "type: %x", outputPkScript) } } // For each output, use its witness type to determine the estimate // weight of its witness, and add it to the proper set of spendable // outputs. var sweepInputs []input.Input for i := range inputs { inp := inputs[i] err := weightEstimate.add(inp) if err != nil { // TODO(yy): check if this is even possible? If so, we // should return the error here instead of filtering! log.Errorf("Failed to get weight estimate for "+ "input=%v, witnessType=%v: %v ", inp.OutPoint(), inp.WitnessType(), err) // Skip inputs for which no weight estimate can be // given. continue } // If this input comes with a committed output, add that as // well. if inp.RequiredTxOut() != nil { weightEstimate.addOutput(inp.RequiredTxOut()) } sweepInputs = append(sweepInputs, inp) } return sweepInputs, weightEstimate, nil } // inputSummary returns a string containing a human readable summary about the // witness types of a list of inputs. func inputTypeSummary(inputs []input.Input) string { // Sort inputs by witness type. sortedInputs := make([]input.Input, len(inputs)) copy(sortedInputs, inputs) sort.Slice(sortedInputs, func(i, j int) bool { return sortedInputs[i].WitnessType().String() < sortedInputs[j].WitnessType().String() }) var parts []string for _, i := range sortedInputs { part := fmt.Sprintf("%v (%v)", i.OutPoint(), i.WitnessType()) parts = append(parts, part) } return strings.Join(parts, ", ") }