ava-labs / ava-labs/Simplex

Builder and verifier compute ICM epoch from different parents after Telocks, deadlocking new epoch

Open
#556 0 comments 0 reactions 0 assignees View on GitHub
medium security
Dominant language
Go
Stars
22
Forks
4
Avg merge
2d 14h
Merged PRs (30d)
34

Description

## Details
When an epoch's sealing block is not immediately finalized, Telocks are built on top of it. Once the sealing block finalizes, the first block B of the new epoch is built on the last Telock. buildBlockEpochSealed computes B's ICM epoch info from the SEALING block (it passes &sealingBlock into buildBlockOrTransitionEpoch, msm.go:1418, which calls computeICMEpochInfo on it, msm.go:557), while verifyBlockEpochSealed computes the expected ICM epoch info from the DIRECT parent - the last Telock (msm.go:1452) - and uses it in the digest comparison for the new-epoch path (msm.go:1478).

ComputeICMEpoch is a deterministic transition f(parentEpoch, parentTimestamp, parentPChainHeight, childTimestamp) mirroring the proposerVM/ACP-181 rule (a child enters a new ICM epoch when its parent's timestamp reached epochStart+duration). The two computations diverge whenever an ICM epoch boundary falls between the sealing block's timestamp and the last Telock's timestamp: the Telock's stored ICMEpochInfo/timestamp make the verifier expect the advanced epoch (with EpochStartTime = the Telock's timestamp and PChainEpochHeight = the Telock's PChainHeight), while the builder - anchored to the sealing block whose timestamp precedes the boundary - keeps the old epoch. The builder's inputs are fixed forever (the sealing block never changes), so every honest proposer builds the same block that every honest verifier rejects via digest mismatch: rounds skip indefinitely and the chain deadlocks permanently at the start of the new Simplex epoch.

Triggering: this occurs naturally whenever an epoch transition's Telock window straddles an ICM epoch boundary. A malicious Telock proposer can also force the condition: it chooses its Telock's timestamp anywhere in [parent.timestamp, now+skew] (verifyTimestamp) and may raise the Telock's PChainHeight up to the verifier's current height (verifyPChainHeight allows any value in [parent, current], and the digest check uses the claimed value), deliberately crossing the boundary or desynchronizing PChainEpochHeight.

## Evidence
1. [msm/msm.go:1414–1419](https://github.com/ava-labs/Simplex/blob/main/msm/msm.go#L1414-L1419)
buildBlockEpochSealed builds the first block of the new epoch by calling buildBlockOrTransitionEpoch with &sealingBlock as the parent, even when the direct parent is a Telock built on top of the sealing block. This is the primary fix location.
2. [msm/msm.go:557–558](https://github.com/ava-labs/Simplex/blob/main/msm/msm.go#L557-L558)
Inside buildBlockOrTransitionEpoch the ICM epoch info is computed from the passed-in parent - the sealing block - using its Timestamp, PChainHeight and stored ICMEpochInfo.
3. [msm/msm.go:1450–1457](https://github.com/ava-labs/Simplex/blob/main/msm/msm.go#L1450-L1457)
verifyBlockEpochSealed computes the expected ICM epoch info from parentBlock - the DIRECT parent (the last Telock, passed in from verifyNonZeroBlock line 473) - so the two sides feed different ParentTimestamp/ParentPChainHeight/ParentEpoch into ComputeICMEpoch.
4. [msm/msm.go:1460–1478](https://github.com/ava-labs/Simplex/blob/main/msm/msm.go#L1460-L1478)
The new-epoch verification path reuses the Telock-derived icmEpochInfo in verifyAgainstExpected (line 1478), whose digest comparison (ICMEpochInfo is canoto field 6 of StateMachineMetadata and part of the block digest) rejects the honestly built block whenever the ICM transition yields a different result for the two parents.
5. [msm/msm.go:572–586](https://github.com/ava-labs/Simplex/blob/main/msm/msm.go#L572-L586)
computeICMEpochInfo derives its output from the parent block's PChainHeight, ICMEpochInfo and Timestamp - exactly the fields that differ between the sealing block and a Telock (Telocks have later timestamps, and a malicious Telock may carry a higher PChainHeight since the Telock verify path at line 1457 accepts the proposer's claimed value within [parent, current]).

## Impact
When the divergence condition holds, every honest proposer's first block of the new epoch is rejected by every honest verifier, and the builder's inputs never change, so the deadlock is permanent (chain halt requiring a coordinated fix). Fail-closed: no confidentiality or integrity loss. Rated HIGH availability, but the finding is gated behind a narrow runtime timing condition, reflected in AT:PRESENT and the overall MEDIUM severity.

## Reproduction steps
1. Requires an epoch transition where at least one Telock is built (sealing block finalization lags) and an ICM epoch boundary crossing within the Telock window - a runtime state condition. It can occur with no attacker at all; a malicious validator who is leader for a Telock can deliberately steer its timestamp (up to now+skew) or PChainHeight to create the divergence when a boundary is near. Delivered via ordinary consensus messages over the network.

## Recommended fix
The block-building path derives the new epoch's ICM epoch info from the sealing block while the verification path derives it from the direct parent (the last Telock), so a deterministic consensus computation is performed on different inputs by the two sides. Fix criteria: Build and verify derive the ICM epoch info of the first block of a new Simplex epoch from the same ancestor block. Verified by a test in which one or more Telocks (with a timestamp crossing an ICM epoch boundary and/or an advanced PChainHeight) separate the sealing block from the first new-epoch block, and the honestly built block passes verification.

---
**Severity:** MEDIUM
**Status:** Open
**Category:** Interpretation conflict
**CWE:** [CWE-436](https://cwe.mitre.org/data/definitions/436.html)
**Repository:** ava-labs/Simplex
**Branch:** main
**Date created:** 2026-08-21

---

---
_Related existing issue: #515_

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in msm/msm.go at buildBlockEpochSealed (1414–1419), then compare buildBlockOrTransitionEpoch (557–586) with verifyBlockEpochSealed (1450–1478). Trace how the sealing block and direct Telock supply ICM epoch inputs, and add a regression test with Telocks crossing an ICM boundary or changing PChainHeight. Done means the honestly built first new-epoch block passes verification.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.