Replayed Telock leaves unsatisfiable verification dependency, stalling non-validator forever

クローズ
#659 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

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

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

2026年10月28日 から。

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

評価

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

調査の方向性

Start with scheduleNewFinalizedBlockTask in nonvalidator/non_validator.go and the pending task logic in common/block_scheduler.go; trace how indexed sequences and epochs affect queued dependencies. Then inspect the non-validator pruning path and existing tests. Done means stale replay tasks are removed and unmarked, superseded old-epoch blocks are not scheduled, and the described regression test passes.

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

説明

medium security

Details

A Byzantine validator from the old epoch can permanently stall a non-validator node (one that follows the chain without voting) at one block height after an epoch change. This needs the old epoch to have ended with two or more Telocks (empty filler blocks). Only a restart recovers the node. The cause is that scheduleNewFinalizedBlockTask queues a replayed Telock whose parent is never indexed, and nothing removes that task.

flowchart TD
  n1["Old-epoch validator that led T2's round"]
  n2["handleFinalization and handleBlock"]
  n3["scheduleNewFinalizedBlockTask"]
  n4["BlockDependencyManager pending task for seq k+2"]
  n5["Non-validator indexing at seq k+2 and later"]
  n6["RemoveOldTasks (never called by non-validator)"]
  n1 -->|"re-sends T2 finalization and block"| n2
  n2 -->|"accepts and schedules T2"| n3
  n3 -->|"queues task waiting on digest of T1"| n4
  n4 -->|"keeps seq k+2 marked scheduled, real block refused"| n5
  n6 -->|"not applied, task never removed"| 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 component
  class n3 vulnerable_code
  class n4 component
  class n5 asset
  class n6 missing_control
  1. Network finishes epoch e with a sealing block and two Telocks. Sealing block S is at seq k, Telock T1 at k+1 and Telock T2 at k+2. The Telocks are finalized but never indexed, and the new epoch k reuses their sequence numbers.
  2. Attacker (old-epoch validator that led T2's round) sends the finalization of T2 to the non-validator. handleFinalization still finds epoch e in n.epochs until the first epoch-k block R1 is indexed. It therefore verifies and stores the old-epoch finalization, and any peer could send it. (nonvalidator/non_validator.go:470-557)
  3. Attacker sends the T2 block message as the round leader. handleBlock accepts the block only from the leader of its round, and the attacker is that leader. A matching finalization is already stored, so the block is scheduled. (nonvalidator/non_validator.go:306-357)
  4. the function scheduleNewFinalizedBlockTask registers a task for seq k+2 that waits on digest(T1). Seq k+1 is not indexed, so the task depends on the prev digest from the finalization header. The task marks seq k+2 as scheduled. (nonvalidator/non_validator.go:559-574)
  5. the scheduler in common/block_scheduler.go never releases the waiting task. ExecuteBlockDependents clears a dependency only when a finished task returns exactly that digest. R1 returns its own digest, and T1's task has already finished or never runs. (common/block_scheduler.go:67-98)
  6. the non-validator never prunes the stale task. After indexing it prunes sequences and epochs but never calls RemoveOldTasks, so the task stays pending. (nonvalidator/non_validator.go:405-414)
  7. the non-validator refuses the real block at seq k+2. IsSequenceScheduled stays true for that sequence, so every later attempt to schedule it returns early. Later sequences wait behind it, and the node stops until restarted. (common/block_scheduler.go:131-137)

Evidence

  1. nonvalidator/non_validator.go:559–574
  2. common/block_scheduler.go:131–137
  3. common/block_scheduler.go:67–98
  4. nonvalidator/non_validator.go:306–357
  5. nonvalidator/non_validator.go:470–557
  6. nonvalidator/non_validator.go:405–414

Impact

The non-validator stops indexing at seq k+2 for good, until it is restarted. It cannot follow the chain, serve newer blocks by replication, or become a validator in a later epoch. Nothing is disclosed or altered.

Reproduction steps

  1. The attacker is the epoch-e validator that led the round of finalized Telock T2 at seq k+2. It may be Byzantine, and it may already be removed from the validator set. The victim is a non-validator running across the epoch transition. That node has indexed the sealing block but not the first epoch-k block R1. It must also not be in epoch k's validator set, because members switch to validator mode. A node started after S knows only epoch k and is affected too. The attacker sends the T2 finalization (any peer may) and the T2 block message (leader only). Honest ordering can also trigger it: S is indexed, T1's task finishes, then T2 arrives. incompleteSequences[k+2] keeps T2's finalization, so a re-sent T2 block reschedules it even if the node already processed both Telocks correctly. The report did not run the reproduction. It comes from a static audit.

Recommended fix

(1) A verification task whose prev dependency can no longer be met stays pending forever and keeps its sequence marked as scheduled. This happens when the preceding sequence was indexed with a different digest, or when the task's epoch was dropped.

Fix criteria: When a sequence is indexed, pending tasks for later sequences are removed and their sequence is unmarked. This applies to tasks whose prev differs from the indexed digest and to tasks whose block epoch is below the minimum kept epoch. The genuine block for that sequence can then be scheduled. The test TestNonValidatorStallsOnStaleTelockDependency from the description passes by indexing seq 5 of epoch 3 after the Telock replay.

(2) The non-validator accepts and schedules old-epoch blocks at sequences above the new epoch's first block. It does not recognize them as superseded Telocks.

Fix criteria: A finalized block is not scheduled for verification in either of two cases. Its epoch is older than that of an already-indexed block at a lower or equal sequence. Or it is a Telock at a sequence above the new epoch's first block. Replaying such a block before seq k+1 is indexed no longer prevents seq k+2 from being indexed.

Status on main

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

scheduleNewFinalizedBlockTask (nonvalidator/non_validator.go:559-574) schedules with prev=&bh.Prev whenever seq-1 is not indexed, and returns early when IsSequenceScheduled(seq) is true. BlockDependencyManager.IsSequenceScheduled (common/block_scheduler.go:131-137) stays true for a pending task until it runs or RemoveOldTasks drops it. The nonvalidator package never calls RemoveOldTasks (grep finds no call). After R1 is indexed, removeOldSequencesAndEpochs (non_validator.go:405-414) only prunes incompleteSequences and epochs. A T2 task waiting on digest(T1) is never released, so later genuine scheduling of k+2 via handleBlock, handleFinalization or processReplicationState (non_validator.go:356/551/691) is a no-op. PR #648 ('Ignore telocks') only changed tests.


Severity: MEDIUM
Status: Open
Category: Improper resource shutdown or release
CWE: CWE-404
Repository: ava-labs/Simplex
Branch: main
Date created: 2026-10-26
Claude Security finding: https://claude.ai/security/findings/find_0177fGoBt3cquBsrHSqEkpXS

主要言語
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 を短くまとめたダイジェスト。