Skip to content

[ConfigManager] Deliver sei.toml for Decoded Sections 2/3 - #4044

Draft
bdchatham wants to merge 2 commits into
plt-775-install-appfrom
plt-775-install-decode
Draft

[ConfigManager] Deliver sei.toml for Decoded Sections 2/3#4044
bdchatham wants to merge 2 commits into
plt-775-install-appfrom
plt-775-install-decode

Conversation

@bdchatham

@bdchatham bdchatham commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Delivers the values an operator's sei.toml supplies for the sections whose reader decodes its
file whole. 1673 lines. Second of three, on top of the install.

Why these need a second delivery

The node's own configuration file is read into a struct by the boot's handler before this runs,
and nothing consults the key-value source for those settings afterwards. So installing a value
into that source reaches nothing for them. Measured on a real boot: viper answers 19999 for a
key whose live struct field holds 26656.

Rehearse into a copy, then publish through the pointers that exist

The values are decoded into a deep copy of the live configuration first. The copy has to be of the
node's own configuration rather than a fresh one, because what a decoder writes depends on what the
target already holds.

Publishing then assigns each section through the pointer already there, rather than replacing the
configuration wholesale. Components in the node take a section rather than the configuration
holding it, and that type carries nine section pointers, so replacing the top level hands every
holder a section frozen at its old values. Assigning through the pointers keeps their identity, so
a component reads the delivered values whichever side of the delivery it was built on.

One section at a time

A decode is all or nothing for whatever it is handed, so a single value a decoder refuses would
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 one applied.

The log level goes first

Before any of the reporting, because a refusal is reported at a level an operator may have raised
the threshold above. Doing it after would mean the one setting somebody changes in order to see a
refusal is the setting a refusal suppresses. An exported level still wins, matching the order a
node already resolves that setting by.

One report corrected

The line saying the file supplied no declared value describes the lookup delivery alone, and both
deliveries run in one pass. An operator whose file moved a setting through a decode was told the
file supplied nothing, a few lines later, in the same boot. It is now scoped to the delivery it
describes.

What review changed

A value that decodes cleanly and means something else. The guard for this checked the sign and
not the magnitude, so the pre-flight command approved two files that change what a node runs:

[p2p] max-connections = 1e20   approved, then applied as 18446744073709551615
[mempool] size = 1.5           approved, then applied as 1

The first saturates to the largest value the field holds, which is the same outcome the guard
already refuses a minus one for. The second truncates, so a mempool written between one and two
carries a single transaction. Both are refused now. Both callers of that guard also printed one of
its reasons for all of them, so a negative number came out as "cannot be negative, and decodes to
the largest value this setting can hold rather than to no limit is a length of time written as a
plain number, which reads as nanoseconds". Each message already stands alone, so the callers print
what they were handed.

Publishing. The delivery replaced the whole configuration struct, which swapped all nine
section pointers. It was correct only because the delivery happens to run before the components
that hold those pointers are built, and nothing stated that order or would fail when it changed.
Each section is now assigned through its own pointer.

Two things the reports got wrong. A password reached the log: the transaction index takes a
PostgreSQL connection string, and this report is the only place the running configuration is
written down. Nothing logs that string today, and a value carrying a password now has it removed,
detected in the value rather than from a list of keys somebody keeps in step. And an unread key was
reported as a key that did not move, because a missing map entry meant "could not read": absent on
both sides, it compared equal, which is what a key an operator wrote and got looks like. The read
now names what it could not read.

Assertions that could not fail. The copy walks the node's type and had no case for an array, so
an array of pointers would have been shared; the test holding it to that promise had the same blind
spot, and its comparison had no case for an interface, so eleven of the twenty-six paths it
enumerated could never fail. A separate test comparing a hundred and fifty keys was comparing the
absence of a value with the absence of a value for a hundred and forty of them, because the node's
own configuration holds the decoded sections and it had been handed every declared key. The read
now fails a test rather than answering partially.

Two tests that skipped on the precondition they exist to measure, dead code, a signature whose
second argument was never read, and two doc comments claiming a guarantee their bodies do not give
are all corrected. The precedence between sei.toml and the node's own files is written down.

Notes for review

Formatters, go vet and golangci-lint clean; -count=2 -shuffle clean.

The verification of that last fix is worth recording, because the first attempt was a false pass:
the mutation pattern had not applied, so an unmutated run was read as proof it was covered. With
the mutation genuinely in place nothing failed, which is how the test for it came to exist.

The divergence record measures a node's second boot rather than its first. A flag bound to the
debug listener address hides that divergence on the first boot only, because the file has not been
read yet for the flag's empty default to lose to, so measuring the first boot measures the one
boot no running node repeats.

@github-actions

github-actions Bot commented Aug 27, 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 27, 2026, 10:15 PM

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.76965% with 130 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.28%. Comparing base (0becc6b) to head (ae60b14).

Files with missing lines Patch % Lines
cmd/seid/cmd/configmanager/tendermint_copy.go 66.66% 57 Missing and 24 partials ⚠️
cmd/seid/cmd/configmanager/tendermint.go 55.85% 38 Missing and 11 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                  Coverage Diff                  @@
##           plt-775-install-app    #4044    +/-   ##
=====================================================
  Coverage                60.27%   60.28%            
=====================================================
  Files                     2056     2058     +2     
  Lines                   176963   177299   +336     
=====================================================
+ Hits                    106671   106887   +216     
- Misses                   60499    60588    +89     
- Partials                  9793     9824    +31     
Flag Coverage Δ
sei-chain-pr 69.04% <64.76%> (-6.21%) ⬇️
sei-db 69.80% <ø> (ø)

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

Files with missing lines Coverage Δ
cmd/seid/cmd/configmanager/install.go 77.94% <100.00%> (+7.13%) ⬆️
config/registry/delivery.go 100.00% <100.00%> (ø)
cmd/seid/cmd/configmanager/tendermint.go 55.85% <55.85%> (ø)
cmd/seid/cmd/configmanager/tendermint_copy.go 66.66% <66.66%> (ø)

... and 2 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.

@bdchatham

Copy link
Copy Markdown
Contributor Author

@seidroid review

bdchatham and others added 2 commits August 27, 2026 14:06
…coded

A node's own configuration file is read into a struct by the boot's handler before this
runs, and nothing consults the key-value source for those settings afterwards. So the
install of the previous change is no delivery at all for them: the values have to be
decoded into that struct instead.

Decoded into a deep copy of the live configuration and published by replacing it, rather
than decoded in place. What a decoder writes depends on what the target already holds, so
the copy has to be of the node's own configuration and not a fresh one.

One section at a time. A decode is all or nothing for whatever it is handed, so a single
value a decoder refuses 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 one applied.

The resolved log level is applied before any of the reporting, because a refusal is
reported at a level an operator may have raised the threshold above, and doing it after
would mean the one setting somebody changes in order to see a refusal is the setting a
refusal suppresses.

One report corrected. The line saying the file supplied no declared value describes the
lookup delivery alone, and both run in one pass, so an operator whose file moved a setting
through a decode was told the file supplied nothing a few lines later. It is now scoped to
the delivery it describes.

Verified by mutation, and the first attempt at that verification was a false pass worth
recording: the pattern had not applied, so an unmutated run was read as proof. With the
mutation genuinely in place nothing failed, which is how the new test came to be written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…that decode to something else

The delivery replaced the node's whole configuration struct. Every section under it
sits behind a pointer of its own, and components take the section rather than the
configuration holding it, so replacing the top level swapped all nine pointers for
fresh ones. Anything already holding a section went on reading the values that
section had before the delivery. It was correct only because the delivery happens to
run before those components are built, and nothing stated that order. Each section
is now assigned through its own pointer, so identity survives and a holder reads the
delivered value whichever side of the delivery it took its pointer from.

The guard on values that decode to something other than what they say checked the
sign and not the magnitude. Measured through the pre-flight command on real files:

  [p2p] max-connections = 1e20   approved, then applied as 18446744073709551615
  [mempool] size = 1.5           approved, then applied as 1

The first saturates to the largest value the field holds, which is the same outcome
the guard already refuses a minus one for. The second truncates, so a mempool
written between one and two carries a single transaction. Both are refused now, and
the walk that answers what a key's field is returns the field's type rather than a
yes or no, so three walks over the same tags became one.

Both callers of that guard printed one of its reasons for all of them. A negative
number came out as "cannot be negative, and decodes to the largest value this
setting can hold rather than to no limit is a length of time written as a plain
number, which reads as nanoseconds". Each message already stands alone, so the
callers print what they were given.

A password reached the log. The transaction index takes a PostgreSQL connection
string, and the report naming what a delivery changed is the only place the running
configuration is written down. Nothing logs that string today. A value carrying a
password now has it taken out, detected in the value rather than from a list of keys
somebody keeps in step.

An unread key reported as a key that did not move. Reading a value used a missing
map entry to mean "could not read", so a key absent from both sides compared equal
and was reported as unchanged, which is what a key an operator wrote and got looks
like. The read now names what it could not read, and the caller says so.

The copy walked the type and had no case for an array, so an array of pointers would
have been shared. The test holding it to that promise had the same blind spot, and
its share check had no case for an interface, so eleven of the twenty-six paths it
enumerated could not fail. All three are fixed.

A test comparing a hundred and fifty keys was comparing the absence of a value with
the absence of a value for a hundred and forty of them: the node's own configuration
holds the decoded sections and it was handed every declared key. The read now fails
the test rather than answering partially, and the test compares each key through the
delivery that owns it.

Two tests skipped on a precondition that is the thing they measure, dead code, a
signature whose second argument was never read, and two doc comments claiming a
guarantee their bodies do not give are all corrected. The precedence between sei.toml
and the node's own files is written down.

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