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", ) }) }