mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #976 from hieblmi/batcher-change-outputs
sweepbatcher: batch change outputs
This commit is contained in:
commit
dd85a618ed
13 changed files with 1689 additions and 164 deletions
|
|
@ -280,6 +280,7 @@ func testCustomSweepConfTarget(t *testing.T) {
|
|||
// yields a much higher fee rate.
|
||||
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, 250)
|
||||
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000)
|
||||
ctx.Lnd.SetMinRelayFee(250)
|
||||
|
||||
cfg := newSwapConfig(
|
||||
&lnd.LndServices, loopdb.NewStoreMock(t), server, nil,
|
||||
|
|
|
|||
|
|
@ -210,6 +210,23 @@ func estimateBatchWeight(batch *batch) (feeDetails, error) {
|
|||
err)
|
||||
}
|
||||
|
||||
// Add change output weights. Change outputs with identical pkscript
|
||||
// will be consolidated into a single output.
|
||||
changeOutputs := make(map[string]struct{})
|
||||
for _, s := range batch.sweeps {
|
||||
if s.change == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pkScriptString := string(s.change.PkScript)
|
||||
if _, has := changeOutputs[pkScriptString]; has {
|
||||
continue
|
||||
}
|
||||
|
||||
weight.AddOutput(s.change.PkScript)
|
||||
changeOutputs[pkScriptString] = struct{}{}
|
||||
}
|
||||
|
||||
// Add inputs.
|
||||
for _, sweep := range batch.sweeps {
|
||||
if sweep.nonCoopHint || sweep.coopFailed {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
|
|
@ -16,24 +17,29 @@ import (
|
|||
|
||||
// Useful constants for tests.
|
||||
const (
|
||||
lowFeeRate = chainfee.FeePerKwFloor
|
||||
highFeeRate = chainfee.SatPerKWeight(30000)
|
||||
lowFeeRate = chainfee.FeePerKwFloor
|
||||
mediumFeeRate = lowFeeRate + 200
|
||||
highFeeRate = chainfee.SatPerKWeight(30000)
|
||||
|
||||
coopInputWeight = lntypes.WeightUnit(230)
|
||||
batchOutputWeight = lntypes.WeightUnit(343)
|
||||
nonCoopInputWeight = lntypes.WeightUnit(393)
|
||||
nonCoopPenalty = nonCoopInputWeight - coopInputWeight
|
||||
coopNewBatchWeight = lntypes.WeightUnit(444)
|
||||
nonCoopNewBatchWeight = coopNewBatchWeight + nonCoopPenalty
|
||||
changeOutputWeight = lntypes.WeightUnit(input.P2TROutputSize)
|
||||
|
||||
// p2pkhDiscount is weight discount P2PKH output has over P2TR output.
|
||||
p2pkhDiscount = lntypes.WeightUnit(
|
||||
input.P2TROutputSize-input.P2PKHOutputSize,
|
||||
) * 4
|
||||
|
||||
coopTwoSweepBatchWeight = coopNewBatchWeight + coopInputWeight
|
||||
nonCoopTwoSweepBatchWeight = coopTwoSweepBatchWeight + 2*nonCoopPenalty
|
||||
v2v3BatchWeight = nonCoopTwoSweepBatchWeight - 25
|
||||
mixedTwoSweepBatchWeight = coopTwoSweepBatchWeight + nonCoopPenalty
|
||||
coopTwoSweepBatchWeight = coopNewBatchWeight + coopInputWeight
|
||||
coopSingleSweepChangeBatchWeight = coopInputWeight + batchOutputWeight + changeOutputWeight
|
||||
coopDoubleSweepChangeBatchWeight = 2*coopInputWeight + batchOutputWeight + changeOutputWeight
|
||||
nonCoopTwoSweepBatchWeight = coopTwoSweepBatchWeight + 2*nonCoopPenalty
|
||||
v2v3BatchWeight = nonCoopTwoSweepBatchWeight - 25
|
||||
mixedTwoSweepBatchWeight = coopTwoSweepBatchWeight + nonCoopPenalty
|
||||
)
|
||||
|
||||
// testHtlcV2SuccessEstimator adds weight of non-cooperative input to estimator
|
||||
|
|
@ -265,6 +271,13 @@ func TestEstimateBatchWeight(t *testing.T) {
|
|||
se3 := testHtlcV3SuccessEstimator
|
||||
trAddr := (*btcutil.AddressTaproot)(nil)
|
||||
|
||||
changeAddr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" +
|
||||
"lwyyeq5ryn6r"
|
||||
changeAddress, err := btcutil.DecodeAddress(changeAddr, nil)
|
||||
require.NoError(t, err)
|
||||
changePkscript, err := txscript.PayToAddrScript(changeAddress)
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
batch *batch
|
||||
|
|
@ -290,6 +303,58 @@ func TestEstimateBatchWeight(t *testing.T) {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "one sweep regular batch with change",
|
||||
batch: &batch{
|
||||
id: 1,
|
||||
rbfCache: rbfCache{
|
||||
FeeRate: lowFeeRate,
|
||||
},
|
||||
sweeps: map[wire.OutPoint]sweep{
|
||||
outpoint1: {
|
||||
htlcSuccessEstimator: se3,
|
||||
change: &wire.TxOut{
|
||||
PkScript: changePkscript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantBatchFeeDetails: feeDetails{
|
||||
BatchId: 1,
|
||||
FeeRate: lowFeeRate,
|
||||
Weight: coopSingleSweepChangeBatchWeight,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "two sweeps regular batch with identical change",
|
||||
batch: &batch{
|
||||
id: 1,
|
||||
rbfCache: rbfCache{
|
||||
FeeRate: lowFeeRate,
|
||||
},
|
||||
sweeps: map[wire.OutPoint]sweep{
|
||||
outpoint1: {
|
||||
htlcSuccessEstimator: se3,
|
||||
change: &wire.TxOut{
|
||||
PkScript: changePkscript,
|
||||
},
|
||||
},
|
||||
outpoint2: {
|
||||
htlcSuccessEstimator: se3,
|
||||
change: &wire.TxOut{
|
||||
PkScript: changePkscript,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantBatchFeeDetails: feeDetails{
|
||||
BatchId: 1,
|
||||
FeeRate: lowFeeRate,
|
||||
Weight: coopDoubleSweepChangeBatchWeight,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "two sweeps regular batch",
|
||||
batch: &batch{
|
||||
|
|
@ -778,6 +843,47 @@ func TestSelectBatches(t *testing.T) {
|
|||
},
|
||||
wantBestBatchesIds: []int32{1, newBatchSignal},
|
||||
},
|
||||
|
||||
{
|
||||
name: "low fee change sweep, placed in new batch",
|
||||
batches: []feeDetails{
|
||||
{
|
||||
BatchId: 1,
|
||||
FeeRate: mediumFeeRate,
|
||||
Weight: coopNewBatchWeight,
|
||||
},
|
||||
},
|
||||
sweep: feeDetails{
|
||||
FeeRate: lowFeeRate,
|
||||
Weight: coopInputWeight + changeOutputWeight,
|
||||
},
|
||||
oneSweepBatch: feeDetails{
|
||||
FeeRate: lowFeeRate,
|
||||
Weight: coopNewBatchWeight,
|
||||
},
|
||||
wantBestBatchesIds: []int32{newBatchSignal, 1},
|
||||
},
|
||||
|
||||
{
|
||||
name: "low fee sweep without change, placed in " +
|
||||
"existing batch",
|
||||
batches: []feeDetails{
|
||||
{
|
||||
BatchId: 1,
|
||||
FeeRate: mediumFeeRate,
|
||||
Weight: coopNewBatchWeight,
|
||||
},
|
||||
},
|
||||
sweep: feeDetails{
|
||||
FeeRate: lowFeeRate,
|
||||
Weight: coopInputWeight,
|
||||
},
|
||||
oneSweepBatch: feeDetails{
|
||||
FeeRate: lowFeeRate,
|
||||
Weight: coopNewBatchWeight,
|
||||
},
|
||||
wantBestBatchesIds: []int32{1, newBatchSignal},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,14 @@ func (b *batch) ensurePresigned(ctx context.Context, newSweeps []*sweep,
|
|||
"adding to an empty batch")
|
||||
}
|
||||
|
||||
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get minRelayFee: %w", err)
|
||||
}
|
||||
|
||||
return ensurePresigned(
|
||||
ctx, newSweeps, b.cfg.presignedHelper, b.cfg.chainParams,
|
||||
ctx, newSweeps, b.cfg.presignedHelper, minRelayFeeRate,
|
||||
b.cfg.chainParams,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +49,7 @@ type presignedTxChecker interface {
|
|||
// inputs of this group only.
|
||||
func ensurePresigned(ctx context.Context, newSweeps []*sweep,
|
||||
presignedTxChecker presignedTxChecker,
|
||||
minRelayFeeRate chainfee.SatPerKWeight,
|
||||
chainParams *chaincfg.Params) error {
|
||||
|
||||
sweeps := make([]sweep, len(newSweeps))
|
||||
|
|
@ -51,6 +58,7 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep,
|
|||
outpoint: s.outpoint,
|
||||
value: s.value,
|
||||
presigned: s.presigned,
|
||||
change: s.change,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,10 +78,10 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep,
|
|||
const currentHeight = 0
|
||||
|
||||
// Check if we can sign with minimum fee rate.
|
||||
const feeRate = chainfee.FeePerKwFloor
|
||||
feeRate := minRelayFeeRate
|
||||
|
||||
tx, _, _, _, err := constructUnsignedTx(
|
||||
sweeps, destAddr, currentHeight, feeRate,
|
||||
sweeps, destAddr, currentHeight, feeRate, minRelayFeeRate,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to construct unsigned tx "+
|
||||
|
|
@ -217,6 +225,14 @@ func (b *batch) presign(ctx context.Context, newSweeps []*sweep) error {
|
|||
|
||||
b.Infof("nextBlockFeeRate is %v", nextBlockFeeRate)
|
||||
|
||||
// Find the minRelayFeeRate.
|
||||
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get minRelayFeeRate: %w", err)
|
||||
}
|
||||
|
||||
b.Infof("minRelayFeeRate is %v", minRelayFeeRate)
|
||||
|
||||
// We need to restore previously added groups. We can do it by reading
|
||||
// all the sweeps from DB (they must be ordered) and grouping by swap.
|
||||
groups, err := b.getSweepsGroups(ctx)
|
||||
|
|
@ -257,7 +273,7 @@ func (b *batch) presign(ctx context.Context, newSweeps []*sweep) error {
|
|||
|
||||
err = presign(
|
||||
ctx, b.cfg.presignedHelper, destAddr, primarySweepID,
|
||||
sweeps, nextBlockFeeRate,
|
||||
sweeps, nextBlockFeeRate, minRelayFeeRate,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to presign a transaction "+
|
||||
|
|
@ -299,7 +315,8 @@ type presigner interface {
|
|||
// 10x of the current next block feerate.
|
||||
func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
|
||||
primarySweepID wire.OutPoint, sweeps []sweep,
|
||||
nextBlockFeeRate chainfee.SatPerKWeight) error {
|
||||
nextBlockFeeRate chainfee.SatPerKWeight,
|
||||
minRelayFeeRate chainfee.SatPerKWeight) error {
|
||||
|
||||
if presigner == nil {
|
||||
return fmt.Errorf("presigner is not installed")
|
||||
|
|
@ -313,6 +330,10 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
|
|||
return fmt.Errorf("nextBlockFeeRate is not set")
|
||||
}
|
||||
|
||||
if minRelayFeeRate == 0 {
|
||||
return fmt.Errorf("minRelayFeeRate is not set")
|
||||
}
|
||||
|
||||
// Keep track of the total amount this batch is sweeping back.
|
||||
batchAmt := btcutil.Amount(0)
|
||||
for _, sweep := range sweeps {
|
||||
|
|
@ -328,9 +349,9 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
|
|||
return fmt.Errorf("timeout is invalid: %d", timeout)
|
||||
}
|
||||
|
||||
// Go from the floor (1.01 sat/vbyte) to 2k sat/vbyte with step of 1.2x.
|
||||
// Go from minRelayFeeRate to 2k sat/vbyte with step of 1.2x.
|
||||
var start = minRelayFeeRate
|
||||
const (
|
||||
start = chainfee.FeePerKwFloor
|
||||
stop = chainfee.AbsoluteFeePerKwFloor * 2_000
|
||||
factorPPM = 1_200_000
|
||||
timeoutThreshold = 50
|
||||
|
|
@ -353,7 +374,7 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
|
|||
for fr := start; fr <= stop; fr = (fr * factorPPM) / 1_000_000 {
|
||||
// Construct an unsigned transaction for this fee rate.
|
||||
tx, _, feeForWeight, fee, err := constructUnsignedTx(
|
||||
sweeps, destAddr, currentHeight, fr,
|
||||
sweeps, destAddr, currentHeight, fr, minRelayFeeRate,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to construct unsigned tx "+
|
||||
|
|
@ -367,12 +388,9 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
|
|||
}
|
||||
|
||||
// Try to presign this transaction.
|
||||
const (
|
||||
loadOnly = false
|
||||
minRelayFee = chainfee.AbsoluteFeePerKwFloor
|
||||
)
|
||||
const loadOnly = false
|
||||
_, err = presigner.SignTx(
|
||||
ctx, primarySweepID, tx, batchAmt, minRelayFee, fr,
|
||||
ctx, primarySweepID, tx, batchAmt, minRelayFeeRate, fr,
|
||||
loadOnly,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -410,15 +428,15 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
|
|||
}
|
||||
|
||||
// Determine the current minimum relay fee based on our chain backend.
|
||||
minRelayFee, err := b.wallet.MinRelayFee(ctx)
|
||||
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get minRelayFee: %w", err),
|
||||
return 0, fmt.Errorf("failed to get minRelayFeeRate: %w", err),
|
||||
false
|
||||
}
|
||||
|
||||
// Cache current height and desired feerate of the batch.
|
||||
currentHeight := b.currentHeight
|
||||
feeRate := max(b.rbfCache.FeeRate, minRelayFee)
|
||||
feeRate := max(b.rbfCache.FeeRate, minRelayFeeRate)
|
||||
|
||||
// Append this sweep to an array of sweeps. This is needed to keep the
|
||||
// order of sweeps stored, as iterating the sweeps map does not
|
||||
|
|
@ -440,7 +458,7 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
|
|||
|
||||
// Construct unsigned batch transaction.
|
||||
tx, weight, _, fee, err := constructUnsignedTx(
|
||||
sweeps, address, currentHeight, feeRate,
|
||||
sweeps, address, currentHeight, feeRate, minRelayFeeRate,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to construct tx: %w", err),
|
||||
|
|
@ -459,7 +477,7 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
|
|||
// Get a pre-signed transaction.
|
||||
const loadOnly = false
|
||||
signedTx, err := b.cfg.presignedHelper.SignTx(
|
||||
ctx, b.primarySweepID, tx, batchAmt, minRelayFee, feeRate,
|
||||
ctx, b.primarySweepID, tx, batchAmt, minRelayFeeRate, feeRate,
|
||||
loadOnly,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -469,7 +487,7 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
|
|||
|
||||
// Run sanity checks to make sure presignedHelper.SignTx complied with
|
||||
// all the invariants.
|
||||
err = CheckSignedTx(tx, signedTx, batchAmt, minRelayFee)
|
||||
err = CheckSignedTx(tx, signedTx, batchAmt, minRelayFeeRate)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("signed tx doesn't correspond the "+
|
||||
"unsigned tx: %w", err), false
|
||||
|
|
@ -493,10 +511,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 +613,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)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -486,6 +486,7 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
primarySweepID wire.OutPoint
|
||||
sweeps []*sweep
|
||||
destPkScript []byte
|
||||
minRelayFeeRate chainfee.SatPerKWeight
|
||||
wantInputAmt btcutil.Amount
|
||||
destPkScriptErr error
|
||||
signedTxErr error
|
||||
|
|
@ -500,8 +501,24 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destPkScript: batchPkScript,
|
||||
wantInputAmt: 1_000_000,
|
||||
destPkScript: batchPkScript,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantInputAmt: 1_000_000,
|
||||
},
|
||||
|
||||
{
|
||||
name: "one input, higher minRelayFeeRate",
|
||||
primarySweepID: op1,
|
||||
sweeps: []*sweep{
|
||||
{
|
||||
outpoint: op1,
|
||||
value: 1_000_000,
|
||||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destPkScript: batchPkScript,
|
||||
minRelayFeeRate: 1000,
|
||||
wantInputAmt: 1_000_000,
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -519,8 +536,9 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destPkScript: batchPkScript,
|
||||
wantInputAmt: 3_000_000,
|
||||
destPkScript: batchPkScript,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantInputAmt: 3_000_000,
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -533,6 +551,7 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
destPkScriptErr: fmt.Errorf("test DestPkScript error"),
|
||||
},
|
||||
|
||||
|
|
@ -546,8 +565,9 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destPkScript: batchPkScript,
|
||||
signedTxErr: fmt.Errorf("test SignTx error"),
|
||||
destPkScript: batchPkScript,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
signedTxErr: fmt.Errorf("test SignTx error"),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -561,7 +581,7 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
}
|
||||
|
||||
err := ensurePresigned(
|
||||
ctx, tc.sweeps, c,
|
||||
ctx, tc.sweeps, c, tc.minRelayFeeRate,
|
||||
&chaincfg.RegressionNetParams,
|
||||
)
|
||||
switch {
|
||||
|
|
@ -576,11 +596,11 @@ func TestEnsurePresigned(t *testing.T) {
|
|||
t, tc.wantInputAmt, c.recordedInputAmt,
|
||||
)
|
||||
require.Equal(
|
||||
t, chainfee.FeePerKwFloor,
|
||||
t, tc.minRelayFeeRate,
|
||||
c.recordedMinRelayFee,
|
||||
)
|
||||
require.Equal(
|
||||
t, chainfee.FeePerKwFloor,
|
||||
t, tc.minRelayFeeRate,
|
||||
c.recordedFeeRate,
|
||||
)
|
||||
require.True(t, c.recordedLoadOnly)
|
||||
|
|
@ -660,6 +680,7 @@ func TestPresign(t *testing.T) {
|
|||
sweeps []sweep
|
||||
destAddr btcutil.Address
|
||||
nextBlockFeeRate chainfee.SatPerKWeight
|
||||
minRelayFeeRate chainfee.SatPerKWeight
|
||||
wantErr string
|
||||
wantOutputs []btcutil.Amount
|
||||
wantLockTimes []uint32
|
||||
|
|
@ -676,6 +697,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "presigner is not installed",
|
||||
},
|
||||
|
||||
|
|
@ -685,6 +707,7 @@ func TestPresign(t *testing.T) {
|
|||
presigner: &mockPresigner{},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "there are no sweeps",
|
||||
},
|
||||
|
||||
|
|
@ -700,6 +723,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
},
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "unsupported address type <nil>",
|
||||
},
|
||||
|
||||
|
|
@ -719,8 +743,30 @@ func TestPresign(t *testing.T) {
|
|||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destAddr: destAddr,
|
||||
wantErr: "nextBlockFeeRate is not set",
|
||||
destAddr: destAddr,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "nextBlockFeeRate is not set",
|
||||
},
|
||||
|
||||
{
|
||||
name: "error: zero minRelayFeeRate",
|
||||
presigner: &mockPresigner{},
|
||||
primarySweepID: op1,
|
||||
sweeps: []sweep{
|
||||
{
|
||||
outpoint: op1,
|
||||
value: 1_000_000,
|
||||
timeout: 1000,
|
||||
},
|
||||
{
|
||||
outpoint: op2,
|
||||
value: 2_000_000,
|
||||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "minRelayFeeRate is not set",
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -739,6 +785,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "timeout is invalid: 0",
|
||||
},
|
||||
|
||||
|
|
@ -759,6 +806,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "not in tx",
|
||||
},
|
||||
|
||||
|
|
@ -775,6 +823,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "not in tx",
|
||||
},
|
||||
|
||||
|
|
@ -791,6 +840,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
999900, 999880, 999856, 999827, 999793, 999752,
|
||||
999702, 999643, 999572, 999486, 999384, 999260,
|
||||
|
|
@ -808,6 +858,34 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "higher minRelayFeeRate, fewer txns",
|
||||
presigner: &mockPresigner{},
|
||||
primarySweepID: op1,
|
||||
sweeps: []sweep{
|
||||
{
|
||||
outpoint: op1,
|
||||
value: 1_000_000,
|
||||
timeout: 1000,
|
||||
},
|
||||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: 10 * chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: 10 * chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
998998, 998797, 998557, 998269, 997923, 997507,
|
||||
997009, 996411, 995694, 994833, 993800, 992560,
|
||||
991072, 989286, 987144, 984573, 981488, 977786,
|
||||
973343, 968012, 961614, 953937, 944725, 933670,
|
||||
920405, 904486, 885383, 862460, 834952,
|
||||
},
|
||||
wantLockTimes: []uint32{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950,
|
||||
950, 950, 950, 950, 950, 950, 950, 950, 950,
|
||||
950, 950, 950, 950, 950,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "two sweeps",
|
||||
presigner: &mockPresigner{},
|
||||
|
|
@ -826,6 +904,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
2999841, 2999810, 2999773, 2999728, 2999673,
|
||||
2999608, 2999530, 2999436, 2999323, 2999188,
|
||||
|
|
@ -863,6 +942,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
2999841, 2999810, 2999773, 2999728, 2999673,
|
||||
2999608, 2999530, 2999436, 2999323, 2999188,
|
||||
|
|
@ -900,6 +980,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: 50 * chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
2999841, 2999810, 2999773, 2999728, 2999673,
|
||||
2999608, 2999530, 2999436, 2999323, 2999188,
|
||||
|
|
@ -936,6 +1017,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: 50 * chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
2999841, 2999810, 2999773, 2999728, 2999673,
|
||||
2999608, 2999530, 2999436, 2999323, 2999188,
|
||||
|
|
@ -972,6 +1054,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantOutputs: []btcutil.Amount{
|
||||
2841, 2810, 2773, 2728, 2673, 2608, 2530, 2436,
|
||||
2400,
|
||||
|
|
@ -1001,6 +1084,7 @@ func TestPresign(t *testing.T) {
|
|||
},
|
||||
destAddr: destAddr,
|
||||
nextBlockFeeRate: chainfee.FeePerKwFloor,
|
||||
minRelayFeeRate: chainfee.FeePerKwFloor,
|
||||
wantErr: "for feeRate 363 sat/kw",
|
||||
},
|
||||
}
|
||||
|
|
@ -1010,7 +1094,7 @@ func TestPresign(t *testing.T) {
|
|||
err := presign(
|
||||
ctx, tc.presigner, tc.destAddr,
|
||||
tc.primarySweepID, tc.sweeps,
|
||||
tc.nextBlockFeeRate,
|
||||
tc.nextBlockFeeRate, tc.minRelayFeeRate,
|
||||
)
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
|
|
@ -1460,7 +1544,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 +1602,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",
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -26,6 +27,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"
|
||||
|
|
@ -125,6 +127,9 @@ type sweep struct {
|
|||
|
||||
// presigned is set, if the sweep should be handled in presigned mode.
|
||||
presigned bool
|
||||
|
||||
// change is the optional change output of the sweep.
|
||||
change *wire.TxOut
|
||||
}
|
||||
|
||||
// batchState is the state of the batch.
|
||||
|
|
@ -1287,9 +1292,13 @@ 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,
|
||||
currentHeight int32, feeRate chainfee.SatPerKWeight,
|
||||
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx,
|
||||
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
|
||||
|
||||
// Sanity check, there should be at least 1 sweep in this batch.
|
||||
|
|
@ -1303,6 +1312,25 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
|
|||
LockTime: uint32(currentHeight),
|
||||
}
|
||||
|
||||
// Consolidate change outputs with identical pkscript.
|
||||
changeOutputs := make(map[string]*wire.TxOut)
|
||||
for _, s := range sweeps {
|
||||
if s.change == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stringPkScript := string(s.change.PkScript)
|
||||
if _, has := changeOutputs[stringPkScript]; has {
|
||||
changeOutputs[stringPkScript].Value += s.change.Value
|
||||
continue
|
||||
}
|
||||
|
||||
changeOutputs[stringPkScript] = &wire.TxOut{
|
||||
Value: s.change.Value,
|
||||
PkScript: s.change.PkScript,
|
||||
}
|
||||
}
|
||||
|
||||
// Add transaction inputs and estimate its weight.
|
||||
var weightEstimate input.TxWeightEstimator
|
||||
for _, sweep := range sweeps {
|
||||
|
|
@ -1348,6 +1376,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 {
|
||||
|
|
@ -1365,15 +1398,99 @@ 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, err := clampBatchFee(
|
||||
feeForWeight, batchAmt-btcutil.Amount(sumChange),
|
||||
minRelayFeeRate, weight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
|
||||
"fee: %w", err)
|
||||
}
|
||||
|
||||
// Ensure that batch amount is equal or 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. Sort the keys first to make tests
|
||||
// deterministic.
|
||||
sortedChangeOutputs := make([]*wire.TxOut, 0, len(changeOutputs))
|
||||
for _, output := range changeOutputs {
|
||||
sortedChangeOutputs = append(sortedChangeOutputs, output)
|
||||
}
|
||||
|
||||
// Sort the keys
|
||||
sort.Slice(sortedChangeOutputs, func(i, j int) bool {
|
||||
return utils.Bip69Less(
|
||||
sortedChangeOutputs[i], sortedChangeOutputs[j],
|
||||
)
|
||||
})
|
||||
|
||||
// Add change outputs orderly.
|
||||
for _, output := range sortedChangeOutputs {
|
||||
batchTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: output.PkScript,
|
||||
Value: output.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
|
||||
}
|
||||
|
|
@ -1433,15 +1550,21 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
|
|||
// known in advance to be non-cooperative (nonCoopHint) and not failed
|
||||
// to sign cooperatively in previous rounds (coopFailed). If any of them
|
||||
// fails, the sweep is excluded from all following rounds and another
|
||||
// round is attempted. Otherwise the cycle completes and we sign the
|
||||
// round is attempted. Otherwise, the cycle completes and we sign the
|
||||
// remaining sweeps non-cooperatively.
|
||||
var (
|
||||
tx *wire.MsgTx
|
||||
weight lntypes.WeightUnit
|
||||
feeForWeight btcutil.Amount
|
||||
fee btcutil.Amount
|
||||
coopInputs int
|
||||
tx *wire.MsgTx
|
||||
weight lntypes.WeightUnit
|
||||
feeForWeight btcutil.Amount
|
||||
fee btcutil.Amount
|
||||
minRelayFeeRate chainfee.SatPerKWeight
|
||||
coopInputs int
|
||||
)
|
||||
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get min relay fee: %w", err),
|
||||
false
|
||||
}
|
||||
for attempt := 1; ; attempt++ {
|
||||
b.Infof("Attempt %d of collecting cooperative signatures.",
|
||||
attempt)
|
||||
|
|
@ -1450,6 +1573,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
|
|||
var err error
|
||||
tx, weight, feeForWeight, fee, err = constructUnsignedTx(
|
||||
sweeps, address, b.currentHeight, b.rbfCache.FeeRate,
|
||||
minRelayFeeRate,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to construct tx: %w", err),
|
||||
|
|
@ -1501,7 +1625,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
|
|||
// If there was any failure of cooperative signing, we need to
|
||||
// update weight estimates (since non-cooperative signing has
|
||||
// larger witness) and hence update the whole transaction and
|
||||
// all the signatures. Otherwise we complete cooperative part.
|
||||
// all the signatures. Otherwise, we complete cooperative part.
|
||||
if !newCoopFailures {
|
||||
break
|
||||
}
|
||||
|
|
@ -1637,7 +1761,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
|
|||
}
|
||||
|
||||
// Publish the transaction.
|
||||
err := b.wallet.PublishTransaction(
|
||||
err = b.wallet.PublishTransaction(
|
||||
ctx, tx, b.cfg.txLabeler(b.id),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -2500,16 +2624,25 @@ func (b *batch) persistSweep(ctx context.Context, sweep sweep,
|
|||
|
||||
// clampBatchFee takes the fee amount and total amount of the sweeps in the
|
||||
// batch and makes sure the fee is not too high. If the fee is too high, it is
|
||||
// clamped to the maximum allowed fee.
|
||||
func clampBatchFee(fee btcutil.Amount,
|
||||
totalAmount btcutil.Amount) btcutil.Amount {
|
||||
// clamped to the maximum allowed fee. If the clamped fee results in a fee rate
|
||||
// below the minimum relay fee, an error is returned.
|
||||
func clampBatchFee(fee btcutil.Amount, totalAmount btcutil.Amount,
|
||||
minRelayFeeRate chainfee.SatPerKWeight,
|
||||
weight lntypes.WeightUnit) (btcutil.Amount, error) {
|
||||
|
||||
maxFeeAmount := btcutil.Amount(float64(totalAmount) *
|
||||
maxFeeToSwapAmtRatio)
|
||||
|
||||
clampedFee := fee
|
||||
if fee > maxFeeAmount {
|
||||
return maxFeeAmount
|
||||
clampedFee = maxFeeAmount
|
||||
}
|
||||
|
||||
return fee
|
||||
clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight)
|
||||
if clampedFeeRate < minRelayFeeRate {
|
||||
return 0, fmt.Errorf("clamped fee rate %v is less than "+
|
||||
"minimum relay fee %v", clampedFeeRate, minRelayFeeRate)
|
||||
}
|
||||
|
||||
return clampedFee, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,39 @@ 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,
|
||||
}
|
||||
|
||||
change1PrimeAddr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" +
|
||||
"lwyyeq5ryn6r"
|
||||
change1PrimeAddress, err := btcutil.DecodeAddress(change1PrimeAddr, nil)
|
||||
require.NoError(t, err)
|
||||
change1PrimePkscript, err := txscript.PayToAddrScript(change1PrimeAddress)
|
||||
require.NoError(t, err)
|
||||
change1Prime := &wire.TxOut{
|
||||
Value: 200_000,
|
||||
PkScript: change1PrimePkscript,
|
||||
}
|
||||
|
||||
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 +107,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 +263,7 @@ func TestConstructUnsignedTx(t *testing.T) {
|
|||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 2400000,
|
||||
Value: 2_400_000,
|
||||
PkScript: batchPkScript,
|
||||
},
|
||||
},
|
||||
|
|
@ -265,7 +305,7 @@ func TestConstructUnsignedTx(t *testing.T) {
|
|||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 2999211,
|
||||
Value: 2_999_211,
|
||||
PkScript: batchPkScript,
|
||||
},
|
||||
},
|
||||
|
|
@ -275,6 +315,311 @@ 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: "identical change pkscripts",
|
||||
sweeps: []sweep{
|
||||
{
|
||||
outpoint: op1,
|
||||
value: 1_000_000,
|
||||
},
|
||||
{
|
||||
outpoint: op2,
|
||||
value: 2_000_000,
|
||||
change: change1,
|
||||
},
|
||||
{
|
||||
outpoint: op3,
|
||||
value: 3_000_000,
|
||||
change: change1,
|
||||
},
|
||||
},
|
||||
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_798_924,
|
||||
PkScript: p2trPkScript,
|
||||
},
|
||||
{
|
||||
Value: 2 * change1.Value,
|
||||
PkScript: change1.PkScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantWeight: 1076,
|
||||
wantFeeForWeight: 1076,
|
||||
wantFee: 1076,
|
||||
},
|
||||
|
||||
{
|
||||
name: "identical change pkscripts different values",
|
||||
sweeps: []sweep{
|
||||
{
|
||||
outpoint: op1,
|
||||
value: 1_000_000,
|
||||
},
|
||||
{
|
||||
outpoint: op2,
|
||||
value: 2_000_000,
|
||||
change: change1,
|
||||
},
|
||||
{
|
||||
outpoint: op3,
|
||||
value: 3_000_000,
|
||||
change: change1Prime,
|
||||
},
|
||||
},
|
||||
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_924,
|
||||
PkScript: p2trPkScript,
|
||||
},
|
||||
{
|
||||
Value: change1.Value + change1Prime.Value,
|
||||
PkScript: change1.PkScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantWeight: 1076,
|
||||
wantFeeForWeight: 1076,
|
||||
wantFee: 1076,
|
||||
},
|
||||
|
||||
{
|
||||
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_000,
|
||||
minRelayFeeRate: 50,
|
||||
wantErr: "batch amount 0.00100294 BTC is < the sum " +
|
||||
"of change outputs 0.00100000 BTC plus fee " +
|
||||
"0.00000058 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 +683,13 @@ func TestConstructUnsignedTx(t *testing.T) {
|
|||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
relayFeeRate := chainfee.FeePerKwFloor
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -727,6 +736,10 @@ func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input,
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to get nextBlockFeeRate: %w", err)
|
||||
}
|
||||
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get minRelayFeeRate: %w", err)
|
||||
}
|
||||
destPkscript, err := txscript.PayToAddrScript(destAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("txscript.PayToAddrScript failed: %w", err)
|
||||
|
|
@ -745,13 +758,16 @@ 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
|
||||
|
||||
return presign(
|
||||
ctx, b.presignedHelper, destAddress, primarySweepID, sweeps,
|
||||
nextBlockFeeRate,
|
||||
nextBlockFeeRate, minRelayFeeRate,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -806,12 +822,18 @@ func (b *Batcher) AddSweep(ctx context.Context, sweepReq *SweepRequest) error {
|
|||
}
|
||||
}
|
||||
|
||||
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get min relay fee: %w", err)
|
||||
}
|
||||
|
||||
// If this is a presigned mode, make sure PresignSweepsGroup was called.
|
||||
// We skip the check for reorg-safely confirmed sweeps, because their
|
||||
// presigned transactions were already cleaned up from the store.
|
||||
if sweep.presigned && !fullyConfirmed {
|
||||
err := ensurePresigned(
|
||||
ctx, sweeps, b.presignedHelper, b.chainParams,
|
||||
ctx, sweeps, b.presignedHelper, minRelayFeeRate,
|
||||
b.chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inputs with primarySweep %v were "+
|
||||
|
|
@ -1548,6 +1570,7 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash,
|
|||
minFeeRate: minFeeRate,
|
||||
nonCoopHint: s.NonCoopHint,
|
||||
presigned: s.IsPresigned,
|
||||
change: s.Change,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
|
|
@ -30,6 +31,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 +50,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 +64,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 +128,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 +226,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 +238,7 @@ func (h *mockPresignedHelper) FetchSweep(_ context.Context,
|
|||
HTLC: swap.Htlc{
|
||||
PkScript: []byte{10, 11, 12},
|
||||
},
|
||||
Change: change,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -273,7 +292,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 +369,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 +432,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)
|
||||
|
||||
|
|
@ -499,7 +518,7 @@ func testPresigned_min_relay_fee(t *testing.T,
|
|||
}()
|
||||
|
||||
// Set high min_relay_fee.
|
||||
lnd.SetMinRelayFee(400)
|
||||
lnd.SetMinRelayFee(252)
|
||||
|
||||
// Create the first sweep.
|
||||
swapHash1 := lntypes.Hash{1, 1, 1}
|
||||
|
|
@ -520,7 +539,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)
|
||||
|
||||
|
|
@ -534,16 +553,16 @@ func testPresigned_min_relay_fee(t *testing.T,
|
|||
// Wait for a transactions to be published.
|
||||
tx := <-lnd.TxPublishChannel
|
||||
gotFeeRate := presignedHelper.getTxFeerate(tx, inputAmt)
|
||||
require.Equal(t, chainfee.SatPerKWeight(402), gotFeeRate)
|
||||
require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
|
||||
|
||||
// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
|
||||
// The only difference of tx2 is a higher lock_time.
|
||||
lnd.SetMinRelayFee(300)
|
||||
lnd.SetMinRelayFee(150)
|
||||
require.NoError(t, lnd.NotifyHeight(601))
|
||||
tx2 := <-lnd.TxPublishChannel
|
||||
require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value)
|
||||
gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
|
||||
require.Equal(t, chainfee.SatPerKWeight(402), gotFeeRate)
|
||||
require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
|
||||
require.Equal(t, uint32(601), tx2.LockTime)
|
||||
|
||||
// Set a higher min_relay_fee, turn off the client and try presigned tx.
|
||||
|
|
@ -554,7 +573,7 @@ func testPresigned_min_relay_fee(t *testing.T,
|
|||
require.NoError(t, lnd.NotifyHeight(602))
|
||||
tx = <-lnd.TxPublishChannel
|
||||
gotFeeRate = presignedHelper.getTxFeerate(tx, inputAmt)
|
||||
require.Equal(t, chainfee.SatPerKWeight(523), gotFeeRate)
|
||||
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
|
||||
// LockTime of a presigned tx is 0.
|
||||
require.Equal(t, uint32(0), tx.LockTime)
|
||||
|
||||
|
|
@ -565,7 +584,7 @@ func testPresigned_min_relay_fee(t *testing.T,
|
|||
tx2 = <-lnd.TxPublishChannel
|
||||
require.Equal(t, tx.TxHash(), tx2.TxHash())
|
||||
gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
|
||||
require.Equal(t, chainfee.SatPerKWeight(523), gotFeeRate)
|
||||
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
|
||||
// LockTime of a presigned tx is 0.
|
||||
require.Equal(t, uint32(0), tx2.LockTime)
|
||||
|
||||
|
|
@ -575,7 +594,7 @@ func testPresigned_min_relay_fee(t *testing.T,
|
|||
tx3 := <-lnd.TxPublishChannel
|
||||
require.Equal(t, tx2.TxOut[0].Value, tx3.TxOut[0].Value)
|
||||
gotFeeRate = presignedHelper.getTxFeerate(tx3, inputAmt)
|
||||
require.Equal(t, chainfee.SatPerKWeight(523), gotFeeRate)
|
||||
require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
|
||||
require.Equal(t, uint32(604), tx3.LockTime)
|
||||
}
|
||||
|
||||
|
|
@ -644,7 +663,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 +689,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 +799,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 +922,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 +1013,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 +1076,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 +1132,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 +1162,528 @@ 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 shouldn't fail.
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add the sweep, triggering the publishing attempt.
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Since a batch was created we check that it registered for its primary
|
||||
// sweep's spend.
|
||||
<-lnd.RegisterSpendChannel
|
||||
|
||||
// Wait for a transactions to be published.
|
||||
tx := <-lnd.TxPublishChannel
|
||||
require.Len(t, tx.TxIn, 2)
|
||||
require.Len(t, tx.TxOut, 2)
|
||||
require.ElementsMatch(
|
||||
t, []wire.OutPoint{op1, op2},
|
||||
[]wire.OutPoint{
|
||||
tx.TxIn[0].PreviousOutPoint,
|
||||
tx.TxIn[1].PreviousOutPoint,
|
||||
},
|
||||
)
|
||||
require.Equal(t, int64(2_493_300), tx.TxOut[0].Value)
|
||||
require.Equal(t, change.Value, tx.TxOut[1].Value)
|
||||
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
|
||||
require.Equal(t, change.PkScript, tx.TxOut[1].PkScript)
|
||||
|
||||
// Mine a blocks to trigger republishing.
|
||||
require.NoError(t, lnd.NotifyHeight(601))
|
||||
}
|
||||
|
||||
// testPresigned_presigned_group_with_identical_change_pkscript tests passing multiple sweeps to
|
||||
// the method PresignSweepsGroup. It tests that a change output of a primary
|
||||
// deposit sweep is properly added to the presigned transaction.
|
||||
func testPresigned_presigned_group_with_identical_change_pkscript(t *testing.T,
|
||||
batcherStore testBatcherStore) {
|
||||
|
||||
defer test.Guard(t)()
|
||||
|
||||
batchPkScript, err := txscript.PayToAddrScript(destAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
||||
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
||||
|
||||
return chainfee.SatPerKWeight(10_000), nil
|
||||
}
|
||||
|
||||
presignedHelper := newMockPresignedHelper()
|
||||
|
||||
batcher := NewBatcher(
|
||||
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
|
||||
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
|
||||
batcherStore, presignedHelper,
|
||||
WithCustomFeeRate(customFeeRate),
|
||||
WithPresignedHelper(presignedHelper),
|
||||
)
|
||||
|
||||
go func() {
|
||||
err := batcher.Run(ctx)
|
||||
checkBatcherError(t, err)
|
||||
}()
|
||||
|
||||
// Create two swaps of a single sweep
|
||||
swapHash1 := lntypes.Hash{1, 1, 1}
|
||||
swapHash2 := lntypes.Hash{2, 2, 2}
|
||||
op1 := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 1},
|
||||
Index: 1,
|
||||
}
|
||||
op2 := wire.OutPoint{
|
||||
Hash: chainhash.Hash{2, 2},
|
||||
Index: 2,
|
||||
}
|
||||
group1 := []Input{
|
||||
{
|
||||
Outpoint: op1,
|
||||
Value: 1_000_000,
|
||||
},
|
||||
}
|
||||
change1 := &wire.TxOut{
|
||||
Value: 500_000,
|
||||
PkScript: []byte{0xaf, 0xfe},
|
||||
}
|
||||
group2 := []Input{
|
||||
{
|
||||
Outpoint: op2,
|
||||
Value: 2_000_000,
|
||||
},
|
||||
}
|
||||
change2 := &wire.TxOut{
|
||||
Value: 600_000,
|
||||
PkScript: []byte{0xaf, 0xfe},
|
||||
}
|
||||
|
||||
presignedHelper.setChangeForPrimaryDeposit(op1, change1)
|
||||
presignedHelper.setChangeForPrimaryDeposit(op2, change2)
|
||||
|
||||
// Enable only one of the sweeps.
|
||||
presignedHelper.SetOutpointOnline(op1, true)
|
||||
presignedHelper.SetOutpointOnline(op2, true)
|
||||
|
||||
// An attempt to presign group1 shouldn't fail.
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change1,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add the sweep, triggering the publishing attempt.
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Since a batch was created we check that it registered for its primary
|
||||
// sweep's spend.
|
||||
<-lnd.RegisterSpendChannel
|
||||
|
||||
// An attempt to presign group2 shouldn't fail.
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group2, sweepTimeout, destAddr, change2,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add the sweep, triggering the publishing attempt.
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash2,
|
||||
Inputs: group2,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for a transactions to be published.
|
||||
tx := <-lnd.TxPublishChannel
|
||||
require.Len(t, tx.TxIn, 2)
|
||||
require.Len(t, tx.TxOut, 2)
|
||||
require.ElementsMatch(
|
||||
t, []wire.OutPoint{op1, op2},
|
||||
[]wire.OutPoint{
|
||||
tx.TxIn[0].PreviousOutPoint,
|
||||
tx.TxIn[1].PreviousOutPoint,
|
||||
},
|
||||
)
|
||||
require.Equal(t, int64(1_893_300), tx.TxOut[0].Value)
|
||||
require.Equal(t, change1.Value+change2.Value, tx.TxOut[1].Value)
|
||||
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
|
||||
require.Equal(t, change1.PkScript, tx.TxOut[1].PkScript)
|
||||
|
||||
// Mine a blocks to trigger republishing.
|
||||
require.NoError(t, lnd.NotifyHeight(601))
|
||||
}
|
||||
|
||||
// testPresigned_presigned_group_with_dust_main_output passes a dust main output
|
||||
// and a change output to PresignSweepsGroup. It will fail because of the dust
|
||||
// main output. Note that the min relay fee is set low enough to pass the
|
||||
// clampBatchFee check, so the error is not related to the fee rate.
|
||||
func testPresigned_presigned_group_with_dust_main_output(t *testing.T,
|
||||
batcherStore testBatcherStore) {
|
||||
|
||||
defer test.Guard(t)()
|
||||
|
||||
batchPkScript, err := txscript.PayToAddrScript(destAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
||||
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
||||
|
||||
return chainfee.SatPerKWeight(100_000), nil
|
||||
}
|
||||
|
||||
presignedHelper := newMockPresignedHelper()
|
||||
|
||||
batcher := NewBatcher(
|
||||
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
|
||||
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
|
||||
batcherStore, presignedHelper,
|
||||
WithCustomFeeRate(customFeeRate),
|
||||
WithPresignedHelper(presignedHelper),
|
||||
)
|
||||
|
||||
go func() {
|
||||
err := batcher.Run(ctx)
|
||||
checkBatcherError(t, err)
|
||||
}()
|
||||
|
||||
// Create a swap of two sweeps.
|
||||
swapHash1 := lntypes.Hash{1, 1, 1}
|
||||
op1 := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 1},
|
||||
Index: 1,
|
||||
}
|
||||
inputValue := int64(1_000_000)
|
||||
group1 := []Input{
|
||||
{
|
||||
Outpoint: op1,
|
||||
Value: 1_000_000,
|
||||
},
|
||||
}
|
||||
dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
|
||||
mainOutput := dustLimit
|
||||
|
||||
// Let's solve an equation of what the fee should be so it is 20%
|
||||
// (clamped fee) of fee (itself) + main output.
|
||||
// fee / (fee+main) = 0.2
|
||||
// fee = 0.2 fee + 0.2 main
|
||||
// 0.8 fee = 0.2 main
|
||||
// fee = 0.25 main
|
||||
clampedFee := mainOutput / 4
|
||||
|
||||
// Set min relay fee low enough to pass the clampBatchFee check.
|
||||
lnd.SetMinRelayFee(166)
|
||||
|
||||
change := &wire.TxOut{
|
||||
// If "+1" is removed, the group would be added successfully.
|
||||
Value: inputValue - dustLimit - clampedFee + 1,
|
||||
PkScript: []byte{0xaf, 0xfe},
|
||||
}
|
||||
|
||||
presignedHelper.setChangeForPrimaryDeposit(op1, change)
|
||||
|
||||
// Enable only one of the sweeps.
|
||||
presignedHelper.SetOutpointOnline(op1, true)
|
||||
|
||||
// An attempt to presign must fail.
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change,
|
||||
)
|
||||
require.EqualError(t, err, "failed to construct unsigned tx for "+
|
||||
"feeRate 166 sat/kw: batch amount 0.01000000 BTC is < the "+
|
||||
"sum of change outputs 0.00999634 BTC plus fee "+
|
||||
"0.00000073 BTC and dust limit 0.00000294 BTC")
|
||||
|
||||
// Add the sweep, triggering the publishing attempt.
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.ErrorContains(t, err, "were not presigned")
|
||||
|
||||
// Now let's verify that we found the edge value correctly.
|
||||
change.Value--
|
||||
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// testPresigned_presigned_group_with_dust_below_relay_fee passes a tx with a
|
||||
// dust main output and one change output to PresignSweepsGroup. The tx fee rate
|
||||
// is below the min relay fee, hence clampBatchFee will return an error.
|
||||
func testPresigned_presigned_group_with_dust_below_relay_fee(t *testing.T,
|
||||
batcherStore testBatcherStore) {
|
||||
|
||||
defer test.Guard(t)()
|
||||
|
||||
batchPkScript, err := txscript.PayToAddrScript(destAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
||||
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
||||
|
||||
return chainfee.SatPerKWeight(100_000), nil
|
||||
}
|
||||
|
||||
presignedHelper := newMockPresignedHelper()
|
||||
|
||||
batcher := NewBatcher(
|
||||
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
|
||||
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
|
||||
batcherStore, presignedHelper,
|
||||
WithCustomFeeRate(customFeeRate),
|
||||
WithPresignedHelper(presignedHelper),
|
||||
)
|
||||
|
||||
go func() {
|
||||
err := batcher.Run(ctx)
|
||||
checkBatcherError(t, err)
|
||||
}()
|
||||
|
||||
// Create a swap of two sweeps.
|
||||
swapHash1 := lntypes.Hash{1, 1, 1}
|
||||
op1 := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 1},
|
||||
Index: 1,
|
||||
}
|
||||
inputValue := int64(1_000_000)
|
||||
group1 := []Input{
|
||||
{
|
||||
Outpoint: op1,
|
||||
Value: 1_000_000,
|
||||
},
|
||||
}
|
||||
dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
|
||||
change := &wire.TxOut{
|
||||
// Note that there is no space for fee.
|
||||
Value: inputValue - dustLimit,
|
||||
PkScript: []byte{0xaf, 0xfe},
|
||||
}
|
||||
|
||||
presignedHelper.setChangeForPrimaryDeposit(op1, change)
|
||||
|
||||
// Enable only one of the sweeps.
|
||||
presignedHelper.SetOutpointOnline(op1, true)
|
||||
|
||||
// An attempt to presign must fail.
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change,
|
||||
)
|
||||
require.EqualError(t, err, "failed to construct unsigned tx for "+
|
||||
"feeRate 253 sat/kw: failed to clamp batch fee: clamped "+
|
||||
"fee rate 132 sat/kw is less than minimum relay fee 253 sat/kw")
|
||||
|
||||
// Add the sweep, triggering the publishing attempt.
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.ErrorContains(t, err, "were not presigned")
|
||||
}
|
||||
|
||||
// testPresigned_presigned_group_with_dust_change tests passing multiple sweeps
|
||||
// to the method PresignSweepsGroup. It tests that a dust change output of a
|
||||
// primary deposit sweep is rejected by PresignSweepsGroup and AddSweep.
|
||||
func testPresigned_presigned_group_with_dust_change(t *testing.T,
|
||||
batcherStore testBatcherStore) {
|
||||
|
||||
defer test.Guard(t)()
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
customFeeRate := func(_ context.Context, _ lntypes.Hash,
|
||||
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
|
||||
|
||||
return chainfee.SatPerKWeight(10_000), nil
|
||||
}
|
||||
|
||||
presignedHelper := newMockPresignedHelper()
|
||||
|
||||
batcher := NewBatcher(
|
||||
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
|
||||
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
|
||||
batcherStore, presignedHelper,
|
||||
WithCustomFeeRate(customFeeRate),
|
||||
WithPresignedHelper(presignedHelper),
|
||||
)
|
||||
|
||||
go func() {
|
||||
err := batcher.Run(ctx)
|
||||
checkBatcherError(t, err)
|
||||
}()
|
||||
|
||||
// Create a swap of two sweeps.
|
||||
swapHash1 := lntypes.Hash{1, 1, 1}
|
||||
op1 := wire.OutPoint{
|
||||
Hash: chainhash.Hash{1, 1},
|
||||
Index: 1,
|
||||
}
|
||||
op2 := wire.OutPoint{
|
||||
Hash: chainhash.Hash{2, 2},
|
||||
Index: 2,
|
||||
}
|
||||
group1 := []Input{
|
||||
{
|
||||
Outpoint: op1,
|
||||
Value: 1_000_000,
|
||||
},
|
||||
{
|
||||
Outpoint: op2,
|
||||
Value: 2_000_000,
|
||||
},
|
||||
}
|
||||
changePkScript := []byte{0xaf, 0xfe}
|
||||
dustLimit := utils.DustLimitForPkScript(changePkScript)
|
||||
change := &wire.TxOut{
|
||||
// If "-1" is removed, the group would be added successfully.
|
||||
Value: int64(dustLimit - 1),
|
||||
PkScript: changePkScript,
|
||||
}
|
||||
|
||||
presignedHelper.setChangeForPrimaryDeposit(op1, change)
|
||||
|
||||
// Enable only one of the sweeps.
|
||||
presignedHelper.SetOutpointOnline(op1, true)
|
||||
presignedHelper.SetOutpointOnline(op2, true)
|
||||
|
||||
// An attempt to presign must fail.
|
||||
err := batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change,
|
||||
)
|
||||
require.EqualError(t, err, "failed to construct unsigned tx for "+
|
||||
"feeRate 253 sat/kw: output 0.00000476 BTC is below dust "+
|
||||
"limit 0.00000477 BTC")
|
||||
|
||||
// Add the sweep, triggering the publishing attempt.
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.ErrorContains(t, err, "were not presigned")
|
||||
|
||||
// Now let's verify that we found the edge value correctly.
|
||||
change.Value++
|
||||
|
||||
err = batcher.PresignSweepsGroup(
|
||||
ctx, group1, sweepTimeout, destAddr, change,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = batcher.AddSweep(ctx, &SweepRequest{
|
||||
SwapHash: swapHash1,
|
||||
Inputs: group1,
|
||||
Notifier: &dummyNotifier,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// wrappedStoreWithPresignedFlag wraps a SweepFetcher store adding IsPresigned
|
||||
// flag to the returned sweeps, taking it from mockPresignedHelper.
|
||||
type wrappedStoreWithPresignedFlag struct {
|
||||
|
|
@ -1304,7 +1853,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 +1948,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 +2069,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 +2133,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 +2348,30 @@ 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("identical change pkscript", func(t *testing.T) {
|
||||
testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
|
||||
})
|
||||
|
||||
t.Run("dust_main_output", func(t *testing.T) {
|
||||
testPresigned_presigned_group_with_dust_main_output(
|
||||
t, NewStoreMock(),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("dust_main_output_below_min_relay_fee", func(t *testing.T) {
|
||||
testPresigned_presigned_group_with_dust_below_relay_fee(
|
||||
t, NewStoreMock(),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("dust_change", func(t *testing.T) {
|
||||
testPresigned_presigned_group_with_dust_change(t, NewStoreMock())
|
||||
})
|
||||
|
||||
t.Run("presigned_and_regular_sweeps", func(t *testing.T) {
|
||||
runTests(t, testPresigned_presigned_and_regular_sweeps)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
15
utils/dust_limit.go
Normal file
15
utils/dust_limit.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/mempool"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
)
|
||||
|
||||
// DustLimitForPkScript returns the dust limit for a given pkScript. An output
|
||||
// must be greater or equal to this value.
|
||||
func DustLimitForPkScript(pkscript []byte) btcutil.Amount {
|
||||
return btcutil.Amount(mempool.GetDustThreshold(&wire.TxOut{
|
||||
PkScript: pkscript,
|
||||
}))
|
||||
}
|
||||
32
utils/dust_limit_test.go
Normal file
32
utils/dust_limit_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type pkScriptGetter func([]byte) ([]byte, error)
|
||||
|
||||
// TestDustLimitForPkScript checks that the dust limit for a given script size
|
||||
// matches the calculation in lnwallet.DustLimitForSize.
|
||||
func TestDustLimitForPkScript(t *testing.T) {
|
||||
getScripts := map[int]pkScriptGetter{
|
||||
input.P2WPKHSize: input.WitnessPubKeyHash,
|
||||
input.P2WSHSize: input.WitnessScriptHash,
|
||||
input.P2SHSize: input.GenerateP2SH,
|
||||
input.P2PKHSize: input.GenerateP2PKH,
|
||||
}
|
||||
|
||||
for scriptSize, getPkScript := range getScripts {
|
||||
pkScript, err := getPkScript([]byte{})
|
||||
require.NoError(t, err, "failed to generate pkScript")
|
||||
|
||||
require.Equal(
|
||||
t, lnwallet.DustLimitForSize(scriptSize),
|
||||
DustLimitForPkScript(pkScript),
|
||||
)
|
||||
}
|
||||
}
|
||||
15
utils/tx_sort.go
Normal file
15
utils/tx_sort.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
)
|
||||
|
||||
// Bip69Less is taken from btcd in btcutil/txsort/txsort.go.
|
||||
func Bip69Less(output1, output2 *wire.TxOut) bool {
|
||||
if output1.Value == output2.Value {
|
||||
return bytes.Compare(output1.PkScript, output2.PkScript) < 0
|
||||
}
|
||||
return output1.Value < output2.Value
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue