Merge pull request #4908 from halseth/anchors-reserve-utxo

[anchors] Reserve wallet balance for anchor fee bumping
This commit is contained in:
Conner Fromknecht 2021-01-19 11:57:07 -08:00 committed by GitHub
commit 1d71a2b55d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 737 additions and 41 deletions

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"strings"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
@ -11,7 +12,9 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/sweep"
"github.com/stretchr/testify/require"
)
// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee
@ -22,7 +25,7 @@ func testCPFP(net *lntest.NetworkHarness, t *harnessTest) {
// Skip this test for neutrino, as it's not aware of mempool
// transactions.
if net.BackendCfg.Name() == "neutrino" {
t.Skipf("skipping reorg test for neutrino backend")
t.Skipf("skipping CPFP test for neutrino backend")
}
// We'll start the test by sending Alice some coins, which she'll use to
@ -160,3 +163,198 @@ func testCPFP(net *lntest.NetworkHarness, t *harnessTest) {
t.Fatalf(err.Error())
}
}
// testAnchorReservedValue tests that we won't allow sending transactions when
// that would take the value we reserve for anchor fee bumping out of our
// wallet.
func testAnchorReservedValue(net *lntest.NetworkHarness, t *harnessTest) {
// Start two nodes supporting anchor channels.
args := commitTypeAnchors.Args()
alice, err := net.NewNode("Alice", args)
require.NoError(t.t, err)
defer shutdownAndAssert(net, t, alice)
bob, err := net.NewNode("Bob", args)
require.NoError(t.t, err)
defer shutdownAndAssert(net, t, bob)
ctxb := context.Background()
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
err = net.ConnectNodes(ctxt, alice, bob)
require.NoError(t.t, err)
// Send just enough coins for Alice to open a channel without a change output.
const (
chanAmt = 1000000
feeEst = 8000
)
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout)
err = net.SendCoins(ctxt, chanAmt+feeEst, alice)
require.NoError(t.t, err)
// Alice opens a channel that would consume all the funds in her
// wallet, without a change output. This should not be allowed.
resErr := lnwallet.ErrReservedValueInvalidated.Error()
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout)
_, err = net.OpenChannel(
ctxt, alice, bob,
lntest.OpenChannelParams{
Amt: chanAmt,
},
)
if err == nil || !strings.Contains(err.Error(), resErr) {
t.Fatalf("expected failure, got: %v", err)
}
// Alice opens a smaller channel. This works since it will have a
// change output.
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout)
aliceChanPoint := openChannelAndAssert(
ctxt, t, net, alice, bob,
lntest.OpenChannelParams{
Amt: chanAmt / 2,
},
)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = alice.WaitForNetworkChannelOpen(ctxt, aliceChanPoint)
require.NoError(t.t, err)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = bob.WaitForNetworkChannelOpen(ctxt, aliceChanPoint)
require.NoError(t.t, err)
// Alice tries to send all coins to an internal address. This is
// allowed, since the final wallet balance will still be above the
// reserved value.
addrReq := &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := alice.NewAddress(ctxt, addrReq)
require.NoError(t.t, err)
sweepReq := &lnrpc.SendCoinsRequest{
Addr: resp.Address,
SendAll: true,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = alice.SendCoins(ctxt, sweepReq)
require.NoError(t.t, err)
block := mineBlocks(t, net, 1, 1)[0]
// The sweep transaction should have exactly one input, the change from
// the previous SendCoins call.
sweepTx := block.Transactions[1]
if len(sweepTx.TxIn) != 1 {
t.Fatalf("expected 1 inputs instead have %v", len(sweepTx.TxIn))
}
// It should have a single output.
if len(sweepTx.TxOut) != 1 {
t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut))
}
// Wait for Alice to see her balance as confirmed.
waitForConfirmedBalance := func() int64 {
var balance int64
err := wait.NoError(func() error {
req := &lnrpc.WalletBalanceRequest{}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := alice.WalletBalance(ctxt, req)
if err != nil {
return err
}
if resp.TotalBalance == 0 {
return fmt.Errorf("no balance")
}
if resp.UnconfirmedBalance > 0 {
return fmt.Errorf("unconfirmed balance")
}
balance = resp.TotalBalance
return nil
}, defaultTimeout)
require.NoError(t.t, err)
return balance
}
_ = waitForConfirmedBalance()
// Alice tries to send all funds to an external address, the reserved
// value must stay in her wallet.
minerAddr, err := net.Miner.NewAddress()
require.NoError(t.t, err)
sweepReq = &lnrpc.SendCoinsRequest{
Addr: minerAddr.String(),
SendAll: true,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = alice.SendCoins(ctxt, sweepReq)
require.NoError(t.t, err)
// We'll mine a block which should include the sweep transaction we
// generated above.
block = mineBlocks(t, net, 1, 1)[0]
// The sweep transaction should have exactly one inputs as we only had
// the the single output from above in the wallet.
sweepTx = block.Transactions[1]
if len(sweepTx.TxIn) != 1 {
t.Fatalf("expected 1 inputs instead have %v", len(sweepTx.TxIn))
}
// It should have two outputs, one being the miner address, the other
// one being the reserve going back to our wallet.
if len(sweepTx.TxOut) != 2 {
t.Fatalf("expected 2 outputs instead have %v", len(sweepTx.TxOut))
}
// The reserved value is now back in Alice's wallet.
aliceBalance := waitForConfirmedBalance()
// Alice closes channel, should now be allowed to send everything to an
// external address.
closeChannelAndAssert(ctxt, t, net, alice, aliceChanPoint, false)
newBalance := waitForConfirmedBalance()
if newBalance <= aliceBalance {
t.Fatalf("Alice's balance did not increase after channel close")
}
// We'll wait for the balance to reflect that the channel has been
// closed and the funds are in the wallet.
sweepReq = &lnrpc.SendCoinsRequest{
Addr: minerAddr.String(),
SendAll: true,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = alice.SendCoins(ctxt, sweepReq)
require.NoError(t.t, err)
// We'll mine a block which should include the sweep transaction we
// generated above.
block = mineBlocks(t, net, 1, 1)[0]
// The sweep transaction should have two inputs, the change output from
// the previous sweep, and the output from the coop closed channel.
sweepTx = block.Transactions[1]
if len(sweepTx.TxIn) != 2 {
t.Fatalf("expected 2 inputs instead have %v", len(sweepTx.TxIn))
}
// It should have a single output.
if len(sweepTx.TxOut) != 1 {
t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut))
}
}

View File

@ -230,6 +230,10 @@ var allTestCases = []*testCase{
name: "cpfp",
test: testCPFP,
},
{
name: "anchors reserved value",
test: testAnchorReservedValue,
},
{
name: "macaroon authentication",
test: testMacaroonAuthentication,

View File

@ -251,3 +251,6 @@
<time> [ERR] UTXN: Failed to sweep first-stage HTLC (CLTV-delayed) output <chan_point>
<time> [ERR] UTXN: Notification chan closed, can't advance output <chan_point>
<time> [ERR] DISC: Unable to rebroadcast stale announcements: unable to retrieve outgoing channels: channel from self node has no policy
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: reserved wallet balance invalidated
<time> [ERR] RPCS: [/lnrpc.Lightning/SendCoins]: reserved wallet balance invalidated
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): reserved wallet balance invalidated

View File

@ -103,6 +103,16 @@ type Intent interface {
// change.
LocalFundingAmt() btcutil.Amount
// Inputs returns all inputs to the final funding transaction that we
// know about. Note that there might be more, but we are not (yet)
// aware of.
Inputs() []wire.OutPoint
// Outputs returns all outputs of the final funding transaction that we
// know about. Note that there might be more, but we are not (yet)
// aware of.
Outputs() []*wire.TxOut
// Cancel allows the caller to cancel a funding Intent at any time.
// This will return any resources such as coins back to the eligible
// pool to be used in order channel fundings.

View File

@ -98,6 +98,30 @@ func (s *ShimIntent) ThawHeight() uint32 {
return s.thawHeight
}
// Inputs returns all inputs to the final funding transaction that we
// know about. For the ShimIntent this will always be none, since it is funded
// externally.
func (s *ShimIntent) Inputs() []wire.OutPoint {
return nil
}
// Outputs returns all outputs of the final funding transaction that we
// know about. Since this is an externally funded channel, the channel output
// is the only known one.
func (s *ShimIntent) Outputs() []*wire.TxOut {
_, txOut, err := s.FundingOutput()
if err != nil {
log.Warnf("Unable to find funding output for shim intent: %v",
err)
// Failed finding funding output, return empty list of known
// outputs.
return nil
}
return []*wire.TxOut{txOut}
}
// FundingKeys couples our multi-sig key along with the remote party's key.
type FundingKeys struct {
// LocalKey is our multi-sig key.

View File

@ -392,6 +392,53 @@ func (i *PsbtIntent) Cancel() {
i.ShimIntent.Cancel()
}
// Inputs returns all inputs to the final funding transaction that we know
// about. These are only known after the PSBT has been verified.
func (i *PsbtIntent) Inputs() []wire.OutPoint {
var inputs []wire.OutPoint
switch i.State {
// We return the inputs to the pending psbt.
case PsbtVerified:
for _, in := range i.PendingPsbt.UnsignedTx.TxIn {
inputs = append(inputs, in.PreviousOutPoint)
}
// We return the inputs to the final funding tx.
case PsbtFinalized, PsbtFundingTxCompiled:
for _, in := range i.FinalTX.TxIn {
inputs = append(inputs, in.PreviousOutPoint)
}
// In all other states we cannot know the inputs to the funding tx, and
// return an empty list.
default:
}
return inputs
}
// Outputs returns all outputs of the final funding transaction that we
// know about. These are only known after the PSBT has been verified.
func (i *PsbtIntent) Outputs() []*wire.TxOut {
switch i.State {
// We return the outputs of the pending psbt.
case PsbtVerified:
return i.PendingPsbt.UnsignedTx.TxOut
// We return the outputs of the final funding tx.
case PsbtFinalized, PsbtFundingTxCompiled:
return i.FinalTX.TxOut
// In all other states we cannot know the final outputs, and return an
// empty list.
default:
return nil
}
}
// PsbtAssembler is a type of chanfunding.Assembler wherein the funding
// transaction is constructed outside of lnd by using partially signed bitcoin
// transactions (PSBT).

View File

@ -152,6 +152,28 @@ func (f *FullIntent) CompileFundingTx(extraInputs []*wire.TxIn,
return fundingTx, nil
}
// Inputs returns all inputs to the final funding transaction that we
// know about. Since this funding transaction is created all from our wallet,
// it will be all inputs.
func (f *FullIntent) Inputs() []wire.OutPoint {
var ins []wire.OutPoint
for _, coin := range f.InputCoins {
ins = append(ins, coin.OutPoint)
}
return ins
}
// Outputs returns all outputs of the final funding transaction that we
// know about. This will be the funding output and the change outputs going
// back to our wallet.
func (f *FullIntent) Outputs() []*wire.TxOut {
outs := f.ShimIntent.Outputs()
outs = append(outs, f.ChangeOutputs...)
return outs
}
// Cancel allows the caller to cancel a funding Intent at any time. This will
// return any resources such as coins back to the eligible pool to be used in
// order channel fundings.

View File

@ -5,6 +5,7 @@ import (
"crypto/sha256"
"errors"
"fmt"
"math"
"net"
"sync"
"sync/atomic"
@ -32,6 +33,12 @@ const (
// The size of the buffered queue of requests to the wallet from the
// outside word.
msgBufferSize = 100
// anchorChanReservedValue is the amount we'll keep around in the
// wallet in case we have to fee bump anchor channels on force close.
// TODO(halseth): update constant to target a specific commit size at
// set fee rate.
anchorChanReservedValue = btcutil.Amount(10_000)
)
var (
@ -39,6 +46,12 @@ var (
// contribution handling process if the process should be paused for
// the construction of a PSBT outside of lnd's wallet.
ErrPsbtFundingRequired = errors.New("PSBT funding required")
// ErrReservedValueInvalidated is returned if we try to publish a
// transaction that would take the walletbalance below what we require
// to keep around to fee bump our open anchor channels.
ErrReservedValueInvalidated = errors.New("reserved wallet balance " +
"invalidated")
)
// PsbtFundingRequired is a type that implements the error interface and
@ -303,7 +316,12 @@ type LightningWallet struct {
// monotonically integer. All requests concerning the channel MUST
// carry a valid, active funding ID.
fundingLimbo map[uint64]*ChannelReservation
limboMtx sync.RWMutex
// reservationIDs maps a pending channel ID to the reservation ID used
// as key in the fundingLimbo map. Used to easily look up a channel
// reservation given a pending channel ID.
reservationIDs map[[32]byte]uint64
limboMtx sync.RWMutex
// lockedOutPoints is a set of the currently locked outpoint. This
// information is kept in order to provide an easy way to unlock all
@ -334,6 +352,7 @@ func NewLightningWallet(Cfg Config) (*LightningWallet, error) {
msgChan: make(chan interface{}, msgBufferSize),
nextFundingID: 0,
fundingLimbo: make(map[uint64]*ChannelReservation),
reservationIDs: make(map[[32]byte]uint64),
lockedOutPoints: make(map[wire.OutPoint]struct{}),
fundingIntents: make(map[[32]byte]chanfunding.Intent),
quit: make(chan struct{}),
@ -404,6 +423,7 @@ func (l *LightningWallet) LockedOutpoints() []*wire.OutPoint {
func (l *LightningWallet) ResetReservations() {
l.nextFundingID = 0
l.fundingLimbo = make(map[uint64]*ChannelReservation)
l.reservationIDs = make(map[[32]byte]uint64)
for outpoint := range l.lockedOutPoints {
l.UnlockOutpoint(outpoint)
@ -510,16 +530,16 @@ func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
// PsbtFundingVerify looks up a previously registered funding intent by its
// pending channel ID and tries to advance the state machine by verifying the
// passed PSBT.
func (l *LightningWallet) PsbtFundingVerify(pid [32]byte,
func (l *LightningWallet) PsbtFundingVerify(pendingChanID [32]byte,
packet *psbt.Packet) error {
l.intentMtx.Lock()
defer l.intentMtx.Unlock()
intent, ok := l.fundingIntents[pid]
intent, ok := l.fundingIntents[pendingChanID]
if !ok {
return fmt.Errorf("no funding intent found for "+
"pendingChannelID(%x)", pid[:])
"pendingChannelID(%x)", pendingChanID[:])
}
psbtIntent, ok := intent.(*chanfunding.PsbtIntent)
if !ok {
@ -530,7 +550,49 @@ func (l *LightningWallet) PsbtFundingVerify(pid [32]byte,
return fmt.Errorf("error verifying PSBT: %v", err)
}
return nil
// Get the channel reservation for that corresponds to this pending
// channel ID.
l.limboMtx.Lock()
pid, ok := l.reservationIDs[pendingChanID]
if !ok {
l.limboMtx.Unlock()
return fmt.Errorf("no channel reservation found for "+
"pendingChannelID(%x)", pendingChanID[:])
}
pendingReservation, ok := l.fundingLimbo[pid]
l.limboMtx.Unlock()
if !ok {
return fmt.Errorf("no channel reservation found for "+
"reservation ID %v", pid)
}
// Now the the PSBT has been verified, we can again check whether the
// value reserved for anchor fee bumping is respected.
numAnchors, err := l.currentNumAnchorChans()
if err != nil {
return err
}
// If this commit type is an anchor channel we add that to our counter,
// but only if we are contributing funds to the channel. This is done
// to still allow incoming channels even though we have no UTXOs
// available, as in bootstrapping phases.
if pendingReservation.partialState.ChanType.HasAnchors() &&
intent.LocalFundingAmt() > 0 {
numAnchors++
}
// We check the reserve value again, this should already have been
// checked for regular FullIntents, but now the PSBT intent is also
// populated.
return l.WithCoinSelectLock(func() error {
_, err := l.CheckReservedValue(
intent.Inputs(), intent.Outputs(), numAnchors,
)
return err
})
}
// PsbtFundingFinalize looks up a previously registered funding intent by its
@ -714,6 +776,46 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
thawHeight = shimIntent.ThawHeight()
}
// Now that we have a funding intent, we'll check whether funding a
// channel using it would violate our reserved value for anchor channel
// fee bumping. We first get our current number of anchor channels.
numAnchors, err := l.currentNumAnchorChans()
if err != nil {
fundingIntent.Cancel()
req.err <- err
req.resp <- nil
return
}
// If this commit type is an anchor channel we add that to our counter,
// but only if we are contributing funds to the channel. This is done
// to still allow incoming channels even though we have no UTXOs
// available, as in bootstrapping phases.
if req.CommitType == CommitmentTypeAnchorsZeroFeeHtlcTx &&
fundingIntent.LocalFundingAmt() > 0 {
numAnchors++
}
// Check the reserved value using the inputs and outputs given by the
// intent. Not that for the PSBT intent type we don't yet have the
// funding tx ready, so this will always pass. We'll do another check
// when the PSBT has been verified.
err = l.WithCoinSelectLock(func() error {
_, err := l.CheckReservedValue(
fundingIntent.Inputs(), fundingIntent.Outputs(),
numAnchors,
)
return err
})
if err != nil {
fundingIntent.Cancel()
req.err <- err
req.resp <- nil
return
}
// The total channel capacity will be the size of the funding output we
// created plus the remote contribution.
capacity := localFundingAmt + remoteFundingAmt
@ -748,6 +850,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
// request.
l.limboMtx.Lock()
l.fundingLimbo[id] = reservation
l.reservationIDs[req.PendingChanID] = id
l.limboMtx.Unlock()
// Funding reservation request successfully handled. The funding inputs
@ -757,6 +860,153 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
req.err <- nil
}
// currentNumAnchorChans returns the current number of anchor channels the
// wallet should be ready to fee bump if needed.
func (l *LightningWallet) currentNumAnchorChans() (int, error) {
// Count all anchor channels that are open or pending
// open, or waiting close.
chans, err := l.Cfg.Database.FetchAllChannels()
if err != nil {
return 0, err
}
var numAnchors int
for _, c := range chans {
if c.ChanType.HasAnchors() {
numAnchors++
}
}
// We also count pending close channels.
pendingClosed, err := l.Cfg.Database.FetchClosedChannels(
true,
)
if err != nil {
return 0, err
}
for _, c := range pendingClosed {
c, err := l.Cfg.Database.FetchHistoricalChannel(
&c.ChanPoint,
)
if err != nil {
// We don't have a guarantee that all channels re found
// in the historical channels bucket, so we continue.
walletLog.Warnf("Unable to fetch historical "+
"channel: %v", err)
continue
}
if c.ChanType.HasAnchors() {
numAnchors++
}
}
return numAnchors, nil
}
// CheckReservedValue checks whether publishing a transaction with the given
// inputs and outputs would violate the value we reserve in the wallet for
// bumping the fee of anchor channels. The numAnchorChans argument should be
// set the the number of open anchor channels controlled by the wallet after
// the transaction has been published.
//
// If the reserved value is violated, the returned error will be
// ErrReservedValueInvalidated. The method will also return the current
// reserved value, both in case of success and in case of
// ErrReservedValueInvalidated.
//
// NOTE: This method should only be run with the CoinSelectLock held.
func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint,
out []*wire.TxOut, numAnchorChans int) (btcutil.Amount, error) {
// Get all unspent coins in the wallet.
witnessOutputs, err := l.ListUnspentWitness(0, math.MaxInt32)
if err != nil {
return 0, err
}
ourInput := make(map[wire.OutPoint]struct{})
for _, op := range in {
ourInput[op] = struct{}{}
}
// When crafting a transaction with inputs from the wallet, these coins
// will usually be locked in the process, and not be returned when
// listing unspents. In this case they have already been deducted from
// the wallet balance. In case they haven't been properly locked, we
// check whether they are still listed among our unspents and deduct
// them.
var walletBalance btcutil.Amount
for _, in := range witnessOutputs {
// Spending an unlocked wallet UTXO, don't add it to the
// balance.
if _, ok := ourInput[in.OutPoint]; ok {
continue
}
walletBalance += in.Value
}
// Now we go through the outputs of the transaction, if any of the
// outputs are paying into the wallet (likely a change output), we add
// it to our final balance.
for _, txOut := range out {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
txOut.PkScript, &l.Cfg.NetParams,
)
if err != nil {
// Non-standard outputs can safely be skipped because
// they're not supported by the wallet.
continue
}
for _, addr := range addrs {
if !l.IsOurAddress(addr) {
continue
}
walletBalance += btcutil.Amount(txOut.Value)
// We break since we don't want to double count the output.
break
}
}
// We reserve a given amount for each anchor channel.
reserved := btcutil.Amount(numAnchorChans) * anchorChanReservedValue
if walletBalance < reserved {
walletLog.Debugf("Reserved value=%v above final "+
"walletbalance=%v with %d anchor channels open",
reserved, walletBalance, numAnchorChans)
return reserved, ErrReservedValueInvalidated
}
return reserved, nil
}
// CheckReservedValueTx calls CheckReservedValue with the inputs and outputs
// from the given tx, with the number of anchor channels currently open in the
// database.
//
// NOTE: This method should only be run with the CoinSelectLock held.
func (l *LightningWallet) CheckReservedValueTx(tx *wire.MsgTx) (btcutil.Amount,
error) {
numAnchors, err := l.currentNumAnchorChans()
if err != nil {
return 0, err
}
var inputs []wire.OutPoint
for _, txIn := range tx.TxIn {
inputs = append(inputs, txIn.PreviousOutPoint)
}
return l.CheckReservedValue(inputs, tx.TxOut, numAnchors)
}
// initOurContribution initializes the given ChannelReservation with our coins
// and change reserved for the channel, and derives the keys to use for this
// channel.
@ -890,6 +1140,7 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
delete(l.fundingLimbo, req.pendingFundingID)
pid := pendingReservation.pendingChanID
delete(l.reservationIDs, pid)
l.intentMtx.Lock()
if intent, ok := l.fundingIntents[pid]; ok {
@ -1440,6 +1691,7 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// Funding complete, this entry can be removed from limbo.
l.limboMtx.Lock()
delete(l.fundingLimbo, res.reservationID)
delete(l.reservationIDs, res.pendingChanID)
l.limboMtx.Unlock()
l.intentMtx.Lock()

View File

@ -1056,7 +1056,25 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64,
return nil, err
}
tx, err := r.server.cc.Wallet.SendOutputs(outputs, feeRate, minconf, label)
// We first do a dry run, to sanity check we won't spend our wallet
// balance below the reserved amount.
authoredTx, err := r.server.cc.Wallet.CreateSimpleTx(
outputs, feeRate, true,
)
if err != nil {
return nil, err
}
_, err = r.server.cc.Wallet.CheckReservedValueTx(authoredTx.Tx)
if err != nil {
return nil, err
}
// If that checks out, we're failry confident that creating sending to
// these outputs will keep the wallet balance above the reserve.
tx, err := r.server.cc.Wallet.SendOutputs(
outputs, feeRate, minconf, label,
)
if err != nil {
return nil, err
}
@ -1253,10 +1271,12 @@ func (r *rpcServer) SendCoins(ctx context.Context,
// With the sweeper instance created, we can now generate a
// transaction that will sweep ALL outputs from the wallet in a
// single transaction. This will be generated in a concurrent
// safe manner, so no need to worry about locking.
// safe manner, so no need to worry about locking. The tx will
// pay to the change address created above if we needed to
// reserve any value, the rest will go to targetAddr.
sweepTxPkg, err := sweep.CraftSweepAllTx(
feePerKw, lnwallet.DefaultDustLimit(),
uint32(bestHeight), targetAddr, wallet,
uint32(bestHeight), nil, targetAddr, wallet,
wallet.WalletController, wallet.WalletController,
r.server.cc.FeeEstimator, r.server.cc.Signer,
)
@ -1264,6 +1284,75 @@ func (r *rpcServer) SendCoins(ctx context.Context,
return nil, err
}
// Before we publish the transaction we make sure it won't
// violate our reserved wallet value.
var reservedVal btcutil.Amount
err = wallet.WithCoinSelectLock(func() error {
var err error
reservedVal, err = wallet.CheckReservedValueTx(
sweepTxPkg.SweepTx,
)
return err
})
// If sending everything to this address would invalidate our
// reserved wallet balance, we create a new sweep tx, where
// we'll send the reserved value back to our wallet.
if err == lnwallet.ErrReservedValueInvalidated {
sweepTxPkg.CancelSweepAttempt()
rpcsLog.Debugf("Reserved value %v not satisfied after "+
"send_all, trying with change output",
reservedVal)
// We'll request a change address from the wallet,
// where we'll send this reserved value back to. This
// ensures this is an address the wallet knows about,
// allowing us to pass the reserved value check.
changeAddr, err := r.server.cc.Wallet.NewAddress(
lnwallet.WitnessPubKey, true,
)
if err != nil {
return nil, err
}
// Send the reserved value to this change address, the
// remaining funds will go to the targetAddr.
outputs := []sweep.DeliveryAddr{
{
Addr: changeAddr,
Amt: reservedVal,
},
}
sweepTxPkg, err = sweep.CraftSweepAllTx(
feePerKw, lnwallet.DefaultDustLimit(),
uint32(bestHeight), outputs, targetAddr, wallet,
wallet.WalletController, wallet.WalletController,
r.server.cc.FeeEstimator, r.server.cc.Signer,
)
if err != nil {
return nil, err
}
// Sanity check the new tx by re-doing the check.
err = wallet.WithCoinSelectLock(func() error {
_, err := wallet.CheckReservedValueTx(
sweepTxPkg.SweepTx,
)
return err
})
if err != nil {
sweepTxPkg.CancelSweepAttempt()
return nil, err
}
} else if err != nil {
sweepTxPkg.CancelSweepAttempt()
return nil, err
}
rpcsLog.Debugf("Sweeping all coins from wallet to addr=%v, "+
"with tx=%v", in.Addr, spew.Sdump(sweepTxPkg.SweepTx))

View File

@ -1192,8 +1192,8 @@ func (s *UtxoSweeper) sweep(inputs inputSet, feeRate chainfee.SatPerKWeight,
// Create sweep tx.
tx, err := createSweepTx(
inputs, s.currentOutputScript, uint32(currentHeight), feeRate,
dustLimit(s.relayFeeRate), s.cfg.Signer,
inputs, nil, s.currentOutputScript, uint32(currentHeight),
feeRate, dustLimit(s.relayFeeRate), s.cfg.Signer,
)
if err != nil {
return fmt.Errorf("create sweep tx: %v", err)
@ -1487,7 +1487,7 @@ func (s *UtxoSweeper) CreateSweepTx(inputs []input.Input, feePref FeePreference,
}
return createSweepTx(
inputs, pkScript, currentBlockHeight, feePerKw,
inputs, nil, pkScript, currentBlockHeight, feePerKw,
dustLimit(s.relayFeeRate), s.cfg.Signer,
)
}

View File

@ -354,7 +354,7 @@ func assertTxFeeRate(t *testing.T, tx *wire.MsgTx,
outputAmt := tx.TxOut[0].Value
fee := btcutil.Amount(inputAmt - outputAmt)
_, estimator := getWeightEstimate(inputs, 0)
_, estimator := getWeightEstimate(inputs, nil, 0)
txWeight := estimator.weight()
expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight))

View File

@ -131,12 +131,14 @@ func generateInputPartitionings(sweepableInputs []txInput,
return sets, nil
}
// createSweepTx builds a signed tx spending the inputs to a the output script.
func createSweepTx(inputs []input.Input, outputPkScript []byte,
currentBlockHeight uint32, feePerKw chainfee.SatPerKWeight,
dustLimit btcutil.Amount, signer input.Signer) (*wire.MsgTx, error) {
// 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,
feePerKw chainfee.SatPerKWeight, dustLimit btcutil.Amount,
signer input.Signer) (*wire.MsgTx, error) {
inputs, estimator := getWeightEstimate(inputs, feePerKw)
inputs, estimator := getWeightEstimate(inputs, outputs, feePerKw)
txFee := estimator.fee()
var (
@ -210,6 +212,16 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
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, fmt.Errorf("insufficient input to create sweep tx")
}
// 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.
@ -219,7 +231,7 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
// above.
if changeAmt >= dustLimit {
sweepTx.AddTxOut(&wire.TxOut{
PkScript: outputPkScript,
PkScript: changePkScript,
Value: int64(changeAmt),
})
} else {
@ -287,8 +299,8 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
// 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, feeRate chainfee.SatPerKWeight) (
[]input.Input, *weightEstimator) {
func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut,
feeRate chainfee.SatPerKWeight) ([]input.Input, *weightEstimator) {
// We initialize a weight estimator so we can accurately asses the
// amount of fees we need to pay for this sweep transaction.
@ -297,13 +309,19 @@ func getWeightEstimate(inputs []input.Input, feeRate chainfee.SatPerKWeight) (
// be more efficient on-chain.
weightEstimate := newWeightEstimator(feeRate)
// Our sweep transaction will pay to a single segwit p2wkh address,
// ensure it contributes to our weight estimate. If the inputs we add
// have required TxOuts, then this will be our change address. Note
// that if we have required TxOuts, 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.
// 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 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.
weightEstimate.addP2WKHOutput()
// For each output, use its witness type to determine the estimate

View File

@ -39,7 +39,7 @@ func TestWeightEstimate(t *testing.T) {
))
}
_, estimator := getWeightEstimate(inputs, 0)
_, estimator := getWeightEstimate(inputs, nil, 0)
weight := int64(estimator.weight())
if weight != expectedWeight {
t.Fatalf("unexpected weight. expected %d but got %d.",

View File

@ -148,13 +148,24 @@ type WalletSweepPackage struct {
CancelSweepAttempt func()
}
// DeliveryAddr is a pair of (address, amount) used to craft a transaction
// paying to more than one specified address.
type DeliveryAddr struct {
// Addr is the address to pay to.
Addr btcutil.Address
// Amt is the amount to pay to the given address.
Amt btcutil.Amount
}
// 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.
// caller to sweep ALL outputs within the wallet to a list of outputs. Any
// leftover amount after these outputs and transaction fee, is sent to a single
// output, as specified by the change 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 chainfee.SatPerKWeight, dustLimit btcutil.Amount,
blockHeight uint32, deliveryAddr btcutil.Address,
blockHeight uint32, deliveryAddrs []DeliveryAddr, changeAddr btcutil.Address,
coinSelectLocker CoinSelectionLocker, utxoSource UtxoSource,
outpointLocker OutpointLocker, feeEstimator chainfee.Estimator,
signer input.Signer) (*WalletSweepPackage, error) {
@ -261,9 +272,25 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount,
inputsToSweep = append(inputsToSweep, &input)
}
// Next, we'll convert the delivery addr to a pkScript that we can use
// Create a list of TxOuts from the given delivery addresses.
var txOuts []*wire.TxOut
for _, d := range deliveryAddrs {
pkScript, err := txscript.PayToAddrScript(d.Addr)
if err != nil {
unlockOutputs()
return nil, err
}
txOuts = append(txOuts, &wire.TxOut{
PkScript: pkScript,
Value: int64(d.Amt),
})
}
// Next, we'll convert the change addr to a pkScript that we can use
// to create the sweep transaction.
deliveryPkScript, err := txscript.PayToAddrScript(deliveryAddr)
changePkScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
unlockOutputs()
@ -273,7 +300,7 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount,
// 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,
inputsToSweep, txOuts, changePkScript, blockHeight, feeRate,
dustLimit, signer,
)
if err != nil {

View File

@ -288,7 +288,8 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx(
0, 100, 10, nil, coinSelectLocker, utxoSource, utxoLocker, nil, nil,
0, 100, 10, nil, nil, coinSelectLocker, utxoSource,
utxoLocker, nil, nil,
)
// Since we instructed the coin select locker to fail above, we should
@ -313,7 +314,8 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx(
0, 100, 10, nil, coinSelectLocker, utxoSource, utxoLocker, nil, nil,
0, 100, 10, nil, nil, coinSelectLocker, utxoSource,
utxoLocker, nil, nil,
)
// Since passed in a p2wsh output, which is unknown, we should fail to
@ -347,8 +349,8 @@ func TestCraftSweepAllTx(t *testing.T) {
utxoLocker := newMockOutpointLocker()
sweepPkg, err := CraftSweepAllTx(
0, 100, 10, deliveryAddr, coinSelectLocker, utxoSource, utxoLocker,
feeEstimator, signer,
0, 100, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLocker, feeEstimator, signer,
)
if err != nil {
t.Fatalf("unable to make sweep tx: %v", err)