import {Node} from "@chainsafe/persistent-merkle-tree"; import {CompositeViewDU} from "@chainsafe/ssz"; import {EPOCHS_PER_ETH1_VOTING_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; /** * Store vote counts for every eth-execution block that has votes; if any eth-execution block wins majority support within a 1024-slot * voting period, formally accept that eth-execution block and set it as the official "latest known eth-execution block" in the eth-consensus state. * * PERF: Processing cost depends on the current amount of votes. * - Best case: Vote is already decided, zero work. See becomesNewEth1Data conditions * - Worst case: 1023 votes and no majority vote yet. */ export function processEth1Data(state: CachedBeaconStateAllForks, eth1Data: phase0.Eth1Data): void { // Convert to view first to hash once and compare hashes const eth1DataView = ssz.phase0.Eth1Data.toViewDU(eth1Data); if (becomesNewEth1Data(state, eth1DataView)) { state.eth1Data = eth1DataView; } state.eth1DataVotes.push(eth1DataView); } /** * Returns true if adding the given `eth1Data` to `state.eth1DataVotes` would * result in a change to `state.eth1Data`. */ export function becomesNewEth1Data( state: BeaconStateAllForks, newEth1Data: CompositeViewDU ): boolean { const SLOTS_PER_ETH1_VOTING_PERIOD = EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH; // If there are not more than 50% votes, then we do not have to count to find a winner. if ((state.eth1DataVotes.length + 1) * 2 <= SLOTS_PER_ETH1_VOTING_PERIOD) { return false; } // Nothing to do if the state already has this as eth1data (happens a lot after majority vote is in) if (isEqualEth1DataView(state.eth1Data, newEth1Data)) { return false; } // Close to half the EPOCHS_PER_ETH1_VOTING_PERIOD it can be expensive to do so many comparisions. // `eth1DataVotes.getAllReadonly()` navigates the tree once to fetch all the LeafNodes efficiently. // Then isEqualEth1DataView compares cached roots (HashObject as of Jan 2022) which is much cheaper // than doing structural equality, which requires tree -> value conversions let sameVotesCount = 0; // biome-ignore lint/complexity/noForEach: ssz api state.eth1DataVotes.forEach((eth1DataVote) => { if (isEqualEth1DataView(eth1DataVote, newEth1Data)) { sameVotesCount++; } }); // The +1 is to account for the `eth1Data` supplied to the function. if ((sameVotesCount + 1) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD) { return true; } return false; } function isEqualEth1DataView( eth1DataA: CompositeViewDU, eth1DataB: CompositeViewDU ): boolean { return isEqualNode(eth1DataA.node, eth1DataB.node); } // TODO: Upstream to persistent-merkle-tree function isEqualNode(nA: Node, nB: Node): boolean { const hA = nA.rootHashObject; const hB = nB.rootHashObject; return ( hA.h0 === hB.h0 && hA.h1 === hB.h1 && hA.h2 === hB.h2 && hA.h3 === hB.h3 && hA.h4 === hB.h4 && hA.h5 === hB.h5 && hA.h6 === hB.h6 && hA.h7 === hB.h7 ); }