Skip to content

feat: gate skip-app-hash-validation behind //go:build shadow via ConsensusPolicy#3401

Open
bdchatham wants to merge 1 commit intomainfrom
feat/skip-apphash-shadow-buildtag
Open

feat: gate skip-app-hash-validation behind //go:build shadow via ConsensusPolicy#3401
bdchatham wants to merge 1 commit intomainfrom
feat/skip-apphash-shadow-buildtag

Conversation

@bdchatham
Copy link
Copy Markdown
Contributor

@bdchatham bdchatham commented May 6, 2026

Why

Shadow nodes need to bypass header / state hash checks to validate alternate execution engines (e.g., Giga V2) against canonical-chain blocks. The previous shape exposed --skip-app-hash-validation as a runtime CLI flag on every seid binary, which made the production validator one config edit away from accepting bad blocks.

Approach

The build tag is the toggle — there is no runtime knob.

// types/consensus_policy_default.go (//go:build !shadow)
type ConsensusPolicy struct{}
func (ConsensusPolicy) SkipAppHashValidation() bool { return false }

// types/consensus_policy_shadow.go (//go:build shadow)
type ConsensusPolicy struct{}
func (ConsensusPolicy) SkipAppHashValidation() bool { return true }

ConsensusPolicy is an empty struct in both builds; the method returns a different constant per build and the compiler folds it. Bypass branches DCE in production. There is no Go API in either build to construct a policy with the opposite behavior.

Block.ValidateBasic is split: shape-only checks stay there for deserializers, hash-integrity checks move to Block.ValidateHashes(policy). validateBlock(state, block, policy) plumbs the policy to BlockExecutor, threaded from node.New down. Production start.go passes DefaultConsensusPolicy() unconditionally — no cobra, no AppOptions, no viper.

This eliminates two failure modes the runtime-flag shape allowed: (1) production binary accepting bad blocks via flag flip, (2) shadow binary silently behaving like production when the operator forgets to pass the flag.

Coverage

All 6 hash gates from the original implementation (commits 9fa70d2ff, 4ce5ed348, d9b7f9283) are preserved: AppHash, LastCommitHash, DataHash, EvidenceHash, ValidatorsHash, NextValidatorsHash. SkipLastResultsHashValidation is left as-is — that's a first-class production feature gated on gigaExecutorConfig.Enabled, different security model.

.github/workflows/ecr.yml publishes sei-chain:shadow-<sha> alongside the existing prod and mock_balances images.

Verification

  • strings seid-default | grep skip-app-hash → 0 hits (no flag, no string in either binary)
  • go build ./... and go build -tags shadow ./... both clean

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedMay 6, 2026, 11:17 PM

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.04%. Comparing base (0c26364) to head (721d5ff).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3401      +/-   ##
==========================================
- Coverage   59.04%   59.04%   -0.01%     
==========================================
  Files        2105     2105              
  Lines      173297   173269      -28     
==========================================
- Hits       102327   102300      -27     
+ Misses      62091    62090       -1     
  Partials     8879     8879              
Flag Coverage Δ
sei-db 70.41% <ø> (ø)

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

Files with missing lines Coverage Δ
sei-cosmos/server/start.go 23.48% <ø> (ø)
sei-tendermint/internal/consensus/replay.go 68.16% <ø> (-0.97%) ⬇️
sei-tendermint/internal/state/execution.go 80.14% <ø> (-0.44%) ⬇️
sei-tendermint/internal/state/validation.go 100.00% <ø> (ø)
sei-tendermint/node/node.go 64.64% <ø> (-0.37%) ⬇️
sei-tendermint/node/public.go 63.15% <ø> (ø)
sei-tendermint/rpc/test/helpers.go 75.40% <ø> (ø)
sei-tendermint/test/e2e/node/main.go 0.00% <ø> (ø)
sei-tendermint/types/block.go 86.12% <ø> (-0.07%) ⬇️
🚀 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 bdchatham force-pushed the feat/skip-apphash-shadow-buildtag branch from b20f4d8 to 5908b0a Compare May 6, 2026 22:58
@bdchatham bdchatham changed the title feat: gate skip-app-hash-validation behind //go:build shadow feat: gate skip-app-hash-validation behind //go:build shadow via ConsensusPolicy May 6, 2026
…ensusPolicy

Replaces the runtime --skip-app-hash-validation CLI flag with a
build-tag-gated ConsensusPolicy value owned by BlockExecutor. The
seid validator binary built without -tags shadow cannot bypass
AppHash, LastCommitHash, DataHash, EvidenceHash, ValidatorsHash, or
NextValidatorsHash checks — the flag, the bypass field, and the
bypass branches do not exist in the production binary. The shadow
binary always runs in shadow mode; there is no runtime knob.

Why no runtime config

Earlier drafts threaded a CLI flag and AppOptions value into a
ConsensusPolicy{skipAppHashValidation bool} struct. That worked but
left an operator footgun: a shadow binary would silently behave like
production unless the operator remembered to pass the flag, then fail
on the first AppHash mismatch and surprise the operator.

The build tag IS the toggle. The shadow binary's purpose is to
shadow-sync, period. ConsensusPolicy carries no fields; the method
SkipAppHashValidation() returns a different constant in each build
and the compiler folds it.

Refactor

- types.ConsensusPolicy is an empty struct in both builds. Method
  set differs: default returns false, shadow returns true.
  DefaultConsensusPolicy() is the only constructor.
- Block.ValidateBasic now does shape-only validation. The 3 hash
  checks (LastCommitHash / DataHash / EvidenceHash) move into a new
  Block.ValidateHashes(policy) gated by SkipAppHashValidation.
  Deserialization paths (BlockFromProto, light-client, store load)
  call ValidateBasic and pick up the shape-only contract.
- validateBlock(state, block, policy) plumbs the policy to the
  AppHash / ValidatorsHash / NextValidatorsHash gates.
- BlockExecutor carries policy as a struct field; NewBlockExecutor
  takes it as a constructor arg.
- node.New / makeNode / Handshaker thread the policy through.
- sei-cosmos/server/start.go passes tmtypes.DefaultConsensusPolicy()
  unconditionally. No CLI flag, no viper, no AppOptions.

What stays unchanged

tmtypes.SkipLastResultsHashValidation remains a global atomic.Bool
flipped from gigaExecutorConfig.Enabled. Giga is a first-class
runtime-togglable production feature with intentional gas-accounting
divergence; that bypass is not in scope.

Empirical proof
- strings seid-default | grep skip-app-hash → 0 hits
- strings seid-shadow  | grep skip-app-hash → 0 hits
  (the flag literal never existed since we never registered it)
- Both builds clean: go build ./... and go build -tags shadow ./...
- Original 6 hash gates (AppHash, LastCommitHash, DataHash,
  EvidenceHash, ValidatorsHash, NextValidatorsHash from commits
  9fa70d2, 4ce5ed3, d9b7f9283) preserved.

Workflow

.github/workflows/ecr.yml adds sei-chain:shadow-<sha> mirroring the
existing mock_balances build-tag pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bdchatham bdchatham force-pushed the feat/skip-apphash-shadow-buildtag branch from 5908b0a to 721d5ff Compare May 6, 2026 23:16
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