loop/sweepbatcher/sweep_batch_test.go

707 lines
14 KiB
Go
Raw Permalink Normal View History

package sweepbatcher
import (
"fmt"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
// TestConstructUnsignedTx verifies that the function constructUnsignedTx
// correctly creates unsigned transactions.
func TestConstructUnsignedTx(t *testing.T) {
// Prepare the necessary data for test cases.
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2, 2},
Index: 2,
}
op3 := wire.OutPoint{
Hash: chainhash.Hash{3, 3, 3},
Index: 3,
}
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
p2trAddr := "bcrt1pa38tp2hgjevqv3jcsxeu7v72n0s5a3ck8q2u8r" +
"k6mm67dv7uk26qq8je7e"
p2trAddress, err := btcutil.DecodeAddress(p2trAddr, nil)
require.NoError(t, err)
p2trPkScript, err := txscript.PayToAddrScript(p2trAddress)
require.NoError(t, err)
change1Addr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" +
"lwyyeq5ryn6r"
change1Address, err := btcutil.DecodeAddress(change1Addr, nil)
require.NoError(t, err)
change1Pkscript, err := txscript.PayToAddrScript(change1Address)
require.NoError(t, err)
change1 := &wire.TxOut{
Value: 100_000,
PkScript: change1Pkscript,
}
change1PrimeAddr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" +
"lwyyeq5ryn6r"
change1PrimeAddress, err := btcutil.DecodeAddress(change1PrimeAddr, nil)
require.NoError(t, err)
change1PrimePkscript, err := txscript.PayToAddrScript(change1PrimeAddress)
require.NoError(t, err)
change1Prime := &wire.TxOut{
Value: 200_000,
PkScript: change1PrimePkscript,
}
change2Addr := "bc1psw0nrrulq4pgyuyk09a3wsutygltys4gxjjw3zl2uz4ep8pa" +
"r2vsvntfe0"
change2Address, err := btcutil.DecodeAddress(change2Addr, nil)
require.NoError(t, err)
change2Pkscript, err := txscript.PayToAddrScript(change2Address)
require.NoError(t, err)
change2 := &wire.TxOut{
Value: 200_000,
PkScript: change2Pkscript,
}
serializedPubKey := []byte{
0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95,
0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03,
0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca,
0x52, 0xc6, 0xb4,
}
p2pkAddress, err := btcutil.NewAddressPubKey(
serializedPubKey, &chaincfg.RegressionNetParams,
)
require.NoError(t, err)
swapHash := lntypes.Hash{1, 1, 1}
swapContract := &loopdb.SwapContract{
CltvExpiry: 222,
AmountRequested: 2_000_000,
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
HtlcKeys: htlcKeys,
}
htlc, err := utils.GetHtlc(
swapHash, swapContract, &chaincfg.RegressionNetParams,
)
require.NoError(t, err)
estimator := htlc.AddSuccessToEstimator
brokenEstimator := func(*input.TxWeightEstimator) error {
return fmt.Errorf("weight estimator test failure")
}
dustLimit := utils.DustLimitForPkScript(batchPkScript)
cases := []struct {
name string
sweeps []sweep
address btcutil.Address
currentHeight int32
feeRate chainfee.SatPerKWeight
minRelayFeeRate chainfee.SatPerKWeight
wantErr string
wantTx *wire.MsgTx
wantWeight lntypes.WeightUnit
wantFeeForWeight btcutil.Amount
wantFee btcutil.Amount
}{
{
name: "no sweeps error",
wantErr: "no sweeps in batch",
},
{
name: "two coop sweeps",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
address: destAddr,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999374,
PkScript: batchPkScript,
},
},
},
wantWeight: 626,
wantFeeForWeight: 626,
wantFee: 626,
},
{
name: "p2tr destination address",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999326,
PkScript: p2trPkScript,
},
},
},
wantWeight: 674,
wantFeeForWeight: 674,
wantFee: 674,
},
{
name: "unknown kind of address",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
address: nil,
wantErr: "unsupported address type",
},
{
name: "pay-to-pubkey address",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
address: p2pkAddress,
wantErr: "unknown address type",
},
{
name: "fee more than 20% clamped",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
address: destAddr,
currentHeight: 800_000,
feeRate: 1_000_000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2_400_000,
PkScript: batchPkScript,
},
},
},
wantWeight: 626,
wantFeeForWeight: 626_000,
wantFee: 600_000,
},
{
name: "coop and noncoop",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
nonCoopHint: true,
htlc: *htlc,
htlcSuccessEstimator: estimator,
},
},
address: destAddr,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
Sequence: 1,
},
},
TxOut: []*wire.TxOut{
{
Value: 2_999_211,
PkScript: batchPkScript,
},
},
},
wantWeight: 789,
wantFeeForWeight: 789,
wantFee: 789,
},
{
name: "single sweep with change",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
change: change1,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
},
TxOut: []*wire.TxOut{
{
Value: 899_384,
PkScript: p2trPkScript,
},
{
Value: change1.Value,
PkScript: change1.PkScript,
},
},
},
wantWeight: 616,
wantFeeForWeight: 616,
wantFee: 616,
},
{
name: "all sweeps different change outputs",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
change: change1,
},
{
outpoint: op3,
value: 3_000_000,
change: change2,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
{
PreviousOutPoint: op3,
},
},
TxOut: []*wire.TxOut{
{
Value: 5_698_752,
PkScript: p2trPkScript,
},
{
Value: change1.Value,
PkScript: change1.PkScript,
},
{
Value: change2.Value,
PkScript: change2.PkScript,
},
},
},
wantWeight: 1248,
wantFeeForWeight: 1248,
wantFee: 1248,
},
{
name: "identical change pkscripts",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
change: change1,
},
{
outpoint: op3,
value: 3_000_000,
change: change1,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
{
PreviousOutPoint: op3,
},
},
TxOut: []*wire.TxOut{
{
Value: 5_798_924,
PkScript: p2trPkScript,
},
{
Value: 2 * change1.Value,
PkScript: change1.PkScript,
},
},
},
wantWeight: 1076,
wantFeeForWeight: 1076,
wantFee: 1076,
},
{
name: "identical change pkscripts different values",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
change: change1,
},
{
outpoint: op3,
value: 3_000_000,
change: change1Prime,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
{
PreviousOutPoint: op3,
},
},
TxOut: []*wire.TxOut{
{
Value: 5_698_924,
PkScript: p2trPkScript,
},
{
Value: change1.Value + change1Prime.Value,
PkScript: change1.PkScript,
},
},
},
wantWeight: 1076,
wantFeeForWeight: 1076,
wantFee: 1076,
},
{
name: "change exceeds input value",
sweeps: []sweep{
{
outpoint: op2,
value: btcutil.Amount(change1.Value - 1),
change: change1,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op2,
},
},
TxOut: []*wire.TxOut{
{
Value: change1.Value,
PkScript: change1.PkScript,
},
},
},
wantErr: "batch amount 0.00099999 BTC is <= the sum " +
"of change outputs 0.00100000 BTC",
},
{
name: "main output dust, batch amount less than " +
"change+fee+dust",
sweeps: []sweep{
{
outpoint: op1,
value: dustLimit,
},
{
outpoint: op2,
value: btcutil.Amount(change1.Value),
change: change1,
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1_000,
minRelayFeeRate: 50,
wantErr: "batch amount 0.00100294 BTC is < the sum " +
"of change outputs 0.00100000 BTC plus fee " +
"0.00000058 BTC and dust limit 0.00000330 BTC",
},
{
name: "change output is dust",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
change: &wire.TxOut{
Value: int64(dustLimit - 1),
PkScript: []byte{0xaf, 0xfe},
},
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 1000,
wantErr: "output 0.00000293 BTC is below dust limit " +
"0.00000477 BTC",
},
{
name: "clamp fee to max fee to swap amount ratio",
sweeps: []sweep{
{
outpoint: op1,
value: btcutil.SatoshiPerBitcoin,
change: &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin * 0.9,
PkScript: change1Pkscript,
},
},
},
address: p2trAddress,
currentHeight: 800_000,
feeRate: 10000000,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
},
TxOut: []*wire.TxOut{
{
Value: btcutil.SatoshiPerBitcoin * 0.08,
PkScript: p2trPkScript,
},
{
Value: btcutil.SatoshiPerBitcoin * 0.9,
PkScript: change1.PkScript,
},
},
},
wantWeight: 616,
wantFeeForWeight: 6_160_000,
wantFee: 2_000_000,
},
{
name: "weight estimator fails",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
nonCoopHint: true,
htlc: *htlc,
htlcSuccessEstimator: brokenEstimator,
},
},
address: destAddr,
currentHeight: 800_000,
feeRate: 1000,
wantErr: "sweep.htlcSuccessEstimator failed: " +
"weight estimator test failure",
},
{
name: "fix fee rounding",
sweeps: []sweep{
{
outpoint: op1,
value: 1_000_000,
},
{
outpoint: op2,
value: 2_000_000,
},
},
address: destAddr,
currentHeight: 800_000,
feeRate: 253,
wantTx: &wire.MsgTx{
Version: 2,
LockTime: 800_000,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: op1,
},
{
PreviousOutPoint: op2,
},
},
TxOut: []*wire.TxOut{
{
Value: 2999841,
PkScript: batchPkScript,
},
},
},
wantWeight: 626,
wantFeeForWeight: 159,
wantFee: 159,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
relayFeeRate := chainfee.FeePerKwFloor
if tc.minRelayFeeRate != 0 {
relayFeeRate = tc.minRelayFeeRate
}
tx, weight, feeForW, fee, err := constructUnsignedTx(
tc.sweeps, tc.address, tc.currentHeight,
tc.feeRate, relayFeeRate,
)
if tc.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
require.Equal(t, tc.wantTx, tx)
require.Equal(t, tc.wantWeight, weight)
require.Equal(t, tc.wantFeeForWeight, feeForW)
require.Equal(t, tc.wantFee, fee)
}
})
}
}