Skip to content

feat(engine-byzantine): add crate for simulating Byzantine faults in consensus #1505

Open
hvanz wants to merge 6 commits intocirclefin:mainfrom
hvanz:engine-byz
Open

feat(engine-byzantine): add crate for simulating Byzantine faults in consensus #1505
hvanz wants to merge 6 commits intocirclefin:mainfrom
hvanz:engine-byz

Conversation

@hvanz
Copy link
Contributor

@hvanz hvanz commented Feb 25, 2026

Closes: #1504

This PR introduces a new engine-byzantine crate that provides configurable Byzantine behavior simulation, along with a comprehensive integration test suite that exercises vote equivocation, proposal equivocation, message dropping, amnesia attacks, and consensus stall scenarios.

The new crate provides two main components that compose with the existing engine architecture:

  • ByzantineNetworkProxy: A ractor actor that sits between the consensus actor and the real network actor. It intercepts outgoing PublishConsensusMsg and PublishLivenessMsg messages and can drop them (simulating silence/censorship), duplicate them with conflicting content (simulating equivocation), or forward them unchanged. All other message types pass through transparently.

  • ByzantineMiddleware: Wraps the test app's Middleware trait to override vote construction. When ignore_locks is enabled, it tracks proposed values and overrides nil prevotes to vote for the most recently proposed value, simulating an amnesia attack (ignoring the Tendermint voting lock mechanism).

Both components are driven by a ByzantineConfig struct that supports five attack types:

Attack Config field Description
Vote equivocation equivocate_votes Sends original vote + a conflicting vote (value flipped to nil)
Proposal equivocation equivocate_proposals Sends original proposal + a proposal with a different value
Vote dropping drop_votes Silently drops outgoing votes (on both consensus and liveness channels)
Proposal dropping drop_proposals Silently drops outgoing proposals
Amnesia ignore_locks Votes for the current round's proposed value even when locked on a different value

Each attack is gated by a Trigger that controls when it fires: Always, Random { probability }, AtHeights, AtRounds, or HeightRange.

Also, integrated the Byzantine engine with test app (crates/test/app/):

  • Added ByzantineConfig as an optional field on the test app's Config struct.
  • Modified App::start() to conditionally spawn a ByzantineNetworkProxy in front of the real network actor and wrap the middleware with ByzantineMiddleware when amnesia is configured.

Testing

The following integration tests where added to crates/test/tests/it/byzantine_engine.rs:

1-of-4 Byzantine node scenarios (minority fault):

  • vote_equivocation_detected — Verifies honest nodes collect MisbehaviorEvidence for vote equivocation.
  • proposal_equivocation_detected — (Currently #[ignore]) Verifies proposal equivocation detection. Known limitation: gossip latency causes conflicting proposals to arrive after height decision.
  • proposal_dropping_liveness — Confirms the network progresses when one node drops all proposals.
  • vote_dropping_liveness — Confirms the network progresses when one node drops all votes.
  • amnesia_attack_liveness — Confirms the network progresses despite one node ignoring voting locks.

2-of-4 Byzantine node scenarios (exceeding fault threshold):

  • two_byzantine_of_four_stalls_consensus — Verifies that when 2 of 4 equal-power nodes drop all votes (50% of voting power), consensus correctly stalls (no decisions made), confirming the f < n/3 fault threshold.
  • two_silent_of_four_stalls_consensus — Same as above but with both votes and proposals dropped.
  • two_proposal_droppers_of_four_still_progresses — Verifies that dropping proposals alone (while still voting) does not prevent liveness, since honest proposer rounds still succeed.
  • two_vote_equivocators_of_four_detected — Verifies equivocation evidence is collected even with 2 equivocating nodes.
  • two_amnesia_of_four_still_progresses — Verifies that amnesia alone does not break liveness when all votes are still delivered.

PR author checklist

Contribution eligibility

  • I am a core contributor, OR I have been explicitly assigned to the linked issue
  • I have read CONTRIBUTING.md and my PR complies with all requirements
  • I understand that PRs not meeting these requirements will be closed without review

For all contributors

For external contributors

hvanz and others added 6 commits February 9, 2026 21:09
Introduces a network proxy actor that intercepts outgoing consensus
messages to equivocate or drop votes/proposals, and a middleware that
ignores voting locks (amnesia attack). Attacks are configurable via
TOML triggers (always, random, at specific heights/rounds, or ranges).
Integrate the engine-byzantine crate into the test app's
node startup to conditionally inject the network proxy and amnesia
middleware based on the new [byzantine] config section.
Previously proposal equivocation only logged a warning and forwarded the
original. Now it constructs and sends a conflicting proposal via an
optional ConflictingValueFn callback. The test app provides a factory
that creates a conflicting value by incrementing the original's u64.
…narios

Cover vote/proposal equivocation detection, message dropping, and
amnesia attacks using the Byzantine engine with the test framework.
The ByzantineNetworkProxy only intercepted votes on the consensus gossip channel but forwarded them transparently on the liveness (rebroadcast) channel. This meant drop_votes was ineffective — votes still reached peers through the liveness path.

Fix the proxy to also apply drop_votes rules to PublishLivenessMsg containing votes.

Add an integration test (two_byzantine_of_four_stalls_consensus) that verifies 2 Byzantine vote-droppers out of 4 equal-power nodes cause consensus to stall, confirming the f < n/3 fault threshold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add four tests covering different attack types with 2 out of 4 equal-power Byzantine nodes:
- two_silent_of_four_stalls_consensus: total silence (drop votes + proposals) causes consensus to stall
- two_proposal_droppers_of_four_still_progresses: dropping proposals alone does not prevent liveness since honest proposer rounds still form quorum
- two_vote_equivocators_of_four_detected: equivocation is detected via MisbehaviorEvidence while consensus still progresses
- two_amnesia_of_four_still_progresses: ignoring locks alone does not prevent liveness since all nodes still vote

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hvanz hvanz self-assigned this Feb 25, 2026
@hvanz hvanz added the test Testing label Feb 25, 2026
@github-actions

This comment was marked as resolved.

@github-actions github-actions bot added the need-triage This issue needs to be triaged label Feb 25, 2026
@github-actions github-actions bot closed this Feb 25, 2026
@hvanz hvanz reopened this Feb 25, 2026
@romac romac removed the need-triage This issue needs to be triaged label Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: Add Byzantine fault simulation engine

2 participants