2018-12-07 09:06:36 +01:00
|
|
|
|
package sweep
|
|
|
|
|
|
|
|
|
|
import (
|
2023-10-24 02:14:52 +02:00
|
|
|
|
"errors"
|
2018-12-07 09:06:36 +01:00
|
|
|
|
"os"
|
|
|
|
|
"runtime/pprof"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
2022-02-23 14:48:00 +01:00
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
2018-12-07 09:06:36 +01:00
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
2020-12-07 10:40:08 +01:00
|
|
|
|
"github.com/btcsuite/btcd/txscript"
|
2018-12-07 09:06:36 +01:00
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
|
"github.com/lightningnetwork/lnd/build"
|
2023-10-24 07:14:21 +02:00
|
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
2024-01-10 21:18:40 +01:00
|
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
2023-10-26 08:27:46 +02:00
|
|
|
|
"github.com/lightningnetwork/lnd/fn"
|
2019-01-16 15:47:43 +01:00
|
|
|
|
"github.com/lightningnetwork/lnd/input"
|
2018-12-07 09:06:36 +01:00
|
|
|
|
"github.com/lightningnetwork/lnd/keychain"
|
2023-10-26 08:27:46 +02:00
|
|
|
|
lnmock "github.com/lightningnetwork/lnd/lntest/mock"
|
2019-05-02 01:06:19 +02:00
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
2019-10-31 03:43:05 +01:00
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
2023-10-26 08:27:46 +02:00
|
|
|
|
"github.com/stretchr/testify/mock"
|
2020-09-04 11:28:17 +02:00
|
|
|
|
"github.com/stretchr/testify/require"
|
2018-12-07 09:06:36 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
testLog = build.NewSubLogger("SWPR_TEST", nil)
|
|
|
|
|
|
|
|
|
|
testMaxSweepAttempts = 3
|
|
|
|
|
|
|
|
|
|
testMaxInputsPerTx = 3
|
2019-05-02 01:06:19 +02:00
|
|
|
|
|
2023-11-03 12:20:10 +01:00
|
|
|
|
defaultFeePref = Params{Fee: FeeEstimateInfo{ConfTarget: 1}}
|
2018-12-07 09:06:36 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type sweeperTestContext struct {
|
|
|
|
|
t *testing.T
|
|
|
|
|
|
|
|
|
|
sweeper *UtxoSweeper
|
|
|
|
|
notifier *MockNotifier
|
|
|
|
|
estimator *mockFeeEstimator
|
|
|
|
|
backend *mockBackend
|
2024-01-10 21:18:40 +01:00
|
|
|
|
store SweeperStore
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
publishChan chan wire.MsgTx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
2019-01-16 15:47:43 +01:00
|
|
|
|
spendableInputs []*input.BaseInput
|
2018-12-07 09:06:36 +01:00
|
|
|
|
testInputCount int
|
|
|
|
|
|
|
|
|
|
testPubKey, _ = btcec.ParsePubKey([]byte{
|
|
|
|
|
0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a,
|
|
|
|
|
0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e,
|
|
|
|
|
0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca,
|
|
|
|
|
0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0,
|
|
|
|
|
0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64,
|
|
|
|
|
0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9,
|
|
|
|
|
0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56,
|
|
|
|
|
0xb4, 0x12, 0xa3,
|
2022-02-23 14:48:00 +01:00
|
|
|
|
})
|
2018-12-07 09:06:36 +01:00
|
|
|
|
)
|
|
|
|
|
|
2019-01-16 15:47:43 +01:00
|
|
|
|
func createTestInput(value int64, witnessType input.WitnessType) input.BaseInput {
|
2018-12-07 09:06:36 +01:00
|
|
|
|
hash := chainhash.Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
2020-11-09 18:49:44 +01:00
|
|
|
|
byte(testInputCount + 1)}
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2019-01-16 15:47:43 +01:00
|
|
|
|
input := input.MakeBaseInput(
|
2018-12-07 09:06:36 +01:00
|
|
|
|
&wire.OutPoint{
|
|
|
|
|
Hash: hash,
|
|
|
|
|
},
|
|
|
|
|
witnessType,
|
2019-01-16 15:47:43 +01:00
|
|
|
|
&input.SignDescriptor{
|
2018-12-07 09:06:36 +01:00
|
|
|
|
Output: &wire.TxOut{
|
|
|
|
|
Value: value,
|
|
|
|
|
},
|
|
|
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
|
|
|
PubKey: testPubKey,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
0,
|
2020-09-04 11:28:17 +02:00
|
|
|
|
nil,
|
2018-12-07 09:06:36 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
testInputCount++
|
|
|
|
|
|
|
|
|
|
return input
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
// Create a set of test spendable inputs.
|
2020-11-06 19:35:01 +01:00
|
|
|
|
for i := 0; i < 20; i++ {
|
2018-12-07 09:06:36 +01:00
|
|
|
|
input := createTestInput(int64(10000+i*500),
|
2019-01-16 15:47:43 +01:00
|
|
|
|
input.CommitmentTimeLock)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
spendableInputs = append(spendableInputs, &input)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createSweeperTestContext(t *testing.T) *sweeperTestContext {
|
|
|
|
|
notifier := NewMockNotifier(t)
|
|
|
|
|
|
2024-01-10 21:18:40 +01:00
|
|
|
|
// Create new store.
|
|
|
|
|
cdb, err := channeldb.MakeTestDB(t)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
var chain chainhash.Hash
|
|
|
|
|
store, err := NewSweeperStore(cdb, &chain)
|
|
|
|
|
require.NoError(t, err)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2019-12-10 15:32:57 +01:00
|
|
|
|
backend := newMockBackend(t, notifier)
|
2019-12-10 16:06:45 +01:00
|
|
|
|
backend.walletUtxos = []*lnwallet.Utxo{
|
|
|
|
|
{
|
2020-11-09 18:49:44 +01:00
|
|
|
|
Value: btcutil.Amount(1_000_000),
|
2019-12-10 16:06:45 +01:00
|
|
|
|
AddressType: lnwallet.WitnessPubKey,
|
|
|
|
|
},
|
|
|
|
|
}
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2019-10-31 03:43:05 +01:00
|
|
|
|
estimator := newMockFeeEstimator(10000, chainfee.FeePerKwFloor)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2023-10-24 06:32:17 +02:00
|
|
|
|
aggregator := NewSimpleUtxoAggregator(
|
|
|
|
|
estimator, DefaultMaxFeeRate.FeePerKWeight(),
|
|
|
|
|
)
|
|
|
|
|
|
2018-12-07 09:06:36 +01:00
|
|
|
|
ctx := &sweeperTestContext{
|
|
|
|
|
notifier: notifier,
|
2019-12-10 15:32:57 +01:00
|
|
|
|
publishChan: backend.publishChan,
|
2018-12-07 09:06:36 +01:00
|
|
|
|
t: t,
|
|
|
|
|
estimator: estimator,
|
|
|
|
|
backend: backend,
|
|
|
|
|
store: store,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.sweeper = New(&UtxoSweeperConfig{
|
2023-10-12 12:03:34 +02:00
|
|
|
|
Notifier: notifier,
|
|
|
|
|
Wallet: backend,
|
|
|
|
|
TickerDuration: 100 * time.Millisecond,
|
|
|
|
|
Store: store,
|
2023-10-26 08:27:46 +02:00
|
|
|
|
Signer: &lnmock.DummySigner{},
|
2018-12-07 09:06:36 +01:00
|
|
|
|
GenSweepScript: func() ([]byte, error) {
|
2021-09-23 21:40:37 +02:00
|
|
|
|
script := make([]byte, input.P2WPKHSize)
|
2022-07-15 22:25:31 +02:00
|
|
|
|
script[0] = 0
|
|
|
|
|
script[1] = 20
|
2018-12-07 09:06:36 +01:00
|
|
|
|
return script, nil
|
|
|
|
|
},
|
2018-11-18 05:57:30 +01:00
|
|
|
|
FeeEstimator: estimator,
|
2018-12-07 09:06:36 +01:00
|
|
|
|
MaxInputsPerTx: testMaxInputsPerTx,
|
|
|
|
|
MaxSweepAttempts: testMaxSweepAttempts,
|
|
|
|
|
NextAttemptDeltaFunc: func(attempts int) int32 {
|
|
|
|
|
// Use delta func without random factor.
|
|
|
|
|
return 1 << uint(attempts-1)
|
|
|
|
|
},
|
2023-10-24 06:32:17 +02:00
|
|
|
|
MaxFeeRate: DefaultMaxFeeRate,
|
|
|
|
|
Aggregator: aggregator,
|
2018-12-07 09:06:36 +01:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
ctx.sweeper.Start()
|
|
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
func (ctx *sweeperTestContext) restartSweeper() {
|
|
|
|
|
ctx.t.Helper()
|
|
|
|
|
|
|
|
|
|
ctx.sweeper.Stop()
|
|
|
|
|
ctx.sweeper = New(ctx.sweeper.cfg)
|
|
|
|
|
ctx.sweeper.Start()
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-07 09:06:36 +01:00
|
|
|
|
func (ctx *sweeperTestContext) finish(expectedGoroutineCount int) {
|
|
|
|
|
// We assume that when finish is called, sweeper has finished all its
|
|
|
|
|
// goroutines. This implies that the waitgroup is empty.
|
|
|
|
|
signalChan := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
ctx.sweeper.wg.Wait()
|
|
|
|
|
close(signalChan)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Simulate exits of the expected number of running goroutines.
|
|
|
|
|
for i := 0; i < expectedGoroutineCount; i++ {
|
|
|
|
|
ctx.sweeper.wg.Done()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We now expect the Wait to succeed.
|
|
|
|
|
select {
|
|
|
|
|
case <-signalChan:
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
|
|
|
|
|
|
|
|
|
ctx.t.Fatalf("lingering goroutines detected after test " +
|
|
|
|
|
"is finished")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore waitgroup state to what it was before.
|
|
|
|
|
ctx.sweeper.wg.Add(expectedGoroutineCount)
|
|
|
|
|
|
|
|
|
|
// Stop sweeper.
|
|
|
|
|
ctx.sweeper.Stop()
|
|
|
|
|
|
|
|
|
|
// We should have consumed and asserted all published transactions in
|
|
|
|
|
// our unit tests.
|
|
|
|
|
ctx.assertNoTx()
|
|
|
|
|
if !ctx.backend.isDone() {
|
|
|
|
|
ctx.t.Fatal("unconfirmed txes remaining")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ctx *sweeperTestContext) assertNoTx() {
|
|
|
|
|
ctx.t.Helper()
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.publishChan:
|
|
|
|
|
ctx.t.Fatalf("unexpected transactions published")
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ctx *sweeperTestContext) receiveTx() wire.MsgTx {
|
|
|
|
|
ctx.t.Helper()
|
|
|
|
|
var tx wire.MsgTx
|
|
|
|
|
select {
|
|
|
|
|
case tx = <-ctx.publishChan:
|
|
|
|
|
return tx
|
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
|
|
|
|
|
|
|
|
|
ctx.t.Fatalf("tx not published")
|
|
|
|
|
}
|
|
|
|
|
return tx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ctx *sweeperTestContext) expectResult(c chan Result, expected error) {
|
|
|
|
|
ctx.t.Helper()
|
|
|
|
|
select {
|
|
|
|
|
case result := <-c:
|
|
|
|
|
if result.Err != expected {
|
|
|
|
|
ctx.t.Fatalf("expected %v result, but got %v",
|
|
|
|
|
expected, result.Err,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(defaultTestTimeout):
|
|
|
|
|
ctx.t.Fatalf("no result received")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-17 04:52:46 +02:00
|
|
|
|
func (ctx *sweeperTestContext) assertPendingInputs(inputs ...input.Input) {
|
|
|
|
|
ctx.t.Helper()
|
|
|
|
|
|
|
|
|
|
inputSet := make(map[wire.OutPoint]struct{}, len(inputs))
|
|
|
|
|
for _, input := range inputs {
|
|
|
|
|
inputSet[*input.OutPoint()] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pendingInputs, err := ctx.sweeper.PendingInputs()
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(pendingInputs) != len(inputSet) {
|
|
|
|
|
ctx.t.Fatalf("expected %d pending inputs, got %d",
|
|
|
|
|
len(inputSet), len(pendingInputs))
|
|
|
|
|
}
|
|
|
|
|
for input := range pendingInputs {
|
|
|
|
|
if _, ok := inputSet[input]; !ok {
|
|
|
|
|
ctx.t.Fatalf("found unexpected input %v", input)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
// assertTxSweepsInputs ensures that the transaction returned within the value
|
|
|
|
|
// received from resultChan spends the given inputs.
|
|
|
|
|
func assertTxSweepsInputs(t *testing.T, sweepTx *wire.MsgTx,
|
|
|
|
|
inputs ...input.Input) {
|
|
|
|
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
if len(sweepTx.TxIn) != len(inputs) {
|
|
|
|
|
t.Fatalf("expected sweep tx to contain %d inputs, got %d",
|
|
|
|
|
len(inputs), len(sweepTx.TxIn))
|
|
|
|
|
}
|
|
|
|
|
m := make(map[wire.OutPoint]struct{}, len(inputs))
|
|
|
|
|
for _, input := range inputs {
|
|
|
|
|
m[*input.OutPoint()] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
for _, txIn := range sweepTx.TxIn {
|
|
|
|
|
if _, ok := m[txIn.PreviousOutPoint]; !ok {
|
|
|
|
|
t.Fatalf("expected tx %v to spend input %v",
|
|
|
|
|
txIn.PreviousOutPoint, sweepTx.TxHash())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertTxFeeRate asserts that the transaction was created with the given
|
|
|
|
|
// inputs and fee rate.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: This assumes that transactions only have one output, as this is the
|
|
|
|
|
// only type of transaction the UtxoSweeper can create at the moment.
|
|
|
|
|
func assertTxFeeRate(t *testing.T, tx *wire.MsgTx,
|
2022-07-15 22:25:31 +02:00
|
|
|
|
expectedFeeRate chainfee.SatPerKWeight, changePk []byte,
|
|
|
|
|
inputs ...input.Input) {
|
2019-05-02 01:06:19 +02:00
|
|
|
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
if len(tx.TxIn) != len(inputs) {
|
|
|
|
|
t.Fatalf("expected %d inputs, got %d", len(tx.TxIn), len(inputs))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m := make(map[wire.OutPoint]input.Input, len(inputs))
|
|
|
|
|
for _, input := range inputs {
|
|
|
|
|
m[*input.OutPoint()] = input
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var inputAmt int64
|
|
|
|
|
for _, txIn := range tx.TxIn {
|
|
|
|
|
input, ok := m[txIn.PreviousOutPoint]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("expected input %v to be provided",
|
|
|
|
|
txIn.PreviousOutPoint)
|
|
|
|
|
}
|
|
|
|
|
inputAmt += input.SignDesc().Output.Value
|
|
|
|
|
}
|
|
|
|
|
outputAmt := tx.TxOut[0].Value
|
|
|
|
|
|
|
|
|
|
fee := btcutil.Amount(inputAmt - outputAmt)
|
2023-08-22 05:06:51 +02:00
|
|
|
|
_, estimator, err := getWeightEstimate(inputs, nil, 0, 0, changePk)
|
2022-07-15 22:25:31 +02:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2020-09-04 11:28:17 +02:00
|
|
|
|
txWeight := estimator.weight()
|
2019-05-02 01:06:19 +02:00
|
|
|
|
|
2020-09-04 11:28:17 +02:00
|
|
|
|
expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight))
|
2019-05-02 01:06:19 +02:00
|
|
|
|
if fee != expectedFee {
|
|
|
|
|
t.Fatalf("expected fee rate %v results in %v fee, got %v fee",
|
|
|
|
|
expectedFeeRate, expectedFee, fee)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-07 09:06:36 +01:00
|
|
|
|
// TestSuccess tests the sweeper happy flow.
|
|
|
|
|
func TestSuccess(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
2019-05-29 22:59:00 +02:00
|
|
|
|
// Sweeping an input without a fee preference should result in an error.
|
2023-11-03 13:05:21 +01:00
|
|
|
|
_, err := ctx.sweeper.SweepInput(spendableInputs[0], Params{
|
|
|
|
|
Fee: &FeeEstimateInfo{},
|
|
|
|
|
})
|
2019-05-29 22:59:00 +02:00
|
|
|
|
if err != ErrNoFeePreference {
|
|
|
|
|
t.Fatalf("expected ErrNoFeePreference, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[0], defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case result := <-resultChan:
|
|
|
|
|
if result.Err != nil {
|
|
|
|
|
t.Fatalf("expected successful spend, but received "+
|
|
|
|
|
"error %v instead", result.Err)
|
|
|
|
|
}
|
|
|
|
|
if result.Tx.TxHash() != sweepTx.TxHash() {
|
|
|
|
|
t.Fatalf("expected sweep tx ")
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
|
t.Fatalf("no result received")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestDust asserts that inputs that are not big enough to raise above the dust
|
|
|
|
|
// limit, are held back until the total set does surpass the limit.
|
|
|
|
|
func TestDust(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweeping a single output produces a tx of 486 weight units. With the
|
|
|
|
|
// test fee rate, the sweep tx will pay 4860 sat in fees.
|
|
|
|
|
//
|
|
|
|
|
// Create an input so that the output after paying fees is still
|
|
|
|
|
// positive (400 sat), but less than the dust limit (537 sat) for the
|
|
|
|
|
// sweep tx output script (P2WPKH).
|
2019-01-16 15:47:43 +01:00
|
|
|
|
dustInput := createTestInput(5260, input.CommitmentTimeLock)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
_, err := ctx.sweeper.SweepInput(&dustInput, defaultFeePref)
|
2023-10-24 06:32:17 +02:00
|
|
|
|
require.NoError(t, err)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
// No sweep transaction is expected now. The sweeper should recognize
|
2019-12-10 16:06:45 +01:00
|
|
|
|
// that the sweep output will not be relayed and not generate the tx. It
|
|
|
|
|
// isn't possible to attach a wallet utxo either, because the added
|
|
|
|
|
// weight would create a negatively yielding transaction at this fee
|
|
|
|
|
// rate.
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
// Sweep another input that brings the tx output above the dust limit.
|
2019-01-16 15:47:43 +01:00
|
|
|
|
largeInput := createTestInput(100000, input.CommitmentTimeLock)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
_, err = ctx.sweeper.SweepInput(&largeInput, defaultFeePref)
|
2023-10-24 06:32:17 +02:00
|
|
|
|
require.NoError(t, err)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
// The second input brings the sweep output above the dust limit. We
|
|
|
|
|
// expect a sweep tx now.
|
|
|
|
|
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
2023-10-24 06:32:17 +02:00
|
|
|
|
require.Len(t, sweepTx.TxIn, 2, "unexpected num of tx inputs")
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-10 16:06:45 +01:00
|
|
|
|
// TestWalletUtxo asserts that inputs that are not big enough to raise above the
|
|
|
|
|
// dust limit are accompanied by a wallet utxo to make them sweepable.
|
|
|
|
|
func TestWalletUtxo(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweeping a single output produces a tx of 439 weight units. At the
|
|
|
|
|
// fee floor, the sweep tx will pay 439*253/1000 = 111 sat in fees.
|
|
|
|
|
//
|
|
|
|
|
// Create an input so that the output after paying fees is still
|
|
|
|
|
// positive (183 sat), but less than the dust limit (537 sat) for the
|
|
|
|
|
// sweep tx output script (P2WPKH).
|
|
|
|
|
//
|
|
|
|
|
// What we now expect is that the sweeper will attach a utxo from the
|
|
|
|
|
// wallet. This increases the tx weight to 712 units with a fee of 180
|
|
|
|
|
// sats. The tx yield becomes then 294-180 = 114 sats.
|
|
|
|
|
dustInput := createTestInput(294, input.WitnessKeyHash)
|
|
|
|
|
|
|
|
|
|
_, err := ctx.sweeper.SweepInput(
|
|
|
|
|
&dustInput,
|
2023-11-03 12:20:10 +01:00
|
|
|
|
Params{Fee: FeeEstimateInfo{FeeRate: chainfee.FeePerKwFloor}},
|
2019-12-10 16:06:45 +01:00
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
|
|
|
|
if len(sweepTx.TxIn) != 2 {
|
|
|
|
|
t.Fatalf("Expected tx to sweep 2 inputs, but contains %v "+
|
|
|
|
|
"inputs instead", len(sweepTx.TxIn))
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-09 18:49:44 +01:00
|
|
|
|
// Calculate expected output value based on wallet utxo of 1_000_000
|
|
|
|
|
// sats.
|
|
|
|
|
expectedOutputValue := int64(294 + 1_000_000 - 180)
|
2019-12-10 16:06:45 +01:00
|
|
|
|
if sweepTx.TxOut[0].Value != expectedOutputValue {
|
|
|
|
|
t.Fatalf("Expected output value of %v, but got %v",
|
|
|
|
|
expectedOutputValue, sweepTx.TxOut[0].Value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-07 09:06:36 +01:00
|
|
|
|
// TestNegativeInput asserts that no inputs with a negative yield are swept.
|
|
|
|
|
// Negative yield means that the value minus the added fee is negative.
|
|
|
|
|
func TestNegativeInput(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweep an input large enough to cover fees, so in any case the tx
|
|
|
|
|
// output will be above the dust limit.
|
2019-01-16 15:47:43 +01:00
|
|
|
|
largeInput := createTestInput(100000, input.CommitmentNoDelay)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
largeInputResult, err := ctx.sweeper.SweepInput(
|
|
|
|
|
&largeInput, defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sweep an additional input with a negative net yield. The weight of
|
|
|
|
|
// the HtlcAcceptedRemoteSuccess input type adds more in fees than its
|
|
|
|
|
// value at the current fee level.
|
2019-01-16 15:47:43 +01:00
|
|
|
|
negInput := createTestInput(2900, input.HtlcOfferedRemoteTimeout)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
negInputResult, err := ctx.sweeper.SweepInput(&negInput, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sweep a third input that has a smaller output than the previous one,
|
|
|
|
|
// but yields positively because of its lower weight.
|
2019-01-16 15:47:43 +01:00
|
|
|
|
positiveInput := createTestInput(2800, input.CommitmentNoDelay)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
positiveInputResult, err := ctx.sweeper.SweepInput(
|
|
|
|
|
&positiveInput, defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We expect that a sweep tx is published now, but it should only
|
|
|
|
|
// contain the large input. The negative input should stay out of sweeps
|
|
|
|
|
// until fees come down to get a positive net yield.
|
|
|
|
|
sweepTx1 := ctx.receiveTx()
|
2019-05-02 01:06:19 +02:00
|
|
|
|
assertTxSweepsInputs(t, &sweepTx1, &largeInput, &positiveInput)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.expectResult(largeInputResult, nil)
|
|
|
|
|
ctx.expectResult(positiveInputResult, nil)
|
|
|
|
|
|
|
|
|
|
// Lower fee rate so that the negative input is no longer negative.
|
|
|
|
|
ctx.estimator.updateFees(1000, 1000)
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
// Create another large input.
|
2019-01-16 15:47:43 +01:00
|
|
|
|
secondLargeInput := createTestInput(100000, input.CommitmentNoDelay)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
secondLargeInputResult, err := ctx.sweeper.SweepInput(
|
|
|
|
|
&secondLargeInput, defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sweepTx2 := ctx.receiveTx()
|
2019-05-02 01:06:19 +02:00
|
|
|
|
assertTxSweepsInputs(t, &sweepTx2, &secondLargeInput, &negInput)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.expectResult(secondLargeInputResult, nil)
|
|
|
|
|
ctx.expectResult(negInputResult, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestChunks asserts that large sets of inputs are split into multiple txes.
|
|
|
|
|
func TestChunks(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweep five inputs.
|
|
|
|
|
for _, input := range spendableInputs[:5] {
|
2019-05-02 01:06:19 +02:00
|
|
|
|
_, err := ctx.sweeper.SweepInput(input, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We expect two txes to be published because of the max input count of
|
|
|
|
|
// three.
|
|
|
|
|
sweepTx1 := ctx.receiveTx()
|
|
|
|
|
if len(sweepTx1.TxIn) != 3 {
|
|
|
|
|
t.Fatalf("Expected first tx to sweep 3 inputs, but contains %v "+
|
|
|
|
|
"inputs instead", len(sweepTx1.TxIn))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sweepTx2 := ctx.receiveTx()
|
|
|
|
|
if len(sweepTx2.TxIn) != 2 {
|
|
|
|
|
t.Fatalf("Expected first tx to sweep 2 inputs, but contains %v "+
|
|
|
|
|
"inputs instead", len(sweepTx1.TxIn))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRemoteSpend asserts that remote spends are properly detected and handled
|
|
|
|
|
// both before the sweep is published as well as after.
|
|
|
|
|
func TestRemoteSpend(t *testing.T) {
|
|
|
|
|
t.Run("pre-sweep", func(t *testing.T) {
|
|
|
|
|
testRemoteSpend(t, false)
|
|
|
|
|
})
|
|
|
|
|
t.Run("post-sweep", func(t *testing.T) {
|
|
|
|
|
testRemoteSpend(t, true)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testRemoteSpend(t *testing.T, postSweep bool) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan1, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[0], defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan2, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[1], defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spend the input with an unknown tx.
|
|
|
|
|
remoteTx := &wire.MsgTx{
|
|
|
|
|
TxIn: []*wire.TxIn{
|
|
|
|
|
{
|
|
|
|
|
PreviousOutPoint: *(spendableInputs[0].OutPoint()),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
err = ctx.backend.publishTransaction(remoteTx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if postSweep {
|
|
|
|
|
|
|
|
|
|
// Tx publication by sweeper returns ErrDoubleSpend. Sweeper
|
|
|
|
|
// will retry the inputs without reporting a result. It could be
|
|
|
|
|
// spent by the remote party.
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case result := <-resultChan1:
|
|
|
|
|
if result.Err != ErrRemoteSpend {
|
|
|
|
|
t.Fatalf("expected remote spend")
|
|
|
|
|
}
|
|
|
|
|
if result.Tx.TxHash() != remoteTx.TxHash() {
|
|
|
|
|
t.Fatalf("expected remote spend tx")
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
|
t.Fatalf("no result received")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !postSweep {
|
|
|
|
|
// Assert that the sweeper sweeps the remaining input.
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
if len(sweepTx.TxIn) != 1 {
|
|
|
|
|
t.Fatal("expected sweep to only sweep the one remaining output")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.expectResult(resultChan2, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
} else {
|
|
|
|
|
// Expected sweeper to be still listening for spend of the
|
|
|
|
|
// error input.
|
|
|
|
|
ctx.finish(2)
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-resultChan2:
|
|
|
|
|
t.Fatalf("no result expected for error input")
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestIdempotency asserts that offering the same input multiple times is
|
|
|
|
|
// handled correctly.
|
|
|
|
|
func TestIdempotency(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
input := spendableInputs[0]
|
|
|
|
|
resultChan1, err := ctx.sweeper.SweepInput(input, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan2, err := ctx.sweeper.SweepInput(input, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan3, err := ctx.sweeper.SweepInput(input, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spend the input of the sweep tx.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.expectResult(resultChan1, nil)
|
|
|
|
|
ctx.expectResult(resultChan2, nil)
|
|
|
|
|
ctx.expectResult(resultChan3, nil)
|
|
|
|
|
|
|
|
|
|
// Offer the same input again. The sweeper will register a spend ntfn
|
|
|
|
|
// for this input. Because the input has already been spent, it will
|
|
|
|
|
// immediately receive the spend notification with a spending tx hash.
|
|
|
|
|
// Because the sweeper kept track of all of its sweep txes, it will
|
|
|
|
|
// recognize the spend as its own.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan4, err := ctx.sweeper.SweepInput(input, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
ctx.expectResult(resultChan4, nil)
|
|
|
|
|
|
|
|
|
|
// Timer is still running, but spend notification was delivered before
|
|
|
|
|
// it expired.
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestNoInputs asserts that nothing happens if nothing happens.
|
|
|
|
|
func TestNoInputs(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// No tx should appear. This is asserted in finish().
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRestart asserts that the sweeper picks up sweeping properly after
|
|
|
|
|
// a restart.
|
|
|
|
|
func TestRestart(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweep input and expect sweep tx.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
input1 := spendableInputs[0]
|
2024-01-10 21:18:40 +01:00
|
|
|
|
_, err := ctx.sweeper.SweepInput(input1, defaultFeePref)
|
|
|
|
|
require.NoError(t, err)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
// Restart sweeper.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
ctx.restartSweeper()
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
2020-03-19 05:43:49 +01:00
|
|
|
|
// Simulate other subsystem (e.g. contract resolver) re-offering inputs.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
spendChan1, err := ctx.sweeper.SweepInput(input1, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
input2 := spendableInputs[1]
|
|
|
|
|
spendChan2, err := ctx.sweeper.SweepInput(input2, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spend inputs of sweep txes and verify that spend channels signal
|
|
|
|
|
// spends.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
// Sweeper should recognize that its sweep tx of the previous run is
|
|
|
|
|
// spending the input.
|
|
|
|
|
select {
|
|
|
|
|
case result := <-spendChan1:
|
|
|
|
|
if result.Err != nil {
|
|
|
|
|
t.Fatalf("expected successful sweep")
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(defaultTestTimeout):
|
|
|
|
|
t.Fatalf("no result received")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Timer tick should trigger republishing a sweep for the remaining
|
|
|
|
|
// input.
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case result := <-spendChan2:
|
|
|
|
|
if result.Err != nil {
|
|
|
|
|
t.Fatalf("expected successful sweep")
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(defaultTestTimeout):
|
|
|
|
|
t.Fatalf("no result received")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restart sweeper again. No action is expected.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
ctx.restartSweeper()
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 21:18:40 +01:00
|
|
|
|
// TestRestartRemoteSpend asserts that the sweeper picks up sweeping properly
|
|
|
|
|
// after a restart with remote spend.
|
2018-12-07 09:06:36 +01:00
|
|
|
|
func TestRestartRemoteSpend(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweep input.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
input1 := spendableInputs[0]
|
2024-01-10 21:18:40 +01:00
|
|
|
|
_, err := ctx.sweeper.SweepInput(input1, defaultFeePref)
|
|
|
|
|
require.NoError(t, err)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
// Sweep another input.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
input2 := spendableInputs[1]
|
2024-01-10 21:18:40 +01:00
|
|
|
|
_, err = ctx.sweeper.SweepInput(input2, defaultFeePref)
|
|
|
|
|
require.NoError(t, err)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
// Restart sweeper.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
ctx.restartSweeper()
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
// Replace the sweep tx with a remote tx spending input 1.
|
|
|
|
|
ctx.backend.deleteUnconfirmed(sweepTx.TxHash())
|
|
|
|
|
|
|
|
|
|
remoteTx := &wire.MsgTx{
|
|
|
|
|
TxIn: []*wire.TxIn{
|
|
|
|
|
{
|
2019-05-02 01:06:19 +02:00
|
|
|
|
PreviousOutPoint: *(input2.OutPoint()),
|
2018-12-07 09:06:36 +01:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
2019-05-02 01:06:19 +02:00
|
|
|
|
if err := ctx.backend.publishTransaction(remoteTx); err != nil {
|
2018-12-07 09:06:36 +01:00
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mine remote spending tx.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
2024-01-10 21:18:40 +01:00
|
|
|
|
// Simulate other subsystem (e.g. contract resolver) re-offering input
|
|
|
|
|
// 0.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
spendChan, err := ctx.sweeper.SweepInput(input1, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expect sweeper to construct a new tx, because input 1 was spend
|
|
|
|
|
// remotely.
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.expectResult(spendChan, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 21:18:40 +01:00
|
|
|
|
// TestRestartConfirmed asserts that the sweeper picks up sweeping properly
|
|
|
|
|
// after a restart with a confirm of our own sweep tx.
|
2018-12-07 09:06:36 +01:00
|
|
|
|
func TestRestartConfirmed(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweep input.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
input := spendableInputs[0]
|
|
|
|
|
if _, err := ctx.sweeper.SweepInput(input, defaultFeePref); err != nil {
|
2018-12-07 09:06:36 +01:00
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
// Restart sweeper.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
ctx.restartSweeper()
|
2018-12-07 09:06:36 +01:00
|
|
|
|
|
|
|
|
|
// Mine the sweep tx.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
2024-01-10 21:18:40 +01:00
|
|
|
|
// Simulate other subsystem (e.g. contract resolver) re-offering input
|
|
|
|
|
// 0.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
spendChan, err := ctx.sweeper.SweepInput(input, defaultFeePref)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Here we expect again a successful sweep.
|
|
|
|
|
ctx.expectResult(spendChan, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRetry tests the sweeper retry flow.
|
|
|
|
|
func TestRetry(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan0, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[0], defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We expect a sweep to be published.
|
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
// Offer a fresh input.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
resultChan1, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[1], defaultFeePref,
|
|
|
|
|
)
|
2018-12-07 09:06:36 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-12 14:35:46 +02:00
|
|
|
|
// A single tx is expected to be published.
|
2018-12-07 09:06:36 +01:00
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
ctx.expectResult(resultChan0, nil)
|
|
|
|
|
ctx.expectResult(resultChan1, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:06:19 +02:00
|
|
|
|
// TestDifferentFeePreferences ensures that the sweeper can have different
|
2019-05-16 02:04:49 +02:00
|
|
|
|
// transactions for different fee preferences. These transactions should be
|
|
|
|
|
// broadcast from highest to lowest fee rate.
|
2019-05-02 01:06:19 +02:00
|
|
|
|
func TestDifferentFeePreferences(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Throughout this test, we'll be attempting to sweep three inputs, two
|
|
|
|
|
// with the higher fee preference, and the last with the lower. We do
|
|
|
|
|
// this to ensure the sweeper can broadcast distinct transactions for
|
|
|
|
|
// each sweep with a different fee preference.
|
2023-11-03 12:20:10 +01:00
|
|
|
|
lowFeePref := FeeEstimateInfo{ConfTarget: 12}
|
2019-10-31 03:43:05 +01:00
|
|
|
|
lowFeeRate := chainfee.SatPerKWeight(5000)
|
2019-05-29 22:59:37 +02:00
|
|
|
|
ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate
|
|
|
|
|
|
2023-11-03 12:20:10 +01:00
|
|
|
|
highFeePref := FeeEstimateInfo{ConfTarget: 6}
|
2019-10-31 03:43:05 +01:00
|
|
|
|
highFeeRate := chainfee.SatPerKWeight(10000)
|
2019-05-29 22:59:37 +02:00
|
|
|
|
ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate
|
2019-05-02 01:06:19 +02:00
|
|
|
|
|
|
|
|
|
input1 := spendableInputs[0]
|
2019-12-09 11:42:58 +01:00
|
|
|
|
resultChan1, err := ctx.sweeper.SweepInput(
|
|
|
|
|
input1, Params{Fee: highFeePref},
|
|
|
|
|
)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
input2 := spendableInputs[1]
|
2019-12-09 11:42:58 +01:00
|
|
|
|
resultChan2, err := ctx.sweeper.SweepInput(
|
|
|
|
|
input2, Params{Fee: highFeePref},
|
|
|
|
|
)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
input3 := spendableInputs[2]
|
2019-12-09 11:42:58 +01:00
|
|
|
|
resultChan3, err := ctx.sweeper.SweepInput(
|
|
|
|
|
input3, Params{Fee: lowFeePref},
|
|
|
|
|
)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-15 22:25:31 +02:00
|
|
|
|
// Generate the same type of sweep script that was used for weight
|
|
|
|
|
// estimation.
|
|
|
|
|
changePk, err := ctx.sweeper.cfg.GenSweepScript()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2019-05-16 02:04:49 +02:00
|
|
|
|
// The first transaction broadcast should be the one spending the higher
|
|
|
|
|
// fee rate inputs.
|
|
|
|
|
sweepTx1 := ctx.receiveTx()
|
2022-07-15 22:25:31 +02:00
|
|
|
|
assertTxFeeRate(t, &sweepTx1, highFeeRate, changePk, input1, input2)
|
2019-05-16 02:04:49 +02:00
|
|
|
|
|
|
|
|
|
// The second should be the one spending the lower fee rate inputs.
|
|
|
|
|
sweepTx2 := ctx.receiveTx()
|
2022-07-15 22:25:31 +02:00
|
|
|
|
assertTxFeeRate(t, &sweepTx2, lowFeeRate, changePk, input3)
|
2019-05-02 01:06:19 +02:00
|
|
|
|
|
|
|
|
|
// With the transactions broadcast, we'll mine a block to so that the
|
|
|
|
|
// result is delivered to each respective client.
|
|
|
|
|
ctx.backend.mine()
|
2019-05-16 02:04:49 +02:00
|
|
|
|
resultChans := []chan Result{resultChan1, resultChan2, resultChan3}
|
|
|
|
|
for _, resultChan := range resultChans {
|
|
|
|
|
ctx.expectResult(resultChan, nil)
|
|
|
|
|
}
|
2019-05-02 01:06:19 +02:00
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
2019-05-17 04:52:46 +02:00
|
|
|
|
|
|
|
|
|
// TestPendingInputs ensures that the sweeper correctly determines the inputs
|
|
|
|
|
// pending to be swept.
|
|
|
|
|
func TestPendingInputs(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Throughout this test, we'll be attempting to sweep three inputs, two
|
|
|
|
|
// with the higher fee preference, and the last with the lower. We do
|
|
|
|
|
// this to ensure the sweeper can return all pending inputs, even those
|
|
|
|
|
// with different fee preferences.
|
|
|
|
|
const (
|
|
|
|
|
lowFeeRate = 5000
|
|
|
|
|
highFeeRate = 10000
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-03 12:20:10 +01:00
|
|
|
|
lowFeePref := FeeEstimateInfo{
|
2019-05-17 04:52:46 +02:00
|
|
|
|
ConfTarget: 12,
|
|
|
|
|
}
|
|
|
|
|
ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate
|
|
|
|
|
|
2023-11-03 12:20:10 +01:00
|
|
|
|
highFeePref := FeeEstimateInfo{
|
2019-05-17 04:52:46 +02:00
|
|
|
|
ConfTarget: 6,
|
|
|
|
|
}
|
|
|
|
|
ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate
|
|
|
|
|
|
|
|
|
|
input1 := spendableInputs[0]
|
2019-12-09 11:42:58 +01:00
|
|
|
|
resultChan1, err := ctx.sweeper.SweepInput(
|
|
|
|
|
input1, Params{Fee: highFeePref},
|
|
|
|
|
)
|
2019-05-17 04:52:46 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
input2 := spendableInputs[1]
|
2019-12-09 11:42:58 +01:00
|
|
|
|
_, err = ctx.sweeper.SweepInput(
|
|
|
|
|
input2, Params{Fee: highFeePref},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2019-05-17 04:52:46 +02:00
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
input3 := spendableInputs[2]
|
2019-12-09 11:42:58 +01:00
|
|
|
|
resultChan3, err := ctx.sweeper.SweepInput(
|
|
|
|
|
input3, Params{Fee: lowFeePref},
|
|
|
|
|
)
|
2019-05-17 04:52:46 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We should expect to see all inputs pending.
|
|
|
|
|
ctx.assertPendingInputs(input1, input2, input3)
|
|
|
|
|
|
2023-10-26 09:13:23 +02:00
|
|
|
|
// We should expect to see both sweep transactions broadcast - one for
|
|
|
|
|
// the higher feerate, the other for the lower.
|
2019-05-17 04:52:46 +02:00
|
|
|
|
ctx.receiveTx()
|
|
|
|
|
ctx.receiveTx()
|
2023-10-26 09:13:23 +02:00
|
|
|
|
|
|
|
|
|
// Mine these txns, and we should expect to see the results delivered.
|
2019-05-17 04:52:46 +02:00
|
|
|
|
ctx.backend.mine()
|
2023-10-26 09:13:23 +02:00
|
|
|
|
ctx.expectResult(resultChan1, nil)
|
2019-05-17 04:52:46 +02:00
|
|
|
|
ctx.expectResult(resultChan3, nil)
|
|
|
|
|
ctx.assertPendingInputs()
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
2019-05-29 23:00:14 +02:00
|
|
|
|
|
|
|
|
|
// TestBumpFeeRBF ensures that the UtxoSweeper can properly handle a fee bump
|
|
|
|
|
// request for an input it is currently attempting to sweep. When sweeping the
|
|
|
|
|
// input with the higher fee rate, a replacement transaction is created.
|
|
|
|
|
func TestBumpFeeRBF(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
2023-11-03 12:20:10 +01:00
|
|
|
|
lowFeePref := FeeEstimateInfo{ConfTarget: 144}
|
2019-10-31 03:43:05 +01:00
|
|
|
|
lowFeeRate := chainfee.FeePerKwFloor
|
2019-05-29 23:00:14 +02:00
|
|
|
|
ctx.estimator.blocksToFee[lowFeePref.ConfTarget] = lowFeeRate
|
|
|
|
|
|
|
|
|
|
// We'll first try to bump the fee of an output currently unknown to the
|
|
|
|
|
// UtxoSweeper. Doing so should result in a lnwallet.ErrNotMine error.
|
2020-01-09 14:41:28 +01:00
|
|
|
|
_, err := ctx.sweeper.UpdateParams(
|
2020-03-11 18:14:26 +01:00
|
|
|
|
wire.OutPoint{}, ParamsUpdate{Fee: lowFeePref},
|
2020-01-09 14:41:28 +01:00
|
|
|
|
)
|
2019-05-29 23:00:14 +02:00
|
|
|
|
if err != lnwallet.ErrNotMine {
|
|
|
|
|
t.Fatalf("expected error lnwallet.ErrNotMine, got \"%v\"", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We'll then attempt to sweep an input, which we'll use to bump its fee
|
|
|
|
|
// later on.
|
|
|
|
|
input := createTestInput(
|
|
|
|
|
btcutil.SatoshiPerBitcoin, input.CommitmentTimeLock,
|
|
|
|
|
)
|
2019-12-09 11:42:58 +01:00
|
|
|
|
sweepResult, err := ctx.sweeper.SweepInput(
|
|
|
|
|
&input, Params{Fee: lowFeePref},
|
|
|
|
|
)
|
2019-05-29 23:00:14 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-15 22:25:31 +02:00
|
|
|
|
// Generate the same type of change script used so we can have accurate
|
|
|
|
|
// weight estimation.
|
|
|
|
|
changePk, err := ctx.sweeper.cfg.GenSweepScript()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2019-05-29 23:00:14 +02:00
|
|
|
|
// Ensure that a transaction is broadcast with the lower fee preference.
|
|
|
|
|
lowFeeTx := ctx.receiveTx()
|
2022-07-15 22:25:31 +02:00
|
|
|
|
assertTxFeeRate(t, &lowFeeTx, lowFeeRate, changePk, &input)
|
2019-05-29 23:00:14 +02:00
|
|
|
|
|
|
|
|
|
// We'll then attempt to bump its fee rate.
|
2023-11-03 12:20:10 +01:00
|
|
|
|
highFeePref := FeeEstimateInfo{ConfTarget: 6}
|
2023-07-26 11:04:04 +02:00
|
|
|
|
highFeeRate := DefaultMaxFeeRate.FeePerKWeight()
|
2019-05-29 23:00:14 +02:00
|
|
|
|
ctx.estimator.blocksToFee[highFeePref.ConfTarget] = highFeeRate
|
|
|
|
|
|
|
|
|
|
// We should expect to see an error if a fee preference isn't provided.
|
2023-11-03 13:05:21 +01:00
|
|
|
|
_, err = ctx.sweeper.UpdateParams(*input.OutPoint(), ParamsUpdate{
|
|
|
|
|
Fee: &FeeEstimateInfo{},
|
|
|
|
|
})
|
2019-05-29 23:00:14 +02:00
|
|
|
|
if err != ErrNoFeePreference {
|
|
|
|
|
t.Fatalf("expected ErrNoFeePreference, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-09 14:41:28 +01:00
|
|
|
|
bumpResult, err := ctx.sweeper.UpdateParams(
|
2020-03-11 18:14:26 +01:00
|
|
|
|
*input.OutPoint(), ParamsUpdate{Fee: highFeePref},
|
2020-01-09 14:41:28 +01:00
|
|
|
|
)
|
2022-05-05 22:11:50 +02:00
|
|
|
|
require.NoError(t, err, "unable to bump input's fee")
|
2019-05-29 23:00:14 +02:00
|
|
|
|
|
|
|
|
|
// A higher fee rate transaction should be immediately broadcast.
|
|
|
|
|
highFeeTx := ctx.receiveTx()
|
2022-07-15 22:25:31 +02:00
|
|
|
|
assertTxFeeRate(t, &highFeeTx, highFeeRate, changePk, &input)
|
2019-05-29 23:00:14 +02:00
|
|
|
|
|
|
|
|
|
// We'll finish our test by mining the sweep transaction.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
ctx.expectResult(sweepResult, nil)
|
|
|
|
|
ctx.expectResult(bumpResult, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
2019-12-09 11:51:13 +01:00
|
|
|
|
|
|
|
|
|
// TestExclusiveGroup tests the sweeper exclusive group functionality.
|
|
|
|
|
func TestExclusiveGroup(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Sweep three inputs in the same exclusive group.
|
|
|
|
|
var results []chan Result
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
exclusiveGroup := uint64(1)
|
|
|
|
|
result, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[i], Params{
|
2023-11-03 12:20:10 +01:00
|
|
|
|
Fee: FeeEstimateInfo{ConfTarget: 6},
|
2019-12-09 11:51:13 +01:00
|
|
|
|
ExclusiveGroup: &exclusiveGroup,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
results = append(results, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We expect all inputs to be published in separate transactions, even
|
|
|
|
|
// though they share the same fee preference.
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
|
|
|
|
if len(sweepTx.TxOut) != 1 {
|
|
|
|
|
t.Fatal("expected a single tx out in the sweep tx")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove all txes except for the one that sweeps the first
|
|
|
|
|
// input. This simulates the sweeps being conflicting.
|
|
|
|
|
if sweepTx.TxIn[0].PreviousOutPoint !=
|
|
|
|
|
*spendableInputs[0].OutPoint() {
|
|
|
|
|
|
|
|
|
|
ctx.backend.deleteUnconfirmed(sweepTx.TxHash())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mine the first sweep tx.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
// Expect the first input to be swept by the confirmed sweep tx.
|
|
|
|
|
result0 := <-results[0]
|
|
|
|
|
if result0.Err != nil {
|
|
|
|
|
t.Fatal("expected first input to be swept")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expect the other two inputs to return an error. They have no chance
|
|
|
|
|
// of confirming.
|
|
|
|
|
result1 := <-results[1]
|
|
|
|
|
if result1.Err != ErrExclusiveGroupSpend {
|
|
|
|
|
t.Fatal("expected second input to be canceled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result2 := <-results[2]
|
|
|
|
|
if result2.Err != ErrExclusiveGroupSpend {
|
|
|
|
|
t.Fatal("expected third input to be canceled")
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-04 11:28:17 +02:00
|
|
|
|
|
|
|
|
|
// TestCpfp tests that the sweeper spends cpfp inputs at a fee rate that exceeds
|
|
|
|
|
// the parent tx fee rate.
|
|
|
|
|
func TestCpfp(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
ctx.estimator.updateFees(1000, chainfee.FeePerKwFloor)
|
|
|
|
|
|
|
|
|
|
// Offer an input with an unconfirmed parent tx to the sweeper. The
|
|
|
|
|
// parent tx pays 3000 sat/kw.
|
|
|
|
|
hash := chainhash.Hash{1}
|
|
|
|
|
input := input.MakeBaseInput(
|
|
|
|
|
&wire.OutPoint{Hash: hash},
|
|
|
|
|
input.CommitmentTimeLock,
|
|
|
|
|
&input.SignDescriptor{
|
|
|
|
|
Output: &wire.TxOut{
|
|
|
|
|
Value: 330,
|
|
|
|
|
},
|
|
|
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
|
|
|
PubKey: testPubKey,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
0,
|
|
|
|
|
&input.TxInfo{
|
|
|
|
|
Weight: 300,
|
|
|
|
|
Fee: 900,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-03 12:20:10 +01:00
|
|
|
|
feePref := FeeEstimateInfo{ConfTarget: 6}
|
2020-09-04 11:28:17 +02:00
|
|
|
|
result, err := ctx.sweeper.SweepInput(
|
|
|
|
|
&input, Params{Fee: feePref, Force: true},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Increase the fee estimate to above the parent tx fee rate.
|
|
|
|
|
ctx.estimator.updateFees(5000, chainfee.FeePerKwFloor)
|
|
|
|
|
|
|
|
|
|
// Signal a new block. This is a trigger for the sweeper to refresh fee
|
|
|
|
|
// estimates.
|
|
|
|
|
ctx.notifier.NotifyEpoch(1000)
|
|
|
|
|
|
|
|
|
|
// Now we do expect a sweep transaction to be published with our input
|
|
|
|
|
// and an attached wallet utxo.
|
|
|
|
|
tx := ctx.receiveTx()
|
|
|
|
|
require.Len(t, tx.TxIn, 2)
|
|
|
|
|
require.Len(t, tx.TxOut, 1)
|
|
|
|
|
|
|
|
|
|
// As inputs we have 10000 sats from the wallet and 330 sats from the
|
|
|
|
|
// cpfp input. The sweep tx is weight expected to be 759 units. There is
|
|
|
|
|
// an additional 300 weight units from the parent to include in the
|
|
|
|
|
// package, making a total of 1059. At 5000 sat/kw, the required fee for
|
|
|
|
|
// the package is 5295 sats. The parent already paid 900 sats, so there
|
|
|
|
|
// is 4395 sat remaining to be paid. The expected output value is
|
2020-11-09 18:49:44 +01:00
|
|
|
|
// therefore: 1_000_000 + 330 - 4395 = 995 935.
|
|
|
|
|
require.Equal(t, int64(995_935), tx.TxOut[0].Value)
|
2020-09-04 11:28:17 +02:00
|
|
|
|
|
|
|
|
|
// Mine the tx and assert that the result is passed back.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
ctx.expectResult(result, nil)
|
|
|
|
|
|
|
|
|
|
ctx.finish(1)
|
|
|
|
|
}
|
2020-11-06 14:34:49 +01:00
|
|
|
|
|
2020-11-06 19:35:01 +01:00
|
|
|
|
type testInput struct {
|
|
|
|
|
*input.BaseInput
|
|
|
|
|
|
|
|
|
|
locktime *uint32
|
2020-11-09 18:56:26 +01:00
|
|
|
|
reqTxOut *wire.TxOut
|
2020-11-06 19:35:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *testInput) RequiredLockTime() (uint32, bool) {
|
|
|
|
|
if i.locktime != nil {
|
|
|
|
|
return *i.locktime, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-09 18:56:26 +01:00
|
|
|
|
func (i *testInput) RequiredTxOut() *wire.TxOut {
|
|
|
|
|
return i.reqTxOut
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-07 10:40:08 +01:00
|
|
|
|
// CraftInputScript is a custom sign method for the testInput type that will
|
|
|
|
|
// encode the spending outpoint and the tx input index as part of the returned
|
|
|
|
|
// witness.
|
|
|
|
|
func (i *testInput) CraftInputScript(_ input.Signer, txn *wire.MsgTx,
|
2022-03-18 18:37:44 +01:00
|
|
|
|
hashCache *txscript.TxSigHashes,
|
|
|
|
|
prevOutputFetcher txscript.PrevOutputFetcher,
|
|
|
|
|
txinIdx int) (*input.Script, error) {
|
2020-12-07 10:40:08 +01:00
|
|
|
|
|
|
|
|
|
// We'll encode the outpoint in the witness, so we can assert that the
|
|
|
|
|
// expected input was signed at the correct index.
|
|
|
|
|
op := i.OutPoint()
|
|
|
|
|
return &input.Script{
|
|
|
|
|
Witness: [][]byte{
|
|
|
|
|
// We encode the hash of the outpoint...
|
|
|
|
|
op.Hash[:],
|
|
|
|
|
// ..the outpoint index...
|
|
|
|
|
{byte(op.Index)},
|
|
|
|
|
// ..and finally the tx input index.
|
|
|
|
|
{byte(txinIdx)},
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertSignedIndex goes through all inputs to the tx and checks that all
|
|
|
|
|
// testInputs have witnesses corresponding to the outpoints they are spending,
|
|
|
|
|
// and are signed at the correct tx input index. All found testInputs are
|
|
|
|
|
// returned such that we can sum up and sanity check that all testInputs were
|
|
|
|
|
// part of the sweep.
|
|
|
|
|
func assertSignedIndex(t *testing.T, tx *wire.MsgTx,
|
|
|
|
|
testInputs map[wire.OutPoint]*testInput) map[wire.OutPoint]struct{} {
|
|
|
|
|
|
|
|
|
|
found := make(map[wire.OutPoint]struct{})
|
|
|
|
|
for idx, txIn := range tx.TxIn {
|
|
|
|
|
op := txIn.PreviousOutPoint
|
|
|
|
|
|
|
|
|
|
// Not a testInput, it won't have the test encoding we require
|
|
|
|
|
// to check outpoint and index.
|
|
|
|
|
if _, ok := testInputs[op]; !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, ok := found[op]; ok {
|
|
|
|
|
t.Fatalf("input already used")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check it was signes spending the correct outpoint, and at
|
|
|
|
|
// the expected tx input index.
|
|
|
|
|
require.Equal(t, txIn.Witness[0], op.Hash[:])
|
|
|
|
|
require.Equal(t, txIn.Witness[1], []byte{byte(op.Index)})
|
|
|
|
|
require.Equal(t, txIn.Witness[2], []byte{byte(idx)})
|
|
|
|
|
found[op] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return found
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-06 19:35:01 +01:00
|
|
|
|
// TestLockTimes checks that the sweeper properly groups inputs requiring the
|
|
|
|
|
// same locktime together into sweep transactions.
|
|
|
|
|
func TestLockTimes(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// We increase the number of max inputs to a tx so that won't
|
|
|
|
|
// impact our test.
|
|
|
|
|
ctx.sweeper.cfg.MaxInputsPerTx = 100
|
|
|
|
|
|
|
|
|
|
// We will set up the lock times in such a way that we expect the
|
|
|
|
|
// sweeper to divide the inputs into 4 diffeerent transactions.
|
|
|
|
|
const numSweeps = 4
|
|
|
|
|
|
|
|
|
|
// Sweep 8 inputs, using 4 different lock times.
|
|
|
|
|
var (
|
|
|
|
|
results []chan Result
|
|
|
|
|
inputs = make(map[wire.OutPoint]input.Input)
|
|
|
|
|
)
|
|
|
|
|
for i := 0; i < numSweeps*2; i++ {
|
|
|
|
|
lt := uint32(10 + (i % numSweeps))
|
|
|
|
|
inp := &testInput{
|
|
|
|
|
BaseInput: spendableInputs[i],
|
|
|
|
|
locktime: <,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err := ctx.sweeper.SweepInput(
|
|
|
|
|
inp, Params{
|
2023-11-03 12:20:10 +01:00
|
|
|
|
Fee: FeeEstimateInfo{ConfTarget: 6},
|
2020-11-06 19:35:01 +01:00
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
results = append(results, result)
|
|
|
|
|
|
|
|
|
|
op := inp.OutPoint()
|
|
|
|
|
inputs[*op] = inp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We also add 3 regular inputs that don't require any specific lock
|
|
|
|
|
// time.
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
inp := spendableInputs[i+numSweeps*2]
|
|
|
|
|
result, err := ctx.sweeper.SweepInput(
|
|
|
|
|
inp, Params{
|
2023-11-03 12:20:10 +01:00
|
|
|
|
Fee: FeeEstimateInfo{ConfTarget: 6},
|
2020-11-06 19:35:01 +01:00
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results = append(results, result)
|
|
|
|
|
|
|
|
|
|
op := inp.OutPoint()
|
|
|
|
|
inputs[*op] = inp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check the sweeps transactions, ensuring all inputs are there, and
|
|
|
|
|
// all the locktimes are satisfied.
|
|
|
|
|
for i := 0; i < numSweeps; i++ {
|
|
|
|
|
sweepTx := ctx.receiveTx()
|
|
|
|
|
if len(sweepTx.TxOut) != 1 {
|
|
|
|
|
t.Fatal("expected a single tx out in the sweep tx")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, txIn := range sweepTx.TxIn {
|
|
|
|
|
op := txIn.PreviousOutPoint
|
|
|
|
|
inp, ok := inputs[op]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("Unexpected outpoint: %v", op)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete(inputs, op)
|
|
|
|
|
|
|
|
|
|
// If this input had a required locktime, ensure the tx
|
|
|
|
|
// has that set correctly.
|
|
|
|
|
lt, ok := inp.RequiredLockTime()
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if lt != sweepTx.LockTime {
|
|
|
|
|
t.Fatalf("Input required locktime %v, sweep "+
|
|
|
|
|
"tx had locktime %v", lt, sweepTx.LockTime)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The should be no inputs not foud in any of the sweeps.
|
|
|
|
|
if len(inputs) != 0 {
|
|
|
|
|
t.Fatalf("had unsweeped inputs")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mine the first sweeps
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
// Results should all come back.
|
|
|
|
|
for i := range results {
|
|
|
|
|
result := <-results[i]
|
|
|
|
|
if result.Err != nil {
|
|
|
|
|
t.Fatal("expected input to be swept")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-09 18:56:26 +01:00
|
|
|
|
|
|
|
|
|
// TestRequiredTxOuts checks that inputs having a required TxOut gets swept with
|
|
|
|
|
// sweep transactions paying into these outputs.
|
|
|
|
|
func TestRequiredTxOuts(t *testing.T) {
|
|
|
|
|
// Create some test inputs and locktime vars.
|
|
|
|
|
var inputs []*input.BaseInput
|
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
|
input := createTestInput(
|
|
|
|
|
int64(btcutil.SatoshiPerBitcoin+i*500),
|
|
|
|
|
input.CommitmentTimeLock,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
inputs = append(inputs, &input)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
locktime1 := uint32(51)
|
|
|
|
|
locktime2 := uint32(52)
|
|
|
|
|
locktime3 := uint32(53)
|
|
|
|
|
|
2021-09-23 21:40:37 +02:00
|
|
|
|
aPkScript := make([]byte, input.P2WPKHSize)
|
|
|
|
|
aPkScript[0] = 'a'
|
|
|
|
|
|
|
|
|
|
bPkScript := make([]byte, input.P2WSHSize)
|
|
|
|
|
bPkScript[0] = 'b'
|
|
|
|
|
|
|
|
|
|
cPkScript := make([]byte, input.P2PKHSize)
|
|
|
|
|
cPkScript[0] = 'c'
|
|
|
|
|
|
|
|
|
|
dPkScript := make([]byte, input.P2SHSize)
|
|
|
|
|
dPkScript[0] = 'd'
|
|
|
|
|
|
|
|
|
|
ePkScript := make([]byte, input.UnknownWitnessSize)
|
|
|
|
|
ePkScript[0] = 'e'
|
|
|
|
|
|
|
|
|
|
fPkScript := make([]byte, input.P2WSHSize)
|
|
|
|
|
fPkScript[0] = 'f'
|
|
|
|
|
|
2020-11-09 18:56:26 +01:00
|
|
|
|
testCases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
inputs []*testInput
|
|
|
|
|
assertSweeps func(*testing.T, map[wire.OutPoint]*testInput,
|
|
|
|
|
[]*wire.MsgTx)
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
// Single input with a required TX out that is smaller.
|
|
|
|
|
// We expect a change output to be added.
|
|
|
|
|
name: "single input, leftover change",
|
|
|
|
|
inputs: []*testInput{
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[0],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: aPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: 100000,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Since the required output value is small, we expect
|
|
|
|
|
// the rest after fees to go into a change output.
|
|
|
|
|
assertSweeps: func(t *testing.T,
|
|
|
|
|
_ map[wire.OutPoint]*testInput,
|
|
|
|
|
txs []*wire.MsgTx) {
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 1, len(txs))
|
|
|
|
|
|
|
|
|
|
tx := txs[0]
|
|
|
|
|
require.Equal(t, 1, len(tx.TxIn))
|
|
|
|
|
|
|
|
|
|
// We should have two outputs, the required
|
|
|
|
|
// output must be the first one.
|
|
|
|
|
require.Equal(t, 2, len(tx.TxOut))
|
|
|
|
|
out := tx.TxOut[0]
|
2021-09-23 21:40:37 +02:00
|
|
|
|
require.Equal(t, aPkScript, out.PkScript)
|
2020-11-09 18:56:26 +01:00
|
|
|
|
require.Equal(t, int64(100000), out.Value)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// An input committing to a slightly smaller output, so
|
|
|
|
|
// it will pay its own fees.
|
|
|
|
|
name: "single input, no change",
|
|
|
|
|
inputs: []*testInput{
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[0],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: aPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
|
|
|
|
|
// Fee will be about 5340 sats.
|
|
|
|
|
// Subtract a bit more to
|
|
|
|
|
// ensure no dust change output
|
|
|
|
|
// is manifested.
|
2021-09-23 21:40:37 +02:00
|
|
|
|
Value: inputs[0].SignDesc().Output.Value - 6300,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// We expect this single input/output pair.
|
|
|
|
|
assertSweeps: func(t *testing.T,
|
|
|
|
|
_ map[wire.OutPoint]*testInput,
|
|
|
|
|
txs []*wire.MsgTx) {
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 1, len(txs))
|
|
|
|
|
|
|
|
|
|
tx := txs[0]
|
|
|
|
|
require.Equal(t, 1, len(tx.TxIn))
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 1, len(tx.TxOut))
|
|
|
|
|
out := tx.TxOut[0]
|
2021-09-23 21:40:37 +02:00
|
|
|
|
require.Equal(t, aPkScript, out.PkScript)
|
2020-11-09 18:56:26 +01:00
|
|
|
|
require.Equal(
|
|
|
|
|
t,
|
2021-09-23 21:40:37 +02:00
|
|
|
|
inputs[0].SignDesc().Output.Value-6300,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
out.Value,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
2020-12-07 10:40:08 +01:00
|
|
|
|
{
|
|
|
|
|
// Two inputs, where the first one required no tx out.
|
|
|
|
|
name: "two inputs, one with required tx out",
|
|
|
|
|
inputs: []*testInput{
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
// We add a normal, non-requiredTxOut
|
|
|
|
|
// input. We use test input 10, to make
|
|
|
|
|
// sure this has a higher yield than
|
|
|
|
|
// the other input, and will be
|
|
|
|
|
// attempted added first to the sweep
|
|
|
|
|
// tx.
|
|
|
|
|
BaseInput: inputs[10],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// The second input requires a TxOut.
|
|
|
|
|
BaseInput: inputs[0],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: aPkScript,
|
2020-12-07 10:40:08 +01:00
|
|
|
|
Value: inputs[0].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// We expect the inputs to have been reordered.
|
|
|
|
|
assertSweeps: func(t *testing.T,
|
|
|
|
|
_ map[wire.OutPoint]*testInput,
|
|
|
|
|
txs []*wire.MsgTx) {
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 1, len(txs))
|
|
|
|
|
|
|
|
|
|
tx := txs[0]
|
|
|
|
|
require.Equal(t, 2, len(tx.TxIn))
|
|
|
|
|
require.Equal(t, 2, len(tx.TxOut))
|
|
|
|
|
|
|
|
|
|
// The required TxOut should be the first one.
|
|
|
|
|
out := tx.TxOut[0]
|
2021-09-23 21:40:37 +02:00
|
|
|
|
require.Equal(t, aPkScript, out.PkScript)
|
2020-12-07 10:40:08 +01:00
|
|
|
|
require.Equal(
|
|
|
|
|
t, inputs[0].SignDesc().Output.Value,
|
|
|
|
|
out.Value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// The first input should be the one having the
|
|
|
|
|
// required TxOut.
|
|
|
|
|
require.Len(t, tx.TxIn, 2)
|
|
|
|
|
require.Equal(
|
|
|
|
|
t, inputs[0].OutPoint(),
|
|
|
|
|
&tx.TxIn[0].PreviousOutPoint,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Second one is the one without a required tx
|
|
|
|
|
// out.
|
|
|
|
|
require.Equal(
|
|
|
|
|
t, inputs[10].OutPoint(),
|
|
|
|
|
&tx.TxIn[1].PreviousOutPoint,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2020-11-09 18:56:26 +01:00
|
|
|
|
{
|
|
|
|
|
// An input committing to an output of equal value, just
|
|
|
|
|
// add input to pay fees.
|
|
|
|
|
name: "single input, extra fee input",
|
|
|
|
|
inputs: []*testInput{
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[0],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: aPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[0].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// We expect an extra input and output.
|
|
|
|
|
assertSweeps: func(t *testing.T,
|
|
|
|
|
_ map[wire.OutPoint]*testInput,
|
|
|
|
|
txs []*wire.MsgTx) {
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 1, len(txs))
|
|
|
|
|
|
|
|
|
|
tx := txs[0]
|
|
|
|
|
require.Equal(t, 2, len(tx.TxIn))
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 2, len(tx.TxOut))
|
|
|
|
|
out := tx.TxOut[0]
|
2021-09-23 21:40:37 +02:00
|
|
|
|
require.Equal(t, aPkScript, out.PkScript)
|
2020-11-09 18:56:26 +01:00
|
|
|
|
require.Equal(
|
|
|
|
|
t, inputs[0].SignDesc().Output.Value,
|
|
|
|
|
out.Value,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// Three inputs added, should be combined into a single
|
|
|
|
|
// sweep.
|
|
|
|
|
name: "three inputs",
|
|
|
|
|
inputs: []*testInput{
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[0],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: aPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[0].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[1],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: bPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[1].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[2],
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: cPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[2].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// We expect an extra input and output to pay fees.
|
|
|
|
|
assertSweeps: func(t *testing.T,
|
|
|
|
|
testInputs map[wire.OutPoint]*testInput,
|
|
|
|
|
txs []*wire.MsgTx) {
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 1, len(txs))
|
|
|
|
|
|
|
|
|
|
tx := txs[0]
|
|
|
|
|
require.Equal(t, 4, len(tx.TxIn))
|
|
|
|
|
require.Equal(t, 4, len(tx.TxOut))
|
|
|
|
|
|
|
|
|
|
// The inputs and outputs must be in the same
|
|
|
|
|
// order.
|
|
|
|
|
for i, in := range tx.TxIn {
|
|
|
|
|
// Last one is the change input/output
|
|
|
|
|
// pair, so we'll skip it.
|
|
|
|
|
if i == 3 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get this input to ensure the output
|
|
|
|
|
// on index i coresponsd to this one.
|
|
|
|
|
inp := testInputs[in.PreviousOutPoint]
|
|
|
|
|
require.NotNil(t, inp)
|
|
|
|
|
|
|
|
|
|
require.Equal(
|
|
|
|
|
t, tx.TxOut[i].Value,
|
|
|
|
|
inp.SignDesc().Output.Value,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// Six inputs added, which 3 different locktimes.
|
|
|
|
|
// Should result in 3 sweeps.
|
|
|
|
|
name: "six inputs",
|
|
|
|
|
inputs: []*testInput{
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[0],
|
|
|
|
|
locktime: &locktime1,
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: aPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[0].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[1],
|
|
|
|
|
locktime: &locktime1,
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: bPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[1].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[2],
|
|
|
|
|
locktime: &locktime2,
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: cPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[2].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[3],
|
|
|
|
|
locktime: &locktime2,
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: dPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[3].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[4],
|
|
|
|
|
locktime: &locktime3,
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: ePkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[4].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
BaseInput: inputs[5],
|
|
|
|
|
locktime: &locktime3,
|
|
|
|
|
reqTxOut: &wire.TxOut{
|
2021-09-23 21:40:37 +02:00
|
|
|
|
PkScript: fPkScript,
|
2020-11-09 18:56:26 +01:00
|
|
|
|
Value: inputs[5].SignDesc().Output.Value,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// We expect three sweeps, each having two of our
|
|
|
|
|
// inputs, one extra input and output to pay fees.
|
|
|
|
|
assertSweeps: func(t *testing.T,
|
|
|
|
|
testInputs map[wire.OutPoint]*testInput,
|
|
|
|
|
txs []*wire.MsgTx) {
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 3, len(txs))
|
|
|
|
|
|
|
|
|
|
for _, tx := range txs {
|
|
|
|
|
require.Equal(t, 3, len(tx.TxIn))
|
|
|
|
|
require.Equal(t, 3, len(tx.TxOut))
|
|
|
|
|
|
|
|
|
|
// The inputs and outputs must be in
|
|
|
|
|
// the same order.
|
|
|
|
|
for i, in := range tx.TxIn {
|
|
|
|
|
// Last one is the change
|
|
|
|
|
// output, so we'll skip it.
|
|
|
|
|
if i == 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get this input to ensure the
|
|
|
|
|
// output on index i coresponsd
|
|
|
|
|
// to this one.
|
|
|
|
|
inp := testInputs[in.PreviousOutPoint]
|
|
|
|
|
require.NotNil(t, inp)
|
|
|
|
|
|
|
|
|
|
require.Equal(
|
|
|
|
|
t, tx.TxOut[i].Value,
|
|
|
|
|
inp.SignDesc().Output.Value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Check that the locktimes are
|
|
|
|
|
// kept intact.
|
|
|
|
|
require.Equal(
|
|
|
|
|
t, tx.LockTime,
|
|
|
|
|
*inp.locktime,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, testCase := range testCases {
|
|
|
|
|
testCase := testCase
|
|
|
|
|
|
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// We increase the number of max inputs to a tx so that
|
|
|
|
|
// won't impact our test.
|
|
|
|
|
ctx.sweeper.cfg.MaxInputsPerTx = 100
|
|
|
|
|
|
|
|
|
|
// Sweep all test inputs.
|
|
|
|
|
var (
|
|
|
|
|
inputs = make(map[wire.OutPoint]*testInput)
|
|
|
|
|
results = make(map[wire.OutPoint]chan Result)
|
|
|
|
|
)
|
|
|
|
|
for _, inp := range testCase.inputs {
|
|
|
|
|
result, err := ctx.sweeper.SweepInput(
|
|
|
|
|
inp, Params{
|
2023-11-03 12:20:10 +01:00
|
|
|
|
Fee: FeeEstimateInfo{
|
|
|
|
|
ConfTarget: 6,
|
|
|
|
|
},
|
2020-11-09 18:56:26 +01:00
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
op := inp.OutPoint()
|
|
|
|
|
results[*op] = result
|
|
|
|
|
inputs[*op] = inp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check the sweeps transactions, ensuring all inputs
|
|
|
|
|
// are there, and all the locktimes are satisfied.
|
|
|
|
|
var sweeps []*wire.MsgTx
|
|
|
|
|
Loop:
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case tx := <-ctx.publishChan:
|
|
|
|
|
sweeps = append(sweeps, &tx)
|
|
|
|
|
case <-time.After(200 * time.Millisecond):
|
|
|
|
|
break Loop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mine the sweeps.
|
|
|
|
|
ctx.backend.mine()
|
|
|
|
|
|
|
|
|
|
// Results should all come back.
|
|
|
|
|
for _, resultChan := range results {
|
|
|
|
|
result := <-resultChan
|
|
|
|
|
if result.Err != nil {
|
|
|
|
|
t.Fatalf("expected input to be "+
|
|
|
|
|
"swept: %v", result.Err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assert the transactions are what we expect.
|
|
|
|
|
testCase.assertSweeps(t, inputs, sweeps)
|
2020-12-07 10:40:08 +01:00
|
|
|
|
|
|
|
|
|
// Finally we assert that all our test inputs were part
|
|
|
|
|
// of the sweeps, and that they were signed correctly.
|
|
|
|
|
sweptInputs := make(map[wire.OutPoint]struct{})
|
|
|
|
|
for _, sweep := range sweeps {
|
|
|
|
|
swept := assertSignedIndex(t, sweep, inputs)
|
|
|
|
|
for op := range swept {
|
|
|
|
|
if _, ok := sweptInputs[op]; ok {
|
|
|
|
|
t.Fatalf("outpoint %v part of "+
|
|
|
|
|
"previous sweep", op)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sweptInputs[op] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require.Equal(t, len(inputs), len(sweptInputs))
|
|
|
|
|
for op := range sweptInputs {
|
|
|
|
|
_, ok := inputs[op]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("swept input %v not part of "+
|
|
|
|
|
"test inputs", op)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-09 18:56:26 +01:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-09 12:24:01 +01:00
|
|
|
|
|
|
|
|
|
// TestSweeperShutdownHandling tests that we notify callers when the sweeper
|
|
|
|
|
// cannot handle requests since it's in the process of shutting down.
|
|
|
|
|
func TestSweeperShutdownHandling(t *testing.T) {
|
|
|
|
|
ctx := createSweeperTestContext(t)
|
|
|
|
|
|
|
|
|
|
// Make the backing notifier break down. This is what happens during
|
|
|
|
|
// lnd shut down, since the notifier is stopped before the sweeper.
|
|
|
|
|
require.Len(t, ctx.notifier.epochChan, 1)
|
|
|
|
|
for epochChan := range ctx.notifier.epochChan {
|
|
|
|
|
close(epochChan)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Give the collector some time to exit.
|
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
// Now trying to sweep inputs should return an error on the error
|
|
|
|
|
// channel.
|
|
|
|
|
resultChan, err := ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[0], defaultFeePref,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case res := <-resultChan:
|
|
|
|
|
require.Equal(t, ErrSweeperShuttingDown, res.Err)
|
|
|
|
|
|
|
|
|
|
case <-time.After(defaultTestTimeout):
|
|
|
|
|
t.Fatalf("no result arrived")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop the sweeper properly.
|
|
|
|
|
err = ctx.sweeper.Stop()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Now attempting to sweep an input should error out immediately.
|
|
|
|
|
_, err = ctx.sweeper.SweepInput(
|
|
|
|
|
spendableInputs[0], defaultFeePref,
|
|
|
|
|
)
|
|
|
|
|
require.Error(t, err)
|
|
|
|
|
}
|
2023-07-12 10:23:29 +02:00
|
|
|
|
|
2023-10-12 14:35:46 +02:00
|
|
|
|
// TestGetInputLists checks that the expected input sets are returned based on
|
|
|
|
|
// whether there are retried inputs or not.
|
|
|
|
|
func TestGetInputLists(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
// Create a test param with a dummy fee preference. This is needed so
|
|
|
|
|
// `feeRateForPreference` won't throw an error.
|
2023-11-03 12:20:10 +01:00
|
|
|
|
param := Params{Fee: FeeEstimateInfo{ConfTarget: 1}}
|
2023-10-12 14:35:46 +02:00
|
|
|
|
|
|
|
|
|
// Create a mock input and mock all the methods used in this test.
|
|
|
|
|
testInput := &input.MockInput{}
|
|
|
|
|
testInput.On("RequiredLockTime").Return(0, false)
|
|
|
|
|
testInput.On("WitnessType").Return(input.CommitmentAnchor)
|
|
|
|
|
testInput.On("OutPoint").Return(&wire.OutPoint{Index: 1})
|
|
|
|
|
testInput.On("RequiredTxOut").Return(nil)
|
|
|
|
|
testInput.On("UnconfParent").Return(nil)
|
|
|
|
|
testInput.On("SignDesc").Return(&input.SignDescriptor{
|
|
|
|
|
Output: &wire.TxOut{Value: 100_000},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create a new and a retried input.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: we use the same input.Input for both pending inputs as we only
|
|
|
|
|
// test the logic of returning the correct non-nil input sets, and not
|
|
|
|
|
// the content the of sets. To validate the content of the sets, we
|
|
|
|
|
// should test `generateInputPartitionings` instead.
|
|
|
|
|
newInput := &pendingInput{
|
|
|
|
|
Input: testInput,
|
|
|
|
|
params: param,
|
|
|
|
|
}
|
|
|
|
|
oldInput := &pendingInput{
|
|
|
|
|
Input: testInput,
|
|
|
|
|
params: param,
|
|
|
|
|
publishAttempts: 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clusterNew contains only new inputs.
|
|
|
|
|
clusterNew := pendingInputs{
|
|
|
|
|
wire.OutPoint{Index: 1}: newInput,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clusterMixed contains a mixed of new and retried inputs.
|
|
|
|
|
clusterMixed := pendingInputs{
|
|
|
|
|
wire.OutPoint{Index: 1}: newInput,
|
|
|
|
|
wire.OutPoint{Index: 2}: oldInput,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clusterOld contains only retried inputs.
|
|
|
|
|
clusterOld := pendingInputs{
|
|
|
|
|
wire.OutPoint{Index: 2}: oldInput,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a test sweeper.
|
|
|
|
|
s := New(&UtxoSweeperConfig{
|
|
|
|
|
MaxInputsPerTx: DefaultMaxInputsPerTx,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
cluster inputCluster
|
|
|
|
|
expectedNilAllSet bool
|
|
|
|
|
expectNilNewSet bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
// When there are only new inputs, we'd expect the
|
|
|
|
|
// first returned set(allSets) to be empty.
|
|
|
|
|
name: "new inputs only",
|
|
|
|
|
cluster: inputCluster{inputs: clusterNew},
|
|
|
|
|
expectedNilAllSet: true,
|
|
|
|
|
expectNilNewSet: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// When there are only retried inputs, we'd expect the
|
|
|
|
|
// second returned set(newSet) to be empty.
|
|
|
|
|
name: "retried inputs only",
|
|
|
|
|
cluster: inputCluster{inputs: clusterOld},
|
|
|
|
|
expectedNilAllSet: false,
|
|
|
|
|
expectNilNewSet: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// When there are mixed inputs, we'd expect two sets
|
|
|
|
|
// are returned.
|
|
|
|
|
name: "mixed inputs",
|
|
|
|
|
cluster: inputCluster{inputs: clusterMixed},
|
|
|
|
|
expectedNilAllSet: false,
|
|
|
|
|
expectNilNewSet: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
|
tc := tc
|
|
|
|
|
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2023-10-24 01:14:55 +02:00
|
|
|
|
allSets, newSets, err := s.getInputLists(tc.cluster)
|
2023-10-12 14:35:46 +02:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
if tc.expectNilNewSet {
|
|
|
|
|
require.Nil(t, newSets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tc.expectedNilAllSet {
|
|
|
|
|
require.Nil(t, allSets)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-24 02:14:52 +02:00
|
|
|
|
|
|
|
|
|
// TestMarkInputsPublished checks that given a list of inputs with different
|
|
|
|
|
// states, only the state `StatePendingPublish` will be marked as `Published`.
|
|
|
|
|
func TestMarkInputsPublished(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Create a mock sweeper store.
|
|
|
|
|
mockStore := NewMockSweeperStore()
|
|
|
|
|
|
|
|
|
|
// Create a test TxRecord and a dummy error.
|
|
|
|
|
dummyTR := &TxRecord{}
|
|
|
|
|
dummyErr := errors.New("dummy error")
|
|
|
|
|
|
|
|
|
|
// Create a test sweeper.
|
|
|
|
|
s := New(&UtxoSweeperConfig{
|
|
|
|
|
Store: mockStore,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create three testing inputs.
|
|
|
|
|
//
|
|
|
|
|
// inputNotExist specifies an input that's not found in the sweeper's
|
|
|
|
|
// `pendingInputs` map.
|
|
|
|
|
inputNotExist := &wire.TxIn{
|
|
|
|
|
PreviousOutPoint: wire.OutPoint{Index: 1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inputInit specifies a newly created input. When marking this as
|
|
|
|
|
// published, we should see an error log as this input hasn't been
|
|
|
|
|
// published yet.
|
|
|
|
|
inputInit := &wire.TxIn{
|
|
|
|
|
PreviousOutPoint: wire.OutPoint{Index: 2},
|
|
|
|
|
}
|
|
|
|
|
s.pendingInputs[inputInit.PreviousOutPoint] = &pendingInput{
|
|
|
|
|
state: StateInit,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inputPendingPublish specifies an input that's about to be published.
|
|
|
|
|
inputPendingPublish := &wire.TxIn{
|
|
|
|
|
PreviousOutPoint: wire.OutPoint{Index: 3},
|
|
|
|
|
}
|
|
|
|
|
s.pendingInputs[inputPendingPublish.PreviousOutPoint] = &pendingInput{
|
|
|
|
|
state: StatePendingPublish,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First, check that when an error is returned from db, it's properly
|
|
|
|
|
// returned here.
|
|
|
|
|
mockStore.On("StoreTx", dummyTR).Return(dummyErr).Once()
|
|
|
|
|
err := s.markInputsPublished(dummyTR, nil)
|
|
|
|
|
require.ErrorIs(err, dummyErr)
|
|
|
|
|
|
|
|
|
|
// We also expect the record has been marked as published.
|
|
|
|
|
require.True(dummyTR.Published)
|
|
|
|
|
|
|
|
|
|
// Then, check that the target input has will be correctly marked as
|
|
|
|
|
// published.
|
|
|
|
|
//
|
|
|
|
|
// Mock the store to return nil
|
|
|
|
|
mockStore.On("StoreTx", dummyTR).Return(nil).Once()
|
|
|
|
|
|
|
|
|
|
// Mark the test inputs. We expect the non-exist input and the
|
|
|
|
|
// inputInit to be skipped, and the final input to be marked as
|
|
|
|
|
// published.
|
|
|
|
|
err = s.markInputsPublished(dummyTR, []*wire.TxIn{
|
|
|
|
|
inputNotExist, inputInit, inputPendingPublish,
|
|
|
|
|
})
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
// We expect unchanged number of pending inputs.
|
|
|
|
|
require.Len(s.pendingInputs, 2)
|
|
|
|
|
|
|
|
|
|
// We expect the init input's state to stay unchanged.
|
|
|
|
|
require.Equal(StateInit,
|
|
|
|
|
s.pendingInputs[inputInit.PreviousOutPoint].state)
|
|
|
|
|
|
|
|
|
|
// We expect the pending-publish input's is now marked as published.
|
|
|
|
|
require.Equal(StatePublished,
|
|
|
|
|
s.pendingInputs[inputPendingPublish.PreviousOutPoint].state)
|
|
|
|
|
|
|
|
|
|
// Assert mocked statements are executed as expected.
|
|
|
|
|
mockStore.AssertExpectations(t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMarkInputsPublishFailed checks that given a list of inputs with
|
|
|
|
|
// different states, only the state `StatePendingPublish` will be marked as
|
|
|
|
|
// `PublishFailed`.
|
|
|
|
|
func TestMarkInputsPublishFailed(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Create a mock sweeper store.
|
|
|
|
|
mockStore := NewMockSweeperStore()
|
|
|
|
|
|
|
|
|
|
// Create a test sweeper.
|
|
|
|
|
s := New(&UtxoSweeperConfig{
|
|
|
|
|
Store: mockStore,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create three testing inputs.
|
|
|
|
|
//
|
|
|
|
|
// inputNotExist specifies an input that's not found in the sweeper's
|
|
|
|
|
// `pendingInputs` map.
|
|
|
|
|
inputNotExist := &wire.TxIn{
|
|
|
|
|
PreviousOutPoint: wire.OutPoint{Index: 1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inputInit specifies a newly created input. When marking this as
|
|
|
|
|
// published, we should see an error log as this input hasn't been
|
|
|
|
|
// published yet.
|
|
|
|
|
inputInit := &wire.TxIn{
|
|
|
|
|
PreviousOutPoint: wire.OutPoint{Index: 2},
|
|
|
|
|
}
|
|
|
|
|
s.pendingInputs[inputInit.PreviousOutPoint] = &pendingInput{
|
|
|
|
|
state: StateInit,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inputPendingPublish specifies an input that's about to be published.
|
|
|
|
|
inputPendingPublish := &wire.TxIn{
|
|
|
|
|
PreviousOutPoint: wire.OutPoint{Index: 3},
|
|
|
|
|
}
|
|
|
|
|
s.pendingInputs[inputPendingPublish.PreviousOutPoint] = &pendingInput{
|
|
|
|
|
state: StatePendingPublish,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark the test inputs. We expect the non-exist input and the
|
|
|
|
|
// inputInit to be skipped, and the final input to be marked as
|
|
|
|
|
// published.
|
|
|
|
|
s.markInputsPublishFailed([]*wire.TxIn{
|
|
|
|
|
inputNotExist, inputInit, inputPendingPublish,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// We expect unchanged number of pending inputs.
|
|
|
|
|
require.Len(s.pendingInputs, 2)
|
|
|
|
|
|
|
|
|
|
// We expect the init input's state to stay unchanged.
|
|
|
|
|
require.Equal(StateInit,
|
|
|
|
|
s.pendingInputs[inputInit.PreviousOutPoint].state)
|
|
|
|
|
|
|
|
|
|
// We expect the pending-publish input's is now marked as publish
|
|
|
|
|
// failed.
|
|
|
|
|
require.Equal(StatePublishFailed,
|
|
|
|
|
s.pendingInputs[inputPendingPublish.PreviousOutPoint].state)
|
|
|
|
|
|
|
|
|
|
// Assert mocked statements are executed as expected.
|
|
|
|
|
mockStore.AssertExpectations(t)
|
|
|
|
|
}
|
2023-10-24 07:14:21 +02:00
|
|
|
|
|
|
|
|
|
// TestMempoolLookup checks that the method `mempoolLookup` works as expected.
|
|
|
|
|
func TestMempoolLookup(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Create a test outpoint.
|
|
|
|
|
op := wire.OutPoint{Index: 1}
|
|
|
|
|
|
|
|
|
|
// Create a mock mempool watcher.
|
|
|
|
|
mockMempool := chainntnfs.NewMockMempoolWatcher()
|
|
|
|
|
|
|
|
|
|
// Create a test sweeper without a mempool.
|
|
|
|
|
s := New(&UtxoSweeperConfig{})
|
|
|
|
|
|
|
|
|
|
// Since we don't have a mempool, we expect the call to return an empty
|
|
|
|
|
// transaction plus a false value indicating it's not found.
|
|
|
|
|
tx, found := s.mempoolLookup(op)
|
|
|
|
|
require.Nil(tx)
|
|
|
|
|
require.False(found)
|
|
|
|
|
|
|
|
|
|
// Re-create the sweeper with the mocked mempool watcher.
|
|
|
|
|
s = New(&UtxoSweeperConfig{
|
|
|
|
|
Mempool: mockMempool,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create a mempool spend event to be returned by the mempool watcher.
|
|
|
|
|
spendChan := make(chan *chainntnfs.SpendDetail, 1)
|
|
|
|
|
spendEvent := &chainntnfs.MempoolSpendEvent{
|
|
|
|
|
Spend: spendChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mock the cancel subscription calls.
|
|
|
|
|
mockMempool.On("CancelMempoolSpendEvent", spendEvent)
|
|
|
|
|
|
|
|
|
|
// Mock the mempool watcher to return an error.
|
|
|
|
|
dummyErr := errors.New("dummy err")
|
|
|
|
|
mockMempool.On("SubscribeMempoolSpent", op).Return(nil, dummyErr).Once()
|
|
|
|
|
|
|
|
|
|
// We expect a nil tx and a false value to be returned.
|
|
|
|
|
//
|
|
|
|
|
// TODO(yy): this means the behavior of not having a mempool is the
|
|
|
|
|
// same as an erroneous mempool. The question is should we
|
|
|
|
|
// differentiate the two from their returned values?
|
|
|
|
|
tx, found = s.mempoolLookup(op)
|
|
|
|
|
require.Nil(tx)
|
|
|
|
|
require.False(found)
|
|
|
|
|
|
|
|
|
|
// Mock the mempool to subscribe to the outpoint.
|
|
|
|
|
mockMempool.On("SubscribeMempoolSpent", op).Return(
|
|
|
|
|
spendEvent, nil).Once()
|
|
|
|
|
|
|
|
|
|
// Without sending a spending details to the `spendChan`, we still
|
|
|
|
|
// expect a nil tx and a false value to be returned.
|
|
|
|
|
tx, found = s.mempoolLookup(op)
|
|
|
|
|
require.Nil(tx)
|
|
|
|
|
require.False(found)
|
|
|
|
|
|
|
|
|
|
// Send a dummy spending details to the `spendChan`.
|
|
|
|
|
dummyTx := &wire.MsgTx{}
|
|
|
|
|
spendChan <- &chainntnfs.SpendDetail{
|
|
|
|
|
SpendingTx: dummyTx,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mock the mempool to subscribe to the outpoint.
|
|
|
|
|
mockMempool.On("SubscribeMempoolSpent", op).Return(
|
|
|
|
|
spendEvent, nil).Once()
|
|
|
|
|
|
|
|
|
|
// Calling the loopup again, we expect the dummyTx to be returned.
|
|
|
|
|
tx, found = s.mempoolLookup(op)
|
|
|
|
|
require.Equal(dummyTx, tx)
|
|
|
|
|
require.True(found)
|
|
|
|
|
|
|
|
|
|
mockMempool.AssertExpectations(t)
|
|
|
|
|
}
|
2023-10-24 07:47:14 +02:00
|
|
|
|
|
|
|
|
|
// TestUpdateSweeperInputs checks that the method `updateSweeperInputs` will
|
|
|
|
|
// properly update the inputs based on their states.
|
|
|
|
|
func TestUpdateSweeperInputs(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Create a test sweeper.
|
|
|
|
|
s := New(nil)
|
|
|
|
|
|
|
|
|
|
// Create a list of inputs using all the states.
|
|
|
|
|
input0 := &pendingInput{state: StateInit}
|
|
|
|
|
input1 := &pendingInput{state: StatePendingPublish}
|
|
|
|
|
input2 := &pendingInput{state: StatePublished}
|
|
|
|
|
input3 := &pendingInput{state: StatePublishFailed}
|
|
|
|
|
input4 := &pendingInput{state: StateSwept}
|
|
|
|
|
input5 := &pendingInput{state: StateExcluded}
|
|
|
|
|
input6 := &pendingInput{state: StateFailed}
|
|
|
|
|
|
|
|
|
|
// Add the inputs to the sweeper. After the update, we should see the
|
|
|
|
|
// terminated inputs being removed.
|
|
|
|
|
s.pendingInputs = map[wire.OutPoint]*pendingInput{
|
|
|
|
|
{Index: 0}: input0,
|
|
|
|
|
{Index: 1}: input1,
|
|
|
|
|
{Index: 2}: input2,
|
|
|
|
|
{Index: 3}: input3,
|
|
|
|
|
{Index: 4}: input4,
|
|
|
|
|
{Index: 5}: input5,
|
|
|
|
|
{Index: 6}: input6,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We expect the inputs with `StateSwept`, `StateExcluded`, and
|
|
|
|
|
// `StateFailed` to be removed.
|
|
|
|
|
expectedInputs := map[wire.OutPoint]*pendingInput{
|
|
|
|
|
{Index: 0}: input0,
|
|
|
|
|
{Index: 1}: input1,
|
|
|
|
|
{Index: 2}: input2,
|
|
|
|
|
{Index: 3}: input3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We expect only the inputs with `StateInit` and `StatePublishFailed`
|
|
|
|
|
// to be returned.
|
|
|
|
|
expectedReturn := map[wire.OutPoint]*pendingInput{
|
|
|
|
|
{Index: 0}: input0,
|
|
|
|
|
{Index: 3}: input3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the sweeper inputs.
|
|
|
|
|
inputs := s.updateSweeperInputs()
|
|
|
|
|
|
|
|
|
|
// Assert the returned inputs are as expected.
|
|
|
|
|
require.Equal(expectedReturn, inputs)
|
|
|
|
|
|
|
|
|
|
// Assert the sweeper inputs are as expected.
|
|
|
|
|
require.Equal(expectedInputs, s.pendingInputs)
|
|
|
|
|
}
|
2023-10-26 08:27:46 +02:00
|
|
|
|
|
|
|
|
|
// TestAttachAvailableRBFInfo checks that the RBF info is attached to the
|
|
|
|
|
// pending input, along with the state being marked as published, when this
|
|
|
|
|
// input can be found both in mempool and the sweeper store.
|
|
|
|
|
func TestAttachAvailableRBFInfo(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Create a test outpoint.
|
|
|
|
|
op := wire.OutPoint{Index: 1}
|
|
|
|
|
|
|
|
|
|
// Create a mock input.
|
|
|
|
|
testInput := &input.MockInput{}
|
|
|
|
|
testInput.On("OutPoint").Return(&op)
|
|
|
|
|
pi := &pendingInput{
|
|
|
|
|
Input: testInput,
|
|
|
|
|
state: StateInit,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a mock mempool watcher and a mock sweeper store.
|
|
|
|
|
mockMempool := chainntnfs.NewMockMempoolWatcher()
|
|
|
|
|
mockStore := NewMockSweeperStore()
|
|
|
|
|
|
|
|
|
|
// Create a mempool spend event to be returned by the mempool watcher.
|
|
|
|
|
spendChan := make(chan *chainntnfs.SpendDetail, 1)
|
|
|
|
|
spendEvent := &chainntnfs.MempoolSpendEvent{
|
|
|
|
|
Spend: spendChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mock the cancel subscription calls.
|
|
|
|
|
mockMempool.On("CancelMempoolSpendEvent", spendEvent)
|
|
|
|
|
|
|
|
|
|
// Create a test sweeper.
|
|
|
|
|
s := New(&UtxoSweeperConfig{
|
|
|
|
|
Store: mockStore,
|
|
|
|
|
Mempool: mockMempool,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// First, mock the mempool to return an error.
|
|
|
|
|
dummyErr := errors.New("dummy err")
|
|
|
|
|
mockMempool.On("SubscribeMempoolSpent", op).Return(nil, dummyErr).Once()
|
|
|
|
|
|
|
|
|
|
// Since the mempool lookup failed, we exepect the original pending
|
|
|
|
|
// input to stay unchanged.
|
|
|
|
|
result := s.attachAvailableRBFInfo(pi)
|
|
|
|
|
require.True(result.rbf.IsNone())
|
|
|
|
|
require.Equal(StateInit, result.state)
|
|
|
|
|
|
|
|
|
|
// Mock the mempool lookup to return a tx three times.
|
|
|
|
|
tx := &wire.MsgTx{}
|
|
|
|
|
mockMempool.On("SubscribeMempoolSpent", op).Return(
|
|
|
|
|
spendEvent, nil).Times(3).Run(func(_ mock.Arguments) {
|
|
|
|
|
// Eeac time the method is called, we send a tx to the spend
|
|
|
|
|
// channel.
|
|
|
|
|
spendChan <- &chainntnfs.SpendDetail{
|
|
|
|
|
SpendingTx: tx,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Mock the store to return an error saying the tx cannot be found.
|
|
|
|
|
mockStore.On("GetTx", tx.TxHash()).Return(nil, ErrTxNotFound).Once()
|
|
|
|
|
|
|
|
|
|
// Although the db lookup failed, the pending input should have been
|
|
|
|
|
// marked as published without attaching any RBF info.
|
|
|
|
|
result = s.attachAvailableRBFInfo(pi)
|
|
|
|
|
require.True(result.rbf.IsNone())
|
|
|
|
|
require.Equal(StatePublished, result.state)
|
|
|
|
|
|
|
|
|
|
// Mock the store to return a db error.
|
|
|
|
|
mockStore.On("GetTx", tx.TxHash()).Return(nil, dummyErr).Once()
|
|
|
|
|
|
|
|
|
|
// Although the db lookup failed, the pending input should have been
|
|
|
|
|
// marked as published without attaching any RBF info.
|
|
|
|
|
result = s.attachAvailableRBFInfo(pi)
|
|
|
|
|
require.True(result.rbf.IsNone())
|
|
|
|
|
require.Equal(StatePublished, result.state)
|
|
|
|
|
|
|
|
|
|
// Mock the store to return a record.
|
|
|
|
|
tr := &TxRecord{
|
|
|
|
|
Fee: 100,
|
|
|
|
|
FeeRate: 100,
|
|
|
|
|
}
|
|
|
|
|
mockStore.On("GetTx", tx.TxHash()).Return(tr, nil).Once()
|
|
|
|
|
|
|
|
|
|
// Call the method again.
|
|
|
|
|
result = s.attachAvailableRBFInfo(pi)
|
|
|
|
|
|
|
|
|
|
// Assert that the RBF info is attached to the pending input.
|
|
|
|
|
rbfInfo := fn.Some(RBFInfo{
|
|
|
|
|
Txid: tx.TxHash(),
|
|
|
|
|
Fee: btcutil.Amount(tr.Fee),
|
|
|
|
|
FeeRate: chainfee.SatPerKWeight(tr.FeeRate),
|
|
|
|
|
})
|
|
|
|
|
require.Equal(rbfInfo, result.rbf)
|
|
|
|
|
|
|
|
|
|
// Assert the state is updated.
|
|
|
|
|
require.Equal(StatePublished, result.state)
|
|
|
|
|
|
|
|
|
|
// Assert mocked statements.
|
|
|
|
|
testInput.AssertExpectations(t)
|
|
|
|
|
mockMempool.AssertExpectations(t)
|
|
|
|
|
mockStore.AssertExpectations(t)
|
|
|
|
|
}
|