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

Epoch discovery accepts a sealing block on a head count, letting low-stake validators forge epochs

Aperta
#664 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@yacovm ci sta già lavorando.

Dal 29/10/2026.

  • #679 di @yacovm — aperta

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
go

Direzione di ricerca

Start with nonvalidator/epochs.go, especially maybeObserveThresholdResponses, then trace the unknown epoch and sealing block paths in nonvalidator/non_validator.go and nonvalidator/future_epoch_listener.go. Review the existing POS tests in simplex/pos_test.go and add coverage for both weighted reporter thresholds and rejecting blocks whose epoch does not match the validated sealing block chain. Done means low stake identities cannot install an epoch or trigger the listener, and an unrelated higher epoch cannot replace a genuine stored block.

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

Descrizione

medium security

Details

A group of validators holding a small share of the stake can make a catching-up node adopt a validator set they invented. The node then stores blocks signed only by that group. A running validator can be stopped and restarted with the attacker's set. This applies only to stake-weighted deployments. The cause is maybeObserveThresholdResponses, which counts identities and ignores Node.Weight.

flowchart TD
  n1["attacker with F(n)+1 low-stake validator identities"]
  n2["maybeObserveThresholdResponses"]
  n3["maybeOpenEpochFromSealingBlock"]
  n4["processQuorumRound and storeQuorumRound"]
  n5["victim node's stored chain and genuine epochs"]
  n6["running validator via FutureEpochListener"]
  n7["stake-weighted threshold"]
  n8["check tying a block's epoch to its sequence"]
  n1 -->|"sends made-up sealing block"| n2
  n2 -->|"head count reaches threshold"| n3
  n3 -->|"fake epoch installed"| n4
  n1 -->|"sends blocks signed by own keys"| n4
  n4 -->|"replaces genuine entries and indexes"| n5
  n2 -->|"same count stops and restarts it"| n6
  n7 -->|"not applied"| n2
  n8 -->|"not applied"| n4
  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 vulnerable_code
  class n3 component
  class n4 vulnerable_code
  class n5 asset
  class n6 asset
  class n7 missing_control
  class n8 missing_control
  1. attacker registers F(n)+1 validator identities holding under one third of the stake. This stays within the fault bound a stake-weighted L1 tolerates, but is enough identities to meet a count-based threshold.
  2. attacker identities each send a ReplicationResponse with the same made-up sealing block. The block has Seq and Epoch set to X, above the victim's tip, and names the attacker's own validator set and keys. (nonvalidator/epochs.go:178-216)
  3. maybeObserveThresholdResponses accepts the sealing block once the response count reaches common.F(len(validators)) + 1. It counts one response per node ID and never reads Node.Weight, so no honest reporter is guaranteed. (nonvalidator/epochs.go:178-216)
  4. maybeOpenEpochFromSealingBlock installs epoch X using the validator set taken from the block's SealingBlockInfo().ValidatorSet. The finalization attached to the sealing block is never checked, and the signer set and signature aggregator are built from the attacker's keys. (nonvalidator/non_validator.go:424-455)
  5. attacker sends blocks for the victim's next sequences that name epoch X, with finalizations signed by its own keys. processQuorumRound checks each finalization against whichever epoch the block names, so the checks pass. (nonvalidator/non_validator.go:697-781)
  6. storeQuorumRound lets the higher-epoch attacker entry replace the genuine stored entry. The genuine entry is treated as a Telock, because nothing ties a block's epoch to its sequence. (nonvalidator/non_validator.go:697-781)
  7. the indexing task indexes the attacker's block and deletes every lower, genuine epoch. The non-validator runs only VM verification, which accepts a nil inner block, and removeOldSequencesAndEpochs(md.Seq, md.Epoch) then removes the genuine epochs. (nonvalidator/non_validator.go:365-418)

Evidence

  1. nonvalidator/epochs.go:178–216
  2. nonvalidator/non_validator.go:209–241
  3. nonvalidator/non_validator.go:783–805
  4. nonvalidator/non_validator.go:424–455
  5. nonvalidator/epochs.go:30–51
  6. nonvalidator/non_validator.go:697–781
  7. nonvalidator/non_validator.go:659–692
  8. nonvalidator/non_validator.go:365–418
  9. external.go:62–75
  10. common/api.go:179–182
  11. README.md:32–38
  12. simplex/pos_test.go:27–37
  13. nonvalidator/future_epoch_listener.go:42–98
  14. instance.go:500–549
  15. instance.go:212–264

Impact

A non-validator indexes blocks the attacker wrote, loses its genuine epochs and follows only the attacker's fork. Its stored chain and the state it serves are attacker-controlled, and it stops following the real chain until an operator repairs it. A running validator is pushed out of real consensus, and doing this to enough validators halts the network. No secret data is exposed.

Reproduction steps

  1. The attacker must control F(n)+1 validator identities on the current P-chain list, where n is the number of identities. Together they hold under one third of the stake, on an L1 whose quorums are weighted by stake. A deployment that measures faults purely by head count is not affected, and the report notes the threat model does not say whether "<1/3 Byzantine validators" means count or stake. Two kinds of victim are reachable. A bootstrapping non-validator reaches the flaw through processSealingBlock. An already-synced non-validator reaches it through handleQrFromUnknownEpoch, which uses the same count threshold for a block naming an unknown epoch. A running validator reaches it through FutureEpochListener, which feeds every replication response to the same count-based collector. When the threshold is met, onHigherSealingBlock and processEpochChange stop the validator and restart it with the attacker's set or as a non-validator.

Recommended fix

(1) The threshold in maybeObserveThresholdResponses is a count of node IDs over the latest P-chain list. On a stake-weighted L1, a low-stake group with many identities can meet it with no honest reporter.

Fix criteria: A sealing block counts as authentic only when its reporters, weighted the same way quorums are (stake from Node.Weight or the host's aggregator), exceed the tolerated Byzantine weight. A test shows that F(n)+1 reporters holding less than one third of the weight neither add an epoch to n.epochs nor trigger FutureEpochListener.

(2) Once an epoch is in n.epochs, a block at any sequence can name it, and a higher-epoch entry displaces a genuine one. Nothing ties a block's epoch to the sealing-block chain below its sequence.

Fix criteria: A block at sequence S is verified only against the epoch whose sealing block is the latest one below S on the validated chain. A test shows that a block at the next sequence naming a higher, unrelated epoch is rejected and does not replace a genuine stored entry.

Status on main

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

maybeObserveThresholdResponses (nonvalidator/epochs.go:173-216) still uses threshold := common.F(len(validators)) + 1 and counts responses per node ID, ignoring Node.Weight. Open issue #615 ('Count threshold voting by stake weight if using POS') tracks part (1). Part (2) also remains: nothing ties a block's epoch to the sealing-block chain below its sequence, and a higher-epoch entry replaces a stored one (nonvalidator/non_validator.go:766-775).

Related: #615


Severity: MEDIUM
Status: Open
Category: Insufficient verification of data authenticity
CWE: CWE-345
Repository: ava-labs/Simplex
Branch: main
Date created: 2026-10-26
Claude Security finding: https://claude.ai/security/findings/find_01CCrWwtgL6tAyAMvgs6HCmp

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

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.