Skip to content

Commit 876ed40

Browse files
authored
Merge pull request #813 from starius/sweepbatcher-max-inputs
sweepbatcher: set max batch size to 1000 sweeps
2 parents a6e9a2b + 7780aca commit 876ed40

File tree

3 files changed

+205
-2
lines changed

3 files changed

+205
-2
lines changed

sweepbatcher/greedy_batch_selection.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import (
1717
// greedyAddSweep selects a batch for the sweep using the greedy algorithm,
1818
// which minimizes costs, and adds the sweep to the batch. To accomplish this,
1919
// it first collects fee details about the sweep being added, about a potential
20-
// new batch composed of this sweep only, and about all existing batches. Then
20+
// new batch composed of this sweep only, and about all existing batches. It
21+
// skips batches with at least MaxSweepsPerBatch swaps to keep tx standard. Then
2122
// it passes the data to selectBatches() function, which emulates adding the
2223
// sweep to each batch and creating new batch for the sweep, and calculates the
2324
// 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 {
4041
// Collect weight and fee rate info about existing batches.
4142
batches := make([]feeDetails, 0, len(b.batches))
4243
for _, existingBatch := range b.batches {
44+
// Enforce MaxSweepsPerBatch. If there are already too many
45+
// sweeps in the batch, do not add another sweep to prevent the
46+
// tx from becoming non-standard.
47+
if len(existingBatch.sweeps) >= MaxSweepsPerBatch {
48+
continue
49+
}
50+
4351
batchFeeDetails, err := estimateBatchWeight(existingBatch)
4452
if err != nil {
4553
return fmt.Errorf("failed to estimate tx weight for "+

sweepbatcher/sweep_batch.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ const (
4545
// maxFeeToSwapAmtRatio is the maximum fee to swap amount ratio that
4646
// we allow for a batch transaction.
4747
maxFeeToSwapAmtRatio = 0.2
48+
49+
// MaxSweepsPerBatch is the maximum number of sweeps in a single batch.
50+
// It is needed to prevent sweep tx from becoming non-standard. Max
51+
// standard transaction is 400k wu, a non-cooperative input is 393 wu.
52+
MaxSweepsPerBatch = 1000
4853
)
4954

5055
var (
@@ -433,6 +438,8 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) {
433438
// If the provided sweep is nil, we can't proceed with any checks, so
434439
// we just return early.
435440
if sweep == nil {
441+
b.log.Infof("the sweep is nil")
442+
436443
return false, nil
437444
}
438445

@@ -462,18 +469,40 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) {
462469
return true, nil
463470
}
464471

472+
// Enforce MaxSweepsPerBatch. If there are already too many sweeps in
473+
// the batch, do not add another sweep to prevent the tx from becoming
474+
// non-standard.
475+
if len(b.sweeps) >= MaxSweepsPerBatch {
476+
b.log.Infof("the batch has already too many sweeps (%d >= %d)",
477+
len(b.sweeps), MaxSweepsPerBatch)
478+
479+
return false, nil
480+
}
481+
465482
// Since all the actions of the batch happen sequentially, we could
466483
// arrive here after the batch got closed because of a spend. In this
467484
// case we cannot add the sweep to this batch.
468485
if b.state != Open {
486+
b.log.Infof("the batch state (%v) is not open", b.state)
487+
469488
return false, nil
470489
}
471490

472491
// If this batch contains a single sweep that spends to a non-wallet
473492
// address, or the incoming sweep is spending to non-wallet address,
474493
// we cannot add this sweep to the batch.
475494
for _, s := range b.sweeps {
476-
if s.isExternalAddr || sweep.isExternalAddr {
495+
if s.isExternalAddr {
496+
b.log.Infof("the batch already has a sweep (%x) with "+
497+
"an external address", s.swapHash[:6])
498+
499+
return false, nil
500+
}
501+
502+
if sweep.isExternalAddr {
503+
b.log.Infof("the batch is not empty and new sweep (%x)"+
504+
" has an external address", sweep.swapHash[:6])
505+
477506
return false, nil
478507
}
479508
}
@@ -486,6 +515,11 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) {
486515
int32(math.Abs(float64(sweep.timeout - s.timeout)))
487516

488517
if timeoutDistance > b.cfg.maxTimeoutDistance {
518+
b.log.Infof("too long timeout distance between the "+
519+
"batch and sweep %x: %d > %d",
520+
sweep.swapHash[:6], timeoutDistance,
521+
b.cfg.maxTimeoutDistance)
522+
489523
return false, nil
490524
}
491525
}

sweepbatcher/sweep_batcher_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/lightninglabs/loop/loopdb"
2020
"github.com/lightninglabs/loop/test"
2121
"github.com/lightninglabs/loop/utils"
22+
"github.com/lightningnetwork/lnd/build"
2223
"github.com/lightningnetwork/lnd/chainntnfs"
2324
"github.com/lightningnetwork/lnd/clock"
2425
"github.com/lightningnetwork/lnd/input"
@@ -1176,6 +1177,161 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) {
11761177
checkBatcherError(t, runErr)
11771178
}
11781179

1180+
// testMaxSweepsPerBatch tests the limit on max number of sweeps per batch.
1181+
func testMaxSweepsPerBatch(t *testing.T, store testStore,
1182+
batcherStore testBatcherStore) {
1183+
1184+
// Disable logging, because this test is very noisy.
1185+
oldLogger := log
1186+
UseLogger(build.NewSubLogger("SWEEP", nil))
1187+
defer UseLogger(oldLogger)
1188+
1189+
defer test.Guard(t, test.WithGuardTimeout(5*time.Minute))()
1190+
1191+
lnd := test.NewMockLnd()
1192+
ctx, cancel := context.WithCancel(context.Background())
1193+
1194+
sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
1195+
require.NoError(t, err)
1196+
1197+
startTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
1198+
testClock := clock.NewTestClock(startTime)
1199+
1200+
// Create muSig2SignSweep failing all sweeps to force non-cooperative
1201+
// scenario (it increases transaction size).
1202+
muSig2SignSweep := func(ctx context.Context,
1203+
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
1204+
paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte,
1205+
prevoutMap map[wire.OutPoint]*wire.TxOut) (
1206+
[]byte, []byte, error) {
1207+
1208+
return nil, nil, fmt.Errorf("test error")
1209+
}
1210+
1211+
// Set publish delay.
1212+
const publishDelay = 3 * time.Second
1213+
1214+
batcher := NewBatcher(
1215+
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
1216+
muSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
1217+
batcherStore, sweepStore, WithPublishDelay(publishDelay),
1218+
WithClock(testClock),
1219+
)
1220+
1221+
var wg sync.WaitGroup
1222+
wg.Add(1)
1223+
1224+
var runErr error
1225+
go func() {
1226+
defer wg.Done()
1227+
runErr = batcher.Run(ctx)
1228+
}()
1229+
1230+
// Wait for the batcher to be initialized.
1231+
<-batcher.initDone
1232+
1233+
const swapsNum = MaxSweepsPerBatch + 1
1234+
1235+
// Expect 2 batches to be registered.
1236+
expectedBatches := (swapsNum + MaxSweepsPerBatch - 1) /
1237+
MaxSweepsPerBatch
1238+
1239+
for i := 0; i < swapsNum; i++ {
1240+
preimage := lntypes.Preimage{2, byte(i % 256), byte(i / 256)}
1241+
swapHash := preimage.Hash()
1242+
1243+
// Create a sweep request.
1244+
sweepReq := SweepRequest{
1245+
SwapHash: swapHash,
1246+
Value: 111,
1247+
Outpoint: wire.OutPoint{
1248+
Hash: chainhash.Hash{1, 1},
1249+
Index: 1,
1250+
},
1251+
Notifier: &dummyNotifier,
1252+
}
1253+
1254+
swap := &loopdb.LoopOutContract{
1255+
SwapContract: loopdb.SwapContract{
1256+
CltvExpiry: 1000,
1257+
AmountRequested: 111,
1258+
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
1259+
HtlcKeys: htlcKeys,
1260+
1261+
// Make preimage unique to pass SQL constraints.
1262+
Preimage: preimage,
1263+
},
1264+
1265+
DestAddr: destAddr,
1266+
SwapInvoice: swapInvoice,
1267+
SweepConfTarget: 123,
1268+
}
1269+
1270+
err = store.CreateLoopOut(ctx, swapHash, swap)
1271+
require.NoError(t, err)
1272+
store.AssertLoopOutStored()
1273+
1274+
// Deliver sweep request to batcher.
1275+
require.NoError(t, batcher.AddSweep(&sweepReq))
1276+
1277+
// If this is new batch, expect a spend registration.
1278+
if i%MaxSweepsPerBatch == 0 {
1279+
<-lnd.RegisterSpendChannel
1280+
}
1281+
}
1282+
1283+
// Eventually the batches are launched and all the sweeps are added.
1284+
require.Eventually(t, func() bool {
1285+
// Make sure all the batches have started.
1286+
if len(batcher.batches) != expectedBatches {
1287+
return false
1288+
}
1289+
1290+
// Make sure all the sweeps were added.
1291+
sweepsNum := 0
1292+
for _, batch := range batcher.batches {
1293+
sweepsNum += len(batch.sweeps)
1294+
}
1295+
return sweepsNum == swapsNum
1296+
}, test.Timeout, eventuallyCheckFrequency)
1297+
1298+
// Advance the clock to publishDelay, so batches are published.
1299+
now := startTime.Add(publishDelay)
1300+
testClock.SetTime(now)
1301+
1302+
// Expect mockSigner.SignOutputRaw calls to sign non-cooperative
1303+
// sweeps.
1304+
for i := 0; i < expectedBatches; i++ {
1305+
<-lnd.SignOutputRawChannel
1306+
}
1307+
1308+
// Wait for txs to be published.
1309+
inputsNum := 0
1310+
const maxWeight = lntypes.WeightUnit(400_000)
1311+
for i := 0; i < expectedBatches; i++ {
1312+
tx := <-lnd.TxPublishChannel
1313+
inputsNum += len(tx.TxIn)
1314+
1315+
// Make sure the transaction size is standard.
1316+
weight := lntypes.WeightUnit(
1317+
blockchain.GetTransactionWeight(btcutil.NewTx(tx)),
1318+
)
1319+
require.Less(t, weight, maxWeight)
1320+
t.Logf("tx weight: %v", weight)
1321+
}
1322+
1323+
// Make sure the number of inputs in batch transactions is equal
1324+
// to the number of swaps.
1325+
require.Equal(t, swapsNum, inputsNum)
1326+
1327+
// Now make the batcher quit by canceling the context.
1328+
cancel()
1329+
wg.Wait()
1330+
1331+
// Make sure the batcher exited without an error.
1332+
checkBatcherError(t, runErr)
1333+
}
1334+
11791335
// testSweepBatcherSweepReentry tests that when an old version of the batch tx
11801336
// gets confirmed the sweep leftovers are sent back to the batcher.
11811337
func testSweepBatcherSweepReentry(t *testing.T, store testStore,
@@ -3468,6 +3624,11 @@ func TestDelays(t *testing.T) {
34683624
runTests(t, testDelays)
34693625
}
34703626

3627+
// TestMaxSweepsPerBatch tests the limit on max number of sweeps per batch.
3628+
func TestMaxSweepsPerBatch(t *testing.T) {
3629+
runTests(t, testMaxSweepsPerBatch)
3630+
}
3631+
34713632
// TestSweepBatcherSweepReentry tests that when an old version of the batch tx
34723633
// gets confirmed the sweep leftovers are sent back to the batcher.
34733634
func TestSweepBatcherSweepReentry(t *testing.T) {

0 commit comments

Comments
 (0)