@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
401 lines (346 loc) • 17.3 kB
text/typescript
import {routes} from "@lodestar/api";
import {ApplicationMethods} from "@lodestar/api/server";
import {
ForkPostElectra,
ForkPreElectra,
SYNC_COMMITTEE_SUBNET_SIZE,
isForkPostElectra,
isForkPostGloas,
} from "@lodestar/params";
import {isStatePostAltair} from "@lodestar/state-transition";
import {Attestation, Epoch, SingleAttestation, isElectraAttestation, ssz, sszTypesFor} from "@lodestar/types";
import {toRootHex} from "@lodestar/utils";
import {
AttestationError,
AttestationErrorCode,
GossipAction,
PayloadAttestationError,
PayloadAttestationErrorCode,
SyncCommitteeError,
} from "../../../../chain/errors/index.js";
import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js";
import {validateApiPayloadAttestationMessage} from "../../../../chain/validation/payloadAttestationMessage.js";
import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js";
import {ApiError, FailureList, IndexedError} from "../../errors.js";
import {ApiModules} from "../../types.js";
export function getBeaconPoolApi({
chain,
logger,
metrics,
network,
}: Pick<ApiModules, "chain" | "logger" | "metrics" | "network">): ApplicationMethods<routes.beacon.pool.Endpoints> {
return {
async getPoolAttestations({slot, committeeIndex}) {
// Already filtered by slot
let attestations: Attestation[] = chain.aggregatedAttestationPool.getAll(slot);
const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot);
if (isForkPostElectra(fork)) {
throw new ApiError(
400,
`Use getPoolAttestationsV2 to retrieve pool attestations for post-electra fork=${fork}`
);
}
if (committeeIndex !== undefined) {
attestations = attestations.filter((attestation) => committeeIndex === attestation.data.index);
}
return {data: attestations};
},
async getPoolAttestationsV2({slot, committeeIndex}) {
// Already filtered by slot
let attestations = chain.aggregatedAttestationPool.getAll(slot);
const fork = chain.config.getForkName(slot ?? attestations[0]?.data.slot ?? chain.clock.currentSlot);
const isPostElectra = isForkPostElectra(fork);
attestations = attestations.filter((attestation) =>
isPostElectra ? isElectraAttestation(attestation) : !isElectraAttestation(attestation)
);
if (committeeIndex !== undefined) {
attestations = attestations.filter((attestation) => committeeIndex === attestation.data.index);
}
return {data: attestations, meta: {version: fork}};
},
async getPoolPayloadAttestations({slot}) {
const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot);
if (!isForkPostGloas(fork)) {
throw new ApiError(400, `Payload attestation pool is not supported before Gloas fork=${fork}`);
}
return {data: chain.payloadAttestationPool.getAll(slot), meta: {version: fork}};
},
async getPoolAttesterSlashings() {
const fork = chain.config.getForkName(chain.clock.currentSlot);
if (isForkPostElectra(fork)) {
throw new ApiError(
400,
`Use getPoolAttesterSlashingsV2 to retrieve pool attester slashings for post-electra fork=${fork}`
);
}
return {data: chain.opPool.getAllAttesterSlashings()};
},
async getPoolAttesterSlashingsV2() {
const fork = chain.config.getForkName(chain.clock.currentSlot);
return {data: chain.opPool.getAllAttesterSlashings(), meta: {version: fork}};
},
async getPoolProposerSlashings() {
return {data: chain.opPool.getAllProposerSlashings()};
},
async getPoolVoluntaryExits() {
return {data: chain.opPool.getAllVoluntaryExits()};
},
async getPoolBLSToExecutionChanges() {
return {data: chain.opPool.getAllBlsToExecutionChanges().map(({data}) => data)};
},
async submitPoolAttestations({signedAttestations}) {
await this.submitPoolAttestationsV2({signedAttestations});
},
async submitPoolAttestationsV2({signedAttestations}) {
const fork = chain.config.getForkName(chain.clock.currentSlot);
const seenTimestampSec = Date.now() / 1000;
const failures: FailureList = [];
// api attestation has high priority, we allow them to be added to pool even when it's late
// this is to prevent "No aggregated attestation for slot" issue
// see https://github.com/ChainSafe/lodestar/issues/7548
const priority = true;
await Promise.all(
signedAttestations.map(async (attestation, i) => {
try {
const validateFn = () => validateApiAttestation(fork, chain, {attestation, serializedData: null});
const {slot, beaconBlockRoot} = attestation.data;
// when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node
// and the block hasn't been in our forkchoice since we haven't seen / processing that block
// see https://github.com/ChainSafe/lodestar/issues/5098
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, validatorCommitteeIndex, committeeSize} =
await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot);
if (network.shouldAggregate(subnet, slot)) {
const insertOutcome = chain.attestationPool.add(
committeeIndex,
attestation,
attDataRootHex,
validatorCommitteeIndex,
committeeSize,
priority
);
metrics?.opPool.attestationPool.apiInsertOutcome.inc({insertOutcome});
}
if (isForkPostElectra(fork)) {
chain.emitter.emit(
routes.events.EventType.singleAttestation,
attestation as SingleAttestation<ForkPostElectra>
);
} else {
chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation<ForkPreElectra>);
chain.emitter.emit(
routes.events.EventType.singleAttestation,
toElectraSingleAttestation(
attestation as SingleAttestation<ForkPreElectra>,
indexedAttestation.attestingIndices[0]
)
);
}
const sentPeers = await network.publishBeaconAttestation(attestation, subnet);
chain.validatorMonitor?.onPoolSubmitUnaggregatedAttestation(
seenTimestampSec,
indexedAttestation,
subnet,
sentPeers
);
} catch (e) {
const logCtx = {slot: attestation.data.slot, index: attestation.data.index};
if (e instanceof AttestationError && e.type.code === AttestationErrorCode.ATTESTATION_ALREADY_KNOWN) {
logger.debug("Ignoring known attestation", logCtx);
// Attestations might already be published by another node as part of a fallback setup or DVT cluster
// and can reach our node by gossip before the api. The error can be ignored and should not result in a 500 response.
return;
}
failures.push({index: i, message: (e as Error).message});
logger.verbose(`Error on submitPoolAttestations [${i}]`, logCtx, e as Error);
if (e instanceof AttestationError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(sszTypesFor(fork).SingleAttestation, attestation, "api_reject");
}
}
})
);
if (failures.length > 0) {
throw new IndexedError("Error processing attestations", failures);
}
},
async submitPoolAttesterSlashings({attesterSlashing}) {
await this.submitPoolAttesterSlashingsV2({attesterSlashing});
},
async submitPoolAttesterSlashingsV2({attesterSlashing}) {
await validateApiAttesterSlashing(chain, attesterSlashing);
const fork = chain.config.getForkName(Number(attesterSlashing.attestation1.data.slot));
chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
await network.publishAttesterSlashing(attesterSlashing);
},
async submitPoolProposerSlashings({proposerSlashing}) {
await validateApiProposerSlashing(chain, proposerSlashing);
chain.opPool.insertProposerSlashing(proposerSlashing);
await network.publishProposerSlashing(proposerSlashing);
},
async submitPoolVoluntaryExit({signedVoluntaryExit}) {
await validateApiVoluntaryExit(chain, signedVoluntaryExit);
chain.opPool.insertVoluntaryExit(signedVoluntaryExit);
chain.emitter.emit(routes.events.EventType.voluntaryExit, signedVoluntaryExit);
await network.publishVoluntaryExit(signedVoluntaryExit);
},
async submitPoolBLSToExecutionChange({blsToExecutionChanges}) {
const failures: FailureList = [];
await Promise.all(
blsToExecutionChanges.map(async (blsToExecutionChange, i) => {
try {
// Ignore even if the change exists and reprocess
await validateApiBlsToExecutionChange(chain, blsToExecutionChange);
const preCapella = chain.clock.currentEpoch < chain.config.CAPELLA_FORK_EPOCH;
chain.opPool.insertBlsToExecutionChange(blsToExecutionChange, preCapella);
chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange);
if (!preCapella) {
await network.publishBlsToExecutionChange(blsToExecutionChange);
}
} catch (e) {
failures.push({index: i, message: (e as Error).message});
logger.verbose(
`Error on submitPoolBLSToExecutionChange [${i}]`,
{validatorIndex: blsToExecutionChange.message.validatorIndex},
e as Error
);
}
})
);
if (failures.length > 0) {
throw new IndexedError("Error processing BLS to execution changes", failures);
}
},
async submitPayloadAttestationMessages({payloadAttestationMessages}) {
const failures: FailureList = [];
await Promise.all(
payloadAttestationMessages.map(async (payloadAttestationMessage, i) => {
try {
const validateFn = () => validateApiPayloadAttestationMessage(chain, payloadAttestationMessage);
const {slot, beaconBlockRoot} = payloadAttestationMessage.data;
const {attDataRootHex, validatorCommitteeIndex} = await validateGossipFnRetryUnknownRoot(
validateFn,
network,
chain,
slot,
beaconBlockRoot
);
const insertOutcome = chain.payloadAttestationPool.add(
payloadAttestationMessage,
attDataRootHex,
validatorCommitteeIndex
);
metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome});
chain.forkChoice.notifyPtcMessages(
toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
[validatorCommitteeIndex],
payloadAttestationMessage.data.payloadPresent
);
await network.publishPayloadAttestationMessage(payloadAttestationMessage);
} catch (e) {
const logCtx = {
slot: payloadAttestationMessage.data.slot,
validatorIndex: payloadAttestationMessage.validatorIndex,
beaconBlockRoot: toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
};
if (
e instanceof PayloadAttestationError &&
e.type.code === PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN
) {
logger.debug("Ignoring known payload attestation message", logCtx);
return;
}
failures.push({index: i, message: (e as Error).message});
logger.verbose(`Error on submitPayloadAttestationMessages [${i}]`, logCtx, e as Error);
if (e instanceof PayloadAttestationError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(
ssz.gloas.PayloadAttestationMessage,
payloadAttestationMessage,
"api_reject"
);
}
}
})
);
if (failures.length > 0) {
throw new IndexedError("Error processing payload attestation messages", failures);
}
},
/**
* POST `/eth/v1/beacon/pool/sync_committees`
*
* Submits sync committee signature objects to the node.
* Sync committee signatures are not present in phase0, but are required for Altair networks.
* If a sync committee signature is validated successfully the node MUST publish that sync committee signature on all applicable subnets.
* If one or more sync committee signatures fail validation the node MUST return a 400 error with details of which sync committee signatures have failed, and why.
*
* https://github.com/ethereum/beacon-APIs/pull/135
*/
async submitPoolSyncCommitteeSignatures({signatures}) {
// Fetch states for all slots of the `signatures`
const slots = new Set<Epoch>();
for (const signature of signatures) {
slots.add(signature.slot);
}
// TODO: Fetch states at signature slots
const state = chain.getHeadState();
if (!isStatePostAltair(state)) {
throw new ApiError(400, "Sync committee pool is not supported before Altair");
}
const failures: FailureList = [];
await Promise.all(
signatures.map(async (signature, i) => {
try {
const synCommittee = state.getIndexedSyncCommittee(signature.slot);
const indexesInCommittee = synCommittee.validatorIndexMap.get(signature.validatorIndex);
if (indexesInCommittee === undefined || indexesInCommittee.length === 0) {
return; // Not a sync committee member
}
// Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node.
// Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us.
await validateApiSyncCommittee(chain, state, signature);
// The same validator can appear multiple times in the sync committee. It can appear multiple times per
// subnet even. First compute on which subnet the signature must be broadcasted to.
const subnets: number[] = [];
// same to api attestation, we allow api SyncCommittee to be added to pool even when it's late
// see https://github.com/ChainSafe/lodestar/issues/7548
const priority = true;
for (const indexInCommittee of indexesInCommittee) {
// Sync committee subnet members are just sequential in the order they appear in SyncCommitteeIndexes array
const subnet = Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE);
const indexInSubcommittee = indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE;
chain.syncCommitteeMessagePool.add(subnet, signature, indexInSubcommittee, priority);
// Cheap de-duplication code to avoid using a Set. indexesInCommittee is always sorted
if (subnets.length === 0 || subnets.at(-1) !== subnet) {
subnets.push(subnet);
}
}
// TODO: Broadcast at once to all topics
await Promise.all(subnets.map(async (subnet) => network.publishSyncCommitteeSignature(signature, subnet)));
} catch (e) {
// TODO: gossipsub should allow publishing same message to different topics
// https://github.com/ChainSafe/js-libp2p-gossipsub/issues/272
if ((e as Error).message === "PublishError.Duplicate") {
return;
}
failures.push({index: i, message: (e as Error).message});
logger.verbose(
`Error on submitPoolSyncCommitteeSignatures [${i}]`,
{slot: signature.slot, validatorIndex: signature.validatorIndex},
e as Error
);
if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, signature, "api_reject");
}
}
})
);
if (failures.length > 0) {
throw new IndexedError("Error processing sync committee signatures", failures);
}
},
};
}