Skip to content
Draft
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
6 changes: 5 additions & 1 deletion app/params/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package params

import (
"github.com/sei-protocol/sei-chain/config/registry"
evmrpcconfig "github.com/sei-protocol/sei-chain/evmrpc/config"
srvconfig "github.com/sei-protocol/sei-chain/sei-cosmos/server/config"
"github.com/sei-protocol/sei-chain/sei-cosmos/types/address"
Expand Down Expand Up @@ -94,8 +95,11 @@ const (
)

// IsFullnodeType returns true if the node is a fullnode-like node (full or archive)
//
// The rule itself lives in the configuration registry, because a section's own package needs the same
// fact and cannot import this one.
func (m NodeMode) IsFullnodeType() bool {
return m == NodeModeFull || m == NodeModeArchive
return registry.IsFullnodeMode(registry.Mode(m))
}

// setValidatorTypeTendermintConfig sets common Tendermint config for validator-like nodes
Expand Down
65 changes: 65 additions & 0 deletions config/registry/detach_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package registry_test

import (
"reflect"
"testing"

"github.com/sei-protocol/sei-chain/config/registry"
)

// listBearing is a probe whose default is a package-level variable, which is the usual shape.
type listBearing struct {
Allowed []string `mapstructure:"allowed"`
Labels map[string]string `mapstructure:"labels"`
Absent []string `mapstructure:"absent"`
}

var listBearingDefault = listBearing{
Allowed: []string{"callTracer", "prestateTracer"},
Labels: map[string]string{"chain": "pacific-1"},
}

// TestAResolvedListIsTheCallersToWriteInto covers what a caller may do with a resolved value.
//
// A section's default is usually a package-level variable, so handing out its slice hands out the array
// that variable holds. A caller sorting or de-duplicating a resolved list in place, which is what a caller
// producing deterministic output does, would rewrite that variable for the whole process: every later
// resolution and every reader that copies the same struct. Two of the lists this reaches in practice are
// deny lists, so the rewrite is silent and it is a security control.
func TestAResolvedListIsTheCallersToWriteInto(t *testing.T) {
registry.Reset()
registry.RegisterSection("probe", &listBearing{}, func(registry.Mode) any { return listBearingDefault })
for _, d := range registry.Defects() {
t.Fatalf("the probe was refused: %v", d.Err)
}

resolved, err := registry.Resolve(registry.ModeFull, registry.Sources{})
if err != nil {
t.Fatalf("Resolve: %v", err)
}

resolved.Values["probe.allowed"].([]string)[0] = "written-by-the-caller"
resolved.Values["probe.labels"].(map[string]string)["chain"] = "written-by-the-caller"

if got := listBearingDefault.Allowed[0]; got != "callTracer" {
t.Errorf("writing into the resolved list changed the section's own default to %q, so every later "+
"resolution and every reader copying that struct carries the caller's value", got)
}
if got := listBearingDefault.Labels["chain"]; got != "pacific-1" {
t.Errorf("writing into the resolved map changed the section's own default to %q", got)
}

again, err := registry.Resolve(registry.ModeFull, registry.Sources{})
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if got := again.Values["probe.allowed"]; !reflect.DeepEqual(got, []string{"callTracer", "prestateTracer"}) {
t.Errorf("a later resolution carries %v, so one caller's edit reached another's answer", got)
}

// A nil list stays nil rather than becoming an empty one, because absent and empty are different
// answers to a reader that checks length.
if got := again.Values["probe.absent"]; got == nil || !reflect.ValueOf(got).IsNil() {
t.Errorf("an unset list resolved to %#v, want a nil slice of its own type", got)
}
}
11 changes: 11 additions & 0 deletions config/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ const (
// Modes returns every mode a default is asked for, in a fixed order.
func Modes() []Mode { return []Mode{ModeValidator, ModeFull, ModeSeed, ModeArchive} }

// IsFullnodeMode reports whether a node of this kind serves queries to callers other than itself.
//
// Stated here because more than one package needs it and they sit on opposite sides of an import edge.
// The package that owns the node a binary was started as also owns the type that describes it, and a
// section's own package needs the same fact to state a default that varies on it while being imported by
// that package rather than importing it.
//
// An archive node counts. It serves queries, which is the property this names, and it is the mode most
// easily forgotten when the rule is written out by hand.
func IsFullnodeMode(mode Mode) bool { return mode == ModeFull || mode == ModeArchive }

// Section is one registered configuration section.
type Section struct {
// Name is the section's own segment, and the first segment of every key it declares.
Expand Down
43 changes: 42 additions & 1 deletion config/registry/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import (
// Resolved is every declared key's value, plus what a caller has to be told about how it got there.
type Resolved struct {
// Values carries one value per declared key.
//
// A key's Go type depends on which source answered it, and a caller that type-asserts has to expect
// all three. A default arrives as the field's own type, so a duration is a duration and a list is a
// list. A file arrives as whatever the file format decodes to, so the same duration is text and the
// same list is a list of untyped elements. An environment variable arrives as one string, always. This
// resolves values and does not convert them, so the reader that owns a key remains the thing that
// turns any of the three into what that key means.
//
// A value is the caller's to write into. Nothing here shares storage with a section's own default.
Values map[string]any
// Overrides are the declared keys something other than this node's defaults supplied, sorted.
//
Expand Down Expand Up @@ -259,11 +268,43 @@ func walkValues(v reflect.Value, prefix string, out map[string]any) error {
}
continue
}
out[path] = fv.Interface()
out[path] = detach(fv)
}
return nil
}

// detach returns a field's value with nothing shared with the struct it came from.
//
// A section's default is usually a package-level variable, so a slice or a map field hands out the
// backing array that variable holds. A caller sorting or de-duplicating a resolved list in place, which is
// what a caller producing deterministic output does, would rewrite that variable for the whole process:
// every later resolution, and every reader that copies the same struct. Two of the lists that reach here
// are deny lists, so the rewrite is silent and it is a security control.
//
// Lookup already copies a section's keys for this reason. This is the same guarantee for its values.
func detach(v reflect.Value) any {
switch v.Kind() {
case reflect.Slice:
if v.IsNil() {
return v.Interface()
}
out := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
reflect.Copy(out, v)
return out.Interface()
case reflect.Map:
if v.IsNil() {
return v.Interface()
}
out := reflect.MakeMapWithSize(v.Type(), v.Len())
for _, key := range v.MapKeys() {
out.SetMapIndex(key, v.MapIndex(key))
}
return out.Interface()
default:
return v.Interface()
}
}

// envValues reads the keys an environment supplies, from the caller's declared set.
//
// Driven by the declared set rather than by the environment, which is also what makes it complete:
Expand Down
36 changes: 36 additions & 0 deletions evmrpc/config/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package config

import "github.com/sei-protocol/sei-chain/config/registry"

// SectionName is this section's name in the configuration key space.
const SectionName = "evm"

// Registration puts this package's configuration section in the registry.
//
// The owning package registers its own section, so the struct, the values and the keys come from one
// place. This section's mapstructure tags already spell the keys its reader resolves, so the registry
// derives what a node reads rather than restating them.
func init() {
registry.RegisterSection(SectionName, &Config{}, defaults)
}

// defaults is what this section resolves to for a node that has written nothing.
//
// The two interface toggles answer per kind of node. A full node and an archive node serve queries, which
// is what these interfaces are for; a validator and a seed serve none, and leaving them open would put a
// public request surface on the node that holds a signing key. The rule is read from the registry rather
// than restated, because the package that owns the node mode imports this one and cannot be imported back.
//
// Two values come from the machine rather than from a decision, and they are not one case. The worker pool
// has a portable answer: the pool re-measures whenever the value it is given is not positive, so a file
// carrying zero lets every node size itself, and a caller rendering into a file should write that rather
// than this. The simulation call limit has no portable answer, because zero there is not a request to
// measure but the absence of a limit, and the limit is the only bound on how many simulations a node runs
// at once. Both describe the host that resolved them, so neither travels.
func defaults(mode registry.Mode) any {
cfg := DefaultConfig
serves := registry.IsFullnodeMode(mode)
cfg.HTTPEnabled = serves
cfg.WSEnabled = serves
return cfg
}
121 changes: 121 additions & 0 deletions evmrpc/config/register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package config

import (
"reflect"
"sort"
"testing"

"github.com/sei-protocol/sei-chain/config/registry"
)

// TestDeclaredKeysAreTheOnesItsReaderResolves holds the derived keys against the reader's own constants.
//
// The section registers the struct its reader fills, so a mapstructure tag is the only spelling of these
// keys and there is no second list to fall behind. What remains is the constants ReadConfig looks up,
// which state the same keys again in the same file, and a rename that moves one and not the other
// compiles.
//
// Written out rather than derived from the struct, because a list derived from the same tags would agree
// with itself whatever those tags said.
func TestDeclaredKeysAreTheOnesItsReaderResolves(t *testing.T) {
for _, defect := range registry.Defects() {
if defect.Section == SectionName {
t.Fatalf("%s was refused, so none of its keys is declared: %v", SectionName, defect.Err)
}
}
want := []string{
flagHTTPEnabled, flagHTTPPort, flagWSEnabled, flagWSPort,
flagReadTimeout, flagReadHeaderTimeout, flagWriteTimeout, flagIdleTimeout,
flagSimulationGasLimit, flagSimulationEVMTimeout, flagCORSOrigins, flagWSOrigins,
flagFilterTimeout, flagMaxTxPoolTxs, flagCheckTxTimeout, flagSlow,
flagEnableSimulation, flagDenyList, flagMaxLogNoBlock, flagMaxLogBytes,
flagMaxBlocksForLog, flagMaxEstimateGasCalls, flagMaxStateOverrideAccounts,
flagMaxStateOverrideSlots, flagMaxSubscriptionsNewHead, flagMaxSubscriptionsLogs,
flagEnableTestAPI, flagMaxConcurrentTraceCalls, flagMaxConcurrentSimulationCalls,
flagMaxTraceLookbackBlocks, flagTraceTimeout, flagMaxTraceStructLogBytes,
flagTraceAllowedTracers, flagTraceAllowJSTracers, flagEnableParallelizedBlockTrace,
flagRPCStatsInterval, flagWorkerPoolSize, flagWorkerQueueSize, flagEVMLegacySeiApis,
flagTraceBakeEnabled, flagTraceBakeWorkers, flagTraceBakeQueueSize, flagTraceBakeTracers,
flagTraceBakeWindowBlocks, flagTraceBakeUseSnapshot, flagTraceBakeSnapshotWindow,
flagIPRateLimitRPS, flagIPRateLimitBurst, flagRateLimitingEnabled, flagTrustedProxyCIDRs,
flagBatchRequestLimit, flagBatchResponseMaxSize, flagMaxRequestBodyBytes,
flagMaxConcurrentRequestBytes, flagWSAdmissionTimeout, flagMaxOpenConnections,
flagBodyReadIdleTimeout,
}
sort.Strings(want)

section, ok := registry.Lookup(SectionName)
if !ok {
t.Fatalf("%s is not registered, so nothing resolves its keys", SectionName)
}
declared := map[string]bool{}
for _, key := range section.Keys {
declared[key] = true
}
for _, key := range want {
if !declared[key] {
t.Errorf("the reader resolves %s and no tag declares it", key)
}
delete(declared, key)
}
for key := range declared {
t.Errorf("%s is declared and no constant in this file resolves it", key)
}
}

// TestEachKindOfNodeResolvesTheInterfacesItIsFor is the mode-varying part of this section.
//
// A full node and an archive node serve queries, which is what these two interfaces are for. A validator
// and a seed serve none, and an open interface on the node that holds a signing key is a public request
// surface on the one node meant to expose the least. The values are written out here rather than taken
// from the same rule the section reads, so a change to that rule fails this and gets looked at.
func TestEachKindOfNodeResolvesTheInterfacesItIsFor(t *testing.T) {
serving := map[registry.Mode]bool{
registry.ModeValidator: false,
registry.ModeSeed: false,
registry.ModeFull: true,
registry.ModeArchive: true,
}
for _, mode := range registry.Modes() {
want, named := serving[mode]
if !named {
t.Fatalf("mode %q has no expectation here, so a mode was added and this was not revisited", mode)
}
resolved, err := registry.Resolve(mode, registry.Sources{})
if err != nil {
t.Fatalf("mode %q: %v", mode, err)
}
for _, key := range []string{flagHTTPEnabled, flagWSEnabled} {
if got := resolved.Values[key]; got != want {
t.Errorf("mode %q: %s resolves to %v, want %v", mode, key, got, want)
}
}
}
}

// TestEachKeyResolvesToTheValueItsFieldHolds covers the binding a key set cannot show.
//
// Resolving carries the key a tag produced together with the value that tag's field held. Comparing the
// defaults struct against itself does not: two tags on each other's fields leave the key set identical and
// every field still holding the value it always did, so a list and a URL change places unnoticed.
func TestEachKeyResolvesToTheValueItsFieldHolds(t *testing.T) {
resolved, err := registry.Resolve(registry.ModeFull, registry.Sources{})
if err != nil {
t.Fatalf("%v", err)
}
for key, want := range map[string]any{
flagCORSOrigins: DefaultConfig.CORSOrigins,
flagDenyList: DefaultConfig.DenyList,
flagTraceAllowedTracers: DefaultConfig.TraceAllowedTracers,
flagEVMLegacySeiApis: DefaultConfig.EnabledLegacySeiApis,
flagTrustedProxyCIDRs: DefaultConfig.TrustedProxyCIDRs,
flagReadTimeout: DefaultConfig.ReadTimeout,
flagHTTPPort: DefaultConfig.HTTPPort,
flagIPRateLimitRPS: DefaultConfig.IPRateLimitRPS,
flagMaxLogBytes: DefaultConfig.MaxLogBytes,
} {
if got := resolved.Values[key]; !reflect.DeepEqual(got, want) {
t.Errorf("%s resolves to %#v (%T), want %#v (%T)", key, got, got, want, want)
}
}
}
24 changes: 24 additions & 0 deletions x/evm/blocktest/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package blocktest

import "github.com/sei-protocol/sei-chain/config/registry"

// SectionName is this section's name in the configuration key space.
const SectionName = "eth_blocktest"

// Registration puts this package's configuration section in the registry.
//
// The owning package registers its own section, so the struct, the values and the keys come from one
// place. This section's mapstructure tags already spell the keys its reader resolves, so the registry
// derives what a node reads rather than restating them.
func init() {
registry.RegisterSection(SectionName, &Config{}, defaults)
}

// defaults is what this section resolves to for a node that has written nothing.
//
// The same values for every mode. This section drives a harness against recorded block data, which is
// not something any kind of node does while serving a chain.
//
// The data path is a tilde path, and it resolves as written. Whoever opens it expands the tilde, so a
// caller that renders this value into a file writes the same text an operator would.
func defaults(registry.Mode) any { return DefaultConfig }
Loading
Loading