Skip to content

[ConfigManager] Register Node Sections 1/4 - #3992

Draft
bdchatham wants to merge 2 commits into
plt-775-node-sections-basefrom
plt-775-node-sections-1
Draft

[ConfigManager] Register Node Sections 1/4#3992
bdchatham wants to merge 2 commits into
plt-775-node-sections-basefrom
plt-775-node-sections-1

Conversation

@bdchatham

@bdchatham bdchatham commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What this does

The node's own configuration file carries 141 keys and no section declared any of
them. This declares the first two: the peer-to-peer table and the remote procedure
call table, 47 keys.

Nothing consumes them. A section reaches the registry through its owning package's
initialisation, and nothing imports the new package yet, so a node's configuration
reads exactly as it did.

Where the registration lives, and why not with the struct

Neither the package that defines these settings nor the package that decides them
can register them. The struct they decode into belongs to the node's configuration
package. The rules that vary them by node kind live in app/params, which imports
that struct. The importing direction is already fixed, so only a third package sees
both, and config/tendermintbase is that package.

This is the shape config/cosmosbase already uses for the upstream server's
sections, and it carries the same consequence: nothing imports it until a consumer
adds the import.

Two things the registry needed first

A section can now leave out a path its 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. The field it named can
be renamed away, and left alone the exclusion then excludes nothing while still
reading in the source as a deliberate omission.

A field that collects what the decode matched no field for now declares no key.
Such a field is tagged to absorb every name the decode did not match, so what lands
in it is what an operator misspelled. Giving it a key of its own would offer the
collector itself as a setting to write. No exclusion can reach it, because an
exclusion names a key and this field has none.

The paths left out here, and why each

The node's root directory. Five of these sections carry a 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. Declared, whatever delivers these values gets 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 walks every registered section rather than the five,
so a section added later carrying the same field fails instead of repeating it.

I did not find that by reading. A prior prototype of this work had a test named for exactly this
failure, and it was the first thing that test caught when I compared against it.

Two paths something outside this binary writes. The cluster's node controller resolves a
peer set from live discovery and patches the addresses into the node's own file. A value
declared here would be decoded over whatever already ran, with the change visible only in
memory and the file still saying otherwise. So external-address and persistent-peers are
left out. The criterion is not that the binary fills them in — it is that something does, and
this file cannot see it.

One path that exists to make a node misbehave. test-dial-fail makes every dial fail. It
has no use on a real network and it is reachable from a file an operator edits by hand.

The one path left out for a reason of its own

p2p.max-outbound-connections is a pointer the node's defaults leave unset, and
unset is what selects the behaviour: the node derives a ceiling from the total
connection limit instead. Any number written here would be a value no generated
file carries. A test fails if the node ever gives it a default, and the fix would
then be to declare it.

What varies by node kind

Four keys, and a test holds all four by value rather than describing them.

Key validator seed full and archive
p2p.laddr tcp://127.0.0.1:26656 tcp://127.0.0.1:26656 tcp://0.0.0.0:26656
rpc.laddr tcp://127.0.0.1:26657 tcp://127.0.0.1:26657 tcp://0.0.0.0:26657
p2p.max-connections 100 1000 100
p2p.allow-duplicate-ip false true false

The two listen addresses are the reason this is measured. A rule that stopped
varying would leave a validator binding the address a query-serving node binds, and
a validator holds a signing key. A comment saying otherwise cannot fail.

How the keys are derived

From the struct's own mapstructure tags, which is what the node decodes through, so
a declared key is a key that reader resolves rather than a second spelling of it. A
renamed tag moves the reader and the declaration together and there is no third
statement to drift from. The test asserts the count rather than the spellings for
that reason.

Verification

gofmt, goimports, go vet clean. golangci-lint reports nothing on the
touched packages. config/..., cmd/seid/... and app/params pass.

I also broke each new behaviour on purpose and checked a test objected: an
exclusion reaching the type walk but not the value walk, a stale exclusion being
accepted, and a collecting field declaring a key. All three were caught.

Reviewing this

This is the first of four, and they stack. Its base is a branch holding the four
section changes already in review, so the diff here shows only the new work. Once
those land, this retargets to the default branch.

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

github-actions Bot commented Aug 24, 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 25, 2026, 1:20 AM

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>
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