nimbus-eth2/beacon_chain/validator_client/scoring.nim

245 lines
8.4 KiB
Nim

# beacon_chain
# Copyright (c) 2023-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/strutils
import ssz_serialization/[types, bitseqs]
import stew/endians2
import stint
import nimcrypto/hash
import "."/common
type
CommitteeBitsArray = BitArray[int(MAX_VALIDATORS_PER_COMMITTEE)]
CommitteeTable = Table[CommitteeIndex, CommitteeBitsArray]
const
DefaultCommitteeTable = default(CommitteeTable)
DefaultCommitteeBitsArray = default(CommitteeBitsArray)
func perfectScore*(score: float64): bool =
score == Inf
func perfectScore*(score: UInt256): bool =
score == high(UInt256)
proc shortScore*(score: float64): string =
if score == Inf:
"<perfect>"
elif score == -Inf:
"<bad>"
else:
formatFloat(score, ffDecimal, 4)
proc shortScore*(score: UInt256): string =
$score
func getLexicographicScore(digest: Eth2Digest): float64 =
# We calculate score on first 8 bytes of digest.
let
dvalue = uint64.fromBytesBE(digest.data.toOpenArray(0, sizeof(uint64) - 1))
value = float64(dvalue) / float64(high(uint64))
value
proc getAttestationDataScore*(rootsSeen: Table[Eth2Digest, Slot],
adata: ProduceAttestationDataResponse): float64 =
let
slot = rootsSeen.getOrDefault(
adata.data.beacon_block_root, FAR_FUTURE_SLOT)
let res =
if (slot == adata.data.slot) and
(adata.data.source.epoch + 1 == adata.data.target.epoch):
# Perfect score
Inf
else:
let score = float64(adata.data.source.epoch) +
float64(adata.data.target.epoch)
if slot == FAR_FUTURE_SLOT:
score
else:
if adata.data.slot + 1 == slot:
# To avoid `DivizionByZero` defect.
score
else:
score + float64(1) / (float64(adata.data.slot) + float64(1) -
float64(slot))
debug "Attestation score", attestation_data = shortLog(adata.data),
block_slot = slot, score = shortScore(res)
res
proc getAttestationDataScore*(vc: ValidatorClientRef,
adata: ProduceAttestationDataResponse): float64 =
getAttestationDataScore(vc.rootsSeen, adata)
proc getAggregatedAttestationDataScore*(
adata: GetAggregatedAttestationResponse
): float64 =
# This procedure returns score value in range [0.0000, 1.0000) and `Inf`.
# It returns perfect score when all the bits was set to `1`, but this could
# provide wrong expectation for some edge cases (when different attestations
# has different committee sizes), but currently this is the only viable way
# to return perfect score.
const MaxLength = int(MAX_VALIDATORS_PER_COMMITTEE)
doAssert(len(adata.data.aggregation_bits) <= MaxLength)
let
size = len(adata.data.aggregation_bits)
ones = countOnes(adata.data.aggregation_bits)
res =
if ones == size:
# We consider score perfect, when all bits was set to 1.
Inf
else:
float64(ones) / float64(size)
debug "Aggregated attestation score", attestation_data = shortLog(adata.data),
block_slot = adata.data.data.slot, committee_size = size,
ones_count = ones, score = shortScore(res)
res
proc getAggregatedAttestationDataScore*(
adata: GetAggregatedAttestationV2Response
): float64 =
# This procedure returns score value in range [0.0000, 1.0000) and `Inf`.
# It returns perfect score when all the bits was set to `1`, but this could
# provide wrong expectation for some edge cases (when different attestations
# has different committee sizes), but currently this is the only viable way
# to return perfect score.
withAttestation(adata):
const MaxLength = int(MAX_VALIDATORS_PER_COMMITTEE)
doAssert(len(forkyAttestation.aggregation_bits) <= MaxLength)
let
size = len(forkyAttestation.aggregation_bits)
ones = countOnes(forkyAttestation.aggregation_bits)
res =
if ones == size:
# We consider score perfect, when all bits was set to 1.
Inf
else:
float64(ones) / float64(size)
debug "Aggregated attestation score",
attestation_data = shortLog(forkyAttestation.data),
block_slot = forkyAttestation.data.slot, committee_size = size,
ones_count = ones, score = shortScore(res)
res
proc getSyncCommitteeContributionDataScore*(
cdata: ProduceSyncCommitteeContributionResponse
): float64 =
# This procedure returns score value in range [0.0000, 1.0000) and `Inf`.
# It returns perfect score when all the bits was set to `1`, but this could
# provide wrong expectation for some edge cases (when different contributions
# has different committee sizes), but currently this is the only viable way
# to return perfect score.
const MaxLength = int(SYNC_SUBCOMMITTEE_SIZE)
doAssert(len(cdata.data.aggregation_bits) <= MaxLength)
let
size = len(cdata.data.aggregation_bits)
ones = countOnes(cdata.data.aggregation_bits)
res =
if ones == size:
# We consider score perfect, when all bits was set to 1.
Inf
else:
float64(ones) / float64(size)
debug "Sync committee contribution score",
contribution_data = shortLog(cdata.data), block_slot = cdata.data.slot,
committee_size = size, ones_count = ones, score = shortScore(res)
res
proc getSyncCommitteeMessageDataScore*(
rootsSeen: Table[Eth2Digest, Slot],
currentSlot: Slot,
cdata: GetBlockRootResponse
): float64 =
let
slot = rootsSeen.getOrDefault(cdata.data.root, FAR_FUTURE_SLOT)
res =
if cdata.execution_optimistic.get(true):
# Responses from the nodes which are optimistically synced only are
# not suitable, score it with minimal possible score.
-Inf
else:
if slot != FAR_FUTURE_SLOT:
# When `slot` has been found score value will be in range of
# `(1, 2]` or `Inf`.
if slot == currentSlot:
# Perfect score
Inf
else:
float64(1) +
float64(1) / (float64(1) + float64(currentSlot) - float64(slot))
else:
# Block monitoring is disabled or we missed a block, in this case
# score value will be in range of `(0, 1]`
getLexicographicScore(cdata.data.root)
debug "Sync committee message score",
head_block_root = shortLog(cdata.data.root), slot = slot,
current_slot = currentSlot, score = shortScore(res)
res
proc getSyncCommitteeMessageDataScore*(
vc: ValidatorClientRef,
cdata: GetBlockRootResponse
): float64 =
getSyncCommitteeMessageDataScore(
vc.rootsSeen, vc.beaconClock.now().slotOrZero(), cdata)
proc processVotes(bits: var CommitteeBitsArray,
attestation: phase0.Attestation): int =
doAssert(len(attestation.aggregation_bits) <= len(bits))
var res = 0
for index in 0 ..< len(attestation.aggregation_bits):
if attestation.aggregation_bits[index]:
if not(bits[index]):
inc(res)
bits[index] = true
res
proc getUniqueVotes*(attestations: openArray[phase0.Attestation]): int =
var
res = 0
attested: Table[Slot, CommitteeTable]
for attestation in attestations:
let count =
attested.mgetOrPut(attestation.data.slot, DefaultCommitteeTable).
mgetOrPut(CommitteeIndex(attestation.data.index),
DefaultCommitteeBitsArray).
processVotes(attestation)
res += count
res
proc getProduceBlockResponseV3Score*(blck: ProduceBlockResponseV3): UInt256 =
let (res, cv, ev) =
block:
var score256 = UInt256.zero
let
cvalue =
if blck.consensusValue.isSome():
let value = blck.consensusValue.get()
score256 = score256 + value
$value
else:
"<missing>"
evalue =
if blck.executionValue.isSome():
let value = blck.executionValue.get()
score256 = score256 + value
$value
else:
"<missing>"
(score256, cvalue, evalue)
debug "Block score", blck = shortLog(blck), consensus_value = cv,
execution_value = ev, score = shortScore(res)
res