Merge pull request #1830 from ethereum/neutral-rewards

Ensure balances remain unchanged for optimal validators during leak
This commit is contained in:
Danny Ryan 2020-05-20 11:15:27 -06:00 committed by GitHub
commit 7cb8e5e8f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 69 deletions

View File

@ -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) 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 is_in_inactivity_leak(state: BeaconState) -> bool:
return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY
```
```python ```python
def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]:
previous_epoch = get_previous_epoch(state) previous_epoch = get_previous_epoch(state)
@ -1378,8 +1397,13 @@ def get_attestation_component_deltas(state: BeaconState,
for index in get_eligible_validator_indices(state): for index in get_eligible_validator_indices(state):
if index in unslashed_attesting_indices: if index in unslashed_attesting_indices:
increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow
reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) if is_in_inactivity_leak(state):
rewards[index] += reward_numerator // (total_balance // increment) # Since full base reward will be canceled out by inactivity penalty deltas,
# optimal participation receives full base reward compensation here.
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: else:
penalties[index] += get_base_reward(state, index) penalties[index] += get_base_reward(state, index)
return rewards, penalties return rewards, penalties
@ -1426,9 +1450,8 @@ def get_inclusion_delay_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequ
a for a in matching_source_attestations a for a in matching_source_attestations
if index in get_attesting_indices(state, a.data, a.aggregation_bits) if index in get_attesting_indices(state, a.data, a.aggregation_bits)
], key=lambda a: a.inclusion_delay) ], key=lambda a: a.inclusion_delay)
proposer_reward = Gwei(get_base_reward(state, index) // PROPOSER_REWARD_QUOTIENT) rewards[attestation.proposer_index] += get_proposer_reward(state, index)
rewards[attestation.proposer_index] += proposer_reward max_attester_reward = get_base_reward(state, index) - get_proposer_reward(state, index)
max_attester_reward = get_base_reward(state, index) - proposer_reward
rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay) rewards[index] += Gwei(max_attester_reward // attestation.inclusion_delay)
# No penalties associated with inclusion delay # No penalties associated with inclusion delay
@ -1442,16 +1465,16 @@ def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], S
Return inactivity reward/penalty deltas for each validator. Return inactivity reward/penalty deltas for each validator.
""" """
penalties = [Gwei(0) for _ in range(len(state.validators))] penalties = [Gwei(0) for _ in range(len(state.validators))]
finality_delay = get_previous_epoch(state) - state.finalized_checkpoint.epoch if is_in_inactivity_leak(state):
if finality_delay > MIN_EPOCHS_TO_INACTIVITY_PENALTY:
matching_target_attestations = get_matching_target_attestations(state, get_previous_epoch(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) matching_target_attesting_indices = get_unslashed_attesting_indices(state, matching_target_attestations)
for index in get_eligible_validator_indices(state): 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: if index not in matching_target_attesting_indices:
effective_balance = state.validators[index].effective_balance 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 # No rewards associated with inactivity penalties
rewards = [Gwei(0) for _ in range(len(state.validators))] rewards = [Gwei(0) for _ in range(len(state.validators))]

View File

@ -1,4 +1,5 @@
from random import Random from random import Random
from lru import LRU
from eth2spec.phase0 import spec as spec_phase0 from eth2spec.phase0 import spec as spec_phase0
from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations
@ -150,7 +151,6 @@ def run_get_inactivity_penalty_deltas(spec, state):
matching_attestations = spec.get_matching_target_attestations(state, spec.get_previous_epoch(state)) matching_attestations = spec.get_matching_target_attestations(state, spec.get_previous_epoch(state))
matching_attesting_indices = spec.get_unslashed_attesting_indices(state, matching_attestations) matching_attesting_indices = spec.get_unslashed_attesting_indices(state, matching_attestations)
finality_delay = spec.get_previous_epoch(state) - state.finalized_checkpoint.epoch
eligible_indices = spec.get_eligible_validator_indices(state) eligible_indices = spec.get_eligible_validator_indices(state)
for index in range(len(state.validators)): for index in range(len(state.validators)):
assert rewards[index] == 0 assert rewards[index] == 0
@ -158,8 +158,9 @@ def run_get_inactivity_penalty_deltas(spec, state):
assert penalties[index] == 0 assert penalties[index] == 0
continue continue
if finality_delay > spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY: if spec.is_in_inactivity_leak(state):
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): if not has_enough_for_reward(spec, state, index):
assert penalties[index] == 0 assert penalties[index] == 0
elif index in matching_attesting_indices: elif index in matching_attesting_indices:
@ -170,6 +171,39 @@ def run_get_inactivity_penalty_deltas(spec, state):
assert penalties[index] == 0 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): def set_some_new_deposits(spec, state, rng):
num_validators = len(state.validators) num_validators = len(state.validators)
# Set ~1/10 to just recently deposited # Set ~1/10 to just recently deposited

View File

@ -14,6 +14,7 @@ from eth2spec.test.helpers.attestations import (
get_valid_attestation, get_valid_attestation,
prepare_state_with_attestations, 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.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 eth2spec.test.phase_0.epoch_processing.run_epoch_process_base import run_epoch_processing_with
from random import Random from random import Random
@ -62,24 +63,6 @@ def test_genesis_epoch_full_attestations_no_rewards(spec, state):
assert state.balances[index] == pre_state.balances[index] assert state.balances[index] == pre_state.balances[index]
@with_all_phases
@spec_state_test
def test_full_attestations(spec, state):
attestations = prepare_state_with_attestations(spec, state)
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)):
if index in attesting_indices:
assert state.balances[index] > pre_state.balances[index]
else:
assert state.balances[index] < pre_state.balances[index]
@with_all_phases @with_all_phases
@spec_state_test @spec_state_test
def test_full_attestations_random_incorrect_fields(spec, state): def test_full_attestations_random_incorrect_fields(spec, state):
@ -173,6 +156,7 @@ def run_with_participation(spec, state, participation_fn):
return att_participants return att_participants
attestations = prepare_state_with_attestations(spec, state, participation_fn=participation_tracker) attestations = prepare_state_with_attestations(spec, state, participation_fn=participation_tracker)
proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations]
pre_state = state.copy() pre_state = state.copy()
@ -182,10 +166,20 @@ def run_with_participation(spec, state, participation_fn):
assert len(attesting_indices) == len(participated) assert len(attesting_indices) == len(participated)
for index in range(len(pre_state.validators)): for index in range(len(pre_state.validators)):
if index in participated: if spec.is_in_inactivity_leak(state):
assert state.balances[index] > pre_state.balances[index] # Proposers can still make money during a leak
if index in proposer_indices and index in participated:
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]
else: else:
assert state.balances[index] < pre_state.balances[index] if index in participated:
assert state.balances[index] > pre_state.balances[index]
else:
assert state.balances[index] < pre_state.balances[index]
@with_all_phases @with_all_phases
@ -195,6 +189,14 @@ def test_almost_empty_attestations(spec, state):
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1)) yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1))
@with_all_phases
@spec_state_test
@leaking()
def test_almost_empty_attestations_with_leak(spec, state):
rng = Random(1234)
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1))
@with_all_phases @with_all_phases
@spec_state_test @spec_state_test
def test_random_fill_attestations(spec, state): def test_random_fill_attestations(spec, state):
@ -202,6 +204,14 @@ def test_random_fill_attestations(spec, state):
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3)) yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3))
@with_all_phases
@spec_state_test
@leaking()
def test_random_fill_attestations_with_leak(spec, state):
rng = Random(4567)
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3))
@with_all_phases @with_all_phases
@spec_state_test @spec_state_test
def test_almost_full_attestations(spec, state): def test_almost_full_attestations(spec, state):
@ -209,12 +219,27 @@ def test_almost_full_attestations(spec, state):
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1)) yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1))
@with_all_phases
@spec_state_test
@leaking()
def test_almost_full_attestations_with_leak(spec, state):
rng = Random(8901)
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1))
@with_all_phases @with_all_phases
@spec_state_test @spec_state_test
def test_full_attestation_participation(spec, state): def test_full_attestation_participation(spec, state):
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm) yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm)
@with_all_phases
@spec_state_test
@leaking()
def test_full_attestation_participation_with_leak(spec, state):
yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm)
@with_all_phases @with_all_phases
@spec_state_test @spec_state_test
def test_duplicate_attestation(spec, state): def test_duplicate_attestation(spec, state):

View File

@ -1,40 +1,6 @@
from eth2spec.test.context import with_all_phases, spec_state_test 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 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 @with_all_phases