Skip to content

[ConfigManager] Register Sections 4/4 - #3977

Draft
bdchatham wants to merge 5 commits into
mainfrom
plt-775-sections-4
Draft

[ConfigManager] Register Sections 4/4#3977
bdchatham wants to merge 5 commits into
mainfrom
plt-775-sections-4

Conversation

@bdchatham

@bdchatham bdchatham commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Registers the five configuration sections whose keys belong to the Cosmos server, and
adds 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 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. A section that carried its name into every key
would rename all fourteen, and an operator's existing file would reach none of them. So
a section now has a name it is looked up and reported under, and a prefix its keys
carry, and for a root section the prefix is empty.

Both walks build a key through one function. Reverting either one on its own fails a
test: the type walk through the key comparison, and the value walk through the existing
check that a rendered default states one value per declared key.

One collision is refused, and one is a gap we are tracking

A key two sections both declare has one default rendered over the other, and which one
depends on the order the sections are walked. That is refused — two identical keys answer
to one environment variable, so the check that already refuses a spelling collision
catches it. It now says so in the right terms: a key colliding with itself is not two
spellings of one variable.

The gap. A key at the top of the file that is also a section's name cannot be written
at all, because no file holds both a value for that name and a table under it, so one of
the two settings is unreachable and nothing says which. Nothing refuses that.

I had a guard for it and removed it, because it has no instance: one section declares keys
at the top of the file and none of its fourteen names is a section's name. It becomes
reachable at the second such section, and the prototype shows that is exactly where it
bites — 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 node and a node
section could not have coexisted with it. So the guard belongs with the change that adds
the second root section, where the naming decision it forces actually arises.

The fact is recorded in the package contract, among the things the registry does not
guard, with the condition that makes it reachable. It is adjacent to PLT-1039, which
tracks the same shape of problem inside a single section: a leaf and a subtree declared
under one tag.

A section can say the environment cannot supply a key

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. That
assertion is the first statement of the whole server configuration, so a resolved
variable installs 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 one schema, and what holds it

The metric section needs a schema, 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 exactly one. A second divergence fails, and if the upstream type ever comes to
match, the schema is left with nothing to justify it and that fails too.

These five are declared and not yet linked

Nothing imports config/cosmosbase, so its init runs only in its own test binary and these five
sections do not enter a real seid. That is deliberate for now: adding a blank import somewhere in the
command tree would put fourteen node-wide keys into the key space a node resolves, which is a behaviour
change and belongs with the work that consumes them rather than with the work that declares them.

Two consequences worth stating rather than leaving to be discovered. A diagnostic run against a real
binary will not see these keys. And the cross-section refusals — the two that depend on what else has
registered — are exercised against probes in the registry's own tests and never against the real
registered set, because that set and these five are never in one binary. Linking them is the first step
of whichever change consumes the registry.

One declared value is not what a running node resolves, and several more are not either

pruning is the one worth naming: the command line registers a flag of the same name defaulting to the
standard schedule, and a bound flag is a source below the file, so a node started with no pruning key
written prunes on that schedule while this declares it keeps everything. It is not the only one — a
command that assembles the server configuration overrides some of these before a node starts, and the
count is measured rather than asserted here.

A caller resolving for a running node therefore has to supply that node's flag values, and only the ones
an operator actually set. A flag nobody typed still reports a default, and this resolution ranks flags
above the file, so passing defaults would put every one of them over an operator's own value.

What I broke on purpose

Eight mutations, each caught: porting the key-joining into only one of the two walks
(both directions), rendering a root section's defaults under its lookup name, dropping
the collision refusal, dropping the environment skip, allowing a refusal with no reason,
resolving the label set as the shape its reader refuses, and giving the node-wide keys a
section prefix.

Two things to know

One declared value is not what a running node uses. The pruning strategy is declared
as keeping everything (DefaultConfig()), 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. Nothing depends on this yet; I can file it if it should be
tracked.

Three defaults vary by kind of node, and every section here answers through the mode
rules the binary already applies rather than restating them. A full node and an archive node
exist to serve queries, so both interfaces that serve them are on; a validator is meant to
expose as little as it can, so both are off, and the upstream default would have declared
gRPC open on every validator. Block retention follows the same rules. The test writes the
three values out by kind of node, so a change to the rules fails it and gets looked at.

The recorded configuration surface does not move, because nothing consumes the registry
on a boot path yet.

The manifest that states which sections a binary declares is deliberately not here. It
has to name every section, so it can only land once these four registration changes are
all on main.

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.
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 23, 2026, 12:10 AM

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.62%. Comparing base (01f3c6a) to head (02878d0).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3977      +/-   ##
==========================================
- Coverage   58.69%   57.62%   -1.07%     
==========================================
  Files        2326     2230      -96     
  Lines      199122   187919   -11203     
==========================================
- Hits       116877   108293    -8584     
+ Misses      71477    69750    -1727     
+ Partials    10768     9876     -892     
Flag Coverage Δ
sei-chain-pr 100.00% <100.00%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
config/cosmosbase/cosmosbase.go 100.00% <100.00%> (ø)
config/registry/environment.go 100.00% <100.00%> (ø)
config/registry/registry.go 100.00% <100.00%> (ø)
config/registry/resolve.go 100.00% <100.00%> (ø)

... and 98 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant