Skip to content

fix(consensus): Linearizer double-commits equivocating blocks at the same slot #24498

@SherlockShemol

Description

@SherlockShemol

Summary

The Linearizer can double-commit equivocating blocks at the same (Round, Author) slot due to incorrect commit status checking.

Problem Description

In linearize_sub_dag(), the commit status check uses is_committed(BlockRef) which includes the Digest:

.filter(|ancestor| {
    ancestor.round > gc_round && !dag_state.is_committed(ancestor)
})

Since two equivocating blocks have different digests, they are treated as different blocks. If Block A is committed first, is_committed(Block B) still returns false, allowing Block B to be committed later through a different DAG path.

Attack Scenario

  1. Byzantine validator V1 creates Block A (Round 1) and broadcasts to 60% of nodes
  2. Block A is committed via main chain
  3. V1 creates Block B (Round 1, same slot, different digest) and broadcasts to remaining 40%
  4. Some honest node (only received B) creates a block referencing B
  5. Block B propagates and passes BlockVerifier (signature is valid)
  6. A later Leader references Block B through a side chain
  7. When that Leader is committed, Linearizer traverses to Block B
  8. is_committed(Block B) returns false (different digest)
  9. Block B is committed even though Block A was already committed!

Production Impact

  • consensus_gc_depth = 60 in production (from sui-protocol-config)
  • This gives attackers a ~30-60 second window to execute the attack
  • Violates consensus safety: same (Round, Author) slot committed twice
  • Requires only 1 Byzantine validator (within f tolerance)

Proposed Fix

Add is_any_block_at_slot_committed(Slot) check to filter out ancestors where any block at that slot has already been committed:

.filter(|ancestor| {
    ancestor.round > gc_round
        && !dag_state.is_committed(ancestor)
        && !dag_state.is_any_block_at_slot_committed((*ancestor).into())
})

Why This Is Not a False Positive

Unlike issue #24475 which used invalid AuthorityIndex that BlockVerifier rejects:

  1. Equivocating blocks are valid VerifiedBlocks - BlockVerifier does NOT check for equivocation
  2. DagState intentionally accepts equivocating blocks - Only self-equivocation is blocked
  3. Production config allows attack - gc_depth=60 provides ample time window

Security Impact

  • Severity: High (Safety violation)
  • Attack Vector: Network (1 Byzantine validator)
  • Impact: Same slot committed twice, violating consensus uniqueness

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions