staticaddr: unit test CalculateWithdrawalTxValues

This commit is contained in:
Slyghtning 2025-12-03 14:10:10 +01:00
parent 880694c8df
commit 780584993b
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF

View file

@ -4,11 +4,19 @@ import (
"context"
"testing"
"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/staticaddr/deposit"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
@ -368,3 +376,233 @@ func TestSignMusig2Tx_MissingOutpointInDepositMap(t *testing.T) {
// Expect an error indicating the missing outpoint.
require.ErrorContains(t, err, "tx outpoint not in deposit index map")
}
// TestCalculateWithdrawalTxValues tests various edge cases in withdrawal
// transaction value calculations.
func TestCalculateWithdrawalTxValues(t *testing.T) {
t.Parallel()
// Create a taproot address for withdrawal.
taprootAddr, err := btcutil.NewAddressTaproot(
make([]byte, 32), &chaincfg.RegressionNetParams,
)
require.NoError(t, err)
// Standard fee rate for testing.
feeRate := chainfee.SatPerKWeight(1000)
// Helper to create deposits.
createDeposit := func(value btcutil.Amount, idx uint32) *deposit.Deposit {
hash := chainhash.Hash{}
hash[0] = byte(idx)
return &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: hash,
Index: idx,
},
Value: value,
}
}
tests := []struct {
name string
deposits []*deposit.Deposit
localAmount btcutil.Amount
feeRate chainfee.SatPerKWeight
withdrawAddr btcutil.Address
commitmentType lnrpc.CommitmentType
expectedErr string
expectDustFee bool // change is dust, given to miners
}{
{
name: "neither address nor commitment type specified",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "either address or commitment type must be specified",
},
{
name: "change is dust - given to miners",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
// Set localAmount such that change after feeWithChange
// would be dust, but change after feeWithoutChange >= 0.
// This triggers case: change-feeWithoutChange >= 0
localAmount: 99300, // Leaves ~700 sats which is dust
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "",
expectDustFee: true,
},
{
name: "insufficient funds after dust and fee",
deposits: []*deposit.Deposit{
createDeposit(1000, 0),
},
localAmount: 900,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "doesn't cover for fees",
},
{
name: "negative change after fees",
deposits: []*deposit.Deposit{
createDeposit(10000, 0),
},
localAmount: 15000,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "doesn't cover for fees",
},
{
name: "min channel size guard - below minimum",
deposits: []*deposit.Deposit{
createDeposit(funding.MinChanFundingSize-10, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
expectedErr: "is lower than the minimum channel " +
"funding size",
},
{
name: "min channel size guard - exactly minimum",
deposits: []*deposit.Deposit{
createDeposit(funding.MinChanFundingSize+1000, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
expectedErr: "",
},
{
name: "withdrawal amount below dust limit",
deposits: []*deposit.Deposit{
createDeposit(400, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "below dust limit",
},
{
name: "change higher than input value",
deposits: []*deposit.Deposit{
createDeposit(10000, 0),
createDeposit(5000, 1),
},
localAmount: 5000,
feeRate: chainfee.SatPerKWeight(100),
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "change amount",
},
{
name: "successful withdrawal with change",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
localAmount: 50000,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "",
},
{
name: "successful withdrawal no change",
deposits: []*deposit.Deposit{
createDeposit(100000, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: taprootAddr,
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
expectedErr: "",
},
{
name: "successful channel open above min size",
deposits: []*deposit.Deposit{
createDeposit(funding.MinChanFundingSize*2, 0),
},
localAmount: 0,
feeRate: feeRate,
withdrawAddr: nil,
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
expectedErr: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
withdrawAmt, changeAmt, err := CalculateWithdrawalTxValues(
tc.deposits, tc.localAmount, tc.feeRate,
tc.withdrawAddr, tc.commitmentType,
)
if tc.expectedErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectedErr)
return
}
require.NoError(t, err)
require.Greater(t, withdrawAmt, btcutil.Amount(0))
require.GreaterOrEqual(t, changeAmt, btcutil.Amount(0))
// Verify that withdrawal amount meets dust threshold.
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
require.GreaterOrEqual(t, withdrawAmt, dustLimit)
// If this is a channel open, verify min channel size.
if tc.commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
require.GreaterOrEqual(
t, withdrawAmt, funding.MinChanFundingSize,
)
}
// If expecting dust to be given to miners, verify
// changeAmt is 0.
if tc.expectDustFee {
require.Equal(t, btcutil.Amount(0), changeAmt,
"change should be 0 when dust is given to miners")
}
// Verify total accounting: inputs = withdrawal + change + fees.
totalInputs := btcutil.Amount(0)
for _, d := range tc.deposits {
totalInputs += d.Value
}
hasChange := changeAmt > 0
weight, err := WithdrawalTxWeight(
len(tc.deposits), tc.withdrawAddr,
tc.commitmentType, hasChange,
)
require.NoError(t, err)
fee := tc.feeRate.FeeForWeight(weight)
// When dust is given to miners, the "fee" includes both
// the transaction fee and the dust amount.
if tc.expectDustFee {
// Total should equal withdrawal + implicit fee (including dust)
implicitFee := totalInputs - withdrawAmt - changeAmt
require.Greater(t, implicitFee, fee,
"implicit fee should be greater than tx fee when dust is given to miners")
} else {
require.Equal(t, totalInputs, withdrawAmt+changeAmt+fee)
}
})
}
}