Skip to content

feat(config): add Validate() method for startup validation#2763

Merged
wolf31o2 merged 14 commits into
blinklabs-io:mainfrom
chrisguiney:config-validate
Jul 17, 2026
Merged

feat(config): add Validate() method for startup validation#2763
wolf31o2 merged 14 commits into
blinklabs-io:mainfrom
chrisguiney:config-validate

Conversation

@chrisguiney

@chrisguiney chrisguiney commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #1485

Adds Config.Validate() to internal/config, checking the fully merged configuration (defaults, YAML, env, CLI flags) before any services start. All violations are aggregated into a single startup error naming each bad setting, so operators can fix everything in one pass.

Checks

  • Mode enums: runMode, startEra, storageMode
  • runMode: load requires immutableDbPath
  • Ports: required listeners (port, privatePort, metricsPort) must be non-zero; optional API ports accept 0 as disabled; all must be ≤ 65535; ports below 1024 are rejected unless the process runs as root
  • Path-traversal guard on cardanoConfig (no .. components), matching the existing network-name guard
  • TLS cert/key must be set as a pair
  • Mempool watermark ranges and eviction < rejection ordering
  • Block producer requires all three credential paths
  • Network name validity; network or networkMagic must be set
  • Duration strings (shutdownTimeout, ledgerCatchupTimeout, chainsync.stallTimeout, mithril.downloadIdleTimeout) parse at startup instead of failing at point of use
  • chainsync.strategy and mithril.backend accepted-value sets

Wire-up

Validation runs in PersistentPreRunE after ApplyFlags. This also closes a gap where CLI flag values bypassed LoadConfig's inline checks entirely (flags are applied after LoadConfig returns, so e.g. --eviction-watermark 1.5 was never validated). The dingo load <path> positional argument is merged into ImmutableDbPath before validation so a config with runMode: load plus a path argument does not fail spuriously.

Deliberately out of scope: file/dir existence checks (embedded-FS fallbacks and later-created paths make stat-at-validate wrong for several fields), and the closed issues referenced by the ticket (#276, #387, #390, #393).

Testing

  • New table-driven tests covering every rule plus error aggregation; the privileged-port check is parameterized on privilege so tests don't depend on the runner's UID
  • Full make test (-race, includes conformance) passes; golangci-lint, modernize, and import-boundaries clean
  • Manual smoke tests: bad port, missing load path, and --cardano-config ../../etc/passwd each fail with a clear named-setting error; multiple violations report together

ARCHITECTURE.md Configuration section updated; DATABASE.md not affected.

🤖 Generated with Claude Code


Summary by cubic

Adds Config.Validate() for startup validation keyed to the command’s effective run mode, and defers defaulting/validation until all sources (YAML, env, flags, args) are merged. Also rejects a negative historyExpiry.frequency instead of silently defaulting.

  • New Features

    • Validate the merged config via internal/config.Config.Validate() before any services start, using the effective mode (serve, load, sync, mithril).
    • Scope listener checks to active listeners: serving requires relay/private/metrics; sync validates metrics/debug; read‑only mithril list/show and load start none; API ports validate only under api storage or when configured runMode is dev.
    • Add internal/config.Config.ApplyDefaults() to fill derived defaults after ApplyFlags (runMode, mempoolCapacity, watermarks, forge thresholds, history‑expiry frequency) and then validate; LoadConfig now only parses/merges sources.
    • Export accepted‑value lists from parsers and derive from them: chainsync.AcceptedHeaderSyncStrategyNames and mithril.AcceptedBackends. internal/config duplicates are guarded by bidirectional parity tests in cmd/dingo.
  • Bug Fixes

    • Port validation: detect duplicate non‑zero ports only when bind addresses overlap (wildcard vs specific); privileged‑port checks use the actual cutoff (root/Windows/CAP_NET_BIND_SERVICE or Linux sysctl).
    • Exempt version, list, help, and completion from config loading and validation.
    • dingo load <path> merges the positional path before validation; load mode enforces immutableDbPath.
    • Harden cardanoConfig path guard to reject any ..; validate CLI flags, parse duration strings at startup, and validate mempool watermarks (including NaN) and block‑producer credentials.
    • Support networkMagic‑only configs: allow an empty network at load/flag time; validation still requires either network or networkMagic.
    • Fix precedence gaps: CLI flags can override invalid YAML values and influence derived defaults (e.g., --run-mode leios sets the leios mempool capacity).
    • History expiry: preserve an explicitly negative historyExpiry.frequency from any source and fail validation; only zero defaults to 1h.

Written for commit b61c234. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Validation now runs deterministically before services start, aggregating multiple configuration issues at once.
    • CLI determines an “effective” run mode for validation; informational commands skip config loading/validation.
    • dingo load <path> can supply immutableDbPath even when omitted from YAML.
  • Bug Fixes
    • Stricter listener port checks (including privileged-port rules, overlap collisions, and mode-specific validation).
    • Improved TLS, mempool watermark, chainsync, Mithril, block-producer credentials, path traversal, and duration/strategy validation.
    • Network parsing is relaxed for networkMagic-only setups and --network "".
  • Tests
    • Added expanded and parity-focused validation coverage for accepted strategy/back-end values.

Add Config.Validate() to internal/config, checking the fully merged
configuration (defaults, YAML, env, CLI flags) before any services
start: mode enums, port ranges (0/privileged/out-of-range), the
load-mode immutableDbPath requirement, a path-traversal guard on
cardanoConfig, TLS cert/key pairing, mempool watermarks,
block-producer credential paths, network identity, and
duration/strategy strings that were previously only parsed at their
point of use. All violations are aggregated into a single startup
error naming each bad setting.

Validation runs in PersistentPreRunE after CLI flags are applied,
closing the gap where flag values bypassed LoadConfig's inline
checks. The `dingo load <path>` positional argument is merged into
ImmutableDbPath before validation so a config with runMode "load"
plus a path argument does not fail spuriously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney
chrisguiney requested review from a team as code owners July 6, 2026 21:27
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds comprehensive validation for merged configuration, including effective run-mode-specific listener checks, port bindability, path safety, TLS pairing, watermarks, credentials, network identity, durations, strategies, and Mithril backends. CLI mode detection now skips informational commands, merges load paths before validation, and passes the effective mode to Config.Validate(). NetworkMagic-only configurations are supported, with expanded regression, command-mode, and whitelist-parity tests and updated architecture documentation.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes chainsync and mithril whitelist/parity work, which the linked issue explicitly calls separate from the startup-validation framework. Split the whitelist/parity and other separate hardcoded-value changes into the issue-specific follow-up PRs.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR adds Config.Validate(), enforces load-mode, port, and path checks, and wires validation into startup before services begin.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is clear, concise, and accurately highlights the new startup configuration validation method.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/config/validate.go (2)

203-230: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Whitelist duplication risks drifting from the real parsers.

Both chainsync.strategy (Line 206-213) and mithril.backend (Line 223-230) re-implement, as string literals, the accepted-value sets that live in chainsync.ParseHeaderSyncStrategy and cmd/dingo's resolveMithrilBackend, respectively — a deliberate choice to avoid importing node-subsystem packages into internal/config, per the comments. That's a reasonable tradeoff, but if either downstream parser's accepted set changes, this validator can silently start accepting/rejecting the wrong values, and a wrong config would then get past Validate() only to fail later at startup — defeating some of the purpose of centralizing checks here. Consider adding a small parity test (e.g. in the layer that can import both) or a code comment cross-reference/TODO to keep them in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/validate.go` around lines 203 - 230, The accepted-value
whitelists for chainsync.strategy and mithril.backend in Validate are duplicated
from chainsync.ParseHeaderSyncStrategy and resolveMithrilBackend, so keep them
from drifting. Add a parity safeguard in a test or a clear cross-reference/TODO
near the switch statements in validate.go so any future parser changes are
reflected here. Use the existing Validate, chainsync.ParseHeaderSyncStrategy,
and resolveMithrilBackend symbols to locate the duplicated logic.

79-98: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider guarding against duplicate port assignments.

None of the configured ports (relay, private, metrics, debug, utxorpc, blockfrost, mesh, bark, midnight) are checked against each other for collisions. Two services accidentally sharing a port number would currently pass validation and only fail at bind time. A simple map[uint]string pass over the same ports slice (skipping zero) would catch this cheaply while everything is already in scope here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/validate.go` around lines 79 - 98, The port validation loop
in validate.go only checks each port individually and does not detect collisions
between services. Add a duplicate-check pass over the existing ports slice in
the validateConfig path, skipping zero-valued ports, and record any repeated
port number across
relayPort/privatePort/metricsPort/debugPort/utxorpcPort/blockfrostPort/meshPort/barkPort/Midnight.Port.
Use a map[uint]string (or equivalent) to compare each port against previously
seen settings and append a validation error when two configured services share
the same port.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/config/validate.go`:
- Around line 79-98: The port validation loop in validateConfig is treating
relay/private/metrics listeners as required even for RunModeLoad, which makes
load-only configs fail unnecessarily. Update the port-checking logic around the
ports slice and validatePort call so those listener ports are skipped or allowed
to be 0 when c.RunMode is RunModeLoad, while keeping the existing required
behavior for normal modes.
- Around line 37-39: The privilege check in Config.Validate is using
os.Geteuid() directly, which misclassifies Windows and can incorrectly reject
low ports there. Update Validate to gate the privileged-port restriction by OS,
so it only treats Unix-like systems as requiring elevated privileges, and keep
the existing c.validate call path intact while adjusting the privilege flag
logic.

---

Nitpick comments:
In `@internal/config/validate.go`:
- Around line 203-230: The accepted-value whitelists for chainsync.strategy and
mithril.backend in Validate are duplicated from
chainsync.ParseHeaderSyncStrategy and resolveMithrilBackend, so keep them from
drifting. Add a parity safeguard in a test or a clear cross-reference/TODO near
the switch statements in validate.go so any future parser changes are reflected
here. Use the existing Validate, chainsync.ParseHeaderSyncStrategy, and
resolveMithrilBackend symbols to locate the duplicated logic.
- Around line 79-98: The port validation loop in validate.go only checks each
port individually and does not detect collisions between services. Add a
duplicate-check pass over the existing ports slice in the validateConfig path,
skipping zero-valued ports, and record any repeated port number across
relayPort/privatePort/metricsPort/debugPort/utxorpcPort/blockfrostPort/meshPort/barkPort/Midnight.Port.
Use a map[uint]string (or equivalent) to compare each port against previously
seen settings and append a validation error when two configured services share
the same port.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a172d546-cc9e-41af-bc49-22db40f3b84a

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd521d and f183c2f.

📒 Files selected for processing (4)
  • ARCHITECTURE.md
  • cmd/dingo/main.go
  • internal/config/validate.go
  • internal/config/validate_test.go

Comment thread internal/config/validate.go Outdated
Comment thread internal/config/validate.go

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cmd/dingo/main.go Outdated
Comment thread internal/config/validate.go Outdated
Comment thread internal/config/validate.go Outdated
- Treat Windows as privileged for the sub-1024 port check; os.Geteuid
  returns -1 there, which otherwise rejected every low port on Windows.
- Require the relay, private, and metrics listener ports only outside
  load mode, since `dingo load` starts none of those listeners.
- Skip Validate() for the informational `version` and `list`
  subcommands so they still run against an otherwise-invalid config.
- Detect two services assigned the same non-zero port, which would
  otherwise only fail at bind time.
- Move the chainsync.strategy and mithril.backend accepted-value sets
  into exported slices and add a parity test in cmd/dingo (which can
  import both) so they cannot silently drift from the real parsers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney

Copy link
Copy Markdown
Contributor Author

Pushed e56ab82 addressing the review feedback:

  • Windows privileged-port check (CodeRabbit / cubic): os.Geteuid() returns -1 on Windows, so Validate() treated every Windows process as unprivileged and rejected sub-1024 ports. Validate() now treats Windows as privileged (runtime.GOOS == "windows" || os.Geteuid() == 0); the restriction is Unix-specific.
  • Listener ports in load mode (CodeRabbit / cubic): relay/private/metrics are now required only when RunMode != RunModeLoad, since dingo load starts none of those listeners. A load-only config may leave them at 0. Serving modes still require them.
  • Utility subcommands (cubic): Validate() is skipped for the informational version and list subcommands so they still run against an otherwise-invalid config.
  • Duplicate port assignments (CodeRabbit nitpick): added a map[uint]string pass that flags two services sharing the same non-zero port (previously only failed at bind time).
  • Whitelist drift (CodeRabbit nitpick): the chainsync.strategy and mithril.backend accepted-value sets are now exported slices, consumed by Validate() and checked by a new parity test in cmd/dingo (which can import both chainsync and resolveMithrilBackend), so they can't silently drift from the real parsers.

New test cases cover the load-mode/serve-mode port gating and duplicate-port detection; the parity tests cover the whitelists. go build ./..., the config + cmd/dingo suites, import-boundaries, golangci-lint, and modernize all pass locally.

On the Windows go-test failure: it was infra, not a code hang. The job annotation is GitHub's generic "The hosted runner lost communication with the server", the go-test step never completed (it was killed at the ~6h job timeout), and the identical go test ./... passed on macOS in ~2.5 min. All changed code paths in this PR are pure, fast functions with no goroutines/IO, and main is consistently green on Windows. This push re-runs the Windows job.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmd/dingo/config_parity_test.go">

<violation number="1" location="cmd/dingo/config_parity_test.go:30">
P3: Both parity tests only guard one direction of drift: they verify that every value the config's accepted list permits is also accepted by the downstream consumer. They do not guard the reverse direction — if someone adds a new valid value to `ParseHeaderSyncStrategy` or `resolveMithrilBackend` without updating the config's accepted list, neither test will catch it. Consider adding a second pass that iterates over the consumer's accepted values and verifies each one appears in the config's accepted list, so the test provides full bidirectional drift coverage.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cmd/dingo/config_parity_test.go
The chainsync.strategy and mithril.backend parity tests only checked
that every value config accepts is accepted by the downstream parser.
They now also assert config's accepted set matches the parser's
contract exactly, so a value added to ParseHeaderSyncStrategy or
resolveMithrilBackend without updating config's list is caught too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-go for merge until the command-specific validation regression is fixed. The new validator applies serving-mode listener requirements based on cfg.RunMode, but several one-shot commands do not start those listeners and can still have the default runMode=serve. It may be cleanest to model this by normalizing an effective run mode before validation, or by adding command-specific run modes for load/sync-style invocations so Validate() can decide which listener set is actually required.

Comment thread cmd/dingo/main.go Outdated
// highest-precedence source for ImmutableDbPath; merge it before
// validation so a config with runMode "load" and no
// immutableDbPath doesn't fail spuriously.
if cmd.Name() == "load" && len(args) > 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only copies the positional path into ImmutableDbPath; it does not make validation treat the explicit dingo load <path> command as load mode. With the default runMode: serve, cfg.Validate() still requires relay/private/metrics ports even though loadRun does not start those listeners. Repro: go run ./cmd/dingo load /tmp --port 0 --private-port 0 --metrics-port 0 fails in validation before reaching the load command. A straightforward fix would be to normalize an effective run mode for this command before validation, or add a distinct run mode/validation profile for one-shot load commands.

Comment thread internal/config/validate.go Outdated
// Ports. The relay, private, and metrics listeners are required for
// the serving modes but not started by "load", so they may be unset
// (0) in a load-only config.
listenersRequired := c.RunMode != RunModeLoad

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conflates configured runMode with the actual Cobra command/service set being executed. Non-serving commands like dingo load <path> and dingo mithril sync can run with the default runMode: serve, but they do not start relay/private listeners, so listener-required validation rejects configs that are valid for those commands. This probably needs command/service-set aware validation, or additional/effective run modes so Validate() can determine which listeners are actually required.

The listener-port requirement keyed off cfg.RunMode, but the one-shot
subcommands (load, sync, mithril) run a fixed operation and keep the
default runMode=serve, so `dingo sync` and `dingo mithril ...` were
wrongly required to have the relay/private/metrics listener ports set.

cmd/dingo now derives an effective run mode from the invoked command and
passes it to Validate(): the bare `dingo` process honors the configured
runMode, `serve` is a serving mode, `load` requires an ImmutableDB
source, and the one-shot utilities (sync, mithril) require neither
listeners nor a source. RunMode.RequiresListeners centralizes the
serving-mode set, and a new effective-only RunModeUtility models the
one-shot commands. The version/list exemption and the load positional-
arg merge now key off the top-level command so nested commands such as
`dingo mithril list` are classified correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney

Copy link
Copy Markdown
Contributor Author

@wolf31o2 fixed in 962a95b — thanks for catching this.

You're right: the listener requirement keyed off cfg.RunMode, but the one-shot subcommands (load, sync, mithril) run a fixed operation and keep the default runMode=serve, so they were wrongly required to have the relay/private/metrics ports set.

Following the first of your suggestions (normalize an effective run mode before validation), cmd/dingo now derives an effective run mode from the invoked command and passes it to Validate():

  • bare dingo → honors the configured runMode (defaults to serve)
  • serve subcommand → serving (requires listeners), regardless of configured runMode
  • load → requires an ImmutableDB source, no listeners
  • sync, mithril (and nested subcommands) → new effective-only RunModeUtility: neither listeners nor a source

RunMode.RequiresListeners() centralizes the serving-mode set. The version/list exemption and the load positional-arg merge now key off the top-level command, so nested commands like dingo mithril list are classified correctly (previously cmd.Name() collided with top-level list).

Verified end-to-end against the built binary:

  • dingo sync / dingo mithril list with relayPort/privatePort/metricsPort: 0 → pass validation and run (no "invalid configuration")
  • dingo serve with those zeroed → still fails with the listener-port errors
  • dingo (runMode=load, no immutableDbPath) → still fails "requires immutableDbPath"
  • dingo version → still prints regardless of config

New unit tests cover effectiveRunMode/isInformationalCommand across the command tree and the utility-mode relaxation in the validator. ARCHITECTURE.md updated. golangci-lint, modernize, import-boundaries, and the config + cmd/dingo suites pass locally.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread cmd/dingo/main.go Outdated
effectiveRunMode passed an unrecognized configured runMode through to
Validate, where RequiresListeners treats unknown modes as needing no
listeners — so listener-port violations would not be aggregated with the
invalid-runMode error. rootCmd dispatches an unrecognized mode to serve,
so effectiveRunMode now does the same, keeping the serving-listener
checks. (In practice runMode is already rejected at load/flag-apply
time; this keeps the validator self-consistent regardless.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the listener validation regression. The new validation should only check ports for listeners that the effective invocation can actually start. Right now utility commands and core-mode serving configs can be rejected because they collide with configured API ports that are inactive for that path.

Comment thread internal/config/validate.go Outdated
{"privatePort", c.PrivatePort, listenersRequired},
{"metricsPort", c.MetricsPort, listenersRequired},
{"debugPort", c.DebugPort, false},
{"utxorpcPort", c.UtxorpcPort, false},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This port set is broader than the listeners that can actually bind for a given invocation. For example, dingo --metrics-port 8080 sync is rejected because metricsPort matches the default meshPort, but sync is classified as a utility and Mesh is not started there. The same shape exists for core-mode serving: UTxORPC/Blockfrost/Mesh/Midnight are gated on API storage mode, so their configured ports should not collide with active listeners when those APIs are inactive. Please derive the validated/deduped port set from the effective invocation mode plus storage mode, so we only reject ports that could actually bind together.

Comment thread cmd/dingo/main.go
return config.RunModeServe
case "load":
return config.RunModeLoad
default:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch-all RunModeUtility is probably too coarse once validation depends on listener surfaces. Please consider creating distinct effective run modes or listener profiles for load, sync, mithril, and serving modes instead of funneling all non-serve/load commands through one utility mode. That keeps load and sync from inheriting validations for listeners they do not start, while still allowing commands that do start metrics/debug or future utility listeners to validate those ports explicitly.

The startup validator range-checked, privilege-checked, and
collision-checked a fixed set of nine ports regardless of whether a given
invocation would actually bind them. That produced false rejections:
`dingo --metrics-port 8080 sync` was rejected because the metrics port
matched the default mesh port even though `sync` starts no Mesh listener,
and core-mode serving configs could collide with the
UTxORPC/Blockfrost/Mesh/Midnight ports that only bind under API storage.

Derive the validated and deduplicated port set from the effective run mode
plus the storage mode, mirroring the gating in (*dingo.Node).Start and
cmd/dingo's node.Run/mithril paths: relay/private/bark bind only in serving
modes; metrics/debug bind in serving modes and the sync/mithril utilities;
UTxORPC/Blockfrost/Mesh/Midnight bind only under API storage (dev mode
forces API on). A port for an inactive listener is skipped entirely, so it
is neither range-checked nor counted toward a collision.

Replace the coarse effective-only RunModeUtility with distinct RunModeSync
and RunModeMithril so each one-shot command validates exactly the ports it
starts (metrics/debug) without inheriting the relay/private/API checks it
does not, and so a future utility listener can opt into its port checks
explicitly.

Update ARCHITECTURE.md and the validation tests accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO-GO: startup validation blocks mithril list / mithril show on metrics/debug port settings they do not use. Please split effective validation for Mithril read-only subcommands from Mithril sync, or skip auxiliary listener validation for list/show, and add a command-mode regression test.

Comment thread cmd/dingo/main.go
return config.RunModeLoad
case "sync":
return config.RunModeSync
case "mithril":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maps every mithril subcommand to the mode that validates auxiliary listeners. mithril sync does start metrics/debug, but mithril list and mithril show only query the aggregator and never bind those ports. Repro on this branch: go run ./cmd/dingo --metrics-port 99999999 mithril list fails during config validation with invalid metricsPort, even though that command does not use the metrics listener. Please distinguish mithril sync from the read-only Mithril subcommands before validation.

chrisguiney and others added 2 commits July 10, 2026 10:22
The previous change mapped every `mithril` subcommand to the effective
mode that validates the metrics and debug ports. But only `mithril sync`
(and `sync --mithril`) starts those listeners; `mithril list` and
`mithril show` just query the aggregator and bind nothing, so
`dingo --metrics-port 99999999 mithril list` was rejected at startup for
a port it never uses.

Resolve the effective run mode from the invoked command rather than only
its top-level name: `mithril sync` maps to RunModeSync (metrics/debug
validated), while the read-only `mithril list`/`show` and bare `mithril`
map to RunModeMithril, which now validates no listener ports. RunModeSync
becomes the Mithril snapshot sync operation shared by `dingo sync
--mithril` and `dingo mithril sync`.

Add command-mode regression tests distinguishing the two, plus a
validation test that read-only Mithril mode ignores metrics/debug ports.
Update ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
- Exempt cobra's built-in help and completion commands from config
  validation: they run the root PersistentPreRunE, so an invalid
  config blocked `dingo help` and shell-completion generation.
- Validate the API listener ports when the configured runMode is
  "dev" even though `dingo serve` yields an effective serve mode:
  node.Run keys dev behavior (which forces API storage) off the
  configured mode, so those listeners bind.
- Allow sub-1024 ports for non-root processes that can actually bind
  them: CAP_NET_BIND_SERVICE and a zeroed
  net.ipv4.ip_unprivileged_port_start (set inside containers by
  runtimes such as Docker) now pass the privileged-port check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/config/validate.go Outdated
Recognizing only a zeroed net.ipv4.ip_unprivileged_port_start missed
deployments that lower the cutoff to a nonzero value (e.g. 80): the
kernel permits binding such ports as non-root, but validation still
rejected them. Thread the cutoff itself through the port checks —
minBindablePort() returns 0 for root/Windows/CAP_NET_BIND_SERVICE and
the sysctl value otherwise — and compare each active port to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-go for merge on the current head. The earlier command-specific listener findings are addressed, and the merge-result tests and linters are clean, but the inline validation regressions below remain blocking.

Comment thread internal/config/validate.go Outdated
if path == "" {
return nil
}
cleaned := filepath.Clean(path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: cleaning the path before checking it removes the traversal component being guarded against. For example, configs/../secret.json becomes secret.json and passes validation; the local CLI repro reports only an unrelated forced port error. The issue and ARCHITECTURE.md both promise no .. components. Inspect the original path components before cleaning (with appropriate platform separator handling), and change the test that currently accepts an inner dotdot.

Comment thread internal/config/validate.go Outdated
c.MempoolCapacity,
))
}
if c.EvictionWatermark <= 0 || c.EvictionWatermark >= 1.0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: these comparisons accept NaN because every ordered comparison with NaN is false. --eviction-watermark NaN passes this validator and then reaches mempool threshold conversion/arithmetic. Reject non-finite values explicitly (or express the range check so NaN cannot satisfy it) for both watermarks, and add CLI/config regression coverage.

Comment thread cmd/dingo/main.go Outdated

// version and list are informational and start no services, so
// they must still run even when the merged config is invalid.
if !isInformationalCommand(top) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this exemption occurs after LoadConfig, so informational commands still fail on configuration loading/validation errors. Repro: dingo --config /tmp/does-not-exist version exits with “failed to load config” instead of printing the version. Classify and bypass runtime config loading before LoadConfig for version/list/help/completion, or narrow any loading to data those commands actually require.

}

// Network identity
if c.Network == "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking integration mismatch: this branch says a custom networkMagic is sufficient when network is empty, but startup cannot reach it. LoadConfig still unconditionally calls ValidateNetworkName(""), and ApplyFlags rejects --network= before Validate. A YAML config with network: "" and networkMagic: 42 fails in LoadConfig. Remove or reconcile the earlier fail-fast validation so the advertised contract and aggregation actually apply.

Comment thread internal/config/validate.go Outdated
}
// Two active listeners sharing a port only fails at bind time; catch it
// here. Zero ports are disabled or OS-assigned, so they don't clash.
seenPorts := make(map[uint]string, len(ports))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking false positive: port number alone is not a listener identity. Active listeners can legally share a port on distinct specific bind addresses, but this map rejects them. For example, debug on 127.0.0.1:13000 and Midnight on 127.0.0.2:13000 are bindable together yet are reported as a collision. Include the effective bind address (and wildcard overlap semantics) in collision detection, or leave bind conflicts to the existing synchronous listener startup paths.

- Reject any ".." path component in cardanoConfig by inspecting the
  original path: cleaning first erased inner components, letting
  traversal-shaped paths like "configs/../secret.json" through.
- Check watermarks for NaN explicitly; every ordered comparison with
  NaN is false, so --eviction-watermark NaN passed the range checks
  and reached mempool threshold arithmetic.
- Skip config loading (not just validation) for the informational
  version/list/help/completion commands so they work with a missing
  or unreadable config file.
- Make the networkMagic-without-network contract reachable: LoadConfig
  no longer rejects an empty network name, the --network flag accepts
  an explicit empty value, and topology resolution returns an empty
  topology (as dev mode does) when there is no network name to resolve
  embedded topology or bootstrap peers from. Validate() enforces that
  network or networkMagic is set.
- Make port-collision detection bind-address aware: listeners on
  distinct specific addresses (e.g. 127.0.0.1 and 127.0.0.2) may share
  a port; only equal or wildcard-overlapping bind addresses collide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmd/dingo/main.go (1)

445-453: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Defer all semantic validation until after ApplyFlags.

By the time Line 453 runs, LoadConfig at Line 436 has already rejected invalid run modes, block-producer credentials, watermarks, and topology settings. Consequently, a higher-precedence CLI flag cannot repair an invalid YAML/env value, mode-dependent defaults may use the pre-CLI mode, and violations are not aggregated by Config.Validate.

Make LoadConfig parse and merge only; perform semantic checks and mode-dependent defaulting after flags are applied.

As per coding guidelines, configuration precedence must be CLI > env > YAML > defaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/dingo/main.go` around lines 445 - 453, Update the configuration flow
around LoadConfig, ApplyFlags, and Config.Validate so LoadConfig only parses and
merges defaults, YAML, and environment values without semantic validation or
mode-dependent defaulting. Apply all CLI flags first, then derive mode-dependent
defaults and run Config.Validate using the final effective configuration,
preserving CLI > env > YAML > defaults precedence and allowing validation errors
to be aggregated after overrides.

Source: Coding guidelines

internal/config/validate_test.go (1)

64-77: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Move RunMode/StartEra validation into Config.Validate.
LoadConfig still short-circuits on those fields, so startup can’t report both errors together. Add a LoadConfig regression for the combined failure case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/validate_test.go` around lines 64 - 77, Move the RunMode and
StartEra checks from LoadConfig into Config.Validate, preserving their existing
invalid-value errors and allowing validation to accumulate both failures. Add a
LoadConfig regression test that supplies invalid values for both fields and
asserts the combined error reports both issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/dingo/config_parity_test.go`:
- Around line 26-60: Replace the hand-maintained canonical slices in
TestChainsyncStrategyWhitelistParity and the corresponding Mithril parity test
with shared accepted-value tables. Define those tables in a package accessible
to both cmd/dingo and internal/config, then update
config.AcceptedChainsyncStrategies and config.AcceptedMithrilBackends and the
parity tests to reuse them while preserving the existing parser checks.

---

Outside diff comments:
In `@cmd/dingo/main.go`:
- Around line 445-453: Update the configuration flow around LoadConfig,
ApplyFlags, and Config.Validate so LoadConfig only parses and merges defaults,
YAML, and environment values without semantic validation or mode-dependent
defaulting. Apply all CLI flags first, then derive mode-dependent defaults and
run Config.Validate using the final effective configuration, preserving CLI >
env > YAML > defaults precedence and allowing validation errors to be aggregated
after overrides.

In `@internal/config/validate_test.go`:
- Around line 64-77: Move the RunMode and StartEra checks from LoadConfig into
Config.Validate, preserving their existing invalid-value errors and allowing
validation to accumulate both failures. Add a LoadConfig regression test that
supplies invalid values for both fields and asserts the combined error reports
both issues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca4af416-36c5-4b8c-a9b9-b3d131158f3e

📥 Commits

Reviewing files that changed from the base of the PR and between f183c2f and dd2bd62.

📒 Files selected for processing (9)
  • ARCHITECTURE.md
  • cmd/dingo/command_mode_test.go
  • cmd/dingo/config_parity_test.go
  • cmd/dingo/main.go
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/config/flags.go
  • internal/config/validate.go
  • internal/config/validate_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • ARCHITECTURE.md

Comment on lines +26 to +60
// internal/config cannot import chainsync or mithril without pulling
// node subsystems into the config package, so the accepted-value sets
// it validates against (config.AcceptedChainsyncStrategies and
// config.AcceptedMithrilBackends) are duplicated from those downstream
// parsers. These parity tests live in cmd/dingo, which can import all
// three, and guard against drift in both directions:
//
// - forward: every value config accepts must be accepted by the real
// parser, so a config never passes Validate() only to fail at
// startup;
// - reverse: config's accepted set must match the parser's contract
// exactly, so a value added to the parser without updating config
// (which would spuriously reject a valid config) is caught too.

// TestChainsyncStrategyWhitelistParity checks config.AcceptedChainsyncStrategies
// against chainsync.ParseHeaderSyncStrategy.
func TestChainsyncStrategyWhitelistParity(t *testing.T) {
// canonical is the contract of chainsync.ParseHeaderSyncStrategy;
// it mirrors that function's switch. The set-equality check below
// ties config's list to it, and the parse check ties it to the real
// parser, so a change to the parser's switch fails this test until
// both this list and config's list are updated.
canonical := []string{
"", "primary", "parallel", "round-robin", "roundrobin", "round_robin",
}
assertWhitelistParity(
t,
"chainsync.strategy",
config.AcceptedChainsyncStrategies,
canonical,
func(v string) error {
_, err := chainsync.ParseHeaderSyncStrategy(v)
return err
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the parity test and the referenced parsers/contracts.
git ls-files cmd/dingo | sed -n '1,120p'
printf '\n--- config_parity_test.go ---\n'
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'

printf '\n--- parser definitions ---\n'
rg -n "ParseHeaderSyncStrategy|AcceptedChainsyncStrategies|AcceptedMithrilBackends|Parse.*Mithril" -S .

Repository: blinklabs-io/dingo

Length of output: 8149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map candidate files before reading implementation slices.
fd -a ".*(chainsync|mithril).*" .
fd -a "config.*\.go|.*parity.*\.go" cmd/dingo

Repository: blinklabs-io/dingo

Length of output: 1892


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'

Repository: blinklabs-io/dingo

Length of output: 5358


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'pwd=%s\n' "$PWD"
git ls-files cmd/dingo/config_parity_test.go
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'

Repository: blinklabs-io/dingo

Length of output: 5413


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "TestChainsyncStrategyWhitelistParity|assertWhitelistParity|canonical := \[\]string" cmd/dingo/config_parity_test.go

Repository: blinklabs-io/dingo

Length of output: 547


Use a shared accepted-value table

Both parity tests still depend on hand-maintained canonical slices, so a new value accepted by chainsync.ParseHeaderSyncStrategy or resolveMithrilBackend can slip past this check until the list is updated manually. Define the accepted values once and reuse them here and in internal/config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/dingo/config_parity_test.go` around lines 26 - 60, Replace the
hand-maintained canonical slices in TestChainsyncStrategyWhitelistParity and the
corresponding Mithril parity test with shared accepted-value tables. Define
those tables in a package accessible to both cmd/dingo and internal/config, then
update config.AcceptedChainsyncStrategies and config.AcceptedMithrilBackends and
the parity tests to reuse them while preserving the existing parser checks.

The parity tests compared internal/config's accepted-value lists
against hand-maintained canonical slices, so a value added to
chainsync.ParseHeaderSyncStrategy or resolveMithrilBackend slipped
past until someone remembered to update the test.

Export the accepted sets from the parser packages and derive the
parsers from them: chainsync.AcceptedHeaderSyncStrategyNames shares
one name table with ParseHeaderSyncStrategy, and cmd/dingo's
resolveMithrilBackend accepts mithril.AcceptedBackends plus the empty
default. The parity tests now compare config's duplicated lists (still
required by the import boundary) against those exported lists, so
extending a parser fails the tests until config catches up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on the current head. The earlier command-specific listener/path/NaN/networkMagic/bind-address findings are fixed, and the focused and conformance tests pass, but the startup flow still violates configuration precedence: LoadConfig performs semantic checks and mode-dependent defaulting before ApplyFlags. Please defer those operations until after flags, then validate/finalize the fully merged configuration. DevNet remains to be rerun after the fix.

Comment thread cmd/dingo/main.go
cfg.ImmutableDbPath = args[0]
}

if err := cfg.Validate(effectiveRunMode(cmd, cfg)); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation call is too late to provide the advertised fully merged validation. LoadConfig has already rejected invalid runMode/startEra, block-producer credentials, and watermarks, and has already selected mode-dependent defaults before ApplyFlags. Confirmed repro: YAML runMode: bogus plus higher-precedence --run-mode serve fails inside LoadConfig; --run-mode leios can also retain the Praos mempool default selected earlier. Make LoadConfig parse/merge only, apply CLI flags, derive defaults from the final mode, then call Validate. Please add precedence, defaulting, and aggregation regression tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb45d3b. LoadConfig now only parses and merges the YAML and environment sources — the runMode/startEra/watermark/block-producer fail-fasts and the mode-dependent MempoolCapacity defaulting are gone from it. A new Config.ApplyDefaults derives defaults from the fully merged configuration, and cmd/dingo calls it after ApplyFlags (and the load positional-arg merge), before Validate. Regression tests added in internal/config/flags_test.go run the full pipeline: an invalid YAML runMode overridden by --run-mode serve now validates cleanly (and is still rejected without the override), --run-mode leios gets the Leios mempool default, and multiple bad settings aggregate into a single Validate error.

LoadConfig rejected invalid runMode/startEra/watermark/block-producer
values and chose the mode-dependent MempoolCapacity default before
ApplyFlags ran, so a CLI flag could neither override an invalid YAML
value (runMode: bogus + --run-mode serve failed in LoadConfig) nor
influence a derived default (--run-mode leios kept the Praos mempool
capacity chosen from the YAML-era mode).

LoadConfig now only parses and merges YAML and environment sources.
The new Config.ApplyDefaults fills in defaults derived from the merged
configuration (runMode, mempool capacity, watermarks, forge slot
thresholds, history-expiry frequency) and cmd/dingo calls it after
ApplyFlags, before Validate, which already covers every semantic check
LoadConfig used to fail fast on — and reports all violations together
instead of stopping at the first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney

Copy link
Copy Markdown
Contributor Author

DevNet rerun on cb45d3b (internal/test/devnet/run-tests.sh): 9 of 10 scenarios pass, including TestSustainedConsensus and TestEpochBoundaryConsensus. TestChainGrowthRate fails (Dingo forged 0 blocks in the 100-slot window), but the same failure reproduces in this environment on the previously reviewed head ef87d7f (0 blocks) and on the upstream/main merge-base 7aa64cd (1 block), so it is pre-existing and not attributable to this PR. Unit, conformance, and lint runs on the merged result are clean.

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on the current head. The prior CLI-precedence and mode-dependent-defaulting blocker is fixed, the branch merges cleanly with current main, and the focused race-enabled and conformance tests pass. One configuration safety issue remains: an explicitly negative history-expiry frequency is silently converted to the one-hour default, so a node can start pruning when the operator supplied a value they may reasonably expect to disable expiry or fail startup. Please preserve the negative value through defaulting, reject it in Validate, and cover YAML, environment, and CLI inputs. DevNet remains to be rerun after the fix.

Comment thread internal/config/config.go Outdated
if c.ForgeStaleGapThresholdSlots == 0 {
c.ForgeStaleGapThresholdSlots = DefaultForgeStaleGapThresholdSlots
}
if c.HistoryExpiry.Frequency <= 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition conflates an unset zero value with an explicitly negative duration. For example, --history-expiry-enabled --history-expiry-frequency=-1s reaches this code, is silently rewritten to 1h, passes validation, and starts the history-expiry worker. The resulting failure mode is particularly unsafe: instead of failing startup (or disabling expiry), the node periodically replaces stable historical block CBOR with expiry markers. Default only Frequency == 0; preserve negative values for Validate to reject.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b61c234: ApplyDefaults now only fills in an unset (zero) frequency, so an explicitly negative value survives defaulting for Validate to reject. Repro now fails startup: dingo --history-expiry-enabled --history-expiry-frequency=-1s ...invalid configuration: invalid historyExpiry.frequency: -1s (must be positive). The pruner's own Frequency <= 0 guard in internal/historyexpiry remains as defense in depth for library callers that bypass Validate.

"invalid %s %q: must be positive", d.setting, d.value,
))
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate historyExpiry.frequency as positive after defaults are applied. Currently ApplyDefaults erases a negative value before this validator runs, so the configuration error cannot be reported or aggregated. After changing defaulting to handle only zero, append an error when the frequency is negative (or non-positive if validation may be called without ApplyDefaults). Add regressions for YAML historyExpiry.frequency: -1s, DINGO_HISTORY_EXPIRY_FREQUENCY=-1s, and --history-expiry-frequency=-1s, especially with history expiry enabled, and assert startup fails rather than using the one-hour default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b61c234: Validate rejects a non-positive historyExpiry.frequency (non-positive rather than just negative, so a caller that skips ApplyDefaults is also caught), aggregated with the other errors. Regression tests cover all three sources with expiry enabled — YAML historyExpiry.frequency: -1s, DINGO_HISTORY_EXPIRY_FREQUENCY=-1s, and --history-expiry-frequency=-1s — via the full LoadConfig→ApplyFlags→ApplyDefaults→Validate pipeline in flags_test.go, asserting validation fails and the negative value is preserved through defaulting.

ApplyDefaults rewrote any non-positive historyExpiry.frequency to the
one-hour default, so an operator who configured a negative value —
plausibly expecting expiry to be disabled or startup to fail — got a
node that silently prunes historical block CBOR on a cadence they never
chose. Only an unset (zero) frequency now takes the default; a negative
value survives defaulting and Validate rejects it alongside any other
configuration errors. Regression tests cover the YAML, environment, and
CLI flag sources with history expiry enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney

Copy link
Copy Markdown
Contributor Author

DevNet rerun on b61c234: 8 of 10 scenarios pass, including TestSustainedConsensus and TestEpochBoundaryConsensus. TestChainGrowthRate fails with the same 0-blocks signature that reproduces on the upstream/main merge-base in this environment (see earlier comment), and TestBasicBlockForging failed this round with the same underlying forging stall (it passed on the previous run of cb45d3b). Both match the pre-existing local block-forging issue that is independent of this PR. Unit, conformance (via make test -race, 52 packages, 0 failures), and lint runs on b61c234 are clean.

@wolf31o2 wolf31o2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-go for merge on the current head. The previously reported history-expiry issue is fixed, the branch merges cleanly with current main, and focused race, conformance, and import-boundary checks pass. However, topology is still loaded before CLI overrides and before the final aggregated validation pass, so the startup flow continues to violate CLI > env > YAML precedence for network and topology. Please defer topology resolution until the fully merged configuration has been defaulted and validated, and add regression coverage for CLI overrides of invalid YAML values.

Comment thread internal/config/config.go
if err := ValidateNetworkName(globalConfig.Network); err != nil {
return nil, err
}
// LoadConfig only parses and merges configuration sources; it makes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] LoadConfig still performs topology resolution below this comment (LoadTopologyConfig at line 1014), so it does more than parse and merge. This can reject a lower-precedence value before CLI flags are applied: I reproduced network: ../bad failing here with unknown network even when the invocation intends to supply --network preview. Remove topology loading from LoadConfig so a CLI override can repair YAML/env values as promised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cfc226f: LoadConfig no longer resolves topology (or anything else derived from mergeable settings) — it parses and merges YAML/env only. Your repro now works: YAML network: ../bad with --network preview proceeds past config loading, validation, and topology resolution; without the override the value is rejected by Validate (invalid network name "../bad"), aggregated with any other errors.

Comment thread cmd/dingo/main.go
cfg.ImmutableDbPath = args[0]
}

// Every configuration source is merged at this point (defaults,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This is not yet the first point at which every configuration source has safely merged: both LoadConfig and ApplyFlags resolve topology before ApplyDefaults/Validate. I also reproduced a missing YAML topology file aborting before a valid --topology override can take effect. Resolve/load topology once after flags, defaults, and validation, and cover both the --network and --topology override cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cfc226f: the ApplyFlags topology reload is removed too, and topology is resolved exactly once in cmd/dingo after ApplyFlags → ApplyDefaults → Validate. Both override cases are covered by pipeline regression tests in flags_test.go (TestPipeline_NetworkFlagRepairsInvalidYAMLNetwork, TestPipeline_TopologyFlagRepairsMissingYAMLTopology), each also asserting the no-override invocation still fails — the bad network in Validate, the missing topology file at topology resolution. Verified against a real binary: --config badtopo.yaml --topology topo.json load ... starts, and without the flag fails with loading topology: failed to load topology file: ....

LoadConfig resolved topology as a side effect, and ApplyFlags resolved
it again, both before ApplyDefaults and Validate ran. Because topology
derives from the network and topology settings, a YAML or environment
value could abort startup before the CLI flag that repairs it was ever
consulted: a traversal-shaped YAML network failed topology resolution
inside LoadConfig despite an explicit --network override, and a missing
YAML topology file aborted despite a valid --topology override.

Topology is now resolved exactly once, in cmd/dingo after ApplyFlags,
ApplyDefaults, and Validate, so it always operates on the final,
validated configuration. Pipeline regression tests cover both override
cases and their no-override rejections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Chris Guiney <chris@guiney.net>
@chrisguiney

Copy link
Copy Markdown
Contributor Author

DevNet rerun on cfc226f: all three nodes come up healthy with the relocated topology resolution (Dingo loads its explicit topology file via the new post-validation path), and 8 of 10 scenarios pass including TestSustainedConsensus and TestEpochBoundaryConsensus. TestChainGrowthRate (2 blocks) and TestBasicBlockForging fail with the same forging-stall signature previously reproduced on the upstream/main merge-base in this environment — pre-existing, not from this PR. Full make test (-race, 52 packages including conformance), golangci-lint, modernize, and import-boundaries are clean on cfc226f.

@wolf31o2
wolf31o2 merged commit 210e669 into blinklabs-io:main Jul 17, 2026
8 of 12 checks passed
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.

Config: add Validate() method for startup validation

2 participants