sweep: add new CraftSweepAllTx method to fully withdrawal from a wallet

In this commit, we add a new function, CraftSweepAllTx. This function
allows callers to craft a transaction which sweeps ALL outputs from the
wallet to a single target address. It can either be used for UTXO
consolidation (at the cost of privacy by co-mingling inputs), or simply
to sweep all funds out of a wallet for various reasons.

In an attempt to ensure this method is loosely coupled and testable, for
all behavior structs, we create brand new interface to accept. This
ensures that we only rely on the minimal number of methods needed to
perform our duty.
This commit is contained in:
Olaoluwa Osuntokun 2018-11-17 20:59:40 -08:00
parent 4ad175c16d
commit 9c29e61826
No known key found for this signature in database
GPG Key ID: CE58F7F8E20FD9A2

@ -2,7 +2,11 @@ package sweep
import (
"fmt"
"math"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwallet"
)
@ -66,3 +70,204 @@ func DetermineFeePerKw(feeEstimator lnwallet.FeeEstimator,
return feePerKw, nil
}
}
// UtxoSource is an interface that allows a caller to access a source of UTXOs
// to use when crafting sweep transactions.
type UtxoSource interface {
// ListUnspentWitness returns all UTXOs from the source that have
// between minConfs and maxConfs number of confirmations.
ListUnspentWitness(minConfs, maxConfs int32) ([]*lnwallet.Utxo, error)
// FetchInputInfo returns the matching output for an outpoint. If the
// outpoint doesn't belong to this UTXO source, then an error should be
// returned.
FetchInputInfo(*wire.OutPoint) (*wire.TxOut, error)
}
// CoinSelectionLocker is an interface that allows the caller to perform an
// operation, which is synchronized with all coin selection attempts. This can
// be used when an operation requires that all coin selection operations cease
// forward progress. Think of this as an exclusive lock on coin selection
// operations.
type CoinSelectionLocker interface {
// WithCoinSelectLock will execute the passed function closure in a
// synchronized manner preventing any coin selection operations from
// proceeding while the closure if executing. This can be seen as the
// ability to execute a function closure under an exclusive coin
// selection lock.
WithCoinSelectLock(func() error) error
}
// OutpointLocker allows a caller to lock/unlock an outpoint. When locked, the
// outpoints shouldn't be used for any sort of channel funding of coin
// selection. Locked outpoints are not expect to be persisted between restarts.
type OutpointLocker interface {
// LockOutpoint locks a target outpoint, rendering it unusable for coin
// selection.
LockOutpoint(o wire.OutPoint)
// UnlockOutpoint unlocks a target outpoint, allowing it to be used for
// coin selection once again.
UnlockOutpoint(o wire.OutPoint)
}
// WalletSweepPackage is a package that gives the caller the ability to sweep
// ALL funds from a wallet in a single transaction. We also package a function
// closure that allows one to abort the operation.
type WalletSweepPackage struct {
// SweepTx is a fully signed, and valid transaction that is broadcast,
// will sweep ALL confirmed coins in the wallet with a single
// transaction.
SweepTx *wire.MsgTx
// CancelSweepAttempt allows the caller to cancel the sweep attempt.
//
// NOTE: If the sweeping transaction isn't or cannot be broadcast, then
// this closure MUST be called, otherwise all selected utxos will be
// unable to be used.
CancelSweepAttempt func()
}
// CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the
// caller to sweep ALL outputs within the wallet to a single UTXO, as specified
// by the delivery address. The sweep transaction will be crafted with the
// target fee rate, and will use the utxoSource and outpointLocker as sources
// for wallet funds.
func CraftSweepAllTx(feeRate lnwallet.SatPerKWeight, blockHeight uint32,
deliveryAddr btcutil.Address, coinSelectLocker CoinSelectionLocker,
utxoSource UtxoSource, outpointLocker OutpointLocker,
feeEstimator lnwallet.FeeEstimator,
signer lnwallet.Signer) (*WalletSweepPackage, error) {
// TODO(roasbeef): turn off ATPL as well when available?
var allOutputs []*lnwallet.Utxo
// We'll make a function closure up front that allows us to unlock all
// selected outputs to ensure that they become available again in the
// case of an error after the outputs have been locked, but before we
// can actually craft a sweeping transaction.
unlockOutputs := func() {
for _, utxo := range allOutputs {
outpointLocker.UnlockOutpoint(utxo.OutPoint)
}
}
// Next, we'll use the coinSelectLocker to ensure that no coin
// selection takes place while we fetch and lock all outputs the wallet
// knows of. Otherwise, it may be possible for a new funding flow to
// lock an output while we fetch the set of unspent witnesses.
err := coinSelectLocker.WithCoinSelectLock(func() error {
// Now that we can be sure that no other coin selection
// operations are going on, we can grab a clean snapshot of the
// current UTXO state of the wallet.
utxos, err := utxoSource.ListUnspentWitness(
1, math.MaxInt32,
)
if err != nil {
return err
}
// We'll now lock each UTXO to ensure that other callers don't
// attempt to use these UTXOs in transactions while we're
// crafting out sweep all transaction.
for _, utxo := range utxos {
outpointLocker.LockOutpoint(utxo.OutPoint)
}
allOutputs = append(allOutputs, utxos...)
return nil
})
if err != nil {
// If we failed at all, we'll unlock any outputs selected just
// in case we had any lingering outputs.
unlockOutputs()
return nil, fmt.Errorf("unable to fetch+lock wallet "+
"utxos: %v", err)
}
// Now that we've locked all the potential outputs to sweep, we'll
// assemble an input for each of them, so we can hand it off to the
// sweeper to generate and sign a transaction for us.
var inputsToSweep []Input
for _, output := range allOutputs {
// We'll consult the utxoSource for information concerning this
// outpoint, we'll need to properly populate a signDescriptor
// for this output.
outputInfo, err := utxoSource.FetchInputInfo(&output.OutPoint)
if err != nil {
unlockOutputs()
return nil, err
}
// As we'll be signing for outputs under control of the wallet,
// we only need to populate the output value and output script.
// The rest of the items will be populated internally within
// the sweeper via the witness generation function.
signDesc := &lnwallet.SignDescriptor{
Output: outputInfo,
HashType: txscript.SigHashAll,
}
pkScript := outputInfo.PkScript
// Based on the output type, we'll map it to the proper witness
// type so we can generate the set of input scripts needed to
// sweep the output.
var witnessType lnwallet.WitnessType
switch {
// If this is a p2wkh output, then we'll assume it's a witness
// key hash witness type.
case txscript.IsPayToWitnessPubKeyHash(pkScript):
witnessType = lnwallet.WitnessKeyHash
// If this is a p2sh output, then as since it's under control
// of the wallet, we'll assume it's a nested p2sh output.
case txscript.IsPayToScriptHash(pkScript):
witnessType = lnwallet.NestedWitnessKeyHash
// All other output types we count as unknown and will fail to
// sweep.
default:
unlockOutputs()
return nil, fmt.Errorf("unable to sweep coins, "+
"unknown script: %x", pkScript[:])
}
// Now that we've constructed the items required, we'll make an
// input which can be passed to the sweeper for ultimate
// sweeping.
input := MakeBaseInput(&output.OutPoint, witnessType, signDesc, 0)
inputsToSweep = append(inputsToSweep, &input)
}
// Next, we'll convert the delivery addr to a pkScript that we can use
// to create the sweep transaction.
deliveryPkScript, err := txscript.PayToAddrScript(deliveryAddr)
if err != nil {
unlockOutputs()
return nil, err
}
// Finally, we'll ask the sweeper to craft a sweep transaction which
// respects our fee preference and targets all the UTXOs of the wallet.
sweepTx, err := createSweepTx(
inputsToSweep, deliveryPkScript, blockHeight, feeRate, signer,
)
if err != nil {
unlockOutputs()
return nil, err
}
return &WalletSweepPackage{
SweepTx: sweepTx,
CancelSweepAttempt: unlockOutputs,
}, nil
}