2025-04-01 12:52:57 -03:00
|
|
|
package sweepbatcher
|
|
|
|
|
|
|
|
|
|
import (
|
2025-10-11 14:44:31 -03:00
|
|
|
"bytes"
|
2025-04-01 12:52:57 -03:00
|
|
|
"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"
|
2025-05-06 14:26:06 -03:00
|
|
|
"github.com/lightninglabs/loop/swap"
|
2025-04-01 12:52:57 -03:00
|
|
|
"github.com/lightninglabs/loop/test"
|
2025-07-22 23:48:19 -03:00
|
|
|
"github.com/lightninglabs/loop/utils"
|
2025-04-01 12:52:57 -03:00
|
|
|
"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
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// changeOutputs is a map of change outputs for a given primary deposit.
|
|
|
|
|
changeOutputs map[wire.OutPoint]*wire.TxOut
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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),
|
2025-07-18 11:07:40 +02:00
|
|
|
changeOutputs: make(map[wire.OutPoint]*wire.TxOut),
|
2025-04-01 12:52:57 -03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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
|
2025-07-18 11:07:40 +02:00
|
|
|
// the exact transaction passed and adds it to presignedBatches. Otherwise, it
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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()
|
|
|
|
|
|
2025-05-08 00:12:30 -03:00
|
|
|
if feeRate < minRelayFee {
|
|
|
|
|
return nil, fmt.Errorf("feeRate (%v) is below minRelayFee (%v)",
|
|
|
|
|
feeRate, minRelayFee)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 21:40:42 -03:00
|
|
|
if !hasInput(tx, primarySweepID) {
|
|
|
|
|
return nil, fmt.Errorf("primarySweepID %v not in tx",
|
|
|
|
|
primarySweepID)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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 {
|
2025-05-14 21:40:42 -03:00
|
|
|
return nil, fmt.Errorf("some outpoint is offline and no " +
|
|
|
|
|
"suitable presigned tx found")
|
2025-04-01 12:52:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2025-05-06 14:26:06 -03:00
|
|
|
// Find IsPresigned.
|
|
|
|
|
_, isPresigned := h.onlineOutpoints[utxo]
|
2025-04-01 12:52:57 -03:00
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// Find change.
|
|
|
|
|
change := h.changeOutputs[utxo]
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
return &SweepInfo{
|
|
|
|
|
// Set Timeout to prevent warning messages about timeout=0.
|
|
|
|
|
Timeout: sweepTimeout,
|
|
|
|
|
|
2025-05-06 14:26:06 -03:00
|
|
|
IsPresigned: isPresigned,
|
|
|
|
|
|
|
|
|
|
HTLC: swap.Htlc{
|
|
|
|
|
PkScript: []byte{10, 11, 12},
|
|
|
|
|
},
|
2025-07-18 11:07:40 +02:00
|
|
|
Change: change,
|
2025-04-01 12:52:57 -03:00
|
|
|
}, 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()
|
|
|
|
|
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 00:12:30 -03:00
|
|
|
// 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.
|
2025-07-22 11:23:58 +02:00
|
|
|
lnd.SetMinRelayFee(252)
|
2025-05-08 00:12:30 -03:00
|
|
|
|
|
|
|
|
// 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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-05-08 00:12:30 -03:00
|
|
|
)
|
|
|
|
|
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)
|
2025-07-22 11:23:58 +02:00
|
|
|
require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
|
2025-05-08 00:12:30 -03:00
|
|
|
|
|
|
|
|
// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
|
|
|
|
|
// The only difference of tx2 is a higher lock_time.
|
2025-07-22 11:23:58 +02:00
|
|
|
lnd.SetMinRelayFee(150)
|
2025-05-08 00:12:30 -03:00
|
|
|
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)
|
2025-07-22 11:23:58 +02:00
|
|
|
require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
|
2025-05-08 00:12:30 -03:00
|
|
|
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)
|
2025-07-22 23:48:19 -03:00
|
|
|
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
|
2025-05-08 00:12:30 -03:00
|
|
|
// 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)
|
2025-07-22 23:48:19 -03:00
|
|
|
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
|
2025-05-08 00:12:30 -03:00
|
|
|
// 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)
|
2025-07-22 23:48:19 -03:00
|
|
|
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
|
2025-05-08 00:12:30 -03:00
|
|
|
require.Equal(t, uint32(604), tx3.LockTime)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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()
|
|
|
|
|
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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.
|
2025-07-18 11:07:40 +02:00
|
|
|
err = batcher.PresignSweepsGroup(
|
|
|
|
|
ctx, group1, sweepTimeout, destAddr, nil,
|
|
|
|
|
)
|
2025-05-14 21:40:42 -03:00
|
|
|
require.ErrorContains(t, err, "some outpoint is offline")
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
// Enable both outpoints.
|
|
|
|
|
presignedHelper.SetOutpointOnline(op2, true)
|
|
|
|
|
|
|
|
|
|
// An attempt to presign must succeed.
|
2025-07-18 11:07:40 +02:00
|
|
|
err = batcher.PresignSweepsGroup(
|
|
|
|
|
ctx, group1, sweepTimeout, destAddr, nil,
|
|
|
|
|
)
|
2025-04-01 12:52:57 -03:00
|
|
|
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.
|
2025-07-18 11:07:40 +02:00
|
|
|
err = batcher.PresignSweepsGroup(
|
|
|
|
|
ctx, group2, sweepTimeout, destAddr, nil,
|
|
|
|
|
)
|
2025-04-01 12:52:57 -03:00
|
|
|
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.
|
2025-07-18 11:07:40 +02:00
|
|
|
err = batcher.PresignSweepsGroup(
|
|
|
|
|
ctx, group3, sweepTimeout, destAddr, nil,
|
|
|
|
|
)
|
2025-04-01 12:52:57 -03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// 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)
|
|
|
|
|
|
2025-07-28 12:20:11 +02:00
|
|
|
// An attempt to presign shouldn't fail.
|
2025-07-18 11:07:40 +02:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-11 14:44:31 -03:00
|
|
|
// testPresigned_fee_portion_with_change ensures that the fee portion reported
|
2025-10-11 22:11:56 -03:00
|
|
|
// to clients accounts for change outputs in the presigned transaction. It also
|
|
|
|
|
// is a regression test for feerate overestimation when tx is published.
|
2025-10-11 14:44:31 -03:00
|
|
|
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)
|
|
|
|
|
|
2025-10-11 22:11:56 -03:00
|
|
|
// 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)
|
|
|
|
|
|
2025-10-11 14:44:31 -03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 12:20:11 +02:00
|
|
|
// 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))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
// 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.
|
2025-07-18 11:07:40 +02:00
|
|
|
func testPresigned_presigned_group_with_dust_main_output(t *testing.T,
|
|
|
|
|
batcherStore testBatcherStore) {
|
|
|
|
|
|
|
|
|
|
defer test.Guard(t)()
|
|
|
|
|
|
2025-07-22 23:48:19 -03:00
|
|
|
batchPkScript, err := txscript.PayToAddrScript(destAddr)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
lnd := test.NewMockLnd()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
return chainfee.SatPerKWeight(100_000), nil
|
2025-07-18 11:07:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}
|
2025-07-22 23:48:19 -03:00
|
|
|
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)
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
change := &wire.TxOut{
|
2025-07-22 23:48:19 -03:00
|
|
|
// If "+1" is removed, the group would be added successfully.
|
|
|
|
|
Value: inputValue - dustLimit - clampedFee + 1,
|
2025-07-18 11:07:40 +02:00
|
|
|
PkScript: []byte{0xaf, 0xfe},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
presignedHelper.setChangeForPrimaryDeposit(op1, change)
|
|
|
|
|
|
|
|
|
|
// Enable only one of the sweeps.
|
|
|
|
|
presignedHelper.SetOutpointOnline(op1, true)
|
|
|
|
|
|
|
|
|
|
// An attempt to presign must fail.
|
2025-07-22 23:48:19 -03:00
|
|
|
err = batcher.PresignSweepsGroup(
|
2025-07-18 11:07:40 +02:00
|
|
|
ctx, group1, sweepTimeout, destAddr, change,
|
|
|
|
|
)
|
|
|
|
|
require.EqualError(t, err, "failed to construct unsigned tx for "+
|
2025-07-22 23:48:19 -03:00
|
|
|
"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")
|
2025-07-18 11:07:40 +02:00
|
|
|
|
|
|
|
|
// 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")
|
2025-07-22 23:48:19 -03:00
|
|
|
|
|
|
|
|
// 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)
|
2025-09-28 18:05:47 -03:00
|
|
|
|
|
|
|
|
// Since a batch was created we check that it registered for its primary
|
|
|
|
|
// sweep's spend.
|
|
|
|
|
<-lnd.RegisterSpendChannel
|
2025-07-18 11:07:40 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
// 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)()
|
|
|
|
|
|
2025-07-22 23:48:19 -03:00
|
|
|
batchPkScript, err := txscript.PayToAddrScript(destAddr)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}
|
2025-07-22 23:48:19 -03:00
|
|
|
dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
|
2025-07-22 11:23:58 +02:00
|
|
|
change := &wire.TxOut{
|
2025-07-22 23:48:19 -03:00
|
|
|
// Note that there is no space for fee.
|
|
|
|
|
Value: inputValue - dustLimit,
|
2025-07-22 11:23:58 +02:00
|
|
|
PkScript: []byte{0xaf, 0xfe},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
presignedHelper.setChangeForPrimaryDeposit(op1, change)
|
|
|
|
|
|
|
|
|
|
// Enable only one of the sweeps.
|
|
|
|
|
presignedHelper.SetOutpointOnline(op1, true)
|
|
|
|
|
|
|
|
|
|
// An attempt to presign must fail.
|
2025-07-22 23:48:19 -03:00
|
|
|
err = batcher.PresignSweepsGroup(
|
2025-07-22 11:23:58 +02:00
|
|
|
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 "+
|
2025-07-22 23:48:19 -03:00
|
|
|
"fee rate 132 sat/kw is less than minimum relay fee 253 sat/kw")
|
2025-07-22 11:23:58 +02:00
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// 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,
|
|
|
|
|
},
|
|
|
|
|
}
|
2025-07-22 23:48:19 -03:00
|
|
|
changePkScript := []byte{0xaf, 0xfe}
|
|
|
|
|
dustLimit := utils.DustLimitForPkScript(changePkScript)
|
2025-07-18 11:07:40 +02:00
|
|
|
change := &wire.TxOut{
|
2025-07-22 23:48:19 -03:00
|
|
|
// If "-1" is removed, the group would be added successfully.
|
2025-07-18 11:07:40 +02:00
|
|
|
Value: int64(dustLimit - 1),
|
2025-07-22 23:48:19 -03:00
|
|
|
PkScript: changePkScript,
|
2025-07-18 11:07:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 "+
|
2025-07-22 23:48:19 -03:00
|
|
|
"feeRate 253 sat/kw: output 0.00000476 BTC is below dust "+
|
2025-07-18 11:07:40 +02:00
|
|
|
"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")
|
2025-07-22 23:48:19 -03:00
|
|
|
|
|
|
|
|
// 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)
|
2025-07-18 11:07:40 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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}},
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
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,
|
2025-05-06 14:25:21 -03:00
|
|
|
batcherStore testBatcherStore, online bool) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
defer test.Guard(t)()
|
|
|
|
|
|
|
|
|
|
require.LessOrEqual(t, numConfirmedSwaps, numSwaps)
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
const (
|
|
|
|
|
sweepsPerSwap = 2
|
|
|
|
|
feeRate = chainfee.SatPerKWeight(10_000)
|
|
|
|
|
swapAmount = 3_000_001
|
|
|
|
|
)
|
|
|
|
|
sweepAmounts := []btcutil.Amount{1_000_001, 2_000_000}
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
lnd := test.NewMockLnd()
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2025-05-05 23:28:23 -03:00
|
|
|
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
|
|
|
|
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
return feeRate, nil
|
2025-04-01 12:52:57 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}()
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
swapHashes := make([]lntypes.Hash, numSwaps)
|
|
|
|
|
groups := make([][]Input, numSwaps)
|
2025-04-01 12:52:57 -03:00
|
|
|
txs := make([]*wire.MsgTx, numSwaps)
|
|
|
|
|
allOps := make([]wire.OutPoint, 0, numSwaps*sweepsPerSwap)
|
2025-04-26 01:01:39 -03:00
|
|
|
spendChans := make([]<-chan *SpendDetail, numSwaps)
|
|
|
|
|
confChans := make([]<-chan *ConfDetail, numSwaps)
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
for i := range numSwaps {
|
|
|
|
|
// Create a swap of sweepsPerSwap sweeps.
|
|
|
|
|
swapHash := lntypes.Hash{byte(i + 1)}
|
2025-04-26 01:01:39 -03:00
|
|
|
swapHashes[i] = swapHash
|
2025-04-01 12:52:57 -03:00
|
|
|
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],
|
2025-04-26 01:01:39 -03:00
|
|
|
Value: sweepAmounts[j],
|
2025-04-01 12:52:57 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-26 01:01:39 -03:00
|
|
|
groups[i] = group
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
// Enable all the sweeps.
|
|
|
|
|
for _, op := range ops {
|
|
|
|
|
presignedHelper.SetOutpointOnline(op, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An attempt to presign must succeed.
|
2025-05-06 14:25:21 -03:00
|
|
|
err := batcher.PresignSweepsGroup(
|
2025-07-18 11:07:40 +02:00
|
|
|
ctx, group, sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
// 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),
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// Add the sweep, triggering the publish attempt.
|
|
|
|
|
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
|
|
|
|
|
SwapHash: swapHash,
|
|
|
|
|
Inputs: group,
|
2025-04-26 01:01:39 -03:00
|
|
|
Notifier: notifier,
|
2025-04-01 12:52:57 -03:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// 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.
|
2025-05-06 14:25:21 -03:00
|
|
|
err := batcher.PresignSweepsGroup(
|
2025-07-18 11:07:40 +02:00
|
|
|
ctx, group, sweepTimeout, destAddr, nil,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// Add the sweep, triggering the publishing attempt.
|
2025-04-01 12:52:57 -03:00
|
|
|
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
|
2025-04-26 01:01:39 -03:00
|
|
|
|
2025-04-27 01:08:41 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
// Make sure that notifiers of confirmed sweeps received notifications.
|
|
|
|
|
for i := range numConfirmedSwaps {
|
|
|
|
|
spend := <-spendChans[i]
|
|
|
|
|
require.Equal(t, txHash, spend.Tx.TxHash())
|
2025-04-27 01:08:41 -03:00
|
|
|
require.Equal(t, wantFee[i], spend.OnChainFeePortion)
|
2025-04-26 01:01:39 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
<-lnd.RegisterConfChannel
|
|
|
|
|
require.NoError(t, lnd.NotifyHeight(
|
|
|
|
|
int32(601+numSwaps+1+batchConfHeight),
|
|
|
|
|
))
|
|
|
|
|
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
|
|
|
|
|
Tx: tx,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CleanupTransactions is called here.
|
|
|
|
|
<-presignedHelper.cleanupCalled
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
// 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())
|
2025-04-27 01:08:41 -03:00
|
|
|
require.Equal(t, wantFee[i], conf.OnChainFeePortion)
|
2025-04-01 12:52:57 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
if !online && numConfirmedSwaps != numSwaps {
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
// 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())
|
2025-04-27 01:08:41 -03:00
|
|
|
require.Equal(t, wantFee[i], spend.OnChainFeePortion)
|
2025-04-26 01:01:39 -03:00
|
|
|
|
|
|
|
|
<-lnd.RegisterConfChannel
|
|
|
|
|
lnd.ConfChannel <- &chainntnfs.TxConfirmation{
|
|
|
|
|
Tx: tx,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conf := <-confChan
|
|
|
|
|
require.Equal(t, tx.TxHash(), conf.Tx.TxHash())
|
2025-04-27 01:08:41 -03:00
|
|
|
require.Equal(t, wantFee[i], conf.OnChainFeePortion)
|
2025-04-26 01:01:39 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If all the swaps were confirmed, stop.
|
|
|
|
|
if numConfirmedSwaps == numSwaps {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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())
|
|
|
|
|
})
|
|
|
|
|
|
2025-05-08 00:12:30 -03:00
|
|
|
t.Run("min_relay_fee", func(t *testing.T) {
|
|
|
|
|
testPresigned_min_relay_fee(t, NewStoreMock())
|
|
|
|
|
})
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
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())
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
t.Run("change", func(t *testing.T) {
|
|
|
|
|
testPresigned_presigned_group_with_change(t, NewStoreMock())
|
|
|
|
|
})
|
|
|
|
|
|
2025-10-11 14:44:31 -03:00
|
|
|
t.Run("fee_portion_change", func(t *testing.T) {
|
|
|
|
|
testPresigned_fee_portion_with_change(t, NewStoreMock())
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-28 12:20:11 +02:00
|
|
|
t.Run("identical change pkscript", func(t *testing.T) {
|
|
|
|
|
testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
t.Run("dust_main_output", func(t *testing.T) {
|
|
|
|
|
testPresigned_presigned_group_with_dust_main_output(
|
|
|
|
|
t, NewStoreMock(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
t.Run("dust_main_output_below_min_relay_fee", func(t *testing.T) {
|
|
|
|
|
testPresigned_presigned_group_with_dust_below_relay_fee(
|
|
|
|
|
t, NewStoreMock(),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
t.Run("dust_change", func(t *testing.T) {
|
|
|
|
|
testPresigned_presigned_group_with_dust_change(t, NewStoreMock())
|
|
|
|
|
})
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
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) {
|
2025-05-06 14:25:21 -03:00
|
|
|
testPresigned_purging(
|
|
|
|
|
t, numSwaps, numConfirmedSwaps,
|
|
|
|
|
NewStoreMock(), online,
|
|
|
|
|
)
|
2025-04-01 12:52:57 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
2025-04-26 01:01:39 -03:00
|
|
|
testPurging(5, 3, false)
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
// Test cases in which the sweeps are online.
|
|
|
|
|
testPurging(2, 1, true)
|
|
|
|
|
testPurging(3, 1, true)
|
|
|
|
|
testPurging(3, 2, true)
|
|
|
|
|
testPurging(5, 2, true)
|
2025-04-26 01:01:39 -03:00
|
|
|
testPurging(5, 3, true)
|
2025-04-01 12:52:57 -03:00
|
|
|
})
|
|
|
|
|
}
|