mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-06 04:59:34 +02:00
multi: add itest to check race between channel_ready and update_add_htlc
This commit adds a new itest case to check the race condition found in issue #7401. In order to control the funding manager's state, a new dev config for the funding manager is introduced to specify a duration we should hold before processing remote node's channel_ready message. A new development config, `DevConfig` is introduced in `lncfg` and will only have effect if built with flag `integration`. This can also be extended for future integration tests if more dev-only flags are needed.
This commit is contained in:
parent
6b41289538
commit
0dd2aa0aef
@ -490,6 +490,10 @@ type Config struct {
|
|||||||
|
|
||||||
// Estimator is used to estimate routing probabilities.
|
// Estimator is used to estimate routing probabilities.
|
||||||
Estimator routing.Estimator
|
Estimator routing.Estimator
|
||||||
|
|
||||||
|
// Dev specifies configs used for integration tests, which is always
|
||||||
|
// empty if not built with `integration` flag.
|
||||||
|
Dev *lncfg.DevConfig `group:"dev" namespace:"dev"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRPCConfig holds the configuration options for the gRPC server.
|
// GRPCConfig holds the configuration options for the gRPC server.
|
||||||
|
@ -337,10 +337,22 @@ func newSerializedKey(pubKey *btcec.PublicKey) serializedPubKey {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DevConfig specifies configs used for integration test only.
|
||||||
|
type DevConfig struct {
|
||||||
|
// ProcessChannelReadyWait is the duration to sleep before processing
|
||||||
|
// remote node's channel ready message once the channel as been marked
|
||||||
|
// as `channelReadySent`.
|
||||||
|
ProcessChannelReadyWait time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// Config defines the configuration for the FundingManager. All elements
|
// Config defines the configuration for the FundingManager. All elements
|
||||||
// within the configuration MUST be non-nil for the FundingManager to carry out
|
// within the configuration MUST be non-nil for the FundingManager to carry out
|
||||||
// its duties.
|
// its duties.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Dev specifies config values used in integration test. For
|
||||||
|
// production, this config will always be an empty struct.
|
||||||
|
Dev *DevConfig
|
||||||
|
|
||||||
// NoWumboChans indicates if we're to reject all incoming wumbo channel
|
// NoWumboChans indicates if we're to reject all incoming wumbo channel
|
||||||
// requests, and also reject all outgoing wumbo channel requests.
|
// requests, and also reject all outgoing wumbo channel requests.
|
||||||
NoWumboChans bool
|
NoWumboChans bool
|
||||||
@ -3523,6 +3535,24 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer,
|
|||||||
msg *lnwire.ChannelReady) {
|
msg *lnwire.ChannelReady) {
|
||||||
|
|
||||||
defer f.wg.Done()
|
defer f.wg.Done()
|
||||||
|
|
||||||
|
// If we are in development mode, we'll wait for specified duration
|
||||||
|
// before processing the channel ready message.
|
||||||
|
if f.cfg.Dev != nil {
|
||||||
|
duration := f.cfg.Dev.ProcessChannelReadyWait
|
||||||
|
log.Warnf("Channel(%v): sleeping %v before processing "+
|
||||||
|
"channel_ready", msg.ChanID, duration)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(duration):
|
||||||
|
log.Warnf("Channel(%v): slept %v before processing "+
|
||||||
|
"channel_ready", msg.ChanID, duration)
|
||||||
|
case <-f.quit:
|
||||||
|
log.Warnf("Channel(%v): quit sleeping", msg.ChanID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("Received ChannelReady for ChannelID(%v) from "+
|
log.Debugf("Received ChannelReady for ChannelID(%v) from "+
|
||||||
"peer %x", msg.ChanID,
|
"peer %x", msg.ChanID,
|
||||||
peer.IdentityKey().SerializeCompressed())
|
peer.IdentityKey().SerializeCompressed())
|
||||||
|
@ -546,4 +546,8 @@ var allTestCases = []*lntest.TestCase{
|
|||||||
Name: "utxo selection funding",
|
Name: "utxo selection funding",
|
||||||
TestFunc: testChannelUtxoSelection,
|
TestFunc: testChannelUtxoSelection,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "update pending open channels",
|
||||||
|
TestFunc: testUpdateOnPendingOpenChannels,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chainreg"
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
"github.com/lightningnetwork/lnd/funding"
|
"github.com/lightningnetwork/lnd/funding"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
"github.com/lightningnetwork/lnd/lntest/node"
|
"github.com/lightningnetwork/lnd/lntest/node"
|
||||||
"github.com/lightningnetwork/lnd/lntest/rpc"
|
"github.com/lightningnetwork/lnd/lntest/rpc"
|
||||||
@ -488,6 +489,194 @@ func runBasicChannelCreationAndUpdates(ht *lntest.HarnessTest,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testUpdateOnPendingOpenChannels checks that `update_add_htlc` followed by
|
||||||
|
// `channel_ready` is properly handled. In specific, when a node is in a state
|
||||||
|
// that it's still processing a remote `channel_ready` message, meanwhile an
|
||||||
|
// `update_add_htlc` is received, this HTLC message is cached and settled once
|
||||||
|
// processing `channel_ready` is complete.
|
||||||
|
func testUpdateOnPendingOpenChannels(ht *lntest.HarnessTest) {
|
||||||
|
// Test funder's behavior. Funder sees the channel pending, but fundee
|
||||||
|
// sees it active and sends an HTLC.
|
||||||
|
ht.Run("pending on funder side", func(t *testing.T) {
|
||||||
|
st := ht.Subtest(t)
|
||||||
|
testUpdateOnFunderPendingOpenChannels(st)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test fundee's behavior. Fundee sees the channel pending, but funder
|
||||||
|
// sees it active and sends an HTLC.
|
||||||
|
ht.Run("pending on fundee side", func(t *testing.T) {
|
||||||
|
st := ht.Subtest(t)
|
||||||
|
testUpdateOnFundeePendingOpenChannels(st)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// testUpdateOnFunderPendingOpenChannels checks that when the fundee sends an
|
||||||
|
// `update_add_htlc` followed by `channel_ready` while the funder is still
|
||||||
|
// processing the fundee's `channel_ready`, the HTLC will be cached and
|
||||||
|
// eventually settled.
|
||||||
|
func testUpdateOnFunderPendingOpenChannels(ht *lntest.HarnessTest) {
|
||||||
|
// Grab the channel participants.
|
||||||
|
alice, bob := ht.Alice, ht.Bob
|
||||||
|
|
||||||
|
// Restart Alice with the config so she won't process Bob's
|
||||||
|
// channel_ready msg immediately.
|
||||||
|
ht.RestartNodeWithExtraArgs(alice, []string{
|
||||||
|
"--dev.processchannelreadywait=10s",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure Alice and Bob are connected.
|
||||||
|
ht.EnsureConnected(alice, bob)
|
||||||
|
|
||||||
|
// Create a new channel that requires 1 confs before it's considered
|
||||||
|
// open.
|
||||||
|
params := lntest.OpenChannelParams{
|
||||||
|
Amt: funding.MaxBtcFundingAmount,
|
||||||
|
PushAmt: funding.MaxBtcFundingAmount / 2,
|
||||||
|
}
|
||||||
|
pendingChan := ht.OpenChannelAssertPending(alice, bob, params)
|
||||||
|
chanPoint := &lnrpc.ChannelPoint{
|
||||||
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||||
|
FundingTxidBytes: pendingChan.Txid,
|
||||||
|
},
|
||||||
|
OutputIndex: pendingChan.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice and Bob should both consider the channel pending open.
|
||||||
|
ht.AssertNumPendingOpenChannels(alice, 1)
|
||||||
|
ht.AssertNumPendingOpenChannels(bob, 1)
|
||||||
|
|
||||||
|
// Mine one block to confirm the funding transaction.
|
||||||
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||||
|
|
||||||
|
// TODO(yy): we've prematurely marked the channel as open before
|
||||||
|
// processing channel ready messages. We need to mark it as open after
|
||||||
|
// we've processed channel ready messages and change the check to,
|
||||||
|
// ht.AssertNumPendingOpenChannels(alice, 1)
|
||||||
|
ht.AssertNumPendingOpenChannels(alice, 0)
|
||||||
|
|
||||||
|
// Bob will consider the channel open as there's no wait time to send
|
||||||
|
// and receive Alice's channel_ready message.
|
||||||
|
ht.AssertNumPendingOpenChannels(bob, 0)
|
||||||
|
|
||||||
|
// Alice and Bob now have different view of the channel. For Bob,
|
||||||
|
// since the channel_ready messages are processed, he will have a
|
||||||
|
// working link to route HTLCs. For Alice, because she hasn't handled
|
||||||
|
// Bob's channel_ready, there's no active link yet.
|
||||||
|
//
|
||||||
|
// Alice now adds an invoice.
|
||||||
|
req := &lnrpc.Invoice{
|
||||||
|
RPreimage: ht.Random32Bytes(),
|
||||||
|
Value: 10_000,
|
||||||
|
}
|
||||||
|
invoice := alice.RPC.AddInvoice(req)
|
||||||
|
|
||||||
|
// Bob sends an `update_add_htlc`, which would result in this message
|
||||||
|
// being cached in Alice's `peer.Brontide` and the payment will stay
|
||||||
|
// in-flight instead of being failed by Alice.
|
||||||
|
bobReq := &routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: invoice.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
}
|
||||||
|
bobStream := bob.RPC.SendPayment(bobReq)
|
||||||
|
ht.AssertPaymentStatusFromStream(bobStream, lnrpc.Payment_IN_FLIGHT)
|
||||||
|
|
||||||
|
// Wait until Alice finishes processing Bob's channel_ready.
|
||||||
|
//
|
||||||
|
// NOTE: no effect before fixing the above TODO.
|
||||||
|
ht.AssertNumPendingOpenChannels(alice, 0)
|
||||||
|
|
||||||
|
// Once Alice sees the channel as active, she will process the cached
|
||||||
|
// premature `update_add_htlc` and settles the payment.
|
||||||
|
ht.AssertPaymentStatusFromStream(bobStream, lnrpc.Payment_SUCCEEDED)
|
||||||
|
|
||||||
|
// Close the channel.
|
||||||
|
ht.CloseChannel(alice, chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testUpdateOnFundeePendingOpenChannels checks that when the funder sends an
|
||||||
|
// `update_add_htlc` followed by `channel_ready` while the fundee is still
|
||||||
|
// processing the funder's `channel_ready`, the HTLC will be cached and
|
||||||
|
// eventually settled.
|
||||||
|
func testUpdateOnFundeePendingOpenChannels(ht *lntest.HarnessTest) {
|
||||||
|
// Grab the channel participants.
|
||||||
|
alice, bob := ht.Alice, ht.Bob
|
||||||
|
|
||||||
|
// Restart Bob with the config so he won't process Alice's
|
||||||
|
// channel_ready msg immediately.
|
||||||
|
ht.RestartNodeWithExtraArgs(bob, []string{
|
||||||
|
"--dev.processchannelreadywait=10s",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure Alice and Bob are connected.
|
||||||
|
ht.EnsureConnected(alice, bob)
|
||||||
|
|
||||||
|
// Create a new channel that requires 1 confs before it's considered
|
||||||
|
// open.
|
||||||
|
params := lntest.OpenChannelParams{
|
||||||
|
Amt: funding.MaxBtcFundingAmount,
|
||||||
|
}
|
||||||
|
pendingChan := ht.OpenChannelAssertPending(alice, bob, params)
|
||||||
|
chanPoint := &lnrpc.ChannelPoint{
|
||||||
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||||
|
FundingTxidBytes: pendingChan.Txid,
|
||||||
|
},
|
||||||
|
OutputIndex: pendingChan.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice and Bob should both consider the channel pending open.
|
||||||
|
ht.AssertNumPendingOpenChannels(alice, 1)
|
||||||
|
ht.AssertNumPendingOpenChannels(bob, 1)
|
||||||
|
|
||||||
|
// Mine one block to confirm the funding transaction.
|
||||||
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||||
|
|
||||||
|
// Alice will consider the channel open as there's no wait time to send
|
||||||
|
// and receive Bob's channel_ready message.
|
||||||
|
ht.AssertNumPendingOpenChannels(alice, 0)
|
||||||
|
|
||||||
|
// TODO(yy): we've prematurely marked the channel as open before
|
||||||
|
// processing channel ready messages. We need to mark it as open after
|
||||||
|
// we've processed channel ready messages and change the check to,
|
||||||
|
// ht.AssertNumPendingOpenChannels(bob, 1)
|
||||||
|
ht.AssertNumPendingOpenChannels(bob, 0)
|
||||||
|
|
||||||
|
// Alice and Bob now have different view of the channel. For Alice,
|
||||||
|
// since the channel_ready messages are processed, she will have a
|
||||||
|
// working link to route HTLCs. For Bob, because he hasn't handled
|
||||||
|
// Alice's channel_ready, there's no active link yet.
|
||||||
|
//
|
||||||
|
// Bob now adds an invoice.
|
||||||
|
req := &lnrpc.Invoice{
|
||||||
|
RPreimage: ht.Random32Bytes(),
|
||||||
|
Value: 10_000,
|
||||||
|
}
|
||||||
|
bobInvoice := bob.RPC.AddInvoice(req)
|
||||||
|
|
||||||
|
// Alice sends an `update_add_htlc`, which would result in this message
|
||||||
|
// being cached in Bob's `peer.Brontide` and the payment will stay
|
||||||
|
// in-flight instead of being failed by Bob.
|
||||||
|
aliceReq := &routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: bobInvoice.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
}
|
||||||
|
aliceStream := alice.RPC.SendPayment(aliceReq)
|
||||||
|
ht.AssertPaymentStatusFromStream(aliceStream, lnrpc.Payment_IN_FLIGHT)
|
||||||
|
|
||||||
|
// Wait until Bob finishes processing Alice's channel_ready.
|
||||||
|
//
|
||||||
|
// NOTE: no effect before fixing the above TODO.
|
||||||
|
ht.AssertNumPendingOpenChannels(bob, 0)
|
||||||
|
|
||||||
|
// Once Bob sees the channel as active, he will process the cached
|
||||||
|
// premature `update_add_htlc` and settles the payment.
|
||||||
|
ht.AssertPaymentStatusFromStream(aliceStream, lnrpc.Payment_SUCCEEDED)
|
||||||
|
|
||||||
|
// Close the channel.
|
||||||
|
ht.CloseChannel(alice, chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
// verifyCloseUpdate is used to verify that a closed channel update is of the
|
// verifyCloseUpdate is used to verify that a closed channel update is of the
|
||||||
// expected type.
|
// expected type.
|
||||||
func verifyCloseUpdate(chanUpdate *lnrpc.ChannelEventUpdate,
|
func verifyCloseUpdate(chanUpdate *lnrpc.ChannelEventUpdate,
|
||||||
|
23
lncfg/dev.go
Normal file
23
lncfg/dev.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//go:build !integration
|
||||||
|
|
||||||
|
package lncfg
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// IsDevBuild returns a bool to indicate whether we are in a development
|
||||||
|
// environment.
|
||||||
|
//
|
||||||
|
// NOTE: always return false here.
|
||||||
|
func IsDevBuild() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevConfig specifies development configs used for production. This struct
|
||||||
|
// should always remain empty.
|
||||||
|
type DevConfig struct{}
|
||||||
|
|
||||||
|
// ChannelReadyWait returns the config value, which is always 0 for production
|
||||||
|
// build.
|
||||||
|
func (d *DevConfig) ChannelReadyWait() time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
26
lncfg/dev_integration.go
Normal file
26
lncfg/dev_integration.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package lncfg
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// IsDevBuild returns a bool to indicate whether we are in a development
|
||||||
|
// environment.
|
||||||
|
//
|
||||||
|
// NOTE: always return true here.
|
||||||
|
func IsDevBuild() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevConfig specifies configs used for integration tests. These configs can
|
||||||
|
// only be used in tests and must NOT be exported for production usage.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
|
type DevConfig struct {
|
||||||
|
ProcessChannelReadyWait time.Duration `long:"processchannelreadywait" description:"Time to sleep before processing remote node's channel_ready message."`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelReadyWait returns the config value `ProcessChannelReadyWait`.
|
||||||
|
func (d *DevConfig) ChannelReadyWait() time.Duration {
|
||||||
|
return d.ProcessChannelReadyWait
|
||||||
|
}
|
10
server.go
10
server.go
@ -1276,8 +1276,18 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
|
|||||||
return ourPolicy, err
|
return ourPolicy, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the development config for funding manager. If we are not in
|
||||||
|
// development mode, this would be nil.
|
||||||
|
var devCfg *funding.DevConfig
|
||||||
|
if lncfg.IsDevBuild() {
|
||||||
|
devCfg = &funding.DevConfig{
|
||||||
|
ProcessChannelReadyWait: cfg.Dev.ChannelReadyWait(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
s.fundingMgr, err = funding.NewFundingManager(funding.Config{
|
s.fundingMgr, err = funding.NewFundingManager(funding.Config{
|
||||||
|
Dev: devCfg,
|
||||||
NoWumboChans: !cfg.ProtocolOptions.Wumbo(),
|
NoWumboChans: !cfg.ProtocolOptions.Wumbo(),
|
||||||
IDKey: nodeKeyDesc.PubKey,
|
IDKey: nodeKeyDesc.PubKey,
|
||||||
IDKeyLoc: nodeKeyDesc.KeyLocator,
|
IDKeyLoc: nodeKeyDesc.KeyLocator,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user