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

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"
"github.com/sei-protocol/sei-chain/sei-db/config"
)

// The names these sections have in the configuration key space.
const (
LightInvarianceSectionName = "light_invariance"
GenesisSectionName = "genesis"
StateStoreSectionName = "state-store"
StateCommitSectionName = "state-commit"
)

// Registration puts this package's configuration sections in the registry.
//
// The owning package registers its own sections, so the struct, the values and the keys come from one
// place and cannot drift apart. The keys derive from mapstructure tags, so a section's spelling and its
// reader's own constants stay the same strings.
func init() {
registry.RegisterSection(LightInvarianceSectionName, &LightInvarianceConfig{}, lightInvarianceDefaults)
registry.RegisterSection(GenesisSectionName, &genesisSchema{}, genesisDefaults)
registry.RegisterSection(StateStoreSectionName, &stateStoreSchema{}, stateStoreDefaults)
registry.RegisterSection(StateCommitSectionName, &stateCommitSchema{}, stateCommitDefaults)
}

// lightInvarianceDefaults is what this section resolves to for a node that has written nothing.
//
// The same value for every mode, and on. What the check compares is a property of every node rather than
// of one kind, so a mode that resolved it off would stop those nodes noticing they had diverged.
func lightInvarianceDefaults(registry.Mode) any { return DefaultLightInvarianceConfig }

// genesisSchema declares the keys the genesis import reader resolves.
//
// A schema and not a transport: nothing decodes into it. The type the reader fills is
// genesistypes.GenesisImportConfig, which carries no mapstructure tags at all, so no key can be derived
// from it. Declaring the spelling here is what lets the registry name the keys the reader looks up.
type genesisSchema struct {
StreamImport bool `mapstructure:"stream-import"`
ImportFile string `mapstructure:"import-file"`
}

// genesisDefaults is what this section resolves to for a node that has written nothing.
//
// Read out of the reader's own default rather than written again here, so a changed default moves both at
// once and this states only which key carries which setting. The same values for every mode: streaming a
// genesis file is what an operator does to import a chain's existing state, and no node mode implies it.
func genesisDefaults(registry.Mode) any {
return genesisSchema{
StreamImport: DefaultGenesisConfig.StreamGenesisImport,
ImportFile: DefaultGenesisConfig.GenesisStreamFile,
}
}

// stateStoreSchema declares the keys parseSSConfigs resolves.
//
// A schema and not a transport: nothing decodes into it. config.StateStoreConfig carries mapstructure
// tags of its own and every one names something other than the key the reader looks up, so deriving from
// that type would declare a set of keys no operator writes. It also holds settings no key reaches, which
// stay at whatever the defaults struct holds; giving them keys would declare settings a written value
// could not change.
type stateStoreSchema struct {
Enable bool `mapstructure:"ss-enable"`
DBDirectory string `mapstructure:"ss-db-directory"`
Backend string `mapstructure:"ss-backend"`
AsyncWriteBuffer int `mapstructure:"ss-async-write-buffer"`
KeepRecent int `mapstructure:"ss-keep-recent"`
PruneIntervalSeconds int `mapstructure:"ss-prune-interval"`
ImportNumWorkers int `mapstructure:"ss-import-num-workers"`
EnableReadWriteMetrics bool `mapstructure:"ss-enable-read-write-metrics"`
SnapshotEnable bool `mapstructure:"ss-snapshot-enable"`
EVMDBDirectory string `mapstructure:"evm-ss-db-directory"`
SeparateEVMSubDBs bool `mapstructure:"evm-ss-separate-dbs"`
EVMSplit bool `mapstructure:"evm-ss-split"`
}

// stateStoreDefaults is what this section resolves to for a node that has written nothing.
//
// Answered per mode, because two of these settings mean something different depending on what kind of node
// asks. An archive node exists to keep history, so it keeps every version; a validator and a seed serve no
// queries, so the store is off for them. Both come from the mode rules the binary already states rather
// than being written again here, so a change to those rules moves this too.
//
// This is the one section here whose declared values are not what its reader produces for a file missing
// the keys, and the divergences are measured rather than described. A test names each one and what a node
// runs today, so a read that gains a presence check has to account for it.
func stateStoreDefaults(mode registry.Mode) any {
server := srvconfig.DefaultConfig()
params.SetAppConfigByMode(server, params.NodeMode(mode))
live := server.StateStore
return stateStoreSchema{
Enable: live.Enable,
DBDirectory: live.DBDirectory,
Backend: live.Backend,
AsyncWriteBuffer: live.AsyncWriteBuffer,
KeepRecent: live.KeepRecent,
PruneIntervalSeconds: live.PruneIntervalSeconds,
ImportNumWorkers: live.ImportNumWorkers,
EnableReadWriteMetrics: live.EnableReadWriteMetrics,
SnapshotEnable: live.SnapshotEnable,
EVMDBDirectory: live.EVMDBDirectory,
SeparateEVMSubDBs: live.SeparateEVMSubDBs,
EVMSplit: live.EVMSplit,
}
}

// stateCommitFlatKVSchema declares the one flat key-value key this package's reader resolves.
//
// A nested segment, because the key is state-commit.flatkv.enable-read-write-metrics. Four further keys
// under that name are read by the Cosmos server's own configuration reader and not by this one, so they
// belong to whoever registers that reader's section rather than to this one.
type stateCommitFlatKVSchema struct {
EnableReadWriteMetrics bool `mapstructure:"enable-read-write-metrics"`
}

// stateCommitSchema declares the keys parseSCConfigs resolves.
//
// A schema and not a transport: nothing decodes into it. config.StateCommitConfig nests its settings
// under MemIAVLConfig, FlatKVConfig and HashLogger, and the keys the reader looks up are flat names on the
// section itself, so no derivation from that type produces them.
//
// The write mode is a plain string rather than the reader's own named type, because the reader parses a
// written name into that type itself. Declaring the named type would have one key answer as a named string
// from these defaults and as a plain one from an operator's file, which is a difference a caller can trip
// over and nothing here needs.
type stateCommitSchema struct {
Enable bool `mapstructure:"sc-enable"`
Directory string `mapstructure:"sc-directory"`
AsyncCommitBuffer int `mapstructure:"sc-async-commit-buffer"`
SnapshotKeepRecent uint32 `mapstructure:"sc-keep-recent"`
SnapshotInterval uint32 `mapstructure:"sc-snapshot-interval"`
SnapshotMinTimeInterval uint32 `mapstructure:"sc-snapshot-min-time-interval"`
SnapshotWriterLimit int `mapstructure:"sc-snapshot-writer-limit"`
SnapshotPrefetchThreshold float64 `mapstructure:"sc-snapshot-prefetch-threshold"`
SnapshotWriteRateMBps int `mapstructure:"sc-snapshot-write-rate-mbps"`
HistoricalProofMaxInFlight int `mapstructure:"sc-historical-proof-max-inflight"`
HistoricalProofRateLimit float64 `mapstructure:"sc-historical-proof-rate-limit"`
HistoricalProofBurst int `mapstructure:"sc-historical-proof-burst"`
WriteMode string `mapstructure:"sc-write-mode"`
WriteModeEnableAuto bool `mapstructure:"sc-write-mode-enable-auto"`
HashLoggerEnable bool `mapstructure:"sc-hash-logger-enable"`
HashLoggerDirectory string `mapstructure:"sc-hash-logger-directory"`
HashLoggerBlocksToRetain uint `mapstructure:"sc-hash-logger-blocks-to-retain"`
HashLoggerTargetFileSize uint `mapstructure:"sc-hash-logger-target-file-size"`
HashLoggerMaxDiskSize uint `mapstructure:"sc-hash-logger-max-disk-size"`
FlatKV stateCommitFlatKVSchema `mapstructure:"flatkv"`
}

// stateCommitDefaults is what this section resolves to for a node that has written nothing.
//
// The declared defaults. Two of them are not what this section's reader produces for a file missing the
// key, and a test names which two and what a node runs instead.
//
// The same values for every mode. How often a node snapshots and how much proof history it serves are
// decisions about disk and load that an operator writes down, and nothing in the binary makes either
// follow from what kind of node is asking.
func stateCommitDefaults(registry.Mode) any {
live := config.DefaultStateCommitConfig()
return stateCommitSchema{
Enable: live.Enable,
Directory: live.Directory,
AsyncCommitBuffer: live.MemIAVLConfig.AsyncCommitBuffer,
SnapshotKeepRecent: live.MemIAVLConfig.SnapshotKeepRecent,
SnapshotInterval: live.MemIAVLConfig.SnapshotInterval,
SnapshotMinTimeInterval: live.MemIAVLConfig.SnapshotMinTimeInterval,
SnapshotWriterLimit: live.MemIAVLConfig.SnapshotWriterLimit,
SnapshotPrefetchThreshold: live.MemIAVLConfig.SnapshotPrefetchThreshold,
SnapshotWriteRateMBps: live.MemIAVLConfig.SnapshotWriteRateMBps,
HistoricalProofMaxInFlight: live.HistoricalProofMaxInFlight,
HistoricalProofRateLimit: live.HistoricalProofRateLimit,
HistoricalProofBurst: live.HistoricalProofBurst,
WriteMode: string(live.WriteMode),
WriteModeEnableAuto: live.WriteModeEnableAuto,
HashLoggerEnable: live.HashLogger.Enable,
HashLoggerDirectory: live.HashLogger.Directory,
HashLoggerBlocksToRetain: live.HashLogger.BlocksToRetain,
HashLoggerTargetFileSize: live.HashLogger.TargetFileSize,
HashLoggerMaxDiskSize: live.HashLogger.MaxDiskSize,
FlatKV: stateCommitFlatKVSchema{
EnableReadWriteMetrics: live.FlatKVConfig.EnableReadWriteMetrics,
},
}
}
179 changes: 179 additions & 0 deletions app/config_register_agreement_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package app

import (
"fmt"
"sort"
"testing"

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

// whatANodeRunsToday is what each diverging key resolves to for a file carrying no keys at all.
//
// Every entry is a read that takes no account of whether the key was present, or a value another key
// transforms afterwards. Held separately from the modes because the reader takes no mode: it produces one
// answer, and which modes disagree with it depends on what the section declares.
var whatANodeRunsToday = map[string]string{
FlagSSEnable: "false",
FlagSSBackend: "",
FlagSSAsyncWriterBuffer: "0",
FlagSSKeepRecent: "0",
FlagSSPruneInterval: "0",
FlagSSImportNumWorkers: "0",
FlagSCEnable: "false",
FlagSCWriteMode: "auto",
}

// whyItMatters says what a node gets today, for the keys where that is worth stating.
var whyItMatters = map[string]string{
FlagSSPruneInterval: "pruning is off, in the store and in the write-ahead log, so installing the " +
"declared value starts deleting what the node was retaining",
FlagSSKeepRecent: "every version is kept, so for an archive node what is declared and what runs " +
"agree about keeping history and for the others they do not",
FlagSCEnable: "state commitment reads as disabled, and a node started that way stops, which is why " +
"no running node has this key missing",
FlagSCWriteMode: "another key transforms this one after it is read, so the mode a node commits " +
"through is derived rather than carried by this key",
}

// theDivergences is which keys disagree with the reader, per mode.
//
// Per mode because the section answers per mode for two of these settings and the reader does not answer
// per mode at all. An archive node declares the retention the reader also produces, so that key agrees for
// archive and disagrees everywhere else; the store toggle is the reverse.
var theDivergences = map[registry.Mode][]string{
registry.ModeValidator: {FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent,
FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode},
registry.ModeSeed: {FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent,
FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode},
registry.ModeFull: {FlagSSEnable, FlagSSBackend, FlagSSAsyncWriterBuffer, FlagSSKeepRecent,
FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode},
registry.ModeArchive: {FlagSSEnable, FlagSSBackend, FlagSSAsyncWriterBuffer,
FlagSSPruneInterval, FlagSSImportNumWorkers, FlagSCEnable, FlagSCWriteMode},
}

// readerValues is what each section's reader produces for a file carrying no keys at all.
//
// Written as a map from key to the field that key fills, because that pairing is what the comparison
// needs and neither the reader nor the section states it: the reader takes a key and assigns a field, and
// the section declares a key and a value.
func readerValues(t *testing.T) map[string]string {
t.Helper()
ss := parseSSConfigs(configtest.AppOpts{})
sc := parseSCConfigs(configtest.AppOpts{})
return map[string]string{
FlagSSEnable: fmt.Sprint(ss.Enable),
FlagSSDirectory: fmt.Sprint(ss.DBDirectory),
FlagSSBackend: fmt.Sprint(ss.Backend),
FlagSSAsyncWriterBuffer: fmt.Sprint(ss.AsyncWriteBuffer),
FlagSSKeepRecent: fmt.Sprint(ss.KeepRecent),
FlagSSPruneInterval: fmt.Sprint(ss.PruneIntervalSeconds),
FlagSSImportNumWorkers: fmt.Sprint(ss.ImportNumWorkers),
FlagSSReadWriteMetrics: fmt.Sprint(ss.EnableReadWriteMetrics),
FlagSSSnapshotEnable: fmt.Sprint(ss.SnapshotEnable),
FlagEVMSSDirectory: fmt.Sprint(ss.EVMDBDirectory),
FlagEVMSSSeparateDBs: fmt.Sprint(ss.SeparateEVMSubDBs),
FlagEVMSSSplit: fmt.Sprint(ss.EVMSplit),
FlagSCEnable: fmt.Sprint(sc.Enable),
FlagSCDirectory: fmt.Sprint(sc.Directory),
FlagSCAsyncCommitBuffer: fmt.Sprint(sc.MemIAVLConfig.AsyncCommitBuffer),
FlagSCSnapshotKeepRecent: fmt.Sprint(sc.MemIAVLConfig.SnapshotKeepRecent),
FlagSCSnapshotInterval: fmt.Sprint(sc.MemIAVLConfig.SnapshotInterval),
FlagSCSnapshotMinTimeInterval: fmt.Sprint(sc.MemIAVLConfig.SnapshotMinTimeInterval),
FlagSCSnapshotWriterLimit: fmt.Sprint(sc.MemIAVLConfig.SnapshotWriterLimit),
FlagSCSnapshotPrefetchThreshold: fmt.Sprint(sc.MemIAVLConfig.SnapshotPrefetchThreshold),
FlagSCSnapshotWriteRateMBps: fmt.Sprint(sc.MemIAVLConfig.SnapshotWriteRateMBps),
FlagSCHistoricalProofMaxInFlight: fmt.Sprint(sc.HistoricalProofMaxInFlight),
FlagSCHistoricalProofRateLimit: fmt.Sprint(sc.HistoricalProofRateLimit),
FlagSCHistoricalProofBurst: fmt.Sprint(sc.HistoricalProofBurst),
FlagSCWriteMode: fmt.Sprint(sc.WriteMode),
FlagSCWriteModeEnableAuto: fmt.Sprint(sc.WriteModeEnableAuto),
FlagSCHashLoggerEnable: fmt.Sprint(sc.HashLogger.Enable),
FlagSCHashLoggerDirectory: fmt.Sprint(sc.HashLogger.Directory),
FlagSCHashLoggerBlocksToRetain: fmt.Sprint(sc.HashLogger.BlocksToRetain),
FlagSCHashLoggerTargetFileSize: fmt.Sprint(sc.HashLogger.TargetFileSize),
FlagSCHashLoggerMaxDiskSize: fmt.Sprint(sc.HashLogger.MaxDiskSize),
FlagSCFlatKVReadWriteMetrics: fmt.Sprint(sc.FlatKVConfig.EnableReadWriteMetrics),
}
}

// TestTheDivergencesFromTheReaderAreTheRecordedOnes measures what the doc comments describe.
//
// The two storage sections declare defaults their readers do not produce for a file missing the keys,
// because most of those reads take no account of whether the key was present. Prose describing which keys
// those are cannot fail when it is wrong, and it was: it named four of the six store settings and one
// commitment setting that does not in fact differ, and missed the setting that selects how a node commits.
//
// So the set is measured here rather than described. A key that starts diverging fails this test, and so
// does one that stops: guarding a read means deleting its row, which is what makes the reconciliation
// something a change has to account for rather than something a comment claims.
func TestTheDivergencesFromTheReaderAreTheRecordedOnes(t *testing.T) {
reader := readerValues(t)
for _, mode := range registry.Modes() {
resolved, err := registry.Resolve(mode, registry.Sources{})
if err != nil {
t.Fatalf("mode %q: %v", mode, err)
}
recorded, named := theDivergences[mode]
if !named {
t.Fatalf("mode %q has no record here, so a mode was added and this was not revisited", mode)
}
listed := make(map[string]bool, len(recorded))
for _, key := range recorded {
listed[key] = true
}

var measured []string
for key, got := range reader {
declared, declares := resolved.Values[key]
if !declares {
t.Errorf("mode %q: %s is read by this package and no section declares it", mode, key)
continue
}
if fmt.Sprint(declared) == got {
if listed[key] {
t.Errorf("mode %q: %s no longer diverges, both sides being %v. Take it off that "+
"mode's list, so the list stays the set of keys installing this section changes",
mode, key, declared)
}
continue
}
measured = append(measured, key)
if !listed[key] {
t.Errorf("mode %q: %s declares %v and its reader produces %q for a file with no keys, and "+
"nothing records that. Installing this section changes what such a node runs. %s",
mode, key, declared, got, whyItMatters[key])
}
if want, stated := whatANodeRunsToday[key]; stated && want != got {
t.Errorf("mode %q: %s is recorded as producing %q and produces %q", mode, key, want, got)
}
}

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

// TestEveryKeyThisPackageDeclaresIsOneItsReadersFill holds the two lists against each other.
//
// The declared keys come from the schemas and the read keys from the map above, so a key on one side only
// is either a setting an operator writes that no reader fills, or one this package reads and nothing
// declares.
func TestEveryKeyThisPackageDeclaresIsOneItsReadersFill(t *testing.T) {
reader := readerValues(t)
for _, section := range []string{StateStoreSectionName, StateCommitSectionName} {
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)
}
}
}
}
Loading
Loading