lnd/sweep/walletsweep_test.go
eugene fdcd726f9a
multi: replace DefaultDustLimit with script-specific DustLimitForSize
This commit updates call-sites to use the proper dust limits for
various script types. This also updates the default dust limit used
in the funding flow to be 354 satoshis instead of 573 satoshis.
2021-09-29 13:33:10 -04:00

396 lines
10 KiB
Go

package sweep
import (
"bytes"
"fmt"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// TestDetermineFeePerKw tests that given a fee preference, the
// DetermineFeePerKw will properly map it to a concrete fee in sat/kw.
func TestDetermineFeePerKw(t *testing.T) {
t.Parallel()
defaultFee := chainfee.SatPerKWeight(999)
relayFee := chainfee.SatPerKWeight(300)
feeEstimator := newMockFeeEstimator(defaultFee, relayFee)
// We'll populate two items in the internal map which is used to query
// a fee based on a confirmation target: the default conf target, and
// an arbitrary conf target. We'll ensure below that both of these are
// properly
feeEstimator.blocksToFee[50] = 300
feeEstimator.blocksToFee[defaultNumBlocksEstimate] = 1000
testCases := []struct {
// feePref is the target fee preference for this case.
feePref FeePreference
// fee is the value the DetermineFeePerKw should return given
// the FeePreference above
fee chainfee.SatPerKWeight
// fail determines if this test case should fail or not.
fail bool
}{
// A fee rate below the fee rate floor should output the floor.
{
feePref: FeePreference{
FeeRate: chainfee.SatPerKWeight(99),
},
fee: chainfee.FeePerKwFloor,
},
// A fee rate above the floor, should pass through and return
// the target fee rate.
{
feePref: FeePreference{
FeeRate: 900,
},
fee: 900,
},
// A specified confirmation target should cause the function to
// query the estimator which will return our value specified
// above.
{
feePref: FeePreference{
ConfTarget: 50,
},
fee: 300,
},
// If the caller doesn't specify any values at all, then we
// should query for the default conf target.
{
feePref: FeePreference{},
fee: 1000,
},
// Both conf target and fee rate are set, we should return with
// an error.
{
feePref: FeePreference{
ConfTarget: 50,
FeeRate: 90000,
},
fee: 300,
fail: true,
},
}
for i, testCase := range testCases {
targetFee, err := DetermineFeePerKw(
feeEstimator, testCase.feePref,
)
switch {
case testCase.fail && err != nil:
continue
case testCase.fail && err == nil:
t.Fatalf("expected failure for #%v", i)
case !testCase.fail && err != nil:
t.Fatalf("unable to estimate fee; %v", err)
}
if targetFee != testCase.fee {
t.Fatalf("#%v: wrong fee: expected %v got %v", i,
testCase.fee, targetFee)
}
}
}
type mockUtxoSource struct {
outputs []*lnwallet.Utxo
}
func newMockUtxoSource(utxos []*lnwallet.Utxo) *mockUtxoSource {
return &mockUtxoSource{
outputs: utxos,
}
}
func (m *mockUtxoSource) ListUnspentWitnessFromDefaultAccount(minConfs int32,
maxConfs int32) ([]*lnwallet.Utxo, error) {
return m.outputs, nil
}
type mockCoinSelectionLocker struct {
fail bool
}
func (m *mockCoinSelectionLocker) WithCoinSelectLock(f func() error) error {
if err := f(); err != nil {
return err
}
if m.fail {
return fmt.Errorf("kek")
}
return nil
}
type mockOutpointLocker struct {
lockedOutpoints map[wire.OutPoint]struct{}
unlockedOutpoints map[wire.OutPoint]struct{}
}
func newMockOutpointLocker() *mockOutpointLocker {
return &mockOutpointLocker{
lockedOutpoints: make(map[wire.OutPoint]struct{}),
unlockedOutpoints: make(map[wire.OutPoint]struct{}),
}
}
func (m *mockOutpointLocker) LockOutpoint(o wire.OutPoint) {
m.lockedOutpoints[o] = struct{}{}
}
func (m *mockOutpointLocker) UnlockOutpoint(o wire.OutPoint) {
m.unlockedOutpoints[o] = struct{}{}
}
var sweepScript = []byte{
0x0, 0x14, 0x64, 0x3d, 0x8b, 0x15, 0x69, 0x4a, 0x54,
0x7d, 0x57, 0x33, 0x6e, 0x51, 0xdf, 0xfd, 0x38, 0xe3,
0xe, 0x6e, 0xf8, 0xef,
}
var deliveryAddr = func() btcutil.Address {
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
sweepScript, &chaincfg.TestNet3Params,
)
if err != nil {
panic(err)
}
return addrs[0]
}()
var testUtxos = []*lnwallet.Utxo{
{
// A p2wkh output.
AddressType: lnwallet.WitnessPubKey,
PkScript: []byte{
0x0, 0x14, 0x64, 0x3d, 0x8b, 0x15, 0x69, 0x4a, 0x54,
0x7d, 0x57, 0x33, 0x6e, 0x51, 0xdf, 0xfd, 0x38, 0xe3,
0xe, 0x6e, 0xf7, 0xef,
},
Value: 1000,
OutPoint: wire.OutPoint{
Index: 1,
},
},
{
// A np2wkh output.
AddressType: lnwallet.NestedWitnessPubKey,
PkScript: []byte{
0xa9, 0x14, 0x97, 0x17, 0xf7, 0xd1, 0x5f, 0x6f, 0x8b,
0x7, 0xe3, 0x58, 0x43, 0x19, 0xb9, 0x7e, 0xa9, 0x20,
0x18, 0xc3, 0x17, 0xd7, 0x87,
},
Value: 2000,
OutPoint: wire.OutPoint{
Index: 2,
},
},
// A p2wsh output.
{
AddressType: lnwallet.UnknownAddressType,
PkScript: []byte{
0x0, 0x20, 0x70, 0x1a, 0x8d, 0x40, 0x1c, 0x84, 0xfb, 0x13,
0xe6, 0xba, 0xf1, 0x69, 0xd5, 0x96, 0x84, 0xe2, 0x7a, 0xbd,
0x9f, 0xa2, 0x16, 0xc8, 0xbc, 0x5b, 0x9f, 0xc6, 0x3d, 0x62,
0x2f, 0xf8, 0xc5, 0x8c,
},
Value: 3000,
OutPoint: wire.OutPoint{
Index: 3,
},
},
}
func assertUtxosLocked(t *testing.T, utxoLocker *mockOutpointLocker,
utxos []*lnwallet.Utxo) {
t.Helper()
for _, utxo := range utxos {
if _, ok := utxoLocker.lockedOutpoints[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never locked", utxo.OutPoint)
}
}
}
func assertNoUtxosUnlocked(t *testing.T, utxoLocker *mockOutpointLocker,
utxos []*lnwallet.Utxo) {
t.Helper()
if len(utxoLocker.unlockedOutpoints) != 0 {
t.Fatalf("outputs have been locked, but shouldn't have been")
}
}
func assertUtxosUnlocked(t *testing.T, utxoLocker *mockOutpointLocker,
utxos []*lnwallet.Utxo) {
t.Helper()
for _, utxo := range utxos {
if _, ok := utxoLocker.unlockedOutpoints[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never unlocked", utxo.OutPoint)
}
}
}
func assertUtxosLockedAndUnlocked(t *testing.T, utxoLocker *mockOutpointLocker,
utxos []*lnwallet.Utxo) {
t.Helper()
for _, utxo := range utxos {
if _, ok := utxoLocker.lockedOutpoints[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never locked", utxo.OutPoint)
}
if _, ok := utxoLocker.unlockedOutpoints[utxo.OutPoint]; !ok {
t.Fatalf("utxo %v was never unlocked", utxo.OutPoint)
}
}
}
// TestCraftSweepAllTxCoinSelectFail tests that if coin selection fails, then
// we unlock any outputs we may have locked in the passed closure.
func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
t.Parallel()
utxoSource := newMockUtxoSource(testUtxos)
coinSelectLocker := &mockCoinSelectionLocker{
fail: true,
}
utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx(
0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLocker, nil,
nil, 0,
)
// Since we instructed the coin select locker to fail above, we should
// get an error.
if err == nil {
t.Fatalf("sweep tx should have failed: %v", err)
}
// At this point, we'll now verify that all outputs were initially
// locked, and then also unlocked due to the failure.
assertUtxosLockedAndUnlocked(t, utxoLocker, testUtxos)
}
// TestCraftSweepAllTxUnknownWitnessType tests that if one of the inputs we
// encounter is of an unknown witness type, then we fail and unlock any prior
// locked outputs.
func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
t.Parallel()
utxoSource := newMockUtxoSource(testUtxos)
coinSelectLocker := &mockCoinSelectionLocker{}
utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx(
0, 10, nil, nil, coinSelectLocker, utxoSource, utxoLocker, nil,
nil, 0,
)
// Since passed in a p2wsh output, which is unknown, we should fail to
// map the output to a witness type.
if err == nil {
t.Fatalf("sweep tx should have failed: %v", err)
}
// At this point, we'll now verify that all outputs were initially
// locked, and then also unlocked since we weren't able to find a
// witness type for the last output.
assertUtxosLockedAndUnlocked(t, utxoLocker, testUtxos)
}
// TestCraftSweepAllTx tests that we'll properly lock all available outputs
// within the wallet, and craft a single sweep transaction that pays to the
// target output.
func TestCraftSweepAllTx(t *testing.T) {
t.Parallel()
// First, we'll make a mock signer along with a fee estimator, We'll
// use zero fees to we can assert a precise output value.
signer := &mock.DummySigner{}
feeEstimator := newMockFeeEstimator(0, 0)
// For our UTXO source, we'll pass in all the UTXOs that we know of,
// other than the final one which is of an unknown witness type.
targetUTXOs := testUtxos[:2]
utxoSource := newMockUtxoSource(targetUTXOs)
coinSelectLocker := &mockCoinSelectionLocker{}
utxoLocker := newMockOutpointLocker()
sweepPkg, err := CraftSweepAllTx(
0, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
utxoLocker, feeEstimator, signer, 0,
)
if err != nil {
t.Fatalf("unable to make sweep tx: %v", err)
}
// At this point, all of the UTXOs that we made above should be locked
// and none of them unlocked.
assertUtxosLocked(t, utxoLocker, testUtxos[:2])
assertNoUtxosUnlocked(t, utxoLocker, testUtxos[:2])
// Now that we have our sweep transaction, we should find that we have
// a UTXO for each input, and also that our final output value is the
// sum of all our inputs.
sweepTx := sweepPkg.SweepTx
if len(sweepTx.TxIn) != len(targetUTXOs) {
t.Fatalf("expected %v utxo, got %v", len(targetUTXOs),
len(sweepTx.TxIn))
}
// We should have a single output that pays to our sweep script
// generated above.
expectedSweepValue := int64(3000)
if len(sweepTx.TxOut) != 1 {
t.Fatalf("should have %v outputs, instead have %v", 1,
len(sweepTx.TxOut))
}
output := sweepTx.TxOut[0]
switch {
case output.Value != expectedSweepValue:
t.Fatalf("expected %v sweep value, instead got %v",
expectedSweepValue, output.Value)
case !bytes.Equal(sweepScript, output.PkScript):
t.Fatalf("expected %x sweep script, instead got %x", sweepScript,
output.PkScript)
}
// If we cancel the sweep attempt, then we should find that all the
// UTXOs within the sweep transaction are now unlocked.
sweepPkg.CancelSweepAttempt()
assertUtxosUnlocked(t, utxoLocker, testUtxos[:2])
}