@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
170 lines (154 loc) • 6.26 kB
text/typescript
import {TopicValidatorResult} from "@libp2p/gossipsub";
import {ChainForkConfig} from "@lodestar/config";
import {Logger} from "@lodestar/utils";
import {AttestationError, GossipAction, GossipActionError} from "../../chain/errors/index.js";
import {Metrics} from "../../metrics/index.js";
import {
BatchGossipHandlerFn,
GossipHandlerFn,
GossipHandlers,
GossipMessageInfo,
GossipValidatorBatchFn,
GossipValidatorFn,
} from "../gossip/interface.js";
import {prettyPrintPeerIdStr} from "../util.js";
export type ValidatorFnModules = {
config: ChainForkConfig;
logger: Logger;
metrics: Metrics | null;
};
/**
* Similar to getGossipValidatorFn but return a function to accept a batch of beacon_attestation messages
* with the same attestation data
*/
export function getGossipValidatorBatchFn(
gossipHandlers: GossipHandlers,
modules: ValidatorFnModules
): GossipValidatorBatchFn {
const {logger, metrics} = modules;
return async function gossipValidatorBatchFn(messageInfos: GossipMessageInfo[]) {
// all messageInfos have same topic type
const type = messageInfos[0].topic.type;
try {
const results = await (gossipHandlers[type] as BatchGossipHandlerFn)(
messageInfos.map((messageInfo) => ({
gossipData: {
serializedData: messageInfo.msg.data,
msgSlot: messageInfo.msgSlot,
indexed: messageInfo.indexed,
},
topic: messageInfo.topic,
peerIdStr: messageInfo.propagationSource,
seenTimestampSec: messageInfo.seenTimestampSec,
}))
);
return results.map((e, i) => {
if (e == null) {
return TopicValidatorResult.Accept;
}
const {clientAgent, clientVersion, propagationSource} = messageInfos[i];
if (!(e instanceof AttestationError)) {
logger.debug(
`Gossip batch validation ${type} threw a non-AttestationError`,
{peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion},
e as Error
);
metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type});
return TopicValidatorResult.Ignore;
}
switch (e.action) {
case GossipAction.IGNORE:
metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type});
// only beacon_attestation topic is validated in batch
metrics?.networkProcessor.gossipAttestationIgnoreByReason.inc({reason: e.type.code});
return TopicValidatorResult.Ignore;
case GossipAction.REJECT:
metrics?.networkProcessor.gossipValidationReject.inc({topic: type});
// only beacon_attestation topic is validated in batch
metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: e.type.code});
logger.debug(
`Gossip validation ${type} rejected`,
{peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion},
e
);
return TopicValidatorResult.Reject;
}
});
} catch (e) {
// Don't expect error here
logger.debug(`Gossip batch validation ${type} threw an error`, {}, e as Error);
const results: TopicValidatorResult[] = [];
for (let i = 0; i < messageInfos.length; i++) {
results.push(TopicValidatorResult.Ignore);
}
return results;
}
};
}
/**
* Returns a GossipSub validator function from a GossipHandlerFn. GossipHandlerFn may throw GossipActionError if one
* or more validation conditions from the consensus-specs#p2p-interface are not satisfied.
*
* This function receives a string topic and a binary message `InMessage` and deserializes both using caches.
* - The topic string should be known in advance and pre-computed
* - The message.data should already by uncompressed when computing its msgID
*
* All logging and metrics associated with gossip object validation should happen in this function. We want to know
* - In debug logs what objects are we processing, the result and some succint metadata
* - In metrics what's the throughput and ratio of accept/ignore/reject per type
*
* @see getGossipHandlers for reasoning on why GossipHandlerFn are used for gossip validation.
*/
export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: ValidatorFnModules): GossipValidatorFn {
const {logger, metrics} = modules;
return async function gossipValidatorFn({
topic,
msg,
propagationSource,
clientAgent,
clientVersion,
seenTimestampSec,
msgSlot,
}) {
const type = topic.type;
try {
await (gossipHandlers[type] as GossipHandlerFn)({
gossipData: {serializedData: msg.data, msgSlot},
topic,
peerIdStr: propagationSource,
seenTimestampSec,
});
metrics?.networkProcessor.gossipValidationAccept.inc({topic: type});
return TopicValidatorResult.Accept;
} catch (e) {
if (!(e instanceof GossipActionError)) {
// not deserve to log error here, it looks too dangerous to users
logger.debug(
`Gossip validation ${type} threw a non-GossipActionError`,
{peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion},
e as Error
);
return TopicValidatorResult.Ignore;
}
// Metrics on specific error reason
// Note: LodestarError.code are bounded pre-declared error messages, not from arbitrary error.message
metrics?.networkProcessor.gossipValidationError.inc({
topic: type,
error: (e as GossipActionError<{code: string}>).type.code,
});
switch (e.action) {
case GossipAction.IGNORE:
metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type});
return TopicValidatorResult.Ignore;
case GossipAction.REJECT:
metrics?.networkProcessor.gossipValidationReject.inc({topic: type});
logger.debug(
`Gossip validation ${type} rejected`,
{peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion},
e
);
return TopicValidatorResult.Reject;
}
}
};
}