Merge pull request #2236 from ethereum/justin_altair_cleanups

Altair cosmetic cleanups plus a couple substantive changes
This commit is contained in:
Danny Ryan 2021-03-15 08:57:35 -06:00 committed by GitHub
commit b8bf7f0f80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 91 additions and 114 deletions

View File

@ -4,7 +4,7 @@ CONFIG_NAME: "mainnet"
# Updated penalty values
# ---------------------------------------------------------------
# 3 * 2**24) (= 50,331,648)
# 3 * 2**24 (= 50,331,648)
ALTAIR_INACTIVITY_PENALTY_QUOTIENT: 50331648
# 2**6 (= 64)
ALTAIR_MIN_SLASHING_PENALTY_QUOTIENT: 64
@ -14,12 +14,12 @@ ALTAIR_PROPORTIONAL_SLASHING_MULTIPLIER: 2
# Misc
# ---------------------------------------------------------------
# 2**10 (=1,024)
# 2**10 (= 1,024)
SYNC_COMMITTEE_SIZE: 1024
# 2**6 (=64)
# 2**6 (= 64)
SYNC_SUBCOMMITTEE_SIZE: 64
# 2**2 (=4)
LEAK_SCORE_BIAS: 4
# 2**2 (= 4)
INACTIVITY_SCORE_BIAS: 4
# Time parameters

View File

@ -4,7 +4,7 @@ CONFIG_NAME: "minimal"
# Updated penalty values
# ---------------------------------------------------------------
# 3 * 2**24) (= 50,331,648)
# 3 * 2**24 (= 50,331,648)
ALTAIR_INACTIVITY_PENALTY_QUOTIENT: 50331648
# 2**6 (= 64)
ALTAIR_MIN_SLASHING_PENALTY_QUOTIENT: 64
@ -18,8 +18,8 @@ ALTAIR_PROPORTIONAL_SLASHING_MULTIPLIER: 2
SYNC_COMMITTEE_SIZE: 32
# [customized]
SYNC_SUBCOMMITTEE_SIZE: 16
# 2**2 (=4)
LEAK_SCORE_BIAS: 4
# 2**2 (= 4)
INACTIVITY_SCORE_BIAS: 4
# Time parameters

View File

@ -35,17 +35,17 @@
- [`get_sync_committee`](#get_sync_committee)
- [`get_base_reward`](#get_base_reward)
- [`get_unslashed_participating_indices`](#get_unslashed_participating_indices)
- [`get_flag_deltas`](#get_flag_deltas)
- [New `get_inactivity_penalty_deltas`](#new-get_inactivity_penalty_deltas)
- [`get_flag_index_deltas`](#get_flag_index_deltas)
- [Modified `get_inactivity_penalty_deltas`](#modified-get_inactivity_penalty_deltas)
- [Beacon state mutators](#beacon-state-mutators)
- [New `slash_validator`](#new-slash_validator)
- [Modified `slash_validator`](#modified-slash_validator)
- [Block processing](#block-processing)
- [Modified `process_attestation`](#modified-process_attestation)
- [Modified `process_deposit`](#modified-process_deposit)
- [Sync committee processing](#sync-committee-processing)
- [Epoch processing](#epoch-processing)
- [Justification and finalization](#justification-and-finalization)
- [Leak scores](#leak-scores)
- [Inactivity scores](#inactivity-scores)
- [Rewards and penalties](#rewards-and-penalties)
- [Slashings](#slashings)
- [Participation flags updates](#participation-flags-updates)
@ -56,20 +56,17 @@
## Introduction
Altair is a patch implementing the first hard fork to the beacon chain.
It has four main features:
Altair is the first beacon chain hard fork. Its main features are:
* Light client support via sync committees
* Incentive accounting reforms, reducing spec complexity
and [TODO] reducing the cost of processing chains that have very little or zero participation for a long span of epochs
* Update penalty configuration values, moving them toward their planned maximally punitive configuration
* Fork choice rule changes to address weaknesses recently discovered in the existing fork choice
* sync committees to support light clients
* incentive accounting reforms to reduce spec complexity
* penalty parameter updates to move them to their planned maximally punitive configuration
## Custom types
| Name | SSZ equivalent | Description |
| - | - | - |
| `ParticipationFlags` | `uint8` | A succinct representation of 8 boolean participation flags |
| `ParticipationFlags` | `uint8` | a succinct representation of 8 boolean participation flags |
## Constants
@ -90,8 +87,7 @@ It has four main features:
| `TIMELY_TARGET_FLAG_NUMERATOR` | `32` |
| `FLAG_DENOMINATOR` | `64` |
**Note**: The participatition flag fractions add up to 7/8.
The remaining 1/8 is for proposer incentives and other future micro-incentives.
**Note**: The sum of the participatition flag fractions (7/8) plus the proposer reward fraction (1/8) equals 1.
### Misc
@ -103,14 +99,14 @@ The remaining 1/8 is for proposer incentives and other future micro-incentives.
### Updated penalty values
This patch updates a few configuration values to move penalty constants toward their final, maxmium security values.
This patch updates a few configuration values to move penalty parameters toward their final, maxmium security values.
*Note*: The spec does *not* override previous configuration values but instead creates new values and replaces usage throughout.
| Name | Value |
| - | - |
| `ALTAIR_INACTIVITY_PENALTY_QUOTIENT` | `uint64(3 * 2**24)` (= 50,331,648) |
| `ALTAIR_MIN_SLASHING_PENALTY_QUOTIENT` | `uint64(2**6)` (=64) |
| `ALTAIR_MIN_SLASHING_PENALTY_QUOTIENT` | `uint64(2**6)` (= 64) |
| `ALTAIR_PROPORTIONAL_SLASHING_MULTIPLIER` | `uint64(2)` |
### Misc
@ -119,7 +115,7 @@ This patch updates a few configuration values to move penalty constants toward t
| - | - |
| `SYNC_COMMITTEE_SIZE` | `uint64(2**10)` (= 1,024) |
| `SYNC_SUBCOMMITTEE_SIZE` | `uint64(2**6)` (= 64) |
| `LEAK_SCORE_BIAS` | 4 |
| `INACTIVITY_SCORE_BIAS` | `uint64(4)` |
### Time parameters
@ -181,18 +177,18 @@ class BeaconState(Container):
# Slashings
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances
# Participation
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [New in Altair]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [New in Altair]
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [Modified in Altair]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [Modified in Altair]
# Finality
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
# Light client sync committees
# Inactivity
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] # [New in Altair]
# Sync
current_sync_committee: SyncCommittee # [New in Altair]
next_sync_committee: SyncCommittee # [New in Altair]
# Leak
leak_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] # [New in Altair]
```
### New containers
@ -307,7 +303,7 @@ def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei:
```python
def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epoch: Epoch) -> Set[ValidatorIndex]:
"""
Retrieve the active and unslashed validator indices for the given epoch and flag index.
Return the active and unslashed validator indices for the given epoch and flag index.
"""
assert epoch in (get_previous_epoch(state), get_current_epoch(state))
if epoch == get_current_epoch(state):
@ -319,19 +315,17 @@ def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epo
return set(filter(lambda index: not state.validators[index].slashed, participating_indices))
```
#### `get_flag_deltas`
#### `get_flag_index_deltas`
```python
def get_flag_deltas(state: BeaconState,
flag_index: int,
numerator: uint64) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
def get_flag_index_deltas(state: BeaconState,
flag_index: int,
numerator: uint64) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
Compute the rewards and penalties associated with a particular duty, by scanning through the participation
flags to determine who participated and who did not and assigning them the appropriate rewards and penalties.
Return the deltas for a given flag index by scanning through the participation flags.
"""
rewards = [Gwei(0)] * len(state.validators)
penalties = [Gwei(0)] * len(state.validators)
unslashed_participating_indices = get_unslashed_participating_indices(state, flag_index, get_previous_epoch(state))
increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balances to avoid uint64 overflow
unslashed_participating_increments = get_total_balance(state, unslashed_participating_indices) // increment
@ -340,19 +334,17 @@ def get_flag_deltas(state: BeaconState,
base_reward = get_base_reward(state, index)
if index in unslashed_participating_indices:
if is_in_inactivity_leak(state):
# Optimal participation is fully rewarded to cancel the inactivity penalty
rewards[index] = base_reward * numerator // FLAG_DENOMINATOR
# This flag reward cancels the inactivity penalty corresponding to the flag index
rewards[index] += Gwei(base_reward * numerator // FLAG_DENOMINATOR)
else:
rewards[index] = (
(base_reward * numerator * unslashed_participating_increments)
// (active_increments * FLAG_DENOMINATOR)
)
reward_numerator = base_reward * numerator * unslashed_participating_increments
rewards[index] += Gwei(reward_numerator // (active_increments * FLAG_DENOMINATOR))
else:
penalties[index] = base_reward * numerator // FLAG_DENOMINATOR
penalties[index] += Gwei(base_reward * numerator // FLAG_DENOMINATOR)
return rewards, penalties
```
#### New `get_inactivity_penalty_deltas`
#### Modified `get_inactivity_penalty_deltas`
*Note*: The function `get_inactivity_penalty_deltas` is modified in the selection of matching target indices
and the removal of `BASE_REWARDS_PER_EPOCH`.
@ -360,39 +352,29 @@ and the removal of `BASE_REWARDS_PER_EPOCH`.
```python
def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
Compute the penalties associated with the inactivity leak, by scanning through the participation
flags to determine who participated and who did not, applying the leak penalty globally and applying
compensatory rewards to participants.
Return the inactivity penalty deltas by considering timely target participation flags and inactivity scores.
"""
rewards = [Gwei(0) for _ in range(len(state.validators))]
penalties = [Gwei(0) for _ in range(len(state.validators))]
if not is_in_inactivity_leak(state):
return rewards, penalties
reward_numerator_sum = sum(numerator for (_, numerator) in get_flag_indices_and_numerators())
matching_target_attesting_indices = get_unslashed_participating_indices(
state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state)
)
for index in get_eligible_validator_indices(state):
# If validator is performing optimally this cancels all attestation rewards for a neutral balance
penalties[index] += Gwei(get_base_reward(state, index) * reward_numerator_sum // FLAG_DENOMINATOR)
if index not in matching_target_attesting_indices and state.leak_scores[index] >= LEAK_SCORE_BIAS:
effective_balance = state.validators[index].effective_balance
leak_penalty = Gwei(
effective_balance * state.leak_scores[index] // LEAK_SCORE_BIAS // ALTAIR_INACTIVITY_PENALTY_QUOTIENT
)
penalties[index] += leak_penalty
if is_in_inactivity_leak(state):
previous_epoch = get_previous_epoch(state)
matching_target_indices = get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, previous_epoch)
for index in get_eligible_validator_indices(state):
for (_, numerator) in get_flag_indices_and_numerators():
# This inactivity penalty cancels the flag reward corresponding to the flag index
penalties[index] += Gwei(get_base_reward(state, index) * numerator // FLAG_DENOMINATOR)
if index not in matching_target_indices:
penalty_numerator = state.validators[index].effective_balance * state.inactivity_scores[index]
penalty_denominator = INACTIVITY_SCORE_BIAS * ALTAIR_INACTIVITY_PENALTY_QUOTIENT
penalties[index] += Gwei(penalty_numerator // penalty_denominator)
return rewards, penalties
```
### Beacon state mutators
#### New `slash_validator`
#### Modified `slash_validator`
*Note*: The function `slash_validator` is modified
with the substitution of `MIN_SLASHING_PENALTY_QUOTIENT` with `ALTAIR_MIN_SLASHING_PENALTY_QUOTIENT`.
*Note*: The function `slash_validator` is modified to use `ALTAIR_MIN_SLASHING_PENALTY_QUOTIENT`.
```python
def slash_validator(state: BeaconState,
@ -483,10 +465,9 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None:
increase_balance(state, get_beacon_proposer_index(state), proposer_reward)
```
#### Modified `process_deposit`
*Note*: The function `process_deposit` is modified to initialize `leak_scores`, `previous_epoch_participation`, `current_epoch_participation`.
*Note*: The function `process_deposit` is modified to initialize `inactivity_scores`, `previous_epoch_participation`, `current_epoch_participation`.
```python
def process_deposit(state: BeaconState, deposit: Deposit) -> None:
@ -504,7 +485,7 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None:
pubkey = deposit.data.pubkey
amount = deposit.data.amount
validator_pubkeys = [v.pubkey for v in state.validators]
validator_pubkeys = [validator.pubkey for validator in state.validators]
if pubkey not in validator_pubkeys:
# Verify the deposit signature (proof of possession) which is not checked by the deposit contract
deposit_message = DepositMessage(
@ -514,15 +495,13 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None:
)
domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks
signing_root = compute_signing_root(deposit_message, domain)
if not bls.Verify(pubkey, signing_root, deposit.data.signature):
return
# Add validator and balance entries
state.validators.append(get_validator_from_deposit(state, deposit))
state.balances.append(amount)
state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000)) # [New in Altair]
state.current_epoch_participation.append(ParticipationFlags(0b0000_0000)) # [New in Altair]
state.leak_scores.append(0) # [New in Altair]
# Initialize validator if the deposit signature is valid
if bls.Verify(pubkey, signing_root, deposit.data.signature):
state.validators.append(get_validator_from_deposit(state, deposit))
state.balances.append(amount)
state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000))
state.current_epoch_participation.append(ParticipationFlags(0b0000_0000))
state.inactivity_scores.append(0)
else:
# Increase balance by deposit amount
index = ValidatorIndex(validator_pubkeys.index(pubkey))
@ -563,7 +542,7 @@ def process_sync_committee(state: BeaconState, body: BeaconBlockBody) -> None:
```python
def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state) # [Modified in Altair]
process_leak_updates(state) # [New in Altair]
process_inactivity_updates(state) # [New in Altair]
process_rewards_and_penalties(state) # [Modified in Altair]
process_registry_updates(state)
process_slashings(state) # [Modified in Altair]
@ -622,36 +601,32 @@ def process_justification_and_finalization(state: BeaconState) -> None:
state.finalized_checkpoint = old_current_justified_checkpoint
```
#### Leak scores
#### Inactivity scores
*Note*: The function `process_leak_updates` is new.
*Note*: The function `process_inactivity_updates` is new.
```python
def process_leak_updates(state: BeaconState) -> None:
matching_target_attesting_indices = get_unslashed_participating_indices(
state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state)
)
def process_inactivity_updates(state: BeaconState) -> None:
for index in get_eligible_validator_indices(state):
if index in matching_target_attesting_indices:
if state.leak_scores[index] > 0:
state.leak_scores[index] -= 1
if index in get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state)):
if state.inactivity_scores[index] > 0:
state.inactivity_scores[index] -= 1
elif is_in_inactivity_leak(state):
state.leak_scores[index] += LEAK_SCORE_BIAS
state.inactivity_scores[index] += INACTIVITY_SCORE_BIAS
```
#### Rewards and penalties
*Note*: The function `process_rewards_and_penalties` is modified to support the incentive reforms.
*Note*: The function `process_rewards_and_penalties` is modified to support the incentive accounting reforms.
```python
def process_rewards_and_penalties(state: BeaconState) -> None:
# No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch
if get_current_epoch(state) == GENESIS_EPOCH:
return
flag_deltas = [
get_flag_deltas(state, flag_index, flag_numerator)
for (flag_index, flag_numerator) in get_flag_indices_and_numerators()
]
flag_indices_and_numerators = get_flag_indices_and_numerators()
flag_deltas = [get_flag_index_deltas(state, index, numerator) for (index, numerator) in flag_indices_and_numerators]
deltas = flag_deltas + [get_inactivity_penalty_deltas(state)]
for (rewards, penalties) in deltas:
for index in range(len(state.validators)):

View File

@ -75,8 +75,8 @@ def upgrade_to_altair(pre: phase0.BeaconState) -> BeaconState:
previous_justified_checkpoint=pre.previous_justified_checkpoint,
current_justified_checkpoint=pre.current_justified_checkpoint,
finalized_checkpoint=pre.finalized_checkpoint,
# Leak
leak_scores=[0 for _ in range(len(pre.validators))],
# Inactivity
inactivity_scores=[0 for _ in range(len(pre.validators))],
)
# Fill in sync committees
post.current_sync_committee = get_sync_committee(post, get_current_epoch(post))

View File

@ -257,7 +257,7 @@ The following values are (non-configurable) constants used throughout the specif
| `WHISTLEBLOWER_REWARD_QUOTIENT` | `uint64(2**9)` (= 512) |
| `PROPOSER_REWARD_QUOTIENT` | `uint64(2**3)` (= 8) |
| `INACTIVITY_PENALTY_QUOTIENT` | `uint64(2**26)` (= 67,108,864) |
| `MIN_SLASHING_PENALTY_QUOTIENT` | `uint64(2**7)` (=128) |
| `MIN_SLASHING_PENALTY_QUOTIENT` | `uint64(2**7)` (= 128) |
| `PROPORTIONAL_SLASHING_MULTIPLIER` | `uint64(1)` |
- The `INACTIVITY_PENALTY_QUOTIENT` equals `INVERSE_SQRT_E_DROP_TIME**2` where `INVERSE_SQRT_E_DROP_TIME := 2**13` epochs (about 36 days) is the time it takes the inactivity penalty to reduce the balance of non-participating validators to about `1/sqrt(e) ~= 60.6%`. Indeed, the balance retained by offline validators after `n` epochs is about `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(n**2/2)`; so after `INVERSE_SQRT_E_DROP_TIME` epochs, it is roughly `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(INACTIVITY_PENALTY_QUOTIENT/2) ~= 1/sqrt(e)`. Note this value will be upgraded to `2**24` after Phase 0 mainnet stabilizes to provide a faster recovery in the event of an inactivity leak.

View File

@ -647,5 +647,5 @@ A validator client should be considered standalone and should consider the beaco
1) Private keys -- private keys should be protected from being exported accidentally or by an attacker.
2) Slashing -- before a validator client signs a message it should validate the data, check it against a local slashing database (do not sign a slashable attestation or block) and update its internal slashing database with the newly signed object.
3) Recovered validator -- Recovering a validator from a private key will result in an empty local slashing db. Best practice is to import (from a trusted source) that validator's attestation history. See [EIP 3076](https://github.com/ethereum/EIPs/pull/3076/files) for a standard slashing interchange format.
4) Far future signing requests -- A validator client can be requested to sign a far into the future attestation, resulting in a valid non-slashable request. If the validator client signs this message, it will result in it blocking itself from attesting any other attestation until the beacon-chain reaches that far into the future epoch. This will result in an inactivity leak and potential ejection due to low balance.
4) Far future signing requests -- A validator client can be requested to sign a far into the future attestation, resulting in a valid non-slashable request. If the validator client signs this message, it will result in it blocking itself from attesting any other attestation until the beacon-chain reaches that far into the future epoch. This will result in an inactivity penalty and potential ejection due to low balance.
A validator client should prevent itself from signing such requests by: a) keeping a local time clock if possible and following best practices to stop time server attacks and b) refusing to sign, by default, any message that has a large (>6h) gap from the current slashing protection database indicated a time "jump" or a long offline event. The administrator can manually override this protection to restart the validator after a genuine long offline event.

View File

@ -79,12 +79,12 @@ def test_empty_sync_committee_committee_genesis(spec, state):
@with_all_phases_except([PHASE0, PHASE1])
@spec_state_test
def test_leak_scores(spec, state):
def test_inactivity_scores(spec, state):
for _ in range(spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2):
next_epoch_via_block(spec, state)
assert spec.is_in_inactivity_leak(state)
previous_leak_scores = state.leak_scores.copy()
previous_inactivity_scores = state.inactivity_scores.copy()
yield 'pre', state
@ -95,5 +95,5 @@ def test_leak_scores(spec, state):
yield 'blocks', [signed_block]
yield 'post', state
for pre, post in zip(previous_leak_scores, state.leak_scores):
assert post == pre + spec.LEAK_SCORE_BIAS
for pre, post in zip(previous_inactivity_scores, state.inactivity_scores):
assert post == pre + spec.INACTIVITY_SCORE_BIAS

View File

@ -42,13 +42,13 @@ def run_deltas(spec, state):
if is_post_altair(spec):
def get_source_deltas(state):
return spec.get_flag_deltas(state, spec.TIMELY_SOURCE_FLAG_INDEX, spec.TIMELY_SOURCE_FLAG_NUMERATOR)
return spec.get_flag_index_deltas(state, spec.TIMELY_SOURCE_FLAG_INDEX, spec.TIMELY_SOURCE_FLAG_NUMERATOR)
def get_head_deltas(state):
return spec.get_flag_deltas(state, spec.TIMELY_HEAD_FLAG_INDEX, spec.TIMELY_HEAD_FLAG_NUMERATOR)
return spec.get_flag_index_deltas(state, spec.TIMELY_HEAD_FLAG_INDEX, spec.TIMELY_HEAD_FLAG_NUMERATOR)
def get_target_deltas(state):
return spec.get_flag_deltas(state, spec.TIMELY_TARGET_FLAG_INDEX, spec.TIMELY_TARGET_FLAG_NUMERATOR)
return spec.get_flag_index_deltas(state, spec.TIMELY_TARGET_FLAG_INDEX, spec.TIMELY_TARGET_FLAG_NUMERATOR)
yield from run_attestation_component_deltas(
spec,
@ -191,7 +191,6 @@ def run_get_inactivity_penalty_deltas(spec, state):
matching_attesting_indices = spec.get_unslashed_participating_indices(
state, spec.TIMELY_TARGET_FLAG_INDEX, spec.get_previous_epoch(state)
)
reward_numerator_sum = sum(numerator for (_, numerator) in spec.get_flag_indices_and_numerators())
eligible_indices = spec.get_eligible_validator_indices(state)
for index in range(len(state.validators)):
@ -202,12 +201,15 @@ def run_get_inactivity_penalty_deltas(spec, state):
if spec.is_in_inactivity_leak(state):
# Compute base_penalty
base_reward = spec.get_base_reward(state, index)
if not is_post_altair(spec):
cancel_base_rewards_per_epoch = spec.BASE_REWARDS_PER_EPOCH
base_reward = spec.get_base_reward(state, index)
base_penalty = cancel_base_rewards_per_epoch * base_reward - spec.get_proposer_reward(state, index)
else:
base_penalty = spec.get_base_reward(state, index) * reward_numerator_sum // spec.FLAG_DENOMINATOR
base_penalty = sum(
base_reward * numerator // spec.FLAG_DENOMINATOR
for (_, numerator) in spec.get_flag_indices_and_numerators()
)
if not has_enough_for_reward(spec, state, index):
assert penalties[index] == 0
@ -221,7 +223,7 @@ def run_get_inactivity_penalty_deltas(spec, state):
def transition_state_to_leak(spec, state, epochs=None):
if epochs is None:
# +1 to trigger leak_score transitions
# +1 to trigger inactivity_score transitions
epochs = spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY + 1
assert epochs >= spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY