From 422008e3c05c9b74d591c85677ca5f8497b0e7a1 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 10 Jan 2021 19:54:49 +0100 Subject: [PATCH] lnwallet+funding+rpcserver: check reserved value before PublishTransaction For a few manual send cases that can be initiated by the user, we check the reserved value. --- lnwallet/wallet.go | 21 +++++++++++ rpcserver.go | 93 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 82ba1983..e550d1e8 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -986,6 +986,27 @@ func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint, 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. diff --git a/rpcserver.go b/rpcserver.go index 19072226..35a84c7f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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,7 +1271,9 @@ 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), nil, targetAddr, wallet, @@ -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))