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
200 changes: 200 additions & 0 deletions config/cosmosbase/agreement_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package cosmosbase

import (
"fmt"
"sort"
"testing"

"github.com/spf13/viper"

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

// whatANodeRunsToday is what each diverging key resolves to for a configuration carrying no keys.
//
// A declared value is what seid init writes for a kind of node. That is not what a node with nothing
// written resolves, and these are the keys where the two differ. Most are reads that take no account of
// whether the key was present, so an absent key casts to a zero and the default beside it is lost.
//
// Held as text because the two sides carry different Go types for the same key often enough that comparing
// values would be comparing shapes. What matters here is which keys disagree and what a node gets instead.
var whatANodeRunsToday = map[string]string{
"api.address": "",
"api.max-open-connections": "0",
"api.rpc-max-body-bytes": "0",
"api.rpc-read-timeout": "0",
"api.swagger": "false",
"grpc.enable": "true",
"minimum-gas-prices": "",
"occ-enabled": "false",
"pruning": "default",
"pruning-keep-every": "",
"telemetry.enabled": "false",
"telemetry.prometheus-retention-time": "0",
}

// whyItMatters says what a node gets today, for the keys where that is worth stating.
var whyItMatters = map[string]string{
"pruning": "a command flag of this name carries the standard schedule below the file, so a node with " +
"nothing written prunes on that schedule where a generated file would have said keep everything",
"grpc.enable": "a command flag of this name defaults the interface on, so a validator with nothing " +
"written serves gRPC where a generated file would have written it off. This is the one interface " +
"toggle of the two that diverges; the REST one agrees",
"api.max-open-connections": "zero is unlimited, so the ceiling a generated file states is simply " +
"absent from a node that never wrote it, and the same holds for the body-size ceiling beside it",
"minimum-gas-prices": "an empty price refuses to start, so this key is one no running node can " +
"actually have unwritten",
"occ-enabled": "the transaction execution path, and no command flag carries it, so an absent key " +
"reads as off where a generated file says on",
}

// readerValues is what a node resolves for a configuration carrying none of these keys.
//
// Driven through the reader rather than reasoned about, because the reader is the authority on what an
// absent key resolves to and its answer differs per key: some reads check that the key was present, most
// do not, and two are rescued by a clamp that does nothing for an absent value.
//
// The start command's flags are bound first, the way a booting node binds them, and that is what makes
// this the answer a node gets rather than the answer the reader gives in isolation. Seventeen of these keys
// are also command flags, so a flag's registration default is what an absent key reaches before the lookup
// comes back empty. Without the binding, a key like the gRPC toggle reads as its type's zero and the
// comparison would report agreement where a node disagrees.
//
// One key has to be supplied. The metric label set is the first thing the reader asks for and it refuses a
// configuration without it, so a reader handed nothing at all answers for no key at all.
func readerValues(t *testing.T) map[string]string {
t.Helper()
v := viper.New()
start := server.StartCmd(nil, t.TempDir(), nil)
if err := v.BindPFlags(start.Flags()); err != nil {
t.Fatalf("bind the start flags: %v", err)
}
v.Set(globalLabelsKey, []any{})
cfg, err := srvconfig.GetConfig(v)
if err != nil {
t.Fatalf("the reader refused a configuration carrying only the label set: %v", err)
}

return map[string]string{
"minimum-gas-prices": fmt.Sprint(cfg.MinGasPrices),
"pruning": fmt.Sprint(cfg.Pruning),
"pruning-keep-recent": fmt.Sprint(cfg.PruningKeepRecent),
"pruning-keep-every": fmt.Sprint(cfg.PruningKeepEvery),
"pruning-interval": fmt.Sprint(cfg.PruningInterval),
"halt-height": fmt.Sprint(cfg.HaltHeight),
"halt-time": fmt.Sprint(cfg.HaltTime),
"freeze-height": fmt.Sprint(cfg.FreezeHeight),
"min-retain-blocks": fmt.Sprint(cfg.MinRetainBlocks),
"inter-block-cache": fmt.Sprint(cfg.InterBlockCache),
"compaction-interval": fmt.Sprint(cfg.CompactionInterval),
"concurrency-workers": fmt.Sprint(cfg.ConcurrencyWorkers),
"occ-enabled": fmt.Sprint(cfg.OccEnabled),
"api.enable": fmt.Sprint(cfg.API.Enable),
"api.swagger": fmt.Sprint(cfg.API.Swagger),
"api.address": fmt.Sprint(cfg.API.Address),
"api.enabled-unsafe-cors": fmt.Sprint(cfg.API.EnableUnsafeCORS),
"api.max-open-connections": fmt.Sprint(cfg.API.MaxOpenConnections),
"api.rpc-read-timeout": fmt.Sprint(cfg.API.RPCReadTimeout),
"api.rpc-write-timeout": fmt.Sprint(cfg.API.RPCWriteTimeout),
"api.rpc-max-body-bytes": fmt.Sprint(cfg.API.RPCMaxBodyBytes),
"grpc.enable": fmt.Sprint(cfg.GRPC.Enable),
"grpc.address": fmt.Sprint(cfg.GRPC.Address),
"grpc.max-recv-msg-size": fmt.Sprint(cfg.GRPC.MaxRecvMsgSize),
"grpc.max-open-connections": fmt.Sprint(cfg.GRPC.MaxOpenConnections),
"grpc.max-connection-idle": fmt.Sprint(cfg.GRPC.MaxConnectionIdle),
"grpc.max-connection-age": fmt.Sprint(cfg.GRPC.MaxConnectionAge),
"grpc.max-connection-age-grace": fmt.Sprint(cfg.GRPC.MaxConnectionAgeGrace),
"grpc.keepalive-time": fmt.Sprint(cfg.GRPC.KeepaliveTime),
"grpc.keepalive-timeout": fmt.Sprint(cfg.GRPC.KeepaliveTimeout),
"grpc.keepalive-min-time": fmt.Sprint(cfg.GRPC.KeepaliveMinTime),
"grpc.keepalive-permit-without-stream": fmt.Sprint(cfg.GRPC.KeepalivePermitWithoutStream),
"telemetry.service-name": fmt.Sprint(cfg.Telemetry.ServiceName),
"telemetry.enabled": fmt.Sprint(cfg.Telemetry.Enabled),
"telemetry.enable-hostname": fmt.Sprint(cfg.Telemetry.EnableHostname),
"telemetry.enable-hostname-label": fmt.Sprint(cfg.Telemetry.EnableHostnameLabel),
"telemetry.enable-service-label": fmt.Sprint(cfg.Telemetry.EnableServiceLabel),
"telemetry.prometheus-retention-time": fmt.Sprint(cfg.Telemetry.PrometheusRetentionTime),
"state-sync.snapshot-interval": fmt.Sprint(cfg.StateSync.SnapshotInterval),
"state-sync.snapshot-keep-recent": fmt.Sprint(cfg.StateSync.SnapshotKeepRecent),
"state-sync.snapshot-directory": fmt.Sprint(cfg.StateSync.SnapshotDirectory),
"index-events": fmt.Sprint(cfg.IndexEvents),
globalLabelsKey: fmt.Sprint(cfg.Telemetry.GlobalLabels),
}
}

// TestTheDivergencesFromTheReaderAreTheRecordedOnes measures what a comment used to count.
//
// A declared value is what seid init writes for a kind of node, and for a good number of these keys that is
// not what a node with nothing written resolves. Which keys those are was carried in prose, in four
// paragraphs, and one of the counts was wrong. Prose cannot fail when it is wrong.
//
// So the set is measured. A key that starts diverging fails, and so does one that stops, which means
// guarding a read has to account for its row rather than quietly making a sentence stale.
//
// Run for the mode whose declared values match the reader's own mode-blind answer most closely, because
// the reader takes no mode and comparing every mode against it would report the mode rules as divergences.
// The mode-varying keys are held by name in the test beside this one.
func TestTheDivergencesFromTheReaderAreTheRecordedOnes(t *testing.T) {
reader := readerValues(t)
resolved, err := registry.Resolve(registry.ModeValidator, registry.Sources{})
if err != nil {
t.Fatalf("Resolve: %v", err)
}

var measured []string
for key, got := range reader {
declared, declares := resolved.Values[key]
if !declares {
t.Errorf("%s is read by the upstream reader and no section here declares it", key)
continue
}
if fmt.Sprint(declared) == got {
if _, listed := whatANodeRunsToday[key]; listed {
t.Errorf("%s no longer diverges, both sides being %v. Take it off the record, so the "+
"record stays the set of keys a generated file states differently from a node that "+
"never wrote them", key, declared)
}
continue
}
measured = append(measured, key)
want, listed := whatANodeRunsToday[key]
switch {
case !listed:
t.Errorf("%s is declared as %v and a node with nothing written resolves %q, and nothing "+
"records that. %s", key, declared, got, whyItMatters[key])
case want != got:
t.Errorf("%s is recorded as resolving %q and resolves %q", key, want, got)
}
}

sort.Strings(measured)
if len(measured) != len(whatANodeRunsToday) {
t.Errorf("measured %d divergences and %d are recorded: %v",
len(measured), len(whatANodeRunsToday), measured)
}
}

// TestEveryKeyTheseSectionsDeclareIsOneTheReaderResolves holds the two lists against each other.
//
// The reader's side is written out above, which is a second statement of the same key set. It is the only
// statement available: this reader looks its keys up as inline strings rather than through constants, so
// there is nothing to compare a tag against. A key on one side only is either a setting an operator writes
// that no reader fills, or one the reader fills that no section here declares.
func TestEveryKeyTheseSectionsDeclareIsOneTheReaderResolves(t *testing.T) {
reader := readerValues(t)
for _, section := range []string{
BaseSectionName, APISectionName, GRPCSectionName, TelemetrySectionName, StateSyncSectionName,
} {
registered, ok := registry.Lookup(section)
if !ok {
t.Fatalf("%s is not registered", section)
}
for _, key := range registered.Keys {
if _, filled := reader[key]; !filled {
t.Errorf("%s declares %s and no field above is paired with it", section, key)
}
}
}
}
157 changes: 157 additions & 0 deletions config/cosmosbase/cosmosbase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Package cosmosbase registers the configuration sections whose keys belong to the Cosmos server.
//
// These five register here rather than beside the structs they describe, and the reason is an import edge.
// The mode rules their defaults answer through live in app/params, which imports the upstream server
// configuration, so that package cannot ask for them without a cycle. A vendored tree is not itself the
// obstacle: other sections do register inside one.
//
// A section belongs here only when its keys are upstream's and that edge is in the way. Everything else
// registers in the package that owns its struct, so the struct, the values and the keys stay together.
package cosmosbase

import (
"github.com/sei-protocol/sei-chain/app/params"
"github.com/sei-protocol/sei-chain/config/registry"
srvconfig "github.com/sei-protocol/sei-chain/sei-cosmos/server/config"
)

// The names these sections have in the configuration key space.
//
// BaseSectionName names a section whose keys carry no prefix at all. The name is for lookups and reports
// and is not part of any key, because giving those settings a section would rename every one of them.
const (
BaseSectionName = "base"
APISectionName = "api"
GRPCSectionName = "grpc"
TelemetrySectionName = "telemetry"
StateSyncSectionName = "state-sync"
)

// globalLabelsKey is the metric label set, which is the one key here no environment variable can supply.
const globalLabelsKey = TelemetrySectionName + ".global-labels"

// Registration puts the upstream server's configuration sections in the registry.
//
// Four of the five register the upstream struct directly, because their mapstructure tags already name the
// keys their reader resolves.
func init() {
registry.RegisterRootKeys(BaseSectionName, &srvconfig.BaseConfig{}, baseDefaults)
registry.RegisterSection(APISectionName, &srvconfig.APIConfig{}, apiDefaults)
registry.RegisterSection(GRPCSectionName, &srvconfig.GRPCConfig{}, grpcDefaults)
registry.RegisterSection(TelemetrySectionName, &telemetrySchema{}, telemetryDefaults)
registry.RegisterSection(StateSyncSectionName, &srvconfig.StateSyncConfig{}, stateSyncDefaults)

registry.RefuseFromEnvironment(TelemetrySectionName, globalLabelsKey,
"the metric label set is a list of name and value rows, and its reader takes that exact shape "+
"rather than casting what it finds, so no single environment string can supply it. Write it "+
"in the configuration file instead")
}

// forMode is the server configuration the seid init command writes for a node of this kind.
//
// The upstream defaults with the binary's own mode rules applied, which is the pipeline that command
// builds and renders through the template. So a declared value here is what that file would have held, and
// a caller writing a configuration file writes what that command would have written.
//
// Named by the command, because this binary generates a file two ways and they do not agree. A node
// starting without one gets a file from a second pipeline that applies no mode rules at all and carries
// overrides of its own, so it writes the standard pruning strategy where this writes keeping everything,
// a metric retention of sixty where this writes seven thousand two hundred, the REST interface on for a
// validator where this writes it off, and a pruning interval drawn at random each time it runs. This
// follows the command an operator runs to provision a node, not the file a node writes for itself.
//
// That is what a declared value states, and it is deliberately not what a node with nothing written
// resolves. Those differ for a good number of these keys, because most are read with no check that the key
// was present and several are bound to a command flag carrying its own default below the file. The set is
// measured rather than counted, in the agreement test beside this one.
//
// Three settings differ by mode today and each of them matters in a different direction. A node that
// serves queries needs the interfaces that serve them; a validator is meant to expose as little as it can;
// and how many blocks a node retains is a decision about its disk.
func forMode(mode registry.Mode) *srvconfig.Config {
out := srvconfig.DefaultConfig()
params.SetAppConfigByMode(out, params.NodeMode(mode))
return out
}

// baseDefaults is what the node-wide settings resolve to for a node of this kind.
//
// One of these keys answers per mode: how many blocks a node retains, which is a hundred thousand for a
// full node and everything for the rest. The other two mode-varying keys in this package are the interface
// toggles, which belong to the sections that own them.
//
// Every one of these keys is read with a casting getter and no check that the key was present, so an
// absent key casts to a zero and clobbers the default beside it. Which keys those are, and what a node
// resolves for each instead, belongs in a measurement rather than in a count here.
//
// A caller resolving for a running node has to supply that node's flag values, and only the ones an
// operator actually set. A flag nobody typed still reports a default, and this resolution ranks flags above
// the file, so passing defaults would put every one of them over an operator's own value.
func baseDefaults(mode registry.Mode) any { return forMode(mode).BaseConfig }

// apiDefaults is what the REST interface settings resolve to for a node of this kind.
//
// On for a full node and an archive node, off for a validator and a seed. Serving queries is what the
// first two are for, and the second two are meant to expose as little as they can.
func apiDefaults(mode registry.Mode) any { return forMode(mode).API }

// grpcDefaults is what the gRPC settings resolve to for a node of this kind.
//
// On for a full node and an archive node, off for a validator and a seed, which is the same rule the REST
// interface follows and for the same reason. The upstream default is on for every kind, so declaring that
// would state an open interface on the nodes meant to expose the least.
//
// Six of these eleven keys are read only when the key is present. Two more are durations read through a
// clamp that rescues a negative value and does nothing for an absent one, so those two are unguarded and
// their clobber leaves no trace. The durations are declared as durations and written into a file as text,
// which is the shape the reader parses back.
func grpcDefaults(mode registry.Mode) any { return forMode(mode).GRPC }

// stateSyncDefaults is what the snapshot settings resolve to for a node of this kind.
//
// All three keys are read with a casting getter and no presence check, and the retention is the one that
// inverts: it is declared as keeping two snapshots and an absent key casts to zero, which the file format
// documents as keeping every snapshot.
func stateSyncDefaults(mode registry.Mode) any { return forMode(mode).StateSync }

// telemetrySchema declares the keys the metric settings reader resolves.
//
// A schema rather than the upstream type, and the only one of these five that needs one. The difference is
// a single field's type. The upstream struct declares the label set as a list of string pairs, and the
// reader takes a list of untyped rows: it asserts that exact shape rather than casting what it finds, and
// the struct's own type does not satisfy it, including that type's empty value. Registering the upstream
// type would resolve a default the reader refuses, and it refuses by returning an error that is the first
// statement of the whole server configuration, so the node stops. Every node, not only one that wrote the
// key.
//
// Every other field matches the upstream type, so this is one field's shape and not the section's.
type telemetrySchema struct {
ServiceName string `mapstructure:"service-name"`
Enabled bool `mapstructure:"enabled"`
EnableHostname bool `mapstructure:"enable-hostname"`
EnableHostnameLabel bool `mapstructure:"enable-hostname-label"`
EnableServiceLabel bool `mapstructure:"enable-service-label"`
PrometheusRetentionTime int64 `mapstructure:"prometheus-retention-time"`
GlobalLabels []any `mapstructure:"global-labels"`
}

// telemetryDefaults is what the metric settings resolve to for a node of this kind.
//
// Read out of the upstream defaults rather than written again here, so a changed default moves both at
// once and this states only which key carries which setting.
//
// The label set is empty, which is what the upstream default holds, so there is nothing to convert into
// the untyped rows the reader takes. A test holds that emptiness, because a default that gained rows would
// need converting and would otherwise reach the reader as the shape it refuses.
func telemetryDefaults(mode registry.Mode) any {
live := forMode(mode).Telemetry
return telemetrySchema{
ServiceName: live.ServiceName,
Enabled: live.Enabled,
EnableHostname: live.EnableHostname,
EnableHostnameLabel: live.EnableHostnameLabel,
EnableServiceLabel: live.EnableServiceLabel,
PrometheusRetentionTime: live.PrometheusRetentionTime,
GlobalLabels: []any{},
}
}
Loading
Loading