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

Ensure balances remain unchanged for optimal validators during leak #1830

Merged
merged 3 commits into from
May 20, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
42 changes: 32 additions & 10 deletions specs/phase0/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,25 @@ def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei:
return Gwei(effective_balance * BASE_REWARD_FACTOR // integer_squareroot(total_balance) // BASE_REWARDS_PER_EPOCH)
```


```python
def get_proposer_reward(state: BeaconState, attesting_index: ValidatorIndex) -> Gwei:
return Gwei(get_base_reward(state, attesting_index) // PROPOSER_REWARD_QUOTIENT)
```


```python
def get_finality_delay(state: BeaconState) -> uint64:
return get_previous_epoch(state) - state.finalized_checkpoint.epoch
```


```python
def in_inactivity_leak(state: BeaconState) -> bool:
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY
```


```python
def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]:
previous_epoch = get_previous_epoch(state)
Expand All @@ -1378,8 +1397,12 @@ def get_attestation_component_deltas(state: BeaconState,
for index in get_eligible_validator_indices(state):
if index in unslashed_attesting_indices:
increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow
reward_numerator = get_base_reward(state, index) * (attesting_balance // increment)
rewards[index] += reward_numerator // (total_balance // increment)
if in_inactivity_leak(state):
# Full base reward will be cancelled out by inactivity penalty deltas
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
rewards[index] += get_base_reward(state, index)
else:
reward_numerator = get_base_reward(state, index) * (attesting_balance // increment)
rewards[index] += reward_numerator // (total_balance // increment)
else:
penalties[index] += get_base_reward(state, index)
return rewards, penalties
Expand Down Expand Up @@ -1426,9 +1449,8 @@ def get_inclusion_delay_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequ
a for a in matching_source_attestations
if index in get_attesting_indices(state, a.data, a.aggregation_bits)
], key=lambda a: a.inclusion_delay)
proposer_reward = Gwei(get_base_reward(state, index) // PROPOSER_REWARD_QUOTIENT)
rewards[attestation.proposer_index] += proposer_reward
max_attester_reward = get_base_reward(state, index) - proposer_reward
rewards[attestation.proposer_index] += get_proposer_reward(state, index)
max_attester_reward = get_base_reward(state, index) - get_proposer_reward(state, index)
rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay)

# No penalties associated with inclusion delay
Expand All @@ -1442,16 +1464,16 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S
Return inactivity reward/penalty deltas for each validator.
"""
penalties = [Gwei(0) for _ in range(len(state.validators))]
finality_delay = get_previous_epoch(state) - state.finalized_checkpoint.epoch

if finality_delay > MIN_EPOCHS_TO_INACTIVITY_PENALTY:
if in_inactivity_leak(state):
matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(state))
matching_target_attesting_indices = get_unslashed_attesting_indices(state, matching_target_attestations)
for index in get_eligible_validator_indices(state):
penalties[index] += Gwei(BASE_REWARDS_PER_EPOCH * get_base_reward(state, index))
# If validator is performing optimally this cancels all rewards for a neutral balance
base_reward = get_base_reward(state, index)
penalties[index] += Gwei(BASE_REWARDS_PER_EPOCH * base_reward - get_proposer_reward(state, index))
if index not in matching_target_attesting_indices:
effective_balance = state.validators[index].effective_balance
penalties[index] += Gwei(effective_balance * finality_delay // INACTIVITY_PENALTY_QUOTIENT)
penalties[index] += Gwei(effective_balance * get_finality_delay(state) // INACTIVITY_PENALTY_QUOTIENT)

# No rewards associated with inactivity penalties
rewards = [Gwei(0) for _ in range(len(state.validators))]
Expand Down
37 changes: 36 additions & 1 deletion tests/core/pyspec/eth2spec/test/helpers/rewards.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from random import Random
from lru import LRU

from eth2spec.phase0 import spec as spec_phase0
from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations
Expand Down Expand Up @@ -159,7 +160,8 @@ def run_get_inactivity_penalty_deltas(spec, state):
continue

if finality_delay > spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY:
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
base_penalty = spec.BASE_REWARDS_PER_EPOCH * spec.get_base_reward(state, index)
base_reward = spec.get_base_reward(state, index)
base_penalty = spec.BASE_REWARDS_PER_EPOCH * base_reward - spec.get_proposer_reward(state, index)
if not has_enough_for_reward(spec, state, index):
assert penalties[index] == 0
elif index in matching_attesting_indices:
Expand All @@ -170,6 +172,39 @@ def run_get_inactivity_penalty_deltas(spec, state):
assert penalties[index] == 0


def transition_state_to_leak(spec, state, epochs=None):
if epochs is None:
epochs = spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY
assert epochs >= spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY

for _ in range(epochs):
next_epoch(spec, state)


_cache_dict = LRU(size=10)


def leaking(epochs=None):

def deco(fn):
def entry(*args, spec, state, **kw):
# If the pre-state is not already known in the LRU, then take it,
# transition it to leak, and put it in the LRU.
# The input state is likely already cached, so the hash-tree-root does not affect speed.
key = (state.hash_tree_root(), spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY, spec.SLOTS_PER_EPOCH, epochs)
global _cache_dict
if key not in _cache_dict:
transition_state_to_leak(spec, state, epochs=epochs)
_cache_dict[key] = state.get_backing() # cache the tree structure, not the view wrapping it.

# Take an entry out of the LRU.
# No copy is necessary, as we wrap the immutable backing with a new view.
state = spec.BeaconState(backing=_cache_dict[key])
return fn(*args, spec=spec, state=state, **kw)
return entry
return deco


def set_some_new_deposits(spec, state, rng):
num_validators = len(state.validators)
# Set ~1/10 to just recently deposited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
get_valid_attestation,
prepare_state_with_attestations,
)
from eth2spec.test.helpers.rewards import leaking
from eth2spec.test.helpers.attester_slashings import get_indexed_attestation_participants
from eth2spec.test.phase_0.epoch_processing.run_epoch_process_base import run_epoch_processing_with
from random import Random
Expand Down Expand Up @@ -80,6 +81,56 @@ def test_full_attestations(spec, state):
assert state.balances[index] < pre_state.balances[index]


@with_all_phases
@spec_state_test
@leaking()
def test_full_attestations_with_leak(spec, state):
attestations = prepare_state_with_attestations(spec, state)

proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations]
pre_state = state.copy()

yield from run_process_rewards_and_penalties(spec, state)

attesting_indices = spec.get_unslashed_attesting_indices(state, attestations)
assert len(attesting_indices) == len(pre_state.validators)
for index in range(len(pre_state.validators)):
# Proposers can still make money during a leak
if index in proposer_indices:
assert state.balances[index] > pre_state.balances[index]
# If not proposer but participated optimally, should have exactly neutral balance
elif index in attesting_indices:
assert state.balances[index] == pre_state.balances[index]
else:
assert state.balances[index] < pre_state.balances[index]


@with_all_phases
@spec_state_test
@leaking()
def test_partial_attestations_with_leak(spec, state):
attestations = prepare_state_with_attestations(spec, state)

attestations = attestations[:len(attestations) // 2]
state.previous_epoch_attestations = state.previous_epoch_attestations[:len(attestations)]
proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations]
pre_state = state.copy()

yield from run_process_rewards_and_penalties(spec, state)

attesting_indices = spec.get_unslashed_attesting_indices(state, attestations)
assert len(attesting_indices) < len(pre_state.validators)
for index in range(len(pre_state.validators)):
# Proposers can still make money during a leak
if index in proposer_indices and index in attesting_indices:
assert state.balances[index] > pre_state.balances[index]
# If not proposer but participated optimally, should have exactly neutral balance
elif index in attesting_indices:
assert state.balances[index] == pre_state.balances[index]
else:
assert state.balances[index] < pre_state.balances[index]
djrtwo marked this conversation as resolved.
Show resolved Hide resolved


@with_all_phases
@spec_state_test
def test_full_attestations_random_incorrect_fields(spec, state):
Expand Down
36 changes: 1 addition & 35 deletions tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,6 @@
from eth2spec.test.context import with_all_phases, spec_state_test
from eth2spec.test.helpers.state import next_epoch
from eth2spec.test.helpers.rewards import leaking
import eth2spec.test.helpers.rewards as rewards_helpers
from lru import LRU


def transition_state_to_leak(spec, state, epochs=None):
if epochs is None:
epochs = spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY
assert epochs >= spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY

for _ in range(epochs):
next_epoch(spec, state)


_cache_dict = LRU(size=10)


def leaking(epochs=None):

def deco(fn):
def entry(*args, spec, state, **kw):
# If the pre-state is not already known in the LRU, then take it,
# transition it to leak, and put it in the LRU.
# The input state is likely already cached, so the hash-tree-root does not affect speed.
key = (state.hash_tree_root(), spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY, spec.SLOTS_PER_EPOCH, epochs)
global _cache_dict
if key not in _cache_dict:
transition_state_to_leak(spec, state, epochs=epochs)
_cache_dict[key] = state.get_backing() # cache the tree structure, not the view wrapping it.

# Take an entry out of the LRU.
# No copy is necessary, as we wrap the immutable backing with a new view.
state = spec.BeaconState(backing=_cache_dict[key])
return fn(*args, spec=spec, state=state, **kw)
return entry
return deco


@with_all_phases
Expand Down