Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
edff731
fix(p2p): make the inbound accept rate configurable and raise its def…
bdchatham Aug 11, 2026
cb0338f
test(config): pin accept-interval under the hidden-knob convention
bdchatham Aug 11, 2026
c704f4d
address review: render accept-interval, pin the default exactly, test…
bdchatham Aug 11, 2026
0ccc3ff
address review round 2: fix the default at the choke point, tighten t…
bdchatham Aug 11, 2026
b816df2
address review round 3: correct the connTracker rationale, close the …
bdchatham Aug 11, 2026
be9ad90
address review round 4: route [p2p] through Config.ValidateBasic
bdchatham Aug 11, 2026
a9607c5
Merge branch 'main' into fix/p2p-accept-rate-configurable
bdchatham Aug 11, 2026
e56a479
address review round 5: collapse the duplicated accept default to one…
bdchatham Aug 12, 2026
0c95de0
conform godocs to the AGENTS.md Godoc rules added on main
bdchatham Aug 12, 2026
a4b857e
address human review: drop the cross-package default coupling and the…
bdchatham Aug 12, 2026
371b1ee
address review: clamp negative pacing intervals, pin the package defa…
bdchatham Aug 12, 2026
d2b60ee
address review: stop documenting the clamp, and stop applying it sile…
bdchatham Aug 12, 2026
c2b0acf
address review: refuse a negative pacing interval instead of clamping it
bdchatham Aug 13, 2026
8f5a204
Merge branch 'main' into fix/p2p-accept-rate-configurable
bdchatham Aug 13, 2026
289b6db
Merge branch 'main' into fix/p2p-accept-rate-configurable
bdchatham Aug 13, 2026
6830f19
Merge branch 'main' into fix/p2p-accept-rate-configurable
bdchatham Aug 14, 2026
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
Comment thread
bdchatham marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,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 {
Comment thread
bdchatham marked this conversation as resolved.
Comment thread
bdchatham marked this conversation as resolved.
Comment thread
bdchatham marked this conversation as resolved.
return fmt.Errorf("error in [p2p] section: %w", err)
Comment thread
bdchatham marked this conversation as resolved.
}
if err := cfg.Mempool.ValidateBasic(); err != nil {
return fmt.Errorf("error in [mempool] section: %w", err)
}
Expand Down Expand Up @@ -732,6 +735,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"`
Comment thread
bdchatham marked this conversation as resolved.
Comment thread
bdchatham marked this conversation as resolved.

// Testing params.
// Force dial to fail
TestDialFail bool `mapstructure:"test-dial-fail"`
Expand Down Expand Up @@ -762,6 +769,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 @@ -782,6 +790,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 {
Comment thread
bdchatham marked this conversation as resolved.
return errors.New("accept-interval can't be negative")
Comment thread
bdchatham marked this conversation as resolved.
}
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 @@ -39,6 +40,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 @@ -231,6 +242,8 @@ func TestP2PConfigValidateBasic(t *testing.T) {
"MaxPacketMsgPayloadSize",
"SendRate",
"RecvRate",
"DialInterval",
"AcceptInterval",
Comment thread
bdchatham marked this conversation as resolved.
}

for _, fieldName := range fieldsToTest {
Expand All @@ -240,6 +253,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 @@ -352,6 +352,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 }}"
Comment thread
bdchatham marked this conversation as resolved.
Comment thread
bdchatham marked this conversation as resolved.

# 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 @@ -99,6 +99,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
6 changes: 5 additions & 1 deletion sei-tendermint/internal/p2p/routeroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ type RouterOptions struct {
// MaxDialRate limits the rate at which router is dialing peers.
MaxDialRate rate.Limit

// MaxAcceptRate limits the rate at which router is accepting TCP connections.
// MaxAcceptRate limits the sustained rate at which router is accepting TCP
// connections; the limiter's burst is MaxConcurrentAccepts. Required — the
// default lives on the `accept-interval` config key, not here, and Validate
// rejects a zero value rather than letting a construction site inherit a rate
// too low to drain the listen backlog.
MaxAcceptRate rate.Limit

// ResolveTimeout is the timeout for resolving NodeAddress URLs.
Expand Down
38 changes: 38 additions & 0 deletions sei-tendermint/internal/p2p/routeroptions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package p2p

import (
"testing"
"time"

"golang.org/x/time/rate"

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

// #3922 removed the package-level pacing fallbacks in favour of requiring both
// rates from the construction site, so the risk this guards has moved rather
// than gone: a site that forgets to set them no longer inherits a rate too low
// to drain the listen backlog, it fails Validate. Every router test harness
// pins these to rate.Inf and node setup derives them from config, so nothing
// else exercises the unset case.
func TestRouterOptionsRequirePacingRates(t *testing.T) {
var o RouterOptions
require.Error(t, o.Validate())

o.MaxDialRate = rate.Every(10 * time.Second)
require.Error(t, o.Validate())

o.MaxAcceptRate = rate.Every(10 * time.Millisecond)
require.NoError(t, o.Validate())

// The accessors are plain reads now; no fallback may reappear between the
// field and the limiter.
require.Equal(t, rate.Every(10*time.Millisecond), o.maxAcceptRate())
require.Equal(t, rate.Every(10*time.Second), o.maxDialRate())

// accept-interval = 0 is the documented way to disable pacing: rate.Every
// maps a non-positive interval to rate.Inf, which Validate still accepts.
o.MaxAcceptRate = rate.Every(0)
require.Equal(t, rate.Inf, o.maxAcceptRate())
require.NoError(t, o.Validate())
}
76 changes: 53 additions & 23 deletions sei-tendermint/node/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,27 +455,22 @@ func buildFullnodeGigaConfig(
}, nil
}

func createRouter(
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, utils.Option[atypes.BlockDB], error) {
closer := func() error { return nil }
noneDB := utils.None[atypes.BlockDB]()
gigaBlockDB := noneDB
ep, err := p2p.ResolveEndpoint(nodeKey.ID().AddressString(cfg.P2P.ListenAddress))
if err != nil {
return nil, closer, noneDB, 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 {
Comment thread
bdchatham marked this conversation as resolved.
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 @@ -497,11 +492,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: rate.Every(cfg.P2P.DialInterval),
MaxAcceptRate: rate.Every(time.Second),
MaxDialRate: dialRate,
MaxAcceptRate: acceptRate,
HandshakeTimeout: utils.Some(cfg.P2P.HandshakeTimeout),
DialTimeout: utils.Some(cfg.P2P.DialTimeout),
PexOnHandshake: cfg.P2P.PexReactor,
Expand All @@ -510,6 +513,33 @@ func createRouter(
MaxOutbound: utils.Some(maxOutbound),
MaxConcurrentAccepts: utils.Some(maxInbound),
Connection: connection,
}, nil
}

func createRouter(
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, utils.Option[atypes.BlockDB], error) {
closer := func() error { return nil }
noneDB := utils.None[atypes.BlockDB]()
gigaBlockDB := noneDB
ep, err := p2p.ResolveEndpoint(nodeKey.ID().AddressString(cfg.P2P.ListenAddress))
if err != nil {
return nil, closer, noneDB, 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, noneDB, err
}
if addr := cfg.P2P.ExternalAddress; addr != "" {
nodeAddr, err := p2p.ParseNodeAddress(nodeKey.ID().AddressString(addr))
Expand Down
Loading
Loading