multi: Add itest for funding timeout

This commit adds an integration test that
verifies the funding timeout behavior in the
funding manager, in dev/integration test.
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
This commit is contained in:
Nishant Bansal 2025-03-01 23:47:00 +05:30
parent 5fe900d18d
commit 49d3fcf5b2
No known key found for this signature in database
GPG key ID: EAE292198F6C5B98
9 changed files with 131 additions and 22 deletions

View file

@ -29,6 +29,7 @@ import (
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/labels"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnutils"
@ -101,11 +102,6 @@ const (
msgBufferSize = 50 msgBufferSize = 50
// MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait
// for the funding transaction to be confirmed before forgetting
// channels that aren't initiated by us. 2016 blocks is ~2 weeks.
MaxWaitNumBlocksFundingConf = 2016
// pendingChansLimit is the maximum number of pending channels that we // pendingChansLimit is the maximum number of pending channels that we
// can have. After this point, pending channel opens will start to be // can have. After this point, pending channel opens will start to be
// rejected. // rejected.
@ -339,6 +335,11 @@ type DevConfig struct {
// remote node's channel ready message once the channel as been marked // remote node's channel ready message once the channel as been marked
// as `channelReadySent`. // as `channelReadySent`.
ProcessChannelReadyWait time.Duration ProcessChannelReadyWait time.Duration
// MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait
// for the funding transaction to be confirmed before forgetting
// channels that aren't initiated by us.
MaxWaitNumBlocksFundingConf uint32
} }
// Config defines the configuration for the FundingManager. All elements // Config defines the configuration for the FundingManager. All elements
@ -3164,9 +3165,20 @@ func (f *Manager) waitForTimeout(completeChan *channeldb.OpenChannel,
defer epochClient.Cancel() defer epochClient.Cancel()
// For the waitBlocksForFundingConf different values are set in case we
// are in a dev environment so enhance test capabilities.
var waitBlocksForFundingConf uint32 = lncfg.
DefaultMaxWaitNumBlocksFundingConf
// Get the waitBlocksForFundingConf. If we are not in development mode,
// this would be DefaultMaxWaitNumBlocksFundingConf.
if lncfg.IsDevBuild() {
waitBlocksForFundingConf = f.cfg.Dev.MaxWaitNumBlocksFundingConf
}
// On block maxHeight we will cancel the funding confirmation wait. // On block maxHeight we will cancel the funding confirmation wait.
broadcastHeight := completeChan.BroadcastHeight() broadcastHeight := completeChan.BroadcastHeight()
maxHeight := broadcastHeight + MaxWaitNumBlocksFundingConf maxHeight := broadcastHeight + waitBlocksForFundingConf
for { for {
select { select {
case epoch, ok := <-epochClient.Epochs: case epoch, ok := <-epochClient.Epochs:
@ -3182,7 +3194,7 @@ func (f *Manager) waitForTimeout(completeChan *channeldb.OpenChannel,
log.Warnf("Waited for %v blocks without "+ log.Warnf("Waited for %v blocks without "+
"seeing funding transaction confirmed,"+ "seeing funding transaction confirmed,"+
" cancelling.", " cancelling.",
MaxWaitNumBlocksFundingConf) waitBlocksForFundingConf)
// Notify the caller of the timeout. // Notify the caller of the timeout.
close(timeoutChan) close(timeoutChan)

View file

@ -2334,14 +2334,15 @@ func TestFundingManagerFundingTimeout(t *testing.T) {
// mine 2016-1, and check that it is still pending. // mine 2016-1, and check that it is still pending.
bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{
Height: fundingBroadcastHeight + Height: fundingBroadcastHeight +
MaxWaitNumBlocksFundingConf - 1, lncfg.DefaultMaxWaitNumBlocksFundingConf - 1,
} }
// Bob should still be waiting for the channel to open. // Bob should still be waiting for the channel to open.
assertNumPendingChannelsRemains(t, bob, 1) assertNumPendingChannelsRemains(t, bob, 1)
bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{
Height: fundingBroadcastHeight + MaxWaitNumBlocksFundingConf, Height: fundingBroadcastHeight +
lncfg.DefaultMaxWaitNumBlocksFundingConf,
} }
// Bob should have sent an Error message to Alice. // Bob should have sent an Error message to Alice.
@ -2387,16 +2388,16 @@ func TestFundingManagerFundingNotTimeoutInitiator(t *testing.T) {
t.Fatalf("alice did not publish funding tx") t.Fatalf("alice did not publish funding tx")
} }
// Increase the height to 1 minus the MaxWaitNumBlocksFundingConf // Increase the height to 1 minus the DefaultMaxWaitNumBlocksFundingConf
// height. // height.
alice.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ alice.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{
Height: fundingBroadcastHeight + Height: fundingBroadcastHeight +
MaxWaitNumBlocksFundingConf - 1, lncfg.DefaultMaxWaitNumBlocksFundingConf - 1,
} }
bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{
Height: fundingBroadcastHeight + Height: fundingBroadcastHeight +
MaxWaitNumBlocksFundingConf - 1, lncfg.DefaultMaxWaitNumBlocksFundingConf - 1,
} }
// Assert both and Alice and Bob still have 1 pending channels. // Assert both and Alice and Bob still have 1 pending channels.
@ -2404,13 +2405,16 @@ func TestFundingManagerFundingNotTimeoutInitiator(t *testing.T) {
assertNumPendingChannelsRemains(t, bob, 1) assertNumPendingChannelsRemains(t, bob, 1)
// Increase both Alice and Bob to MaxWaitNumBlocksFundingConf height. // Increase both Alice and Bob to DefaultMaxWaitNumBlocksFundingConf
// height.
alice.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ alice.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{
Height: fundingBroadcastHeight + MaxWaitNumBlocksFundingConf, Height: fundingBroadcastHeight +
lncfg.DefaultMaxWaitNumBlocksFundingConf,
} }
bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{
Height: fundingBroadcastHeight + MaxWaitNumBlocksFundingConf, Height: fundingBroadcastHeight +
lncfg.DefaultMaxWaitNumBlocksFundingConf,
} }
// Since Alice was the initiator, the channel should not have timed out. // Since Alice was the initiator, the channel should not have timed out.

View file

@ -670,6 +670,10 @@ var allTestCases = []*lntest.TestCase{
Name: "fee replacement", Name: "fee replacement",
TestFunc: testFeeReplacement, TestFunc: testFeeReplacement,
}, },
{
Name: "funding manager funding timeout",
TestFunc: testFundingManagerFundingTimeout,
},
} }
// appendPrefixed is used to add a prefix to each test name in the subtests // appendPrefixed is used to add a prefix to each test name in the subtests

View file

@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest"
@ -844,7 +845,7 @@ func testFundingExpiryBlocksOnPending(ht *lntest.HarnessTest) {
// blocks and verify the value of FundingExpiryBlock at each step. // blocks and verify the value of FundingExpiryBlock at each step.
const numEmptyBlocks = 3 const numEmptyBlocks = 3
for i := int32(0); i < numEmptyBlocks; i++ { for i := int32(0); i < numEmptyBlocks; i++ {
expectedVal := funding.MaxWaitNumBlocksFundingConf - i expectedVal := lncfg.DefaultMaxWaitNumBlocksFundingConf - i
pending := ht.AssertNumPendingOpenChannels(alice, 1) pending := ht.AssertNumPendingOpenChannels(alice, 1)
require.Equal(ht, expectedVal, pending[0].FundingExpiryBlocks) require.Equal(ht, expectedVal, pending[0].FundingExpiryBlocks)
pending = ht.AssertNumPendingOpenChannels(bob, 1) pending = ht.AssertNumPendingOpenChannels(bob, 1)
@ -967,3 +968,55 @@ func testOpenChannelLockedBalance(ht *lntest.HarnessTest) {
// Finally, we check to make sure the balance is unlocked again. // Finally, we check to make sure the balance is unlocked again.
ht.AssertWalletLockedBalance(alice, 0) ht.AssertWalletLockedBalance(alice, 0)
} }
// testFundingManagerFundingTimeout tests that after an OpenChannel, and before
// the funding transaction is confirmed, if a user is not the channel initiator,
// the channel is forgotten after waitBlocksForFundingConf.
func testFundingManagerFundingTimeout(ht *lntest.HarnessTest) {
// Set the maximum wait blocks for funding confirmation.
waitBlocksForFundingConf := 10
// Create nodes for testing, ensuring Alice has sufficient initial
// funds.
alice := ht.NewNodeWithCoins("Alice", nil)
bob := ht.NewNode("Bob", nil)
// Restart Bob with the custom configuration for funding confirmation
// timeout.
ht.RestartNodeWithExtraArgs(bob, []string{
"--dev.maxwaitnumblocksfundingconf=10",
})
// Ensure Alice and Bob are connected.
ht.EnsureConnected(alice, bob)
// Open the channel between Alice and Bob. This runs through the process
// up until the funding transaction is broadcasted.
ht.OpenChannelAssertPending(alice, bob, lntest.OpenChannelParams{
Amt: 500000,
PushAmt: 0,
})
// At this point, both nodes have a pending channel waiting for the
// funding transaction to be confirmed.
ht.AssertNumPendingOpenChannels(alice, 1)
ht.AssertNumPendingOpenChannels(bob, 1)
// We expect Bob to forget the channel after waitBlocksForFundingConf
// blocks, so mine waitBlocksForFundingConf-1, and check that it is
// still pending.
ht.MineEmptyBlocks(waitBlocksForFundingConf - 1)
ht.AssertNumPendingOpenChannels(bob, 1)
// Now mine one additional block to reach waitBlocksForFundingConf.
ht.MineEmptyBlocks(1)
// Bob should now have forgotten the channel.
ht.AssertNumPendingOpenChannels(bob, 0)
// Since Alice was the initiator, her pending channel should remain.
ht.AssertNumPendingOpenChannels(alice, 1)
// Cleanup the mempool by mining blocks.
ht.MineBlocksAndAssertNumTxes(6, 1)
}

View file

@ -72,6 +72,11 @@ const (
// DefaultZombieSweeperInterval is the default time interval at which // DefaultZombieSweeperInterval is the default time interval at which
// unfinished (zombiestate) open channel flows are purged from memory. // unfinished (zombiestate) open channel flows are purged from memory.
DefaultZombieSweeperInterval = 1 * time.Minute DefaultZombieSweeperInterval = 1 * time.Minute
// DefaultMaxWaitNumBlocksFundingConf is the maximum number of blocks to
// wait for the funding transaction to confirm before forgetting
// channels that aren't initiated by us. 2016 blocks is ~2 weeks.
DefaultMaxWaitNumBlocksFundingConf = 2016
) )
// CleanAndExpandPath expands environment variables and leading ~ in the // CleanAndExpandPath expands environment variables and leading ~ in the

View file

@ -46,3 +46,9 @@ func (d *DevConfig) GetReservationTimeout() time.Duration {
func (d *DevConfig) GetZombieSweeperInterval() time.Duration { func (d *DevConfig) GetZombieSweeperInterval() time.Duration {
return DefaultZombieSweeperInterval return DefaultZombieSweeperInterval
} }
// GetMaxWaitNumBlocksFundingConf returns the config value for
// `MaxWaitNumBlocksFundingConf`.
func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 {
return DefaultMaxWaitNumBlocksFundingConf
}

View file

@ -21,10 +21,11 @@ func IsDevBuild() bool {
// //
//nolint:ll //nolint:ll
type DevConfig struct { type DevConfig struct {
ProcessChannelReadyWait time.Duration `long:"processchannelreadywait" description:"Time to sleep before processing remote node's channel_ready message."` ProcessChannelReadyWait time.Duration `long:"processchannelreadywait" description:"Time to sleep before processing remote node's channel_ready message."`
ReservationTimeout time.Duration `long:"reservationtimeout" description:"The maximum time we keep a pending channel open flow in memory."` ReservationTimeout time.Duration `long:"reservationtimeout" description:"The maximum time we keep a pending channel open flow in memory."`
ZombieSweeperInterval time.Duration `long:"zombiesweeperinterval" description:"The time interval at which channel opening flows are evaluated for zombie status."` ZombieSweeperInterval time.Duration `long:"zombiesweeperinterval" description:"The time interval at which channel opening flows are evaluated for zombie status."`
UnsafeDisconnect bool `long:"unsafedisconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels."` UnsafeDisconnect bool `long:"unsafedisconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels."`
MaxWaitNumBlocksFundingConf uint32 `long:"maxwaitnumblocksfundingconf" description:"Maximum blocks to wait for funding confirmation before discarding non-initiated channels."`
} }
// ChannelReadyWait returns the config value `ProcessChannelReadyWait`. // ChannelReadyWait returns the config value `ProcessChannelReadyWait`.
@ -54,3 +55,13 @@ func (d *DevConfig) GetZombieSweeperInterval() time.Duration {
func (d *DevConfig) GetUnsafeDisconnect() bool { func (d *DevConfig) GetUnsafeDisconnect() bool {
return d.UnsafeDisconnect return d.UnsafeDisconnect
} }
// GetMaxWaitNumBlocksFundingConf returns the config value for
// `MaxWaitNumBlocksFundingConf`.
func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 {
if d.MaxWaitNumBlocksFundingConf == 0 {
return DefaultMaxWaitNumBlocksFundingConf
}
return d.MaxWaitNumBlocksFundingConf
}

View file

@ -3850,9 +3850,21 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) {
commitBaseWeight := blockchain.GetTransactionWeight(utx) commitBaseWeight := blockchain.GetTransactionWeight(utx)
commitWeight := commitBaseWeight + witnessWeight commitWeight := commitBaseWeight + witnessWeight
// For the waitBlocksForFundingConf different values are set in
// case we are in dev environment so enhance test capabilities.
var waitBlocksForFundingConf uint32 = lncfg.
DefaultMaxWaitNumBlocksFundingConf
// Get the waitBlocksForFundingConf. If we are not in
// development mode, this would be nil.
if lncfg.IsDevBuild() {
waitBlocksForFundingConf = r.cfg.Dev.
GetMaxWaitNumBlocksFundingConf()
}
// FundingExpiryBlocks is the distance from the current block // FundingExpiryBlocks is the distance from the current block
// height to the broadcast height + MaxWaitNumBlocksFundingConf. // height to the broadcast height + waitBlocksForFundingConf.
maxFundingHeight := funding.MaxWaitNumBlocksFundingConf + maxFundingHeight := waitBlocksForFundingConf +
pendingChan.BroadcastHeight() pendingChan.BroadcastHeight()
fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight

View file

@ -1449,6 +1449,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
if lncfg.IsDevBuild() { if lncfg.IsDevBuild() {
devCfg = &funding.DevConfig{ devCfg = &funding.DevConfig{
ProcessChannelReadyWait: cfg.Dev.ChannelReadyWait(), ProcessChannelReadyWait: cfg.Dev.ChannelReadyWait(),
MaxWaitNumBlocksFundingConf: cfg.Dev.
GetMaxWaitNumBlocksFundingConf(),
} }
reservationTimeout = cfg.Dev.GetReservationTimeout() reservationTimeout = cfg.Dev.GetReservationTimeout()