Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Unverified sealing-block finalization lets a validator redirect a syncing node's replication requests

オープン
#666 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@yacovm がすでに取り組んでいます。

2026年10月30日 から。

  • #681 @yacovm による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
45/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
go

調査の方向性

Start with nonvalidator/non_validator.go around processEpochReplicationQuorumRound, processSealingBlock, and storeQuorumRound, then trace how simplex/replication_state.go and simplex/requestor.go adopt QC signers for request routing. Add a test for an f+1-th sealing-block response with forged signers. Done means its signers do not change highestObserved and later replication requests still reach genuine validators.

索引モデルが issue の本文から書いたものです。

説明

low security

Details

A Byzantine validator can stall a lagging or new non-validator node's catch-up, one node per race it wins. It must be in the latest validator set and its response must be the f+1-th matching sealing-block response. The cause is that processSealingBlock stores the finalization without checking its signatures, so its signer list decides where later requests go.

flowchart TD
  n1["Byzantine validator"]
  n2["processSealingBlock and storeQuorumRound"]
  n3["VerifyQC on sealing-block finalization"]
  n4["requestor highestObserved signers"]
  n5["syncing node's catch-up"]
  n1 -->|"sends forged finalization as f+1-th response"| n2
  n3 -->|"not applied"| n2
  n2 -->|"stores forged signer list"| n4
  n4 -->|"sends requests to attacker-chosen IDs, stalling it"| n5
  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 missing_control
  class n4 component
  class n5 asset
  1. Byzantine validator waits for a lagging or new node to start epoch replication. The validator must be in the latest validator set, because the threshold collector only counts senders listed in Comm.Validators().
  2. Byzantine validator sends a ReplicationResponse with the genuine newest sealing block and a forged finalization. The forged QC lists only the attacker, or nobody usable, as signers, and the response must be the f+1-th matching one, which is a timing race. maybeObserveThresholdResponses counts only the epoch and digest, so the attached finalization plays no part. (nonvalidator/epochs.go:173-220)
  3. the node's processEpochReplicationQuorumRound passes the completing sender's quorum round to processSealingBlock. The comment in the code acknowledges that this finalization has not been verified. (nonvalidator/non_validator.go:209-242)
  4. storeQuorumRound stores the sealing block's quorum round without calling VerifyQC. verifyQuorumRound only checks that the finalization header equals the block header, and anyone can copy that header. (nonvalidator/non_validator.go:750-758)
  5. ReplicationState.StoreQuorumRound takes the signer list from the unverified QC and adopts it as highestObserved. newSignedQuorum reads signers from Finalization.QC.Signers(). The newest sealing block is the first and highest entry stored, so its signers win. (simplex/requestor.go:180-251)
  6. the requestor's sendRequests addresses every batch and every timeout re-send to the forged signer IDs. If the list is empty, BatchSequences returns nil and nothing is requested at all. (simplex/requestor.go:180-251)
  7. processReplicationState never reaches the point where the forged finalization is checked. A stored QC is verified only when its sequence is next to commit, and a node whose requests go to the wrong IDs never gets there. (nonvalidator/non_validator.go:659-692)

Evidence

  1. nonvalidator/non_validator.go:244–251
  2. nonvalidator/non_validator.go:750–758
  3. nonvalidator/non_validator.go:209–242
  4. nonvalidator/epochs.go:173–220
  5. nonvalidator/non_validator.go:732–747
  6. simplex/replication_state.go:135–187
  7. simplex/requestor.go:24–40
  8. simplex/requestor.go:180–251
  9. nonvalidator/non_validator.go:659–692
  10. nonvalidator/non_validator.go:783–805

Impact

The victim node's catch-up stalls until a verified higher finalization reaches it through handleFinalization. On an idle chain, or when the node's vote is needed to finalize new blocks, the stall can persist, and a lagging validator cannot rejoin consensus meanwhile. Safety holds: the fake finalization is never indexed, because it fails VerifyQC when it is next to commit. Each won race affects one node.

Reproduction steps

  1. The attacker is a Byzantine validator in the latest validator set. It waits for a lagging or new non-validator node to enter epoch replication, the first phase of catching up. It must win a timing race, because its response has to be the one that completes the f+1 threshold for the newest sealing block. The report names a second route: any peer can attach fake finalizations to older sealing blocks through the hash-chain branch (canValidate). That variant alone is minor. Those sequences are lower, so they never replace highestObserved, and each costs only one re-request (:685) once reached.

Recommended fix

During epoch replication, sealing-block quorum rounds go into the replication state without their finalization being verified. The unverified QC's signer list then becomes highestObserved, which decides where all replication requests are sent.

Fix criteria: No finalization, and no signer list taken from it, enters the replication state or the requestor's routing until it verifies against the validator set of its own epoch. Alternatively, routing uses a trusted set such as Comm.Validators(). A test confirms it: when the f+1-th sealing-block response carries a QC with forged signers, highestObserved.signers is unchanged. Later ReplicationRequests still go to genuine validators.

Status on main

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

During epoch replication, processEpochReplicationQuorumRound and handleQrFromUnknownEpoch call processSealingBlock (nonvalidator/non_validator.go:221-231, 795-804), which calls storeQuorumRound (non_validator.go:248-252). That path stores sealing blocks without VerifyQC (non_validator.go:755-758); verifyQuorumRound only checks that the header is consistent (non_validator.go:734-747). ReplicationState.StoreQuorumRound then calls receivedSignedQuorum(newSignedQuorum(...)), which adopts the unverified QC.Signers() as highestObserved (simplex/replication_state.go:183-186, simplex/requestor.go:24-40, 239-245), and sendRequests routes every batch and resend to those signers (requestor.go:177-188). Only the next-to-commit VerifyQC failure falls back to Comm.Validators() (non_validator.go:681-686), and that fallback does not reset highestObserved.

Related: #442 (open, related: requests should go to validators)


Severity: LOW
Status: Open
Category: Improper signature verification
CWE: CWE-347
Repository: ava-labs/Simplex
Branch: main
Date created: 2026-10-26
Claude Security finding: https://claude.ai/security/findings/find_01Y4JZoVbjowYqTefsKBxVNQ

主要言語
Go
スター
22
フォーク
4
平均マージ
1日 16時間
マージ済み PR(30日)
35

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

ava-labs/Simplex のほかの issue

ava-labs/Simplex の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。