Skip to content

fix(io): lenient decode so ReadConfigFromDir parses real seid config.toml#36

Merged
bdchatham merged 2 commits into
mainfrom
fix/lenient-legacy-decode-quoted-scalars
Jun 30, 2026
Merged

fix(io): lenient decode so ReadConfigFromDir parses real seid config.toml#36
bdchatham merged 2 commits into
mainfrom
fix/lenient-legacy-decode-quoted-scalars

Conversation

@bdchatham

Copy link
Copy Markdown
Collaborator

What

ReadConfigFromDir could not parse a config.toml rendered by the standard seid/tendermint template, because the template emits some numeric and bool fields quoted — e.g. duplicate-txs-cache-size = "100000", gossip-tx-key-only = "true" — while the reader used strict BurntSushi/toml, which rejects a quoted string into an int/bool field:

toml: line 308 (last key "mempool.duplicate-txs-cache-size"):
  incompatible types: TOML value has type string; destination has type integer

The legacy reader (cosmos/Viper) tolerates this via weakly-typed mapstructure coercion; this library did not.

Why it matters

This was caught by the v2 in-binary ConfigManager differential in sei-chain (PLT-775). The v2 manager reads the existing two-file config into the unified model, re-writes it atomically, and re-enters the legacy reader. Because ReadConfigFromDir errored on the quoted primitives, v2 would fail at boot on any real node whose config.toml came from the standard template. It's a class, not one field (every quoted numeric/bool field is affected — gossip-tx-key-only, etc.).

Fix

Decode the TOML to a generic map, then mapstructure-decode into the legacy structs with:

  • WeaklyTypedInput: true — coerces quoted int/bool/uint (the whole class, generically), and
  • TextUnmarshallerHookFunc — keeps Duration and other string-encoded types parsing as before.

This mirrors how cosmos/Viper reads the same files, so a real seid config.toml round-trips through the unified model. Write path is unchanged.

Test

TestReadConfigFromDir_CoercesQuotedScalars reproduces the quoted int / bool / Duration case (previously a hard decode error). Full suite green. versionv0.0.21 (needs a tag on merge so sei-chain can pin it).

🤖 Generated with Claude Code

…toml

The seid/tendermint config templates emit some numeric and bool fields
QUOTED (e.g. `duplicate-txs-cache-size = "100000"`, `gossip-tx-key-only =
"true"`). The legacy reader (cosmos/Viper) tolerates this via weakly-typed
mapstructure coercion, but ReadConfigFromDir decoded with strict
BurntSushi/toml, which rejects a quoted string into an int/bool field:

  toml: line 308 "mempool.duplicate-txs-cache-size": incompatible types:
  TOML value has type string; destination has type integer

So a node whose config.toml came from the standard template could not be
read into the unified model at all — the v2 in-binary ConfigManager
(sei-chain PLT-775) would fail at boot.

Fix: decode TOML to a generic map, then mapstructure-decode into the legacy
structs with WeaklyTypedInput (coerces quoted int/bool/uint) and the
TextUnmarshaller hook (keeps Duration and other string-encoded types
parsing). This mirrors how cosmos/Viper reads the same files, so a real
seid config.toml round-trips through the model.

Regression test reproduces the quoted int / bool / Duration case. version
-> v0.0.21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cursor

cursor Bot commented Jun 30, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how every node reads config.toml at boot; weak typing is bounded by tests but mis-parsed values could affect runtime limits if coercion ever widens.

Overview
ReadConfigFromDir no longer fails on standard seid/tendermint config.toml files that quote numeric and bool fields (e.g. "100000", "true"). It now decodes TOML into a generic map and uses mapstructure with WeaklyTypedInput, plus hooks for Duration and to reject empty strings into numeric/bool fields so limits are not silently zeroed.

The write path is unchanged. New tests cover successful coercion and cases that must still error (invalid numbers, empty numeric strings, bad durations). Version bumped to v0.0.21.

Reviewed by Cursor Bugbot for commit 3296aa4. Bugbot is set up for automated code reviews on this repo. Configure here.

Addresses the /xreview slate on #36:
- dep: github.com/mitchellh/mapstructure (archived) -> github.com/go-viper/
  mapstructure/v2 (maintained fork viper migrated to); drop-in, and respects
  the repo's minimal/maintained-deps invariant (idiomatic-reviewer).
- guard: rejectEmptyScalarStringHook errors on an empty string bound to a
  numeric/bool field instead of letting WeaklyTypedInput silently coerce it to
  zero/false (the dissenter's empty-string->zero footgun, e.g. a blanked
  connection limit pinning to 0).
- tests: TestReadConfigFromDir_LocksLeniencyBoundary pins what must still error
  (non-numeric string, empty-string-into-int, malformed duration) so a future
  decoder change can't silently widen coercion.
- comment: soften "mirrors cosmos/Viper" -> "approximates", and document the
  one remaining accepted widening (non-string scalar -> string field), which
  seid templates never emit (sei-network-specialist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Suggested version: v0.0.21

Comparing to: v0.0.20 (diff)

Changes in go.mod file(s):

(empty)

gorelease says:

The following panic happened checking types near:
	/opt/hostedtoolcache/go/1.26.4/x64/src/internal/coverage/rtcov/rtcov.go:19:6
panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x60b0d2]

goroutine 11 [running]:
go/types.(*Checker).handleBailout(0x245a2f73a000, 0x245a2f435c30)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/check.go:473 +0x91
panic({0x6ea840?, 0x98eba0?})
	/opt/hostedtoolcache/go/1.26.4/x64/src/runtime/panic.go:860 +0x13a
go/types.(*Checker).objDecl.func1()
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/decl.go:55 +0x5c
panic({0x6ea840?, 0x98eba0?})
	/opt/hostedtoolcache/go/1.26.4/x64/src/runtime/panic.go:860 +0x13a
go/types.(*StdSizes).Sizeof(0x0, {0x759c68, 0x992360})
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/sizes.go:229 +0x312
go/types.(*Config).sizeof(...)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/sizes.go:334
go/types.representableConst.func1(...)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/const.go:77
go/types.representableConst({0x75ba60, 0x767980}, 0x245a2f73a000, 0x992360, 0x0)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/const.go:93 +0x1e9
go/types.(*Checker).arrayLength(0x245a2f73a000, {0x75b518, 0x245a2f71c150?})
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/typexpr.go:501 +0x1ce
go/types.(*Checker).typInternal(0x245a2f73a000, {0x75acd8, 0x245a2f71c180}, 0x0)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/typexpr.go:310 +0xa75
go/types.(*Checker).declaredType(0x245a2f73a000, {0x75acd8, 0x245a2f71c180}, 0x245a2f4353d8?)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/typexpr.go:193 +0x2f
go/types.(*Checker).varType(0x245a2f73a000, {0x75acd8, 0x245a2f71c180})
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/typexpr.go:158 +0x25
go/types.(*Checker).structType(0x245a2f73a000, 0x245a2f71cd20, 0x0?)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/struct.go:114 +0x17c
go/types.(*Checker).typInternal(0x245a2f73a000, {0x75ad08, 0x245a2f39c858}, 0x245a2f7228c0)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/typexpr.go:324 +0x1b5
go/types.(*Checker).declaredType(0x245a2f73a000, {0x75ad08, 0x245a2f39c858}, 0x420694?)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/typexpr.go:193 +0x2f
go/types.(*Checker).typeDecl(0x245a2f73a000, 0x245a2f7228c0, 0x245a2f4b4140)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/decl.go:623 +0xae5
go/types.(*Checker).objDecl(0x245a2f73a000, {0x761650, 0x245a2f7228c0})
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/decl.go:159 +0x98d
go/types.(*Checker).packageObjects(0x245a2f73a000)
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/resolver.go:678 +0x355
go/types.(*Checker).checkFiles(0x245a2f73a000, {0x245a2f4d4008?, 0x5c4d45?, 0x72b2e0?})
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/check.go:534 +0x385
go/types.(*Checker).Files(0x245a2f3c4000?, {0x245a2f4d4008?, 0x245a2f3ba0c0?, 0x6?})
	/opt/hostedtoolcache/go/1.26.4/x64/src/go/types/check.go:491 +0x75
golang.org/x/tools/go/packages.(*loader).loadPackage(0x245a2f3c4000, 0x245a2f39e000)
	/home/runner/go/pkg/mod/golang.org/x/tools@v0.2.0/go/packages/packages.go:1037 +0x8f2
golang.org/x/tools/go/packages.(*loader).loadRecursive.func1()
	/home/runner/go/pkg/mod/golang.org/x/tools@v0.2.0/go/packages/packages.go:847 +0x1a7
sync.(*Once).doSlow(0x0?, 0x0?)
	/opt/hostedtoolcache/go/1.26.4/x64/src/sync/once.go:78 +0xac
sync.(*Once).Do(...)
	/opt/hostedtoolcache/go/1.26.4/x64/src/sync/once.go:69
golang.org/x/tools/go/packages.(*loader).loadRecursive(0x0?, 0x0?)
	/home/runner/go/pkg/mod/golang.org/x/tools@v0.2.0/go/packages/packages.go:835 +0x3b
golang.org/x/tools/go/packages.(*loader).loadRecursive.func1.1(0x0?)
	/home/runner/go/pkg/mod/golang.org/x/tools@v0.2.0/go/packages/packages.go:842 +0x26
created by golang.org/x/tools/go/packages.(*loader).loadRecursive.func1 in goroutine 80
	/home/runner/go/pkg/mod/golang.org/x/tools@v0.2.0/go/packages/packages.go:841 +0x8c

gocompat says:

Your branch is up to date with 'origin/main'.

Cutting a Release (and modifying non-markdown files)

This PR is modifying both version.json and non-markdown files.
The Release Checker is not able to analyse files that are not checked in to main. This might cause the above analysis to be inaccurate.
Please consider performing all the code changes in a separate PR before cutting the release.

Automatically created GitHub Release

A draft GitHub Release has been created.
It is going to be published when this PR is merged.
You can modify its' body to include any release notes you wish to include with the release.

@bdchatham bdchatham merged commit 3fd5c81 into main Jun 30, 2026
4 checks passed
@bdchatham bdchatham deleted the fix/lenient-legacy-decode-quoted-scalars branch June 30, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant