diff --git a/config.go b/config.go index eca50485..cc981792 100644 --- a/config.go +++ b/config.go @@ -240,6 +240,7 @@ type Config struct { Alias string `long:"alias" description:"The node alias. Used as a moniker by peers and intelligence services"` Color string `long:"color" description:"The color of the node in hex format (i.e. '#3399FF'). Used to customize node appearance in intelligence services"` MinChanSize int64 `long:"minchansize" description:"The smallest channel size (in satoshis) that we should accept. Incoming channels smaller than this will be rejected"` + MaxChanSize int64 `long:"maxchansize" description:"The largest channel size (in satoshis) that we should accept. Incoming channels larger than this will be rejected"` DefaultRemoteMaxHtlcs uint16 `long:"default-remote-max-htlcs" description:"The default max_htlc applied when opening or accepting channels. This value limits the number of concurrent HTLCs that the remote party can add to the commitment. The maximum possible value is 483."` @@ -387,6 +388,7 @@ func DefaultConfig() Config { Alias: defaultAlias, Color: defaultColor, MinChanSize: int64(minChanFundingSize), + MaxChanSize: int64(0), DefaultRemoteMaxHtlcs: defaultRemoteMaxHtlcs, NumGraphSyncPeers: defaultMinPeers, HistoricalSyncInterval: discovery.DefaultHistoricalSyncInterval, @@ -610,7 +612,7 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) { } // Ensure that the specified values for the min and max channel size - // don't are within the bounds of the normal chan size constraints. + // are within the bounds of the normal chan size constraints. if cfg.Autopilot.MinChannelSize < int64(minChanFundingSize) { cfg.Autopilot.MinChannelSize = int64(minChanFundingSize) } @@ -622,6 +624,38 @@ func ValidateConfig(cfg Config, usageMessage string) (*Config, error) { return nil, err } + // Ensure that --maxchansize is properly handled when set by user. + // For non-Wumbo channels this limit remains 16777215 satoshis by default + // as specified in BOLT-02. For wumbo channels this limit is 1,000,000,000. + // satoshis (10 BTC). Always enforce --maxchansize explicitly set by user. + // If unset (marked by 0 value), then enforce proper default. + if cfg.MaxChanSize == 0 { + if cfg.ProtocolOptions.Wumbo() { + cfg.MaxChanSize = int64(MaxBtcFundingAmountWumbo) + } else { + cfg.MaxChanSize = int64(MaxBtcFundingAmount) + } + } + + // Ensure that the user specified values for the min and max channel + // size make sense. + if cfg.MaxChanSize < cfg.MinChanSize { + return nil, fmt.Errorf("invalid channel size parameters: "+ + "max channel size %v, must be no less than min chan size %v", + cfg.MaxChanSize, cfg.MinChanSize, + ) + } + + // Don't allow superflous --maxchansize greater than + // BOLT 02 soft-limit for non-wumbo channel + if !cfg.ProtocolOptions.Wumbo() && cfg.MaxChanSize > int64(MaxFundingAmount) { + return nil, fmt.Errorf("invalid channel size parameters: "+ + "maximum channel size %v is greater than maximum non-wumbo"+ + " channel size %v", + cfg.MaxChanSize, MaxFundingAmount, + ) + } + // Ensure a valid max channel fee allocation was set. if cfg.MaxChannelFeeAllocation <= 0 || cfg.MaxChannelFeeAllocation > 1 { return nil, fmt.Errorf("invalid max channel fee allocation: "+ diff --git a/fundingmanager.go b/fundingmanager.go index 262d3f98..a9276f1c 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -67,6 +67,11 @@ const ( // in the real world. MaxBtcFundingAmount = btcutil.Amount(1<<24) - 1 + // MaxBtcFundingAmountWumbo is a soft-limit on the maximum size of wumbo + // channels. This limit is 10 BTC and is the only thing standing between + // you and limitless channel size (apart from 21 million cap) + MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000) + // maxLtcFundingAmount is a soft-limit of the maximum channel size // currently accepted on the Litecoin chain within the Lightning // Protocol. @@ -360,6 +365,11 @@ type fundingConfig struct { // due to fees. MinChanSize btcutil.Amount + // MaxChanSize is the largest channel size that we'll accept as an + // inbound channel. We have such a parameter, so that you may decide how + // WUMBO you would like your channel. + MaxChanSize btcutil.Amount + // MaxPendingChannels is the maximum number of pending channels we // allow for each peer. MaxPendingChannels int @@ -1269,13 +1279,11 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { return } - // We'll reject any request to create a channel that's above the - // current soft-limit for channel size, but only if we're rejecting all - // wumbo channel initiations. - if f.cfg.NoWumboChans && msg.FundingAmount > MaxFundingAmount { + // Ensure that the remote party respects our maximum channel size. + if amt > f.cfg.MaxChanSize { f.failFundingFlow( fmsg.peer, fmsg.msg.PendingChannelID, - lnwire.ErrChanTooLarge, + lnwallet.ErrChanTooLarge(amt, f.cfg.MaxChanSize), ) return } diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 59faf448..41173203 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -432,6 +432,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, }, ZombieSweeperInterval: 1 * time.Hour, ReservationTimeout: 1 * time.Nanosecond, + MaxChanSize: MaxFundingAmount, MaxPendingChannels: lncfg.DefaultMaxPendingChannels, NotifyOpenChannelEvent: evt.NotifyOpenChannelEvent, OpenChannelPredicate: chainedAcceptor, @@ -3212,6 +3213,75 @@ func expectOpenChannelMsg(t *testing.T, msgChan chan lnwire.Message) *lnwire.Ope return openChannelReq } +func TestMaxChannelSizeConfig(t *testing.T) { + t.Parallel() + + // Create a set of funding managers that will reject wumbo + // channels but set --maxchansize explicitly lower than soft-limit. + // Verify that wumbo rejecting funding managers will respect --maxchansize + // below 16777215 satoshi (MaxFundingAmount) limit. + alice, bob := setupFundingManagers(t, func(cfg *fundingConfig) { + cfg.NoWumboChans = true + cfg.MaxChanSize = MaxFundingAmount - 1 + }) + + // Attempt to create a channel above the limit + // imposed by --maxchansize, which should be rejected. + updateChan := make(chan *lnrpc.OpenStatusUpdate) + errChan := make(chan error, 1) + initReq := &openChanReq{ + targetPubkey: bob.privKey.PubKey(), + chainHash: *fundingNetParams.GenesisHash, + localFundingAmt: MaxFundingAmount, + pushAmt: lnwire.NewMSatFromSatoshis(0), + private: false, + updates: updateChan, + err: errChan, + } + + // After processing the funding open message, bob should respond with + // an error rejecting the channel that exceeds size limit. + alice.fundingMgr.initFundingWorkflow(bob, initReq) + openChanMsg := expectOpenChannelMsg(t, alice.msgChan) + bob.fundingMgr.processFundingOpen(openChanMsg, alice) + assertErrorSent(t, bob.msgChan) + + // Create a set of funding managers that will reject wumbo + // channels but set --maxchansize explicitly higher than soft-limit + // A --maxchansize greater than this limit should have no effect. + tearDownFundingManagers(t, alice, bob) + alice, bob = setupFundingManagers(t, func(cfg *fundingConfig) { + cfg.NoWumboChans = true + cfg.MaxChanSize = MaxFundingAmount + 1 + }) + + // We expect Bob to respond with an Accept channel message. + alice.fundingMgr.initFundingWorkflow(bob, initReq) + openChanMsg = expectOpenChannelMsg(t, alice.msgChan) + bob.fundingMgr.processFundingOpen(openChanMsg, alice) + assertFundingMsgSent(t, bob.msgChan, "AcceptChannel") + + // Verify that wumbo accepting funding managers will respect --maxchansize + // Create the funding managers, this time allowing + // wumbo channels but setting --maxchansize explicitly. + tearDownFundingManagers(t, alice, bob) + alice, bob = setupFundingManagers(t, func(cfg *fundingConfig) { + cfg.NoWumboChans = false + cfg.MaxChanSize = btcutil.Amount(100000000) + }) + + // Attempt to create a channel above the limit + // imposed by --maxchansize, which should be rejected. + initReq.localFundingAmt = btcutil.SatoshiPerBitcoin + 1 + + // After processing the funding open message, bob should respond with + // an error rejecting the channel that exceeds size limit. + alice.fundingMgr.initFundingWorkflow(bob, initReq) + openChanMsg = expectOpenChannelMsg(t, alice.msgChan) + bob.fundingMgr.processFundingOpen(openChanMsg, alice) + assertErrorSent(t, bob.msgChan) +} + // TestWumboChannelConfig tests that the funding manager will respect the wumbo // channel config param when creating or accepting new channels. func TestWumboChannelConfig(t *testing.T) { @@ -3260,6 +3330,7 @@ func TestWumboChannelConfig(t *testing.T) { tearDownFundingManagers(t, alice, bob) alice, bob = setupFundingManagers(t, func(cfg *fundingConfig) { cfg.NoWumboChans = false + cfg.MaxChanSize = MaxBtcFundingAmountWumbo }) // We should now be able to initiate a wumbo channel funding w/o any diff --git a/lntest/itest/lnd_max_channel_size_test.go b/lntest/itest/lnd_max_channel_size_test.go new file mode 100644 index 00000000..9d0745b9 --- /dev/null +++ b/lntest/itest/lnd_max_channel_size_test.go @@ -0,0 +1,120 @@ +// +build rpctest + +package itest + +import ( + "context" + "fmt" + "strings" + + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/lntest" +) + +// testMaxChannelSize tests that lnd handles --maxchansize parameter +// correctly. Wumbo nodes should enforce a default soft limit of 10 BTC by +// default. This limit can be adjusted with --maxchansize config option +func testMaxChannelSize(net *lntest.NetworkHarness, t *harnessTest) { + // We'll make two new nodes, both wumbo but with the default + // limit on maximum channel size (10 BTC) + wumboNode, err := net.NewNode( + "wumbo", []string{"--protocol.wumbo-channels"}, + ) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, wumboNode) + + wumboNode2, err := net.NewNode( + "wumbo2", []string{"--protocol.wumbo-channels"}, + ) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, wumboNode2) + + // We'll send 11 BTC to the wumbo node so it can test the wumbo soft limit. + ctxb := context.Background() + err = net.SendCoins(ctxb, 11*btcutil.SatoshiPerBitcoin, wumboNode) + if err != nil { + t.Fatalf("unable to send coins to wumbo node: %v", err) + } + + // Next we'll connect both nodes, then attempt to make a wumbo channel + // funding request, which should fail as it exceeds the default wumbo + // soft limit of 10 BTC. + err = net.EnsureConnected(ctxb, wumboNode, wumboNode2) + if err != nil { + t.Fatalf("unable to connect peers: %v", err) + } + + chanAmt := lnd.MaxBtcFundingAmountWumbo + 1 + _, err = net.OpenChannel( + ctxb, wumboNode, wumboNode2, lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + if err == nil { + t.Fatalf("expected channel funding to fail as it exceeds 10 BTC limit") + } + + // The test should show failure due to the channel exceeding our max size. + if !strings.Contains(err.Error(), "exceeds maximum chan size") { + t.Fatalf("channel should be rejected due to size, instead "+ + "error was: %v", err) + } + + // Next we'll create a non-wumbo node to verify that it enforces the + // BOLT-02 channel size limit and rejects our funding request. + miniNode, err := net.NewNode("mini", nil) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, miniNode) + + err = net.EnsureConnected(ctxb, wumboNode, miniNode) + if err != nil { + t.Fatalf("unable to connect peers: %v", err) + } + + _, err = net.OpenChannel( + ctxb, wumboNode, miniNode, lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + if err == nil { + t.Fatalf("expected channel funding to fail as it exceeds 0.16 BTC limit") + } + + // The test should show failure due to the channel exceeding our max size. + if !strings.Contains(err.Error(), "exceeds maximum chan size") { + t.Fatalf("channel should be rejected due to size, instead "+ + "error was: %v", err) + } + + // We'll now make another wumbo node with appropriate maximum channel size + // to accept our wumbo channel funding. + wumboNode3, err := net.NewNode( + "wumbo3", []string{"--protocol.wumbo-channels", + fmt.Sprintf("--maxchansize=%v", int64(lnd.MaxBtcFundingAmountWumbo+1))}, + ) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, wumboNode3) + + // Creating a wumbo channel between these two nodes should succeed. + err = net.EnsureConnected(ctxb, wumboNode, wumboNode3) + if err != nil { + t.Fatalf("unable to connect peers: %v", err) + } + chanPoint := openChannelAndAssert( + ctxb, t, net, wumboNode, wumboNode3, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + closeChannelAndAssert(ctxb, t, net, wumboNode, chanPoint, false) + +} diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 94dbf293..b5a532a8 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -14374,6 +14374,10 @@ var testsCases = []*testCase{ name: "wumbo channels", test: testWumboChannels, }, + { + name: "maximum channel size", + test: testMaxChannelSize, + }, } // TestLightningNetworkDaemon performs a series of integration tests amongst a diff --git a/lntest/itest/lnd_wumbo_channels_test.go b/lntest/itest/lnd_wumbo_channels_test.go index 31f379b8..6084b596 100644 --- a/lntest/itest/lnd_wumbo_channels_test.go +++ b/lntest/itest/lnd_wumbo_channels_test.go @@ -62,7 +62,7 @@ func testWumboChannels(net *lntest.NetworkHarness, t *harnessTest) { // The test should indicate a failure due to the channel being too // large. - if !strings.Contains(err.Error(), "channel too large") { + if !strings.Contains(err.Error(), "exceeds maximum chan size") { t.Fatalf("channel should be rejected due to size, instead "+ "error was: %v", err) } diff --git a/lntest/itest/log_error_whitelist.txt b/lntest/itest/log_error_whitelist.txt index 831f49c2..86717c7e 100644 --- a/lntest/itest/log_error_whitelist.txt +++ b/lntest/itest/log_error_whitelist.txt @@ -203,6 +203,15 @@