2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-08 01:10:29 +00:00
loop/sweepbatcher/greedy_batch_selection_test.go
Boris Nagaev db5e0d1d27
sweepbatcher: fix inaccuracies in fee estimations
Use SigHashDefault instead of SigHashAll in weight estimations.
Actual code uses SigHashDefault, not SigHashAll. SigHashAll is 1wu larger.

Use the actual destination address in weight estimator, not taproot always.
In the test the type of address is P2WPKH, not taproot.

Updated testSweepFetcher to calculate fee, fee rate and weight accurately and
to compare the data observed in the published transaction with the estimates.
2024-07-18 13:38:57 -03:00

699 lines
17 KiB
Go

package sweepbatcher
import (
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
// Useful constants for tests.
const (
lowFeeRate = chainfee.FeePerKwFloor
highFeeRate = chainfee.SatPerKWeight(30000)
coopInputWeight = lntypes.WeightUnit(230)
nonCoopInputWeight = lntypes.WeightUnit(521)
nonCoopPenalty = nonCoopInputWeight - coopInputWeight
coopNewBatchWeight = lntypes.WeightUnit(444)
nonCoopNewBatchWeight = coopNewBatchWeight + nonCoopPenalty
// p2pkhDiscount is weight discount P2PKH output has over P2TR output.
p2pkhDiscount = lntypes.WeightUnit(
input.P2TROutputSize-input.P2PKHOutputSize,
) * 4
coopTwoSweepBatchWeight = coopNewBatchWeight + coopInputWeight
nonCoopTwoSweepBatchWeight = coopTwoSweepBatchWeight +
2*nonCoopPenalty
v2v3BatchWeight = nonCoopTwoSweepBatchWeight - 153
)
// testHtlcV2SuccessEstimator adds weight of non-cooperative input to estimator
// using HTLC v2.
func testHtlcV2SuccessEstimator(estimator *input.TxWeightEstimator) error {
swapHash := lntypes.Hash{1, 1, 1}
htlc, err := swap.NewHtlcV2(
111, htlcKeys.SenderScriptKey, htlcKeys.ReceiverScriptKey,
swapHash, &chaincfg.RegressionNetParams,
)
if err != nil {
return err
}
return htlc.AddSuccessToEstimator(estimator)
}
// testHtlcV3SuccessEstimator adds weight of non-cooperative input to estimator
// using HTLC v3.
func testHtlcV3SuccessEstimator(estimator *input.TxWeightEstimator) error {
swapHash := lntypes.Hash{1, 1, 1}
htlc, err := swap.NewHtlcV3(
input.MuSig2Version100RC2, 111,
htlcKeys.SenderInternalPubKey, htlcKeys.ReceiverInternalPubKey,
htlcKeys.SenderScriptKey, htlcKeys.ReceiverScriptKey, swapHash,
&chaincfg.RegressionNetParams,
)
if err != nil {
return err
}
return htlc.AddSuccessToEstimator(estimator)
}
// TestEstimateSweepFeeIncrement tests that weight and fee estimations work
// correctly for a sweep and one sweep batch.
func TestEstimateSweepFeeIncrement(t *testing.T) {
// Useful variables reused in test cases.
se3 := testHtlcV3SuccessEstimator
trAddr := (*btcutil.AddressTaproot)(nil)
p2pkhAddr := (*btcutil.AddressPubKeyHash)(nil)
cases := []struct {
name string
sweep *sweep
wantSweepFeeDetails feeDetails
wantNewBatchFeeDetails feeDetails
}{
{
name: "regular",
sweep: &sweep{
minFeeRate: lowFeeRate,
htlcSuccessEstimator: se3,
},
wantSweepFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
wantNewBatchFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
{
name: "high fee rate",
sweep: &sweep{
minFeeRate: highFeeRate,
htlcSuccessEstimator: se3,
},
wantSweepFeeDetails: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
wantNewBatchFeeDetails: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
{
name: "isExternalAddr taproot",
sweep: &sweep{
minFeeRate: lowFeeRate,
htlcSuccessEstimator: se3,
isExternalAddr: true,
destAddr: trAddr,
},
wantSweepFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
IsExternalAddr: true,
},
wantNewBatchFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
IsExternalAddr: true,
},
},
{
name: "isExternalAddr P2PKH",
sweep: &sweep{
minFeeRate: lowFeeRate,
htlcSuccessEstimator: se3,
isExternalAddr: true,
destAddr: p2pkhAddr,
},
wantSweepFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
IsExternalAddr: true,
},
wantNewBatchFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight -
p2pkhDiscount,
NonCoopWeight: nonCoopNewBatchWeight -
p2pkhDiscount,
IsExternalAddr: true,
},
},
{
name: "non-coop",
sweep: &sweep{
minFeeRate: lowFeeRate,
htlcSuccessEstimator: se3,
nonCoopHint: true,
},
wantSweepFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
NonCoopHint: true,
},
wantNewBatchFeeDetails: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
gotSweepFeeDetails, gotNewBatchFeeDetails, err :=
estimateSweepFeeIncrement(tc.sweep)
require.NoError(t, err)
require.Equal(
t, tc.wantSweepFeeDetails, gotSweepFeeDetails,
)
require.Equal(
t, tc.wantNewBatchFeeDetails,
gotNewBatchFeeDetails,
)
})
}
}
// TestEstimateBatchWeight tests that weight and fee estimations work correctly
// for batches.
func TestEstimateBatchWeight(t *testing.T) {
// Useful variables reused in test cases.
swapHash1 := lntypes.Hash{1, 1, 1}
swapHash2 := lntypes.Hash{2, 2, 2}
se2 := testHtlcV2SuccessEstimator
se3 := testHtlcV3SuccessEstimator
trAddr := (*btcutil.AddressTaproot)(nil)
cases := []struct {
name string
batch *batch
wantBatchFeeDetails feeDetails
}{
{
name: "one sweep regular batch",
batch: &batch{
id: 1,
rbfCache: rbfCache{
FeeRate: lowFeeRate,
},
sweeps: map[lntypes.Hash]sweep{
swapHash1: {
htlcSuccessEstimator: se3,
},
},
},
wantBatchFeeDetails: feeDetails{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
{
name: "two sweeps regular batch",
batch: &batch{
id: 1,
rbfCache: rbfCache{
FeeRate: lowFeeRate,
},
sweeps: map[lntypes.Hash]sweep{
swapHash1: {
htlcSuccessEstimator: se3,
},
swapHash2: {
htlcSuccessEstimator: se3,
},
},
},
wantBatchFeeDetails: feeDetails{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopTwoSweepBatchWeight,
NonCoopWeight: nonCoopTwoSweepBatchWeight,
},
},
{
name: "v2 and v3 sweeps",
batch: &batch{
id: 1,
rbfCache: rbfCache{
FeeRate: lowFeeRate,
},
sweeps: map[lntypes.Hash]sweep{
swapHash1: {
htlcSuccessEstimator: se2,
},
swapHash2: {
htlcSuccessEstimator: se3,
},
},
},
wantBatchFeeDetails: feeDetails{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopTwoSweepBatchWeight,
NonCoopWeight: v2v3BatchWeight,
},
},
{
name: "high fee rate",
batch: &batch{
id: 1,
rbfCache: rbfCache{
FeeRate: highFeeRate,
},
sweeps: map[lntypes.Hash]sweep{
swapHash1: {
htlcSuccessEstimator: se3,
},
},
},
wantBatchFeeDetails: feeDetails{
BatchId: 1,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
{
name: "non-coop",
batch: &batch{
id: 1,
rbfCache: rbfCache{
FeeRate: lowFeeRate,
},
sweeps: map[lntypes.Hash]sweep{
swapHash1: {
htlcSuccessEstimator: se3,
},
swapHash2: {
htlcSuccessEstimator: se3,
nonCoopHint: true,
},
},
},
wantBatchFeeDetails: feeDetails{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopTwoSweepBatchWeight,
NonCoopWeight: nonCoopTwoSweepBatchWeight,
NonCoopHint: true,
},
},
{
name: "isExternalAddr",
batch: &batch{
id: 1,
rbfCache: rbfCache{
FeeRate: lowFeeRate,
},
sweeps: map[lntypes.Hash]sweep{
swapHash1: {
htlcSuccessEstimator: se3,
isExternalAddr: true,
destAddr: trAddr,
},
},
},
wantBatchFeeDetails: feeDetails{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
IsExternalAddr: true,
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
gotBatchFeeDetails, err := estimateBatchWeight(tc.batch)
require.NoError(t, err)
require.Equal(
t, tc.wantBatchFeeDetails, gotBatchFeeDetails,
)
})
}
}
// TestSelectBatches tests greedy batch selection algorithm.
func TestSelectBatches(t *testing.T) {
cases := []struct {
name string
batches []feeDetails
sweep, oneSweepBatch feeDetails
wantBestBatchesIds []int32
}{
{
name: "no existing batches",
batches: []feeDetails{},
sweep: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
oneSweepBatch: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchesIds: []int32{newBatchSignal},
},
{
name: "low fee sweep, low fee existing batch",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
oneSweepBatch: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchesIds: []int32{1, newBatchSignal},
},
{
name: "low fee sweep, high fee existing batch",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
oneSweepBatch: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchesIds: []int32{newBatchSignal, 1},
},
{
name: "low fee sweep, low + high fee existing batches",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
oneSweepBatch: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchesIds: []int32{1, newBatchSignal, 2},
},
{
name: "high fee sweep, low + high fee existing batches",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
oneSweepBatch: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchesIds: []int32{2, newBatchSignal, 1},
},
{
name: "high fee noncoop sweep",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
NonCoopHint: true,
},
oneSweepBatch: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchesIds: []int32{newBatchSignal, 2, 1},
},
{
name: "high fee noncoop sweep, high batch noncoop",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
},
sweep: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
NonCoopHint: true,
},
oneSweepBatch: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchesIds: []int32{2, newBatchSignal, 1},
},
{
name: "low fee noncoop sweep",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
NonCoopHint: true,
},
oneSweepBatch: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchesIds: []int32{newBatchSignal, 1, 2},
},
{
name: "low fee noncoop sweep, low batch noncoop",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
NonCoopHint: true,
},
oneSweepBatch: feeDetails{
FeeRate: lowFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
NonCoopHint: true,
},
wantBestBatchesIds: []int32{1, newBatchSignal, 2},
},
{
name: "external address sweep",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
},
sweep: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
IsExternalAddr: true,
},
oneSweepBatch: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
IsExternalAddr: true,
},
wantBestBatchesIds: []int32{newBatchSignal},
},
{
name: "external address batch",
batches: []feeDetails{
{
BatchId: 1,
FeeRate: highFeeRate - 1,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
{
BatchId: 2,
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
IsExternalAddr: true,
},
},
sweep: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopInputWeight,
NonCoopWeight: nonCoopInputWeight,
},
oneSweepBatch: feeDetails{
FeeRate: highFeeRate,
CoopWeight: coopNewBatchWeight,
NonCoopWeight: nonCoopNewBatchWeight,
},
wantBestBatchesIds: []int32{1, newBatchSignal},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
gotBestBatchesIds, err := selectBatches(
tc.batches, tc.sweep, tc.oneSweepBatch,
)
require.NoError(t, err)
require.Equal(
t, tc.wantBestBatchesIds, gotBestBatchesIds,
)
})
}
}