Skip to content

Commit b7b3578

Browse files
Only clear builder payment if the slashed validator is the proposer (#5365)
1 parent ef10f6e commit b7b3578

4 files changed

Lines changed: 88 additions & 7 deletions

File tree

specs/gloas/beacon-chain.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ class Builder(Container):
226226
class BuilderPendingPayment(Container):
227227
weight: Gwei
228228
withdrawal: BuilderPendingWithdrawal
229+
proposer_index: ValidatorIndex
229230
```
230231

231232
#### `BuilderPendingWithdrawal`
@@ -1482,6 +1483,7 @@ def process_execution_payload_bid(
14821483
amount=amount,
14831484
builder_index=builder_index,
14841485
),
1486+
proposer_index=block.proposer_index,
14851487
)
14861488
state.builder_pending_payments[SLOTS_PER_EPOCH + bid.slot % SLOTS_PER_EPOCH] = (
14871489
pending_payment
@@ -1813,16 +1815,22 @@ def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSla
18131815
assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature)
18141816

18151817
# [New in Gloas:EIP7732]
1816-
# Remove the BuilderPendingPayment corresponding to
1817-
# this proposal if it is still in the 2-epoch window.
1818+
# Remove the BuilderPendingPayment corresponding to this proposal if it is
1819+
# still in the 2-epoch window. Only clear it when the slashed validator is
1820+
# the proposer associated with the payment; otherwise an unrelated same-slot
1821+
# equivocation could grief an honest proposer's payment.
18181822
slot = header_1.slot
18191823
proposal_epoch = compute_epoch_at_slot(slot)
18201824
if proposal_epoch == get_current_epoch(state):
18211825
payment_index = SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH
1822-
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
1826+
payment = state.builder_pending_payments[payment_index]
1827+
if payment.proposer_index == header_1.proposer_index:
1828+
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
18231829
elif proposal_epoch == get_previous_epoch(state):
18241830
payment_index = slot % SLOTS_PER_EPOCH
1825-
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
1831+
payment = state.builder_pending_payments[payment_index]
1832+
if payment.proposer_index == header_1.proposer_index:
1833+
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
18261834

18271835
slash_validator(state, header_1.proposer_index)
18281836
```

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,55 @@ def test_builder_payment_deletion_previous_epoch_last_slot(spec, state):
423423
pre_state,
424424
proposer_slashing,
425425
)
426+
427+
428+
@with_gloas_and_later
429+
@spec_state_test
430+
def test_builder_payment_not_deleted_foreign_equivocation(spec, state):
431+
"""
432+
Test that a proposer slashing does NOT delete a builder pending payment recorded for
433+
a different proposer. This guards against a griefing vector: the clear is keyed only
434+
by header slot, so without the proposer check any validator equivocating on a slot
435+
could clear an honest proposer's payment for that slot.
436+
437+
Input State Configured:
438+
- builder_pending_payments: current-epoch entry recorded for the payment proposer
439+
- proposer_slashing: valid slashing of a different validator for the same slot,
440+
within the 2-epoch window
441+
442+
Output State Verified:
443+
- validators[slashed_index].slashed: True
444+
- builder_pending_payments: entry left intact (slashed validator is not the
445+
payment's proposer)
446+
"""
447+
active = spec.get_active_validator_indices(state, spec.get_current_epoch(state))
448+
payment_proposer = active[0] # the payment is recorded for this proposer
449+
slashed_proposer = active[-1] # equivocates on the slot, not the payment's proposer
450+
assert payment_proposer != slashed_proposer
451+
452+
proposer_slashing, _ = prepare_process_proposer_slashing(
453+
spec,
454+
state,
455+
advance_epochs=2,
456+
slot_offset=Random(2024).randrange(spec.SLOTS_PER_EPOCH),
457+
proposer_index=slashed_proposer,
458+
parent_root_2=b"\x99" * 32, # Make headers different
459+
builder_payment_amount=spec.MIN_ACTIVATION_BALANCE,
460+
builder_payment_fee_recipient=b"\x42" * 20,
461+
builder_payment_weight=1000,
462+
builder_payment_proposer_index=payment_proposer,
463+
)
464+
465+
slashed_slot = proposer_slashing.signed_header_1.message.slot
466+
assert spec.compute_epoch_at_slot(slashed_slot) == spec.get_current_epoch(state)
467+
468+
pre_state = state.copy()
469+
470+
yield from run_proposer_slashing_processing(spec, state, proposer_slashing)
471+
472+
assert_process_proposer_slashing(
473+
spec,
474+
state,
475+
pre_state,
476+
proposer_slashing,
477+
)

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ def check_proposer_slashing_effect(
5252
- slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] incremented by effective_balance
5353
- balances[slashed_index] decreased by slash penalty
5454
- balances[proposer_index] increased by whistleblower reward
55-
- [GLOAS+] builder_pending_payments cleared if header slot within 2-epoch window
55+
- [GLOAS+] builder_pending_payments entry cleared if the slashed validator is the
56+
payment's proposer and the header slot is within the 2-epoch window; left intact
57+
otherwise
5658
"""
5759
current_epoch = spec.get_current_epoch(state)
5860
pre_validator = pre_state.validators[slashed_index]
@@ -147,12 +149,23 @@ def check_proposer_slashing_effect(
147149

148150
if proposal_epoch == current_epoch:
149151
payment_index = spec.SLOTS_PER_EPOCH + header_slot % spec.SLOTS_PER_EPOCH
150-
assert state.builder_pending_payments[payment_index] == spec.BuilderPendingPayment()
151152
elif proposal_epoch == spec.get_previous_epoch(state):
152153
payment_index = header_slot % spec.SLOTS_PER_EPOCH
153-
assert state.builder_pending_payments[payment_index] == spec.BuilderPendingPayment()
154154
else:
155+
payment_index = None
156+
157+
if payment_index is None:
158+
# Slot is outside the 2-epoch window: payments are untouched
155159
assert state.builder_pending_payments == pre_state.builder_pending_payments
160+
elif pre_state.builder_pending_payments[payment_index].proposer_index == slashed_index:
161+
# The slashed validator is this payment's proposer, so it is cleared
162+
assert state.builder_pending_payments[payment_index] == spec.BuilderPendingPayment()
163+
else:
164+
# This payment has a different proposer, so it is left intact
165+
assert (
166+
state.builder_pending_payments[payment_index]
167+
== pre_state.builder_pending_payments[payment_index]
168+
)
156169

157170

158171
def get_valid_proposer_slashing(

tests/infra/helpers/proposer_slashings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def prepare_process_proposer_slashing(
6767
builder_payment_fee_recipient=None, # Fee recipient address (20 bytes)
6868
builder_payment_weight=None, # Weight for the pending payment (default: 0)
6969
builder_payment_builder_index=None, # Builder index for the payment (default: last builder)
70+
builder_payment_proposer_index=None, # Proposer recorded on the payment (default: slashed proposer)
7071
):
7172
"""
7273
Prepare a proposer slashing operation with configurable headers and state.
@@ -203,9 +204,16 @@ def prepare_process_proposer_slashing(
203204
builder_index=builder_index,
204205
)
205206

207+
payment_proposer_index = (
208+
spec.ValidatorIndex(builder_payment_proposer_index)
209+
if builder_payment_proposer_index is not None
210+
else effective_proposer_1
211+
)
212+
206213
pending_payment = spec.BuilderPendingPayment(
207214
weight=spec.Gwei(weight),
208215
withdrawal=pending_withdrawal,
216+
proposer_index=payment_proposer_index,
209217
)
210218

211219
state.builder_pending_payments[payment_index] = pending_payment

0 commit comments

Comments
 (0)