diff --git a/funding/manager.go b/funding/manager.go index 395cccb2a..6e81c6f8b 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -29,6 +29,7 @@ import ( "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/labels" + "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnutils" @@ -101,11 +102,6 @@ const ( 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 // can have. After this point, pending channel opens will start to be // rejected. @@ -339,6 +335,11 @@ type DevConfig struct { // remote node's channel ready message once the channel as been marked // as `channelReadySent`. 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 @@ -3164,9 +3165,21 @@ func (f *Manager) waitForTimeout(completeChan *channeldb.OpenChannel, defer epochClient.Cancel() + // The value of waitBlocksForFundingConf is adjusted in a development + // environment to enhance test capabilities. Otherwise, it is set to + // DefaultMaxWaitNumBlocksFundingConf. + waitBlocksForFundingConf := uint32( + lncfg.DefaultMaxWaitNumBlocksFundingConf, + ) + + if lncfg.IsDevBuild() { + waitBlocksForFundingConf = + f.cfg.Dev.MaxWaitNumBlocksFundingConf + } + // On block maxHeight we will cancel the funding confirmation wait. broadcastHeight := completeChan.BroadcastHeight() - maxHeight := broadcastHeight + MaxWaitNumBlocksFundingConf + maxHeight := broadcastHeight + waitBlocksForFundingConf for { select { case epoch, ok := <-epochClient.Epochs: @@ -3182,7 +3195,7 @@ func (f *Manager) waitForTimeout(completeChan *channeldb.OpenChannel, log.Warnf("Waited for %v blocks without "+ "seeing funding transaction confirmed,"+ " cancelling.", - MaxWaitNumBlocksFundingConf) + waitBlocksForFundingConf) // Notify the caller of the timeout. close(timeoutChan) diff --git a/funding/manager_test.go b/funding/manager_test.go index b6130176d..ad4a11b49 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -2334,14 +2334,15 @@ func TestFundingManagerFundingTimeout(t *testing.T) { // mine 2016-1, and check that it is still pending. bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ Height: fundingBroadcastHeight + - MaxWaitNumBlocksFundingConf - 1, + lncfg.DefaultMaxWaitNumBlocksFundingConf - 1, } // Bob should still be waiting for the channel to open. assertNumPendingChannelsRemains(t, bob, 1) bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ - Height: fundingBroadcastHeight + MaxWaitNumBlocksFundingConf, + Height: fundingBroadcastHeight + + lncfg.DefaultMaxWaitNumBlocksFundingConf, } // 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") } - // Increase the height to 1 minus the MaxWaitNumBlocksFundingConf + // Increase the height to 1 minus the DefaultMaxWaitNumBlocksFundingConf // height. alice.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ Height: fundingBroadcastHeight + - MaxWaitNumBlocksFundingConf - 1, + lncfg.DefaultMaxWaitNumBlocksFundingConf - 1, } bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ Height: fundingBroadcastHeight + - MaxWaitNumBlocksFundingConf - 1, + lncfg.DefaultMaxWaitNumBlocksFundingConf - 1, } // Assert both and Alice and Bob still have 1 pending channels. @@ -2404,13 +2405,16 @@ func TestFundingManagerFundingNotTimeoutInitiator(t *testing.T) { 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{ - Height: fundingBroadcastHeight + MaxWaitNumBlocksFundingConf, + Height: fundingBroadcastHeight + + lncfg.DefaultMaxWaitNumBlocksFundingConf, } bob.mockNotifier.epochChan <- &chainntnfs.BlockEpoch{ - Height: fundingBroadcastHeight + MaxWaitNumBlocksFundingConf, + Height: fundingBroadcastHeight + + lncfg.DefaultMaxWaitNumBlocksFundingConf, } // Since Alice was the initiator, the channel should not have timed out. diff --git a/itest/list_on_test.go b/itest/list_on_test.go index a8be6634c..78e242b1a 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -670,6 +670,10 @@ var allTestCases = []*lntest.TestCase{ Name: "fee replacement", TestFunc: testFeeReplacement, }, + { + Name: "funding manager funding timeout", + TestFunc: testFundingManagerFundingTimeout, + }, } // appendPrefixed is used to add a prefix to each test name in the subtests diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index 70260fab5..e46acd682 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/funding" + "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" @@ -844,7 +845,7 @@ func testFundingExpiryBlocksOnPending(ht *lntest.HarnessTest) { // blocks and verify the value of FundingExpiryBlock at each step. const numEmptyBlocks = 3 for i := int32(0); i < numEmptyBlocks; i++ { - expectedVal := funding.MaxWaitNumBlocksFundingConf - i + expectedVal := lncfg.DefaultMaxWaitNumBlocksFundingConf - i pending := ht.AssertNumPendingOpenChannels(alice, 1) require.Equal(ht, expectedVal, pending[0].FundingExpiryBlocks) 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. ht.AssertWalletLockedBalance(alice, 0) } + +// testFundingManagerFundingTimeout tests that after an OpenChannel, and before +// the funding transaction is confirmed, if the node 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) +} diff --git a/lncfg/config.go b/lncfg/config.go index 889b327f6..94ca02f46 100644 --- a/lncfg/config.go +++ b/lncfg/config.go @@ -72,6 +72,11 @@ const ( // DefaultZombieSweeperInterval is the default time interval at which // unfinished (zombiestate) open channel flows are purged from memory. 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 diff --git a/lncfg/dev.go b/lncfg/dev.go index 0c887457f..6b2471024 100644 --- a/lncfg/dev.go +++ b/lncfg/dev.go @@ -46,3 +46,9 @@ func (d *DevConfig) GetReservationTimeout() time.Duration { func (d *DevConfig) GetZombieSweeperInterval() time.Duration { return DefaultZombieSweeperInterval } + +// GetMaxWaitNumBlocksFundingConf returns the config value for +// `MaxWaitNumBlocksFundingConf`. +func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 { + return DefaultMaxWaitNumBlocksFundingConf +} diff --git a/lncfg/dev_integration.go b/lncfg/dev_integration.go index bbbc08037..c6467af2b 100644 --- a/lncfg/dev_integration.go +++ b/lncfg/dev_integration.go @@ -21,10 +21,11 @@ func IsDevBuild() bool { // //nolint:ll type DevConfig struct { - 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."` - 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."` + 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."` + 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."` + MaxWaitNumBlocksFundingConf uint32 `long:"maxwaitnumblocksfundingconf" description:"Maximum blocks to wait for funding confirmation before discarding non-initiated channels."` } // ChannelReadyWait returns the config value `ProcessChannelReadyWait`. @@ -54,3 +55,13 @@ func (d *DevConfig) GetZombieSweeperInterval() time.Duration { func (d *DevConfig) GetUnsafeDisconnect() bool { return d.UnsafeDisconnect } + +// GetMaxWaitNumBlocksFundingConf returns the config value for +// `MaxWaitNumBlocksFundingConf`. +func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 { + if d.MaxWaitNumBlocksFundingConf == 0 { + return DefaultMaxWaitNumBlocksFundingConf + } + + return d.MaxWaitNumBlocksFundingConf +} diff --git a/rpcserver.go b/rpcserver.go index 7236a5dad..ad8806059 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3850,9 +3850,21 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) { commitBaseWeight := blockchain.GetTransactionWeight(utx) commitWeight := commitBaseWeight + witnessWeight + // The value of waitBlocksForFundingConf is adjusted in a + // development environment to enhance test capabilities. + // Otherwise, it is set to DefaultMaxWaitNumBlocksFundingConf. + waitBlocksForFundingConf := uint32( + lncfg.DefaultMaxWaitNumBlocksFundingConf, + ) + + if lncfg.IsDevBuild() { + waitBlocksForFundingConf = + r.cfg.Dev.GetMaxWaitNumBlocksFundingConf() + } + // FundingExpiryBlocks is the distance from the current block - // height to the broadcast height + MaxWaitNumBlocksFundingConf. - maxFundingHeight := funding.MaxWaitNumBlocksFundingConf + + // height to the broadcast height + waitBlocksForFundingConf. + maxFundingHeight := waitBlocksForFundingConf + pendingChan.BroadcastHeight() fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight diff --git a/server.go b/server.go index ecb3eceae..01019583f 100644 --- a/server.go +++ b/server.go @@ -1449,6 +1449,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, if lncfg.IsDevBuild() { devCfg = &funding.DevConfig{ ProcessChannelReadyWait: cfg.Dev.ChannelReadyWait(), + MaxWaitNumBlocksFundingConf: cfg.Dev. + GetMaxWaitNumBlocksFundingConf(), } reservationTimeout = cfg.Dev.GetReservationTimeout()