[ConfigManager] Read sei.toml at Boot - #3999
Draft
bdchatham wants to merge 41 commits into
Draft
Conversation
Four sections, each registered by the package that owns its struct, so the struct, the values and the keys come from one place and cannot drift apart. The keys derive from the mapstructure tags, which is what makes the registry's spelling and each reader's own constants the same strings, and each package's test holds the two against each other rather than against a written-out list. admin_server 2 keys giga_executor 2 keys receipt-store 6 keys wasm 3 keys Three register their own struct. wasm needs a schema, because the upstream type carries no mapstructure tags at all, so keys derived from it would be field names rather than the ones the module reads. Its simulation gas limit is text because the field it stands for is an optional number and absent is a meaning of its own: unset means the consensus block gas limit applies, which no number can say. Two of that type's settings declare nothing, one having no key any reader resolves and the other written into app.toml by the template and read by nothing. Registering the receipt store needed a distinction the registry did not draw. mapstructure reads a tag of "-" as skip this field, and a configuration struct uses it for a field something else assigns: KeepRecent comes from the global min-retain-blocks flag at the app layer, ExternalPruning from whatever constructs the garbage collector. The registry read that as a missing name and refused the whole section. Such a field now declares no key, which is narrower than declaring one that resolves to a default and safer for the same reason: a declared key is written at override precedence, so a default would land on top of the value that code assigned, and a node with min-retain-blocks set would silently keep nothing. A field with no tag at all stays a defect, because that is the opposite intent, a key nothing names reaching no field. The skip lives in tagOf, which both walks already share, so the declared keys and the rendered defaults describe the same fields. Reverting either walk's skip on its own fails a test. The recorded configuration surface does not move: nothing consumes the registry yet, and no golden changed. 100% of statements in config/registry, race clean. Three mutations each fail a named test: refusing a dash again, skipping an untagged field, and letting the two walks disagree.
Four sections owned by the app package, each registered where its struct lives. genesis 2 keys light_invariance 1 key state-commit 20 keys state-store 12 keys light_invariance registers the type its reader fills. The other three declare a schema, because keys derived from the type the reader fills are not the keys the reader looks up. The genesis import type carries no mapstructure tags at all. State store and state commit both tag their fields with something other than the name resolved from configuration, and state commit nests its settings under three inner structs while the keys are flat names on the section, apart from the one flat key-value setting that has a segment of its own. State commit's write mode is text rather than the reader's 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. Each section's test holds its resolved keys and values against the reader's own constants and its own defaults. Resolving is what it compares, 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. Putting the genesis tags on each other's fields leaves the key set identical and fails the test. State store's declared defaults are not what its reader produces for a file missing those keys. It starts from the declared defaults, then assigns eleven of its twelve fields straight from a lookup with no check that the key was present, so an absent key casts to a zero and clobbers the default beside it: the store reads as disabled, with no backend, keeping every version, and committing synchronously. Only the snapshot key is guarded, and its own comment at the read says why. A node whose app.toml predates one of the other keys therefore runs the clobbered value today and the declared default once something installs this section, and guarding the remaining reads is what makes those the same thing. The recorded configuration surface does not move, because nothing consumes the registry on a boot path yet.
Four sections, each registered by the package that owns its struct. eth_blocktest 2 keys eth_replay 4 keys evm 57 keys evm_query 1 key All four register the struct their reader fills. None needs a schema, because in each of them the mapstructure tags already spell the keys the reader looks up, so the registry derives what a node reads and nothing restates a list of fifty-seven keys. Each package's test holds the derived keys against the reader's own constants, which are the second statement of the same set in the same file: a rename that moves one and not the other compiles. The EVM section resolves the same values for every mode. Nothing consults a node's kind while reading these keys, so a file missing them serves both interfaces whatever kind of node it is, and that is what these resolve to. A node seid init provisioned is the other case and needs nothing here, since that path writes the two interface toggles per mode and a written value is what resolves. Two of that section's values come from the machine rather than from a decision. The simulation call limit is the processor count and the worker pool is twice it, capped, so they describe whichever host resolved them. That is stated where they are declared, because a caller rendering them into a file carries one host's sizing to whatever reads that file next. Replay declares three of its four keys under the name the template writes and one under a different one. The template renders eth_replay_contract_state_checks and the reader looks up contract_state_checks, so every generated file already carries a name nothing resolves. The declared key is the one a value reaches a reader through, and a test refuses the other: declaring it would add a key an operator can set and no reader answers, which is worse than the mismatch, because the value would look as though it applied. The recorded configuration surface does not move, because nothing consumes the registry on a boot path yet.
Five sections whose keys belong to the Cosmos server, and the two registry capabilities they need. api 8 keys base 14 keys grpc 11 keys state-sync 3 keys telemetry 7 keys These sections have no owning package here. Their structs and their readers live in sei-cosmos, which this repository vendors rather than authors, so there is nowhere upstream to put a registration this registry would see. Four of the five register the upstream struct directly, because its mapstructure tags already name the keys the reader resolves. A section can now declare keys at the root of the file. The node-wide settings are written at the top of app.toml and read as pruning and halt-height, with no segment in front, so a section carrying a name into every key would rename all fourteen and an operator's existing file would reach none of them. A section therefore has a name it is looked up by and a prefix its keys carry, and for a root section the prefix is empty. Both walks build a key through one function, so a root key gains no separator on either side; reverting either one on its own fails a test, the value walk through the check that a rendered default states one value per declared key. Two keys can now collide where two prefixes never could. A key two sections both declare has one default rendered over the other, and which one depends on the order the sections are walked. And a root key that is also a section's name cannot be written at all, because a file holding both a value for that name and a table under it is not valid TOML, so one of the two is unreachable and nothing says which. Both are refused, in either registration order. A section can now say that an environment variable cannot supply one of its keys. The metric label set is a list of name and value rows and its reader asserts that exact shape rather than casting what it finds, so no single string satisfies it, and the assertion is the first statement of the whole server configuration. A resolved variable would install a value that stops the node; leaving the channel out means the file's value applies and the node runs. The reason is required rather than optional, because an operator whose variable is ignored has to be told why, and a refusal with no reason is itself refused. The metric section is the one here that needs a schema, and for one field's shape rather than for a spelling. Its label set is declared as untyped rows to match what the reader takes. A test holds every other field to the upstream field's name, tag and type, and holds the count of differing types at one, so a second divergence is a failure and a converged upstream type leaves the schema with nothing to justify it. Nothing here varies a default by mode. seid init writes the two interface toggles and the block retention per mode, so a node it provisioned carries those as written values, and these are what a node with nothing written runs. One declared value is not what a running node uses, and it is worth knowing which. The pruning strategy is declared as keeping everything, while the command line registers a flag of the same name defaulting to the standard strategy, and a bound flag is a source of its own below the file. A node started with no pruning key written prunes on the standard schedule. Whoever resolves for a running node has to supply the flag values to get the answer that node uses. The recorded configuration surface does not move, because nothing consumes the registry on a boot path yet.
The rule was right and the reason recorded for it was not, which matters because it is the reason the remaining sections will cite. It said a declared key would be written at override precedence and land on top of the value that code assigned. That cannot happen for either field it named: the app layer assigns the receipt store's retention after the reader has returned, so the assignment is last and wins. It also said such a node would silently keep nothing, and the field's own comment says the opposite, that keeping zero versions means keeping everything. An operator handed that sentence during an incident looks for missing receipts and finds a full disk. The reproducible reason is the one every other refusal here rests on. A key for such a field is one an operator can write that the assignment then discards, so it reaches no field. A field with no tag stays a defect for the same reason read from the other end, because it would declare a key derived from a field name and no operator writes that. The two look alike in a diff and mean opposite things, which is why the package's own contract now states the distinction rather than leaving it in a comment beside one branch. Two of the four sections held their declared keys against a written-out list of the same strings. That is a second statement of the key set, which is what a section exists to remove: a tag and the list move together and the reader keeps asking for the old spelling. Both now hold against the constants their reader passes to Get. The admin server's reader was spelling its two keys inline, so it has constants for them now, and its registration no longer recites the derived keys in prose that drifts the moment a tag moves. One assertion is gone because it could not fail, the exact key set having been compared three lines above it. Two declared values are also stated elsewhere in the binary, and each now says so where it is declared. The wasm query gas limit resolves to ten times what the template writes into a generated file, so a node provisioned by the binary runs the smaller number and a node whose file predates the section runs the declared one; whoever renders declared values into a file has to decide which survives, and that limit bounds the work one smart query can ask of a node serving queries to anyone. The receipt store's database directory resolves to an empty string, and the emptiness carries the meaning: the app layer fills it from the host, keeping the former path for a node that already holds the store there. A path written into a file is one host's answer and names an empty directory on another. One comment said the contract debug switch has no key any reader resolves. It is read from the node-wide trace flag, so its key belongs to the root of the file rather than to that section, which also means the section's three keys do not determine the configuration the module ends up with. A new test asks the whole set at once. Two refusals depend on what else has registered, neither is visible from inside either section, and the section that loses is dropped whole with every key it declared. Registering a section whose key collides with the receipt store's leaves all four section suites green and fails only this one.
Two of the state store's settings mean something different depending on what kind of node asks, and this section answered the same for all four. An archive node exists to keep history, and it was declaring a retention of a hundred thousand versions. The binary already says otherwise, in the mode rules it applies when it writes a file: an archive node keeps everything, and a validator and a seed run with the store off. Those rules are now where this section's answer comes from, so a change to them moves this too. That is the correction the pinned defect asks for. The record of it says to pin how configuration resolves today and correct it in the versioned manager, and this registry is the versioned manager, so declaring the rendered value would have pinned the defect a second time in the place meant to fix it. Nothing runs differently yet, because nothing consumes the registry on a boot path, and the direction matters more than the timing: state store pruning deletes inside the store on a timer, the archive volumes are protected against being deleted rather than against being emptied, and pruning frees disk so nothing that watches disk would fire. The rest of this change replaces prose with measurement. The declared values for the two storage sections are not what their readers produce for a file missing the keys, and the comment describing which keys those are was wrong three ways: it named four of the six store settings, missed two, named a commitment setting that does not in fact differ, and missed the one that selects how a node commits. The write mode is read through a presence check and then rewritten unconditionally, so a node with nothing written commits in the derived mode rather than the one that key carries. A comment cannot fail when it is wrong. So the set is measured against the readers now, per mode, and recorded as data: 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. Each key set is also held against this package's own read-site record, which is kept for another purpose and held against a golden file, rather than against a list written beside it in the same commit. The record spells its keys with the reader's constants and the section derives them from tags, so a rename on either side alone fails. One comment said the other keys under the commitment section's flat key-value name have no reader. Four of them are read by the Cosmos server's own reader, so they belong to whoever registers that section, and saying they reach nothing would have closed the door on declaring them. The whole-registry defect sweep here is now scoped to the four sections this file registers. A refusal that depends on what else has registered is not this package's to answer for, and the sweep that covers it lives where every section is linked.
Four comments said more than their subject. The light-invariance default now states the mode decision and leaves what the check compares to the check. The genesis schema says why it exists and leaves what holds it to the test that holds it. The helper that resolves says once that it renders every section, so a failure naming another one is read correctly. And the write-mode default is asked for every mode rather than one, which is how the tests beside it ask.
The failure messages for an excluded field still claimed the mechanism the comment above them no longer does: that a declared key would land over whatever assigns the field. They now say what is true, that such a key is one an operator can write and the assignment discards. The half of that test covering an untagged field asserted only that some refusal was recorded, so it passed on a refusal raised for any other reason. It now requires the message to name the field, and changing the name it looks for fails it. The registry's contract adds the one thing its new rule leaves unsaid: the exclusion tag is meaningful only on an exported field, because an unexported one carrying any tag is refused before the tag is read. The giga executor's default says why this section does not vary by mode without stating it as a rule for every section, since another section in this work does vary and the binary is what decides which. The fixture in the registry's own spec no longer describes that package as one that would register, because it now does. The wasm section's defaults function takes a name that does not collide with a local of the same name elsewhere in that package, and its test asks every mode rather than one.
Three of these settings mean something different depending on what kind of node asks, and all five sections answered the same for every one. The binary already states the rules, in what it applies when it writes a file, and every section here now answers through them. Each of the three matters in a different direction. A full node and an archive node exist to serve queries, and both interfaces that serve them were declared closed. A validator is meant to expose as little as it can, and gRPC was declared open on every one of them, which is the opposite of what the rule beside it says it is for. And the number of blocks a node retains was declared as keeping everything for a full node, where the rule prunes at a hundred thousand. The rules are read rather than restated, so one added later moves these sections with nothing here changing, and the test writes the three values out by kind of node so a change to the rules fails and gets looked at. Resolving every mode as a validator, opening gRPC on a validator, and changing the retention each fail it. The two sections no rule touches answer through the same function, so there is one place a mode is applied rather than a decision per section about whether to apply it.
…aces per mode A resolved list was the section's own list. A section's default is usually a package-level variable, so a slice field handed out the array that variable holds, and one in-place write by a caller rewrote it for the whole process: every later resolution, and every reader that copies the same struct. Two of the five lists this reaches are deny lists, so the rewrite is silent and it is a security control. The registry already copies a section's keys for exactly this reason and said so in a comment; values now get the same guarantee, at the one function both walks pass through. Removing it fails a test that writes into a resolved list and asks the section's default what it holds. The EVM interfaces answer per kind of node. A full node and an archive node serve queries, which is what those interfaces are for. A validator and a seed serve none, and this section declared both open for every kind, which puts a public request surface on the node that holds a signing key. The rule already exists in this binary. It could not be read from here, because the package that owns the node mode imports this one, so the rule moved to the registry, which is a leaf both sides reach, and the node-mode type now delegates to it rather than stating it twice. Forgetting archive in that one statement now fails a test. Each section also holds its keys to the values their fields hold, not to its own defaults struct compared with itself. That comparison agreed with itself while two tags sat on the wrong fields: the key set stays identical and every field still holds the value it always did, so an endpoint and a directory, or a deny list and an origin list, change places unnoticed. Both of those swaps now fail. Each section reports its own refusal. A registration the registry cannot use is recorded rather than raised, and these tests inferred it from a lookup coming back empty, which threw away the sentence saying why. Two tests are gone because they restated checks that already exist a few files away, where the message is better. Two comments are corrected: enabling replay opens a client without reaching the endpoint, so an unreachable one surfaces during replay rather than at startup, and the two machine-derived values are not one case, because the worker pool re-measures when it is given a value that is not positive and the simulation limit reads zero as no limit at all. The registry now states what a resolved value's type depends on, because it resolves values and does not convert them: a default arrives as its field's type, a file as whatever the format decodes to, and an environment variable as one string.
…d nothing A resolution answered for any string. A section's defaults answer per mode, and a mode this package does not know reached whatever each section does with an argument it cannot match, which for these five is the rules answering as though it were a full node. So an empty string, a capitalised name, or one with a trailing space resolved the interfaces a full node serves onto whichever node asked, with no error. It is refused now, naming the four. A refusal of the environment channel is recorded by a key, so a slip in the spelling named a key no section declares. The channel would never have offered that key, so the refusal covered nothing while reading as though it did, and the key it was written for went on resolving from a variable. Both sets exist for the first time when something resolves, because a refusal may be recorded before the section declaring its key registers, so that is where they are compared. The reason a refusal carries is required and had no consumer. The channel was skipped before the variable was read, so the one fact a diagnostic needs, that an operator set it, was discarded at the cheapest possible point. The variable is read now and its value still thrown away, and the key comes back named, so a required reason is one somebody can be told. A refused key nobody set is not reported, because a value nobody chose is not news. A refusal also names the section that declares the key, so a refused key is attributable the way every other defect is. It was putting the key where the section belongs, which made a defect read as though a key had registered and made a scoped sweep skip it. Four of the metric section's seven hand-copied values were held against nothing. That is the one section here that has to restate its values, so it is the one where a field can be assigned from its neighbour, and assigning the hostname toggle from the enabled toggle survived the suite. Every one of the seven is now held as the key it resolves to rather than as a struct field, because a struct compared with itself agrees while two values sit on the wrong fields. Five comments said things the code does not. The node-wide settings claimed to be unchanged by mode while one of their own keys answers per mode. A count of non-zero defaults was wrong. Two different counts of six read as one, and the pair the sentence lost is read through a clamp that does nothing for an absent key. The package's reason for existing named a vendored tree, when other sections register inside one and the real obstacle is an import edge. And a paragraph named two sections that belong to another change.
Four statements in the package contract described the previous shape. A second entry point exists, for the settings written at the top of a file rather than inside a table, and the contract showed one. The list of what makes a registration unusable no longer enumerated: two sections declaring one key and a top-level key sharing a section's name both became possible once a key could sit at the root, and a refusal of the environment carrying no reason is refused too. The resolution order had gained a per-key hole in one channel and did not say so. And the first step of adding a section told an author to use the name as the first segment of every key, which is false for a section whose keys have none. A mode this package does not declare is also refused now, and the contract says that where it says a default answers per mode.
refuseOverlap refused two collisions and only one of them could happen. Two sections declaring one key is already refused by the environment check, which two identical keys reach by answering to one variable, so that arm was a second guard on a case already covered. The other arm, a key at the top of the file sharing a section's name, was the only one it alone caught, and it has no instance: one section declares keys at the top of the file and none of its fourteen names is a section's. So the code goes and the fact stays. The contract names the collision among the things this package does not guard, with what makes it reachable, because a second such section is where it starts to matter. The prototype found that out by hand: it named the section holding config.toml's top-level keys after the file rather than after the node, because the client file declares a top-level key called node and a node section could not have coexisted with it. The one case the removed guard described better is named better now where it is still refused. Two identical keys were being reported as two spellings of one environment variable, and the reason a dot and a hyphen are the same character to the environment is not the reason a key collides with itself.
…sures where a node differs These sections said their values were what a node with nothing written resolves. They are not, and the difference was carried in four paragraphs of prose with one of the counts wrong. What they are is what seid init writes for a kind of node: the upstream defaults with the binary's own mode rules applied is exactly the pipeline that renders a generated app.toml, so a declared value is what that file would have held. That is a claim about a real pipeline in this binary rather than a judgement, so it can be held, and it is what a caller writing a configuration file wants. Where a node with nothing written resolves something else is now measured. The reader is driven with the start command's flags bound, the way a booting node binds them, because seventeen of these keys are also flags and a flag's registration default is what an absent key reaches before the lookup comes back empty. Twelve keys differ, and the measurement corrected the prose twice over: seven keys the paragraphs implied differ do not once the flags are bound, and the gRPC toggle does, which no paragraph named. Of the two interface toggles it is the only one that diverges, because its flag defaults the interface on while a generated validator file writes it off. A key that starts diverging fails, and so does one that stops, so guarding a read has to account for its row. Dropping the flag binding fails it too, which is what keeps the record measuring what a node gets rather than what the reader says in isolation.
This binary writes an app.toml two ways and they disagree on four keys. The provisioning command applies the mode rules and renders the result; a node starting without a file runs a second pipeline that applies no mode rules at all and carries overrides of its own. So it writes the standard pruning strategy where the command writes keeping everything, a metric retention of sixty against seven thousand two hundred, the REST interface on for a validator against off, and a pruning interval drawn at random on every run. A declared value follows the command an operator runs to provision a node. That was already true and the comment said only that seid writes it, which is ninety per cent of a fact.
A declared value is what the seid init command writes for a kind of node. This section departs from that once: 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, which is to pin what a node resolves today and correct it in the versioned declaration rather than at the point that loses it. So the departure is intended, and it is now held rather than asserted. It fails if the command starts carrying the rule, which is the day the departure should be deleted. It fails if this section stops departing, which would declare a retention on the one kind of node whose purpose is keeping what it would prune. Both directions are checked, because a departure nothing measures cannot be told from an oversight.
These four said their values are what a node with nothing written resolves. They are what the seid init command writes for a kind of node, which is a different statement and a checkable one: that command applies the same mode rule to this section's own defaults and renders the result, and for the EVM section it passes what it applied through rather than refilling it from a mode-blind copy, so a declared value here is the value that reaches the file.
…de runs The section declared this module's own default and the command writes a tenth of it, so the two disagreed by a factor of ten on the only bound on the work one smart query can ask of a node that serves queries to anyone. Declaring the larger one meant a caller rendering a file from these values would loosen that bound, and a test named for the module's default invited exactly that reading. The number now lives once, beside the section, and the command reads it. So a declared value is what reaches an operator's file, and the generated file is unchanged, which the command's own characterization suite confirms. Raising it to the module's default fails a test rather than silently widening a public surface. The module's default is still what a node whose file carries no wasm section resolves. That is a different question, and it is held as one. Three other sections say what their declared values are rather than what a node with nothing written resolves.
# Conflicts: # config/registry/doc.go
A node that selects this configuration manager reads sei.toml, resolves it against the binary's declared defaults, and installs the result into the source the rest of the boot reads. Selecting the manager is a switch rather than a configuration change: a node with no sei.toml, an unreadable one, or one naming a mode this binary does not declare defaults for installs nothing and every key reads as it always has. Only the keys something other than the defaults supplied are installed. A resolution answers for every declared key, so installing all of it would write a default over whatever an operator's app.toml holds for each of the hundred and fifty keys their sei.toml does not mention. A key reaches the node exactly when a source supplied it. The flag snapshot is taken at the entry to Apply, before the handler that copies configuration values into flags and marks them changed. After that runs, a flag an operator typed and a key their app.toml holds cannot be told apart, and a flag layer built from that state would put app.toml above sei.toml. Two things nothing else reports are logged: a key the file writes that no section declares, and an environment variable set for a key the environment cannot carry. Resolve now reports an undeclared name for the file only. A node is started with flags that were never configuration keys, so pooling all three sources warned about thirty-nine working flags on every boot with the file's one real typo somewhere inside the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tagOf carried its rationale inline, which meant the steps were never named. Each condition that needed explaining is now a predicate whose name says why it is there, and the rationale moved to that predicate's doc comment. The tag excluding a field from configuration and the spelling no written key can match are the two that carried comments. Splitting the tag into a name and the squash option is a step of its own, so the body reads as a sequence rather than parsing in place. Behaviour is unchanged and no test moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ions The node's own configuration file carries 141 keys and no section declared any of them. These two are the first, and the registry needed two things before they could be. A section can now leave out a path the struct carries. Two kinds of field earn it. One a reader refuses outright, where writing the key stops the node, so declaring it would put a setting in the space whose only effect is an outage. And one whose absence is itself the setting, where a default would be this package inventing one. An exclusion naming no field the struct carries is refused, because the field it named can be renamed away and leave the exclusion reading as a deliberate omission while excluding nothing. A field that collects what the decode matched no field for now declares no key. What lands in it is what an operator misspelled, so giving it a key would offer the collector itself as a setting to write. Neither the package defining these settings nor the package deciding them can register them: the struct belongs to the node's configuration package and the rules that vary it by node kind live in the parameters package, which imports that struct. So a third package does it, the same shape the upstream server sections already use. Four keys vary by node kind and a test holds all four by value. Two are listen addresses, so a rule that stopped varying would leave a validator binding the address a query-serving node binds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither varies by node kind. How long a node waits at each step of a round has to agree across the validator set for the set to reach a decision, and what a node holds before a transaction is decided is a limit on its own memory. The consensus struct carries fifteen fields the node removed as settings, and it marks each one deprecated. They are excluded: declaring one would offer a key that changes nothing about how the node runs. So the section declares nine of its twenty-four paths. The reader has a check that names the removed settings an operator wrote, and it reaches eight of the fifteen. Six are durations or booleans, where a written zero and an unwritten field hold the same value, so no check can tell them apart. One more it omits. Nothing calls the check in any case. A test holds which eight it reaches, so making it complete fails rather than leaving the count stale, and a second test holds every exclusion to the struct's own deprecated marking rather than to that check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
State sync, the transaction index, instrumentation, the signing key paths and self remediation. That is every table in the node's own configuration file. The transaction index varies by node kind: a node that serves queries indexes transactions so it can answer them, and a validator and a seed serve none, so they index nothing and keep the write. State sync leaves one path out. The servers to fetch a snapshot from are the operator's own peers, so there is no value to inherit, and an address written here would name a host this binary cannot know about. A test now walks the section names this package owns rather than a list kept beside them, so the next section is covered by registering it. Another asserts the registry refused nothing, which is the check no single section can make: two of the refusals depend on what else has registered, and a section that loses is dropped whole rather than reported by itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourteen keys sit at the top of that file with no table of their own. They are declared against a schema rather than the node's top-level type, because that type carries the nine tables as well and declaring against it would declare every table's keys a second time. The schema squashes the same base group that type squashes, so those spellings still come from the node's own tags, and it restates the two fields held beside that group. A test holds those two to the type they came from by name, tag and type, and holds the count, so a third one appearing there fails rather than going undeclared. Two paths are left out. The home directory is where this file is found, so a value inside it would be the file naming its own location, and the command line already carries it. The node mode is the fact the file states at the top under its own name, and a second spelling would let the two disagree, with the resolution answering for one and the node reading the other. A test also checks that no root key is another section's name. Nothing refuses that collision, and the two settings it produces cannot both be written because no file holds a value for a name and a table under it. This is the first change to declare root keys beside another package's, so the check lives here until it has somewhere better to be. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five of these sections carry a root directory field tagged the same as the key at the top of the file, and the node fills every one of them from the command line after the file is read. So the file never carries the value and each section states the empty string for it. Declaring it hands whatever delivers these values an empty root to write over a running node's, and a node that cannot find its data directory, its genesis file or its signing key does not start. Two of the five are here. The test checks every registered section rather than the five, so a section added later that carries the same field fails rather than shipping the same hole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # config/tendermintbase/tendermintbase.go # config/tendermintbase/tendermintbase_test.go
…ory out Both carry the root directory field the node fills from the command line after the file is read, so both stated the empty string for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last of the five sections carrying the root directory field the node fills from the command line after the file is read. The signing key section is the one where a blank root is worst: a node that cannot find its key does not sign. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # config/tendermintbase/tendermintbase_test.go
# Conflicts: # config/registry/spec_test.go
A value written for one of those sections resolved correctly and reached nothing. The boot's handler reads that file once into a struct before the install runs, and the node reads the struct, so a value put into the source afterwards was ignored in silence. Those sections are now decoded into that struct instead, which is the mechanism the handler used and therefore the same casts, tags and hooks. A section declares which of the two ways its reader takes, and the sections of this file declare it in the same loop that registers them, so one cannot be registered without being delivered. A section that disagrees fails a test rather than quietly changing nothing. Four properties hold it. Only what a source supplied is decoded. The struct already holds what the node's own file said, so delivering a default over it would replace an operator's file with one nobody chose, on every boot, for every key their sei.toml does not mention. A section at a time. A decode is all or nothing for whatever it is handed, so one refused value would otherwise cost every key in the file rather than the keys of the section it appeared in. Decoded into a copy and published by replacing it. A decoder gathers errors and keeps going, so a refused value partway leaves its target holding some new values and some old. The copy is of the configuration the node already has and not a fresh one, because what a decoder writes can depend on what its target already holds, and only a copy holds the same things. It shares nothing: a decoder writes a list into the array its target holds, so one shared list would edit the original. The log level is applied from the resolution before any of this. A refusal is reported at a level an operator may have raised the threshold above, so waiting for a successful decode would mean the one setting somebody changes to see a refusal is the setting a refusal suppresses. Keys nothing in this binary writes are not declared. A cluster controller resolves a peer set from live discovery and patches the addresses in, a node computes a trust height and hash from the chain tip each start, and a moniker is stamped per instance. A value from here would be decoded over whichever already ran, with the change visible only in memory. Two keys that no longer have any effect and two that exist to make a node misbehave are left out as well. A field holding an interface can no longer be declared. What a decoder writes into one depends on what the field already holds, so a rehearsal in a copy would answer for the copy. Every delivered key that moved is logged with what it moved from. The node's own file still says what it said, and every tool an operator reaches for reads that file, so this is the only place the two can be told apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A typed flag now reaches the key it carries. A flag's name and that key are not always spelled the same: the node's own flags separate words with an underscore where the tag they decode through uses a hyphen. Compared as strings such a flag looked like a name nothing declares, so it was dropped and the file won over the command line, which inverts the one channel an operator reaches for during an incident. Matched through the environment spelling instead, where a dot, a hyphen and an underscore are one character. The registry already refuses to let two declared keys share that spelling, so a flag matches at most one key. A length of time written as a plain number is refused. The file format has no way to say how long something is, so a length of time is written as text with a unit. A plain number decodes cleanly and reads as nanoseconds, so sixty means sixty billionths of a second and the node starts. Nothing later objects, because nothing later can tell. The delivery is the only place that can: the resolution sees a number and a key, and only the struct says the key is a length of time. The two writers of this file are measured against each other. A declared value is what the init command writes for a kind of node, and that command is not the only thing here that writes the file: a node started without one gets it generated by the boot. They disagree on four keys, and the record says which and what a node gets instead. Two were predicted and two were not. The profiling address does not diverge after all, because the boot assigns it and a bound flag's empty default overwrites it inside the same function. The transaction indexer does, because the boot's writer applies none of the rules that vary a setting by kind of node, so a validator that let it generate this file indexes every transaction. A command answers, without starting a node, whether this binary can use a sei.toml. A boot may not refuse a file, so every value it cannot use is a report on a node that has already restarted, and a fleet rolling a change forward reads it after the change is on every node. The same questions have exact answers beforehand: the file, the binary and the environment are the whole input. Run it first and a mistyped value costs a failed check rather than a restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two reviews ran this code rather than read it. Seven things it got wrong. A log level resolved here beat one exported in the environment. The logger reads a variable of its own before any of this runs, under a name that is not the one this key answers to, and the boot's handler steps aside when it is set: a flag beats it and a file does not. Applying regardless put the file above it, so an operator who exported a level and adopted this file found the level ignored. A typed flag still wins, which is the order that was already there. This manager's own reporting is held at a level its reports survive. One level covers every logger in the process and an operator writes it, so a fleet that runs its nodes quiet silenced every report: what was applied, what moved, what was refused. Measured, a value was delivered and nothing was said about it. The floor goes on after anything that sets a level, because both setters reach every logger. A length of time written as zero is applied. It was refused as a plain number, and the reason a plain number is refused is that it means nanoseconds and is out by a factor of a billion. At zero there is no factor: several of these settings document zero as the way to turn them off and three declare it. An operator writing it lost every other key in the section. A negative number is refused where the setting cannot hold one. Minus one is how an operator says no limit in most software they have used, and the decoder wraps it to the largest value the field has, so the ceiling on connected peers stopped bounding anything and a window in seconds became centuries. The report of a key nothing declares sat one line above the level, so a file that raised the level to see its own mistakes still could not. The reader that names what moved swallowed a failure, so a section it could not read at all was reported as a section whose values all matched. A bounded report could drop any key, in alphabetical order. The bound guarded against a file filling a boot log, which a list bounded by its own section cannot do. Two comments an operator reads named the wrong mechanism, and one row of the divergence record gave the wrong reason for a real difference. A disagreement about what kind of node this is is now reported. Two files state that under different names, and nothing compared them, so a node resolving a validator's values while running as a query-serving node read correctly in every report about it. Reported rather than corrected: what kind of node this is gets decided when it is provisioned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A declared value is what a provisioning command writes for a kind of node, not what any particular node runs, so delivering one would replace a setting an operator never mentioned on every boot. Both deliveries avoid that by narrowing to the keys a source supplied, and each does it in its own function. That makes it a rule three call sites remember rather than one a single function enforces. Until the narrowing has one home this is the guard: boot with a file supplying one key and assert nothing else moved, in either delivery, for every kind of node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## plt-775-node-sections-4 #3999 +/- ##
===========================================================
+ Coverage 57.61% 57.70% +0.09%
===========================================================
Files 2230 2236 +6
Lines 187690 188421 +731
===========================================================
+ Hits 108134 108725 +591
- Misses 69689 69788 +99
- Partials 9867 9908 +41
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
The same rename as its sibling. This record holds what the boot's own writer produces for a node that arrives without a configuration file, so it says that, and the explanations beside it are reasoning. Named for that writer rather than for the manager, because the two records measure different things: one is what the manager this replaces resolves, this is what the second of the binary's two writers generates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
A node that selects this configuration manager reads
sei.tomland runs what it says. Untilnow the file was declared and nothing consumed it.
Two deliveries, because the two halves of a node's configuration are read differently and only
one of them can be delivered by putting a value into the source the boot builds.
Keys a reader looks up one at a time are installed into that source. These are read when
the app asks, which is after this runs.
Keys a reader decodes whole are decoded into the struct instead. The boot's handler reads
the node's own configuration file once into
tmcfg.Configbefore this runs, and the node readsthat struct, so a value put into the source afterwards reaches nothing. Measured before
building it: with the value installed, the source read
tcp://0.0.0.0:19999while the nodestill ran
tcp://0.0.0.0:26656.A section declares which of the two it takes, and the sections of the node's own file declare
it in the same loop that registers them, so one cannot be registered without being delivered. A
section that disagrees fails a test rather than quietly changing nothing.
The properties this rests on
Only what a source supplied is delivered. A resolution answers for every declared key, and
a declared value is what a provisioning command writes for a kind of node rather than what any
node runs. Delivering one would replace a setting an operator never mentioned, on every boot,
for every key their file omits. This is the cut that makes the whole thing safe to enable, and
it now has a test of its own: both deliveries narrow independently, in three functions, so the
rule was a convention three call sites remembered.
TestNoDeliveryCarriesADeclaredDefaultboots with a file supplying one key and asserts nothing else moved, in either delivery, for
every kind of node.
A section at a time. A decode is all or nothing for whatever it is handed, so one refused
value would otherwise cost every key in the file rather than the keys of the section it
appeared in. An operator who fixes one setting and mistypes another has to end up with the
first applied.
Decoded into a copy, published by replacing it. A decoder gathers errors and keeps going,
so a refused value partway leaves its target holding some new values and some old, with
nothing to compare against. The copy is of the configuration the node already has and not a
fresh one, because what a decoder writes can depend on what its target already holds. It
shares nothing: a decoder writes a list into the array its target holds, so one shared list
would edit the original during the rehearsal meant to protect it.
Nothing here can refuse a boot. Selecting this manager is a switch, not a configuration
change. No file, an unreadable one, a mode this binary does not declare defaults for: each
installs nothing and every key reads as it always has. A mistyped line in a hand-editable file
must not become an outage on the next restart.
The flag snapshot is taken at the entry to
Apply. The handler copies configuration valuesinto flags and marks them changed, so afterwards a flag an operator typed and a key their
app.tomlholds are indistinguishable, and a flag layer built from that state would putapp.tomlabovesei.toml. There is no later point where the truth survives.Values the decoder accepts and turns into something else
Both decode cleanly, which is why nothing downstream objects.
A length of time written as a plain number is read as nanoseconds, so sixty means sixty
billionths of a second and the node starts. Refused, with the unit to write. Zero is allowed:
nanoseconds and seconds are the same at zero, several of these settings document zero as the
way to turn them off, and three declare it.
A negative number where the field cannot hold one wraps to the largest value that field
has. Minus one is how an operator says "no limit" in most software they have used, and it would
turn the ceiling on connected peers into no ceiling and a window in seconds into six centuries.
Refused for the eight declared keys on unsigned fields.
Keys this does not declare, and who writes them instead
The criterion is not that this binary fills a value in. It is that something does, and the
file cannot see it.
The cluster's node controller resolves a peer set from live discovery and patches the addresses
in. A node computes a trust height and hash from the chain tip each time it starts. A moniker
is stamped per instance, and its default is the host name of whatever machine resolved it, so
no two machines agree on what it declares. A value from here would be decoded over whichever
already ran, with the change visible only in memory while the file kept saying otherwise.
Two keys that no longer have any effect and two that exist to make a node misbehave are out as
well.
rpc.unsafeis deliberately still declared: it is a real operator setting, and excludingit on the grounds that it is dangerous would use this mechanism for a purpose none of its other
categories serve. The diff log below is what surfaces it.
What an operator can see
The node's own configuration file still says what it said, and every tool an operator reaches
for reads that file: a patch command, a validator, an audit, somebody reading it over their
shoulder at three in the morning. None of them describes the running node after this.
So every delivered key that moved is logged with what it moved from. Not bounded: an earlier
version capped the list at twenty entries in alphabetical order, which meant a file writing
many keys of one section dropped whichever sorted last, and for the RPC section that is
rpc.unsafe. A section's changed list is bounded by its own declaration, so the cap guardedagainst something that cannot happen.
Two more things nothing else reports: a key the file writes that no section declares, and an
environment variable set for a key the environment cannot carry.
This manager's own reporting is held at a level its reports survive. One log level covers every
logger in the process and an operator writes it, and a fleet that runs its nodes quiet was
silencing all of this — measured: a value was delivered and not one line was emitted. The floor
is re-applied after anything that sets a level, because both setters reach every logger.
A disagreement about what kind of node this is
Two files state that under different names.
sei.tomlrecords it at the top and every valueresolved here is the answer for that kind. The node's own file states it again in a key of its
own, and that is what the node runs as.
This does not declare the second, on purpose: two keys for one fact can be written to disagree.
The cost is that they can still disagree across the two files, and a node resolving a
validator's values while running as a query-serving node reads correctly in every other report
about it. It is reported now, and not corrected — what kind of node this is gets decided when
it is provisioned, and a configuration manager is not the thing that should change it.
A command that answers before a node restarts
seid sei-config checkresolves the file the way a boot resolves it and reports every valuethis binary would refuse, without starting anything. Non-zero if there is one.
A boot may not refuse a file, so every value it cannot use is a report on a node that has
already restarted, and a fleet rolling a change forward reads it after the change is on every
node. The same questions have exact answers beforehand: the file, the binary and the
environment are the whole input. Run it first and a mistyped value costs a failed check rather
than a restart.
It needs a writable home and
HOMEset, because it is a subcommand of the root and the root'sown setup writes
client.toml,config.tomlandapp.toml. A pipeline has to copy the fileinto a writable directory rather than pointing at a read-only mount.
Two writers of the node's own file, measured against each other
A declared value is what the init command writes for a kind of node, and that command is not
the only thing here that writes this file: a node started without one gets it generated by the
boot instead. They disagree on four keys, and a test records which and what a node gets.
Two of the four were not what I predicted. The profiling address does not diverge, because the
boot assigns it and a bound flag's empty default overwrites it inside the same function. The
transaction indexer does, because the boot's writer produces a file for a node that serves
queries — the kind it defaults to — so that row is a validator's resolution against a file
generated for something else.
No fleet node is affected: the controller and the provisioning scripts both set the indexer
explicitly, and no fleet node lets the boot generate this file.
Verification
gofmt -s,goimports,go vetclean.golangci-lintreports only three pre-existingreflect.Ptrfindings that the version CI runs does not flag.config/...,cmd/seid/...andapp/...pass.I also broke each property on purpose and checked which test objected. Rehearsing into a fresh
configuration instead of a copy of the node's blanks the root directories and delivers defaults
over the operator's file. Publishing a refused decode applies half a section. Delivering
defaults overwrites an unwritten key. One decode for every section makes a typo cost another
section's settings. Comparing flag names as strings loses a typed flag to the file. Accepting a
bare number for a length of time gives a node sixty nanoseconds. Accepting a negative gives it
eighteen million million million connected peers. Each was caught.
Three of those were found by a peer review that ran the code rather than read it, along with
the log level inversion and a report that claimed values matched when it had failed to read
any of them. Every one of those defects decoded cleanly and would have been logged as a
success.
Reviewing this
Its base is the last of the node-section changes, so the diff here is only this step.
mainhas to be merged into that stack before the first of those is retargeted.
A declared field holding an interface is now refused at registration, because what a decoder
writes into one depends on the value already there, which would make a rehearsal in a copy
answer for the copy rather than for the node. That check runs after exclusions, since an
excluded path is not declared.