diff --git a/app/config_register.go b/app/config_register.go new file mode 100644 index 0000000000..e7ab04ba8f --- /dev/null +++ b/app/config_register.go @@ -0,0 +1,194 @@ +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 the seid init command writes for a node of this kind, with one deliberate +// departure. +// +// 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. +// +// The departure is the retention an archive node keeps. The mode rules set it to keep everything and the +// command does not write that, because the type it renders declares a state store field of its own and +// fills it from the mode-blind default, so the rule is applied and then discarded. PLT-955 records that, +// and records the decision: pin what a node resolves today and correct it here, in the versioned +// declaration, rather than at the point that loses it. So this states the rule and the command states the +// value the rule was overwritten by, and the test beside this holds both, because a departure nothing +// measures is indistinguishable from an oversight. +// +// The declared values are also not what this section's reader produces for a file missing the keys, which +// is a different comparison and measured separately. +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, + }, + } +} diff --git a/app/config_register_agreement_test.go b/app/config_register_agreement_test.go new file mode 100644 index 0000000000..ca84b68182 --- /dev/null +++ b/app/config_register_agreement_test.go @@ -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) + } + } + } +} diff --git a/app/config_register_test.go b/app/config_register_test.go new file mode 100644 index 0000000000..7fe25c5c86 --- /dev/null +++ b/app/config_register_test.go @@ -0,0 +1,273 @@ +package app + +import ( + "reflect" + "sort" + "testing" + + "github.com/sei-protocol/sei-chain/config/registry" + "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/testutil/configtest" +) + +// manifestKeys returns the keys a section's read-site record names, plus any named here. +// +// The record is this package's own statement of which keys each reader looks up, kept for another purpose +// and held against a golden file. Taking the key set from it means a section's declaration is compared +// against something maintained under a different discipline, rather than against a list written beside it +// by the same hand in the same commit. +func manifestKeys(specs []configtest.KeySpec, also ...string) []string { + out := make([]string, 0, len(specs)+len(also)) + for _, spec := range specs { + out = append(out, spec.Key) + } + out = append(out, also...) + sort.Strings(out) + return out +} + +// requireDeclares holds a section's declared keys against the record of what its reader looks up. +func requireDeclares(t *testing.T, section string, want []string) { + t.Helper() + for _, defect := range registry.Defects() { + if defect.Section == section { + t.Fatalf("%s was refused, so none of its keys is declared: %v", section, defect.Err) + } + } + registered, ok := registry.Lookup(section) + if !ok { + t.Fatalf("%s is not registered, so nothing resolves its keys", section) + } + if !reflect.DeepEqual(registered.Keys, want) { + t.Errorf("%s declares\n %v\nand its read-site record names\n %v\nA key on one side only is either "+ + "a setting an operator writes that no reader fills, or one this package reads and nothing "+ + "declares", section, registered.Keys, want) + } +} + +// requireResolves holds a section's resolved values against what its reader's own defaults hold. +// +// Resolving renders every registered section, so a section elsewhere whose defaults cannot state a value +// for a key it declares fails here too. The registry names that section in the error, so the message +// points at the real one rather than at whichever test asked. +// +// Resolving is what to compare against rather than the registered struct, because the resolved map carries +// the key a tag produced and the value that tag's field held. A comparison of struct to struct agrees with +// itself while two tags sit on the wrong fields, since each field still holds the value the test names for +// it. The swap moves the value to the other key, and this notices. +func requireResolves(t *testing.T, mode registry.Mode, section string, want map[string]any) { + t.Helper() + resolved, err := registry.Resolve(mode, registry.Sources{}) + if err != nil { + t.Fatalf("mode %q: %v", mode, err) + } + for key, expected := range want { + if got := resolved.Values[key]; !reflect.DeepEqual(got, expected) { + t.Errorf("mode %q: %s resolves to %#v (%T), want %#v (%T)", + mode, key, got, got, expected, expected) + } + } +} + +// TestLightInvarianceDeclaresAndResolves covers the one section registered as the type its reader fills. +func TestLightInvarianceDeclaresAndResolves(t *testing.T) { + requireDeclares(t, LightInvarianceSectionName, manifestKeys(lightInvarianceKeys)) + for _, mode := range registry.Modes() { + requireResolves(t, mode, LightInvarianceSectionName, map[string]any{ + flagSupplyEnabled: DefaultLightInvarianceConfig.SupplyEnabled, + }) + } +} + +// TestGenesisDeclaresAndResolves holds the genesis schema against the record and the reader's defaults. +// +// The record names one of the two keys as a row and the other beside it, because that one is read as a type +// assertion rather than a guarded cast and a row would predict the wrong resolution. Both are this +// package's, so both are declared. +func TestGenesisDeclaresAndResolves(t *testing.T) { + requireDeclares(t, GenesisSectionName, manifestKeys(genesisKeys, flagGenesisImportFile)) + for _, mode := range registry.Modes() { + requireResolves(t, mode, GenesisSectionName, map[string]any{ + flagGenesisStreamImport: DefaultGenesisConfig.StreamGenesisImport, + flagGenesisImportFile: DefaultGenesisConfig.GenesisStreamFile, + }) + } +} + +// TestStateStoreDeclaresEveryKeyItsReaderResolves holds the schema against the read-site record. +func TestStateStoreDeclaresEveryKeyItsReaderResolves(t *testing.T) { + requireDeclares(t, StateStoreSectionName, manifestKeys(ssKeys)) +} + +// TestStateStoreResolvesWhatEachKindOfNodeNeeds is the mode-varying half of this section. +// +// Two of these settings mean something different depending on what kind of node asks, and the values are +// written out here rather than taken from the same rules the section reads. An archive node exists to keep +// history, so a retention that pruned it would be the one declaration here that destroys data, and it +// would do so with nothing to alert on, because pruning frees disk rather than filling it. +func TestStateStoreResolvesWhatEachKindOfNodeNeeds(t *testing.T) { + byMode := map[registry.Mode]struct { + enable bool + keepRecent int + }{ + registry.ModeValidator: {enable: false, keepRecent: 100000}, + registry.ModeSeed: {enable: false, keepRecent: 100000}, + registry.ModeFull: {enable: true, keepRecent: 100000}, + registry.ModeArchive: {enable: true, keepRecent: 0}, + } + for _, mode := range registry.Modes() { + want, named := byMode[mode] + if !named { + t.Fatalf("mode %q has no expectation here, so a mode was added and this was not revisited", mode) + } + requireResolves(t, mode, StateStoreSectionName, map[string]any{ + FlagSSEnable: want.enable, + FlagSSKeepRecent: want.keepRecent, + }) + } +} + +// TestStateStoreResolvesItsOtherValuesTheSameForEveryMode covers the ten settings a mode does not change. +func TestStateStoreResolvesItsOtherValuesTheSameForEveryMode(t *testing.T) { + live := config.DefaultStateStoreConfig() + for _, mode := range registry.Modes() { + requireResolves(t, mode, StateStoreSectionName, map[string]any{ + FlagSSDirectory: live.DBDirectory, + FlagSSBackend: live.Backend, + FlagSSAsyncWriterBuffer: live.AsyncWriteBuffer, + FlagSSPruneInterval: live.PruneIntervalSeconds, + FlagSSImportNumWorkers: live.ImportNumWorkers, + FlagSSReadWriteMetrics: live.EnableReadWriteMetrics, + FlagSSSnapshotEnable: live.SnapshotEnable, + FlagEVMSSDirectory: live.EVMDBDirectory, + FlagEVMSSSeparateDBs: live.SeparateEVMSubDBs, + FlagEVMSSSplit: live.EVMSplit, + }) + } +} + +// TestStateCommitDeclaresEveryKeyItsReaderResolves holds the schema against the read-site record. +// +// Twenty keys: the seventeen the record holds as rows, and three it names beside them because each has a +// target of its own. The four keys under this section's flat key-value name that only the Cosmos server's +// reader resolves are not among them, and are not this section's to declare. +func TestStateCommitDeclaresEveryKeyItsReaderResolves(t *testing.T) { + requireDeclares(t, StateCommitSectionName, manifestKeys(scKeys, + FlagSCWriteMode, FlagSCWriteModeEnableAuto, FlagSCHashLoggerTargetFileSize)) +} + +// TestStateCommitResolvesTheModuleDeclaredValues covers the value side of the same registration. +// +// The write mode is a plain string here because the reader parses a written name into its own type, and +// comparing values is what holds it to that: the named type carries the same text and is not the same +// value. +func TestStateCommitResolvesTheModuleDeclaredValues(t *testing.T) { + live := config.DefaultStateCommitConfig() + for _, mode := range registry.Modes() { + requireResolves(t, mode, StateCommitSectionName, map[string]any{ + FlagSCEnable: live.Enable, + FlagSCDirectory: live.Directory, + FlagSCAsyncCommitBuffer: live.MemIAVLConfig.AsyncCommitBuffer, + FlagSCSnapshotKeepRecent: live.MemIAVLConfig.SnapshotKeepRecent, + FlagSCSnapshotInterval: live.MemIAVLConfig.SnapshotInterval, + FlagSCSnapshotMinTimeInterval: live.MemIAVLConfig.SnapshotMinTimeInterval, + FlagSCSnapshotWriterLimit: live.MemIAVLConfig.SnapshotWriterLimit, + FlagSCSnapshotPrefetchThreshold: live.MemIAVLConfig.SnapshotPrefetchThreshold, + FlagSCSnapshotWriteRateMBps: live.MemIAVLConfig.SnapshotWriteRateMBps, + FlagSCHistoricalProofMaxInFlight: live.HistoricalProofMaxInFlight, + FlagSCHistoricalProofRateLimit: live.HistoricalProofRateLimit, + FlagSCHistoricalProofBurst: live.HistoricalProofBurst, + FlagSCWriteMode: string(live.WriteMode), + FlagSCWriteModeEnableAuto: live.WriteModeEnableAuto, + FlagSCHashLoggerEnable: live.HashLogger.Enable, + FlagSCHashLoggerDirectory: live.HashLogger.Directory, + FlagSCHashLoggerBlocksToRetain: live.HashLogger.BlocksToRetain, + FlagSCHashLoggerTargetFileSize: live.HashLogger.TargetFileSize, + FlagSCHashLoggerMaxDiskSize: live.HashLogger.MaxDiskSize, + FlagSCFlatKVReadWriteMetrics: live.FlatKVConfig.EnableReadWriteMetrics, + }) + } +} + +// TestStateCommitWriteModeDefaultIsOneTheReaderAccepts covers the one declared value that is parsed text. +// +// Every other declared value is used as it stands. This one is a name the reader turns into a mode, so a +// default nothing parses would put a value in a generated file that stops the node it was generated for. +func TestStateCommitWriteModeDefaultIsOneTheReaderAccepts(t *testing.T) { + for _, mode := range registry.Modes() { + resolved, err := registry.Resolve(mode, registry.Sources{}) + if err != nil { + t.Fatalf("mode %q: %v", mode, err) + } + declared, ok := resolved.Values[FlagSCWriteMode].(string) + if !ok { + t.Fatalf("mode %q: %s resolves to %T, and the reader parses text", + mode, FlagSCWriteMode, resolved.Values[FlagSCWriteMode]) + } + if _, err := config.ParseSCWriteMode(declared); err != nil { + t.Errorf("mode %q: %s resolves to %q, which this binary's own reader refuses: %v", + mode, FlagSCWriteMode, declared, err) + } + } +} + +// TestTheSectionsThisPackageRegistersAreUsable covers what the registry refuses. +// +// Scoped to the four names this file registers. The whole-registry sweep belongs where every section is +// linked, because a refusal that depends on what else registered is not this package's to answer for. +func TestTheSectionsThisPackageRegistersAreUsable(t *testing.T) { + mine := map[string]bool{ + LightInvarianceSectionName: true, + GenesisSectionName: true, + StateStoreSectionName: true, + StateCommitSectionName: true, + } + for _, defect := range registry.Defects() { + if mine[defect.Section] { + t.Errorf("%s was refused, so none of its keys is declared: %v", defect.Section, defect.Err) + } + } +} + +// TestTheArchiveRetentionDepartsFromWhatTheCommandWrites measures the one deliberate departure. +// +// A declared value is what the seid init command writes for a kind of node. This section departs from that +// in exactly one place: the retention an archive node keeps. The mode rules set it to keep everything, and +// the command does not write that, because the type it renders declares a state store field of its own and +// fills it from the mode-blind default, so the rule is applied and then thrown away. +// +// PLT-955 records the defect and the decision to correct it in the versioned declaration rather than at the +// point that loses it. So the departure is intended, and it is held here for two reasons. It fails if the +// command starts writing the rule, which is the day this departure should be deleted. And it fails if this +// section stops departing, which would put a retention on the one kind of node whose purpose is keeping +// what it would prune. +func TestTheArchiveRetentionDepartsFromWhatTheCommandWrites(t *testing.T) { + live := config.DefaultStateStoreConfig() + + // What the command renders for an archive node: the mode rules are applied to the server + // configuration, and then the type it renders fills its own state store field from the mode-blind + // default, which is what reaches the file. + written := live.KeepRecent + if written == 0 { + t.Fatalf("the mode-blind default retention is already zero, so this departure measures nothing " + + "and the comparison below holds for any declaration") + } + + resolved, err := registry.Resolve(registry.ModeArchive, registry.Sources{}) + if err != nil { + t.Fatalf("%v", err) + } + declared := resolved.Values[FlagSSKeepRecent] + + if declared == written { + t.Errorf("%s resolves to %v for an archive node, which is what the command writes. Either the "+ + "command now carries the mode rule, in which case this departure and its note should go, or "+ + "this section stopped departing and an archive node is declared to prune the history it "+ + "exists to keep", FlagSSKeepRecent, declared) + } + if declared != 0 { + t.Errorf("%s resolves to %v for an archive node, want zero. The mode rule keeps every version, "+ + "and departing from the command is only defensible while this states that rule", + FlagSSKeepRecent, declared) + } +}