package sweep import ( "context" "fmt" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/swap" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" ) // Sweeper creates htlc sweep txes. type Sweeper struct { Lnd *lndclient.LndServices } // CreateSweepTx creates an htlc sweep tx. func (s *Sweeper) CreateSweepTx( globalCtx context.Context, height int32, sequence uint32, htlc *swap.Htlc, htlcOutpoint wire.OutPoint, keyBytes [33]byte, witnessFunc func(sig []byte) (wire.TxWitness, error), amount, destAmount, feeOnlyDest, feeOnlyChange, feeBoth btcutil.Amount, destAddr, changeAddr btcutil.Address, warnf func(message string, args ...interface{})) (*wire.MsgTx, error) { // Compose tx. sweepTx := wire.NewMsgTx(2) sweepTx.LockTime = uint32(height) // Add HTLC input. sweepTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: htlcOutpoint, SignatureScript: htlc.SigScript, Sequence: sequence, }) destinations, err := deduceDestinations(amount, destAmount, feeOnlyDest, feeOnlyChange, feeBoth, destAddr, changeAddr) if err != nil { return nil, err } if len(destinations) == 1 && destinations[0].addr == changeAddr { warnf("Not sufficient coin size to send to destAddr, so sending to changeAddr. amount=%s, destination=destAmount, fee=%s. This must be a bug.", amount, destAmount, feeOnlyDest) } // Add outputs. for _, dst := range destinations { sweepPkScript, err := txscript.PayToAddrScript(dst.addr) if err != nil { return nil, err } sweepTx.AddTxOut(&wire.TxOut{ PkScript: sweepPkScript, Value: int64(dst.amount), }) } // Generate a signature for the swap htlc transaction. key, err := btcec.ParsePubKey(keyBytes[:], btcec.S256()) if err != nil { return nil, err } signDesc := lndclient.SignDescriptor{ WitnessScript: htlc.Script(), Output: &wire.TxOut{ Value: int64(amount), }, HashType: txscript.SigHashAll, InputIndex: 0, KeyDesc: keychain.KeyDescriptor{ PubKey: key, }, } rawSigs, err := s.Lnd.Signer.SignOutputRaw( globalCtx, sweepTx, []*lndclient.SignDescriptor{&signDesc}, ) if err != nil { return nil, fmt.Errorf("signing: %v", err) } sig := rawSigs[0] // Add witness stack to the tx input. sweepTx.TxIn[0].Witness, err = witnessFunc(sig) if err != nil { return nil, err } return sweepTx, nil } // GetSweepFee calculates the required tx fee to spend to P2WKH. It takes a // function that is expected to add the weight of the input to the weight // estimator. func (s *Sweeper) GetSweepFee(ctx context.Context, addInputEstimate func(*input.TxWeightEstimator), destAddr, changeAddr btcutil.Address, sweepConfTarget int32) ( feeOnlyDest, feeOnlyChange, feeBoth btcutil.Amount, err error) { // Get fee estimate from lnd. feeRate, err := s.Lnd.WalletKit.EstimateFee(ctx, sweepConfTarget) if err != nil { return 0, 0, 0, fmt.Errorf("estimate fee: %v", err) } // Calculate feeOnlyDest. var estOnlyDest input.TxWeightEstimator if err := addOutput(&estOnlyDest, destAddr); err != nil { return 0, 0, 0, err } addInputEstimate(&estOnlyDest) feeOnlyDest = feeRate.FeeForWeight(int64(estOnlyDest.Weight())) if changeAddr != nil { // Calculate feeOnlyChange. var estOnlyChange input.TxWeightEstimator if err := addOutput(&estOnlyChange, changeAddr); err != nil { return 0, 0, 0, err } addInputEstimate(&estOnlyChange) feeOnlyChange = feeRate.FeeForWeight(int64(estOnlyChange.Weight())) // Calculate feeBoth. var estBoth input.TxWeightEstimator if err := addOutput(&estBoth, destAddr); err != nil { return 0, 0, 0, err } if err := addOutput(&estBoth, changeAddr); err != nil { return 0, 0, 0, err } addInputEstimate(&estBoth) feeBoth = feeRate.FeeForWeight(int64(estBoth.Weight())) } return feeOnlyDest, feeOnlyChange, feeBoth, nil } func addOutput(weightEstimate *input.TxWeightEstimator, addr btcutil.Address) error { switch addr.(type) { case *btcutil.AddressWitnessScriptHash: weightEstimate.AddP2WSHOutput() case *btcutil.AddressWitnessPubKeyHash: weightEstimate.AddP2WKHOutput() case *btcutil.AddressScriptHash: weightEstimate.AddP2SHOutput() case *btcutil.AddressPubKeyHash: weightEstimate.AddP2PKHOutput() default: return fmt.Errorf("unknown address type %T", addr) } return nil } const dustOutput = 2020 // FIXME: find the actual value. type destination struct { addr btcutil.Address amount btcutil.Amount } func deduceDestinations(amount, destAmount, feeOnlyDest, feeOnlyChange, feeBoth btcutil.Amount, destAddr, changeAddr btcutil.Address) ([]destination, error) { if (destAmount != 0) != (changeAddr != nil) { return nil, fmt.Errorf("provide either both destAmount and changeAddr or none of them") } if (feeOnlyChange != 0) != (changeAddr != nil) { return nil, fmt.Errorf("provide either both feeOnlyChange and changeAddr or none of them") } if (feeBoth != 0) != (changeAddr != nil) { return nil, fmt.Errorf("provide either both feeBoth and changeAddr or none of them") } if changeAddr == nil { // No change. Just put everything on the main destination address. return []destination{ { addr: destAddr, amount: amount - feeOnlyDest, }, }, nil } changeAmount := amount - destAmount - feeBoth if changeAmount > dustOutput { // Change is large enough. Return tx with 2 outputs. return []destination{ { addr: destAddr, amount: destAmount, }, { addr: changeAddr, amount: changeAmount, }, }, nil } // If we are here, then changeAmount is below dustOutput so we drop the change. if amount-destAmount >= feeOnlyDest { // We still can send destAmount to destAddr. return []destination{ { addr: destAddr, amount: destAmount, }, }, nil } // We can not send destAmount to destAddr. This must be a bug, because we // checked in swapClientServer.LoopOut that amt >= max_miner_fee + dest_amt. // However it is better to send everything to change address than to crash. // The warning is logged by CreateSweepTx. return []destination{ { addr: changeAddr, amount: amount - feeOnlyChange, }, }, nil }