lnwallet: add tests for new conf scaling helper funcs
What changed, and why it matters
This commit only adds a new test file (lnwallet/confscale_test.go) with unit and property-based tests for existing confirmation-scaling helper functions. It does not change any production code, introduce new functionality, or modify security-relevant behavior. There is no indication of a security fix or vulnerability being addressed.
No security action required. Treat as routine test-coverage addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff creates lnwallet/confscale_test.go containing Go tests for ScaleNumConfs, FundingConfsForAmounts, and CloseConfsForCapacity. It uses the rapid property-based testing library and stretchr/testify to assert bounds, monotonicity, determinism, and known-value behavior. No implementation code is altered; this is purely test coverage.
Changed components
lnwallet/confscale_test.goInspect captured patch +340 / −0
diff --git a/lnwallet/confscale_test.go b/lnwallet/confscale_test.go
new file mode 100644
index 0000000..53165fc
--- /dev/null
+++ b/lnwallet/confscale_test.go
@@ -0,0 +1,340 @@
+package lnwallet
+
+import (
+ "testing"
+
+ "github.com/btcsuite/btcd/btcutil"
+ "github.com/lightningnetwork/lnd/lnwire"
+ "github.com/stretchr/testify/require"
+ "pgregory.net/rapid"
+)
+
+// TestScaleNumConfsProperties tests various properties that ScaleNumConfs
+// should satisfy using property-based testing.
+func TestScaleNumConfsProperties(t *testing.T) {
+ t.Parallel()
+
+ // The result should always be bounded between the minimum and maximum
+ // number of confirmations regardless of input values.
+ t.Run("bounded_result", func(t *testing.T) {
+ rapid.Check(t, func(t *rapid.T) {
+ // Generate random channel amount and push amount.
+ chanAmt := rapid.Uint64Range(
+ 0, maxChannelSize*10,
+ ).Draw(t, "chanAmt")
+ pushAmtSats := rapid.Uint64Range(
+ 0, chanAmt,
+ ).Draw(t, "pushAmtSats")
+ pushAmt := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmtSats),
+ )
+
+ result := ScaleNumConfs(
+ btcutil.Amount(chanAmt), pushAmt,
+ )
+
+ // Check bounds
+ require.GreaterOrEqual(
+ t, result, uint16(minRequiredConfs),
+ "result should be >= minRequiredConfs",
+ )
+ require.LessOrEqual(
+ t, result, uint16(maxRequiredConfs),
+ "result should be <= maxRequiredConfs",
+ )
+ })
+ })
+
+ // Larger channel amounts and push amounts should require equal or more
+ // confirmations, ensuring the function is monotonically increasing.
+ t.Run("monotonicity", func(t *testing.T) {
+ rapid.Check(t, func(t *rapid.T) {
+ // Generate two channel amounts where amt1 <= amt2.
+ amt1 := rapid.Uint64Range(
+ 0, maxChannelSize,
+ ).Draw(t, "amt1")
+ amt2 := rapid.Uint64Range(
+ amt1, maxChannelSize,
+ ).Draw(t, "amt2")
+
+ // Generate push amounts proportional to channel size.
+ pushAmt1Sats := rapid.Uint64Range(
+ 0, amt1,
+ ).Draw(t, "pushAmt1")
+ pushAmt2Sats := rapid.Uint64Range(
+ pushAmt1Sats, amt2,
+ ).Draw(t, "pushAmt2")
+
+ pushAmt1 := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmt1Sats),
+ )
+ pushAmt2 := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmt2Sats),
+ )
+
+ confs1 := ScaleNumConfs(btcutil.Amount(amt1), pushAmt1)
+ confs2 := ScaleNumConfs(btcutil.Amount(amt2), pushAmt2)
+
+ // Larger or equal stake should require equal or more
+ // confirmations.
+ require.GreaterOrEqual(
+ t, confs2, confs1,
+ "larger amount should require equal or "+
+ "more confirmations",
+ )
+ })
+ })
+
+ // Wumbo channels (those exceeding the max standard channel size) should
+ // always require the maximum number of confirmations for safety.
+ t.Run("wumbo_max_confs", func(t *testing.T) {
+ rapid.Check(t, func(t *rapid.T) {
+ // Generate wumbo channel amount (above maxChannelSize).
+ wumboAmt := rapid.Uint64Range(
+ maxChannelSize+1, maxChannelSize*100,
+ ).Draw(t, "wumboAmt")
+ pushAmtSats := rapid.Uint64Range(
+ 0, wumboAmt,
+ ).Draw(t, "pushAmtSats")
+ pushAmt := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmtSats),
+ )
+
+ result := ScaleNumConfs(
+ btcutil.Amount(wumboAmt), pushAmt,
+ )
+
+ require.Equal(
+ t, uint16(maxRequiredConfs), result,
+ "wumbo channels should always get "+
+ "max confirmations",
+ )
+ })
+ })
+
+ // Zero channel amounts should always result in the minimum number of
+ // confirmations since there's no value at risk.
+ t.Run("zero_gets_min", func(t *testing.T) {
+ result := ScaleNumConfs(0, 0)
+ require.Equal(
+ t, uint16(minRequiredConfs), result,
+ "zero amount should get minimum confirmations",
+ )
+ })
+
+ // The function should be deterministic, always returning the same
+ // output for the same input values.
+ t.Run("determinism", func(t *testing.T) {
+ rapid.Check(t, func(t *rapid.T) {
+ chanAmt := rapid.Uint64Range(
+ 0, maxChannelSize*2,
+ ).Draw(t, "chanAmt")
+ pushAmtSats := rapid.Uint64Range(
+ 0, chanAmt,
+ ).Draw(t, "pushAmtSats")
+ pushAmt := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmtSats),
+ )
+
+ // Call multiple times with same inputs.
+ result1 := ScaleNumConfs(
+ btcutil.Amount(chanAmt), pushAmt,
+ )
+ result2 := ScaleNumConfs(
+ btcutil.Amount(chanAmt), pushAmt,
+ )
+ result3 := ScaleNumConfs(
+ btcutil.Amount(chanAmt), pushAmt,
+ )
+
+ require.Equal(
+ t, result1, result2,
+ "function should be deterministic",
+ )
+ require.Equal(
+ t, result2, result3,
+ "function should be deterministic",
+ )
+ })
+ })
+
+ // Adding a push amount to a channel should require equal or more
+ // confirmations compared to the same channel without a push amount.
+ t.Run("push_amount_effect", func(t *testing.T) {
+ rapid.Check(t, func(t *rapid.T) {
+ // Fix channel amount, vary push amount
+ chanAmt := rapid.Uint64Range(
+ 1, maxChannelSize,
+ ).Draw(t, "chanAmt")
+ pushAmt1Sats := rapid.Uint64Range(
+ 0, chanAmt/2,
+ ).Draw(t, "pushAmt1")
+ pushAmt2Sats := rapid.Uint64Range(
+ pushAmt1Sats, chanAmt,
+ ).Draw(t, "pushAmt2")
+
+ pushAmt1 := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmt1Sats),
+ )
+ pushAmt2 := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmt2Sats),
+ )
+
+ confs1 := ScaleNumConfs(
+ btcutil.Amount(chanAmt), pushAmt1,
+ )
+ confs2 := ScaleNumConfs(
+ btcutil.Amount(chanAmt), pushAmt2,
+ )
+
+ // More push amount should require equal or more
+ // confirmations.
+ require.GreaterOrEqual(
+ t, confs2, confs1,
+ "larger push amount should "+
+ "require equal or more confirmations",
+ )
+ })
+ })
+}
+
+// TestScaleNumConfsKnownValues tests ScaleNumConfs with specific known values
+// to ensure the scaling formula works as expected.
+func TestScaleNumConfsKnownValues(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ chanAmt btcutil.Amount
+ pushAmt lnwire.MilliSatoshi
+ expected uint16
+ }{
+ {
+ name: "zero amounts",
+ chanAmt: 0,
+ pushAmt: 0,
+ expected: minRequiredConfs,
+ },
+ {
+ name: "tiny channel",
+ chanAmt: 1000,
+ pushAmt: 0,
+ expected: minRequiredConfs,
+ },
+ {
+ name: "small channel no push",
+ chanAmt: 100_000,
+ pushAmt: 0,
+ expected: minRequiredConfs,
+ },
+ {
+ name: "half max channel no push",
+ chanAmt: maxChannelSize / 2,
+ pushAmt: 0,
+ expected: 2,
+ },
+ {
+ name: "max channel no push",
+ chanAmt: maxChannelSize,
+ pushAmt: 0,
+ expected: maxRequiredConfs,
+ },
+ {
+ name: "wumbo channel",
+ chanAmt: maxChannelSize * 2,
+ pushAmt: 0,
+ expected: maxRequiredConfs,
+ },
+ {
+ name: "small channel with push",
+ chanAmt: 100_000,
+ pushAmt: lnwire.NewMSatFromSatoshis(50_000),
+ expected: minRequiredConfs,
+ },
+ {
+ name: "medium channel with significant push",
+ chanAmt: maxChannelSize / 4,
+ pushAmt: lnwire.NewMSatFromSatoshis(
+ maxChannelSize / 4,
+ ),
+ expected: 2,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ result := ScaleNumConfs(tc.chanAmt, tc.pushAmt)
+
+ require.Equal(
+ t, tc.expected, result,
+ "chanAmt=%d, pushAmt=%d", tc.chanAmt,
+ tc.pushAmt,
+ )
+ })
+ }
+}
+
+// TestFundingConfsForAmounts verifies that FundingConfsForAmounts is a simple
+// wrapper around ScaleNumConfs.
+func TestFundingConfsForAmounts(t *testing.T) {
+ t.Parallel()
+
+ rapid.Check(t, func(t *rapid.T) {
+ chanAmt := rapid.Uint64Range(
+ 0, maxChannelSize*2,
+ ).Draw(t, "chanAmt")
+ pushAmtSats := rapid.Uint64Range(
+ 0, chanAmt,
+ ).Draw(t, "pushAmtSats")
+ pushAmt := lnwire.NewMSatFromSatoshis(
+ btcutil.Amount(pushAmtSats),
+ )
+
+ // Both functions should return the same result.
+ scaleResult := ScaleNumConfs(btcutil.Amount(chanAmt), pushAmt)
+ fundingResult := FundingConfsForAmounts(
+ btcutil.Amount(chanAmt), pushAmt,
+ )
+
+ require.Equal(
+ t, scaleResult, fundingResult,
+ "FundingConfsForAmounts should return "+
+ "same result as ScaleNumConfs",
+ )
+ })
+}
+
+// TestCloseConfsForCapacity verifies that CloseConfsForCapacity correctly
+// wraps ScaleNumConfs with zero push amount and enforces a minimum of 3
+// confirmations for reorg safety.
+func TestCloseConfsForCapacity(t *testing.T) {
+ t.Parallel()
+
+ rapid.Check(t, func(t *rapid.T) {
+ capacity := rapid.Uint64Range(
+ 0, maxChannelSize*2,
+ ).Draw(t, "capacity")
+
+ // CloseConfsForCapacity should be equivalent to ScaleNumConfs
+ // with 0 push, but with a minimum of 3 confirmations enforced
+ // for reorg safety.
+ closeConfs := CloseConfsForCapacity(btcutil.Amount(capacity))
+ scaleConfs := ScaleNumConfs(btcutil.Amount(capacity), 0)
+
+ // The result should be at least the scaled value, but with a
+ // minimum of 3 confirmations.
+ const minCloseConfs = 3
+ expectedConfs := uint32(scaleConfs)
+ if expectedConfs < minCloseConfs {
+ expectedConfs = minCloseConfs
+ }
+
+ require.Equal(
+ t, expectedConfs, closeConfs,
+ "CloseConfsForCapacity should match "+
+ "ScaleNumConfs with 0 push amount, "+
+ "but with minimum of 3 confs",
+ )
+ })
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.