Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Replicated certificate for a different block deletes the voted round, allowing a second vote

Cerrado
#662 0 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@yacovm ya está trabajando en esto.

Desde el 28/10/2026.

  • #677 de @yacovm — abierto

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
go

Línea de trabajo

Start with simplex/epoch.go, especially storeNotarization, storeFinalization, processNotarizedBlock, processFinalizedBlock, and the proposal and voting paths cited in the report. Trace how the round's vote record changes after a conflicting certificate, then reproduce the A/B/C sequence described in the issue. Done means the node broadcasts no second vote for a different digest, writes only one block record per round, and still adopts and replicates B.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

medium security

Details

A Byzantine validator that leads a round can make an honest validator sign votes for two different blocks in that round. The attacker must be a validator and the round's leader. The code at fault is the recovery code in storeNotarization and related functions. It deletes e.rounds[R], the only record of the vote, so storeProposal stops refusing a second block. No second notarization results.

flowchart TD
  n1["Byzantine leader validator"]
  n2["handleBlockMessage"]
  n3["storeProposal guard"]
  n4["Second Vote for round R and second WAL block"]
  n5["storeNotarization and other recovery code"]
  n6["Durable record that the node voted in round R"]
  n1 -->|"sends a second block for round R"| n2
  n2 -->|"schedules verification"| n3
  n3 -->|"accepts block, node signs second vote"| n4
  n1 -->|"conflicting certificate reaches the node"| n5
  n5 -->|"deletes the entry the guard reads"| n3
  n6 -->|"does not exist"| n3
  classDef actor stroke:#2563eb,stroke-width:2px
  classDef vulnerable_code stroke:#dc2626,stroke-width:2px
  classDef existing_control stroke:#16a34a,stroke-width:2px
  classDef missing_control stroke:#dc2626,stroke-width:2px,stroke-dasharray:5 5
  classDef asset stroke:#9333ea,stroke-width:2px
  classDef component stroke:#6b7280,stroke-width:1px
  class n1 actor
  class n2 component
  class n3 vulnerable_code
  class n4 asset
  class n5 component
  class n6 missing_control
  1. Byzantine leader sends block A to the victim and block B to the other honest validators. The leader of round R is the attacker, so it can choose what each node receives.
  2. Other honest validators vote for B and broadcast its notarization. B collects 2f+1 real votes, so the certificate is genuine and nothing is forged.
  3. the victim's storeNotarization deletes e.rounds[R] when the certificate names a different block. That entry is the node's only record that it voted in round R. The node stays in round R and requests block B through replication. (simplex/epoch.go:431-446)
  4. Byzantine leader sends a new BlockMessage for round R holding a block C whose digest differs from A's. handleBlockMessage accepts it because the round is current and the leader signed it. deleteFutureProposal already cleared proposalBeingProcessed when block A's task ran, and nothing checks whether the node already voted. (simplex/epoch.go:1981-1992)
  5. the verification task calls storeProposal, which succeeds. storeProposal refuses a second block only while e.rounds[R] exists, and that entry is gone. (simplex/epoch.go:3286-3297)
  6. the verification task calls doProposed, which signs and broadcasts a second Vote for round R. The vote decision depends only on storeProposal succeeding, and the second block is appended to the WAL (write-ahead log). (simplex/epoch.go:2251-2287)

Evidence

  1. simplex/epoch.go:2251–2287
  2. simplex/epoch.go:431–446
  3. simplex/epoch.go:2168–2177
  4. simplex/epoch.go:2088–2095
  5. simplex/epoch.go:3003–3014
  6. simplex/epoch.go:3286–3297
  7. simplex/epoch.go:1981–1992
  8. simplex/epoch.go:1248–1255

Impact

The honest validator signs and broadcasts two votes for round R with different digests, and writes both blocks to its WAL. This breaks the first-block-only voting rule. It does not produce a second notarization or finalization, because a third block C can gather at most f Byzantine votes plus f victim votes, which is below quorum. The report calls this a broken protocol invariant, not a safety break on its own. It would become one if another path deleted e.rounds[R] without a genuine conflicting certificate.

Reproduction steps

  1. The attacker is a Byzantine validator that leads round R. It sends block A to the victim and block B to the other honest validators. B gets a genuine notarization of 2f+1 votes, which the honest nodes broadcast themselves, and the attacker may also send it. The victim's storeNotarization sees a different header and deletes e.rounds[R]. The same deletion happens in three other places: storeFinalization for a direct Finalization message, processNotarizedBlock for a replicated notarization, and processFinalizedBlock for a replicated finalization. The leader then sends a new BlockMessage for round R before block B arrives through replication. Only validators reach these handlers. The only race is beating B's replication round trip, and the attacker controls that timing.

Recommended fix

The node's memory of having voted in round R lives only in e.rounds[R]. The recovery code in processNotarizedBlock, processFinalizedBlock, storeNotarization and storeFinalization deletes that entry, so the first-block-only voting rule is no longer enforced for that round.

Fix criteria: After a node has voted in round R, it never signs a second Vote for a different digest in round R, whatever certificates it learns later. Reproduce the scenario: vote for A, inject a valid notarization or finalization for B, then deliver a new proposal C for the same round. Assert that no second VoteMessage is broadcast and only one block record per round is written. Also assert that the node still replicates and adopts B.

Status on main

Re-checked against main at 90995e0 on 2026-10-26; still present:

storeNotarization deletes e.rounds[R] on a header mismatch (simplex/epoch.go:435-444). processNotarizedBlock (epoch.go:2168-2177) and processFinalizedBlock (epoch.go:2093-2100) do the same. createBlockVerificationTask clears proposalBeingProcessed via deleteFutureProposal (epoch.go:2229, 1248-1255). handleBlockMessage (epoch.go:1915-2022) has no record that the node already voted in the current round R. A new leader proposal therefore passes, storeProposal succeeds because e.rounds[R] is gone (epoch.go:3286-3296), and doProposed/voteOnBlock (epoch.go:3064-3079) signs a second vote with nothing to stop it. No PR or issue found.


Severity: MEDIUM
Status: Open
Category: Improper workflow enforcement
CWE: CWE-841
Repository: ava-labs/Simplex
Branch: main
Date created: 2026-10-26
Claude Security finding: https://claude.ai/security/findings/find_0166rYDpWCzUnzXN4KqY6UUS

Lenguaje dominante
Go
Estrellas
22
Forks
4
Merge medio
1 d 17 h
PR fusionados (30 d)
33

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de ava-labs/Simplex

Todos los issues de ava-labs/Simplex

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.