2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-09 19:10:47 +00:00

sweepbatcher: set max batch size to 1000 sweeps

This is needed to avoid non-standard batch transactions (larger than 400k wu).
A non-cooperative input is 393 wu, so 1000 inputs are still under 400k wu.
This commit is contained in:
Boris Nagaev 2024-08-24 00:20:45 -03:00
parent a6e9a2b652
commit f21a21ee41
No known key found for this signature in database
3 changed files with 182 additions and 1 deletions

View File

@ -17,7 +17,8 @@ import (
// greedyAddSweep selects a batch for the sweep using the greedy algorithm,
// which minimizes costs, and adds the sweep to the batch. To accomplish this,
// it first collects fee details about the sweep being added, about a potential
// new batch composed of this sweep only, and about all existing batches. Then
// new batch composed of this sweep only, and about all existing batches. It
// skips batches with at least MaxSweepsPerBatch swaps to keep tx standard. Then
// it passes the data to selectBatches() function, which emulates adding the
// sweep to each batch and creating new batch for the sweep, and calculates the
// costs of each alternative. Based on the estimates of selectBatches(), this
@ -40,6 +41,13 @@ func (b *Batcher) greedyAddSweep(ctx context.Context, sweep *sweep) error {
// Collect weight and fee rate info about existing batches.
batches := make([]feeDetails, 0, len(b.batches))
for _, existingBatch := range b.batches {
// Enforce MaxSweepsPerBatch. If there are already too many
// sweeps in the batch, do not add another sweep to prevent the
// tx from becoming non-standard.
if len(existingBatch.sweeps) >= MaxSweepsPerBatch {
continue
}
batchFeeDetails, err := estimateBatchWeight(existingBatch)
if err != nil {
return fmt.Errorf("failed to estimate tx weight for "+

View File

@ -45,6 +45,11 @@ const (
// maxFeeToSwapAmtRatio is the maximum fee to swap amount ratio that
// we allow for a batch transaction.
maxFeeToSwapAmtRatio = 0.2
// MaxSweepsPerBatch is the maximum number of sweeps in a single batch.
// It is needed to prevent sweep tx from becoming non-standard. Max
// standard transaction is 400k wu, a non-cooperative input is 393 wu.
MaxSweepsPerBatch = 1000
)
var (
@ -462,6 +467,13 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) {
return true, nil
}
// Enforce MaxSweepsPerBatch. If there are already too many sweeps in
// the batch, do not add another sweep to prevent the tx from becoming
// non-standard.
if len(b.sweeps) >= MaxSweepsPerBatch {
return false, nil
}
// Since all the actions of the batch happen sequentially, we could
// arrive here after the batch got closed because of a spend. In this
// case we cannot add the sweep to this batch.

View File

@ -19,6 +19,7 @@ import (
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/input"
@ -1176,6 +1177,161 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) {
checkBatcherError(t, runErr)
}
// testMaxSweepsPerBatch tests the limit on max number of sweeps per batch.
func testMaxSweepsPerBatch(t *testing.T, store testStore,
batcherStore testBatcherStore) {
// Disable logging, because this test is very noisy.
oldLogger := log
UseLogger(build.NewSubLogger("SWEEP", nil))
defer UseLogger(oldLogger)
defer test.Guard(t, test.WithGuardTimeout(5*time.Minute))()
lnd := test.NewMockLnd()
ctx, cancel := context.WithCancel(context.Background())
sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
require.NoError(t, err)
startTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
testClock := clock.NewTestClock(startTime)
// Create muSig2SignSweep failing all sweeps to force non-cooperative
// scenario (it increases transaction size).
muSig2SignSweep := func(ctx context.Context,
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte,
prevoutMap map[wire.OutPoint]*wire.TxOut) (
[]byte, []byte, error) {
return nil, nil, fmt.Errorf("test error")
}
// Set publish delay.
const publishDelay = 3 * time.Second
batcher := NewBatcher(
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
muSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
batcherStore, sweepStore, WithPublishDelay(publishDelay),
WithClock(testClock),
)
var wg sync.WaitGroup
wg.Add(1)
var runErr error
go func() {
defer wg.Done()
runErr = batcher.Run(ctx)
}()
// Wait for the batcher to be initialized.
<-batcher.initDone
const swapsNum = MaxSweepsPerBatch + 1
// Expect 2 batches to be registered.
expectedBatches := (swapsNum + MaxSweepsPerBatch - 1) /
MaxSweepsPerBatch
for i := 0; i < swapsNum; i++ {
preimage := lntypes.Preimage{2, byte(i % 256), byte(i / 256)}
swapHash := preimage.Hash()
// Create a sweep request.
sweepReq := SweepRequest{
SwapHash: swapHash,
Value: 111,
Outpoint: wire.OutPoint{
Hash: chainhash.Hash{1, 1},
Index: 1,
},
Notifier: &dummyNotifier,
}
swap := &loopdb.LoopOutContract{
SwapContract: loopdb.SwapContract{
CltvExpiry: 1000,
AmountRequested: 111,
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
HtlcKeys: htlcKeys,
// Make preimage unique to pass SQL constraints.
Preimage: preimage,
},
DestAddr: destAddr,
SwapInvoice: swapInvoice,
SweepConfTarget: 123,
}
err = store.CreateLoopOut(ctx, swapHash, swap)
require.NoError(t, err)
store.AssertLoopOutStored()
// Deliver sweep request to batcher.
require.NoError(t, batcher.AddSweep(&sweepReq))
// If this is new batch, expect a spend registration.
if i%MaxSweepsPerBatch == 0 {
<-lnd.RegisterSpendChannel
}
}
// Eventually the batches are launched and all the sweeps are added.
require.Eventually(t, func() bool {
// Make sure all the batches have started.
if len(batcher.batches) != expectedBatches {
return false
}
// Make sure all the sweeps were added.
sweepsNum := 0
for _, batch := range batcher.batches {
sweepsNum += len(batch.sweeps)
}
return sweepsNum == swapsNum
}, test.Timeout, eventuallyCheckFrequency)
// Advance the clock to publishDelay, so batches are published.
now := startTime.Add(publishDelay)
testClock.SetTime(now)
// Expect mockSigner.SignOutputRaw calls to sign non-cooperative
// sweeps.
for i := 0; i < expectedBatches; i++ {
<-lnd.SignOutputRawChannel
}
// Wait for txs to be published.
inputsNum := 0
const maxWeight = lntypes.WeightUnit(400_000)
for i := 0; i < expectedBatches; i++ {
tx := <-lnd.TxPublishChannel
inputsNum += len(tx.TxIn)
// Make sure the transaction size is standard.
weight := lntypes.WeightUnit(
blockchain.GetTransactionWeight(btcutil.NewTx(tx)),
)
require.Less(t, weight, maxWeight)
t.Logf("tx weight: %v", weight)
}
// Make sure the number of inputs in batch transactions is equal
// to the number of swaps.
require.Equal(t, swapsNum, inputsNum)
// Now make the batcher quit by canceling the context.
cancel()
wg.Wait()
// Make sure the batcher exited without an error.
checkBatcherError(t, runErr)
}
// testSweepBatcherSweepReentry tests that when an old version of the batch tx
// gets confirmed the sweep leftovers are sent back to the batcher.
func testSweepBatcherSweepReentry(t *testing.T, store testStore,
@ -3468,6 +3624,11 @@ func TestDelays(t *testing.T) {
runTests(t, testDelays)
}
// TestMaxSweepsPerBatch tests the limit on max number of sweeps per batch.
func TestMaxSweepsPerBatch(t *testing.T) {
runTests(t, testMaxSweepsPerBatch)
}
// TestSweepBatcherSweepReentry tests that when an old version of the batch tx
// gets confirmed the sweep leftovers are sent back to the batcher.
func TestSweepBatcherSweepReentry(t *testing.T) {