diff --git a/instantout/actions.go b/instantout/actions.go index eb8a6415..d1c405fd 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -317,9 +317,16 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, return f.handleErrorAndUnlockReservations(ctx, err) } + minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx) + if err != nil { + return f.handleErrorAndUnlockReservations(ctx, err) + } + // Now that our nonces are set, we can create and sign the htlc // transaction. - htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network) + htlcTx, err := f.InstantOut.createHtlcTransaction( + f.cfg.Network, minRelayFee, + ) if err != nil { return f.handleErrorAndUnlockReservations(ctx, err) } @@ -382,6 +389,11 @@ func (f *FSM) PushPreimageAction(ctx context.Context, return f.handleErrorAndUnlockReservations(ctx, err) } + minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx) + if err != nil { + return f.handleErrorAndUnlockReservations(ctx, err) + } + pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage( ctx, &swapserverrpc.PushPreimageRequest{ @@ -400,7 +412,9 @@ func (f *FSM) PushPreimageAction(ctx context.Context, // Now that we have the sweepless sweep signatures we can build and // publish the sweepless sweep transaction. - sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate) + sweepTx, err := f.InstantOut.createSweeplessSweepTx( + feeRate, minRelayFee, + ) if err != nil { f.LastActionError = err return OnErrorPublishHtlc diff --git a/instantout/instantout.go b/instantout/instantout.go index 6c83958a..cfea1e60 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -18,6 +18,7 @@ import ( "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" @@ -145,8 +146,8 @@ func (i *InstantOut) getInputReservations() (InputReservations, error) { } // createHtlcTransaction creates the htlc transaction for the instant out. -func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) ( - *wire.MsgTx, error) { +func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params, + minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) { if network == nil { return nil, errors.New("no network provided") @@ -170,7 +171,16 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) ( // Estimate the fee weight := htlcWeight(len(inputReservations)) fee := i.htlcFeeRate.FeeForWeight(weight) - if fee > i.Value/5 { + + // We cap the fee at 20% of the deposit value. + _, clamped, err := utils.ClampSweepFee( + fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate, + weight, + ) + if err != nil { + return nil, err + } + if clamped { return nil, errors.New("fee is higher than 20% of " + "sweep value") } @@ -193,8 +203,8 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) ( // createSweeplessSweepTx creates the sweepless sweep transaction for the // instant out. -func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) ( - *wire.MsgTx, error) { +func (i *InstantOut) createSweeplessSweepTx(feerate, + minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) { inputReservations, err := i.getInputReservations() if err != nil { @@ -214,7 +224,14 @@ func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) ( // Estimate the fee weight := sweeplessSweepWeight(len(inputReservations)) fee := feerate.FeeForWeight(weight) - if fee > i.Value/5 { + _, clamped, err := utils.ClampSweepFee( + fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate, + weight, + ) + if err != nil { + return nil, err + } + if clamped { return nil, errors.New("fee is higher than 20% of " + "sweep value") } diff --git a/staticaddr/deposit/actions.go b/staticaddr/deposit/actions.go index 3347ec03..362417e7 100644 --- a/staticaddr/deposit/actions.go +++ b/staticaddr/deposit/actions.go @@ -11,6 +11,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/lntypes" ) @@ -47,12 +48,25 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context, "estimation failed: %w", err)) } + minRelayFeeRate, err := f.cfg.WalletKit.MinRelayFee(ctx) + if err != nil { + return f.HandleError(fmt.Errorf("timeout sweep min relay "+ + "query failed: %w", err)) + } + weight := script.ExpirySpendWeight() fee := feeRateEstimator.FeeForWeight(lntypes.WeightUnit(weight)) // We cap the fee at 20% of the deposit value. - if fee > f.deposit.Value/5 { + _, clamped, err := utils.ClampSweepFee( + fee, f.deposit.Value, utils.MaxFeeToAmountRatio, + minRelayFeeRate, lntypes.WeightUnit(weight), + ) + if err != nil { + return f.HandleError(err) + } + if clamped { return f.HandleError(errors.New("fee is greater than 20% of " + "the deposit value")) } diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go index fe91fd5a..9f4b99be 100644 --- a/sweepbatcher/sweep_batch.go +++ b/sweepbatcher/sweep_batch.go @@ -46,10 +46,6 @@ const ( // transaction. batchConfHeight = 3 - // maxFeeToSwapAmtRatio is the maximum fee to swap amount ratio that - // we allow for a batch transaction. - maxFeeToSwapAmtRatio = 0.2 - // MaxSweepsPerBatch is the maximum number of sweeps in a single batch. // It is needed to prevent sweep tx from becoming non-standard. Max // standard transaction is 400k wu, a non-cooperative input is 393 wu. @@ -1414,9 +1410,9 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address, } // Clamp the calculated fee to the max allowed fee amount for the batch. - fee, err := clampBatchFee( + fee, _, err := utils.ClampSweepFee( feeForWeight, batchAmt-btcutil.Amount(sumChange), - minRelayFeeRate, weight, + utils.MaxFeeToAmountRatio, minRelayFeeRate, weight, ) if err != nil { return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+ @@ -2644,28 +2640,3 @@ func (b *batch) persistConfirmedBatch(ctx context.Context, return b.store.ConfirmBatchWithSweeps(ctx, b.dbBatch(), sweeps) } - -// clampBatchFee takes the fee amount and total amount of the sweeps in the -// batch and makes sure the fee is not too high. If the fee is too high, it is -// clamped to the maximum allowed fee. If the clamped fee results in a fee rate -// below the minimum relay fee, an error is returned. -func clampBatchFee(fee btcutil.Amount, totalAmount btcutil.Amount, - minRelayFeeRate chainfee.SatPerKWeight, - weight lntypes.WeightUnit) (btcutil.Amount, error) { - - maxFeeAmount := btcutil.Amount(float64(totalAmount) * - maxFeeToSwapAmtRatio) - - clampedFee := fee - if fee > maxFeeAmount { - clampedFee = maxFeeAmount - } - - clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight) - if clampedFeeRate < minRelayFeeRate { - return 0, fmt.Errorf("clamped fee rate %v is less than "+ - "minimum relay fee %v", clampedFeeRate, minRelayFeeRate) - } - - return clampedFee, nil -} diff --git a/utils/fees.go b/utils/fees.go new file mode 100644 index 00000000..7d7ffb91 --- /dev/null +++ b/utils/fees.go @@ -0,0 +1,44 @@ +package utils + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +const ( + // MaxFeeToAmountRatio is the maximum fee to total amount ratio allowed + // for a sweep transaction. + MaxFeeToAmountRatio = 0.2 +) + +// ClampSweepFee caps a fee to a percentage of the provided total amount and +// verifies the resulting fee rate is not below the minimum relay fee. It +// returns the clamped fee, whether it was clamped, or an error if the clamped +// fee would fall below the minimum relay fee. +func ClampSweepFee(fee btcutil.Amount, totalAmount btcutil.Amount, + ratio float64, minRelayFeeRate chainfee.SatPerKWeight, + weight lntypes.WeightUnit) (btcutil.Amount, bool, error) { + + maxFeeAmount := btcutil.Amount(float64(totalAmount) * ratio) + + clampedFee := fee + clamped := false + if fee > maxFeeAmount { + clampedFee = maxFeeAmount + clamped = true + } + + if minRelayFeeRate > 0 { + clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight) + if clampedFeeRate < minRelayFeeRate { + return 0, clamped, fmt.Errorf("clamped fee rate %v is "+ + "less than minimum relay fee %v", + clampedFeeRate, minRelayFeeRate) + } + } + + return clampedFee, clamped, nil +} diff --git a/utils/fees_test.go b/utils/fees_test.go new file mode 100644 index 00000000..c0a1914b --- /dev/null +++ b/utils/fees_test.go @@ -0,0 +1,53 @@ +package utils + +import ( + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/stretchr/testify/require" +) + +// TestClampSweepFee verifies clamping still respects min relay fee +// requirements and reports clamping. +func TestClampSweepFee(t *testing.T) { + weight := lntypes.WeightUnit(400) + minRelay := chainfee.SatPerKWeight(253) + total := btcutil.Amount(100_000) + + // Fee below the clamp threshold and above min relay should pass + // through. + fee := chainfee.SatPerKWeight(1_000).FeeForWeight(weight) + clamped, clampedFlag, err := ClampSweepFee( + fee, total, MaxFeeToAmountRatio, minRelay, weight, + ) + require.NoError(t, err) + require.Equal(t, fee, clamped) + require.False(t, clampedFlag) + + // A clamped fee that would fall below min relay should error. + // The fee will be clamped to 20 sats. + fee = btcutil.Amount(10_000) + total = btcutil.Amount(100) + _, clampedFlag, err = ClampSweepFee( + fee, total, MaxFeeToAmountRatio, minRelay, weight, + ) + require.True(t, clampedFlag) + require.Error(t, err) + + // A fee above the clamp threshold should be clamped without error when + // still above min relay. The fee is 30% of total, will clamp to 20%. + fee = btcutil.Amount(30_000) + total = btcutil.Amount(100_000) + clamped, clampedFlag, err = ClampSweepFee( + fee, total, MaxFeeToAmountRatio, minRelay, weight, + ) + require.NoError(t, err) + require.True(t, clampedFlag) + + expectedClampedFee := btcutil.Amount( + float64(total) * MaxFeeToAmountRatio, + ) + require.Equal(t, expectedClampedFee, clamped) +}