UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

111 lines 5.8 kB
import { EPOCHS_PER_ETH1_VOTING_PERIOD, SLOTS_PER_EPOCH, isForkPostElectra } from "@lodestar/params"; import { computeTimeAtSlot } from "@lodestar/state-transition"; import { toRootHex } from "@lodestar/utils"; export async function getEth1VotesToConsider(config, state, eth1DataGetter) { const fork = config.getForkName(state.slot); if (isForkPostElectra(fork)) { const { eth1DepositIndex, depositRequestsStartIndex } = state; if (eth1DepositIndex === Number(depositRequestsStartIndex)) { return state.eth1DataVotes.getAllReadonly(); } } const periodStart = votingPeriodStartTime(config, state); const { SECONDS_PER_ETH1_BLOCK, ETH1_FOLLOW_DISTANCE } = config; // Modified version of the spec function to fetch the required range directly from the DB return (await eth1DataGetter({ timestampRange: { // Spec v0.12.2 // is_candidate_block = // block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE <= period_start && // block.timestamp + SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2 >= period_start lte: periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE, gte: periodStart - SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE * 2, }, })).filter((eth1Data) => eth1Data.depositCount >= state.eth1Data.depositCount); } export function pickEth1Vote(state, votesToConsider) { const votesToConsiderKeys = new Set(); for (const eth1Data of votesToConsider) { votesToConsiderKeys.add(getEth1DataKey(eth1Data)); } const eth1DataHashToEth1Data = new Map(); const eth1DataVoteCountByRoot = new Map(); const eth1DataVotesOrder = []; // BeaconStateAllForks is always represented as a tree with a hashing cache. // To check equality its cheaper to use hashTreeRoot as keys. // However `votesToConsider` is an array of values since those are read from DB. // TODO: Optimize cache of known votes, to prevent re-hashing stored values. // Note: for low validator counts it's not very important, since this runs once per proposal const eth1DataVotes = state.eth1DataVotes.getAllReadonly(); for (const eth1DataVote of eth1DataVotes) { const rootHex = getEth1DataKey(eth1DataVote); if (votesToConsiderKeys.has(rootHex)) { const prevVoteCount = eth1DataVoteCountByRoot.get(rootHex); eth1DataVoteCountByRoot.set(rootHex, 1 + (prevVoteCount ?? 0)); // Cache eth1DataVote to root Map only once per root if (prevVoteCount === undefined) { eth1DataHashToEth1Data.set(rootHex, eth1DataVote); eth1DataVotesOrder.push(rootHex); } } } const eth1DataRootsMaxVotes = getKeysWithMaxValue(eth1DataVoteCountByRoot); // No votes, vote for the last valid vote if (eth1DataRootsMaxVotes.length === 0) { return votesToConsider.at(-1) ?? state.eth1Data; } // If there's a single winning vote with a majority vote that one if (eth1DataRootsMaxVotes.length === 1) { return eth1DataHashToEth1Data.get(eth1DataRootsMaxVotes[0]) ?? state.eth1Data; } // If there are multiple winning votes, vote for the latest one const latestMostVotedRoot = eth1DataVotesOrder[Math.max(...eth1DataRootsMaxVotes.map((root) => eth1DataVotesOrder.indexOf(root)))]; return eth1DataHashToEth1Data.get(latestMostVotedRoot) ?? state.eth1Data; } /** * Returns the array of keys with max value. May return 0, 1 or more keys */ function getKeysWithMaxValue(map) { const entries = Array.from(map.entries()); let keysMax = []; let valueMax = -Infinity; for (const [key, value] of entries) { if (value > valueMax) { keysMax = [key]; valueMax = value; } else if (value === valueMax) { keysMax.push(key); } } return keysMax; } /** * Key-ed by fastSerializeEth1Data(). votesToConsider is read from DB as struct and always has a length of 2048. * `state.eth1DataVotes` has a length between 0 and ETH1_FOLLOW_DISTANCE with an equal probability of each value. * So to get the average faster time to key both votesToConsider and state.eth1DataVotes it's better to use * fastSerializeEth1Data(). However, a long term solution is to cache valid votes in memory and prevent having * to recompute their key on every proposal. * * With `fastSerializeEth1Data()`: avg time 20 ms/op * ✓ pickEth1Vote - no votes 233.0587 ops/s 4.290764 ms/op - 121 runs 1.02 s * ✓ pickEth1Vote - max votes 29.21546 ops/s 34.22845 ms/op - 25 runs 1.38 s * * With `toHexString(ssz.phase0.Eth1Data.hashTreeRoot(eth1Data))`: avg time 23 ms/op * ✓ pickEth1Vote - no votes 46.12341 ops/s 21.68096 ms/op - 133 runs 3.40 s * ✓ pickEth1Vote - max votes 37.89912 ops/s 26.38583 ms/op - 29 runs 1.27 s */ function getEth1DataKey(eth1Data) { return fastSerializeEth1Data(eth1Data); } /** * Serialize eth1Data types to a unique string ID. It is only used for comparison. */ export function fastSerializeEth1Data(eth1Data) { return toRootHex(eth1Data.blockHash) + eth1Data.depositCount.toString(16) + toRootHex(eth1Data.depositRoot); } export function votingPeriodStartTime(config, state) { const eth1VotingPeriodStartSlot = state.slot - (state.slot % (EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH)); return computeTimeAtSlot(config, eth1VotingPeriodStartSlot, state.genesisTime); } //# sourceMappingURL=eth1Vote.js.map