# prysm-6 disprove attempt: init-sync batch path vs Gloas `on_block`

## VERDICT: CONFIRMED (narrowly), confidence medium-low on impact

The code divergence is real at pin eb681c1d. `onBlockBatch` never feeds in-block payload attestations to fork choice. `fillInForkChoiceMissingBlocks` also marks the parent's FULL node (`insertFirstPayloadIfNeeded`) from `bid.parent_block_hash == parent's block_hash` alone. That runs before batch signature verification and before `InsertPayload`, and nothing rolls it back. The spec requires `is_payload_verified(store, block.parent_root)` before building on a full parent, and `notify_ptc_messages` in every `on_block`. The condition does not need a malicious peer in the normal case: the FULL mark is applied even when envelopes[0] is present and valid. I did not find a check on the same path that gives the spec's result. The strongest refutation (routes 1 and 5) shows that the common case is benign. The surviving, exploitable sub-case is a batch served with no envelopes and a first block that claims a full parent. I cannot call it a consensus split. I rate it fork-choice divergence, low-to-medium severity. I did not run anything, so the impact is reasoned from code only.

## Refutation attempts, in order

**(1) Another check on the same path gives the spec result: PARTIAL. It covers the envelope-present case only.**
- `getBatchPrestate` (process_block.go, around L130-L160) handles the case where the first block builds on a full parent and envelopes[0] is supplied. It calls `BlockBuiltOnParentEnvelope(envelopes[0], b)` and, if the DB has no envelope for the parent, `notifyNewEnvelope`, which asks the EL. Its signature is added to `sigSet` (L~215), the batch signature check is at L332-L334, and `InsertPayload(env)` is at L368. So when an envelope is supplied, the envelope is eventually EL-checked and signature-checked, and the FULL node is built with the real gas limit.
- That does not undo the early mark. `insertFirstPayloadIfNeeded` (process_block_helpers.go#L457-L469) runs at process_block.go#L235, before `sigSet.Verify` (#L332-L334) and before `InsertPayload` (#L368). `MarkFullNode` (forkchoice/doubly-linked-tree/gloas.go#L468-L486) sets `optimistic: true` and creates the node. `InsertPayload` (gloas.go#L489-L519) returns early when a full node already exists ("We don't import two payloads for the same root"). So with `applied == true` the later `InsertPayload` is a no-op. This also means `updateNewFullNodeWeight` is skipped, so previously recorded payload-status votes for that root are not added to the new FULL node. This is a second, smaller finding that I only reasoned about and did not confirm with a test.
- If the batch then fails (signature check, EL error, `errBatchEnvelopeMismatch` at the `eidx != len(envelopes)` check), `onBlockBatch` returns the error. I found no rollback of the mark anywhere. `MarkFullNode` has no inverse in `forkchoice/interfaces.go#L132`.
- No envelope supplied: `getBatchPrestate` returns early when `len(envelopes) == 0` (it never checks `full`). The state transition then runs `ProcessParentExecutionPayload` (core/gloas/parent_payload.go#L49-L56), which only checks the requests root against the parent bid, and no EL call is made. `eidx(0) == len(envelopes)(0)` so no mismatch error occurs. The fetcher side, `validatePayloadBlockConsistency` (sync/initial-sync/blocks_fetcher_payload.go#L52-L58), only downscores the peer in the empty-envelopes case, and only when blocks and payloads came from the same peer. It does not set `r.err`. So I could not find a gate stopping this case. I did not trace every caller in `round_robin.go` (only L263 `HasFullNode` was seen), so a later gate may exist.

**(2) Cannot occur on a valid chain: not established.** On a valid chain with honest peers, envelopes[0] exists and verifies, so the FULL mark ends up correct. The invariant "full parent implies the envelope was delivered and verified" holds for a valid chain, but the mark is made before the proof, so a peer can break the ordering. The spec's assert is local to the store, not a chain invariant.

**(3) Spec misread: no.** Quoted from consensus-specs 8a80ffa8 `specs/gloas/fork-choice.md` (the Modified `on_block` section, the `is_payload_verified` assert near L1059-L1061 and the `notify_ptc_messages` call at L1095):
- `if is_parent_node_full(store, block): assert is_payload_verified(store, block.parent_root)`
- `notify_ptc_messages(store, state, block.body.payload_attestations)`
- `is_payload_verified`: "Return whether the execution payload envelope for the beacon block with root root has been locally delivered and verified via on_execution_payload_envelope." The definition is `return root in store.payloads`.
- Anchor store has `payloads={}` (L243).
- Caveat: the spec's `on_block` sits inside a loop of per-block handlers. Prysm's batch intentionally defers the EL call to `notifyEngineAndSaveData`, which is a legitimate optimization. It only deviates where the store state is observable before verification completes.

**(4) A later commit fixes it: not found.** `api.github.com/repos/OffchainLabs/prysm/commits?path=...&sha=develop` shows no commit after the pin touching this logic, up to a head of 2026-10-21 (8c923e6c, #17739). That is a bid-getter refactor. Relevant earlier commits are #17093 "Mark full node gas limit", 2026-07-10, which added the gas limit read in `insertFirstPayloadIfNeeded`, and #17566 "Gloas batch envelope binding", 2026-10-14. I did not read the #17566 diff, so I cannot say whether it covers the empty-envelopes case.

**(5) Batch-synced blocks are old, and envelopes are checked elsewhere: partly true.**
- PTC votes inside a block at slot s concern slot s-1. For a batch that ends well before the head, those votes cannot change head selection, because later blocks and attestations dominate and the PTC window has passed. The missing votes only matter for the last one or two slots of the final batch, and for the tip-of-chain "undecided" payload decision. I have not established that this cannot flip a decision. It is plausible but not proven.
- `ParentPayloadReady` (blockchain/chain_info.go#L478-L491) returns `HasFullNode(parentRoot)`. Because the early mark makes `HasFullNode` true, it reports ready without verification, so it does not backstop the batch path. The same holds for the other `HasFullNode` uses (sync/validate_beacon_block_gloas.go#L75 and #L127, sync/validate_beacon_attestation.go#L393), which also treat the unverified mark as verified.
- Rollback on batch failure: none found.

## Corrected record

- **Spec:** consensus-specs 8a80ffa8, specs/gloas/fork-choice.md `on_block`, the `is_payload_verified` assert (~L1059-L1061) and `notify_ptc_messages` (L1095); `is_payload_verified` at ~L311-L320. These are the lines the brief cited, which I re-read in the pin, and the line numbers are approximate.
- **Code at eb681c1d:**
  - beacon-chain/blockchain/process_block.go#L188-L382 (`onBlockBatch`); fill-in call #L235; sig verify #L332-L334; `InsertPayload` #L368; `InsertChain` #L372; the per-block path calls `handleBlockPayloadAttestations` at #L89, but `onBlockBatch` does not
  - beacon-chain/blockchain/process_block_helpers.go#L390-L455 (`fillInForkChoiceMissingBlocks`), #L444 and #L447 (calls), #L457-L469 (`insertFirstPayloadIfNeeded`)
  - beacon-chain/blockchain/chain_info.go#L463-L476 (`builtOnFullParentInForkchoice`: block-hash comparison only, no envelope check)
  - beacon-chain/forkchoice/doubly-linked-tree/gloas.go#L468-L486 (`MarkFullNode`), #L489-L519 (`InsertPayload`)
  - beacon-chain/core/gloas/parent_payload.go#L49-L56 (requests-root check only)
- **Condition:** initial sync, a batch whose first block builds on a full parent whose parent node is in fork choice.
  - (a) Envelope supplied: FULL is marked early and `InsertPayload` becomes a no-op. The envelope is still verified later, so the effect is limited to the gas-limit source and the skipped weight update.
  - (b) No envelope supplied (peer withholds): FULL is marked with no EL verification and no envelope. The block-hash match is only a comparison against the claimed bid.
  - (c) The batch is rejected after the early mark: the FULL node persists with `optimistic: true` and no verified envelope.
  - In-block PTC votes are never applied for any batch block.
- **Other client:** Lighthouse a376defa, consensus/fork_choice/src/fork_choice.rs#L829-L839. I did not re-read that file here and rely on the brief that it agrees with the spec.
- **Confidence:** medium that the divergence exists; low that it has observable effect; low-to-medium that a hostile peer can exploit case (b) end to end, since I traced only part of the sync path.
- **Severity:** fork-choice divergence (local view). Not a consensus split and not an invalid block accepted, because FULL status without an envelope does not by itself accept an invalid state transition. It could cause the node to build on, or vote for, a full-parent payload it never verified. This is on a Gloas devnet-only path, not live mainnet code.
- **Suggested fix:** move `insertFirstPayloadIfNeeded` after signature verification, or require `applied` before marking, and call `handleBlockPayloadAttestations` for batch blocks after `InsertChain`.

## Known-check

Searched `api.github.com/search/issues` for "insertFirstPayloadIfNeeded OR onBlockBatch OR MarkFullNode". I found nothing about this issue.
- #17093 "Mark full node gas limit", closed (gas-limit source, not this issue)
- #17652 "Verify gossip payload envelope signatures without loading state", closed
- #17497 "Gloas late head payloads", closed
- #17273 and #17275 (init-sync FCU), closed
- #17566 "Gloas batch envelope binding", merged (commit), diff not read
- #17735 (restart rebuild, open): not re-opened here; the brief says it is a different path

I did not exhaustively search issues for "initial sync" with envelopes, so "not known" means not found, not proven absent.

## URLs read

- https://raw.githubusercontent.com/OffchainLabs/prysm/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/beacon-chain/blockchain/process_block.go
- https://raw.githubusercontent.com/OffchainLabs/prysm/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/beacon-chain/blockchain/process_block_helpers.go
- https://raw.githubusercontent.com/OffchainLabs/prysm/eb681c1d13b57c2d0c5da27a63bb41ac9d071e1a/beacon-chain/core/gloas/parent_payload.go
- Shallow clone of https://github.com/OffchainLabs/prysm at eb681c1d: forkchoice, chain_info.go, initial-sync fetcher files
- https://raw.githubusercontent.com/ethereum/consensus-specs/8a80ffa8/specs/gloas/fork-choice.md
- https://api.github.com/repos/OffchainLabs/prysm/commits?path=<file>&sha=develop (three files)
- https://api.github.com/search/issues?q=repo:OffchainLabs/prysm+insertFirstPayloadIfNeeded+OR+onBlockBatch+OR+MarkFullNode
