Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test double proposer slashings and exits #1781

Merged
merged 4 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
45 changes: 39 additions & 6 deletions tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,55 @@
from eth2spec.test.helpers.block_header import sign_block_header
from eth2spec.test.helpers.keys import pubkey_to_privkey
from eth2spec.test.helpers.state import get_balance


def get_valid_proposer_slashing(spec, state, signed_1=False, signed_2=False):
current_epoch = spec.get_current_epoch(state)
validator_index = spec.get_active_validator_indices(state, current_epoch)[-1]
privkey = pubkey_to_privkey[state.validators[validator_index].pubkey]
def check_proposer_slashing_effect(spec, pre_state, state, slashed_index):
slashed_validator = state.validators[slashed_index]
assert slashed_validator.slashed
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH

proposer_index = spec.get_beacon_proposer_index(state)
slash_penalty = state.validators[slashed_index].effective_balance // spec.MIN_SLASHING_PENALTY_QUOTIENT
whistleblower_reward = state.validators[slashed_index].effective_balance // spec.WHISTLEBLOWER_REWARD_QUOTIENT
if proposer_index != slashed_index:
# slashed validator lost initial slash penalty
assert (
get_balance(state, slashed_index)
== get_balance(pre_state, slashed_index) - slash_penalty
)
# block proposer gained whistleblower reward
# >= because proposer could have reported multiple
assert (
get_balance(state, proposer_index)
>= get_balance(pre_state, proposer_index) + whistleblower_reward
)
else:
# proposer reported themself so get penalty and reward
# >= because proposer could have reported multiple
assert (
get_balance(state, slashed_index)
>= get_balance(pre_state, slashed_index) - slash_penalty + whistleblower_reward
)


def get_valid_proposer_slashing(spec, state, random_root=b'\x99' * 32,
slashed_index=None, signed_1=False, signed_2=False):
if slashed_index is None:
current_epoch = spec.get_current_epoch(state)
slashed_index = spec.get_active_validator_indices(state, current_epoch)[-1]
privkey = pubkey_to_privkey[state.validators[slashed_index].pubkey]
slot = state.slot

header_1 = spec.BeaconBlockHeader(
slot=slot,
proposer_index=validator_index,
proposer_index=slashed_index,
parent_root=b'\x33' * 32,
state_root=b'\x44' * 32,
body_root=b'\x55' * 32,
)
header_2 = header_1.copy()
header_2.parent_root = b'\x99' * 32
header_2.parent_root = random_root

if signed_1:
signed_header_1 = sign_block_header(spec, state, header_1, privkey)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls, with_all_phases
from eth2spec.test.helpers.block import build_empty_block_for_next_slot
from eth2spec.test.helpers.block_header import sign_block_header
from eth2spec.test.helpers.keys import privkeys
from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing
from eth2spec.test.helpers.state import get_balance, next_epoch
from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect
from eth2spec.test.helpers.state import next_epoch


def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True):
Expand All @@ -14,6 +15,8 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True)
If ``valid == False``, run expecting ``AssertionError``
"""

pre_state = state.copy()

yield 'pre', state
yield 'proposer_slashing', proposer_slashing

Expand All @@ -22,25 +25,31 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True)
yield 'post', None
return

proposer_index = proposer_slashing.signed_header_1.message.proposer_index
pre_proposer_balance = get_balance(state, proposer_index)

spec.process_proposer_slashing(state, proposer_slashing)
yield 'post', state

# check if slashed
slashed_validator = state.validators[proposer_index]
assert slashed_validator.slashed
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH

# lost whistleblower reward
assert get_balance(state, proposer_index) < pre_proposer_balance
slashed_proposer_index = proposer_slashing.signed_header_1.message.proposer_index
check_proposer_slashing_effect(spec, pre_state, state, slashed_proposer_index)


@with_all_phases
@spec_state_test
def test_success(spec, state):
# Get proposer for next slot
block = build_empty_block_for_next_slot(spec, state)
proposer_index = block.proposer_index

# Create slashing for same proposer
proposer_slashing = get_valid_proposer_slashing(spec, state,
slashed_index=proposer_index,
signed_1=True, signed_2=True)

yield from run_proposer_slashing_processing(spec, state, proposer_slashing)


@with_all_phases
@spec_state_test
def test_success_slashed_and_proposer_index_the_same(spec, state):
proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True)

yield from run_proposer_slashing_processing(spec, state, proposer_slashing)
Expand Down
200 changes: 164 additions & 36 deletions tests/core/pyspec/eth2spec/test/sanity/test_blocks.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from copy import deepcopy

from eth2spec.utils import bls

from eth2spec.test.helpers.state import get_balance, state_transition_and_sign_block, next_slot, next_epoch
from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block, \
transition_unsigned_block
from eth2spec.test.helpers.keys import privkeys, pubkeys
from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing, get_indexed_attestation_participants
from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing
from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect
from eth2spec.test.helpers.attestations import get_valid_attestation
from eth2spec.test.helpers.deposits import prepare_state_and_deposit

Expand Down Expand Up @@ -228,11 +226,11 @@ def test_empty_epoch_transition_not_finalizing(spec, state):
@spec_state_test
def test_proposer_slashing(spec, state):
# copy for later balance lookups.
pre_state = deepcopy(state)
pre_state = state.copy()
proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True)
validator_index = proposer_slashing.signed_header_1.message.proposer_index
slashed_index = proposer_slashing.signed_header_1.message.proposer_index

assert not state.validators[validator_index].slashed
assert not state.validators[slashed_index].slashed

yield 'pre', state

Expand All @@ -247,20 +245,102 @@ def test_proposer_slashing(spec, state):
yield 'blocks', [signed_block]
yield 'post', state

# check if slashed
check_proposer_slashing_effect(spec, pre_state, state, slashed_index)


@with_all_phases
@spec_state_test
def test_double_same_proposer_slashings_same_block(spec, state):
proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True)
slashed_index = proposer_slashing.signed_header_1.message.proposer_index
assert not state.validators[slashed_index].slashed

yield 'pre', state

block = build_empty_block_for_next_slot(spec, state)
block.body.proposer_slashings = [proposer_slashing, proposer_slashing]
signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True)

yield 'blocks', [signed_block]
yield 'post', None


@with_all_phases
@spec_state_test
def test_double_similar_proposer_slashings_same_block(spec, state):
slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1]

# Same validator, but different slashable offences in the same block
proposer_slashing_1 = get_valid_proposer_slashing(spec, state, random_root=b'\xaa' * 32,
slashed_index=slashed_index,
signed_1=True, signed_2=True)
proposer_slashing_2 = get_valid_proposer_slashing(spec, state, random_root=b'\xbb' * 32,
slashed_index=slashed_index,
signed_1=True, signed_2=True)
assert not state.validators[slashed_index].slashed

yield 'pre', state

block = build_empty_block_for_next_slot(spec, state)
block.body.proposer_slashings = [proposer_slashing_1, proposer_slashing_2]
signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True)

yield 'blocks', [signed_block]
yield 'post', None

djrtwo marked this conversation as resolved.
Show resolved Hide resolved

@with_all_phases
@spec_state_test
def test_multiple_different_proposer_slashings_same_block(spec, state):
pre_state = state.copy()

num_slashings = 3
proposer_slashings = []
for i in range(num_slashings):
slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i]
assert not state.validators[slashed_index].slashed

proposer_slashing = get_valid_proposer_slashing(spec, state,
slashed_index=slashed_index,
signed_1=True, signed_2=True)
proposer_slashings.append(proposer_slashing)

yield 'pre', state

#
# Add to state via block transition
#
block = build_empty_block_for_next_slot(spec, state)
block.body.proposer_slashings = proposer_slashings

signed_block = state_transition_and_sign_block(spec, state, block)

yield 'blocks', [signed_block]
yield 'post', state

for proposer_slashing in proposer_slashings:
slashed_index = proposer_slashing.signed_header_1.message.proposer_index
check_proposer_slashing_effect(spec, pre_state, state, slashed_index)


def check_attester_slashing_effect(spec, pre_state, state, validator_index):
slashed_validator = state.validators[validator_index]
assert slashed_validator.slashed
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
# lost whistleblower reward
assert get_balance(state, validator_index) < get_balance(pre_state, validator_index)

proposer_index = spec.get_beacon_proposer_index(state)
# gained whistleblower reward
assert get_balance(state, proposer_index) > get_balance(pre_state, proposer_index)


@with_all_phases
@spec_state_test
def test_attester_slashing(spec, state):
# copy for later balance lookups.
pre_state = deepcopy(state)
pre_state = state.copy()

attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True)
validator_index = get_indexed_attestation_participants(spec, attester_slashing.attestation_1)[0]
Expand All @@ -280,19 +360,11 @@ def test_attester_slashing(spec, state):
yield 'blocks', [signed_block]
yield 'post', state

slashed_validator = state.validators[validator_index]
assert slashed_validator.slashed
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
# lost whistleblower reward
assert get_balance(state, validator_index) < get_balance(pre_state, validator_index)
check_attester_slashing_effect(spec, pre_state, state, validator_index)

proposer_index = spec.get_beacon_proposer_index(state)
# gained whistleblower reward
assert (
get_balance(state, proposer_index) >
get_balance(pre_state, proposer_index)
)
# TODO: currently mainnet limits attester-slashings per block to 1.
# When this is increased, it should be tested to cover various combinations
# of duplicate slashings and overlaps of slashed attestations within the same block


@with_all_phases
Expand Down Expand Up @@ -443,35 +515,38 @@ def test_attestation(spec, state):
assert spec.hash_tree_root(state.previous_epoch_attestations) == pre_current_attestations_root


def prepare_signed_exits(spec, state, indices):
domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT)

def create_signed_exit(index):
exit = spec.VoluntaryExit(
epoch=spec.get_current_epoch(state),
validator_index=index,
)
signing_root = spec.compute_signing_root(exit, domain)
return spec.SignedVoluntaryExit(message=exit, signature=bls.Sign(privkeys[index], signing_root))

return [create_signed_exit(index) for index in indices]


# In phase1 a committee is computed for PERSISTENT_COMMITTEE_PERIOD slots ago,
# exceeding the minimal-config randao mixes memory size.
# Applies to all voluntary-exit sanity block tests.

@with_phases(['phase0'])
@spec_state_test
def test_voluntary_exit(spec, state):
validator_index = spec.get_active_validator_indices(
state,
spec.get_current_epoch(state)
)[-1]
validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1]

# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH

signed_exits = prepare_signed_exits(spec, state, [validator_index])
yield 'pre', state

voluntary_exit = spec.VoluntaryExit(
epoch=spec.get_current_epoch(state),
validator_index=validator_index,
)
domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT)
signing_root = spec.compute_signing_root(voluntary_exit, domain)
signed_voluntary_exit = spec.SignedVoluntaryExit(
message=voluntary_exit,
signature=bls.Sign(privkeys[validator_index], signing_root)
)

# Add to state via block transition
initiate_exit_block = build_empty_block_for_next_slot(spec, state)
initiate_exit_block.body.voluntary_exits.append(signed_voluntary_exit)
initiate_exit_block.body.voluntary_exits = signed_exits
signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block)

assert state.validators[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH
Expand All @@ -486,6 +561,59 @@ def test_voluntary_exit(spec, state):
assert state.validators[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH


@with_phases(['phase0'])
@spec_state_test
def test_double_validator_exit_same_block(spec, state):
validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1]

# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH

# Same index tries to exit twice, but should only be able to do so once.
signed_exits = prepare_signed_exits(spec, state, [validator_index, validator_index])
yield 'pre', state

# Add to state via block transition
initiate_exit_block = build_empty_block_for_next_slot(spec, state)
initiate_exit_block.body.voluntary_exits = signed_exits
signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block, expect_fail=True)

yield 'blocks', [signed_initiate_exit_block]
yield 'post', None

djrtwo marked this conversation as resolved.
Show resolved Hide resolved

@with_phases(['phase0'])
@spec_state_test
def test_multiple_different_validator_exits_same_block(spec, state):
validator_indices = [
spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i]
for i in range(3)
]
# move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH

signed_exits = prepare_signed_exits(spec, state, validator_indices)
yield 'pre', state

# Add to state via block transition
initiate_exit_block = build_empty_block_for_next_slot(spec, state)
initiate_exit_block.body.voluntary_exits = signed_exits
signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block)

for index in validator_indices:
assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH

# Process within epoch transition
exit_block = build_empty_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH)
signed_exit_block = state_transition_and_sign_block(spec, state, exit_block)

yield 'blocks', [signed_initiate_exit_block, signed_exit_block]
yield 'post', state

for index in validator_indices:
assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH


@with_all_phases
@spec_state_test
def test_balance_driven_status_transitions(spec, state):
Expand Down