Refuse to start a mock_chain_validation build outside its reserve role - #4147
Refuse to start a mock_chain_validation build outside its reserve role#4147blindchaser wants to merge 1 commit into
Conversation
A mock_chain_validation binary swallows ErrAppHash and the validator-set sentinels, which is what lets a reserve node keep following a chain it disagrees with. That same property makes two configurations dangerous rather than merely wrong: as a validator it would prevote and precommit blocks whose app hash contradicts its own state instead of prevoting nil, and left unpinned it resolves to auto and joins the migration on the first block after governance raises the batch size, spending the reserve with nothing to signal that it happened. Both are now refused in startInProcess, which every deployed node reaches and no test does. The unpinned case is the default state of a fresh node rather than an unlikely slip: a generated app.toml renders sc-write-mode = "memiavl_only" but not sc-write-mode-enable-auto, and while that unrendered key stays true it discards the rendered one. Production builds compile the no-op variant and are unaffected. Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit a161677)
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
PR SummaryMedium Risk Overview The guard runs in Tests cover both build tags: production accepts all mode × write-mode pairs; Reviewed by Cursor Bugbot for commit 31a9ded. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (33.33%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## release/v6.7 #4147 +/- ##
================================================
- Coverage 61.34% 60.30% -1.04%
================================================
Files 2163 2065 -98
Lines 188785 177105 -11680
================================================
- Hits 115813 106812 -9001
+ Misses 62254 60517 -1737
+ Partials 10718 9776 -942
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
A small, well-placed startup guard: startInProcess is genuinely the only production node-start path (the other node.New call sites are test/inprocess-tagged), and config.StateCommit.WriteMode from GetConfig is the post-ApplyWriteModeAuto effective mode, so the pin check is reading the right value. No blockers; two non-blocking gaps — the sibling mock_block_validation image has the same validator hazard and is left unguarded, and the guard's godoc carries rationale the repo guide says belongs inline.
Findings: 0 blocking | 3 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- None at the file/PR level.
- 2 suggestion(s)/nit(s) flagged inline on specific lines.
- 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.
Pre-existing issues
- [suggestion] No CI job runs
go test -tags mock_chain_validation(go-test.ymlandsei-db-tests.ymlsetGOFLAGS=-tags=ledger,test_ledger_mock), so the entiremock_chain_validationtest surface — the pre-existingsei-tendermint/types/consensus_policy_mock_chain_validation_test.goand friends, and nowsei-cosmos/server/reserve_policy_mock_chain_validation_test.go— is neither run nor compile-checked on a PR. The only thing that compiles that build is the on-demand/nightly ECR image job, so a break in tag-gated code surfaces at image-build time rather than at review time.
| @@ -0,0 +1,10 @@ | |||
| //go:build !mock_chain_validation | |||
There was a problem hiding this comment.
[suggestion] The !mock_chain_validation tag means a mock_block_validation binary compiles this no-op, but that build has the same voting hazard the guard exists to close: sei-tendermint/types/consensus_policy_mock_block_validation.go swallows ErrAppHash (plus ErrDataHash, ErrUpgradeBeforeTrigger), so such a validator would also prevote and precommit blocks whose app hash contradicts its own state. It is a shipped image too — .github/workflows/ecr.yml pushes sei-chain:mock_block_validation-<ref> alongside the chain-validation one.
The write-mode pin is specific to the reserve-node role and shouldn't apply there, but the validator half arguably should. Widening the guard file to !mock_block_validation && !mock_chain_validation and giving mock_block_validation a variant that refuses ModeValidator only would make "a build that cannot detect divergence must not vote" the invariant rather than a per-tag convention. If that is deliberately out of scope for this PR, a one-line note saying so would help the next person reading this tag.
| // Tendermint mode and effective state-commit write mode. This build starts | ||
| // only as a non-validator pinned to memiavl_only. | ||
| // | ||
| // Both conditions are what make the build a reserve rather than a liability. |
There was a problem hiding this comment.
[suggestion] AGENTS.md § Godoc asks for what, not why or how: "Rationale, trade-offs, and mechanism belong in an inline comment at the line that needs them, or nowhere", and "Multi-paragraph godocs are rare." These two paragraphs are rationale (why a validator is unsafe, why an unpinned node spends the reserve) and a trade-off (restart cost vs. divergent block) attached to the doc comment.
The content is worth keeping — it just belongs at the branches it explains. Suggest trimming the godoc to the first paragraph (lines 12-14, which already says exactly what the function does) and moving the validator rationale above the nodeMode == tmcfg.ModeValidator check and the migration/pin rationale above the writeMode != sctypes.MemiavlOnly check.
On top of
v6.7.0-rc1. That tag is currently the head ofrelease/v6.7(
e59189ea7), so this branch is the tag plus one commit and the cherry-pick wasclean.
What this changes
mock_chain_validationbuilds now refuse to start unless they are a non-validatorpinned to
memiavl_only. Production builds compile a no-op and are unaffected.The check is one function with two build variants, called once from
startInProcess:Why
A
mock_chain_validationbinary swallowsErrAppHashand the validator-setsentinels. That is exactly what makes it useful as a reserve node — it keeps
following a chain it disagrees with — and it is also what makes two configurations
dangerous rather than merely wrong.
As a validator it would prevote and precommit blocks whose app hash contradicts
its own state, instead of prevoting nil. A node that cannot detect divergence must not
vote on it.
Left unpinned it resolves to
autoand joins the migration on the first blockafter governance raises the batch size, spending the reserve with nothing to signal
that it happened.
The unpinned case is the default state of a fresh node rather than an unlikely slip.
A generated
app.tomlrenderssc-write-mode = "memiavl_only"but does notrender
sc-write-mode-enable-auto, and while that unrendered key staystrueitdiscards the rendered one. So pinning the visible key is not enough, which is why the
error message names the invisible one.
Where the guard lives
startInProcessis the single function every deployed node passes through and no testreaches, so the guard is an invariant rather than a convention a later call site can
forget. Refusing at startup costs a restart to discover, and saves finding out at the
first divergent block.
Testing
Both build variants are covered, since the interesting behaviour is which variant
compiles:
TestAssertReserveNodeAllowed_Default_AcceptsEveryCombinationsweeps all 3 nodemodes × 9 write modes and asserts a production build refuses nothing.
TestAssertReserveNodeAllowed_MockChainValidation_Matrixsweeps the same grid andasserts exactly one cell is allowed.
TestAssertReserveNodeAllowed_MockChainValidation_RefusesPinnedValidatorcovers acorrectly pinned validator, so the write-mode half cannot mask the mode half.
TestAssertReserveNodeAllowed_MockChainValidation_ErrorNamesTheAutoKeypins thatthe message names
sc-write-mode-enable-auto, because that is the key the operatorhas to set and the one a generated
app.tomlnever shows them.go test ./sei-cosmos/server/passes with and without-tags mock_chain_validation,and
make fmtcheckis clean.Release note
v6.7.0-rc1is already cut, so merging this implies an rc2.