You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/sweep/sweeper.go

235 lines
6.2 KiB
Go

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
}