diff --git a/docs/release-notes/release-notes-0.14.0.md b/docs/release-notes/release-notes-0.14.0.md index 4fb1a96c6e..46613c13fc 100644 --- a/docs/release-notes/release-notes-0.14.0.md +++ b/docs/release-notes/release-notes-0.14.0.md @@ -405,6 +405,9 @@ you. [Lnd is updated to use the version of Neutrino containing this fix](https://github.com/lightningnetwork/lnd/pull/5807). +* [Use the change output index when validating the reserved wallet balance for + SendCoins/SendMany calls](https://github.com/lightningnetwork/lnd/pull/5665) + ## Documentation The [code contribution guidelines have been updated to mention the new diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index a99fd1b1e5..b0ba460f42 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -272,6 +272,20 @@ type addSingleFunderSigsMsg struct { err chan error } +// CheckReservedValueTxReq is the request struct used to call +// CheckReservedValueTx with. It contains the transaction to check as well as +// an optional explicitly defined index to denote a change output that is not +// watched by the wallet. +type CheckReservedValueTxReq struct { + // Tx is the transaction to check the outputs for. + Tx *wire.MsgTx + + // ChangeIndex denotes an optional output index that can be explicitly + // set for a change that is not being watched by the wallet and would + // otherwise not be recognized as a change output. + ChangeIndex *int +} + // LightningWallet is a domain specific, yet general Bitcoin wallet capable of // executing workflow required to interact with the Lightning Network. It is // domain specific in the sense that it understands all the fancy scripts used @@ -1036,8 +1050,8 @@ func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint, // database. // // NOTE: This method should only be run with the CoinSelectLock held. -func (l *LightningWallet) CheckReservedValueTx(tx *wire.MsgTx) (btcutil.Amount, - error) { +func (l *LightningWallet) CheckReservedValueTx(req CheckReservedValueTxReq) ( + btcutil.Amount, error) { numAnchors, err := l.currentNumAnchorChans() if err != nil { @@ -1045,11 +1059,44 @@ func (l *LightningWallet) CheckReservedValueTx(tx *wire.MsgTx) (btcutil.Amount, } var inputs []wire.OutPoint - for _, txIn := range tx.TxIn { + for _, txIn := range req.Tx.TxIn { inputs = append(inputs, txIn.PreviousOutPoint) } - return l.CheckReservedValue(inputs, tx.TxOut, numAnchors) + reservedVal, err := l.CheckReservedValue( + inputs, req.Tx.TxOut, numAnchors, + ) + switch { + + // If the error returned from CheckReservedValue is + // ErrReservedValueInvalidated, then it did nonetheless return + // the required reserved value and we check for the optional + // change index. + case errors.Is(err, ErrReservedValueInvalidated): + // Without a change index provided there is nothing more to + // check and the error is returned. + if req.ChangeIndex == nil { + return reservedVal, err + } + + // If a change index was provided we make only sure that it + // would leave sufficient funds for the reserved balance value. + // + // Note: This is used if a change output index is explicitly set + // but that may not be watched by the wallet and therefore is + // not picked up by the call to CheckReservedValue above. + chIdx := *req.ChangeIndex + if chIdx < 0 || chIdx >= len(req.Tx.TxOut) || + req.Tx.TxOut[chIdx].Value < int64(reservedVal) { + + return reservedVal, err + } + + case err != nil: + return reservedVal, err + } + + return reservedVal, nil } // initOurContribution initializes the given ChannelReservation with our coins diff --git a/rpcserver.go b/rpcserver.go index 528412ba35..54d62c07be 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1009,7 +1009,14 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64, return nil, err } - _, err = r.server.cc.Wallet.CheckReservedValueTx(authoredTx.Tx) + // Check the authored transaction and use the explicitly set change index + // to make sure that the wallet reserved balance is not invalidated. + _, err = r.server.cc.Wallet.CheckReservedValueTx( + lnwallet.CheckReservedValueTxReq{ + Tx: authoredTx.Tx, + ChangeIndex: &authoredTx.ChangeIndex, + }, + ) if err != nil { return nil, err } @@ -1242,7 +1249,9 @@ func (r *rpcServer) SendCoins(ctx context.Context, err = wallet.WithCoinSelectLock(func() error { var err error reservedVal, err = wallet.CheckReservedValueTx( - sweepTxPkg.SweepTx, + lnwallet.CheckReservedValueTxReq{ + Tx: sweepTxPkg.SweepTx, + }, ) return err }) @@ -1292,7 +1301,9 @@ func (r *rpcServer) SendCoins(ctx context.Context, // Sanity check the new tx by re-doing the check. err = wallet.WithCoinSelectLock(func() error { _, err := wallet.CheckReservedValueTx( - sweepTxPkg.SweepTx, + lnwallet.CheckReservedValueTxReq{ + Tx: sweepTxPkg.SweepTx, + }, ) return err })