Skip to content

Commit

Permalink
allow individual calculation of validator balances across epoch bound…
Browse files Browse the repository at this point in the history
…aries (#6416)
  • Loading branch information
tersec authored Jul 6, 2024
1 parent 3f051e9 commit 3db571d
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 13 deletions.
2 changes: 1 addition & 1 deletion beacon_chain/networking/network_metadata.nim
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ proc getMetadataForNetwork*(networkName: string): Eth2NetworkMetadata =
quit 1

if networkName in ["goerli", "prater"]:
warn "Goerli is deprecated and will stop being supported; https://blog.ethereum.org/2023/11/30/goerli-lts-update suggests migrating to Holesky or Sepolia"
warn "Goerli is deprecated and unsupported; https://blog.ethereum.org/2023/11/30/goerli-lts-update suggests migrating to Holesky or Sepolia"

let metadata =
when const_preset == "gnosis":
Expand Down
175 changes: 167 additions & 8 deletions beacon_chain/spec/state_transition_epoch.nim
Original file line number Diff line number Diff line change
Expand Up @@ -707,13 +707,11 @@ template get_flag_and_inactivity_delta(
state: altair.BeaconState | bellatrix.BeaconState | capella.BeaconState |
deneb.BeaconState | electra.BeaconState,
base_reward_per_increment: Gwei, finality_delay: uint64,
previous_epoch: Epoch,
active_increments: uint64,
previous_epoch: Epoch, active_increments: uint64,
penalty_denominator: uint64,
epoch_participation: ptr EpochParticipationFlags,
participating_increments: array[3, uint64],
info: var altair.EpochInfo,
vidx: ValidatorIndex
participating_increments: array[3, uint64], info: var altair.EpochInfo,
vidx: ValidatorIndex, inactivity_score: uint64
): (ValidatorIndex, Gwei, Gwei, Gwei, Gwei, Gwei, Gwei) =
let
base_reward = get_base_reward_increment(state, vidx, base_reward_per_increment)
Expand Down Expand Up @@ -751,7 +749,7 @@ template get_flag_and_inactivity_delta(
0.Gwei
else:
let penalty_numerator =
state.validators[vidx].effective_balance * state.inactivity_scores[vidx]
state.validators[vidx].effective_balance * inactivity_score
penalty_numerator div penalty_denominator

(vidx, reward(TIMELY_SOURCE_FLAG_INDEX),
Expand Down Expand Up @@ -804,7 +802,46 @@ iterator get_flag_and_inactivity_deltas*(
yield get_flag_and_inactivity_delta(
state, base_reward_per_increment, finality_delay, previous_epoch,
active_increments, penalty_denominator, epoch_participation,
participating_increments, info, vidx)
participating_increments, info, vidx, state.inactivity_scores[vidx])

func get_flag_and_inactivity_delta_for_validator(
cfg: RuntimeConfig,
state: deneb.BeaconState | electra.BeaconState,
base_reward_per_increment: Gwei, info: var altair.EpochInfo,
finality_delay: uint64, vidx: ValidatorIndex, inactivity_score: Gwei):
Opt[(ValidatorIndex, Gwei, Gwei, Gwei, Gwei, Gwei, Gwei)] =
## Return the deltas for a given ``flag_index`` by scanning through the
## participation flags.
const INACTIVITY_PENALTY_QUOTIENT =
when state is altair.BeaconState:
INACTIVITY_PENALTY_QUOTIENT_ALTAIR
else:
INACTIVITY_PENALTY_QUOTIENT_BELLATRIX

static: doAssert ord(high(TimelyFlag)) == 2

let
previous_epoch = get_previous_epoch(state)
active_increments = get_active_increments(info)
penalty_denominator =
cfg.INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT
epoch_participation =
if previous_epoch == get_current_epoch(state):
unsafeAddr state.current_epoch_participation
else:
unsafeAddr state.previous_epoch_participation
participating_increments = [
get_unslashed_participating_increment(info, TIMELY_SOURCE_FLAG_INDEX),
get_unslashed_participating_increment(info, TIMELY_TARGET_FLAG_INDEX),
get_unslashed_participating_increment(info, TIMELY_HEAD_FLAG_INDEX)]

if not is_eligible_validator(info.validators[vidx]):
return Opt.none((ValidatorIndex, Gwei, Gwei, Gwei, Gwei, Gwei, Gwei))

Opt.some get_flag_and_inactivity_delta(
state, base_reward_per_increment, finality_delay, previous_epoch,
active_increments, penalty_denominator, epoch_participation,
participating_increments, info, vidx, inactivity_score.uint64)

# https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/phase0/beacon-chain.md#rewards-and-penalties-1
func process_rewards_and_penalties*(
Expand Down Expand Up @@ -1000,6 +1037,22 @@ func get_slashing_penalty*(validator: Validator,
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/phase0/beacon-chain.md#slashings
# https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/altair/beacon-chain.md#slashings
# https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.3/specs/bellatrix/beacon-chain.md#slashings
func get_slashing(
state: ForkyBeaconState, total_balance: Gwei, vidx: ValidatorIndex): Gwei =
# For efficiency reasons, it doesn't make sense to have process_slashings use
# this per-validator index version, but keep them parallel otherwise.
let
epoch = get_current_epoch(state)
adjusted_total_slashing_balance = get_adjusted_total_slashing_balance(
state, total_balance)

let validator = unsafeAddr state.validators.item(vidx)
if slashing_penalty_applies(validator[], epoch):
get_slashing_penalty(
validator[], adjusted_total_slashing_balance, total_balance)
else:
0.Gwei

func process_slashings*(state: var ForkyBeaconState, total_balance: Gwei) =
let
epoch = get_current_epoch(state)
Expand Down Expand Up @@ -1164,7 +1217,7 @@ template compute_inactivity_update(
# TODO activeness already checked; remove redundant checks between
# is_active_validator and is_unslashed_participating_index
if is_unslashed_participating_index(
state, TIMELY_TARGET_FLAG_INDEX, previous_epoch, index.ValidatorIndex):
state, TIMELY_TARGET_FLAG_INDEX, previous_epoch, index):
inactivity_score -= min(1'u64, inactivity_score)
else:
inactivity_score += cfg.INACTIVITY_SCORE_BIAS
Expand Down Expand Up @@ -1195,6 +1248,7 @@ func process_inactivity_updates*(

let
pre_inactivity_score = state.inactivity_scores.asSeq()[index]
index = index.ValidatorIndex # intentional shadowing
inactivity_score =
compute_inactivity_update(cfg, state, info, pre_inactivity_score)

Expand Down Expand Up @@ -1507,3 +1561,108 @@ proc process_epoch*(
process_sync_committee_updates(state)

ok()

proc get_validator_balance_after_epoch*(
cfg: RuntimeConfig,
state: deneb.BeaconState | electra.BeaconState,
flags: UpdateFlags, cache: var StateCache, info: var altair.EpochInfo,
index: ValidatorIndex): Gwei =
# Run a subset of process_epoch() which affects an individual validator,
# without modifying state itself
info.init(state) # TODO avoid quadratic aspects here

# Can't use process_justification_and_finalization(), but use its helper
# function. Used to calculate inactivity_score.
let jf_info =
# process_justification_and_finalization() skips first two epochs
if get_current_epoch(state) <= GENESIS_EPOCH + 1:
JustificationAndFinalizationInfo(
previous_justified_checkpoint: state.previous_justified_checkpoint,
current_justified_checkpoint: state.current_justified_checkpoint,
finalized_checkpoint: state.finalized_checkpoint,
justification_bits: state.justification_bits)
else:
weigh_justification_and_finalization(
state, info.balances.current_epoch,
info.balances.previous_epoch[TIMELY_TARGET_FLAG_INDEX],
info.balances.current_epoch_TIMELY_TARGET, flags)

# Used as part of process_rewards_and_penalties
let inactivity_score =
# process_inactivity_updates skips GENESIS_EPOCH and ineligible validators
if get_current_epoch(state) == GENESIS_EPOCH or
not is_eligible_validator(info.validators[index]):
0.Gwei
else:
let
finality_delay =
get_previous_epoch(state) - jf_info.finalized_checkpoint.epoch
not_in_inactivity_leak = not is_in_inactivity_leak(finality_delay)
pre_inactivity_score = state.inactivity_scores.asSeq()[index]

# This is a template which uses not_in_inactivity_leak and index
compute_inactivity_update(cfg, state, info, pre_inactivity_score).Gwei

# process_rewards_and_penalties for a single validator
let reward_and_penalties_balance = block:
# process_rewards_and_penalties doesn't run at GENESIS_EPOCH
if get_current_epoch(state) == GENESIS_EPOCH:
state.balances.item(index)
else:
let
total_active_balance = info.balances.current_epoch
base_reward_per_increment = get_base_reward_per_increment(
total_active_balance)
finality_delay = get_finality_delay(state)

var balance = state.balances.item(index)
let maybeDelta = get_flag_and_inactivity_delta_for_validator(
cfg, state, base_reward_per_increment, info, finality_delay, index,
inactivity_score)
if maybeDelta.isOk:
# Can't use isErrOr in generics
let (validator_index, reward0, reward1, reward2, penalty0, penalty1, penalty2) =
maybeDelta.get
info.validators[validator_index].delta.rewards += reward0 + reward1 + reward2
info.validators[validator_index].delta.penalties += penalty0 + penalty1 + penalty2
increase_balance(balance, info.validators[index].delta.rewards)
decrease_balance(balance, info.validators[index].delta.penalties)
balance

# The two directly balance-changing operations, from Altair through Deneb,
# are these. The rest is necessary to look past a single epoch transition,
# but that's not the use case here.
var post_epoch_balance = reward_and_penalties_balance
decrease_balance(
post_epoch_balance,
get_slashing(state, info.balances.current_epoch, index))

# Electra adds process_pending_balance_deposit to the list of potential
# balance-changing epoch operations. This should probably be cached, so
# the 16+ invocations of this function each time, e.g., withdrawals are
# calculated don't repeat it, if it's empirically too expensive. Limits
# exist on how large this structure can get though.
when type(state).kind >= ConsensusFork.Electra:
let available_for_processing = state.deposit_balance_to_consume +
get_activation_exit_churn_limit(cfg, state, cache)
var processed_amount = 0.Gwei

for deposit in state.pending_balance_deposits:
let
validator = state.validators.item(deposit.index)
deposit_validator_index = ValidatorIndex.init(deposit.index).valueOr:
break

# Validator is exiting, postpone the deposit until after withdrawable epoch
if validator.exit_epoch < FAR_FUTURE_EPOCH:
if not(get_current_epoch(state) <= validator.withdrawable_epoch) and
deposit_validator_index == index:
increase_balance(post_epoch_balance, deposit.amount)
# Validator is not exiting, attempt to process deposit
else:
if not(processed_amount + deposit.amount > available_for_processing):
if deposit_validator_index == index:
increase_balance(post_epoch_balance, deposit.amount)
processed_amount += deposit.amount

post_epoch_balance
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import
chronicles,
# Beacon chain internals
../../../beacon_chain/spec/[presets, state_transition_epoch],
../../../beacon_chain/spec/datatypes/[altair, deneb],
../../../beacon_chain/spec/datatypes/altair,
# Test utilities
../../testutil,
../fixtures_utils, ../os_ops,
Expand All @@ -22,6 +22,8 @@ import

from std/sequtils import mapIt, toSeq
from std/strutils import rsplit
from ../../../beacon_chain/spec/datatypes/deneb import BeaconState
from ../../teststateutil import checkPerValidatorBalanceCalc

const
RootDir = SszTestsDir/const_preset/"deneb"/"epoch_processing"
Expand Down Expand Up @@ -73,13 +75,15 @@ template runSuite(
# ---------------------------------------------------------------
runSuite(JustificationFinalizationDir, "Justification & Finalization"):
let info = altair.EpochInfo.init(state)
check checkPerValidatorBalanceCalc(state)
process_justification_and_finalization(state, info.balances)
Result[void, cstring].ok()

# Inactivity updates
# ---------------------------------------------------------------
runSuite(InactivityDir, "Inactivity"):
let info = altair.EpochInfo.init(state)
check checkPerValidatorBalanceCalc(state)
process_inactivity_updates(cfg, state, info)
Result[void, cstring].ok()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import
chronicles,
# Beacon chain internals
../../../beacon_chain/spec/[presets, state_transition_epoch],
../../../beacon_chain/spec/datatypes/[altair, electra],
../../../beacon_chain/spec/datatypes/altair,
# Test utilities
../../testutil,
../fixtures_utils, ../os_ops,
Expand All @@ -22,6 +22,8 @@ import

from std/sequtils import mapIt, toSeq
from std/strutils import rsplit
from ../../../beacon_chain/spec/datatypes/electra import BeaconState
from ../../teststateutil import checkPerValidatorBalanceCalc

const
RootDir = SszTestsDir/const_preset/"electra"/"epoch_processing"
Expand Down Expand Up @@ -76,13 +78,15 @@ template runSuite(
# ---------------------------------------------------------------
runSuite(JustificationFinalizationDir, "Justification & Finalization"):
let info = altair.EpochInfo.init(state)
check checkPerValidatorBalanceCalc(state)
process_justification_and_finalization(state, info.balances)
Result[void, cstring].ok()

# Inactivity updates
# ---------------------------------------------------------------
runSuite(InactivityDir, "Inactivity"):
let info = altair.EpochInfo.init(state)
check checkPerValidatorBalanceCalc(state)
process_inactivity_updates(cfg, state, info)
Result[void, cstring].ok()

Expand Down
6 changes: 5 additions & 1 deletion tests/consensus_spec/test_fixture_sanity_blocks.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import
chronicles,
../../beacon_chain/spec/forks,
../../beacon_chain/spec/state_transition,
../../beacon_chain/spec/[state_transition, state_transition_epoch],
./os_ops,
../testutil

Expand All @@ -21,6 +21,7 @@ from ../../beacon_chain/spec/presets import
const_preset, defaultRuntimeConfig
from ./fixtures_utils import
SSZ, SszTestsDir, hash_tree_root, parseTest, readSszBytes, toSszType
from ../teststateutil import checkPerValidatorBalanceCalc

proc runTest(
consensusFork: static ConsensusFork,
Expand Down Expand Up @@ -52,6 +53,9 @@ proc runTest(
discard state_transition(
defaultRuntimeConfig, fhPreState[], blck, cache, info, flags = {},
noRollback).expect("should apply block")
withState(fhPreState[]):
when consensusFork >= ConsensusFork.Deneb:
check checkPerValidatorBalanceCalc(forkyState.data)
else:
let res = state_transition(
defaultRuntimeConfig, fhPreState[], blck, cache, info, flags = {},
Expand Down
20 changes: 19 additions & 1 deletion tests/teststateutil.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import
forks, state_transition, state_transition_block]

from ".."/beacon_chain/bloomfilter import constructBloomFilter
from ".."/beacon_chain/spec/state_transition_epoch import
get_validator_balance_after_epoch, process_epoch


func round_multiple_down(x: Gwei, n: Gwei): Gwei =
## Round the input to the previous multiple of "n"
Expand Down Expand Up @@ -97,4 +100,19 @@ proc getTestStates*(
doAssert getStateField(tmpState[], slot) == slot

if tmpState[].kind == consensusFork:
result.add assignClone(tmpState[])
result.add assignClone(tmpState[])

proc checkPerValidatorBalanceCalc*(
state: deneb.BeaconState | electra.BeaconState): bool =
var
info: altair.EpochInfo
cache: StateCache
let tmpState = newClone(state) # slow, but tolerable for tests
discard process_epoch(defaultRuntimeConfig, tmpState[], {}, cache, info)
for i in 0 ..< tmpState.balances.len:
if tmpState.balances.item(i) != get_validator_balance_after_epoch(
defaultRuntimeConfig, state, default(UpdateFlags), cache, info,
i.ValidatorIndex):
return false

true

0 comments on commit 3db571d

Please sign in to comment.