mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
multi: factor out sweep fee clamping function
This commit is contained in:
parent
9ee7a36f37
commit
0b53296aa8
6 changed files with 153 additions and 40 deletions
|
|
@ -317,9 +317,16 @@ func (f *FSM) BuildHTLCAction(ctx context.Context,
|
||||||
return f.handleErrorAndUnlockReservations(ctx, err)
|
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
|
// Now that our nonces are set, we can create and sign the htlc
|
||||||
// transaction.
|
// transaction.
|
||||||
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
|
htlcTx, err := f.InstantOut.createHtlcTransaction(
|
||||||
|
f.cfg.Network, minRelayFee,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f.handleErrorAndUnlockReservations(ctx, err)
|
return f.handleErrorAndUnlockReservations(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
@ -382,6 +389,11 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
|
||||||
return f.handleErrorAndUnlockReservations(ctx, err)
|
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(
|
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
|
||||||
ctx,
|
ctx,
|
||||||
&swapserverrpc.PushPreimageRequest{
|
&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
|
// Now that we have the sweepless sweep signatures we can build and
|
||||||
// publish the sweepless sweep transaction.
|
// publish the sweepless sweep transaction.
|
||||||
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
|
sweepTx, err := f.InstantOut.createSweeplessSweepTx(
|
||||||
|
feeRate, minRelayFee,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.LastActionError = err
|
f.LastActionError = err
|
||||||
return OnErrorPublishHtlc
|
return OnErrorPublishHtlc
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/lightninglabs/loop/instantout/reservation"
|
"github.com/lightninglabs/loop/instantout/reservation"
|
||||||
"github.com/lightninglabs/loop/loopdb"
|
"github.com/lightninglabs/loop/loopdb"
|
||||||
"github.com/lightninglabs/loop/swap"
|
"github.com/lightninglabs/loop/swap"
|
||||||
|
"github.com/lightninglabs/loop/utils"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
|
@ -145,8 +146,8 @@ func (i *InstantOut) getInputReservations() (InputReservations, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// createHtlcTransaction creates the htlc transaction for the instant out.
|
// createHtlcTransaction creates the htlc transaction for the instant out.
|
||||||
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params,
|
||||||
*wire.MsgTx, error) {
|
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
if network == nil {
|
if network == nil {
|
||||||
return nil, errors.New("no network provided")
|
return nil, errors.New("no network provided")
|
||||||
|
|
@ -170,7 +171,16 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
||||||
// Estimate the fee
|
// Estimate the fee
|
||||||
weight := htlcWeight(len(inputReservations))
|
weight := htlcWeight(len(inputReservations))
|
||||||
fee := i.htlcFeeRate.FeeForWeight(weight)
|
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 " +
|
return nil, errors.New("fee is higher than 20% of " +
|
||||||
"sweep value")
|
"sweep value")
|
||||||
}
|
}
|
||||||
|
|
@ -193,8 +203,8 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
||||||
|
|
||||||
// createSweeplessSweepTx creates the sweepless sweep transaction for the
|
// createSweeplessSweepTx creates the sweepless sweep transaction for the
|
||||||
// instant out.
|
// instant out.
|
||||||
func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
|
func (i *InstantOut) createSweeplessSweepTx(feerate,
|
||||||
*wire.MsgTx, error) {
|
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
inputReservations, err := i.getInputReservations()
|
inputReservations, err := i.getInputReservations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -214,7 +224,14 @@ func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
|
||||||
// Estimate the fee
|
// Estimate the fee
|
||||||
weight := sweeplessSweepWeight(len(inputReservations))
|
weight := sweeplessSweepWeight(len(inputReservations))
|
||||||
fee := feerate.FeeForWeight(weight)
|
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 " +
|
return nil, errors.New("fee is higher than 20% of " +
|
||||||
"sweep value")
|
"sweep value")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/lightninglabs/lndclient"
|
"github.com/lightninglabs/lndclient"
|
||||||
"github.com/lightninglabs/loop/fsm"
|
"github.com/lightninglabs/loop/fsm"
|
||||||
"github.com/lightninglabs/loop/staticaddr/script"
|
"github.com/lightninglabs/loop/staticaddr/script"
|
||||||
|
"github.com/lightninglabs/loop/utils"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -47,12 +48,25 @@ func (f *FSM) PublishDepositExpirySweepAction(ctx context.Context,
|
||||||
"estimation failed: %w", err))
|
"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()
|
weight := script.ExpirySpendWeight()
|
||||||
|
|
||||||
fee := feeRateEstimator.FeeForWeight(lntypes.WeightUnit(weight))
|
fee := feeRateEstimator.FeeForWeight(lntypes.WeightUnit(weight))
|
||||||
|
|
||||||
// We cap the fee at 20% of the deposit value.
|
// 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 " +
|
return f.HandleError(errors.New("fee is greater than 20% of " +
|
||||||
"the deposit value"))
|
"the deposit value"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,6 @@ const (
|
||||||
// transaction.
|
// transaction.
|
||||||
batchConfHeight = 3
|
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.
|
// MaxSweepsPerBatch is the maximum number of sweeps in a single batch.
|
||||||
// It is needed to prevent sweep tx from becoming non-standard. Max
|
// It is needed to prevent sweep tx from becoming non-standard. Max
|
||||||
// standard transaction is 400k wu, a non-cooperative input is 393 wu.
|
// 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.
|
// 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),
|
feeForWeight, batchAmt-btcutil.Amount(sumChange),
|
||||||
minRelayFeeRate, weight,
|
utils.MaxFeeToAmountRatio, minRelayFeeRate, weight,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
|
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)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
44
utils/fees.go
Normal file
44
utils/fees.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
53
utils/fees_test.go
Normal file
53
utils/fees_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue