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/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,20 @@ func (f *Manager) waitForTimeout(completeChan *channeldb.OpenChannel,
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.
broadcastHeight := completeChan.BroadcastHeight()
maxHeight := broadcastHeight + MaxWaitNumBlocksFundingConf
maxHeight := broadcastHeight + waitBlocksForFundingConf
for {
select {
case epoch, ok := <-epochClient.Epochs:
@ -3182,7 +3194,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)

View file

@ -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.

View file

@ -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

View file

@ -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 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
// 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

View file

@ -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
}

View file

@ -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
}

View file

@ -3850,9 +3850,21 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) {
commitBaseWeight := blockchain.GetTransactionWeight(utx)
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
// height to the broadcast height + MaxWaitNumBlocksFundingConf.
maxFundingHeight := funding.MaxWaitNumBlocksFundingConf +
// height to the broadcast height + waitBlocksForFundingConf.
maxFundingHeight := waitBlocksForFundingConf +
pendingChan.BroadcastHeight()
fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight

View file

@ -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()