Slashing penalty calculation change (#1217)

If the exit queue is very long, then a validator may take many months to exit. With the code as currently written, however, self-slashing is a potentially lucrative route to get one's money out faster, because one can exit in 36 days.

This PR changes it so that slashing can only extend your withdrawal time, not contract it. Also, instead of the slashed balances used to calculate one's slashing penalty being those in `[withdrawal - 54 days ... withdrawal - 18 days]`, we now run the penalization algorithm once every 36 days that a validator is slashed but not withdrawn, so that it covers the 36-day period where the validator was actually slashed.  It also moves the minimum slashing penalty to the `slash_validator` function so that it is only applied once.

We also simplify the `slashed_balances` logic to be per-epoch.
This commit is contained in:
vbuterin 2019-06-28 21:35:26 +08:00 committed by Justin
parent 0ba933e088
commit c764202a57
5 changed files with 92 additions and 46 deletions

View File

@ -76,7 +76,7 @@ MIN_EPOCHS_TO_INACTIVITY_PENALTY: 4
# 2**16 (= 65,536) epochs ~0.8 years # 2**16 (= 65,536) epochs ~0.8 years
EPOCHS_PER_HISTORICAL_VECTOR: 65536 EPOCHS_PER_HISTORICAL_VECTOR: 65536
# 2**13 (= 8,192) epochs ~36 days # 2**13 (= 8,192) epochs ~36 days
EPOCHS_PER_SLASHED_BALANCES_VECTOR: 8192 EPOCHS_PER_SLASHINGS_VECTOR: 8192
# 2**24 (= 16,777,216) historical roots, ~26,131 years # 2**24 (= 16,777,216) historical roots, ~26,131 years
HISTORICAL_ROOTS_LIMIT: 16777216 HISTORICAL_ROOTS_LIMIT: 16777216
# 2**40 (= 1,099,511,627,776) validator spots # 2**40 (= 1,099,511,627,776) validator spots
@ -88,7 +88,7 @@ VALIDATOR_REGISTRY_LIMIT: 1099511627776
# 2**5 (= 32) # 2**5 (= 32)
BASE_REWARD_FACTOR: 32 BASE_REWARD_FACTOR: 32
# 2**9 (= 512) # 2**9 (= 512)
WHISTLEBLOWING_REWARD_QUOTIENT: 512 WHISTLEBLOWER_REWARD_QUOTIENT: 512
# 2**3 (= 8) # 2**3 (= 8)
PROPOSER_REWARD_QUOTIENT: 8 PROPOSER_REWARD_QUOTIENT: 8
# 2**25 (= 33,554,432) # 2**25 (= 33,554,432)

View File

@ -77,7 +77,7 @@ EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS: 4096
# [customized] smaller state # [customized] smaller state
EPOCHS_PER_HISTORICAL_VECTOR: 64 EPOCHS_PER_HISTORICAL_VECTOR: 64
# [customized] smaller state # [customized] smaller state
EPOCHS_PER_SLASHED_BALANCES_VECTOR: 64 EPOCHS_PER_SLASHINGS_VECTOR: 64
# 2**24 (= 16,777,216) historical roots # 2**24 (= 16,777,216) historical roots
HISTORICAL_ROOTS_LIMIT: 16777216 HISTORICAL_ROOTS_LIMIT: 16777216
# 2**40 (= 1,099,511,627,776) validator spots # 2**40 (= 1,099,511,627,776) validator spots
@ -89,7 +89,7 @@ VALIDATOR_REGISTRY_LIMIT: 1099511627776
# 2**5 (= 32) # 2**5 (= 32)
BASE_REWARD_FACTOR: 32 BASE_REWARD_FACTOR: 32
# 2**9 (= 512) # 2**9 (= 512)
WHISTLEBLOWING_REWARD_QUOTIENT: 512 WHISTLEBLOWER_REWARD_QUOTIENT: 512
# 2**3 (= 8) # 2**3 (= 8)
PROPOSER_REWARD_QUOTIENT: 8 PROPOSER_REWARD_QUOTIENT: 8
# 2**25 (= 33,554,432) # 2**25 (= 33,554,432)

View File

@ -234,7 +234,7 @@ The following values are (non-configurable) constants used throughout the specif
| Name | Value | Unit | Duration | | Name | Value | Unit | Duration |
| - | - | :-: | :-: | | - | - | :-: | :-: |
| `EPOCHS_PER_HISTORICAL_VECTOR` | `2**16` (= 65,536) | epochs | ~0.8 years | | `EPOCHS_PER_HISTORICAL_VECTOR` | `2**16` (= 65,536) | epochs | ~0.8 years |
| `EPOCHS_PER_SLASHED_BALANCES_VECTOR` | `2**13` (= 8,192) | epochs | ~36 days | | `EPOCHS_PER_SLASHINGS_VECTOR` | `2**13` (= 8,192) | epochs | ~36 days |
| `HISTORICAL_ROOTS_LIMIT` | `2**24` (= 16,777,216) | historical roots | ~26,131 years | | `HISTORICAL_ROOTS_LIMIT` | `2**24` (= 16,777,216) | historical roots | ~26,131 years |
| `VALIDATOR_REGISTRY_LIMIT` | `2**40` (= 1,099,511,627,776) | validator spots | | | `VALIDATOR_REGISTRY_LIMIT` | `2**40` (= 1,099,511,627,776) | validator spots | |
@ -243,7 +243,7 @@ The following values are (non-configurable) constants used throughout the specif
| Name | Value | | Name | Value |
| - | - | | - | - |
| `BASE_REWARD_FACTOR` | `2**6` (= 64) | | `BASE_REWARD_FACTOR` | `2**6` (= 64) |
| `WHISTLEBLOWING_REWARD_QUOTIENT` | `2**9` (= 512) | | `WHISTLEBLOWER_REWARD_QUOTIENT` | `2**9` (= 512) |
| `PROPOSER_REWARD_QUOTIENT` | `2**3` (= 8) | | `PROPOSER_REWARD_QUOTIENT` | `2**3` (= 8) |
| `INACTIVITY_PENALTY_QUOTIENT` | `2**25` (= 33,554,432) | | `INACTIVITY_PENALTY_QUOTIENT` | `2**25` (= 33,554,432) |
| `MIN_SLASHING_PENALTY_QUOTIENT` | `2**5` (= 32) | | `MIN_SLASHING_PENALTY_QUOTIENT` | `2**5` (= 32) |
@ -520,7 +520,7 @@ class BeaconState(Container):
randao_mixes: Vector[Hash, EPOCHS_PER_HISTORICAL_VECTOR] randao_mixes: Vector[Hash, EPOCHS_PER_HISTORICAL_VECTOR]
active_index_roots: Vector[Hash, EPOCHS_PER_HISTORICAL_VECTOR] # Active registry digests for light clients active_index_roots: Vector[Hash, EPOCHS_PER_HISTORICAL_VECTOR] # Active registry digests for light clients
# Slashings # Slashings
slashed_balances: Vector[Gwei, EPOCHS_PER_SLASHED_BALANCES_VECTOR] # Sums of slashed effective balances slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances
# Attestations # Attestations
previous_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] previous_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH]
current_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] current_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH]
@ -1097,21 +1097,22 @@ def slash_validator(state: BeaconState,
""" """
Slash the validator with index ``slashed_index``. Slash the validator with index ``slashed_index``.
""" """
current_epoch = get_current_epoch(state) epoch = get_current_epoch(state)
initiate_validator_exit(state, slashed_index) initiate_validator_exit(state, slashed_index)
state.validators[slashed_index].slashed = True validator = state.validators[slashed_index]
state.validators[slashed_index].withdrawable_epoch = Epoch(current_epoch + EPOCHS_PER_SLASHED_BALANCES_VECTOR) validator.slashed = True
slashed_balance = state.validators[slashed_index].effective_balance validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR))
state.slashed_balances[current_epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR] += slashed_balance state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance
decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT)
# Apply proposer and whistleblower rewards
proposer_index = get_beacon_proposer_index(state) proposer_index = get_beacon_proposer_index(state)
if whistleblower_index is None: if whistleblower_index is None:
whistleblower_index = proposer_index whistleblower_index = proposer_index
whistleblowing_reward = Gwei(slashed_balance // WHISTLEBLOWING_REWARD_QUOTIENT) whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT)
proposer_reward = Gwei(whistleblowing_reward // PROPOSER_REWARD_QUOTIENT) proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT)
increase_balance(state, proposer_index, proposer_reward) increase_balance(state, proposer_index, proposer_reward)
increase_balance(state, whistleblower_index, whistleblowing_reward - proposer_reward) increase_balance(state, whistleblower_index, whistleblower_reward - proposer_reward)
decrease_balance(state, slashed_index, whistleblowing_reward)
``` ```
## Genesis ## Genesis
@ -1493,18 +1494,9 @@ def process_registry_updates(state: BeaconState) -> None:
def process_slashings(state: BeaconState) -> None: def process_slashings(state: BeaconState) -> None:
epoch = get_current_epoch(state) epoch = get_current_epoch(state)
total_balance = get_total_active_balance(state) total_balance = get_total_active_balance(state)
# Compute slashed balances in the current epoch
total_at_start = state.slashed_balances[(epoch + 1) % EPOCHS_PER_SLASHED_BALANCES_VECTOR]
total_at_end = state.slashed_balances[epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR]
total_penalties = total_at_end - total_at_start
for index, validator in enumerate(state.validators): for index, validator in enumerate(state.validators):
if validator.slashed and epoch + EPOCHS_PER_SLASHED_BALANCES_VECTOR // 2 == validator.withdrawable_epoch: if validator.slashed and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch:
penalty = max( penalty = validator.effective_balance * min(sum(state.slashings) * 3, total_balance) // total_balance
validator.effective_balance * min(total_penalties * 3, total_balance) // total_balance,
validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT
)
decrease_balance(state, ValidatorIndex(index), penalty) decrease_balance(state, ValidatorIndex(index), penalty)
``` ```
@ -1532,10 +1524,8 @@ def process_final_updates(state: BeaconState) -> None:
get_active_validator_indices(state, Epoch(next_epoch + ACTIVATION_EXIT_DELAY)) get_active_validator_indices(state, Epoch(next_epoch + ACTIVATION_EXIT_DELAY))
) )
) )
# Set total slashed balances # Reset slashings
state.slashed_balances[next_epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR] = ( state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0)
state.slashed_balances[current_epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR]
)
# Set randao mix # Set randao mix
state.randao_mixes[next_epoch % EPOCHS_PER_HISTORICAL_VECTOR] = get_randao_mix(state, current_epoch) state.randao_mixes[next_epoch % EPOCHS_PER_HISTORICAL_VECTOR] = get_randao_mix(state, current_epoch)
# Set historical root accumulator # Set historical root accumulator

View File

@ -453,7 +453,7 @@ def process_early_derived_secret_reveal(state: BeaconState,
# Apply penalty # Apply penalty
proposer_index = get_beacon_proposer_index(state) proposer_index = get_beacon_proposer_index(state)
whistleblower_index = reveal.masker_index whistleblower_index = reveal.masker_index
whistleblowing_reward = Gwei(penalty // WHISTLEBLOWING_REWARD_QUOTIENT) whistleblowing_reward = Gwei(penalty // WHISTLEBLOWER_REWARD_QUOTIENT)
proposer_reward = Gwei(whistleblowing_reward // PROPOSER_REWARD_QUOTIENT) proposer_reward = Gwei(whistleblowing_reward // PROPOSER_REWARD_QUOTIENT)
increase_balance(state, proposer_index, proposer_reward) increase_balance(state, proposer_index, proposer_reward)
increase_balance(state, whistleblower_index, whistleblowing_reward - proposer_reward) increase_balance(state, whistleblower_index, whistleblowing_reward - proposer_reward)

View File

@ -25,31 +25,56 @@ def run_attester_slashing_processing(spec, state, attester_slashing, valid=True)
yield 'post', None yield 'post', None
return return
slashed_index = attester_slashing.attestation_1.custody_bit_0_indices[0] slashed_indices = (
pre_slashed_balance = get_balance(state, slashed_index) attester_slashing.attestation_1.custody_bit_0_indices
+ attester_slashing.attestation_1.custody_bit_1_indices
)
proposer_index = spec.get_beacon_proposer_index(state) proposer_index = spec.get_beacon_proposer_index(state)
pre_proposer_balance = get_balance(state, proposer_index) pre_proposer_balance = get_balance(state, proposer_index)
pre_slashings = {slashed_index: get_balance(state, slashed_index) for slashed_index in slashed_indices}
pre_withdrawalable_epochs = {
slashed_index: state.validators[slashed_index].withdrawable_epoch
for slashed_index in slashed_indices
}
total_proposer_rewards = sum(
balance // spec.WHISTLEBLOWER_REWARD_QUOTIENT
for balance in pre_slashings.values()
)
# Process slashing # Process slashing
spec.process_attester_slashing(state, attester_slashing) spec.process_attester_slashing(state, attester_slashing)
slashed_validator = state.validators[slashed_index] for slashed_index in slashed_indices:
pre_withdrawalable_epoch = pre_withdrawalable_epochs[slashed_index]
slashed_validator = state.validators[slashed_index]
# Check slashing # Check slashing
assert slashed_validator.slashed assert slashed_validator.slashed
assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH if pre_withdrawalable_epoch < spec.FAR_FUTURE_EPOCH:
expected_withdrawable_epoch = max(
pre_withdrawalable_epoch,
spec.get_current_epoch(state) + spec.EPOCHS_PER_SLASHINGS_VECTOR
)
assert slashed_validator.withdrawable_epoch == expected_withdrawable_epoch
else:
assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
assert get_balance(state, slashed_index) < pre_slashings[slashed_index]
if slashed_index != proposer_index: if proposer_index not in slashed_indices:
# lost whistleblower reward
assert get_balance(state, slashed_index) < pre_slashed_balance
# gained whistleblower reward # gained whistleblower reward
assert get_balance(state, proposer_index) > pre_proposer_balance assert get_balance(state, proposer_index) == pre_proposer_balance + total_proposer_rewards
else: else:
# gained rewards for all slashings, which may include others. And only lost that of themselves. # gained rewards for all slashings, which may include others. And only lost that of themselves.
# Netto at least 0, if more people where slashed, a balance increase. expected_balance = (
assert get_balance(state, slashed_index) >= pre_slashed_balance pre_proposer_balance
+ total_proposer_rewards
- pre_slashings[proposer_index] // spec.MIN_SLASHING_PENALTY_QUOTIENT
)
assert get_balance(state, proposer_index) == expected_balance
yield 'post', state yield 'post', state
@ -82,6 +107,37 @@ def test_success_surround(spec, state):
yield from run_attester_slashing_processing(spec, state, attester_slashing) yield from run_attester_slashing_processing(spec, state, attester_slashing)
@with_all_phases
@always_bls
@spec_state_test
def test_success_already_exited_recent(spec, state):
attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True)
slashed_indices = (
attester_slashing.attestation_1.custody_bit_0_indices
+ attester_slashing.attestation_1.custody_bit_1_indices
)
for index in slashed_indices:
spec.initiate_validator_exit(state, index)
yield from run_attester_slashing_processing(spec, state, attester_slashing)
@with_all_phases
@always_bls
@spec_state_test
def test_success_already_exited_long_ago(spec, state):
attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True)
slashed_indices = (
attester_slashing.attestation_1.custody_bit_0_indices
+ attester_slashing.attestation_1.custody_bit_1_indices
)
for index in slashed_indices:
spec.initiate_validator_exit(state, index)
state.validators[index].withdrawable_epoch = spec.get_current_epoch(state) + 2
yield from run_attester_slashing_processing(spec, state, attester_slashing)
@with_all_phases @with_all_phases
@always_bls @always_bls
@spec_state_test @spec_state_test