Skip to content

Commit 8a3df1d

Browse files
authored
Fix payload availability lookup using the parent block's slot (#5473)
In `get_attestation_participation_flag_indices`, the payload availability bit was looked up at `data.slot`, which is not the slot of the attested block when there are skipped slots. As a result, an attestation to a skipped slot with `data.index == 1` always failed payload matching and missed out on the timely head reward. Since the timely head flag requires both the minimum inclusion delay and a head root that matches the block root at the attestation slot, an attestation can only receive the flag if it attests to the parent block of the block that includes it. The availability bit for the attested block therefore lives at the parent block's slot, which `process_execution_payload_bid` now returns (read from the bid in the state before it is overwritten) and `process_block` passes down to `process_attestation`. This is a loop-free alternative to #5442, as suggested in #5442 (comment). Unlike the walk back through the historical block roots, there is no special case when the attested block is more than `SLOTS_PER_HISTORICAL_ROOT` slots old. The availability bit at the parent block's slot is always up to date, because the including block's parent payload processing is what last wrote it. Fixes #5399.
1 parent b87d580 commit 8a3df1d

5 files changed

Lines changed: 130 additions & 28 deletions

File tree

specs/gloas/beacon-chain.md

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -872,11 +872,21 @@ def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorInd
872872
#### Modified `get_attestation_participation_flag_indices`
873873

874874
*Note*: The function `get_attestation_participation_flag_indices` is modified to
875-
include a new payload matching constraint to `is_matching_head`.
875+
include a new payload matching constraint to `is_matching_head`. The new
876+
`parent_slot` parameter is the slot of the parent block of the block being
877+
processed. Since the timely head flag requires both the minimum inclusion delay
878+
and a head root that matches the block root at the attestation slot, an
879+
attestation can only receive the flag if it attests to the parent block. The
880+
payload availability of the attested block is therefore tracked at
881+
`parent_slot`, even when `data.slot` is a skipped slot.
876882

877883
```python
878884
def get_attestation_participation_flag_indices(
879-
state: BeaconState, data: AttestationData, inclusion_delay: Uint64
885+
state: BeaconState,
886+
data: AttestationData,
887+
inclusion_delay: Uint64,
888+
# [New in Gloas:EIP7732]
889+
parent_slot: Slot,
880890
) -> Sequence[int]:
881891
"""
882892
Return the flag indices that are satisfied by an attestation.
@@ -898,7 +908,7 @@ def get_attestation_participation_flag_indices(
898908
assert data.index == 0
899909
payload_matches = True
900910
else:
901-
slot_index = data.slot % SLOTS_PER_HISTORICAL_ROOT
911+
slot_index = parent_slot % SLOTS_PER_HISTORICAL_ROOT
902912
payload_index = state.execution_payload_availability[slot_index]
903913
payload_matches = data.index == payload_index
904914

@@ -1251,11 +1261,11 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
12511261
# [Modified in Gloas:EIP7732]
12521262
# Removed `process_execution_payload`
12531263
# [New in Gloas:EIP7732]
1254-
process_execution_payload_bid(state, block.body.signed_execution_payload_bid)
1264+
parent_slot = process_execution_payload_bid(state, block.body.signed_execution_payload_bid)
12551265
process_randao(state, block.body)
12561266
process_eth1_data(state, block.body)
12571267
# [Modified in Gloas:EIP7732]
1258-
process_operations(state, block.body)
1268+
process_operations(state, block.body, parent_slot)
12591269
process_sync_aggregate(state, block.body.sync_aggregate)
12601270
```
12611271

@@ -1607,10 +1617,14 @@ def verify_execution_payload_bid_signature(
16071617

16081618
##### New `process_execution_payload_bid`
16091619

1620+
*Note*: This function returns the slot of the parent block, read from the bid in
1621+
the state before it is overwritten by the new bid. The slot is later given to
1622+
`process_attestation` to look up the payload availability of the attested block.
1623+
16101624
```python
16111625
def process_execution_payload_bid(
16121626
state: BeaconState, signed_bid: SignedExecutionPayloadBid
1613-
) -> None:
1627+
) -> Slot:
16141628
bid = signed_bid.message
16151629
builder_index = bid.builder_index
16161630
amount = bid.value
@@ -1658,25 +1672,37 @@ def process_execution_payload_bid(
16581672
pending_payment
16591673
)
16601674

1675+
# Cache the parent block's slot before overwriting the bid
1676+
parent_slot = state.latest_execution_payload_bid.slot
1677+
16611678
# Cache the signed execution payload bid
16621679
state.latest_execution_payload_bid = bid
1680+
1681+
return parent_slot
16631682
```
16641683

16651684
#### Operations
16661685

16671686
##### Modified `process_operations`
16681687

1669-
*Note*: `process_operations` is modified to process PTC attestations and removes
1670-
calls to `process_deposit_request`, `process_withdrawal_request`, and
1688+
*Note*: `process_operations` is modified to process PTC attestations, to pass
1689+
the parent block's slot to `process_attestation`, and removes calls to
1690+
`process_deposit_request`, `process_withdrawal_request`, and
16711691
`process_consolidation_request`.
16721692

16731693
```python
1674-
def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
1694+
def process_operations(
1695+
state: BeaconState,
1696+
body: BeaconBlockBody,
1697+
# [New in Gloas:EIP7732]
1698+
parent_slot: Slot,
1699+
) -> None:
16751700
assert len(body.deposits) == 0
16761701

1677-
def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
1702+
# [Modified in Gloas:EIP7732]
1703+
def for_ops(operations: Sequence[Any], fn: Callable[..., None], *args: Any) -> None:
16781704
for operation in operations:
1679-
fn(state, operation)
1705+
fn(state, operation, *args)
16801706

16811707
# [New in Gloas:EIP7688]
16821708
assert len(body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS
@@ -1690,7 +1716,7 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
16901716
for_ops(body.proposer_slashings, process_proposer_slashing)
16911717
for_ops(body.attester_slashings, process_attester_slashing)
16921718
# [Modified in Gloas:EIP7732]
1693-
for_ops(body.attestations, process_attestation)
1719+
for_ops(body.attestations, process_attestation, parent_slot)
16941720
for_ops(body.voluntary_exits, process_voluntary_exit)
16951721
for_ops(body.bls_to_execution_changes, process_bls_to_execution_change)
16961722
# [Modified in Gloas:EIP7732]
@@ -1827,10 +1853,16 @@ def process_builder_exit_request(state: BeaconState, request: BuilderExitRequest
18271853

18281854
*Note*: The function is modified to track the weight for pending builder
18291855
payments and to use the `index` field in the `AttestationData` to signal the
1830-
payload availability.
1856+
payload availability. The new `parent_slot` parameter, the slot of the parent
1857+
block, is where the payload availability of the attested block is looked up.
18311858

18321859
```python
1833-
def process_attestation(state: BeaconState, attestation: Attestation) -> None:
1860+
def process_attestation(
1861+
state: BeaconState,
1862+
attestation: Attestation,
1863+
# [New in Gloas:EIP7732]
1864+
parent_slot: Slot,
1865+
) -> None:
18341866
data = attestation.data
18351867
assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state))
18361868
assert data.target.epoch == compute_epoch_at_slot(data.slot)
@@ -1855,8 +1887,9 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None:
18551887
assert len(attestation.aggregation_bits) == committee_offset
18561888

18571889
# Participation flag indices
1890+
# [Modified in Gloas:EIP7732]
18581891
participation_flag_indices = get_attestation_participation_flag_indices(
1859-
state, data, state.slot - data.slot
1892+
state, data, state.slot - data.slot, parent_slot
18601893
)
18611894

18621895
# Verify signature

tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_attestation.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from eth_consensus_specs.test.context import (
22
spec_state_test,
33
with_gloas_and_later,
4+
with_presets,
45
)
56
from eth_consensus_specs.test.helpers.attestations import (
67
get_valid_attestation,
78
run_attestation_processing,
89
sign_attestation,
910
)
1011
from eth_consensus_specs.test.helpers.block import apply_empty_block
12+
from eth_consensus_specs.test.helpers.constants import MINIMAL
1113
from eth_consensus_specs.test.helpers.state import (
1214
next_slots,
1315
transition_to_slot_via_block,
@@ -348,15 +350,19 @@ def test_same_slot_attestation_ignores_payload_availability(spec, state):
348350
@spec_state_test
349351
def test_matching_payload_gets_head_flag(spec, state):
350352
"""
351-
Test get_attestation_participation_flag_indices for historical slots where
352-
is_matching_payload = data.index == state.execution_payload_availability[data.slot % SLOTS_PER_HISTORICAL_ROOT]
353-
and is_matching_head = is_matching_blockroot and is_matching_payload.
353+
Test that ``get_attestation_participation_flag_indices`` compares
354+
``data.index`` against the availability bit of the attested block's slot
355+
(the parent block's slot), not ``data.slot``, when the attestation is for
356+
a skipped slot.
354357
"""
355358
# Use missed slot scenario: blocks for slots 1, 2, 3 but skip slot 4
356359
slot_4_block_root = _setup_missed_slot_scenario(spec, state)
357360

358-
# Set payload availability bit to 1 for slot 4
359-
state.execution_payload_availability[4] = 1
361+
# The attested block is at slot 3, so its availability bit is the one
362+
# that matters. Set it to 1 to indicate the payload was revealed. Unset
363+
# the bit at data.slot to show that it is ignored.
364+
state.execution_payload_availability[3] = 1
365+
state.execution_payload_availability[4] = 0
360366

361367
# Create attestation with index = 1 to match the availability bit
362368
attestation = get_valid_attestation(spec, state, slot=4, beacon_block_root=slot_4_block_root)
@@ -388,8 +394,11 @@ def test_mismatched_payload_no_head_flag(spec, state):
388394
# Use missed slot scenario: blocks for slots 1, 2, 3 but skip slot 4
389395
slot_4_block_root = _setup_missed_slot_scenario(spec, state)
390396

391-
# Set payload availability bit to 0 for slot 4
392-
state.execution_payload_availability[4] = 0
397+
# The attested block is at slot 3, so its availability bit is the one
398+
# that matters. Set it to 0 to indicate the payload was withheld. Set
399+
# the bit at data.slot to show that it is ignored.
400+
state.execution_payload_availability[3] = 0
401+
state.execution_payload_availability[4] = 1
393402

394403
# Create attestation with index = 1 which does NOT match availability bit = 0
395404
attestation = get_valid_attestation(spec, state, slot=4, beacon_block_root=slot_4_block_root)
@@ -411,6 +420,44 @@ def test_mismatched_payload_no_head_flag(spec, state):
411420
assert not final_head_flag, "Should not get head flag when payload doesn't match"
412421

413422

423+
@with_gloas_and_later
424+
@with_presets([MINIMAL], reason="too slow")
425+
@spec_state_test
426+
def test_old_attested_block_gets_head_flag(spec, state):
427+
"""
428+
Test that an attestation still receives the timely head flag when the
429+
attested block is ``SLOTS_PER_HISTORICAL_ROOT`` slots old. The availability
430+
bit at the parent block's slot is up to date no matter how old the parent
431+
block is, because the parent payload processing of the including block is
432+
what last wrote it.
433+
"""
434+
# Skip every slot since genesis, so the attested block (genesis) is
435+
# SLOTS_PER_HISTORICAL_ROOT slots before the attestation slot
436+
attestation_slot = spec.Slot(spec.SLOTS_PER_HISTORICAL_ROOT)
437+
next_slots(spec, state, attestation_slot + spec.MIN_ATTESTATION_INCLUSION_DELAY)
438+
439+
attestation = get_valid_attestation(spec, state, slot=attestation_slot, signed=True)
440+
441+
assert attestation.data.index == 0
442+
assert spec.is_attestation_same_slot(state, attestation.data) is False
443+
# The attested block (genesis) still matches the historical block roots
444+
assert attestation.data.beacon_block_root == spec.get_block_root_at_slot(
445+
state, attestation_slot
446+
)
447+
# The genesis payload was never revealed, which matches data.index == 0
448+
parent_slot = state.latest_execution_payload_bid.slot
449+
assert parent_slot == 0
450+
assert not state.execution_payload_availability[parent_slot % spec.SLOTS_PER_HISTORICAL_ROOT]
451+
452+
attesting_indices = spec.get_attesting_indices(state, attestation)
453+
validator_index = next(iter(attesting_indices))
454+
455+
yield from run_attestation_processing(spec, state, attestation, valid=True)
456+
457+
final_participation = state.current_epoch_participation[validator_index]
458+
assert spec.has_flag(final_participation, spec.TIMELY_HEAD_FLAG_INDEX)
459+
460+
414461
@with_gloas_and_later
415462
@spec_state_test
416463
def test_builder_payment_weight_tracking_previous_epoch(spec, state):
@@ -438,7 +485,10 @@ def test_builder_payment_weight_tracking_previous_epoch(spec, state):
438485

439486
attester = spec.get_beacon_committee(state, attestation_slot, 0)[0]
440487
expected_flag_indices = spec.get_attestation_participation_flag_indices(
441-
state, attestation.data, state.slot - attestation.data.slot
488+
state,
489+
attestation.data,
490+
state.slot - attestation.data.slot,
491+
state.latest_execution_payload_bid.slot,
442492
)
443493
pre_prev_flags = state.previous_epoch_participation[attester]
444494
pre_curr_flags = state.current_epoch_participation[attester]

tests/core/pyspec/eth_consensus_specs/test/helpers/attestations.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
from eth_consensus_specs.utils.ssz.ssz_typing import Bitlist
1919

2020

21+
def process_attestation(spec, state, attestation):
22+
if is_post_gloas(spec):
23+
# Outside of block processing, the bid in the state is still the
24+
# parent block's bid, so its slot is the parent block's slot.
25+
spec.process_attestation(state, attestation, state.latest_execution_payload_bid.slot)
26+
else:
27+
spec.process_attestation(state, attestation)
28+
29+
2130
def run_attestation_processing(spec, state, attestation, valid=True):
2231
"""
2332
Run ``process_attestation``, yielding:
@@ -33,7 +42,7 @@ def run_attestation_processing(spec, state, attestation, valid=True):
3342

3443
# If the attestation is invalid, processing is aborted, and there is no post-state.
3544
if not valid:
36-
expect_assertion_error(lambda: spec.process_attestation(state, attestation))
45+
expect_assertion_error(lambda: process_attestation(spec, state, attestation))
3746
yield "post", None
3847
return
3948

@@ -42,7 +51,7 @@ def run_attestation_processing(spec, state, attestation, valid=True):
4251
previous_epoch_count = len(state.previous_epoch_attestations)
4352

4453
# process attestation
45-
spec.process_attestation(state, attestation)
54+
process_attestation(spec, state, attestation)
4655

4756
# Make sure the attestation has been processed
4857
if not is_post_altair(spec):
@@ -253,7 +262,7 @@ def add_attestations_to_state(spec, state, attestations, slot):
253262
if state.slot < slot:
254263
spec.process_slots(state, slot)
255264
for attestation in attestations:
256-
spec.process_attestation(state, attestation)
265+
process_attestation(spec, state, attestation)
257266

258267

259268
def get_valid_attestations_at_slot(

tests/core/pyspec/eth_consensus_specs/test/helpers/block_processing.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from eth_consensus_specs.test.helpers.attestations import process_attestation
2+
3+
14
def for_ops(state, operations, fn) -> None:
25
for operation in operations:
36
fn(state, operation)
@@ -19,7 +22,9 @@ def get_process_calls(spec):
1922
state, block.body.shard_headers, spec.process_shard_header
2023
),
2124
"process_attestation": lambda state, block: for_ops(
22-
state, block.body.attestations, spec.process_attestation
25+
state,
26+
block.body.attestations,
27+
lambda state, attestation: process_attestation(spec, state, attestation),
2328
),
2429
"process_deposit": lambda state, block: for_ops(
2530
state, block.body.deposits, spec.process_deposit

tests/core/pyspec/eth_consensus_specs/test/phase0/sanity/test_blocks.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ def process_and_sign_block_without_header_validations(spec, state, block):
185185
# Perform rest of process_block transitions
186186
spec.process_randao(state, block.body)
187187
spec.process_eth1_data(state, block.body)
188-
spec.process_operations(state, block.body)
188+
if is_post_gloas(spec):
189+
# The bid is not processed here, so the bid in the state is still
190+
# the parent block's bid and its slot is the parent block's slot.
191+
spec.process_operations(state, block.body, state.latest_execution_payload_bid.slot)
192+
else:
193+
spec.process_operations(state, block.body)
189194
if is_post_altair(spec):
190195
spec.process_sync_aggregate(state, block.body.sync_aggregate)
191196

0 commit comments

Comments
 (0)