Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions sei-tendermint/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ func (cfg *Config) ValidateBasic() error {
if err := cfg.RPC.ValidateBasic(); err != nil {
return fmt.Errorf("error in [rpc] section: %w", err)
}
if err := cfg.P2P.ValidateBasic(); err != nil {
return fmt.Errorf("error in [p2p] section: %w", err)
}
if err := cfg.Mempool.ValidateBasic(); err != nil {
return fmt.Errorf("error in [mempool] section: %w", err)
}
Expand Down Expand Up @@ -711,6 +714,10 @@ type P2PConfig struct {
// How often node should dial a new peer.
DialInterval time.Duration `mapstructure:"dial-interval"`

// How often node should accept a new inbound connection. A value of 0 disables
// the limiter.
AcceptInterval time.Duration `mapstructure:"accept-interval"`

// Testing params.
// Force dial to fail
TestDialFail bool `mapstructure:"test-dial-fail"`
Expand Down Expand Up @@ -741,6 +748,7 @@ func DefaultP2PConfig() *P2PConfig {
HandshakeTimeout: 10 * time.Second,
DialTimeout: 3 * time.Second,
DialInterval: 10 * time.Second,
AcceptInterval: 10 * time.Millisecond,
TestDialFail: false,
QueueType: "simple-priority",
}
Expand All @@ -761,6 +769,12 @@ func (cfg *P2PConfig) ValidateBasic() error {
if cfg.RecvRate < 0 {
return errors.New("recv-rate can't be negative")
}
if cfg.DialInterval < 0 {
return errors.New("dial-interval can't be negative")
}
if cfg.AcceptInterval < 0 {
return errors.New("accept-interval can't be negative")
}
return nil
}

Expand Down
28 changes: 28 additions & 0 deletions sei-tendermint/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/sei-protocol/sei-chain/sei-tendermint/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/time/rate"
)

func TestDefaultConfig(t *testing.T) {
Expand Down Expand Up @@ -38,6 +39,16 @@ func TestConfigValidateBasic(t *testing.T) {
assert.Error(t, cfg.ValidateBasic())
}

// Asserts Config.ValidateBasic routes the [p2p] section, not merely that the
// section's own checks work.
func TestConfigValidateBasicRoutesP2P(t *testing.T) {
cfg := DefaultConfig()
require.NoError(t, cfg.ValidateBasic())

cfg.P2P.AcceptInterval = -1
require.Error(t, cfg.ValidateBasic())
}

func TestTLSConfiguration(t *testing.T) {
cfg := DefaultConfig()
cfg.SetRoot("/home/user")
Expand Down Expand Up @@ -229,6 +240,8 @@ func TestP2PConfigValidateBasic(t *testing.T) {
"MaxPacketMsgPayloadSize",
"SendRate",
"RecvRate",
"DialInterval",
"AcceptInterval",
}

for _, fieldName := range fieldsToTest {
Expand All @@ -238,6 +251,21 @@ func TestP2PConfigValidateBasic(t *testing.T) {
}
}

// Pins the accept-interval default exactly, so changing it is deliberate and
// visible in the diff.
func TestP2PConfigAcceptInterval(t *testing.T) {
cfg := DefaultP2PConfig()
require.NoError(t, cfg.ValidateBasic())

require.Equal(t, 10*time.Millisecond, cfg.AcceptInterval)

// A zero interval is the documented escape hatch for disabling the limiter
// outright, and must stay valid rather than becoming a zero rate.
cfg.AcceptInterval = 0
require.NoError(t, cfg.ValidateBasic())
require.Equal(t, rate.Inf, rate.Every(cfg.AcceptInterval))
}

// --- WalFile legacy fallback tests ---

func TestWalFile_NewDefault_NoLegacy(t *testing.T) {
Expand Down
61 changes: 61 additions & 0 deletions sei-tendermint/config/p2p_compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package config_test

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/spf13/viper"
"github.com/stretchr/testify/require"

tmconfig "github.com/sei-protocol/sei-chain/sei-tendermint/config"
)

// readP2PConfig decodes a config.toml into a default Config: absent keys keep
// the value already in the struct.
func readP2PConfig(t *testing.T, body string) *tmconfig.P2PConfig {
t.Helper()

path := filepath.Join(t.TempDir(), "config.toml")
require.NoError(t, os.WriteFile(path, []byte(body), 0600))

v := viper.New()
v.SetConfigFile(path)
require.NoError(t, v.ReadInConfig())

cfg := tmconfig.DefaultConfig()
require.NoError(t, v.Unmarshal(cfg))
return cfg.P2P
}

// dial-interval is absent from the generated template (see checkConfig in
// toml_test.go), so nothing else shows it is readable at all.
func TestP2PPacingKnobsParseFromExistingConfig(t *testing.T) {
p2p := readP2PConfig(t, `
[p2p]
laddr = "tcp://0.0.0.0:26656"
dial-interval = "5s"
accept-interval = "20ms"
`)

require.Equal(t, 5*time.Second, p2p.DialInterval)
require.Equal(t, 20*time.Millisecond, p2p.AcceptInterval)
require.NoError(t, p2p.ValidateBasic())
}

// TestP2PConfigPredatingPacingKnobsKeepsDefaults asserts a config.toml written
// before these keys existed still parses to the defaults rather than to zero,
// which rate.Every would read as "no pacing".
func TestP2PConfigPredatingPacingKnobsKeepsDefaults(t *testing.T) {
p2p := readP2PConfig(t, `
[p2p]
laddr = "tcp://0.0.0.0:26656"
`)

defaults := tmconfig.DefaultP2PConfig()
require.Equal(t, defaults.AcceptInterval, p2p.AcceptInterval)
require.Equal(t, defaults.DialInterval, p2p.DialInterval)
require.NotZero(t, p2p.AcceptInterval, "a zero accept-interval disables accept pacing entirely")
require.NoError(t, p2p.ValidateBasic())
}
6 changes: 6 additions & 0 deletions sei-tendermint/config/toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ allow-duplicate-ip = {{ .P2P.AllowDuplicateIP }}
handshake-timeout = "{{ .P2P.HandshakeTimeout }}"
dial-timeout = "{{ .P2P.DialTimeout }}"

# How often the node accepts a new inbound connection. A larger interval paces
# the accept loop more slowly; if the kernel accept backlog outpaces it, arriving
# peers wait past handshake-timeout and the node silently stops acquiring inbound
# peers. A value of 0 disables the limiter.
accept-interval = "{{ .P2P.AcceptInterval }}"

# Time to wait before flushing messages out on the connection
# TODO: Remove once MConnConnection is removed.
flush-throttle-timeout = "{{ .P2P.FlushThrottleTimeout }}"
Expand Down
19 changes: 19 additions & 0 deletions sei-tendermint/config/toml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ func checkConfig(t *testing.T, configFile string) {
t.Errorf("config file was not expected to contain %s", e)
}
}

// accept-interval is rendered deliberately: an accept rate too low to drain
// the kernel backlog silently stops a node acquiring inbound peers, and the
// template is where an operator looks. Keep it discoverable.
if !configContainsKey(configFile, "accept-interval") {
t.Errorf("config file was expected to contain accept-interval but did not")
}

// dial-interval remains an expert-only knob, left out of the generated
// template while still being parsed from existing config files.
// See TestP2PPacingKnobsParseFromExistingConfig.
var hiddenP2PElems = []string{
"dial-interval",
}
for _, e := range hiddenP2PElems {
if configContainsKey(configFile, e) {
t.Errorf("config file was not expected to contain %s", e)
}
}
}

func configContainsKey(configFile string, key string) bool {
Expand Down
9 changes: 7 additions & 2 deletions sei-tendermint/internal/p2p/routeroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ type RouterOptions struct {
// MaxDialRate limits the rate at which router is dialing peers. Defaults to 0.1/s.
MaxDialRate utils.Option[rate.Limit]

// MaxAcceptRate limits the rate at which router is accepting TCP connections. Defaults to 1/s.
// MaxAcceptRate limits the sustained rate at which router is accepting TCP
// connections; the limiter's burst is MaxConcurrentAccepts. Defaults to 100/s.
MaxAcceptRate utils.Option[rate.Limit]

// ResolveTimeout is the timeout for resolving NodeAddress URLs.
Expand Down Expand Up @@ -161,7 +162,11 @@ func (o *RouterOptions) maxDialRate() rate.Limit {
}

func (o *RouterOptions) maxAcceptRate() rate.Limit {
return o.MaxAcceptRate.Or(rate.Every(time.Second))
// The fallback is load-bearing on this branch: unlike main, RouterOptions.Validate
// here has no "rate must be > 0" guard, so a construction site that leaves the
// field unset silently inherits this value rather than failing. 1/s was too low
// to drain the listen backlog, which is the defect #3899 fixes.
return o.MaxAcceptRate.Or(rate.Every(10 * time.Millisecond))
}

func (o *RouterOptions) incomingConnectionWindow() time.Duration {
Expand Down
24 changes: 24 additions & 0 deletions sei-tendermint/internal/p2p/routeroptions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package p2p

import (
"testing"
"time"

"golang.org/x/time/rate"

"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require"
)

// Every test harness pins MaxAcceptRate/MaxDialRate to rate.Inf and node setup
// always sets them, so these fallbacks are otherwise unexercised.
func TestRouterOptionsPacingDefaults(t *testing.T) {
var o RouterOptions

require.Equal(t, rate.Every(10*time.Millisecond), o.maxAcceptRate())
require.Equal(t, rate.Every(10*time.Second), o.maxDialRate())

// An explicit value wins over the fallback.
o.MaxAcceptRate = utils.Some(rate.Inf)
require.Equal(t, rate.Inf, o.maxAcceptRate())
}
73 changes: 52 additions & 21 deletions sei-tendermint/node/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,26 +263,22 @@ func buildGigaConfig(
}, nil
}

func createRouter(
p2pMetrics *p2p.Metrics,
nodeInfoProducer func() *types.NodeInfo,
nodeKey types.NodeKey,
validatorKey utils.Option[atypes.SecretKey],
cfg *config.Config,
app utils.Option[*proxy.Proxy],
genDoc *types.GenesisDoc,
dbProvider config.DBProvider,
) (*p2p.Router, closer, error) {
closer := func() error { return nil }
ep, err := p2p.ResolveEndpoint(nodeKey.ID().AddressString(cfg.P2P.ListenAddress))
if err != nil {
return nil, closer, err
}
var privatePeerIDs []types.NodeID
for _, id := range tmstrings.SplitAndTrimEmpty(cfg.P2P.PrivatePeerIDs, ",", " ") {
privatePeerIDs = append(privatePeerIDs, types.NodeID(id))
}
// pacingRate returns the rate limit for a configured pacing interval.
func pacingRate(key string, interval time.Duration) (rate.Limit, error) {
// rate.Every maps every non-positive interval to rate.Inf. A configured 0 means
// "disable the limiter" and is honoured; a negative value is a typo that would
// silently disable pacing instead. ValidateBasic rejects it wherever it runs,
// but an already-deployed config never reaches ValidateBasic, so refuse it here
// too rather than letting the two paths disagree about the same input.
if interval < 0 {
return 0, fmt.Errorf("p2p %v must not be negative, got %v", key, interval)
}
return rate.Every(interval), nil
}

// p2pRouterOptions returns the router's connection budget and pacing, derived
// from the p2p config.
func p2pRouterOptions(cfg *config.Config, ep p2p.Endpoint, privatePeerIDs []types.NodeID) (*p2p.RouterOptions, error) {
// MaxConnections defaults to 64
maxConns := 64
if cfg.P2P.MaxConnections > 0 {
Expand All @@ -304,10 +300,19 @@ func createRouter(
connection.SendRate = cfg.P2P.SendRate
connection.RecvRate = cfg.P2P.RecvRate
connection.MaxPacketMsgPayloadSize = cfg.P2P.MaxPacketMsgPayloadSize
options := &p2p.RouterOptions{
dialRate, err := pacingRate("dial-interval", cfg.P2P.DialInterval)
if err != nil {
return nil, err
}
acceptRate, err := pacingRate("accept-interval", cfg.P2P.AcceptInterval)
if err != nil {
return nil, err
}
return &p2p.RouterOptions{
Endpoint: ep,
MaxIncomingConnectionAttempts: utils.Some(cfg.P2P.MaxIncomingConnectionAttempts),
MaxDialRate: utils.Some(rate.Every(cfg.P2P.DialInterval)),
MaxDialRate: utils.Some(dialRate),
MaxAcceptRate: utils.Some(acceptRate),
HandshakeTimeout: utils.Some(cfg.P2P.HandshakeTimeout),
DialTimeout: utils.Some(cfg.P2P.DialTimeout),
PexOnHandshake: cfg.P2P.PexReactor,
Expand All @@ -316,6 +321,32 @@ func createRouter(
MaxOutbound: utils.Some(maxOutbound),
MaxConcurrentAccepts: utils.Some(maxInbound),
Connection: connection,
}, nil
}

func createRouter(
p2pMetrics *p2p.Metrics,
nodeInfoProducer func() *types.NodeInfo,
nodeKey types.NodeKey,
validatorKey utils.Option[atypes.SecretKey],
cfg *config.Config,
app utils.Option[*proxy.Proxy],
genDoc *types.GenesisDoc,
dbProvider config.DBProvider,
) (*p2p.Router, closer, error) {
closer := func() error { return nil }
ep, err := p2p.ResolveEndpoint(nodeKey.ID().AddressString(cfg.P2P.ListenAddress))
if err != nil {
return nil, closer, err
}
var privatePeerIDs []types.NodeID
for _, id := range tmstrings.SplitAndTrimEmpty(cfg.P2P.PrivatePeerIDs, ",", " ") {
privatePeerIDs = append(privatePeerIDs, types.NodeID(id))
}

options, err := p2pRouterOptions(cfg, ep, privatePeerIDs)
if err != nil {
return nil, closer, err
}
if addr := cfg.P2P.ExternalAddress; addr != "" {
nodeAddr, err := p2p.ParseNodeAddress(nodeKey.ID().AddressString(addr))
Expand Down
Loading
Loading