Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 58 additions & 32 deletions sei-tendermint/internal/state/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
//-----------------------------------------------------
// Validate block

// Swallow-eligible failure sites in this function consult
// types.ConsensusPolicy via ShouldSwallow(kind, err); the audit-row
// ErrorKind passed at each site is the cross-reference (see
// types.ErrorKind*).
func validateBlock(state State, block *types.Block, policy types.ConsensusPolicy) error {
// Validate internal consistency.
if err := block.ValidateBasic(policy); err != nil {
Expand Down Expand Up @@ -42,43 +46,57 @@ func validateBlock(state State, block *types.Block, policy types.ConsensusPolicy
}
// Validate prev block info.
if !block.LastBlockID.Equals(state.LastBlockID) {
return fmt.Errorf("wrong Block.Header.LastBlockID. Expected %v, got %v",
state.LastBlockID,
block.LastBlockID,
)
if err := policy.ShouldSwallow(types.ErrorKindLastBlockID,
fmt.Errorf("wrong Block.Header.LastBlockID. Expected %v, got %v",
state.LastBlockID, block.LastBlockID)); err != nil {
return err
}
}

// Validate app info
if !policy.SkipAppHashValidation() && !bytes.Equal(block.AppHash, state.AppHash) {
return fmt.Errorf("wrong Block.Header.AppHash. Expected %X, got %v",
state.AppHash,
block.AppHash,
)
// Validate app info.
if !bytes.Equal(block.AppHash, state.AppHash) {
if err := policy.ShouldSwallow(types.ErrorKindAppHash,
fmt.Errorf("wrong Block.Header.AppHash. Expected %X, got %v",
state.AppHash, block.AppHash)); err != nil {
return err
}
}
hashCP := state.ConsensusParams.HashConsensusParams()
if !bytes.Equal(block.ConsensusHash, hashCP) {
return fmt.Errorf("wrong Block.Header.ConsensusHash. Expected %X, got %v",
hashCP,
block.ConsensusHash,
)
if err := policy.ShouldSwallow(types.ErrorKindConsensusHash,
fmt.Errorf("wrong Block.Header.ConsensusHash. Expected %X, got %v",
hashCP, block.ConsensusHash)); err != nil {
return err
}
}
if !types.SkipLastResultsHashValidation.Load() && !bytes.Equal(block.LastResultsHash, state.LastResultsHash) {
return fmt.Errorf("wrong Block.Header.LastResultsHash. Expected %X, got %v",
state.LastResultsHash,
block.LastResultsHash,
)
// Giga production escape hatch: tmtypes.SkipLastResultsHashValidation
// is set unconditionally by Giga at app init (app.go:749) and is
// load-bearing for Giga's production halt-resistance on
// LastResultsHash. This is the only Skip*-style early-return preserved
// in the codebase; migrating Giga onto a build-tagged ConsensusPolicy
// variant is its own future workstream.
if !types.SkipLastResultsHashValidation.Load() {
if !bytes.Equal(block.LastResultsHash, state.LastResultsHash) {
if err := policy.ShouldSwallow(types.ErrorKindLastResultsHash,
fmt.Errorf("wrong Block.Header.LastResultsHash. Expected %X, got %v",
state.LastResultsHash, block.LastResultsHash)); err != nil {
return err
}
}
}
if !bytes.Equal(block.ValidatorsHash, state.Validators.Hash()) {
return fmt.Errorf("wrong Block.Header.ValidatorsHash. Expected %X, got %v",
state.Validators.Hash(),
block.ValidatorsHash,
)
if err := policy.ShouldSwallow(types.ErrorKindValidatorsHash,
fmt.Errorf("wrong Block.Header.ValidatorsHash. Expected %X, got %v",
state.Validators.Hash(), block.ValidatorsHash)); err != nil {
return err
}
}
if !bytes.Equal(block.NextValidatorsHash, state.NextValidators.Hash()) {
return fmt.Errorf("wrong Block.Header.NextValidatorsHash. Expected %X, got %v",
state.NextValidators.Hash(),
block.NextValidatorsHash,
)
if err := policy.ShouldSwallow(types.ErrorKindNextValidatorsHash,
fmt.Errorf("wrong Block.Header.NextValidatorsHash. Expected %X, got %v",
state.NextValidators.Hash(), block.NextValidatorsHash)); err != nil {
return err
}
}

// Validate block LastCommit.
Expand All @@ -90,7 +108,10 @@ func validateBlock(state State, block *types.Block, policy types.ConsensusPolicy
// LastCommit.Signatures length is checked in VerifyCommit.
if err := state.LastValidators.VerifyCommit(
state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit); err != nil {
return fmt.Errorf("VerifyCommit(): %w", err)
if swErr := policy.ShouldSwallow(types.ErrorKindLastCommitVerify,
fmt.Errorf("VerifyCommit(): %w", err)); swErr != nil {
return swErr
}
}
}

Expand All @@ -99,9 +120,11 @@ func validateBlock(state State, block *types.Block, policy types.ConsensusPolicy
// a legit address and a known validator.
// The length is checked in ValidateBasic above.
if !state.Validators.HasAddress(block.ProposerAddress) {
return fmt.Errorf("block.Header.ProposerAddress %X is not a validator",
block.ProposerAddress,
)
if err := policy.ShouldSwallow(types.ErrorKindProposerNotInValidatorSet,
fmt.Errorf("block.Header.ProposerAddress %X is not a validator",
block.ProposerAddress)); err != nil {
return err
}
}

// Validate block Time
Expand Down Expand Up @@ -130,7 +153,10 @@ func validateBlock(state State, block *types.Block, policy types.ConsensusPolicy

// Check evidence doesn't exceed the limit amount of bytes.
if max, got := state.ConsensusParams.Evidence.MaxBytes, block.Evidence.ByteSize(); got > max {
return types.NewErrEvidenceOverflow(max, got)
if err := policy.ShouldSwallow(types.ErrorKindEvidenceOverflow,
types.NewErrEvidenceOverflow(max, got)); err != nil {
return err
}
}

return nil
Expand Down
24 changes: 17 additions & 7 deletions sei-tendermint/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,36 @@ func (b *Block) ValidateBasic(policy ConsensusPolicy) error {
if w, g := b.LastCommit.Hash(), b.LastCommitHash; !bytes.Equal(w, g) {
// Fall back to legacy hash calculation pre-6.4.
if wLegacy := b.LastCommit.legacyHash(); !bytes.Equal(wLegacy, g) {
return fmt.Errorf("wrong Header.LastCommitHash. Expected %X, got %X", w, g)
if err := policy.ShouldSwallow(ErrorKindLastCommitHash,
fmt.Errorf("wrong Header.LastCommitHash. Expected %X, got %X", w, g)); err != nil {
return err
}
}
}

if !policy.SkipDataHashValidation() {
// NOTE: b.Data.Txs may be nil, but b.Data.Hash() still works fine.
if w, g := b.Data.Hash(false), b.DataHash; !bytes.Equal(w, g) {
return fmt.Errorf("wrong Header.DataHash. Expected %X, got %X. Len of txs %d", w, g, len(b.Txs))
// NOTE: b.Data.Txs may be nil, but b.Data.Hash() still works fine.
if w, g := b.Data.Hash(false), b.DataHash; !bytes.Equal(w, g) {
if err := policy.ShouldSwallow(ErrorKindDataHash,
fmt.Errorf("wrong Header.DataHash. Expected %X, got %X. Len of txs %d", w, g, len(b.Txs))); err != nil {
return err
}
}

// NOTE: b.Evidence may be nil, but we're just looping.
for i, ev := range b.Evidence {
if err := ev.ValidateBasic(); err != nil {
return fmt.Errorf("invalid evidence (#%d): %v", i, err)
if swErr := policy.ShouldSwallow(ErrorKindPerEvidenceValidateBasic,
fmt.Errorf("invalid evidence (#%d): %v", i, err)); swErr != nil {
return swErr
}
}
}

if w, g := b.Evidence.Hash(), b.EvidenceHash; !bytes.Equal(w, g) {
return fmt.Errorf("wrong Header.EvidenceHash. Expected %X, got %X", w, g)
if err := policy.ShouldSwallow(ErrorKindEvidenceHash,
fmt.Errorf("wrong Header.EvidenceHash. Expected %X, got %X", w, g)); err != nil {
return err
}
}

return nil
Expand Down
63 changes: 59 additions & 4 deletions sei-tendermint/types/consensus_policy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,62 @@
// Package types — ConsensusPolicy is a zero-sized, build-tag-selected gate
// that decides, per ErrorKind, whether a halting validation failure halts
// (default) or is swallowed (counter incremented, then continued). The
// single method ShouldSwallow(kind, err) is declared in exactly one of three
// per-tag files, so each binary compiles in one fixed policy with no runtime
// branch:
//
// default (production) → returns err for every kind; production halting
// semantics are unchanged
// mock_block_validation → returns nil for AppHash and DataHash; preserves
// the long-standing behavior of that tag
// mock_chain_validation → returns nil for every swallow-eligible audit-row
// kind (M2 deliverable)
//
// One Skip*-style early-return is preserved alongside the policy:
// tmtypes.SkipLastResultsHashValidation; see validation.go for context.
package types

// DefaultConsensusPolicy returns the zero-value policy. Behavior is
// build-tag-dependent: production builds enforce every check; mock_block_validation
// builds bypass every gated check (see consensus_policy_default.go and
// consensus_policy_mock_block_validation.go).
// DefaultConsensusPolicy returns the zero-value policy for the current build.
func DefaultConsensusPolicy() ConsensusPolicy { return ConsensusPolicy{} }

// ErrorKind names a swallow-eligible validation failure. Constants
// correspond to rows in the M1.0 audit (docs/designs/
// mock-chain-validation-m1-audit.md); the string value is the metric label
// emitted on sei_unsafe_validation_skipped_total{kind=...}.
type ErrorKind string

const (
ErrorKindAppHash ErrorKind = "app_hash"
ErrorKindDataHash ErrorKind = "data_hash"
ErrorKindLastResultsHash ErrorKind = "last_results_hash"
ErrorKindLastBlockID ErrorKind = "last_block_id"
ErrorKindConsensusHash ErrorKind = "consensus_hash"
ErrorKindValidatorsHash ErrorKind = "validators_hash"
ErrorKindNextValidatorsHash ErrorKind = "next_validators_hash"
ErrorKindLastCommitVerify ErrorKind = "last_commit_verify"
ErrorKindProposerNotInValidatorSet ErrorKind = "proposer_not_in_validator_set"
ErrorKindEvidenceOverflow ErrorKind = "evidence_overflow"
ErrorKindLastCommitHash ErrorKind = "last_commit_hash"
ErrorKindEvidenceHash ErrorKind = "evidence_hash"
ErrorKindPerEvidenceValidateBasic ErrorKind = "per_evidence_validate_basic"
)

// AllSwallowEligibleErrorKinds returns the audit's swallow-eligible set.
// Tests iterate this list to assert the per-variant matrix.
func AllSwallowEligibleErrorKinds() []ErrorKind {
return []ErrorKind{
ErrorKindAppHash,
ErrorKindDataHash,
ErrorKindLastResultsHash,
ErrorKindLastBlockID,
ErrorKindConsensusHash,
ErrorKindValidatorsHash,
ErrorKindNextValidatorsHash,
ErrorKindLastCommitVerify,
ErrorKindProposerNotInValidatorSet,
ErrorKindEvidenceOverflow,
ErrorKindLastCommitHash,
ErrorKindEvidenceHash,
ErrorKindPerEvidenceValidateBasic,
}
}
7 changes: 2 additions & 5 deletions sei-tendermint/types/consensus_policy_default.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
//go:build !mock_block_validation
//go:build !mock_block_validation && !mock_chain_validation

package types

// ConsensusPolicy is empty in production builds. Its methods return
// constant false so the compiler DCEs the bypass branches.
type ConsensusPolicy struct{}

func (ConsensusPolicy) SkipAppHashValidation() bool { return false }
func (ConsensusPolicy) SkipDataHashValidation() bool { return false }
func (ConsensusPolicy) ShouldSwallow(_ ErrorKind, err error) error { return err }
17 changes: 13 additions & 4 deletions sei-tendermint/types/consensus_policy_mock_block_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

package types

// ConsensusPolicy is empty in mock_block_validation builds. Each Skip*
// method returns true unconditionally — running this binary IS the bypass.
// Swallow set is AppHash + DataHash only — these are the two checks the
// mock_block_validation tag has always relaxed; preserving that exact set
// keeps user-visible outcomes under this tag unchanged across the refactor.
// All other audit-row kinds halt as in production.
type ConsensusPolicy struct{}

func (ConsensusPolicy) SkipAppHashValidation() bool { return true }
func (ConsensusPolicy) SkipDataHashValidation() bool { return true }
func (ConsensusPolicy) ShouldSwallow(kind ErrorKind, err error) error {
switch kind {
case ErrorKindAppHash, ErrorKindDataHash:
unsafeValidationSkippedTotal.WithLabelValues(string(kind)).Inc()
return nil
default:
return err
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:build mock_block_validation

package types

import (
"errors"
"testing"
)

func TestConsensusPolicy_MockBlockValidation_Matrix(t *testing.T) {
policy := DefaultConsensusPolicy()
testErr := errors.New("sentinel")
swallowExpected := map[ErrorKind]bool{
ErrorKindAppHash: true,
ErrorKindDataHash: true,
ErrorKindLastResultsHash: false,
ErrorKindLastBlockID: false,
ErrorKindConsensusHash: false,
ErrorKindValidatorsHash: false,
ErrorKindNextValidatorsHash: false,
ErrorKindLastCommitVerify: false,
ErrorKindProposerNotInValidatorSet: false,
ErrorKindEvidenceOverflow: false,
ErrorKindLastCommitHash: false,
ErrorKindEvidenceHash: false,
ErrorKindPerEvidenceValidateBasic: false,
}
for _, kind := range AllSwallowEligibleErrorKinds() {
swallow, ok := swallowExpected[kind]
if !ok {
t.Errorf("test matrix missing entry for ErrorKind %q — audit added a new row?", kind)
continue
}
got := policy.ShouldSwallow(kind, testErr)
if swallow {
if got != nil {
t.Errorf("mock_block_validation ConsensusPolicy.ShouldSwallow(%q, testErr) = %v, want nil", kind, got)
}
} else {
if got != testErr {
t.Errorf("mock_block_validation ConsensusPolicy.ShouldSwallow(%q, testErr) = %v, want testErr", kind, got)
}
}
}
}

func TestConsensusPolicy_MockBlockValidation_UnknownKindReturnsErr(t *testing.T) {
policy := DefaultConsensusPolicy()
testErr := errors.New("sentinel")
if got := policy.ShouldSwallow(ErrorKind("not_a_real_kind"), testErr); got != testErr {
t.Errorf("mock_block_validation ConsensusPolicy.ShouldSwallow(unknown, testErr) = %v, want testErr", got)
}
}
35 changes: 35 additions & 0 deletions sei-tendermint/types/consensus_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build !mock_block_validation && !mock_chain_validation

package types

import (
"errors"
"testing"
)

func TestConsensusPolicy_Default_AllKindsReturnErr(t *testing.T) {
policy := DefaultConsensusPolicy()
testErr := errors.New("sentinel")
for _, kind := range AllSwallowEligibleErrorKinds() {
if got := policy.ShouldSwallow(kind, testErr); got != testErr {
t.Errorf("default ConsensusPolicy.ShouldSwallow(%q, testErr) = %v, want testErr", kind, got)
}
}
}

func TestConsensusPolicy_Default_UnknownKindReturnsErr(t *testing.T) {
policy := DefaultConsensusPolicy()
testErr := errors.New("sentinel")
if got := policy.ShouldSwallow(ErrorKind("not_a_real_kind"), testErr); got != testErr {
t.Errorf("default ConsensusPolicy.ShouldSwallow(unknown, testErr) = %v, want testErr", got)
}
}

// Guards the M1.0 audit's 13-row invariant — a change here means the audit
// (docs/designs/mock-chain-validation-m1-audit.md) needs to be revisited.
func TestAllSwallowEligibleErrorKinds_Count(t *testing.T) {
got := len(AllSwallowEligibleErrorKinds())
if got != 13 {
t.Errorf("AllSwallowEligibleErrorKinds() returned %d kinds, want 13 (per M1.0 audit)", got)
}
}
16 changes: 16 additions & 0 deletions sei-tendermint/types/swallow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package types

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

// unsafeValidationSkippedTotal counts halting validation failures that were
// swallowed by a non-default ConsensusPolicy. Always zero in production builds.
var unsafeValidationSkippedTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "sei_unsafe_validation_skipped_total",
Help: "Halting validation failures swallowed by a non-default ConsensusPolicy (mock_block_validation, mock_chain_validation). Always zero in production builds.",
},
[]string{"kind"},
)
Loading