# beacon_chain # Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. {.push raises: [].} import std/[os, strutils], stew/bitops2, ../beacon_chain/spec/[ datatypes/base, datatypes/phase0, datatypes/altair, datatypes/bellatrix, beaconstate, state_transition_epoch, state_transition_block, signatures], ../beacon_chain/consensus_object_pools/blockchain_dag from ../beacon_chain/spec/datatypes/capella import BeaconState type RewardsAndPenalties* = object source_outcome*: int64 max_source_reward*: Gwei target_outcome*: int64 max_target_reward*: Gwei head_outcome*: int64 max_head_reward*: Gwei inclusion_delay_outcome*: int64 max_inclusion_delay_reward*: Gwei sync_committee_outcome*: int64 max_sync_committee_reward*: Gwei proposer_outcome*: int64 inactivity_penalty*: Gwei slashing_outcome*: int64 deposits*: Gwei inclusion_delay*: Option[uint64] ParticipationFlags* = object currentEpochParticipation: EpochParticipationFlags previousEpochParticipation: EpochParticipationFlags PubkeyToIndexTable = Table[ValidatorPubKey, ValidatorIndex] AuxiliaryState* = object epochParticipationFlags: ParticipationFlags pubkeyToIndex: PubkeyToIndexTable const epochInfoFileNameDigitsCount = 8 epochFileNameExtension = ".epoch" func copyParticipationFlags*(auxiliaryState: var AuxiliaryState, forkedState: ForkedHashedBeaconState) = withState(forkedState): when consensusFork > ConsensusFork.Phase0: template flags: untyped = auxiliaryState.epochParticipationFlags flags.currentEpochParticipation = forkyState.data.current_epoch_participation flags.previousEpochParticipation = forkyState.data.previous_epoch_participation from std/sequtils import allIt func matchFilenameUnaggregatedFiles(filename: string): bool = # epochNumberRegexStr & epochFileNameExtension filename.len == epochInfoFileNameDigitsCount + epochFileNameExtension.len and filename.endsWith(epochFileNameExtension) and # TODO should use toOpenArray, but # https://github.com/nim-lang/Nim/issues/15952 # https://github.com/nim-lang/Nim/issues/19969 allIt(filename[0 ..< epochInfoFileNameDigitsCount], it.isDigit) static: for filename in [ "00000000.epoch", "00243929.epoch", "04957024.epoch", "39820353.epoch", "82829191.epoch", "85740966.epoch", "93321944.epoch", "98928899.epoch"]: doAssert filename.matchFilenameUnaggregatedFiles for filename in [ # Valid aggregated, not unaggregated "03820350_13372742.epoch", "04117778_69588614.epoch", "25249017_64218993.epoch", "34265267_41589365.epoch", "57926659_59282297.epoch", "67699314_92835461.epoch", "0000000.epoch", # Too short "000000000.epoch", # Too long "75787x73.epoch", # Incorrect number format "00000000.ecpoh"]: # Wrong extension doAssert not filename.matchFilenameUnaggregatedFiles func matchFilenameAggregatedFiles(filename: string): bool = # epochNumberRegexStr & "_" & epochNumberRegexStr & epochFileNameExtension filename.len == epochInfoFileNameDigitsCount * 2 + "_".len + epochFileNameExtension.len and filename.endsWith(epochFileNameExtension) and # TODO should use toOpenArray, but # https://github.com/nim-lang/Nim/issues/15952 # https://github.com/nim-lang/Nim/issues/19969 allIt(filename[0 ..< epochInfoFileNameDigitsCount], it.isDigit) and filename[epochInfoFileNameDigitsCount] == '_' and allIt( filename[epochInfoFileNameDigitsCount + 1 ..< 2 * epochInfoFileNameDigitsCount + 1], it.isDigit) static: for filename in [ "03820350_13372742.epoch", "04117778_69588614.epoch", "25249017_64218993.epoch", "34265267_41589365.epoch", "57926659_59282297.epoch", "67699314_92835461.epoch"]: doAssert filename.matchFilenameAggregatedFiles for filename in [ # Valid unaggregated, not aggregated "00000000.epoch", "00243929.epoch", "04957024.epoch", "39820353.epoch", "82829191.epoch", "85740966.epoch", "93321944.epoch", "98928899.epoch", "00000000_0000000.epoch", # Too short "31x85971_93149672.epoch", # Incorrect number format, first field "18049105&72034596.epoch", # No underscore separator "31485971_931496x2.epoch", # Incorrect number format, second field "15227487_86601706.echop"]: # Wrong extension doAssert not filename.matchFilenameAggregatedFiles proc getUnaggregatedFilesEpochRange*( dir: string ): tuple[firstEpoch, lastEpoch: Epoch] {.raises: [OSError, ValueError].} = var smallestEpochFileName = '9'.repeat(epochInfoFileNameDigitsCount) & epochFileNameExtension var largestEpochFileName = '0'.repeat(epochInfoFileNameDigitsCount) & epochFileNameExtension for (_, fn) in walkDir(dir.string, relative = true): if fn.matchFilenameUnaggregatedFiles: if fn < smallestEpochFileName: smallestEpochFileName = fn if fn > largestEpochFileName: largestEpochFileName = fn result.firstEpoch = parseUInt( smallestEpochFileName[0 ..< epochInfoFileNameDigitsCount]).Epoch result.lastEpoch = parseUInt( largestEpochFileName[0 ..< epochInfoFileNameDigitsCount]).Epoch proc getUnaggregatedFilesLastEpoch*( dir: string): Epoch {.raises: [OSError, ValueError].} = dir.getUnaggregatedFilesEpochRange.lastEpoch proc getAggregatedFilesLastEpoch*( dir: string): Epoch {.raises: [OSError, ValueError].}= var largestEpochInFileName = 0'u for (_, fn) in walkDir(dir.string, relative = true): if fn.matchFilenameAggregatedFiles: let fileLastEpoch = parseUInt( fn[epochInfoFileNameDigitsCount + 1 .. 2 * epochInfoFileNameDigitsCount]) if fileLastEpoch > largestEpochInFileName: largestEpochInFileName = fileLastEpoch largestEpochInFileName.Epoch func epochAsString*(epoch: Epoch): string = let strEpoch = $epoch '0'.repeat(epochInfoFileNameDigitsCount - strEpoch.len) & strEpoch func getFilePathForEpoch*(epoch: Epoch, dir: string): string = dir / epochAsString(epoch) & epochFileNameExtension func getFilePathForEpochs*(startEpoch, endEpoch: Epoch, dir: string): string = let fileName = epochAsString(startEpoch) & "_" & epochAsString(endEpoch) & epochFileNameExtension dir / fileName func getBlockRange*(dag: ChainDAGRef, start, ends: Slot): seq[BlockId] = # Range of block in reverse order doAssert start < ends result = newSeqOfCap[BlockId](ends - start) var current = ends while current > start: current -= 1 let bsid = dag.getBlockIdAtSlot(current).valueOr: continue if bsid.bid.slot < start: # current might be empty break result.add(bsid.bid) current = bsid.bid.slot # skip empty slots func getOutcome(delta: RewardDelta): int64 = delta.rewards.int64 - delta.penalties.int64 func collectSlashings( rewardsAndPenalties: var seq[RewardsAndPenalties], state: ForkyBeaconState, total_balance: Gwei) = let epoch = get_current_epoch(state) adjusted_total_slashing_balance = get_adjusted_total_slashing_balance( state, total_balance) for index in 0 ..< state.validators.len: let validator = unsafeAddr state.validators[index] if slashing_penalty_applies(validator[], epoch): rewardsAndPenalties[index].slashing_outcome += validator[].get_slashing_penalty( adjusted_total_slashing_balance, total_balance).int64 proc collectEpochRewardsAndPenalties*( rewardsAndPenalties: var seq[RewardsAndPenalties], state: var phase0.BeaconState, cache: var StateCache, cfg: RuntimeConfig, flags: UpdateFlags) = if get_current_epoch(state) == GENESIS_EPOCH: return var info: phase0.EpochInfo info.init(state) info.process_attestations(state, cache) doAssert info.validators.len == state.validators.len rewardsAndPenalties.setLen(state.validators.len) process_justification_and_finalization(state, info.balances, flags) let finality_delay = get_finality_delay(state) total_balance = info.balances.current_epoch total_balance_sqrt = integer_squareroot(distinctBase(total_balance)) for index, validator in info.validators: if not is_eligible_validator(validator): continue let base_reward = get_base_reward_sqrt( state, index.ValidatorIndex, total_balance_sqrt) template get_attestation_component_reward_helper( attesting_balance: Gwei): Gwei = get_attestation_component_reward(attesting_balance, info.balances.current_epoch, base_reward, finality_delay) template rp: untyped = rewardsAndPenalties[index] rp.source_outcome = get_source_delta( validator, base_reward, info.balances, finality_delay).getOutcome rp.max_source_reward = get_attestation_component_reward_helper( info.balances.previous_epoch_attesters) rp.target_outcome = get_target_delta( validator, base_reward, info.balances, finality_delay).getOutcome rp.max_target_reward = get_attestation_component_reward_helper( info.balances.previous_epoch_target_attesters) rp.head_outcome = get_head_delta( validator, base_reward, info.balances, finality_delay).getOutcome rp.max_head_reward = get_attestation_component_reward_helper( info.balances.previous_epoch_head_attesters) let (inclusion_delay_delta, proposer_delta) = get_inclusion_delay_delta( validator, base_reward) rp.inclusion_delay_outcome = inclusion_delay_delta.getOutcome rp.max_inclusion_delay_reward = base_reward - state_transition_epoch.get_proposer_reward(base_reward) rp.inactivity_penalty = get_inactivity_penalty_delta( validator, base_reward, finality_delay).penalties if proposer_delta.isSome: let proposer_index = proposer_delta.get[0] if proposer_index < info.validators.lenu64: rewardsAndPenalties[proposer_index].proposer_outcome += proposer_delta.get[1].getOutcome rewardsAndPenalties.collectSlashings(state, info.balances.current_epoch) proc collectEpochRewardsAndPenalties*( rewardsAndPenalties: var seq[RewardsAndPenalties], state: var (altair.BeaconState | bellatrix.BeaconState | capella.BeaconState | deneb.BeaconState | electra.BeaconState), cache: var StateCache, cfg: RuntimeConfig, flags: UpdateFlags) = if get_current_epoch(state) == GENESIS_EPOCH: return var info: altair.EpochInfo info.init(state) doAssert info.validators.len == state.validators.len rewardsAndPenalties.setLen(state.validators.len) process_justification_and_finalization(state, info.balances, flags) process_inactivity_updates(cfg, state, info) 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) for validator_index, reward_source, reward_target, reward_head, penalty_source, penalty_target, penalty_inactivity in get_flag_and_inactivity_deltas( cfg, state, base_reward_per_increment, info, finality_delay): template rp: untyped = rewardsAndPenalties[validator_index] let base_reward = get_base_reward_increment( state, validator_index, base_reward_per_increment) active_increments = get_active_increments(info) template unslashed_participating_increment(flag_index: untyped): untyped = get_unslashed_participating_increment(info, flag_index) template max_flag_index_reward(flag_index: untyped): untyped = get_flag_index_reward( state, base_reward, active_increments, unslashed_participating_increment(flag_index), PARTICIPATION_FLAG_WEIGHTS[flag_index], finality_delay) rp.source_outcome = reward_source.int64 - penalty_source.int64 rp.max_source_reward = max_flag_index_reward(TimelyFlag.TIMELY_SOURCE_FLAG_INDEX) rp.target_outcome = reward_target.int64 - penalty_target.int64 rp.max_target_reward = max_flag_index_reward(TimelyFlag.TIMELY_TARGET_FLAG_INDEX) rp.head_outcome = reward_head.int64 rp.max_head_reward = max_flag_index_reward(TimelyFlag.TIMELY_HEAD_FLAG_INDEX) rewardsAndPenalties[validator_index].inactivity_penalty += penalty_inactivity rewardsAndPenalties.collectSlashings(state, info.balances.current_epoch) func collectFromSlashedValidator( rewardsAndPenalties: var seq[RewardsAndPenalties], state: ForkyBeaconState, slashedIndex, proposerIndex: ValidatorIndex) = template slashed_validator: untyped = state.validators[slashedIndex] let slashingPenalty = get_slashing_penalty(state, slashed_validator.effective_balance) let whistleblowerReward = get_whistleblower_reward(slashed_validator.effective_balance) rewardsAndPenalties[slashedIndex].slashing_outcome -= slashingPenalty.int64 rewardsAndPenalties[proposerIndex].slashing_outcome += whistleblowerReward.int64 func collectFromProposerSlashings( rewardsAndPenalties: var seq[RewardsAndPenalties], forkedState: ForkedHashedBeaconState, forkedBlock: ForkedTrustedSignedBeaconBlock) = withStateAndBlck(forkedState, forkedBlock): for proposer_slashing in forkyBlck.message.body.proposer_slashings: doAssert check_proposer_slashing( forkyState.data, proposer_slashing, {}).isOk let slashedIndex = proposer_slashing.signed_header_1.message.proposer_index rewardsAndPenalties.collectFromSlashedValidator( forkyState.data, slashedIndex.ValidatorIndex, forkyBlck.message.proposer_index.ValidatorIndex) func collectFromAttesterSlashings( rewardsAndPenalties: var seq[RewardsAndPenalties], forkedState: ForkedHashedBeaconState, forkedBlock: ForkedTrustedSignedBeaconBlock) = withStateAndBlck(forkedState, forkedBlock): for attester_slashing in forkyBlck.message.body.attester_slashings: let attester_slashing_validity = check_attester_slashing( forkyState.data, attester_slashing, {}) doAssert attester_slashing_validity.isOk for slashedIndex in attester_slashing_validity.value: rewardsAndPenalties.collectFromSlashedValidator( forkyState.data, slashedIndex, forkyBlck.message.proposer_index.ValidatorIndex) func collectFromAttestations( rewardsAndPenalties: var seq[RewardsAndPenalties], forkedState: ForkedHashedBeaconState, forkedBlock: ForkedTrustedSignedBeaconBlock, epochParticipationFlags: var ParticipationFlags, cache: var StateCache) = withStateAndBlck(forkedState, forkedBlock): when consensusFork > ConsensusFork.Phase0: let base_reward_per_increment = get_base_reward_per_increment( get_total_active_balance(forkyState.data, cache)) doAssert base_reward_per_increment > 0.Gwei for attestation in forkyBlck.message.body.attestations: doAssert check_attestation( forkyState.data, attestation, {}, cache).isOk let proposerReward = if attestation.data.target.epoch == get_current_epoch(forkyState.data): get_proposer_reward( forkyState.data, attestation, base_reward_per_increment, cache, epochParticipationFlags.currentEpochParticipation) else: get_proposer_reward( forkyState.data, attestation, base_reward_per_increment, cache, epochParticipationFlags.previousEpochParticipation) rewardsAndPenalties[forkyBlck.message.proposer_index] .proposer_outcome += proposerReward.int64 let inclusionDelay = forkyState.data.slot - attestation.data.slot for index in get_attesting_indices( forkyState.data, attestation.data, attestation.aggregation_bits, cache): rewardsAndPenalties[index].inclusion_delay = some(inclusionDelay.uint64) proc collectFromDeposits( rewardsAndPenalties: var seq[RewardsAndPenalties], forkedState: ForkedHashedBeaconState, forkedBlock: ForkedTrustedSignedBeaconBlock, pubkeyToIndex: var PubkeyToIndexTable, cfg: RuntimeConfig) = withStateAndBlck(forkedState, forkedBlock): for deposit in forkyBlck.message.body.deposits: let pubkey = deposit.data.pubkey let amount = deposit.data.amount var index = findValidatorIndex(forkyState.data, pubkey) if index.isNone: if pubkey in pubkeyToIndex: try: index = Opt[ValidatorIndex].ok(pubkeyToIndex[pubkey]) except KeyError as e: raiseAssert "pubkey was checked to exist: " & e.msg if index.isSome: try: rewardsAndPenalties[index.get()].deposits += amount except KeyError as e: raiseAssert "rewardsAndPenalties lacks expected index " & $index.get() elif verify_deposit_signature(cfg, deposit.data): pubkeyToIndex[pubkey] = ValidatorIndex(rewardsAndPenalties.len) rewardsAndPenalties.add( RewardsAndPenalties(deposits: amount)) func collectFromSyncAggregate( rewardsAndPenalties: var seq[RewardsAndPenalties], forkedState: ForkedHashedBeaconState, forkedBlock: ForkedTrustedSignedBeaconBlock, cache: var StateCache) = withStateAndBlck(forkedState, forkedBlock): when consensusFork > ConsensusFork.Phase0: let total_active_balance = get_total_active_balance(forkyState.data, cache) participant_reward = get_participant_reward(total_active_balance) proposer_reward = state_transition_block.get_proposer_reward(participant_reward) indices = get_sync_committee_cache( forkyState.data, cache).current_sync_committee template aggregate: untyped = forkyBlck.message.body.sync_aggregate doAssert indices.len == SYNC_COMMITTEE_SIZE doAssert aggregate.sync_committee_bits.len == SYNC_COMMITTEE_SIZE doAssert forkyState.data.current_sync_committee.pubkeys.len == SYNC_COMMITTEE_SIZE for i in 0 ..< SYNC_COMMITTEE_SIZE: rewardsAndPenalties[indices[i]].max_sync_committee_reward += participant_reward if aggregate.sync_committee_bits[i]: rewardsAndPenalties[indices[i]].sync_committee_outcome += participant_reward.int64 rewardsAndPenalties[forkyBlck.message.proposer_index] .proposer_outcome += proposer_reward.int64 else: rewardsAndPenalties[indices[i]].sync_committee_outcome -= participant_reward.int64 proc collectBlockRewardsAndPenalties*( rewardsAndPenalties: var seq[RewardsAndPenalties], forkedState: ForkedHashedBeaconState, forkedBlock: ForkedTrustedSignedBeaconBlock, auxiliaryState: var AuxiliaryState, cache: var StateCache, cfg: RuntimeConfig) = rewardsAndPenalties.collectFromProposerSlashings(forkedState, forkedBlock) rewardsAndPenalties.collectFromAttesterSlashings(forkedState, forkedBlock) rewardsAndPenalties.collectFromAttestations( forkedState, forkedBlock, auxiliaryState.epochParticipationFlags, cache) rewardsAndPenalties.collectFromDeposits( forkedState, forkedBlock, auxiliaryState.pubkeyToIndex, cfg) # This table is needed only to resolve double deposits in the same block, so # it can be cleared after processing all deposits for the current block. auxiliaryState.pubkeyToIndex.clear rewardsAndPenalties.collectFromSyncAggregate(forkedState, forkedBlock, cache) func serializeToCsv*(rp: RewardsAndPenalties, avgInclusionDelay = none(float)): string = for name, value in fieldPairs(rp): if value isnot Option: result &= $value & "," if avgInclusionDelay.isSome: result.addFloat(avgInclusionDelay.get) elif rp.inclusion_delay.isSome: result &= $rp.inclusion_delay.get result &= "\n"