diff --git a/sweepbatcher/presigned.go b/sweepbatcher/presigned.go index 385f34cf..d49506e2 100644 --- a/sweepbatcher/presigned.go +++ b/sweepbatcher/presigned.go @@ -51,6 +51,7 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep, outpoint: s.outpoint, value: s.value, presigned: s.presigned, + change: s.change, } } @@ -493,10 +494,12 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error, signedFeeRate := chainfee.NewSatPerKWeight(fee, realWeight) numSweeps := len(tx.TxIn) + numChange := len(tx.TxOut) - 1 b.Infof("attempting to publish custom signed tx=%v, desiredFeerate=%v,"+ - " signedFeeRate=%v, weight=%v, fee=%v, sweeps=%d, destAddr=%s", + " signedFeeRate=%v, weight=%v, fee=%v, sweeps=%d, "+ + "changeOutputs=%d, destAddr=%s", txHash, feeRate, signedFeeRate, realWeight, fee, numSweeps, - address) + numChange, address) b.debugLogTx("serialized batch", tx) // Publish the transaction. @@ -593,23 +596,31 @@ func CheckSignedTx(unsignedTx, signedTx *wire.MsgTx, inputAmt btcutil.Amount, } // Compare outputs. - if len(unsignedTx.TxOut) != 1 { - return fmt.Errorf("unsigned tx has %d outputs, want 1", - len(unsignedTx.TxOut)) - } - if len(signedTx.TxOut) != 1 { - return fmt.Errorf("the signed tx has %d outputs, want 1", + if len(unsignedTx.TxOut) != len(signedTx.TxOut) { + return fmt.Errorf("unsigned tx has %d outputs, signed tx has "+ + "%d outputs, should be equal", len(unsignedTx.TxOut), len(signedTx.TxOut)) } - unsignedOut := unsignedTx.TxOut[0] - signedOut := signedTx.TxOut[0] - if !bytes.Equal(unsignedOut.PkScript, signedOut.PkScript) { - return fmt.Errorf("mismatch of output pkScript: %x, %x", - unsignedOut.PkScript, signedOut.PkScript) + for i, o := range unsignedTx.TxOut { + if !bytes.Equal(o.PkScript, signedTx.TxOut[i].PkScript) { + return fmt.Errorf("mismatch of output pkScript: %x, %x", + o.PkScript, signedTx.TxOut[i].PkScript) + } + if i != 0 && o.Value != signedTx.TxOut[i].Value { + return fmt.Errorf("mismatch of output value: %d, %d", + o.Value, signedTx.TxOut[i].Value) + } + } + + // Calculate the total value of all outputs to help determine the + // transaction fee. + totalOutputValue := btcutil.Amount(0) + for _, o := range signedTx.TxOut { + totalOutputValue += btcutil.Amount(o.Value) } // Find the feerate of signedTx. - fee := inputAmt - btcutil.Amount(signedOut.Value) + fee := inputAmt - totalOutputValue weight := lntypes.WeightUnit( blockchain.GetTransactionWeight(btcutil.NewTx(signedTx)), ) diff --git a/sweepbatcher/presigned_test.go b/sweepbatcher/presigned_test.go index 629ff1de..d4f59373 100644 --- a/sweepbatcher/presigned_test.go +++ b/sweepbatcher/presigned_test.go @@ -1460,7 +1460,8 @@ func TestCheckSignedTx(t *testing.T) { }, inputAmt: 3_000_000, minRelayFee: 253, - wantErr: "unsigned tx has 2 outputs, want 1", + wantErr: "unsigned tx has 2 outputs, signed tx " + + "has 1 outputs, should be equal", }, { @@ -1517,7 +1518,153 @@ func TestCheckSignedTx(t *testing.T) { }, inputAmt: 3_000_000, minRelayFee: 253, - wantErr: "the signed tx has 2 outputs, want 1", + wantErr: "unsigned tx has 1 outputs, signed tx " + + "has 2 outputs, should be equal", + }, + + { + name: "pkscript mismatch", + unsignedTx: &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + Sequence: 2, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 2999374, + PkScript: batchPkScript, + }, + }, + LockTime: 800_000, + }, + signedTx: &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + Sequence: 2, + Witness: wire.TxWitness{ + []byte("test"), + }, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 2999374, + PkScript: []byte{0xaf, 0xfe}, // Just to make it different. + }, + }, + LockTime: 799_999, + }, + inputAmt: 3_000_000, + minRelayFee: 253, + wantErr: "mismatch of output pkScript", + }, + + { + name: "value mismatch, first output", + unsignedTx: &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + Sequence: 2, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 2999374, + PkScript: batchPkScript, + }, + }, + LockTime: 800_000, + }, + signedTx: &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + Sequence: 2, + Witness: wire.TxWitness{ + []byte("test"), + }, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1_337_000, // Just to make it different. + PkScript: batchPkScript, + }, + }, + LockTime: 799_999, + }, + inputAmt: 3_000_000, + minRelayFee: 253, + wantErr: "", + }, + + { + name: "value mismatch, change output", + unsignedTx: &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + Sequence: 2, + }, + { + PreviousOutPoint: op1, + Sequence: 2, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 2999374, + PkScript: batchPkScript, + }, + { + Value: 1_337_000, + PkScript: batchPkScript, + }, + }, + LockTime: 800_000, + }, + signedTx: &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + Sequence: 2, + Witness: wire.TxWitness{ + []byte("test"), + }, + }, + { + PreviousOutPoint: op1, + Sequence: 2, + Witness: wire.TxWitness{ + []byte("test"), + }, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 2_493_300, + PkScript: batchPkScript, + }, + { + Value: 1_338, // Just to make it different. + PkScript: batchPkScript, + }, + }, + LockTime: 799_999, + }, + inputAmt: 3_000_000, + minRelayFee: 253, + wantErr: "mismatch of output value", }, { diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go index 50ecafbb..98f576df 100644 --- a/sweepbatcher/sweep_batch.go +++ b/sweepbatcher/sweep_batch.go @@ -26,6 +26,7 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" sweeppkg "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/input" @@ -1290,10 +1291,14 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte, } // constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr. -// It also returns absolute fee (from weight and clamped). +// It also returns absolute fee (from weight and clamped). The main output is +// the first output of the transaction, followed by an optional list of change +// outputs. If the main output value is below dust limit this function will +// return an error. func constructUnsignedTx(sweeps []sweep, address btcutil.Address, - currentHeight int32, feeRate chainfee.SatPerKWeight) (*wire.MsgTx, - lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) { + currentHeight int32, feeRate chainfee.SatPerKWeight) ( + *wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, + error) { // Sanity check, there should be at least 1 sweep in this batch. if len(sweeps) == 0 { @@ -1306,6 +1311,13 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address, LockTime: uint32(currentHeight), } + var changeOutputs []*wire.TxOut + for _, sweep := range sweeps { + if sweep.change != nil { + changeOutputs = append(changeOutputs, sweep.change) + } + } + // Add transaction inputs and estimate its weight. var weightEstimate input.TxWeightEstimator for _, sweep := range sweeps { @@ -1351,6 +1363,11 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address, "failed: %w", err) } + // Add the optional change outputs to weight estimates. + for _, o := range changeOutputs { + weightEstimate.AddOutput(o.PkScript) + } + // Keep track of the total amount this batch is sweeping back. batchAmt := btcutil.Amount(0) for _, sweep := range sweeps { @@ -1368,15 +1385,78 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address, feeForWeight++ } - // Clamp the calculated fee to the max allowed fee amount for the batch. - fee := clampBatchFee(feeForWeight, batchAmt) - // Add the batch transaction output, which excludes the fees paid to - // miners. + // miners. Reduce the amount by the sum of change outputs, if any. + var sumChange int64 + for _, change := range changeOutputs { + sumChange += change.Value + } + + // Ensure that the batch amount is greater than the sum of change. + if batchAmt <= btcutil.Amount(sumChange) { + return nil, 0, 0, 0, fmt.Errorf("batch amount %v is <= the "+ + "sum of change outputs %v", batchAmt, + btcutil.Amount(sumChange)) + } + + // Clamp the calculated fee to the max allowed fee amount for the batch. + fee := clampBatchFee(feeForWeight, batchAmt-btcutil.Amount(sumChange)) + + // Ensure that batch amount exceeds the sum of change outputs and the + // fee, and that it is also greater than dust limit for the main + // output. + dustLimit := utils.DustLimitForPkScript(batchPkScript) + if fee+btcutil.Amount(sumChange)+dustLimit > batchAmt { + return nil, 0, 0, 0, fmt.Errorf("batch amount %v is <= the "+ + "sum of change outputs %v plus fee %v and dust "+ + "limit %v", batchAmt, btcutil.Amount(sumChange), + fee, dustLimit) + } + + // Add the main output first. batchTx.AddTxOut(&wire.TxOut{ PkScript: batchPkScript, - Value: int64(batchAmt - fee), + Value: int64(batchAmt-fee) - sumChange, }) + // Then add change outputs. + for _, txOut := range changeOutputs { + batchTx.AddTxOut(&wire.TxOut{ + PkScript: txOut.PkScript, + Value: txOut.Value, + }) + } + + // Check that for each swap, inputs exceed the change outputs. + if len(changeOutputs) != 0 { + swap2Inputs := make(map[lntypes.Hash]btcutil.Amount) + swap2Change := make(map[lntypes.Hash]btcutil.Amount) + for _, sweep := range sweeps { + swap2Inputs[sweep.swapHash] += sweep.value + if sweep.change != nil { + swap2Change[sweep.swapHash] += + btcutil.Amount(sweep.change.Value) + } + } + + for swapHash, inputs := range swap2Inputs { + change := swap2Change[swapHash] + if inputs <= change { + return nil, 0, 0, 0, fmt.Errorf(""+ + "inputs %v <= change %v for swap %x", + inputs, change, swapHash[:6]) + } + } + } + + // Ensure that each output is above dust limit. + for _, txOut := range batchTx.TxOut { + dustLimit = utils.DustLimitForPkScript(txOut.PkScript) + if btcutil.Amount(txOut.Value) < dustLimit { + return nil, 0, 0, 0, fmt.Errorf("output %v is below "+ + "dust limit %v", btcutil.Amount(txOut.Value), + dustLimit) + } + } return batchTx, weight, feeForWeight, fee, nil } diff --git a/sweepbatcher/sweep_batch_test.go b/sweepbatcher/sweep_batch_test.go index 570565b0..78527874 100644 --- a/sweepbatcher/sweep_batch_test.go +++ b/sweepbatcher/sweep_batch_test.go @@ -29,6 +29,10 @@ func TestConstructUnsignedTx(t *testing.T) { Hash: chainhash.Hash{2, 2, 2}, Index: 2, } + op3 := wire.OutPoint{ + Hash: chainhash.Hash{3, 3, 3}, + Index: 3, + } batchPkScript, err := txscript.PayToAddrScript(destAddr) require.NoError(t, err) @@ -40,6 +44,28 @@ func TestConstructUnsignedTx(t *testing.T) { p2trPkScript, err := txscript.PayToAddrScript(p2trAddress) require.NoError(t, err) + change1Addr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" + + "lwyyeq5ryn6r" + change1Address, err := btcutil.DecodeAddress(change1Addr, nil) + require.NoError(t, err) + change1Pkscript, err := txscript.PayToAddrScript(change1Address) + require.NoError(t, err) + change1 := &wire.TxOut{ + Value: 100_000, + PkScript: change1Pkscript, + } + + change2Addr := "bc1psw0nrrulq4pgyuyk09a3wsutygltys4gxjjw3zl2uz4ep8pa" + + "r2vsvntfe0" + change2Address, err := btcutil.DecodeAddress(change2Addr, nil) + require.NoError(t, err) + change2Pkscript, err := txscript.PayToAddrScript(change2Address) + require.NoError(t, err) + change2 := &wire.TxOut{ + Value: 200_000, + PkScript: change2Pkscript, + } + serializedPubKey := []byte{ 0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, @@ -70,12 +96,15 @@ func TestConstructUnsignedTx(t *testing.T) { return fmt.Errorf("weight estimator test failure") } + dustLimit := utils.DustLimitForPkScript(batchPkScript) + cases := []struct { name string sweeps []sweep address btcutil.Address currentHeight int32 feeRate chainfee.SatPerKWeight + minRelayFeeRate chainfee.SatPerKWeight wantErr string wantTx *wire.MsgTx wantWeight lntypes.WeightUnit @@ -223,7 +252,7 @@ func TestConstructUnsignedTx(t *testing.T) { }, TxOut: []*wire.TxOut{ { - Value: 2400000, + Value: 2_400_000, PkScript: batchPkScript, }, }, @@ -265,7 +294,7 @@ func TestConstructUnsignedTx(t *testing.T) { }, TxOut: []*wire.TxOut{ { - Value: 2999211, + Value: 2_999_211, PkScript: batchPkScript, }, }, @@ -275,6 +304,208 @@ func TestConstructUnsignedTx(t *testing.T) { wantFee: 789, }, + { + name: "single sweep with change", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: change1, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1000, + wantTx: &wire.MsgTx{ + Version: 2, + LockTime: 800_000, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op1, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 899_384, + PkScript: p2trPkScript, + }, + { + Value: change1.Value, + PkScript: change1.PkScript, + }, + }, + }, + wantWeight: 616, + wantFeeForWeight: 616, + wantFee: 616, + }, + + { + name: "all sweeps different change outputs", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + }, + { + outpoint: op2, + value: 2_000_000, + change: change1, + }, + { + outpoint: op3, + value: 3_000_000, + change: change2, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1000, + wantTx: &wire.MsgTx{ + Version: 2, + LockTime: 800_000, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op1, + }, + { + PreviousOutPoint: op2, + }, + { + PreviousOutPoint: op3, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 5_698_752, + PkScript: p2trPkScript, + }, + { + Value: change1.Value, + PkScript: change1.PkScript, + }, + { + Value: change2.Value, + PkScript: change2.PkScript, + }, + }, + }, + wantWeight: 1248, + wantFeeForWeight: 1248, + wantFee: 1248, + }, + + { + name: "change exceeds input value", + sweeps: []sweep{ + { + outpoint: op2, + value: btcutil.Amount(change1.Value - 1), + change: change1, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1000, + wantTx: &wire.MsgTx{ + Version: 2, + LockTime: 800_000, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op2, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: change1.Value, + PkScript: change1.PkScript, + }, + }, + }, + wantErr: "batch amount 0.00099999 BTC is <= the sum " + + "of change outputs 0.00100000 BTC", + }, + + { + name: "main output dust, batch amount less than " + + "change+fee+dust", + sweeps: []sweep{ + { + outpoint: op1, + value: dustLimit, + }, + { + outpoint: op2, + value: btcutil.Amount(change1.Value), + change: change1, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1, + wantErr: "batch amount 0.00100294 BTC is <= the sum " + + "of change outputs 0.00100000 BTC plus fee " + + "0.00000001 BTC and dust limit 0.00000330 BTC", + }, + + { + name: "change output is dust", + sweeps: []sweep{ + { + outpoint: op1, + value: 1_000_000, + change: &wire.TxOut{ + Value: int64(dustLimit - 1), + PkScript: []byte{0xaf, 0xfe}, + }, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 1000, + wantErr: "output 0.00000293 BTC is below dust limit " + + "0.00000477 BTC", + }, + + { + name: "clamp fee to max fee to swap amount ratio", + sweeps: []sweep{ + { + outpoint: op1, + value: btcutil.SatoshiPerBitcoin, + change: &wire.TxOut{ + Value: btcutil.SatoshiPerBitcoin * 0.9, + PkScript: change1Pkscript, + }, + }, + }, + address: p2trAddress, + currentHeight: 800_000, + feeRate: 10000000, + wantTx: &wire.MsgTx{ + Version: 2, + LockTime: 800_000, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: op1, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: btcutil.SatoshiPerBitcoin * 0.08, + PkScript: p2trPkScript, + }, + { + Value: btcutil.SatoshiPerBitcoin * 0.9, + PkScript: change1.PkScript, + }, + }, + }, + wantWeight: 616, + wantFeeForWeight: 6_160_000, + wantFee: 2_000_000, + }, + { name: "weight estimator fails", sweeps: []sweep{ @@ -338,9 +569,13 @@ func TestConstructUnsignedTx(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + relayFeeRate := minRelayFeeRate + if tc.minRelayFeeRate != 0 { + relayFeeRate = tc.minRelayFeeRate + } tx, weight, feeForW, fee, err := constructUnsignedTx( tc.sweeps, tc.address, tc.currentHeight, - tc.feeRate, + tc.feeRate, relayFeeRate, ) if tc.wantErr != "" { require.Error(t, err) diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go index dcd987ef..2afeb327 100644 --- a/sweepbatcher/sweep_batcher.go +++ b/sweepbatcher/sweep_batcher.go @@ -125,6 +125,9 @@ type SweepInfo struct { // value should be stable for a sweep. Currently presigned and // non-presigned sweeps never appear in the same batch. IsPresigned bool + + // Change is an optional change output of the sweep. + Change *wire.TxOut } // SweepFetcher is used to get details of a sweep. @@ -168,7 +171,10 @@ type PresignedHelper interface { // SignTx signs an unsigned transaction or returns a pre-signed tx. // It must satisfy the following invariants: // - the set of inputs is the same, though the order may change; - // - the output is the same, but its amount may be different; + // - the main output is the same, but its amount may be different; + // - the main output is the first output in the transaction; + // - an optional set of change outputs may be added, the values and + // pkscripts must be preserved. // - feerate is higher or equal to minRelayFee; // - LockTime may be decreased; // - transaction version must be the same; @@ -177,6 +183,7 @@ type PresignedHelper interface { // When choosing a presigned transaction, a transaction with fee rate // closer to the fee rate passed is selected. If loadOnly is set, it // doesn't try to sign the transaction and only loads a presigned tx. + // These rules are enforced by CheckSignedTx function. SignTx(ctx context.Context, primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount, minRelayFee, feeRate chainfee.SatPerKWeight, @@ -711,9 +718,11 @@ func (b *Batcher) Run(ctx context.Context) error { // group. This method must be called prior to AddSweep if presigned mode is // enabled, otherwise AddSweep will fail. All the sweeps must belong to the same // swap. The order of sweeps is important. The first sweep serves as -// primarySweepID if the group starts a new batch. +// primarySweepID if the group starts a new batch. The change output may be nil +// to indicate that the sweep group does not create a change output. func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input, - sweepTimeout int32, destAddress btcutil.Address) error { + sweepTimeout int32, destAddress btcutil.Address, + changeOutput *wire.TxOut) error { if len(inputs) == 0 { return fmt.Errorf("no inputs passed to PresignSweepsGroup") @@ -745,6 +754,9 @@ func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input, } } + // Set the change output on the primary group sweep. + sweeps[0].change = changeOutput + // The sweeps are ordered inside the group, the first one is the primary // outpoint in the batch. primarySweepID := sweeps[0].outpoint @@ -1548,6 +1560,7 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash, minFeeRate: minFeeRate, nonCoopHint: s.NonCoopHint, presigned: s.IsPresigned, + change: s.Change, }, nil } diff --git a/sweepbatcher/sweep_batcher_presigned_test.go b/sweepbatcher/sweep_batcher_presigned_test.go index 36f11f75..fd28bc7d 100644 --- a/sweepbatcher/sweep_batcher_presigned_test.go +++ b/sweepbatcher/sweep_batcher_presigned_test.go @@ -17,7 +17,9 @@ import ( "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,6 +32,9 @@ type mockPresignedHelper struct { // participating in presigning. onlineOutpoints map[wire.OutPoint]bool + // changeOutputs is a map of change outputs for a given primary deposit. + changeOutputs map[wire.OutPoint]*wire.TxOut + // presignedBatches is the collection of presigned batches. The key is // primarySweepID. presignedBatches map[wire.OutPoint][]*wire.MsgTx @@ -46,6 +51,7 @@ type mockPresignedHelper struct { func newMockPresignedHelper() *mockPresignedHelper { return &mockPresignedHelper{ onlineOutpoints: make(map[wire.OutPoint]bool), + changeOutputs: make(map[wire.OutPoint]*wire.TxOut), presignedBatches: make(map[wire.OutPoint][]*wire.MsgTx), cleanupCalled: make(chan struct{}), } @@ -59,6 +65,16 @@ func (h *mockPresignedHelper) SetOutpointOnline(op wire.OutPoint, online bool) { h.onlineOutpoints[op] = online } +// setChangeForPrimaryDeposit sets the change output of a primary deposit sweep. +func (h *mockPresignedHelper) setChangeForPrimaryDeposit(op wire.OutPoint, + change *wire.TxOut) { + + h.mu.Lock() + defer h.mu.Unlock() + + h.changeOutputs[op] = change +} + // offlineInputs returns inputs of a tx which are offline. func (h *mockPresignedHelper) offlineInputs(tx *wire.MsgTx) []wire.OutPoint { offline := make([]wire.OutPoint, 0, len(tx.TxIn)) @@ -113,7 +129,7 @@ func (h *mockPresignedHelper) DestPkScript(ctx context.Context, } // SignTx tries to sign the transaction. If all the inputs are online, it signs -// the exact transaction passed and adds it to presignedBatches. Otherwise it +// the exact transaction passed and adds it to presignedBatches. Otherwise, it // looks for a transaction in presignedBatches satisfying the criteria. func (h *mockPresignedHelper) SignTx(ctx context.Context, primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount, @@ -211,6 +227,9 @@ func (h *mockPresignedHelper) FetchSweep(_ context.Context, // Find IsPresigned. _, isPresigned := h.onlineOutpoints[utxo] + // Find change. + change := h.changeOutputs[utxo] + return &SweepInfo{ // Set Timeout to prevent warning messages about timeout=0. Timeout: sweepTimeout, @@ -220,6 +239,7 @@ func (h *mockPresignedHelper) FetchSweep(_ context.Context, HTLC: swap.Htlc{ PkScript: []byte{10, 11, 12}, }, + Change: change, }, nil } @@ -273,7 +293,7 @@ func testPresigned_forgotten_presign(t *testing.T, presignedHelper.SetOutpointOnline(op1, false) err := batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.Error(t, err) require.ErrorContains(t, err, "offline") @@ -350,7 +370,7 @@ func testPresigned_input1_offline_then_input2(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -413,7 +433,7 @@ func testPresigned_input1_offline_then_input2(t *testing.T, presignedHelper.SetOutpointOnline(op2, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op2, Value: 2_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -520,7 +540,7 @@ func testPresigned_min_relay_fee(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err := batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: inputAmt}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -644,7 +664,7 @@ func testPresigned_two_inputs_one_goes_offline(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq1)) @@ -670,7 +690,7 @@ func testPresigned_two_inputs_one_goes_offline(t *testing.T, presignedHelper.SetOutpointOnline(op2, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op2, Value: 2_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq2)) @@ -780,7 +800,7 @@ func testPresigned_first_publish_fails(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) presignedHelper.SetOutpointOnline(op1, false) @@ -903,7 +923,7 @@ func testPresigned_locktime(t *testing.T, presignedHelper.SetOutpointOnline(op1, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op1, Value: 1_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) presignedHelper.SetOutpointOnline(op1, false) @@ -994,14 +1014,18 @@ func testPresigned_presigned_group(t *testing.T, presignedHelper.SetOutpointOnline(op2, false) // An attempt to presign must fail. - err = batcher.PresignSweepsGroup(ctx, group1, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, nil, + ) require.ErrorContains(t, err, "some outpoint is offline") // Enable both outpoints. presignedHelper.SetOutpointOnline(op2, true) // An attempt to presign must succeed. - err = batcher.PresignSweepsGroup(ctx, group1, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, nil, + ) require.NoError(t, err) // Add the sweep, triggering the publish attempt. @@ -1053,7 +1077,9 @@ func testPresigned_presigned_group(t *testing.T, presignedHelper.SetOutpointOnline(op4, true) // An attempt to presign must succeed. - err = batcher.PresignSweepsGroup(ctx, group2, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group2, sweepTimeout, destAddr, nil, + ) require.NoError(t, err) // Add the sweep. It should go to the same batch. @@ -1107,7 +1133,9 @@ func testPresigned_presigned_group(t *testing.T, presignedHelper.SetOutpointOnline(op6, true) // An attempt to presign must succeed. - err = batcher.PresignSweepsGroup(ctx, group3, sweepTimeout, destAddr) + err = batcher.PresignSweepsGroup( + ctx, group3, sweepTimeout, destAddr, nil, + ) require.NoError(t, err) // Add the sweep. It should go to the same batch. @@ -1135,6 +1163,271 @@ func testPresigned_presigned_group(t *testing.T, require.Equal(t, batchPkScript, tx.TxOut[0].PkScript) } +// testPresigned_presigned_group_with_change tests passing multiple sweeps to +// the method PresignSweepsGroup. It tests that a change output of a primary +// deposit sweep is properly added to the presigned transaction. +func testPresigned_presigned_group_with_change(t *testing.T, + batcherStore testBatcherStore) { + + defer test.Guard(t)() + + batchPkScript, err := txscript.PayToAddrScript(destAddr) + require.NoError(t, err) + + lnd := test.NewMockLnd() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + customFeeRate := func(_ context.Context, _ lntypes.Hash, + _ wire.OutPoint) (chainfee.SatPerKWeight, error) { + + return chainfee.SatPerKWeight(10_000), nil + } + + presignedHelper := newMockPresignedHelper() + + batcher := NewBatcher( + lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, + testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams, + batcherStore, presignedHelper, + WithCustomFeeRate(customFeeRate), + WithPresignedHelper(presignedHelper), + ) + + go func() { + err := batcher.Run(ctx) + checkBatcherError(t, err) + }() + + // Create a swap of two sweeps. + swapHash1 := lntypes.Hash{1, 1, 1} + op1 := wire.OutPoint{ + Hash: chainhash.Hash{1, 1}, + Index: 1, + } + op2 := wire.OutPoint{ + Hash: chainhash.Hash{2, 2}, + Index: 2, + } + group1 := []Input{ + { + Outpoint: op1, + Value: 1_000_000, + }, + { + Outpoint: op2, + Value: 2_000_000, + }, + } + change := &wire.TxOut{ + Value: 500_000, + PkScript: []byte{0xaf, 0xfe}, + } + + presignedHelper.setChangeForPrimaryDeposit(op1, change) + + // Enable only one of the sweeps. + presignedHelper.SetOutpointOnline(op1, true) + presignedHelper.SetOutpointOnline(op2, true) + + // An attempt to presign must fail. + err = batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, change, + ) + require.NoError(t, err) + + // Add the sweep, triggering the publishing attempt. + err = batcher.AddSweep(ctx, &SweepRequest{ + SwapHash: swapHash1, + Inputs: group1, + Notifier: &dummyNotifier, + }) + require.NoError(t, err) + + // Since a batch was created we check that it registered for its primary + // sweep's spend. + <-lnd.RegisterSpendChannel + + // Wait for a transactions to be published. + tx := <-lnd.TxPublishChannel + require.Len(t, tx.TxIn, 2) + require.Len(t, tx.TxOut, 2) + require.ElementsMatch( + t, []wire.OutPoint{op1, op2}, + []wire.OutPoint{ + tx.TxIn[0].PreviousOutPoint, + tx.TxIn[1].PreviousOutPoint, + }, + ) + require.Equal(t, int64(2_493_300), tx.TxOut[0].Value) + require.Equal(t, change.Value, tx.TxOut[1].Value) + require.Equal(t, batchPkScript, tx.TxOut[0].PkScript) + require.Equal(t, change.PkScript, tx.TxOut[1].PkScript) + + // Mine a blocks to trigger republishing. + require.NoError(t, lnd.NotifyHeight(601)) +} + +// testPresigned_presigned_group_with_dust_main_output 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_main_output(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, + } + inputValue := int64(1_000_000) + group1 := []Input{ + { + Outpoint: op1, + Value: 1_000_000, + }, + } + dustLimit := int64(lnwallet.DustLimitForSize(input.P2TRSize)) + change := &wire.TxOut{ + Value: inputValue - dustLimit + 1, + PkScript: []byte{0xaf, 0xfe}, + } + + presignedHelper.setChangeForPrimaryDeposit(op1, change) + + // Enable only one of the sweeps. + presignedHelper.SetOutpointOnline(op1, true) + + // An attempt to presign must fail. + err := batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, change, + ) + require.EqualError(t, err, "failed to construct unsigned tx for "+ + "feeRate 253 sat/kw: batch amount 0.01000000 BTC is <= the "+ + "sum of change outputs 0.00999671 BTC plus fee "+ + "0.00000065 BTC and dust limit 0.00000294 BTC") + + // Add the sweep, triggering the publishing attempt. + err = batcher.AddSweep(ctx, &SweepRequest{ + SwapHash: swapHash1, + Inputs: group1, + Notifier: &dummyNotifier, + }) + require.ErrorContains(t, err, "were not presigned") +} + +// 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, + }, + } + dustLimit := lnwallet.DustLimitForSize(input.P2TRSize) + change := &wire.TxOut{ + Value: int64(dustLimit - 1), + PkScript: []byte{0xaf, 0xfe}, + } + + presignedHelper.setChangeForPrimaryDeposit(op1, change) + + // Enable only one of the sweeps. + presignedHelper.SetOutpointOnline(op1, true) + presignedHelper.SetOutpointOnline(op2, true) + + // An attempt to presign must fail. + err := batcher.PresignSweepsGroup( + ctx, group1, sweepTimeout, destAddr, change, + ) + require.EqualError(t, err, "failed to construct unsigned tx for "+ + "feeRate 253 sat/kw: output 0.00000329 BTC is below dust "+ + "limit 0.00000477 BTC") + + // Add the sweep, triggering the publishing attempt. + err = batcher.AddSweep(ctx, &SweepRequest{ + SwapHash: swapHash1, + Inputs: group1, + Notifier: &dummyNotifier, + }) + require.ErrorContains(t, err, "were not presigned") +} + // wrappedStoreWithPresignedFlag wraps a SweepFetcher store adding IsPresigned // flag to the returned sweeps, taking it from mockPresignedHelper. type wrappedStoreWithPresignedFlag struct { @@ -1304,7 +1597,7 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, presignedHelper.SetOutpointOnline(op2, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op2, Value: 2_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq2)) @@ -1399,7 +1692,7 @@ func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore, presignedHelper.SetOutpointOnline(op4, true) err = batcher.PresignSweepsGroup( ctx, []Input{{Outpoint: op4, Value: 3_000_000}}, - sweepTimeout, destAddr, + sweepTimeout, destAddr, nil, ) require.NoError(t, err) require.NoError(t, batcher.AddSweep(ctx, &sweepReq4)) @@ -1520,7 +1813,7 @@ func testPresigned_purging(t *testing.T, numSwaps, numConfirmedSwaps int, // An attempt to presign must succeed. err := batcher.PresignSweepsGroup( - ctx, group, sweepTimeout, destAddr, + ctx, group, sweepTimeout, destAddr, nil, ) require.NoError(t, err) @@ -1584,11 +1877,11 @@ func testPresigned_purging(t *testing.T, numSwaps, numConfirmedSwaps int, // An attempt to presign must succeed. err := batcher.PresignSweepsGroup( - ctx, group, sweepTimeout, destAddr, + ctx, group, sweepTimeout, destAddr, nil, ) require.NoError(t, err) - // Add the sweep, triggering the publish attempt. + // Add the sweep, triggering the publishing attempt. require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{ SwapHash: swapHash, Inputs: group, @@ -1799,6 +2092,20 @@ func TestPresigned(t *testing.T) { testPresigned_presigned_group(t, NewStoreMock()) }) + t.Run("change", func(t *testing.T) { + testPresigned_presigned_group_with_change(t, NewStoreMock()) + }) + + t.Run("dust_main_output", func(t *testing.T) { + testPresigned_presigned_group_with_dust_main_output( + t, NewStoreMock(), + ) + }) + + t.Run("dust_change", func(t *testing.T) { + testPresigned_presigned_group_with_dust_change(t, NewStoreMock()) + }) + t.Run("presigned_and_regular_sweeps", func(t *testing.T) { runTests(t, testPresigned_presigned_and_regular_sweeps) }) diff --git a/sweepbatcher/sweep_batcher_test.go b/sweepbatcher/sweep_batcher_test.go index 12672443..f7430781 100644 --- a/sweepbatcher/sweep_batcher_test.go +++ b/sweepbatcher/sweep_batcher_test.go @@ -229,7 +229,7 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore, sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op1, }}, Notifier: &dummyNotifier, @@ -238,7 +238,7 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -276,7 +276,7 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore, sweepReq2 := SweepRequest{ SwapHash: lntypes.Hash{2, 2, 2}, Inputs: []Input{{ - Value: 222, + Value: 2222, Outpoint: op2, }}, Notifier: &dummyNotifier, @@ -285,7 +285,7 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore, swap2 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111 + defaultMaxTimeoutDistance - 1, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -322,7 +322,7 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore, sweepReq3 := SweepRequest{ SwapHash: lntypes.Hash{3, 3, 3}, Inputs: []Input{{ - Value: 333, + Value: 3333, Outpoint: op3, }}, Notifier: &dummyNotifier, @@ -331,7 +331,7 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore, swap3 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111 + defaultMaxTimeoutDistance + 1, - AmountRequested: 333, + AmountRequested: 3333, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -539,7 +539,7 @@ func testTxLabeler(t *testing.T, store testStore, sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op1, }}, Notifier: &dummyNotifier, @@ -548,7 +548,7 @@ func testTxLabeler(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -692,7 +692,7 @@ func testPublishErrorHandler(t *testing.T, store testStore, sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{1, 1}, Index: 1, @@ -704,7 +704,7 @@ func testPublishErrorHandler(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -774,7 +774,7 @@ func testSweepBatcherSimpleLifecycle(t *testing.T, store testStore, Index: 1, } const ( - inputValue = 111 + inputValue = 1111 outputValue = 50 fee = inputValue - outputValue ) @@ -1209,7 +1209,7 @@ func testSweepBatcherSkippedTxns(t *testing.T, store testStore, } swapHash := lntypes.Hash{1, 1, 1} const ( - inputValue = 111 + inputValue = 1111 initiationHeight = 550 ) @@ -1419,7 +1419,7 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) { sweepReq := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op1, }}, Notifier: &dummyNotifier, @@ -1428,7 +1428,7 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) { swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 1000, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -1712,7 +1712,7 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) { sweepReq2 := SweepRequest{ SwapHash: lntypes.Hash{2, 2, 2}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{2, 2}, Index: 2, @@ -1727,7 +1727,7 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) { // CltvExpiry is not urgent, but close. CltvExpiry: 600 + blocksInDelay*2 + 5, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -1795,7 +1795,7 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) { sweepReq3 := SweepRequest{ SwapHash: lntypes.Hash{3, 3, 3}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{3, 3}, Index: 3, @@ -1808,7 +1808,7 @@ func testDelays(t *testing.T, store testStore, batcherStore testBatcherStore) { // CltvExpiry is urgent. CltvExpiry: 600 + blocksInDelay*2 - 5, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -1871,8 +1871,8 @@ func testCustomDelays(t *testing.T, store testStore, swapHash2 := lntypes.Hash{2, 2, 2} const ( - swapSize1 = 111 - swapSize2 = 222 + swapSize1 = 1111 + swapSize2 = 2222 ) // initialDelay returns initialDelay depending of batch size (sats). @@ -1945,7 +1945,7 @@ func testCustomDelays(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 1000, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -2013,7 +2013,7 @@ func testCustomDelays(t *testing.T, store testStore, swap2 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 1000, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2152,7 +2152,7 @@ func testMaxSweepsPerBatch(t *testing.T, store testStore, sweepReq := SweepRequest{ SwapHash: swapHash, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: outpoint, }}, Notifier: &dummyNotifier, @@ -2161,7 +2161,7 @@ func testMaxSweepsPerBatch(t *testing.T, store testStore, swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 1000, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2269,7 +2269,7 @@ func testSweepBatcherSweepReentry(t *testing.T, store testStore, Hash: chainhash.Hash{1, 1}, Index: 1, } - value1 := btcutil.Amount(111) + value1 := btcutil.Amount(1111) sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ @@ -2282,7 +2282,7 @@ func testSweepBatcherSweepReentry(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -2298,7 +2298,7 @@ func testSweepBatcherSweepReentry(t *testing.T, store testStore, sweepReq2 := SweepRequest{ SwapHash: lntypes.Hash{2, 2, 2}, Inputs: []Input{{ - Value: 222, + Value: 2222, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{2, 2}, Index: 2, @@ -2310,7 +2310,7 @@ func testSweepBatcherSweepReentry(t *testing.T, store testStore, swap2 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2329,7 +2329,7 @@ func testSweepBatcherSweepReentry(t *testing.T, store testStore, sweepReq3 := SweepRequest{ SwapHash: lntypes.Hash{3, 3, 3}, Inputs: []Input{{ - Value: 333, + Value: 3333, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{3, 3}, Index: 3, @@ -2341,7 +2341,7 @@ func testSweepBatcherSweepReentry(t *testing.T, store testStore, swap3 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 333, + AmountRequested: 3333, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2536,7 +2536,7 @@ func testSweepBatcherGroup(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -2555,11 +2555,11 @@ func testSweepBatcherGroup(t *testing.T, store testStore, Inputs: []Input{ { Outpoint: outpoint1, - Value: 111, + Value: 1111, }, { Outpoint: outpoint2, - Value: 222, + Value: 2222, }, }, Notifier: &dummyNotifier, @@ -2621,7 +2621,7 @@ func testSweepBatcherNonWalletAddr(t *testing.T, store testStore, sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op1, }}, Notifier: &dummyNotifier, @@ -2630,7 +2630,7 @@ func testSweepBatcherNonWalletAddr(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -2668,7 +2668,7 @@ func testSweepBatcherNonWalletAddr(t *testing.T, store testStore, sweepReq2 := SweepRequest{ SwapHash: lntypes.Hash{2, 2, 2}, Inputs: []Input{{ - Value: 222, + Value: 2222, Outpoint: op2, }}, Notifier: &dummyNotifier, @@ -2677,7 +2677,7 @@ func testSweepBatcherNonWalletAddr(t *testing.T, store testStore, swap2 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111 + defaultMaxTimeoutDistance - 1, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2714,7 +2714,7 @@ func testSweepBatcherNonWalletAddr(t *testing.T, store testStore, sweepReq3 := SweepRequest{ SwapHash: lntypes.Hash{3, 3, 3}, Inputs: []Input{{ - Value: 333, + Value: 3333, Outpoint: op3, }}, Notifier: &dummyNotifier, @@ -2723,7 +2723,7 @@ func testSweepBatcherNonWalletAddr(t *testing.T, store testStore, swap3 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111 + defaultMaxTimeoutDistance + 1, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2799,6 +2799,8 @@ func testSweepBatcherComposite(t *testing.T, store testStore, ctx, cancel := context.WithCancel(context.Background()) defer cancel() + lnd.SetMinRelayFee(200) + sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams) require.NoError(t, err) @@ -2839,7 +2841,7 @@ func testSweepBatcherComposite(t *testing.T, store testStore, sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op1, }}, Notifier: &dummyNotifier, @@ -2848,7 +2850,7 @@ func testSweepBatcherComposite(t *testing.T, store testStore, swap1 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -2866,7 +2868,7 @@ func testSweepBatcherComposite(t *testing.T, store testStore, sweepReq2 := SweepRequest{ SwapHash: lntypes.Hash{2, 2, 2}, Inputs: []Input{{ - Value: 222, + Value: 2222, Outpoint: op2, }}, Notifier: &dummyNotifier, @@ -2875,7 +2877,7 @@ func testSweepBatcherComposite(t *testing.T, store testStore, swap2 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111 + defaultMaxTimeoutDistance - 1, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -2896,7 +2898,7 @@ func testSweepBatcherComposite(t *testing.T, store testStore, sweepReq3 := SweepRequest{ SwapHash: lntypes.Hash{3, 3, 3}, Inputs: []Input{{ - Value: 333, + Value: 3333, Outpoint: op3, }}, Notifier: &dummyNotifier, @@ -2905,7 +2907,7 @@ func testSweepBatcherComposite(t *testing.T, store testStore, swap3 := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111 + defaultMaxTimeoutDistance - 3, - AmountRequested: 333, + AmountRequested: 3333, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, @@ -3242,7 +3244,7 @@ func testRestoringEmptyBatch(t *testing.T, store testStore, sweepReq := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op, }}, Notifier: &dummyNotifier, @@ -3251,7 +3253,7 @@ func testRestoringEmptyBatch(t *testing.T, store testStore, swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -3426,7 +3428,7 @@ func testHandleSweepTwice(t *testing.T, backend testStore, sweepReq1 := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op1, }}, Notifier: &dummyNotifier, @@ -3439,7 +3441,7 @@ func testHandleSweepTwice(t *testing.T, backend testStore, Contract: &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: shortCltv, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -3456,7 +3458,7 @@ func testHandleSweepTwice(t *testing.T, backend testStore, sweepReq2 := SweepRequest{ SwapHash: lntypes.Hash{2, 2, 2}, Inputs: []Input{{ - Value: 222, + Value: 2222, Outpoint: op2, }}, Notifier: &dummyNotifier, @@ -3469,7 +3471,7 @@ func testHandleSweepTwice(t *testing.T, backend testStore, Contract: &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: longCltv, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -3524,7 +3526,7 @@ func testHandleSweepTwice(t *testing.T, backend testStore, Contract: &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: shortCltv, - AmountRequested: 222, + AmountRequested: 2222, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -3628,7 +3630,7 @@ func testRestoringPreservesConfTarget(t *testing.T, store testStore, sweepReq := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: op, }}, Notifier: &dummyNotifier, @@ -3637,7 +3639,7 @@ func testRestoringPreservesConfTarget(t *testing.T, store testStore, swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, }, @@ -3955,7 +3957,7 @@ func testSweepBatcherCloseDuringAdding(t *testing.T, store testStore, swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, // Make preimage unique to pass SQL constraints. Preimage: lntypes.Preimage{i}, @@ -3981,7 +3983,7 @@ func testSweepBatcherCloseDuringAdding(t *testing.T, store testStore, sweepReq := SweepRequest{ SwapHash: lntypes.Hash{i, i, i}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{i, i}, Index: 1, @@ -4066,7 +4068,7 @@ func testCustomSignMuSig2(t *testing.T, store testStore, sweepReq := SweepRequest{ SwapHash: lntypes.Hash{1, 1, 1}, Inputs: []Input{{ - Value: 111, + Value: 1111, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{1, 1}, Index: 1, @@ -4078,7 +4080,7 @@ func testCustomSignMuSig2(t *testing.T, store testStore, swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: 111, - AmountRequested: 111, + AmountRequested: 1111, ProtocolVersion: loopdb.ProtocolVersionMuSig2, HtlcKeys: htlcKeys, },