Skip to content

Commit 896ae94

Browse files
committed
staticaddr: unit test CalculateWithdrawalTxValues
1 parent e5b2c17 commit 896ae94

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed

staticaddr/withdraw/manager_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import (
44
"context"
55
"testing"
66

7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/btcsuite/btcd/chaincfg"
9+
"github.com/btcsuite/btcd/chaincfg/chainhash"
710
"github.com/btcsuite/btcd/txscript"
811
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightninglabs/loop/staticaddr/deposit"
913
"github.com/lightninglabs/loop/swapserverrpc"
1014
"github.com/lightninglabs/loop/test"
15+
"github.com/lightningnetwork/lnd/funding"
1116
"github.com/lightningnetwork/lnd/input"
17+
"github.com/lightningnetwork/lnd/lnrpc"
18+
"github.com/lightningnetwork/lnd/lnwallet"
19+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1220
"github.com/stretchr/testify/require"
1321
)
1422

@@ -368,3 +376,233 @@ func TestSignMusig2Tx_MissingOutpointInDepositMap(t *testing.T) {
368376
// Expect an error indicating the missing outpoint.
369377
require.ErrorContains(t, err, "tx outpoint not in deposit index map")
370378
}
379+
380+
// TestCalculateWithdrawalTxValues tests various edge cases in withdrawal
381+
// transaction value calculations.
382+
func TestCalculateWithdrawalTxValues(t *testing.T) {
383+
t.Parallel()
384+
385+
// Create a taproot address for withdrawal.
386+
taprootAddr, err := btcutil.NewAddressTaproot(
387+
make([]byte, 32), &chaincfg.RegressionNetParams,
388+
)
389+
require.NoError(t, err)
390+
391+
// Standard fee rate for testing.
392+
feeRate := chainfee.SatPerKWeight(1000)
393+
394+
// Helper to create deposits.
395+
createDeposit := func(value btcutil.Amount, idx uint32) *deposit.Deposit {
396+
hash := chainhash.Hash{}
397+
hash[0] = byte(idx)
398+
return &deposit.Deposit{
399+
OutPoint: wire.OutPoint{
400+
Hash: hash,
401+
Index: idx,
402+
},
403+
Value: value,
404+
}
405+
}
406+
407+
tests := []struct {
408+
name string
409+
deposits []*deposit.Deposit
410+
localAmount btcutil.Amount
411+
feeRate chainfee.SatPerKWeight
412+
withdrawAddr btcutil.Address
413+
commitmentType lnrpc.CommitmentType
414+
expectedErr string
415+
expectDustFee bool // change is dust, given to miners
416+
}{
417+
{
418+
name: "neither address nor commitment type specified",
419+
deposits: []*deposit.Deposit{
420+
createDeposit(100000, 0),
421+
},
422+
localAmount: 0,
423+
feeRate: feeRate,
424+
withdrawAddr: nil,
425+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
426+
expectedErr: "either address or commitment type must be specified",
427+
},
428+
{
429+
name: "change is dust - given to miners",
430+
deposits: []*deposit.Deposit{
431+
createDeposit(100000, 0),
432+
},
433+
// Set localAmount such that change after feeWithChange
434+
// would be dust, but change after feeWithoutChange >= 0.
435+
// This triggers case: change-feeWithoutChange >= 0
436+
localAmount: 99300, // Leaves ~700 sats which is dust
437+
feeRate: feeRate,
438+
withdrawAddr: taprootAddr,
439+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
440+
expectedErr: "",
441+
expectDustFee: true,
442+
},
443+
{
444+
name: "insufficient funds after dust and fee",
445+
deposits: []*deposit.Deposit{
446+
createDeposit(1000, 0),
447+
},
448+
localAmount: 900,
449+
feeRate: feeRate,
450+
withdrawAddr: taprootAddr,
451+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
452+
expectedErr: "doesn't cover for fees",
453+
},
454+
{
455+
name: "negative change after fees",
456+
deposits: []*deposit.Deposit{
457+
createDeposit(10000, 0),
458+
},
459+
localAmount: 15000,
460+
feeRate: feeRate,
461+
withdrawAddr: taprootAddr,
462+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
463+
expectedErr: "doesn't cover for fees",
464+
},
465+
{
466+
name: "min channel size guard - below minimum",
467+
deposits: []*deposit.Deposit{
468+
createDeposit(funding.MinChanFundingSize-10, 0),
469+
},
470+
localAmount: 0,
471+
feeRate: feeRate,
472+
withdrawAddr: nil,
473+
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
474+
expectedErr: "is lower than the minimum channel " +
475+
"funding size",
476+
},
477+
{
478+
name: "min channel size guard - exactly minimum",
479+
deposits: []*deposit.Deposit{
480+
createDeposit(funding.MinChanFundingSize+1000, 0),
481+
},
482+
localAmount: 0,
483+
feeRate: feeRate,
484+
withdrawAddr: nil,
485+
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
486+
expectedErr: "",
487+
},
488+
{
489+
name: "withdrawal amount below dust limit",
490+
deposits: []*deposit.Deposit{
491+
createDeposit(400, 0),
492+
},
493+
localAmount: 0,
494+
feeRate: feeRate,
495+
withdrawAddr: taprootAddr,
496+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
497+
expectedErr: "below dust limit",
498+
},
499+
{
500+
name: "change higher than input value",
501+
deposits: []*deposit.Deposit{
502+
createDeposit(10000, 0),
503+
createDeposit(5000, 1),
504+
},
505+
localAmount: 5000,
506+
feeRate: chainfee.SatPerKWeight(100),
507+
withdrawAddr: taprootAddr,
508+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
509+
expectedErr: "change amount",
510+
},
511+
{
512+
name: "successful withdrawal with change",
513+
deposits: []*deposit.Deposit{
514+
createDeposit(100000, 0),
515+
},
516+
localAmount: 50000,
517+
feeRate: feeRate,
518+
withdrawAddr: taprootAddr,
519+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
520+
expectedErr: "",
521+
},
522+
{
523+
name: "successful withdrawal no change",
524+
deposits: []*deposit.Deposit{
525+
createDeposit(100000, 0),
526+
},
527+
localAmount: 0,
528+
feeRate: feeRate,
529+
withdrawAddr: taprootAddr,
530+
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
531+
expectedErr: "",
532+
},
533+
{
534+
name: "successful channel open above min size",
535+
deposits: []*deposit.Deposit{
536+
createDeposit(funding.MinChanFundingSize*2, 0),
537+
},
538+
localAmount: 0,
539+
feeRate: feeRate,
540+
withdrawAddr: nil,
541+
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
542+
expectedErr: "",
543+
},
544+
}
545+
546+
for _, tc := range tests {
547+
t.Run(tc.name, func(t *testing.T) {
548+
withdrawAmt, changeAmt, err := CalculateWithdrawalTxValues(
549+
tc.deposits, tc.localAmount, tc.feeRate,
550+
tc.withdrawAddr, tc.commitmentType,
551+
)
552+
553+
if tc.expectedErr != "" {
554+
require.Error(t, err)
555+
require.ErrorContains(t, err, tc.expectedErr)
556+
return
557+
}
558+
559+
require.NoError(t, err)
560+
require.Greater(t, withdrawAmt, btcutil.Amount(0))
561+
require.GreaterOrEqual(t, changeAmt, btcutil.Amount(0))
562+
563+
// Verify that withdrawal amount meets dust threshold.
564+
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
565+
require.GreaterOrEqual(t, withdrawAmt, dustLimit)
566+
567+
// If this is a channel open, verify min channel size.
568+
if tc.commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
569+
require.GreaterOrEqual(
570+
t, withdrawAmt, funding.MinChanFundingSize,
571+
)
572+
}
573+
574+
// If expecting dust to be given to miners, verify
575+
// changeAmt is 0.
576+
if tc.expectDustFee {
577+
require.Equal(t, btcutil.Amount(0), changeAmt,
578+
"change should be 0 when dust is given to miners")
579+
}
580+
581+
// Verify total accounting: inputs = withdrawal + change + fees.
582+
totalInputs := btcutil.Amount(0)
583+
for _, d := range tc.deposits {
584+
totalInputs += d.Value
585+
}
586+
587+
hasChange := changeAmt > 0
588+
weight, err := WithdrawalTxWeight(
589+
len(tc.deposits), tc.withdrawAddr,
590+
tc.commitmentType, hasChange,
591+
)
592+
require.NoError(t, err)
593+
594+
fee := tc.feeRate.FeeForWeight(weight)
595+
596+
// When dust is given to miners, the "fee" includes both
597+
// the transaction fee and the dust amount.
598+
if tc.expectDustFee {
599+
// Total should equal withdrawal + implicit fee (including dust)
600+
implicitFee := totalInputs - withdrawAmt - changeAmt
601+
require.Greater(t, implicitFee, fee,
602+
"implicit fee should be greater than tx fee when dust is given to miners")
603+
} else {
604+
require.Equal(t, totalInputs, withdrawAmt+changeAmt+fee)
605+
}
606+
})
607+
}
608+
}

0 commit comments

Comments
 (0)