UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

350 lines • 16.8 kB
import { BLS_WITHDRAWAL_PREFIX, ForkName, ForkSeq, MAX_ATTESTER_SLASHINGS, MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_BLS_TO_EXECUTION_CHANGES, MAX_PROPOSER_SLASHINGS, MAX_VOLUNTARY_EXITS, } from "@lodestar/params"; import { computeEpochAtSlot, computeStartSlotAtEpoch, getAttesterSlashableIndices, isValidVoluntaryExit, } from "@lodestar/state-transition"; import { sszTypesFor, } from "@lodestar/types"; import { fromHex, toHex, toRootHex } from "@lodestar/utils"; import { BlockType } from "../interface.js"; import { BlockProductionStep } from "../produceBlock/produceBlockBody.js"; import { isValidBlsToExecutionChangeForBlockInclusion } from "./utils.js"; export class OpPool { constructor() { /** Map of uniqueId(AttesterSlashing) -> AttesterSlashing */ this.attesterSlashings = new Map(); /** Map of to slash validator index -> ProposerSlashing */ this.proposerSlashings = new Map(); /** Map of to exit validator index -> SignedVoluntaryExit */ this.voluntaryExits = new Map(); /** Set of seen attester slashing indexes. No need to prune */ this.attesterSlashingIndexes = new Set(); /** Map of validator index -> SignedBLSToExecutionChange */ this.blsToExecutionChanges = new Map(); } // Getters for metrics get attesterSlashingsSize() { return this.attesterSlashings.size; } get proposerSlashingsSize() { return this.proposerSlashings.size; } get voluntaryExitsSize() { return this.voluntaryExits.size; } get blsToExecutionChangeSize() { return this.blsToExecutionChanges.size; } async fromPersisted(db) { const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = await Promise.all([ db.attesterSlashing.entries(), db.proposerSlashing.values(), db.voluntaryExit.values(), db.blsToExecutionChange.values(), ]); for (const attesterSlashing of attesterSlashings) { this.insertAttesterSlashing(ForkName.electra, attesterSlashing.value, attesterSlashing.key); } for (const proposerSlashing of proposerSlashings) { this.insertProposerSlashing(proposerSlashing); } for (const voluntaryExit of voluntaryExits) { this.insertVoluntaryExit(voluntaryExit); } for (const item of blsToExecutionChanges) { this.insertBlsToExecutionChange(item.data, item.preCapella); } } async toPersisted(db) { await Promise.all([ persistDiff(db.attesterSlashing, Array.from(this.attesterSlashings.entries()).map(([key, value]) => ({ key: fromHex(key), value: value.attesterSlashing, })), toHex), persistDiff(db.proposerSlashing, Array.from(this.proposerSlashings.entries()).map(([key, value]) => ({ key, value })), (index) => index), persistDiff(db.voluntaryExit, Array.from(this.voluntaryExits.entries()).map(([key, value]) => ({ key, value })), (index) => index), persistDiff(db.blsToExecutionChange, Array.from(this.blsToExecutionChanges.entries()).map(([key, value]) => ({ key, value })), (index) => index), ]); } // Use the opPool as seen cache for gossip validation /** Returns false if at least one intersecting index has not been seen yet */ hasSeenAttesterSlashing(intersectingIndices) { for (const validatorIndex of intersectingIndices) { if (!this.attesterSlashingIndexes.has(validatorIndex)) { return false; } } return true; } hasSeenVoluntaryExit(validatorIndex) { return this.voluntaryExits.has(validatorIndex); } hasSeenBlsToExecutionChange(validatorIndex) { return this.blsToExecutionChanges.has(validatorIndex); } hasSeenProposerSlashing(validatorIndex) { return this.proposerSlashings.has(validatorIndex); } /** Must be validated beforehand */ insertAttesterSlashing(fork, attesterSlashing, rootHash) { if (!rootHash) { rootHash = sszTypesFor(fork).AttesterSlashing.hashTreeRoot(attesterSlashing); } // TODO: Do once and cache attached to the AttesterSlashing object const intersectingIndices = getAttesterSlashableIndices(attesterSlashing); this.attesterSlashings.set(toRootHex(rootHash), { attesterSlashing, intersectingIndices, }); for (const index of intersectingIndices) { this.attesterSlashingIndexes.add(index); } } /** Must be validated beforehand */ insertProposerSlashing(proposerSlashing) { this.proposerSlashings.set(proposerSlashing.signedHeader1.message.proposerIndex, proposerSlashing); } /** Must be validated beforehand */ insertVoluntaryExit(voluntaryExit) { this.voluntaryExits.set(voluntaryExit.message.validatorIndex, voluntaryExit); } /** Must be validated beforehand */ insertBlsToExecutionChange(blsToExecutionChange, preCapella = false) { this.blsToExecutionChanges.set(blsToExecutionChange.message.validatorIndex, { data: blsToExecutionChange, preCapella, }); } /** * Get proposer and attester slashings and voluntary exits and bls to execution change for inclusion in a block. * * This function computes both types of slashings and exits, because attester slashings and exits may be invalidated by * slashings included earlier in the block. */ getSlashingsAndExits(state, blockType, metrics) { const { config } = state; const stateEpoch = computeEpochAtSlot(state.slot); const stateFork = config.getForkSeq(state.slot); const toBeSlashedIndices = new Set(); const proposerSlashings = []; const stepsMetrics = blockType === BlockType.Full ? metrics?.executionBlockProductionTimeSteps : metrics?.builderBlockProductionTimeSteps; const endProposerSlashing = stepsMetrics?.startTimer(); for (const proposerSlashing of this.proposerSlashings.values()) { const index = proposerSlashing.signedHeader1.message.proposerIndex; const validator = state.validators.getReadonly(index); if (!validator.slashed && validator.activationEpoch <= stateEpoch && stateEpoch < validator.withdrawableEpoch) { proposerSlashings.push(proposerSlashing); // Set of validators to be slashed, so we don't attempt to construct invalid attester slashings. toBeSlashedIndices.add(index); if (proposerSlashings.length >= MAX_PROPOSER_SLASHINGS) { break; } } } endProposerSlashing?.({ step: BlockProductionStep.proposerSlashing, }); const endAttesterSlashings = stepsMetrics?.startTimer(); const attesterSlashings = []; const maxAttesterSlashings = stateFork >= ForkSeq.electra ? MAX_ATTESTER_SLASHINGS_ELECTRA : MAX_ATTESTER_SLASHINGS; attesterSlashing: for (const attesterSlashing of this.attesterSlashings.values()) { /** Indices slashable in this attester slashing */ const slashableIndices = new Set(); for (let i = 0; i < attesterSlashing.intersectingIndices.length; i++) { const index = attesterSlashing.intersectingIndices[i]; // If we already have a slashing for this index, we can continue on to the next slashing if (toBeSlashedIndices.has(index)) { continue attesterSlashing; } const validator = state.validators.getReadonly(index); if (isSlashableAtEpoch(validator, stateEpoch)) { slashableIndices.add(index); } if (attesterSlashings.length >= maxAttesterSlashings) { break attesterSlashing; } } // If there were slashable indices in this slashing // Then include the slashing and count the slashable indices if (slashableIndices.size > 0) { attesterSlashings.push(attesterSlashing.attesterSlashing); for (const index of slashableIndices) { toBeSlashedIndices.add(index); } } } endAttesterSlashings?.({ step: BlockProductionStep.attesterSlashings, }); const endVoluntaryExits = stepsMetrics?.startTimer(); const voluntaryExits = []; for (const voluntaryExit of this.voluntaryExits.values()) { if (!toBeSlashedIndices.has(voluntaryExit.message.validatorIndex) && isValidVoluntaryExit(stateFork, state, voluntaryExit, false) && // Signature validation is skipped in `isValidVoluntaryExit(,,false)` since it was already validated in gossip // However we must make sure that the signature fork is the same, or it will become invalid if included through // a future fork. isVoluntaryExitSignatureIncludable(stateFork, config.getForkSeq(computeStartSlotAtEpoch(voluntaryExit.message.epoch)))) { voluntaryExits.push(voluntaryExit); if (voluntaryExits.length >= MAX_VOLUNTARY_EXITS) { break; } } } endVoluntaryExits?.({ step: BlockProductionStep.voluntaryExits, }); const endBlsToExecutionChanges = stepsMetrics?.startTimer(); const blsToExecutionChanges = []; for (const blsToExecutionChange of this.blsToExecutionChanges.values()) { if (isValidBlsToExecutionChangeForBlockInclusion(state, blsToExecutionChange.data)) { blsToExecutionChanges.push(blsToExecutionChange.data); if (blsToExecutionChanges.length >= MAX_BLS_TO_EXECUTION_CHANGES) { break; } } } endBlsToExecutionChanges?.({ step: BlockProductionStep.blsToExecutionChanges, }); return [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges]; } /** For beacon pool API */ getAllAttesterSlashings() { return Array.from(this.attesterSlashings.values()).map((attesterSlashings) => attesterSlashings.attesterSlashing); } /** For beacon pool API */ getAllProposerSlashings() { return Array.from(this.proposerSlashings.values()); } /** For beacon pool API */ getAllVoluntaryExits() { return Array.from(this.voluntaryExits.values()); } /** For beacon pool API */ getAllBlsToExecutionChanges() { return Array.from(this.blsToExecutionChanges.values()); } /** * Prune all types of transactions given the latest head state */ pruneAll(headBlock, headState) { this.pruneAttesterSlashings(headState); this.pruneProposerSlashings(headState); this.pruneVoluntaryExits(headState); this.pruneBlsToExecutionChanges(headBlock, headState); } /** * Prune attester slashings for all slashed or withdrawn validators. */ pruneAttesterSlashings(headState) { const finalizedEpoch = headState.finalizedCheckpoint.epoch; attesterSlashing: for (const [key, attesterSlashing] of this.attesterSlashings.entries()) { // Slashings that don't slash any validators can be dropped for (let i = 0; i < attesterSlashing.intersectingIndices.length; i++) { const index = attesterSlashing.intersectingIndices[i]; // Declare that a validator is still slashable if they have not exited prior // to the finalized epoch. // // We cannot check the `slashed` field since the `head` is not finalized and // a fork could un-slash someone. if (headState.validators.getReadonly(index).exitEpoch > finalizedEpoch) { continue attesterSlashing; } } // All intersecting indices are not slashable this.attesterSlashings.delete(key); } } /** * Prune proposer slashings for validators which are exited in the finalized epoch. */ pruneProposerSlashings(headState) { const finalizedEpoch = headState.finalizedCheckpoint.epoch; for (const [key, proposerSlashing] of this.proposerSlashings.entries()) { const index = proposerSlashing.signedHeader1.message.proposerIndex; if (headState.validators.getReadonly(index).exitEpoch <= finalizedEpoch) { this.proposerSlashings.delete(key); } } } /** * Call after finalizing * Prune if validator has already exited at or before the finalized checkpoint of the head. */ pruneVoluntaryExits(headState) { const { config } = headState; const headStateFork = config.getForkSeq(headState.slot); const finalizedEpoch = headState.finalizedCheckpoint.epoch; for (const [key, voluntaryExit] of this.voluntaryExits.entries()) { // VoluntaryExit messages signed in the previous fork become invalid and can never be included in any future // block, so just drop as the head state advances into the next fork. if (config.getForkSeq(computeStartSlotAtEpoch(voluntaryExit.message.epoch)) < headStateFork) { this.voluntaryExits.delete(key); } // TODO: Improve this simplistic condition if (voluntaryExit.message.epoch <= finalizedEpoch) { this.voluntaryExits.delete(key); } } } /** * Prune BLS to execution changes that have been applied to the state more than 1 block ago. * In the worse case where head block is reorged, the same BlsToExecutionChange message can be re-added * to opPool once gossipsub seen cache TTL passes. */ pruneBlsToExecutionChanges(headBlock, headState) { const { config } = headState; const recentBlsToExecutionChanges = config.getForkSeq(headBlock.message.slot) >= ForkSeq.capella ? headBlock.message.body.blsToExecutionChanges : []; const recentBlsToExecutionChangeIndexes = new Set(recentBlsToExecutionChanges.map((blsToExecutionChange) => blsToExecutionChange.message.validatorIndex)); for (const [key, blsToExecutionChange] of this.blsToExecutionChanges.entries()) { const { validatorIndex } = blsToExecutionChange.data.message; if (!recentBlsToExecutionChangeIndexes.has(validatorIndex)) { const validator = headState.validators.getReadonly(validatorIndex); if (validator.withdrawalCredentials[0] !== BLS_WITHDRAWAL_PREFIX) { this.blsToExecutionChanges.delete(key); } } } } } /** * Returns true if a pre-validated signature is still valid to be included in a specific block's fork */ function isVoluntaryExitSignatureIncludable(stateFork, voluntaryExitFork) { if (stateFork >= ForkSeq.deneb) { // Exists are perpetually valid https://eips.ethereum.org/EIPS/eip-7044 return true; } // Can only include exits from the current and previous fork return voluntaryExitFork === stateFork || voluntaryExitFork === stateFork - 1; } function isSlashableAtEpoch(validator, epoch) { return !validator.slashed && validator.activationEpoch <= epoch && epoch < validator.withdrawableEpoch; } /** * Persist target items `items` in `dbRepo` doing minimum put and delete writes. * Reads all keys in repository to compute the diff between current persisted data and target data. */ async function persistDiff(dbRepo, items, serializeKey) { const persistedKeys = await dbRepo.keys(); const itemsToPut = []; const keysToDelete = []; const persistedKeysSerialized = new Set(persistedKeys.map(serializeKey)); for (const item of items) { if (!persistedKeysSerialized.has(serializeKey(item.key))) { itemsToPut.push(item); } } const targetKeysSerialized = new Set(items.map((item) => serializeKey(item.key))); for (const persistedKey of persistedKeys) { if (!targetKeysSerialized.has(serializeKey(persistedKey))) { keysToDelete.push(persistedKey); } } if (itemsToPut.length > 0) await dbRepo.batchPut(itemsToPut); if (keysToDelete.length > 0) await dbRepo.batchDelete(keysToDelete); } //# sourceMappingURL=opPool.js.map