You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/sweepbatcher/greedy_batch_selection_test.go

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,
)
})
}
}