walletrpc+sweep: refactor BumpFee to properly handle sweep request
This commit is contained in:
3 files changed
+204
-79
No files matched your search
@@ -30,6 +30,8 @@ import (
|
|||||||
base "github.com/btcsuite/btcwallet/wallet"
|
base "github.com/btcsuite/btcwallet/wallet"
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
|
"github.com/lightningnetwork/lnd/contractcourt"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/labels"
|
"github.com/lightningnetwork/lnd/labels"
|
||||||
@@ -951,6 +953,131 @@ func UnmarshallOutPoint(op *lnrpc.OutPoint) (*wire.OutPoint, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateBumpFeeRequest makes sure the deprecated fields are not used when
|
||||||
|
// the new fields are set.
|
||||||
|
func validateBumpFeeRequest(in *BumpFeeRequest) (
|
||||||
|
fn.Option[chainfee.SatPerKWeight], bool, error) {
|
||||||
|
|
||||||
|
// Get the specified fee rate if set.
|
||||||
|
satPerKwOpt := fn.None[chainfee.SatPerKWeight]()
|
||||||
|
|
||||||
|
// We only allow using either the deprecated field or the new field.
|
||||||
|
switch {
|
||||||
|
case in.SatPerByte != 0 && in.SatPerVbyte != 0:
|
||||||
|
return satPerKwOpt, false, fmt.Errorf("either SatPerByte or " +
|
||||||
|
"SatPerVbyte should be set, but not both")
|
||||||
|
|
||||||
|
case in.SatPerByte != 0:
|
||||||
|
satPerKw := chainfee.SatPerVByte(
|
||||||
|
in.SatPerByte,
|
||||||
|
).FeePerKWeight()
|
||||||
|
satPerKwOpt = fn.Some(satPerKw)
|
||||||
|
|
||||||
|
case in.SatPerVbyte != 0:
|
||||||
|
satPerKw := chainfee.SatPerVByte(
|
||||||
|
in.SatPerVbyte,
|
||||||
|
).FeePerKWeight()
|
||||||
|
satPerKwOpt = fn.Some(satPerKw)
|
||||||
|
}
|
||||||
|
|
||||||
|
var immediate bool
|
||||||
|
switch {
|
||||||
|
case in.Force && in.Immediate:
|
||||||
|
return satPerKwOpt, false, fmt.Errorf("either Force or " +
|
||||||
|
"Immediate should be set, but not both")
|
||||||
|
|
||||||
|
case in.Force:
|
||||||
|
immediate = in.Force
|
||||||
|
|
||||||
|
case in.Immediate:
|
||||||
|
immediate = in.Immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
return satPerKwOpt, immediate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareSweepParams creates the sweep params to be used for the sweeper. It
|
||||||
|
// returns the new params and a bool indicating whether this is an existing
|
||||||
|
// input.
|
||||||
|
func (w *WalletKit) prepareSweepParams(in *BumpFeeRequest,
|
||||||
|
op wire.OutPoint, currentHeight int32) (sweep.Params, bool, error) {
|
||||||
|
|
||||||
|
// Return an error if both deprecated and new fields are used.
|
||||||
|
feerate, immediate, err := validateBumpFeeRequest(in)
|
||||||
|
if err != nil {
|
||||||
|
return sweep.Params{}, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current pending inputs.
|
||||||
|
inputMap, err := w.cfg.Sweeper.PendingInputs()
|
||||||
|
if err != nil {
|
||||||
|
return sweep.Params{}, false, fmt.Errorf("unable to get "+
|
||||||
|
"pending inputs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the pending input.
|
||||||
|
//
|
||||||
|
// TODO(yy): act differently based on the state of the input?
|
||||||
|
inp, ok := inputMap[op]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
// NOTE: if this input doesn't exist and the new budget is not
|
||||||
|
// specified, the params would have a zero budget.
|
||||||
|
params := sweep.Params{
|
||||||
|
Immediate: immediate,
|
||||||
|
StartingFeeRate: feerate,
|
||||||
|
Budget: btcutil.Amount(in.Budget),
|
||||||
|
}
|
||||||
|
if in.TargetConf != 0 {
|
||||||
|
params.DeadlineHeight = fn.Some(
|
||||||
|
int32(in.TargetConf) + currentHeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the existing budget used for this input. Note that this value
|
||||||
|
// must be greater than zero.
|
||||||
|
budget := inp.Params.Budget
|
||||||
|
|
||||||
|
// Set the new budget if specified.
|
||||||
|
if in.Budget != 0 {
|
||||||
|
budget = btcutil.Amount(in.Budget)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For an existing input, we assign it first, then overwrite it if
|
||||||
|
// a deadline is requested.
|
||||||
|
deadline := inp.Params.DeadlineHeight
|
||||||
|
|
||||||
|
// Set the deadline if target conf is specified.
|
||||||
|
//
|
||||||
|
// TODO(yy): upgrade `falafel` so we can make this field optional. Atm
|
||||||
|
// we cannot distinguish between user's not setting the field and
|
||||||
|
// setting it to 0.
|
||||||
|
if in.TargetConf != 0 {
|
||||||
|
deadline = fn.Some(int32(in.TargetConf) + currentHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the new sweep params.
|
||||||
|
//
|
||||||
|
// NOTE: if this input doesn't exist and the new budget is not
|
||||||
|
// specified, the params would have a zero budget.
|
||||||
|
params := sweep.Params{
|
||||||
|
Immediate: immediate,
|
||||||
|
StartingFeeRate: feerate,
|
||||||
|
DeadlineHeight: deadline,
|
||||||
|
Budget: budget,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
log.Infof("[BumpFee]: bumping fee for existing input=%v, old "+
|
||||||
|
"params=%v, new params=%v", op, inp.Params, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BumpFee allows bumping the fee rate of an arbitrary input. A fee preference
|
// BumpFee allows bumping the fee rate of an arbitrary input. A fee preference
|
||||||
// can be expressed either as a specific fee rate or a delta of blocks in which
|
// can be expressed either as a specific fee rate or a delta of blocks in which
|
||||||
// the output should be swept on-chain within. If a fee preference is not
|
// the output should be swept on-chain within. If a fee preference is not
|
||||||
@@ -965,67 +1092,82 @@ func (w *WalletKit) BumpFee(ctx context.Context,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only allow using either the deprecated field or the new field.
|
// Get the current height so we can calculate the deadline height.
|
||||||
if in.SatPerByte != 0 && in.SatPerVbyte != 0 {
|
_, currentHeight, err := w.cfg.Chain.GetBestBlock()
|
||||||
return nil, fmt.Errorf("either SatPerByte or " +
|
if err != nil {
|
||||||
"SatPerVbyte should be set, but not both")
|
return nil, fmt.Errorf("unable to retrieve current height: %w",
|
||||||
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the request's fee preference.
|
// We now create a new sweeping params and update it in the sweeper.
|
||||||
satPerKw := chainfee.SatPerKVByte(in.SatPerVbyte * 1000).FeePerKWeight()
|
// This will complicate the RBF conditions if this input has already
|
||||||
if in.SatPerByte != 0 {
|
// been offered to sweeper before and it has already been included in a
|
||||||
satPerKw = chainfee.SatPerKVByte(
|
// tx with other inputs. If this is the case, two results are possible:
|
||||||
in.SatPerByte * 1000,
|
// - either this input successfully RBFed the existing tx, or,
|
||||||
).FeePerKWeight()
|
// - the budget of this input was not enough to RBF the existing tx.
|
||||||
}
|
params, existing, err := w.prepareSweepParams(in, *op, currentHeight)
|
||||||
feePreference := sweep.FeeEstimateInfo{
|
if err != nil {
|
||||||
ConfTarget: uint32(in.TargetConf),
|
|
||||||
FeeRate: satPerKw,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll attempt to bump the fee of the input through the UtxoSweeper.
|
|
||||||
// If it is currently attempting to sweep the input, then it'll simply
|
|
||||||
// bump its fee, which will result in a replacement transaction (RBF)
|
|
||||||
// being broadcast. If it is not aware of the input however,
|
|
||||||
// lnwallet.ErrNotMine is returned.
|
|
||||||
params := sweep.Params{
|
|
||||||
Fee: feePreference,
|
|
||||||
Immediate: in.Immediate,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = w.cfg.Sweeper.UpdateParams(*op, params)
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
return &BumpFeeResponse{
|
|
||||||
Status: "Successfully registered rbf-tx with sweeper",
|
|
||||||
}, nil
|
|
||||||
case lnwallet.ErrNotMine:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Attempting to CPFP outpoint %s", op)
|
// If this input exists, we will update its params.
|
||||||
|
if existing {
|
||||||
|
_, err = w.cfg.Sweeper.UpdateParams(*op, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Since we're unable to perform a bump through RBF, we'll assume the
|
return &BumpFeeResponse{
|
||||||
// user is attempting to bump an unconfirmed transaction's fee rate by
|
Status: "Successfully registered rbf-tx with sweeper",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a new sweeping request for this input.
|
||||||
|
err = w.sweepNewInput(op, uint32(currentHeight), params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BumpFeeResponse{
|
||||||
|
Status: "Successfully registered CPFP-tx with the sweeper",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sweepNewInput handles the case where an input is seen the first time by the
|
||||||
|
// sweeper. It will fetch the output from the wallet and construct an input and
|
||||||
|
// offer it to the sweeper.
|
||||||
|
//
|
||||||
|
// NOTE: if the budget is not set, the default budget ratio is used.
|
||||||
|
func (w *WalletKit) sweepNewInput(op *wire.OutPoint, currentHeight uint32,
|
||||||
|
params sweep.Params) error {
|
||||||
|
|
||||||
|
log.Debugf("Attempting to sweep outpoint %s", op)
|
||||||
|
|
||||||
|
// Since the sweeper is not aware of the input, we'll assume the user
|
||||||
|
// is attempting to bump an unconfirmed transaction's fee rate by
|
||||||
// sweeping an output within it under control of the wallet with a
|
// sweeping an output within it under control of the wallet with a
|
||||||
// higher fee rate, essentially performing a Child-Pays-For-Parent
|
// higher fee rate. In this case, this will be a CPFP.
|
||||||
// (CPFP).
|
|
||||||
//
|
//
|
||||||
// We'll gather all of the information required by the UtxoSweeper in
|
// We'll gather all of the information required by the UtxoSweeper in
|
||||||
// order to sweep the output.
|
// order to sweep the output.
|
||||||
utxo, err := w.cfg.Wallet.FetchInputInfo(op)
|
utxo, err := w.cfg.Wallet.FetchInputInfo(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're only able to bump the fee of unconfirmed transactions.
|
// We're only able to bump the fee of unconfirmed transactions.
|
||||||
if utxo.Confirmations > 0 {
|
if utxo.Confirmations > 0 {
|
||||||
return nil, errors.New("unable to bump fee of a confirmed " +
|
return errors.New("unable to bump fee of a confirmed " +
|
||||||
"transaction")
|
"transaction")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's no budget set, use the default value.
|
||||||
|
if params.Budget == 0 {
|
||||||
|
params.Budget = utxo.Value.MulF64(
|
||||||
|
contractcourt.DefaultBudgetRatio,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
signDesc := &input.SignDescriptor{
|
signDesc := &input.SignDescriptor{
|
||||||
Output: &wire.TxOut{
|
Output: &wire.TxOut{
|
||||||
PkScript: utxo.PkScript,
|
PkScript: utxo.PkScript,
|
||||||
@@ -1044,32 +1186,18 @@ func (w *WalletKit) BumpFee(ctx context.Context,
|
|||||||
witnessType = input.TaprootPubKeySpend
|
witnessType = input.TaprootPubKeySpend
|
||||||
signDesc.HashType = txscript.SigHashDefault
|
signDesc.HashType = txscript.SigHashDefault
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown input witness %v", op)
|
return fmt.Errorf("unknown input witness %v", op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll use the current height as the height hint since we're dealing
|
log.Infof("[BumpFee]: bumping fee for new input=%v, params=%v", op,
|
||||||
// with an unconfirmed transaction.
|
params)
|
||||||
_, currentHeight, err := w.cfg.Chain.GetBestBlock()
|
|
||||||
if err != nil {
|
inp := input.NewBaseInput(op, witnessType, signDesc, currentHeight)
|
||||||
return nil, fmt.Errorf("unable to retrieve current height: %w",
|
if _, err = w.cfg.Sweeper.SweepInput(inp, params); err != nil {
|
||||||
err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
inp := input.NewBaseInput(
|
return nil
|
||||||
op, witnessType, signDesc, uint32(currentHeight),
|
|
||||||
)
|
|
||||||
|
|
||||||
sweepParams := sweep.Params{
|
|
||||||
Fee: feePreference,
|
|
||||||
Immediate: in.Immediate,
|
|
||||||
}
|
|
||||||
if _, err = w.cfg.Sweeper.SweepInput(inp, sweepParams); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &BumpFeeResponse{
|
|
||||||
Status: "Successfully registered cpfp-tx with the sweeper",
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSweeps returns a list of the sweeps that our node has published.
|
// ListSweeps returns a list of the sweeps that our node has published.
|
||||||
|
|||||||
+4
-4
@@ -490,12 +490,12 @@ func (t *TxPublisher) createAndCheckTx(req *BumpRequest, f FeeFunction) (
|
|||||||
req.Inputs, req.DeliveryAddress, f.FeeRate(),
|
req.Inputs, req.DeliveryAddress, f.FeeRate(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("create sweep tx: %w", err)
|
return nil, fee, fmt.Errorf("create sweep tx: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check the budget still covers the fee.
|
// Sanity check the budget still covers the fee.
|
||||||
if fee > req.Budget {
|
if fee > req.Budget {
|
||||||
return nil, 0, fmt.Errorf("%w: budget=%v, fee=%v",
|
return nil, fee, fmt.Errorf("%w: budget=%v, fee=%v",
|
||||||
ErrNotEnoughBudget, req.Budget, fee)
|
ErrNotEnoughBudget, req.Budget, fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,8 +522,8 @@ func (t *TxPublisher) createAndCheckTx(req *BumpRequest, f FeeFunction) (
|
|||||||
return tx, fee, nil
|
return tx, fee, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, 0, fmt.Errorf("tx=%v failed mempool check: %w", tx.TxHash(),
|
return nil, fee, fmt.Errorf("tx=%v failed mempool check: %w",
|
||||||
err)
|
tx.TxHash(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// broadcast takes a monitored tx and publishes it to the network. Prior to the
|
// broadcast takes a monitored tx and publishes it to the network. Prior to the
|
||||||
|
|||||||
+8
-11
@@ -1087,14 +1087,6 @@ func (s *UtxoSweeper) handlePendingSweepsReq(
|
|||||||
func (s *UtxoSweeper) UpdateParams(input wire.OutPoint,
|
func (s *UtxoSweeper) UpdateParams(input wire.OutPoint,
|
||||||
params Params) (chan Result, error) {
|
params Params) (chan Result, error) {
|
||||||
|
|
||||||
// Ensure the client provided a sane fee preference.
|
|
||||||
_, err := params.Fee.Estimate(
|
|
||||||
s.cfg.FeeEstimator, s.cfg.MaxFeeRate.FeePerKWeight(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
responseChan := make(chan *updateResp, 1)
|
responseChan := make(chan *updateResp, 1)
|
||||||
select {
|
select {
|
||||||
case s.updateReqs <- &updateReq{
|
case s.updateReqs <- &updateReq{
|
||||||
@@ -1140,9 +1132,14 @@ func (s *UtxoSweeper) handleUpdateReq(req *updateReq) (
|
|||||||
|
|
||||||
// Create the updated parameters struct. Leave the exclusive group
|
// Create the updated parameters struct. Leave the exclusive group
|
||||||
// unchanged.
|
// unchanged.
|
||||||
newParams := sweeperInput.params
|
newParams := Params{
|
||||||
newParams.Fee = req.params.Fee
|
Fee: req.params.Fee,
|
||||||
newParams.Immediate = req.params.Immediate
|
StartingFeeRate: req.params.StartingFeeRate,
|
||||||
|
Immediate: req.params.Immediate,
|
||||||
|
Budget: req.params.Budget,
|
||||||
|
DeadlineHeight: req.params.DeadlineHeight,
|
||||||
|
ExclusiveGroup: sweeperInput.params.ExclusiveGroup,
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("Updating parameters for %v(state=%v) from (%v) to (%v)",
|
log.Debugf("Updating parameters for %v(state=%v) from (%v) to (%v)",
|
||||||
req.input, sweeperInput.state, sweeperInput.params, newParams)
|
req.input, sweeperInput.state, sweeperInput.params, newParams)
|
||||||
|
|||||||
Reference in new issue
Block a user