# ePBS Fork Choice Payload Extension and Reorgs This document outlines `get_head` scenarios under ePBS fork choice. The goal is to cover how payload extension, LMD-GHOST weight, proposer boost, and reorgs interact ## Notation * `n` is the current slot. `n-1`, `n-2`, `n-3` are previous slots. * `B@k` denotes beacon block `B` proposed at slot `k`. * Fork-choice node states: * `(B, P)` = PENDING * `(B, E)` = EMPTY * `(B, F)` = FULL * `EP[B]` means `B ∈ execution_payload_states` * `PTC[B]=T/F` means `sum(ptc_vote[B]) > PAYLOAD_TIMELY_THRESHOLD` * `PB=X` means `proposer_boost_root == X` (or `0` if unset) * `PB.parent` means `blocks[PB].parent_root` * `PB extends FULL` means `is_parent_node_full(blocks[PB]) == true` ## Invariants A list of invariants we plan cover in the following test cases - Payload ambiguity exists only `n-1` and does not drive weight. (`(B, E)` and `(B, F)` at `n-1` have weight `0`) - Payload semantics at `>=n-2` is determined by LMD weight via `LatestMessage.payload_present` - PENDING nodes always receive LMD weight - PB can only force EMPTY if PTC threshold is not met and on the relevant branch - PB can still force FULL if PTC threshold is not met and on the relevant branch - Children enumeration alternates: `PENDING → {EMPTY,(FULL if EP)} → PENDING → ...` (although implementation detail) ## Test cases All `get_head` tests assume: * There's a justified checkpoint `J` * Every block mentioned descends from `J` * `get_head` is called after all blocks in the setup are processed ### A. `n-1`, payload extension cases These cases isolate `should_extend_payload` and payload tie-breaking at `n-1`. This runs at start of `n` * A1. No execution payload * Setup: `B@(n-1)`, `EP[B]=false` * Expect: from `(B, P)` only child is `(B, E)` * A2. Execution payload exists, PTC timely * Setup: `B@(n-1)`, `EP[B]=true`, `PTC[B]=true` * Expect: `(B, F)` beats `(B, E)` * A3. Execution payload exists, PTC untimely, PB unset * Setup: `B@(n-1)`, `EP[B]=true`, `PTC[B]=false`, `PB=0` * Expect: `(B, F)` beats `(B, E)` * A4. Execution payload exists, PTC untimely, PB on another branch * Setup: `B@(n-1)`, `EP[B]=true`, `PTC[B]=false`, `PB=A`, `PB.parent ≠ B` * Expect: `(B, F)` beats `(B, E)` * A5. Execution payload exists, PTC untimely, PB extends FULL * Setup: `B@(n-1)`, `EP[B]=true`, `PTC[B]=false`, `PB=A`, `PB.parent == B`, `PB extends FULL` * Expect: `(B, F)` beats `(B, E)` * A6. Execution payload exists, PTC untimely, PB extends EMPTY * Setup: `B@(n-1)`, `EP[B]=true`, `PTC[B]=false`, `PB=A`, `PB.parent == B`, `PB extends EMPTY` * Expect: `(B, E)` beats `(B, F)` * A7. PTC timely but execution payload missing * Setup: `B@(n-1)`, `EP[B]=false`, `PTC[B]=true` * Expect: `(B, E)` beats `(B, F)` ### B. `n-2` with `n-1` skipped, lmd-weight cases These cases validate that payload semantics at `n-2` flow through LMD weight without `n-1` * B1. No execution payload * Setup: `J@(n-3) → B@(n-2)`, `EP[B]=false` * Expect: only `(B, E)` exists, `get_head = (B, E)` * B2. Execution payload exists, no attestations * Setup: `J@(n-3) → B@(n-2)`, `EP[B]=true`, `latest_messages = {}` * Expect: `(B, F)` wins via payload tiebreaker * B3. Execution payload missing, votes claim payload_present * Setup: `J@(n-3) → B@(n-2)`, `EP[B]=false`, `latest_messages = {root=B, slot>B.slot, payload_present=true}` * Expect: Cant be `FULL` without local execution state, `get_head = (B, E)` * B4. Execution payload exists, all votes payload_present true * Setup: `EP[B]=true`, `latest_messages's payload_present=true` * Expect: `get_head = (B, F)` * B5. Execution payload exists, all votes payload_present false * Setup: same as B4, but `payload_present=false` * Expect: `get_head = (B, E)` * B6. Two competing n-2 blocks, different weight * Setup: `B@(n-2)`, `B'@(n-2)`, `weight[B] > weight[B']`, `EP[B]=true` * Expect: `get_head = (B, F)` * B7. Two competing n-2 blocks, higher weight favors EMPTY * Setup: `weight[B'] > weight[B]`, `EP[B]=true` * Expect: `get_head = (B, E)` * B8. Equal weight, lexicographic tie break * Setup: equal weight, `B.root > B'.root`, `EP[B]=false` * Expect: `(B, E)` wins lexicographically ### C. `n` with `n-1` and `n-2`, reorg cases These cases cover block at `n` gets reorg by block in `n-1` or `n-2` `A@(n-2)` resolved as FULL or EMPTY `B@(n-1)` resolved as FULL or EMPTY, contending with `A` `C@(n)` is processed, but `get_head` reorgs `C` out and selects a head from `A||B` and `EMPTY||FULL` - C1. - Setup: `get_weight(A, FULL)` is highest after processing `C` - Expect: `get_head = (A, FULL)`, `C` is reorged while building on `B` - C2. - Setup: `get_weight(A, EMPTY)` is highest after processing `C` - Expect: `get_head = (A, EMPTY)`, `C` is reorged while building on `B` - C3. - Setup: `get_weight(B, FULL)` is highest after processing `C` - Expect: `get_head = (B, FULL)`, `C` is reorged while building on `A` - C4. - Setup: `get_weight(B, EMPTY)` is highest after processing `C` - Expect: `get_head = (B, EMPTY)`, `C` is reorged while building on `A` Here is the same set, renamed cleanly to section **D** and case labels **D1–D4**, with no other changes. ### D. proposer boost and equivocation cases These cases assume blocks exist at `n-2`, `n-1`, and `n`, and fork choice is evaluated after processing the block at `n` * D1. Equivocation at n-2 does not suppress proposer boost * Setup: * Two equivocating blocks at `n-2`, `n-1` block contends with the `n-2` blocks * Block `n` builds on top of one `n-2` block * Conditions: Equivocations are at `n-2`, not the previous slot * Expect: `should_apply_proposer_boost = true`, `get_head = n` * D2. Equivocation at n-1, head is not weak * Setup: * Two equivocating blocks at `n-1`, `n-2` block contends with `n-1` * Block `n` builds on top of `n-1` * Conditions: `n-1` head weight is not weak (> 20%) * Expect: `should_apply_proposer_boost = true`, `get_head = n` * D3. Equivocation at n-1, head is weak but not PTC timely * Setup: * Two equivocating blocks at `n-1`, `n-2` block contends with `n-1` * Block `n` builds on top of `n-1` * Conditions: `n-1` is weak (< 20%) and is not PTC timely * Expect: `should_apply_proposer_boost = true`, `get_head = n` * D4. Equivocation at n-1, head is weak and PTC timely * Setup: * Two equivocating blocks at `n-1`, `n-2` block contends with `n-1` * Block `n` builds on top of `n-1` * Conditions:`n-1` is weak (< 20%) and is PTC timely * Expect: `should_apply_proposer_boost = false`, `get_head = n-2`