Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions specs/gloas/fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,10 @@ def should_apply_proposer_boost(store: Store) -> bool:
if store.proposer_boost_root == Root():
return False

# Withhold boost if the boosted block's proposer has equivocated
if is_proposer_equivocation(store, store.proposer_boost_root):
return False

block = store.blocks[store.proposer_boost_root]
parent_root = block.parent_root
parent = store.blocks[parent_root]
Expand Down
4 changes: 3 additions & 1 deletion specs/gloas/p2p-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,9 @@ def validate_beacon_block_gossip(
if block.slot <= finalized_slot:
raise GossipIgnore("block is not from a slot greater than the latest finalized slot")

# [IGNORE] The block is the first block with valid signature received for the slot and proposer
# [IGNORE] The block is the first block with valid signature received for the slot and proposer.
# Note: Implementations SHOULD still pass this block to `on_block` so both blocks enter
# `store.blocks` and `is_proposer_equivocation()` can observe the equivocation.
proposer_slot_key = (block.slot, block.proposer_index)
if proposer_slot_key in seen.proposer_slots:
raise GossipIgnore("block is not the first valid block for this slot and proposer")
Expand Down
4 changes: 3 additions & 1 deletion specs/phase0/p2p-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,9 @@ def validate_beacon_block_gossip(
if block.slot <= finalized_slot:
raise GossipIgnore("block is not from a slot greater than the latest finalized slot")

# [IGNORE] The block is the first block with valid signature received for the slot and proposer
# [IGNORE] The block is the first block with valid signature received for the slot and proposer.
# Note: Implementations SHOULD still pass this block to `on_block` so both blocks enter
# `store.blocks` and `is_proposer_equivocation()` can observe the equivocation.
proposer_slot_key = (block.slot, block.proposer_index)
if proposer_slot_key in seen.proposer_slots:
raise GossipIgnore("block is not the first valid block for this slot and proposer")
Comment on lines +640 to 644

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the executable gossip spec could do this now, ie. add equivocating blocks to fork choice store, this is required in several places in fork choice, eg. should_apply_proposer_boost or get_proposer_head

but doing this here seems wrong to me, so the comment says first block with valid signature but at that point, there was no signature check yet

the signature check is done further below on line 651 and also noticed we do the signature check before the proposer check itself, it seems same in lodestar but need to double check why that is

but if we wanna add a equivocating block via on_block it probably has to be done at the end of gossip validation unless we are fine with skipping some gossip checks

cc @jtraglia

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,52 @@ def test_should_apply_proposer_boost_withheld(spec, state):

output_store_checks(spec, store, test_steps, with_viable_for_head_weights=True)
yield "steps", test_steps


@with_gloas_and_later
@with_presets([MINIMAL], reason="too slow")
@spec_state_test
def test_should_apply_proposer_boost_proposer_equivocation(spec, state):
"""
The boosted block's own proposer equivocated: a second block from the same
proposer at the boosted block's slot. Boost is withheld regardless of the
parent-adjacency and weakness conditions checked later in the function. Here the
parent is two slots back, so absent the equivocation the "not adjacent" escape
would apply the boost; the equivocation overrides it.
"""
store, state, test_steps = yield from setup_finalized_store(spec, state)

# Leave a gap so the boosted block's parent is two slots back (non-adjacent),
# which on its own would make should_apply_proposer_boost return True.
next_slot(spec, state)

pre_state = state.copy()
block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
block_root = signed_block.message.hash_tree_root()
yield from tick_and_add_block(spec, store, signed_block, test_steps)
assert store.proposer_boost_root == block_root
# The boosted block's parent is two slots back (empty slot from next_slot above).
assert store.blocks[block.parent_root].slot + 2 == block.slot

# Baseline: without an equivocation, the boost applies (non-adjacent escape).
assert spec.should_apply_proposer_boost(store) is True
_assert_weight_reflects_boost(spec, store, block_root, boost_applied=True)

# Build a same-slot same-proposer equivocation of the boosted block.
equivocation_state = pre_state.copy()
equivocation_block = build_empty_block(spec, equivocation_state, slot=block.slot)
equivocation_block.body.graffiti = spec.Bytes32(b"\x01" * 32)
signed_equivocation = state_transition_and_sign_block(
spec, equivocation_state, equivocation_block
)
assert signed_equivocation.message.proposer_index == signed_block.message.proposer_index
assert signed_equivocation.message.hash_tree_root() != block_root
yield from add_block(spec, store, signed_equivocation, test_steps)

# The boosted block's proposer has now equivocated -> boost is withheld.
assert spec.should_apply_proposer_boost(store) is False
_assert_weight_reflects_boost(spec, store, block_root, boost_applied=False)

output_store_checks(spec, store, test_steps, with_viable_for_head_weights=True)
yield "steps", test_steps