Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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.
Comment thread
bdchatham marked this conversation as resolved.
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
40 changes: 40 additions & 0 deletions evmrpc/config/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 the seid init command writes for a node of this kind.
//
// That command applies the same mode rule to this section's own defaults and renders the result, and what
// it renders is passed through rather than refilled from a mode-blind copy, so a declared value here is the
// value that reaches the file.
//
// The two interface toggles are what the rule changes. 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
Comment thread
bdchatham marked this conversation as resolved.
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 the seid init command writes for a node of this kind.
//
// 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