Epoch discovery accepts a sealing block on a head count, letting low-stake validators forge epochs
I maintainer di solito rispondono entro 1 giorno
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
- Ambito
- distributed-systems, security
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
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
- attacker registers
F(n)+1validator 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. - attacker identities each send a
ReplicationResponsewith the same made-up sealing block. The block hasSeqandEpochset to X, above the victim's tip, and names the attacker's own validator set and keys. (nonvalidator/epochs.go:178-216) maybeObserveThresholdResponsesaccepts the sealing block once the response count reachescommon.F(len(validators)) + 1. It counts one response per node ID and never readsNode.Weight, so no honest reporter is guaranteed. (nonvalidator/epochs.go:178-216)maybeOpenEpochFromSealingBlockinstalls epoch X using the validator set taken from the block'sSealingBlockInfo().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)- attacker sends blocks for the victim's next sequences that name epoch X, with finalizations signed by its own keys.
processQuorumRoundchecks each finalization against whichever epoch the block names, so the checks pass. (nonvalidator/non_validator.go:697-781) storeQuorumRoundlets 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)- 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
- nonvalidator/epochs.go:178–216
- nonvalidator/non_validator.go:209–241
- nonvalidator/non_validator.go:783–805
- nonvalidator/non_validator.go:424–455
- nonvalidator/epochs.go:30–51
- nonvalidator/non_validator.go:697–781
- nonvalidator/non_validator.go:659–692
- nonvalidator/non_validator.go:365–418
- external.go:62–75
- common/api.go:179–182
- README.md:32–38
- simplex/pos_test.go:27–37
- nonvalidator/future_epoch_listener.go:42–98
- instance.go:500–549
- 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
- The attacker must control
F(n)+1validator identities on the current P-chain list, wherenis 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 throughprocessSealingBlock. An already-synced non-validator reaches it throughhandleQrFromUnknownEpoch, which uses the same count threshold for a block naming an unknown epoch. A running validator reaches it throughFutureEpochListener, which feeds every replication response to the same count-based collector. When the threshold is met,onHigherSealingBlockandprocessEpochChangestop 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
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di ava-labs/Simplex
-
low security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
I maintainer di solito rispondono entro 1 giorno
-
Replicated notarized block dropped when the leader refills the round, stalling the validatorForse già presa @yacovm l’ha presa 1 giorno fa. Apertamedium security
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
I maintainer di solito rispondono entro 1 giorno
-
Replicated empty notarization lets a previous-epoch Telock become the new epoch's chain tipForse già presa @yacovm l’ha presa 1 giorno fa. Apertamedium security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 38/100
ava-labs/Simplex#663 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Failed notarization WAL write lets a node sign both a finalize vote and an empty voteForse già presa @yacovm l’ha presa 3 giorni fa. Apertamedium security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 38/100
I maintainer di solito rispondono entro 1 giorno
-
Builder ignores parent's P-chain height, so a leader's maximal height stalls honest proposalsForse già presa @yacovm l’ha presa 3 giorni fa. Apertamedium security
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di ava-labs/Simplex
Issue simili
-
bug from-studio
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
esengine/DeepSeek-Reasonix#14868 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 80/100
project-zot/zot#4636 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
keyxmakerx/Chronicle#1911 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
eugenioenko/ttt#1293 · 1 commento · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
needs/area needs/kind needs/priority
Difficoltà 2/5 1-3 ore Idoneità per principianti 80/100
I maintainer di solito rispondono entro 1 giorno