Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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

Chiusa
#662 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@yacovm ci sta già lavorando.

Dal 28/10/2026.

  • #677 di @yacovm — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
go

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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

Lingua principale
Go
Stelle
22
Fork
4
Merge medio
1g 16h
PR unite (30g)
35

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di ava-labs/Simplex

Tutte le issue di ava-labs/Simplex

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.