loop/sweepbatcher/sweep_batcher_presigned_test.go

2575 lines
69 KiB
Go
Raw Permalink Normal View History

package sweepbatcher
import (
"bytes"
"context"
"fmt"
"os"
"sync"
"testing"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockPresignedHelper implements PresignedHelper interface and stores arguments
// passed in its methods to validate correctness of function publishPresigned.
type mockPresignedHelper struct {
// onlineOutpoints specifies which outpoints are capable of
// participating in presigning.
onlineOutpoints map[wire.OutPoint]bool
// changeOutputs is a map of change outputs for a given primary deposit.
changeOutputs map[wire.OutPoint]*wire.TxOut
// presignedBatches is the collection of presigned batches. The key is
// primarySweepID.
presignedBatches map[wire.OutPoint][]*wire.MsgTx
// mu should be hold by all the public methods of this type.
mu sync.Mutex
// cleanupCalled is a channel where an element is sent every time
// CleanupTransactions is called.
cleanupCalled chan struct{}
}
// newMockPresignedHelper returns new instance of mockPresignedHelper.
func newMockPresignedHelper() *mockPresignedHelper {
return &mockPresignedHelper{
onlineOutpoints: make(map[wire.OutPoint]bool),
changeOutputs: make(map[wire.OutPoint]*wire.TxOut),
presignedBatches: make(map[wire.OutPoint][]*wire.MsgTx),
cleanupCalled: make(chan struct{}),
}
}
// SetOutpointOnline changes the online status of an outpoint.
func (h *mockPresignedHelper) SetOutpointOnline(op wire.OutPoint, online bool) {
h.mu.Lock()
defer h.mu.Unlock()
h.onlineOutpoints[op] = online
}
// setChangeForPrimaryDeposit sets the change output of a primary deposit sweep.
func (h *mockPresignedHelper) setChangeForPrimaryDeposit(op wire.OutPoint,
change *wire.TxOut) {
h.mu.Lock()
defer h.mu.Unlock()
h.changeOutputs[op] = change
}
// offlineInputs returns inputs of a tx which are offline.
func (h *mockPresignedHelper) offlineInputs(tx *wire.MsgTx) []wire.OutPoint {
offline := make([]wire.OutPoint, 0, len(tx.TxIn))
for _, txIn := range tx.TxIn {
if !h.onlineOutpoints[txIn.PreviousOutPoint] {
offline = append(offline, txIn.PreviousOutPoint)
}
}
return offline
}
// sign signs the transaction.
func (h *mockPresignedHelper) sign(tx *wire.MsgTx) {
// Sign all the inputs.
for i := range tx.TxIn {
tx.TxIn[i].Witness = wire.TxWitness{
make([]byte, 64),
}
}
}
// getTxFeerate returns fee rate of a transaction.
func (h *mockPresignedHelper) getTxFeerate(tx *wire.MsgTx,
inputAmt btcutil.Amount) chainfee.SatPerKWeight {
// "Sign" tx's copy to assess the weight.
tx2 := tx.Copy()
h.sign(tx2)
weight := lntypes.WeightUnit(
blockchain.GetTransactionWeight(btcutil.NewTx(tx2)),
)
fee := inputAmt - btcutil.Amount(tx.TxOut[0].Value)
return chainfee.NewSatPerKWeight(fee, weight)
}
// DestPkScript returns destination pkScript used in presigned tx sweeping
// these inputs.
func (h *mockPresignedHelper) DestPkScript(ctx context.Context,
primarySweepID wire.OutPoint) ([]byte, error) {
h.mu.Lock()
defer h.mu.Unlock()
for _, tx := range h.presignedBatches[primarySweepID] {
return tx.TxOut[0].PkScript, nil
}
return nil, fmt.Errorf("tx with primarySweepID %v not found",
primarySweepID)
}
// SignTx tries to sign the transaction. If all the inputs are online, it signs
// the exact transaction passed and adds it to presignedBatches. Otherwise, it
// looks for a transaction in presignedBatches satisfying the criteria.
func (h *mockPresignedHelper) SignTx(ctx context.Context,
primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount,
minRelayFee, feeRate chainfee.SatPerKWeight,
loadOnly bool) (*wire.MsgTx, error) {
h.mu.Lock()
defer h.mu.Unlock()
if feeRate < minRelayFee {
return nil, fmt.Errorf("feeRate (%v) is below minRelayFee (%v)",
feeRate, minRelayFee)
}
if !hasInput(tx, primarySweepID) {
return nil, fmt.Errorf("primarySweepID %v not in tx",
primarySweepID)
}
// If all the inputs are online and loadOnly is not set, sign this exact
// transaction.
if offline := h.offlineInputs(tx); len(offline) == 0 && !loadOnly {
tx = tx.Copy()
h.sign(tx)
// Add to the collection.
h.presignedBatches[primarySweepID] = append(
h.presignedBatches[primarySweepID], tx,
)
return tx, nil
}
// Try to find a transaction in the collection satisfying all the
// criteria of PresignedHelper.SignTx. If there are many such
// transactions, select a transaction with feerate which is the closest
// to the feerate of the input tx.
var (
bestTx *wire.MsgTx
bestFeerateDistance chainfee.SatPerKWeight
)
for _, candidate := range h.presignedBatches[primarySweepID] {
err := CheckSignedTx(tx, candidate, inputAmt, minRelayFee)
if err != nil {
continue
}
feeRateDistance := h.getTxFeerate(candidate, inputAmt) - feeRate
if feeRateDistance < 0 {
feeRateDistance = -feeRateDistance
}
if bestTx == nil || feeRateDistance < bestFeerateDistance {
bestTx = candidate
bestFeerateDistance = feeRateDistance
}
}
if bestTx == nil {
return nil, fmt.Errorf("some outpoint is offline and no " +
"suitable presigned tx found")
}
return bestTx.Copy(), nil
}
// CleanupTransactions removes all transactions related to any of the outpoints.
func (h *mockPresignedHelper) CleanupTransactions(ctx context.Context,
inputs []wire.OutPoint) error {
h.mu.Lock()
defer h.mu.Unlock()
for _, primarySweepID := range inputs {
delete(h.presignedBatches, primarySweepID)
}
h.cleanupCalled <- struct{}{}
return nil
}
// sweepTimeout is swap timeout block height used in tests of presigned mode.
const sweepTimeout = 1000
// FetchSweep returns blank SweepInfo.
// This method implements SweepFetcher interface.
func (h *mockPresignedHelper) FetchSweep(_ context.Context,
_ lntypes.Hash, utxo wire.OutPoint) (*SweepInfo, error) {
h.mu.Lock()
defer h.mu.Unlock()
// Find IsPresigned.
_, isPresigned := h.onlineOutpoints[utxo]
// Find change.
change := h.changeOutputs[utxo]
return &SweepInfo{
// Set Timeout to prevent warning messages about timeout=0.
Timeout: sweepTimeout,
IsPresigned: isPresigned,
HTLC: swap.Htlc{
PkScript: []byte{10, 11, 12},
},
Change: change,
}, nil
}
// testPresigned_forgotten_presign checks that adding sweeps causes the batcher
// to fail if the sweeps were not presigned with PresignSweepsGroup. In addition
// to that it checks that PresignSweepsGroup fails if the outpoint is offline.
func testPresigned_forgotten_presign(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(10_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper))
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create the first sweep.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: 1_000_000,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
// This should fail, because the input is offline.
presignedHelper.SetOutpointOnline(op1, false)
err := batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
sweepTimeout, destAddr, nil,
)
require.Error(t, err)
require.ErrorContains(t, err, "offline")
// Make sure that the batcher crashes if AddSweep is called before
// PresignSweepsGroup even if the input is online.
presignedHelper.SetOutpointOnline(op1, true)
err = batcher.AddSweep(ctx, &sweepReq1)
require.ErrorContains(t, err, "were not presigned")
}
// testPresigned_input1_offline_then_input2 tests presigned mode for the
// following scenario: first input is added, then goes offline, then feerate
// grows, one of presigned transactions is published, and then another online
// input is added and is assigned to another batch.
func testPresigned_input1_offline_then_input2(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const (
feeRateLow = chainfee.SatPerKWeight(10_000)
feeRateMedium = chainfee.SatPerKWeight(30_000)
feeRateHigh = chainfee.SatPerKWeight(31_000)
)
currentFeeRate := feeRateLow
setFeeRate := func(feeRate chainfee.SatPerKWeight) {
currentFeeRate = feeRate
}
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return currentFeeRate, nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper))
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
setFeeRate(feeRateLow)
// Create the first sweep.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: 1_000_000,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
// Enable the input and presign.
presignedHelper.SetOutpointOnline(op1, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Increase fee rate and turn off the input, so it can't sign updated
// tx. The feerate is close to the feerate of one of presigned txs.
setFeeRate(feeRateMedium)
presignedHelper.SetOutpointOnline(op1, false)
// Deliver sweep request to batcher.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 1)
require.Len(t, tx.TxOut, 1)
require.Equal(t, op1, tx.TxIn[0].PreviousOutPoint)
require.Equal(t, int64(988618), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
// Make sure the fee rate is feeRateMedium.
batch := getOnlyBatch(t, ctx, batcher)
var (
numSweeps int
cachedFeeRate chainfee.SatPerKWeight
)
batch.testRunInEventLoop(ctx, func() {
numSweeps = len(batch.sweeps)
cachedFeeRate = batch.rbfCache.FeeRate
})
require.Equal(t, 1, numSweeps)
require.Equal(t, feeRateMedium, cachedFeeRate)
// Raise feerate and trigger new publishing. The tx should be the same.
setFeeRate(feeRateHigh)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
require.NoError(t, lnd.NotifyHeight(601))
tx2 := <-lnd.TxPublishChannel
require.Equal(t, tx.TxHash(), tx2.TxHash())
// Now add another input. It is online, but the first input is still
// offline, so another input should go to another batch.
swapHash2 := lntypes.Hash{2, 2, 2}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
sweepReq2 := SweepRequest{
SwapHash: swapHash2,
Inputs: []Input{{
Value: 2_000_000,
Outpoint: op2,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op2, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op2, Value: 2_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Deliver sweep request to batcher.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
batch2 := <-lnd.TxPublishChannel
require.Len(t, batch2.TxIn, 1)
require.Len(t, batch2.TxOut, 1)
require.Equal(t, op2, batch2.TxIn[0].PreviousOutPoint)
require.Equal(t, int64(1987724), batch2.TxOut[0].Value)
require.Equal(t, batchPkScript, batch2.TxOut[0].PkScript)
// Now confirm the first batch. Make sure its presigned transactions
// were removed, but not the transactions of the second batch.
presignedSize1 := len(presignedHelper.presignedBatches)
tx2hash := tx2.TxHash()
spendDetail := &chainntnfs.SpendDetail{
SpentOutPoint: &op1,
SpendingTx: tx2,
SpenderTxHash: &tx2hash,
SpenderInputIndex: 0,
SpendingHeight: 601,
}
lnd.SpendChannel <- spendDetail
<-lnd.RegisterConfChannel
require.NoError(t, lnd.NotifyHeight(604))
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: tx2,
}
<-presignedHelper.cleanupCalled
presignedSize2 := len(presignedHelper.presignedBatches)
require.Greater(t, presignedSize2, 0)
require.Greater(t, presignedSize1, presignedSize2)
// Make sure we still have presigned transactions for the second batch.
presignedHelper.SetOutpointOnline(op2, false)
const loadOnly = true
_, err = presignedHelper.SignTx(
ctx, op2, batch2, 2_000_000, chainfee.FeePerKwFloor,
chainfee.FeePerKwFloor, loadOnly,
)
require.NoError(t, err)
}
// testPresigned_min_relay_fee tests that online and presigned transactions
// comply with min_relay_fee.
func testPresigned_min_relay_fee(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const inputAmt = 1_000_000
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.FeePerKwFloor, nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper))
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Set high min_relay_fee.
lnd.SetMinRelayFee(252)
// Create the first sweep.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: inputAmt,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
// Enable the input and presign.
presignedHelper.SetOutpointOnline(op1, true)
err := batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op1, Value: inputAmt}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Deliver sweep request to batcher.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
gotFeeRate := presignedHelper.getTxFeerate(tx, inputAmt)
require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
// The only difference of tx2 is a higher lock_time.
lnd.SetMinRelayFee(150)
require.NoError(t, lnd.NotifyHeight(601))
tx2 := <-lnd.TxPublishChannel
require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value)
gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
require.Equal(t, uint32(601), tx2.LockTime)
// Set a higher min_relay_fee, turn off the client and try presigned tx.
lnd.SetMinRelayFee(500)
presignedHelper.SetOutpointOnline(op1, false)
// Check fee rate of the presigned tx broadcasted.
require.NoError(t, lnd.NotifyHeight(602))
tx = <-lnd.TxPublishChannel
gotFeeRate = presignedHelper.getTxFeerate(tx, inputAmt)
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
// LockTime of a presigned tx is 0.
require.Equal(t, uint32(0), tx.LockTime)
// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
// It should re-broadcast the same presigned tx.
lnd.SetMinRelayFee(450)
require.NoError(t, lnd.NotifyHeight(603))
tx2 = <-lnd.TxPublishChannel
require.Equal(t, tx.TxHash(), tx2.TxHash())
gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
// LockTime of a presigned tx is 0.
require.Equal(t, uint32(0), tx2.LockTime)
// Even if the client is back online, fee rate doesn't decrease.
presignedHelper.SetOutpointOnline(op1, true)
require.NoError(t, lnd.NotifyHeight(604))
tx3 := <-lnd.TxPublishChannel
require.Equal(t, tx2.TxOut[0].Value, tx3.TxOut[0].Value)
gotFeeRate = presignedHelper.getTxFeerate(tx3, inputAmt)
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
require.Equal(t, uint32(604), tx3.LockTime)
}
// testPresigned_two_inputs_one_goes_offline tests presigned mode for the
// following scenario: two online inputs are added, then one of them goes
// offline, then feerate grows and a presigned transaction is used.
func testPresigned_two_inputs_one_goes_offline(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const (
feeRateLow = chainfee.SatPerKWeight(10_000)
feeRateMedium = chainfee.SatPerKWeight(30_000)
feeRateHigh = chainfee.SatPerKWeight(40_000)
)
currentFeeRate := feeRateLow
setFeeRate := func(feeRate chainfee.SatPerKWeight) {
currentFeeRate = feeRate
}
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return currentFeeRate, nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
setFeeRate(feeRateLow)
// Create the first sweep.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: 1_000_000,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op1, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Add second sweep.
swapHash2 := lntypes.Hash{2, 2, 2}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
sweepReq2 := SweepRequest{
SwapHash: swapHash2,
Inputs: []Input{{
Value: 2_000_000,
Outpoint: op2,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op2, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op2, Value: 2_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 2)
require.Len(t, tx.TxOut, 1)
require.ElementsMatch(
t, []wire.OutPoint{op1, op2},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
},
)
require.Equal(t, int64(2993740), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
// Now turn off the second input, raise feerate and trigger new
// publishing. The feerate is close to one of the presigned feerates,
// so this should result in RBF.
presignedHelper.SetOutpointOnline(op2, false)
setFeeRate(feeRateMedium)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))
require.NoError(t, lnd.NotifyHeight(601))
tx2 := <-lnd.TxPublishChannel
require.NotEqual(t, tx.TxHash(), tx2.TxHash())
require.Len(t, tx2.TxIn, 2)
require.Len(t, tx2.TxOut, 1)
require.ElementsMatch(
t, []wire.OutPoint{op1, op2},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
},
)
require.Equal(t, int64(2982007), tx2.TxOut[0].Value)
require.Equal(t, batchPkScript, tx2.TxOut[0].PkScript)
}
// testPresigned_first_publish_fails tests presigned mode for the following
// scenario: one input is added and goes offline, feerate grows a transaction is
// attempted to be published, but fails. Then the input goes online and is
// published being signed online.
func testPresigned_first_publish_fails(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const (
feeRateLow = chainfee.SatPerKWeight(10_000)
feeRateMedium = chainfee.SatPerKWeight(30_000)
feeRateHigh = chainfee.SatPerKWeight(40_000)
)
currentFeeRate := feeRateLow
setFeeRate := func(feeRate chainfee.SatPerKWeight) {
currentFeeRate = feeRate
}
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return currentFeeRate, nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
setFeeRate(feeRateLow)
// Create the first sweep.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: 1_000_000,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op1, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
presignedHelper.SetOutpointOnline(op1, false)
// Make sure that publish attempt fails.
lnd.PublishHandler = func(ctx context.Context, tx *wire.MsgTx,
label string) error {
return fmt.Errorf("test error")
}
// Add the sweep, triggering the publish attempt.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Replace the logger in the batch with wrappedLogger to watch messages.
batch := getOnlyBatch(t, ctx, batcher)
testLogger := &wrappedLogger{
Logger: batch.log(),
}
batch.setLog(testLogger)
// Trigger another publish attempt in case the publish error was logged
// before we installed the logger watcher.
require.NoError(t, lnd.NotifyHeight(601))
// Wait for batcher to log the publish error. It is logged with
// publishErrorHandler, so the format is "%s: %v".
require.EventuallyWithT(t, func(c *assert.CollectT) {
testLogger.mu.Lock()
defer testLogger.mu.Unlock()
assert.Contains(c, testLogger.warnMessages, "%s: %v")
}, test.Timeout, eventuallyCheckFrequency)
// Now turn on the first input, raise feerate and trigger new
// publishing, which should succeed.
lnd.PublishHandler = nil
setFeeRate(feeRateMedium)
presignedHelper.SetOutpointOnline(op1, true)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
require.NoError(t, lnd.NotifyHeight(602))
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 1)
require.Len(t, tx.TxOut, 1)
require.Equal(t, op1, tx.TxIn[0].PreviousOutPoint)
require.Equal(t, int64(988120), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
}
// testPresigned_locktime tests presigned mode for the following scenario: one
// input is added and goes offline, feerate grows, but this is constrainted by
// locktime logic, so the published transaction has medium feerate (maximum
// feerate among transactions without locktime protection). Then blocks are
// mined and a transaction with a higher feerate is published.
func testPresigned_locktime(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const (
feeRateLow = chainfee.SatPerKWeight(10_000)
feeRateHigh = chainfee.SatPerKWeight(10_000_000)
)
currentFeeRate := feeRateLow
setFeeRate := func(feeRate chainfee.SatPerKWeight) {
currentFeeRate = feeRate
}
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return currentFeeRate, nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
setFeeRate(feeRateLow)
// Create the first sweep.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: 1_000_000,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op1, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
presignedHelper.SetOutpointOnline(op1, false)
setFeeRate(feeRateHigh)
// Add the sweep, triggering the publish attempt.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 1)
require.Len(t, tx.TxOut, 1)
require.Equal(t, op1, tx.TxIn[0].PreviousOutPoint)
require.Equal(t, int64(966015), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
// Mine blocks to overcome the locktime constraint.
require.NoError(t, lnd.NotifyHeight(950))
tx2 := <-lnd.TxPublishChannel
require.Equal(t, int64(824648), tx2.TxOut[0].Value)
}
// testPresigned_presigned_group tests passing multiple sweeps to the method
// PresignSweepsGroup.
func testPresigned_presigned_group(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(10_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create a swap of two sweeps.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
group1 := []Input{
{
Outpoint: op1,
Value: 1_000_000,
},
{
Outpoint: op2,
Value: 2_000_000,
},
}
// Enable only one of the sweeps.
presignedHelper.SetOutpointOnline(op1, true)
presignedHelper.SetOutpointOnline(op2, false)
// An attempt to presign must fail.
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, nil,
)
require.ErrorContains(t, err, "some outpoint is offline")
// Enable both outpoints.
presignedHelper.SetOutpointOnline(op2, true)
// An attempt to presign must succeed.
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Add the sweep, triggering the publish attempt.
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
}))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 2)
require.Len(t, tx.TxOut, 1)
require.ElementsMatch(
t, []wire.OutPoint{op1, op2},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
},
)
require.Equal(t, int64(2993740), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
// Add another group of sweeps.
swapHash2 := lntypes.Hash{2, 2, 2}
op3 := wire.OutPoint{
Hash: chainhash.Hash{3, 3},
Index: 3,
}
op4 := wire.OutPoint{
Hash: chainhash.Hash{4, 4},
Index: 4,
}
group2 := []Input{
{
Outpoint: op3,
Value: 3_000_000,
},
{
Outpoint: op4,
Value: 4_000_000,
},
}
presignedHelper.SetOutpointOnline(op3, true)
presignedHelper.SetOutpointOnline(op4, true)
// An attempt to presign must succeed.
err = batcher.PresignSweepsGroup(
ctx, group2, sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Add the sweep. It should go to the same batch.
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash2,
Inputs: group2,
Notifier: &dummyNotifier,
}))
// Mine a blocks to trigger republishing.
require.NoError(t, lnd.NotifyHeight(601))
tx = <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 4)
require.Len(t, tx.TxOut, 1)
require.ElementsMatch(
t, []wire.OutPoint{op1, op2, op3, op4},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
tx.TxIn[2].PreviousOutPoint,
tx.TxIn[3].PreviousOutPoint,
},
)
require.Equal(t, int64(9989140), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
// Turn off one of existing outpoints and add another group.
presignedHelper.SetOutpointOnline(op1, false)
swapHash3 := lntypes.Hash{3, 3, 3}
op5 := wire.OutPoint{
Hash: chainhash.Hash{5, 5},
Index: 5,
}
op6 := wire.OutPoint{
Hash: chainhash.Hash{6, 6},
Index: 6,
}
group3 := []Input{
{
Outpoint: op5,
Value: 5_000_000,
},
{
Outpoint: op6,
Value: 6_000_000,
},
}
presignedHelper.SetOutpointOnline(op5, true)
presignedHelper.SetOutpointOnline(op6, true)
// An attempt to presign must succeed.
err = batcher.PresignSweepsGroup(
ctx, group3, sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Add the sweep. It should go to the same batch.
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash3,
Inputs: group3,
Notifier: &dummyNotifier,
}))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
tx = <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 2)
require.Len(t, tx.TxOut, 1)
require.ElementsMatch(
t, []wire.OutPoint{op5, op6},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
},
)
require.Equal(t, int64(10993740), tx.TxOut[0].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
}
// testPresigned_presigned_group_with_change tests passing multiple sweeps to
// the method PresignSweepsGroup. It tests that a change output of a primary
// deposit sweep is properly added to the presigned transaction.
func testPresigned_presigned_group_with_change(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(10_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create a swap of two sweeps.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
group1 := []Input{
{
Outpoint: op1,
Value: 1_000_000,
},
{
Outpoint: op2,
Value: 2_000_000,
},
}
change := &wire.TxOut{
Value: 500_000,
PkScript: []byte{0xaf, 0xfe},
}
presignedHelper.setChangeForPrimaryDeposit(op1, change)
// Enable only one of the sweeps.
presignedHelper.SetOutpointOnline(op1, true)
presignedHelper.SetOutpointOnline(op2, true)
// An attempt to presign shouldn't fail.
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change,
)
require.NoError(t, err)
// Add the sweep, triggering the publishing attempt.
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.NoError(t, err)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 2)
require.Len(t, tx.TxOut, 2)
require.ElementsMatch(
t, []wire.OutPoint{op1, op2},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
},
)
require.Equal(t, int64(2_493_300), tx.TxOut[0].Value)
require.Equal(t, change.Value, tx.TxOut[1].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
require.Equal(t, change.PkScript, tx.TxOut[1].PkScript)
// Mine a blocks to trigger republishing.
require.NoError(t, lnd.NotifyHeight(601))
}
// testPresigned_fee_portion_with_change ensures that the fee portion reported
// to clients accounts for change outputs in the presigned transaction. It also
// is a regression test for feerate overestimation when tx is published.
func testPresigned_fee_portion_with_change(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(10_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
swapHash := lntypes.Hash{2, 2, 2}
op := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
group := []Input{
{
Outpoint: op,
Value: 1_000_000,
},
}
change := &wire.TxOut{
Value: 250_000,
PkScript: []byte{0xca, 0xfe},
}
presignedHelper.setChangeForPrimaryDeposit(op, change)
presignedHelper.SetOutpointOnline(op, true)
require.NoError(t, batcher.PresignSweepsGroup(
ctx, group, sweepTimeout, destAddr, change,
))
spendChan := make(chan *SpendDetail, 1)
confChan := make(chan *ConfDetail, 1)
notifier := &SpendNotifier{
SpendChan: spendChan,
SpendErrChan: make(chan error, 1),
ConfChan: confChan,
ConfErrChan: make(chan error, 1),
QuitChan: make(chan bool, 1),
}
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash,
Inputs: group,
Notifier: notifier,
}))
spendReg := <-lnd.RegisterSpendChannel
require.NotNil(t, spendReg)
require.NotNil(t, spendReg.Outpoint)
require.Equal(t, op, *spendReg.Outpoint)
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 1)
require.Len(t, tx.TxOut, 2)
// Mine a blocks to trigger republishing.
require.NoError(t, lnd.NotifyHeight(601))
// Make sure it is the same tx.
tx2 := <-lnd.TxPublishChannel
require.Len(t, tx2.TxOut, len(tx.TxOut))
require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value)
var (
outputSum int64
foundChange bool
)
for _, txOut := range tx.TxOut {
outputSum += txOut.Value
if txOut.Value != change.Value {
continue
}
if !bytes.Equal(txOut.PkScript, change.PkScript) {
continue
}
foundChange = true
}
require.True(t, foundChange)
totalInput := int64(group[0].Value)
require.LessOrEqual(t, outputSum, totalInput)
expectedFee := btcutil.Amount(totalInput - outputSum)
require.Greater(t, expectedFee, btcutil.Amount(0))
txHash := tx.TxHash()
spendDetail := &chainntnfs.SpendDetail{
SpentOutPoint: &op,
SpendingTx: tx,
SpenderTxHash: &txHash,
SpenderInputIndex: 0,
SpendingHeight: spendReg.HeightHint + 1,
}
lnd.SpendChannel <- spendDetail
spend := <-spendChan
require.Equal(t, expectedFee, spend.OnChainFeePortion)
confReg := <-lnd.RegisterConfChannel
require.True(t, bytes.Equal(tx.TxOut[0].PkScript, confReg.PkScript) ||
bytes.Equal(tx.TxOut[1].PkScript, confReg.PkScript))
require.NoError(
t, lnd.NotifyHeight(spendReg.HeightHint+batchConfHeight+1),
)
lnd.ConfChannel <- &chainntnfs.TxConfirmation{Tx: tx}
require.Eventually(t, func() bool {
select {
case <-presignedHelper.cleanupCalled:
return true
default:
return false
}
}, test.Timeout, eventuallyCheckFrequency)
conf := <-confChan
require.Equal(t, expectedFee, conf.OnChainFeePortion)
}
// testPresigned_presigned_group_with_identical_change_pkscript tests passing multiple sweeps to
// the method PresignSweepsGroup. It tests that a change output of a primary
// deposit sweep is properly added to the presigned transaction.
func testPresigned_presigned_group_with_identical_change_pkscript(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(10_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create two swaps of a single sweep
swapHash1 := lntypes.Hash{1, 1, 1}
swapHash2 := lntypes.Hash{2, 2, 2}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
group1 := []Input{
{
Outpoint: op1,
Value: 1_000_000,
},
}
change1 := &wire.TxOut{
Value: 500_000,
PkScript: []byte{0xaf, 0xfe},
}
group2 := []Input{
{
Outpoint: op2,
Value: 2_000_000,
},
}
change2 := &wire.TxOut{
Value: 600_000,
PkScript: []byte{0xaf, 0xfe},
}
presignedHelper.setChangeForPrimaryDeposit(op1, change1)
presignedHelper.setChangeForPrimaryDeposit(op2, change2)
// Enable only one of the sweeps.
presignedHelper.SetOutpointOnline(op1, true)
presignedHelper.SetOutpointOnline(op2, true)
// An attempt to presign group1 shouldn't fail.
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change1,
)
require.NoError(t, err)
// Add the sweep, triggering the publishing attempt.
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.NoError(t, err)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// An attempt to presign group2 shouldn't fail.
err = batcher.PresignSweepsGroup(
ctx, group2, sweepTimeout, destAddr, change2,
)
require.NoError(t, err)
// Add the sweep, triggering the publishing attempt.
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash2,
Inputs: group2,
Notifier: &dummyNotifier,
})
require.NoError(t, err)
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 2)
require.Len(t, tx.TxOut, 2)
require.ElementsMatch(
t, []wire.OutPoint{op1, op2},
[]wire.OutPoint{
tx.TxIn[0].PreviousOutPoint,
tx.TxIn[1].PreviousOutPoint,
},
)
require.Equal(t, int64(1_893_300), tx.TxOut[0].Value)
require.Equal(t, change1.Value+change2.Value, tx.TxOut[1].Value)
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
require.Equal(t, change1.PkScript, tx.TxOut[1].PkScript)
// Mine a blocks to trigger republishing.
require.NoError(t, lnd.NotifyHeight(601))
}
// testPresigned_presigned_group_with_dust_main_output passes a dust main output
// and a change output to PresignSweepsGroup. It will fail because of the dust
// main output. Note that the min relay fee is set low enough to pass the
// clampBatchFee check, so the error is not related to the fee rate.
func testPresigned_presigned_group_with_dust_main_output(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(100_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create a swap of two sweeps.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
inputValue := int64(1_000_000)
group1 := []Input{
{
Outpoint: op1,
Value: 1_000_000,
},
}
dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
mainOutput := dustLimit
// Let's solve an equation of what the fee should be so it is 20%
// (clamped fee) of fee (itself) + main output.
// fee / (fee+main) = 0.2
// fee = 0.2 fee + 0.2 main
// 0.8 fee = 0.2 main
// fee = 0.25 main
clampedFee := mainOutput / 4
// Set min relay fee low enough to pass the clampBatchFee check.
lnd.SetMinRelayFee(166)
change := &wire.TxOut{
// If "+1" is removed, the group would be added successfully.
Value: inputValue - dustLimit - clampedFee + 1,
PkScript: []byte{0xaf, 0xfe},
}
presignedHelper.setChangeForPrimaryDeposit(op1, change)
// Enable only one of the sweeps.
presignedHelper.SetOutpointOnline(op1, true)
// An attempt to presign must fail.
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change,
)
require.EqualError(t, err, "failed to construct unsigned tx for "+
"feeRate 166 sat/kw: batch amount 0.01000000 BTC is < the "+
"sum of change outputs 0.00999634 BTC plus fee "+
"0.00000073 BTC and dust limit 0.00000294 BTC")
// Add the sweep, triggering the publishing attempt.
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.ErrorContains(t, err, "were not presigned")
// Now let's verify that we found the edge value correctly.
change.Value--
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change,
)
require.NoError(t, err)
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.NoError(t, err)
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
}
// testPresigned_presigned_group_with_dust_below_relay_fee passes a tx with a
// dust main output and one change output to PresignSweepsGroup. The tx fee rate
// is below the min relay fee, hence clampBatchFee will return an error.
func testPresigned_presigned_group_with_dust_below_relay_fee(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
batchPkScript, err := txscript.PayToAddrScript(destAddr)
require.NoError(t, err)
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(100_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create a swap of two sweeps.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
inputValue := int64(1_000_000)
group1 := []Input{
{
Outpoint: op1,
Value: 1_000_000,
},
}
dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
change := &wire.TxOut{
// Note that there is no space for fee.
Value: inputValue - dustLimit,
PkScript: []byte{0xaf, 0xfe},
}
presignedHelper.setChangeForPrimaryDeposit(op1, change)
// Enable only one of the sweeps.
presignedHelper.SetOutpointOnline(op1, true)
// An attempt to presign must fail.
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change,
)
require.EqualError(t, err, "failed to construct unsigned tx for "+
"feeRate 253 sat/kw: failed to clamp batch fee: clamped "+
"fee rate 132 sat/kw is less than minimum relay fee 253 sat/kw")
// Add the sweep, triggering the publishing attempt.
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.ErrorContains(t, err, "were not presigned")
}
// testPresigned_presigned_group_with_dust_change tests passing multiple sweeps
// to the method PresignSweepsGroup. It tests that a dust change output of a
// primary deposit sweep is rejected by PresignSweepsGroup and AddSweep.
func testPresigned_presigned_group_with_dust_change(t *testing.T,
batcherStore testBatcherStore) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return chainfee.SatPerKWeight(10_000), nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
// Create a swap of two sweeps.
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
group1 := []Input{
{
Outpoint: op1,
Value: 1_000_000,
},
{
Outpoint: op2,
Value: 2_000_000,
},
}
changePkScript := []byte{0xaf, 0xfe}
dustLimit := utils.DustLimitForPkScript(changePkScript)
change := &wire.TxOut{
// If "-1" is removed, the group would be added successfully.
Value: int64(dustLimit - 1),
PkScript: changePkScript,
}
presignedHelper.setChangeForPrimaryDeposit(op1, change)
// Enable only one of the sweeps.
presignedHelper.SetOutpointOnline(op1, true)
presignedHelper.SetOutpointOnline(op2, true)
// An attempt to presign must fail.
err := batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change,
)
require.EqualError(t, err, "failed to construct unsigned tx for "+
"feeRate 253 sat/kw: output 0.00000476 BTC is below dust "+
"limit 0.00000477 BTC")
// Add the sweep, triggering the publishing attempt.
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.ErrorContains(t, err, "were not presigned")
// Now let's verify that we found the edge value correctly.
change.Value++
err = batcher.PresignSweepsGroup(
ctx, group1, sweepTimeout, destAddr, change,
)
require.NoError(t, err)
err = batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash1,
Inputs: group1,
Notifier: &dummyNotifier,
})
require.NoError(t, err)
}
// wrappedStoreWithPresignedFlag wraps a SweepFetcher store adding IsPresigned
// flag to the returned sweeps, taking it from mockPresignedHelper.
type wrappedStoreWithPresignedFlag struct {
backend SweepFetcher
helper *mockPresignedHelper
}
// // FetchSweep returns details of the sweep.
func (s *wrappedStoreWithPresignedFlag) FetchSweep(ctx context.Context,
swap lntypes.Hash, utxo wire.OutPoint) (*SweepInfo, error) {
sweepInfo, err := s.backend.FetchSweep(ctx, swap, utxo)
if err != nil {
return nil, err
}
// Attach IsPresigned flag.
s.helper.mu.Lock()
defer s.helper.mu.Unlock()
_, sweepInfo.IsPresigned = s.helper.onlineOutpoints[utxo]
return sweepInfo, nil
}
// testPresigned_presigned_and_regular_sweeps tests a combination of presigned
// mode and regular mode for the following scenario: one regular input is added,
// then a presigned input is added and it goes to another batch, because they
// should not appear in the same batch. Then another regular and another
// presigned inputs are added and go to the existing batches of their types.
func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore,
batcherStore testBatcherStore) {
defer test.Guard(t)()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const (
feeRateLow = chainfee.SatPerKWeight(10_000)
feeRateMedium = chainfee.SatPerKWeight(30_000)
feeRateHigh = chainfee.SatPerKWeight(40_000)
)
currentFeeRate := feeRateLow
setFeeRate := func(feeRate chainfee.SatPerKWeight) {
currentFeeRate = feeRate
}
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return currentFeeRate, nil
}
presignedHelper := newMockPresignedHelper()
sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
require.NoError(t, err)
sweepFetcher := &wrappedStoreWithPresignedFlag{
backend: sweepStore,
helper: presignedHelper,
}
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, sweepFetcher,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
setFeeRate(feeRateLow)
/////////////////////////////////////
// Create the first regular sweep. //
/////////////////////////////////////
swapHash1 := lntypes.Hash{1, 1, 1}
op1 := wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
}
sweepReq1 := SweepRequest{
SwapHash: swapHash1,
Inputs: []Input{{
Value: 1_000_000,
Outpoint: op1,
}},
Notifier: &dummyNotifier,
}
swap1 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 1_000_000,
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
HtlcKeys: htlcKeys,
// Make preimage unique to pass SQL constraints.
Preimage: lntypes.Preimage{1},
},
DestAddr: destAddr,
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err = store.CreateLoopOut(ctx, swapHash1, swap1)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx1 := <-lnd.TxPublishChannel
require.Len(t, tx1.TxIn, 1)
require.Len(t, tx1.TxOut, 1)
///////////////////////////////////////
// Create the first presigned sweep. //
///////////////////////////////////////
swapHash2 := lntypes.Hash{2, 2, 2}
op2 := wire.OutPoint{
Hash: chainhash.Hash{2, 2},
Index: 2,
}
swap2 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 2_000_000,
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
HtlcKeys: htlcKeys,
// Make preimage unique to pass SQL constraints.
Preimage: lntypes.Preimage{2},
},
DestAddr: destAddr,
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err = store.CreateLoopOut(ctx, swapHash2, swap2)
require.NoError(t, err)
store.AssertLoopOutStored()
sweepReq2 := SweepRequest{
SwapHash: swapHash2,
Inputs: []Input{{
Value: 2_000_000,
Outpoint: op2,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op2, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op2, Value: 2_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))
// Since a batch was created we check that it registered for its primary
// sweep's spend.
<-lnd.RegisterSpendChannel
// Wait for a transactions to be published.
tx2 := <-lnd.TxPublishChannel
require.Len(t, tx2.TxIn, 1)
require.Len(t, tx2.TxOut, 1)
require.Equal(t, op2, tx2.TxIn[0].PreviousOutPoint)
//////////////////////////////////////
// Create the second regular sweep. //
//////////////////////////////////////
swapHash3 := lntypes.Hash{3, 3, 3}
op3 := wire.OutPoint{
Hash: chainhash.Hash{3, 3},
Index: 3,
}
sweepReq3 := SweepRequest{
SwapHash: swapHash3,
Inputs: []Input{{
Value: 4_000_000,
Outpoint: op3,
}},
Notifier: &dummyNotifier,
}
swap3 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 4_000_000,
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
HtlcKeys: htlcKeys,
// Make preimage unique to pass SQL constraints.
Preimage: lntypes.Preimage{3},
},
DestAddr: destAddr,
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err = store.CreateLoopOut(ctx, swapHash3, swap3)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
require.NoError(t, batcher.AddSweep(ctx, &sweepReq3))
////////////////////////////////////////
// Create the second presigned sweep. //
////////////////////////////////////////
swapHash4 := lntypes.Hash{4, 4, 4}
op4 := wire.OutPoint{
Hash: chainhash.Hash{4, 4},
Index: 4,
}
swap4 := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 111,
AmountRequested: 3_000_000,
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
HtlcKeys: htlcKeys,
// Make preimage unique to pass SQL constraints.
Preimage: lntypes.Preimage{4},
},
DestAddr: destAddr,
SwapInvoice: swapInvoice,
SweepConfTarget: 111,
}
err = store.CreateLoopOut(ctx, swapHash4, swap4)
require.NoError(t, err)
store.AssertLoopOutStored()
sweepReq4 := SweepRequest{
SwapHash: swapHash4,
Inputs: []Input{{
Value: 3_000_000,
Outpoint: op4,
}},
Notifier: &dummyNotifier,
}
presignedHelper.SetOutpointOnline(op4, true)
err = batcher.PresignSweepsGroup(
ctx, []Input{{Outpoint: op4, Value: 3_000_000}},
sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
require.NoError(t, batcher.AddSweep(ctx, &sweepReq4))
// Wait for the both batches to have two sweeps.
require.Eventually(t, func() bool {
// Make sure there are two batches.
batches := getBatches(ctx, batcher)
if len(batches) != 2 {
return false
}
// Make sure each batch has two sweeps.
for _, batch := range batches {
var numSweeps int
batch.testRunInEventLoop(ctx, func() {
numSweeps = len(batch.sweeps)
})
if numSweeps != 2 {
return false
}
}
return true
}, test.Timeout, eventuallyCheckFrequency)
// Mine a block to trigger both batches publishing.
require.NoError(t, lnd.NotifyHeight(601))
// Wait for a transactions to be published.
tx3 := <-lnd.TxPublishChannel
require.Len(t, tx3.TxIn, 2)
require.Len(t, tx3.TxOut, 1)
require.Equal(t, int64(4993740), tx3.TxOut[0].Value)
tx4 := <-lnd.TxPublishChannel
require.Len(t, tx4.TxIn, 2)
require.Len(t, tx4.TxOut, 1)
require.Equal(t, int64(4993740), tx4.TxOut[0].Value)
}
// testPresigned_purging tests what happens if a non-final version of the batch
// is confirmed. Missing sweeps may be online or offline at that moment, which
// depends on the last argument of the function. In online case they are added
// to another online batch. In offline case they must are added to a new batch
// having valid presigned transactions.
func testPresigned_purging(t *testing.T, numSwaps, numConfirmedSwaps int,
batcherStore testBatcherStore, online bool) {
defer test.Guard(t)()
require.LessOrEqual(t, numConfirmedSwaps, numSwaps)
const (
sweepsPerSwap = 2
feeRate = chainfee.SatPerKWeight(10_000)
swapAmount = 3_000_001
)
sweepAmounts := []btcutil.Amount{1_000_001, 2_000_000}
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
customFeeRate := func(_ context.Context, _ lntypes.Hash,
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
return feeRate, nil
}
presignedHelper := newMockPresignedHelper()
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, presignedHelper,
WithCustomFeeRate(customFeeRate),
WithPresignedHelper(presignedHelper),
)
go func() {
err := batcher.Run(ctx)
checkBatcherError(t, err)
}()
swapHashes := make([]lntypes.Hash, numSwaps)
groups := make([][]Input, numSwaps)
txs := make([]*wire.MsgTx, numSwaps)
allOps := make([]wire.OutPoint, 0, numSwaps*sweepsPerSwap)
spendChans := make([]<-chan *SpendDetail, numSwaps)
confChans := make([]<-chan *ConfDetail, numSwaps)
for i := range numSwaps {
// Create a swap of sweepsPerSwap sweeps.
swapHash := lntypes.Hash{byte(i + 1)}
swapHashes[i] = swapHash
ops := make([]wire.OutPoint, sweepsPerSwap)
group := make([]Input, sweepsPerSwap)
for j := range sweepsPerSwap {
ops[j] = wire.OutPoint{
Hash: chainhash.Hash{byte(1 + i*2 + j)},
Index: uint32(1 + i*2 + j),
}
allOps = append(allOps, ops[j])
group[j] = Input{
Outpoint: ops[j],
Value: sweepAmounts[j],
}
}
groups[i] = group
// Enable all the sweeps.
for _, op := range ops {
presignedHelper.SetOutpointOnline(op, true)
}
// An attempt to presign must succeed.
err := batcher.PresignSweepsGroup(
ctx, group, sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Create a spending notification channel.
spendChan := make(chan *SpendDetail, 1)
spendChans[i] = spendChan
confChan := make(chan *ConfDetail, 1)
confChans[i] = confChan
notifier := &SpendNotifier{
SpendChan: spendChan,
SpendErrChan: make(chan error, 1),
ConfChan: confChan,
ConfErrChan: make(chan error, 1),
QuitChan: make(chan bool, 1),
}
// Add the sweep, triggering the publish attempt.
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash,
Inputs: group,
Notifier: notifier,
}))
// For the first group it should register for the sweep's spend
// and publish a transaction.
if i == 0 {
<-lnd.RegisterSpendChannel
} else {
// Trigger transaction publishing after each group.
require.NoError(t, lnd.NotifyHeight(int32(601+i)))
}
// Wait for a transactions to be published.
tx := <-lnd.TxPublishChannel
txs[i] = tx
}
// Record batch ID of the first batch.
batch1id := getOnlyBatch(t, ctx, batcher).id
// Turn off all the sweeps.
for _, op := range allOps {
presignedHelper.SetOutpointOnline(op, false)
}
// In case we are testing the addition of the remaining sweeps to a
// batch in online state, we need to create that batch now.
opx := wire.OutPoint{Hash: chainhash.Hash{3, 2, 1}, Index: 1}
if online && numConfirmedSwaps < numSwaps {
swapHash := lntypes.Hash{1, 2, 3}
const amount = 1_234_567
group := []Input{
{
Outpoint: opx,
Value: amount,
},
}
// Enable the sweep.
presignedHelper.SetOutpointOnline(opx, true)
// An attempt to presign must succeed.
err := batcher.PresignSweepsGroup(
ctx, group, sweepTimeout, destAddr, nil,
)
require.NoError(t, err)
// Add the sweep, triggering the publishing attempt.
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHash,
Inputs: group,
Notifier: &dummyNotifier,
}))
<-lnd.RegisterSpendChannel
tx := <-lnd.TxPublishChannel
require.Len(t, tx.TxIn, 1)
require.Equal(t, opx, tx.TxIn[0].PreviousOutPoint)
// Now enable our main sweeps again so the remaining ones are
// added to this new batch.
for _, op := range allOps {
presignedHelper.SetOutpointOnline(op, true)
}
}
// Now mine the transaction which includes first numConfirmedSwaps.
tx := txs[numConfirmedSwaps-1]
// Now confirm previously broadcasted transaction (op1 and op2).
txHash := tx.TxHash()
spendDetail := &chainntnfs.SpendDetail{
SpentOutPoint: &allOps[0],
SpendingTx: tx,
SpenderTxHash: &txHash,
SpenderInputIndex: 0,
SpendingHeight: int32(601 + numSwaps + 1),
}
lnd.SpendChannel <- spendDetail
// Calculate the expected on-chain fee of the swap.
wantFee := make([]btcutil.Amount, numConfirmedSwaps)
for i := range numConfirmedSwaps {
batchAmount := swapAmount * btcutil.Amount(numConfirmedSwaps)
txFee := batchAmount - btcutil.Amount(tx.TxOut[0].Value)
numConfirmedSweeps := numConfirmedSwaps * sweepsPerSwap
feePerSweep := txFee / btcutil.Amount(numConfirmedSweeps)
roundingDiff := txFee - feePerSweep*btcutil.Amount(
numConfirmedSweeps,
)
swapFee := feePerSweep * 2
// Add rounding difference to the first swap.
if i == 0 {
swapFee += roundingDiff
}
wantFee[i] = swapFee
}
// Make sure that notifiers of confirmed sweeps received notifications.
for i := range numConfirmedSwaps {
spend := <-spendChans[i]
require.Equal(t, txHash, spend.Tx.TxHash())
require.Equal(t, wantFee[i], spend.OnChainFeePortion)
}
<-lnd.RegisterConfChannel
require.NoError(t, lnd.NotifyHeight(
int32(601+numSwaps+1+batchConfHeight),
))
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: tx,
}
// CleanupTransactions is called here.
<-presignedHelper.cleanupCalled
// Increasing block height caused the second batch to re-publish.
if online && numConfirmedSwaps < numSwaps {
<-lnd.TxPublishChannel
}
// Make sure that notifiers of confirmed sweeps received notifications.
for i := range numConfirmedSwaps {
conf := <-confChans[i]
require.Equal(t, txHash, conf.Tx.TxHash())
require.Equal(t, wantFee[i], conf.OnChainFeePortion)
}
if !online && numConfirmedSwaps != numSwaps {
// If the sweeps are offline, the missing sweeps in the
// confirmed transaction should be re-added to the batcher as
// new batch. The groups are added incrementally, so we need
// to wait until the batch reaches the expected size.
<-lnd.RegisterSpendChannel
<-lnd.TxPublishChannel
}
// Now make sure that a correct spend and conf contification is sent if
// AddSweep is called after confirming the sweeps.
for i := range numConfirmedSwaps {
// Create a spending notification channel.
spendChan := make(chan *SpendDetail, 1)
confChan := make(chan *ConfDetail)
notifier := &SpendNotifier{
SpendChan: spendChan,
SpendErrChan: make(chan error, 1),
ConfChan: confChan,
ConfErrChan: make(chan error, 1),
QuitChan: make(chan bool, 1),
}
// Add the sweep, triggering the publish attempt.
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
SwapHash: swapHashes[i],
Inputs: groups[i],
Notifier: notifier,
}))
spendReg := <-lnd.RegisterSpendChannel
spendReg.SpendChannel <- spendDetail
spend := <-spendChan
require.Equal(t, txHash, spend.Tx.TxHash())
require.Equal(t, wantFee[i], spend.OnChainFeePortion)
<-lnd.RegisterConfChannel
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: tx,
}
conf := <-confChan
require.Equal(t, tx.TxHash(), conf.Tx.TxHash())
require.Equal(t, wantFee[i], conf.OnChainFeePortion)
}
// If all the swaps were confirmed, stop.
if numConfirmedSwaps == numSwaps {
return
}
// Wait to new batch to appear and to have the expected size.
wantSize := (numSwaps - numConfirmedSwaps) * sweepsPerSwap
if online {
// Add opx to the list of expected inputs.
wantSize++
}
require.Eventually(t, func() bool {
// Wait for a batch with new ID to appear.
batches := getBatches(ctx, batcher)
var batch2 *batch
for _, b := range batches {
if b.id != batch1id {
batch2 = b
}
}
if batch2 == nil {
return false
}
// Check the size of the second batch.
return batch2.numSweeps(ctx) == wantSize
}, test.Timeout, eventuallyCheckFrequency)
// Now trigger batch publishing and inspect the published tx.
require.NoError(t, lnd.NotifyHeight(int32(
601+numSwaps+1+batchConfHeight+1,
)))
tx2 := <-lnd.TxPublishChannel
wantOps := allOps[numConfirmedSwaps*sweepsPerSwap:]
if online {
// Deep copy wantOps to unlink from allOps.
wantOps = append([]wire.OutPoint{}, wantOps...)
wantOps = append(wantOps, opx)
}
gotOps := make([]wire.OutPoint, 0, len(tx2.TxIn))
for _, txIn := range tx2.TxIn {
gotOps = append(gotOps, txIn.PreviousOutPoint)
}
require.ElementsMatch(t, wantOps, gotOps)
}
// TestPresigned tests presigned mode. Most sub-tests doesn't use loopdb.
func TestPresigned(t *testing.T) {
logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout))
logger.SetLevel(btclog.LevelTrace)
UseLogger(logger.SubSystem("SWEEP"))
t.Run("forgotten_presign", func(t *testing.T) {
testPresigned_forgotten_presign(t, NewStoreMock())
})
t.Run("input1_offline_then_input2", func(t *testing.T) {
testPresigned_input1_offline_then_input2(t, NewStoreMock())
})
t.Run("min_relay_fee", func(t *testing.T) {
testPresigned_min_relay_fee(t, NewStoreMock())
})
t.Run("two_inputs_one_goes_offline", func(t *testing.T) {
testPresigned_two_inputs_one_goes_offline(t, NewStoreMock())
})
t.Run("first_publish_fails", func(t *testing.T) {
testPresigned_first_publish_fails(t, NewStoreMock())
})
t.Run("locktime", func(t *testing.T) {
testPresigned_locktime(t, NewStoreMock())
})
t.Run("presigned_group", func(t *testing.T) {
testPresigned_presigned_group(t, NewStoreMock())
})
t.Run("change", func(t *testing.T) {
testPresigned_presigned_group_with_change(t, NewStoreMock())
})
t.Run("fee_portion_change", func(t *testing.T) {
testPresigned_fee_portion_with_change(t, NewStoreMock())
})
t.Run("identical change pkscript", func(t *testing.T) {
testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
})
t.Run("dust_main_output", func(t *testing.T) {
testPresigned_presigned_group_with_dust_main_output(
t, NewStoreMock(),
)
})
t.Run("dust_main_output_below_min_relay_fee", func(t *testing.T) {
testPresigned_presigned_group_with_dust_below_relay_fee(
t, NewStoreMock(),
)
})
t.Run("dust_change", func(t *testing.T) {
testPresigned_presigned_group_with_dust_change(t, NewStoreMock())
})
t.Run("presigned_and_regular_sweeps", func(t *testing.T) {
runTests(t, testPresigned_presigned_and_regular_sweeps)
})
t.Run("purging", func(t *testing.T) {
testPurging := func(numSwaps, numConfirmedSwaps int,
online bool) {
name := fmt.Sprintf("%d of %d swaps confirmed",
numConfirmedSwaps, numSwaps)
if online {
name += ", sweeps online"
} else {
name += ", sweeps offline"
}
t.Run(name, func(t *testing.T) {
testPresigned_purging(
t, numSwaps, numConfirmedSwaps,
NewStoreMock(), online,
)
})
}
// Test cases in which the sweeps are offline.
testPurging(1, 1, false)
testPurging(2, 1, false)
testPurging(2, 2, false)
testPurging(3, 1, false)
testPurging(3, 2, false)
testPurging(5, 2, false)
testPurging(5, 3, false)
// Test cases in which the sweeps are online.
testPurging(2, 1, true)
testPurging(3, 1, true)
testPurging(3, 2, true)
testPurging(5, 2, true)
testPurging(5, 3, true)
})
}