Skip to content
This repository has been archived by the owner on Jul 1, 2021. It is now read-only.

Commit

Permalink
Merge pull request #375 from ralexstokes/enable-eth1-data-voting
Browse files Browse the repository at this point in the history
Enable eth1 data voting per epoch
  • Loading branch information
ralexstokes committed Mar 12, 2019
2 parents 80768c0 + f6dcc48 commit 813a337
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 1 deletion.
50 changes: 50 additions & 0 deletions eth2/beacon/state_machines/forks/serenity/epoch_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from eth_utils.toolz import (
curry,
pipe,
first,
)

from eth2.beacon import helpers
Expand Down Expand Up @@ -72,6 +73,7 @@
from eth2.beacon.datastructures.reward_settlement_context import RewardSettlementContext
from eth2.beacon.types.attestations import Attestation
from eth2.beacon.types.crosslink_records import CrosslinkRecord
from eth2.beacon.types.eth1_data_vote import Eth1DataVote
from eth2.beacon.types.pending_attestation_records import PendingAttestationRecord
from eth2.beacon.types.states import BeaconState
from eth2.beacon.typing import (
Expand All @@ -83,6 +85,54 @@
)


#
# Eth1 data votes
#
def _majority_threshold(config: BeaconConfig) -> int:
"""
Return the value constituting the majority threshold for an Eth1 data vote.
"""
return config.EPOCHS_PER_ETH1_VOTING_PERIOD * config.SLOTS_PER_EPOCH


@curry
def _is_majority_vote(config: BeaconConfig, vote: Eth1DataVote) -> bool:
return vote.vote_count * 2 > _majority_threshold(config)


def _update_eth1_vote_if_exists(state: BeaconState, config: BeaconConfig) -> BeaconState:
"""
This function searches the 'pending' Eth1 data votes in ``state`` to find one Eth1 data vote
containing majority support.
If such a vote is found, update the ``state`` entry for the latest vote.
Regardless of the existence of such a vote, clear the 'pending' storage.
"""

latest_eth1_data = state.latest_eth1_data

try:
majority_vote = first(
filter(_is_majority_vote(config), state.eth1_data_votes)
)
latest_eth1_data = majority_vote.eth1_data
except StopIteration:
pass

return state.copy(
latest_eth1_data=latest_eth1_data,
eth1_data_votes=(),
)


def process_eth1_data_votes(state: BeaconState, config: BeaconConfig) -> BeaconState:
next_epoch = state.next_epoch(config.SLOTS_PER_EPOCH)
should_process = next_epoch % config.EPOCHS_PER_ETH1_VOTING_PERIOD == 0
if should_process:
return _update_eth1_vote_if_exists(state, config)
return state


#
# Justification
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
validate_proposer_signature,
)
from .epoch_processing import (
process_eth1_data_votes,
process_justification,
process_crosslinks,
process_ejections,
Expand Down Expand Up @@ -88,7 +89,7 @@ def per_block_transition(self,
return state

def per_epoch_transition(self, state: BeaconState, block: BaseBeaconBlock) -> BeaconState:
# TODO: state = process_eth1_data_votes(state, self.config)
state = process_eth1_data_votes(state, self.config)
state = process_justification(state, self.config)
state = process_crosslinks(state, self.config)
state = process_rewards_and_penalties(state, self.config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
ZERO_HASH32,
)

from eth_utils.toolz import (
assoc,
curry,
)

from eth2._utils.tuple import (
update_tuple_item,
)
Expand Down Expand Up @@ -50,6 +55,8 @@
from eth2.beacon.datastructures.inclusion_info import InclusionInfo
from eth2.beacon.types.attestations import Attestation
from eth2.beacon.types.attestation_data import AttestationData
from eth2.beacon.types.eth1_data import Eth1Data
from eth2.beacon.types.eth1_data_vote import Eth1DataVote
from eth2.beacon.types.crosslink_records import CrosslinkRecord
from eth2.beacon.types.pending_attestation_records import PendingAttestationRecord
from eth2.beacon.state_machines.forks.serenity.epoch_processing import (
Expand All @@ -58,13 +65,17 @@
_compute_total_penalties,
_current_previous_epochs_justifiable,
_get_finalized_epoch,
_is_majority_vote,
_majority_threshold,
_process_rewards_and_penalties_for_attestation_inclusion,
_process_rewards_and_penalties_for_crosslinks,
_process_rewards_and_penalties_for_finality,
_update_eth1_vote_if_exists,
_update_latest_active_index_roots,
process_crosslinks,
process_ejections,
process_exit_queue,
process_eth1_data_votes,
process_final_updates,
process_justification,
process_slashings,
Expand All @@ -76,6 +87,118 @@
from eth2.beacon.types.validator_records import ValidatorRecord


#
# Eth1 data votes
#
def test_majority_threshold(config):
threshold = config.EPOCHS_PER_ETH1_VOTING_PERIOD * config.SLOTS_PER_EPOCH
assert _majority_threshold(config) == threshold


@curry
def _mk_eth1_data_vote(params, vote_count):
return Eth1DataVote(**assoc(params, "vote_count", vote_count))


def test_ensure_majority_votes(sample_eth1_data_vote_params, config):
threshold = _majority_threshold(config)
votes = map(_mk_eth1_data_vote(sample_eth1_data_vote_params), range(2 * threshold))
for vote in votes:
if vote.vote_count * 2 > threshold:
assert _is_majority_vote(config, vote)
else:
assert not _is_majority_vote(config, vote)


def _some_bytes(seed):
return hash_eth2(b'some_hash' + abs(seed).to_bytes(32, 'little'))


@pytest.mark.parametrize(
(
'vote_offsets' # a tuple of offsets against the majority threshold
),
(
# no eth1_data_votes
(),
# a minority of eth1_data_votes (single)
(-2,),
# a plurality of eth1_data_votes (multiple but not majority)
(-2, -2),
# almost a majority!
(0,),
# a majority of eth1_data_votes
(12,),
# NOTE: we are accepting more than one block per slot if
# there are multiple majorities so no need to test this
)
)
def test_ensure_update_eth1_vote_if_exists(sample_beacon_state_params,
config,
vote_offsets):
# one less than a majority is the majority divided by 2
threshold = _majority_threshold(config) / 2
data_votes = tuple(
Eth1DataVote(
eth1_data=Eth1Data(
deposit_root=_some_bytes(offset),
block_hash=_some_bytes(offset),
),
vote_count=threshold + offset,
) for offset in vote_offsets
)
params = assoc(sample_beacon_state_params, "eth1_data_votes", data_votes)
state = BeaconState(**params)

if data_votes: # we should have non-empty votes for non-empty inputs
assert state.eth1_data_votes

updated_state = _update_eth1_vote_if_exists(state, config)

# we should *always* clear the pending set
assert not updated_state.eth1_data_votes

# we should update the 'latest' entry if we have a majority
for offset in vote_offsets:
if offset <= 0:
assert state.latest_eth1_data == updated_state.latest_eth1_data
else:
assert len(data_votes) == 1 # sanity check
assert updated_state.latest_eth1_data == data_votes[0].eth1_data


def test_only_process_eth1_data_votes_per_period(sample_beacon_state_params, config):
slots_per_epoch = config.SLOTS_PER_EPOCH
epochs_per_voting_period = config.EPOCHS_PER_ETH1_VOTING_PERIOD
number_of_epochs_to_sample = 3

# NOTE: we process if the _next_ epoch is on a voting period, so subtract 1 here
# NOTE: we also avoid the epoch 0 so change range bounds
epochs_to_process_votes = [
(epochs_per_voting_period * epoch) - 1 for epoch in range(1, number_of_epochs_to_sample + 1)
]
state = BeaconState(**sample_beacon_state_params)

last_epoch_to_process_votes = epochs_to_process_votes[-1]
# NOTE: we arbitrarily pick two after; if this fails here, think about how to
# change so we avoid including another voting period
some_epochs_after_last_target = last_epoch_to_process_votes + 2
assert some_epochs_after_last_target % epochs_per_voting_period != 0

for epoch in range(some_epochs_after_last_target):
slot = get_epoch_start_slot(epoch, slots_per_epoch)
state = state.copy(slot=slot)
updated_state = process_eth1_data_votes(state, config)
if epoch in epochs_to_process_votes:
# we should get back a different state object
assert id(state) != id(updated_state)
# in particular, with no eth1 data votes
assert not updated_state.eth1_data_votes
else:
# we get back the same state (by value)
assert state == updated_state


#
# Justification
#
Expand Down

0 comments on commit 813a337

Please sign in to comment.