UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

296 lines • 14.8 kB
import { defaultTopicScoreParams, } from "@libp2p/gossipsub/score"; import { ATTESTATION_SUBNET_COUNT, PTC_SIZE, SLOTS_PER_EPOCH, TARGET_AGGREGATORS_PER_COMMITTEE } from "@lodestar/params"; import { computeCommitteeCount } from "@lodestar/state-transition"; import { getActiveForkBoundaries } from "../forks.js"; import { GossipType } from "./interface.js"; import { stringifyGossipTopic } from "./topic.js"; export const GOSSIP_D = 8; export const GOSSIP_D_LOW = 6; export const GOSSIP_D_HIGH = 12; const MAX_IN_MESH_SCORE = 10.0; const MAX_FIRST_MESSAGE_DELIVERIES_SCORE = 40.0; const BEACON_BLOCK_WEIGHT = 0.5; const BEACON_AGGREGATE_PROOF_WEIGHT = 0.5; const VOLUNTARY_EXIT_WEIGHT = 0.05; const PROPOSER_SLASHING_WEIGHT = 0.05; const ATTESTER_SLASHING_WEIGHT = 0.05; const BLS_TO_EXECUTION_CHANGE_WEIGHT = 0.05; const EXECUTION_PAYLOAD_WEIGHT = 0.5; const PAYLOAD_ATTESTATION_WEIGHT = 0.05; const EXECUTION_PAYLOAD_BID_WEIGHT = 0.05; const PROPOSER_PREFERENCES_WEIGHT = 0.05; const beaconAttestationSubnetWeight = 1 / ATTESTATION_SUBNET_COUNT; const maxPositiveScore = (MAX_IN_MESH_SCORE + MAX_FIRST_MESSAGE_DELIVERIES_SCORE) * (BEACON_BLOCK_WEIGHT + +BEACON_AGGREGATE_PROOF_WEIGHT + beaconAttestationSubnetWeight * ATTESTATION_SUBNET_COUNT + VOLUNTARY_EXIT_WEIGHT + PROPOSER_SLASHING_WEIGHT + ATTESTER_SLASHING_WEIGHT + BLS_TO_EXECUTION_CHANGE_WEIGHT + EXECUTION_PAYLOAD_WEIGHT + PAYLOAD_ATTESTATION_WEIGHT + EXECUTION_PAYLOAD_BID_WEIGHT + PROPOSER_PREFERENCES_WEIGHT); /** * The following params is implemented by Lighthouse at * https://github.com/sigp/lighthouse/blob/b0ac3464ca5fb1e9d75060b56c83bfaf990a3d25/beacon_node/eth2_libp2p/src/behaviour/gossipsub_scoring_parameters.rs#L83 */ export const gossipScoreThresholds = { gossipThreshold: -4000, publishThreshold: -8000, graylistThreshold: -16000, acceptPXThreshold: 100, opportunisticGraftThreshold: 5, }; /** * Peer may sometimes has negative gossipsub score and we give it time to recover, however gossipsub score comes below this we need to take into account. * Given gossipsubThresold = -4000, it's comfortable to only ignore negative score gossip peer score > -1000 */ export const negativeGossipScoreIgnoreThreshold = -1000; /** * Explanation of each param https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#peer-scoring */ export function computeGossipPeerScoreParams({ config, eth2Context, }) { const decayIntervalMs = config.SLOT_DURATION_MS; const decayToZero = 0.01; const epochDurationMs = config.SLOT_DURATION_MS * SLOTS_PER_EPOCH; const slotDurationMs = config.SLOT_DURATION_MS; const scoreParameterDecayFn = (decayTimeMs) => { return scoreParameterDecayWithBase(decayTimeMs, decayIntervalMs, decayToZero); }; const behaviourPenaltyDecay = scoreParameterDecayFn(epochDurationMs * 10); const behaviourPenaltyThreshold = 6; const targetValue = decayConvergence(behaviourPenaltyDecay, 10 / SLOTS_PER_EPOCH) - behaviourPenaltyThreshold; const topicScoreCap = maxPositiveScore * 0.5; const params = { topics: getAllTopicsScoreParams(config, eth2Context, { epochDurationMs, slotDurationMs, scoreParameterDecayFn, }), decayInterval: decayIntervalMs, decayToZero, // time to remember counters for a disconnected peer, should be in ms retainScore: epochDurationMs * 100, appSpecificWeight: 1, IPColocationFactorThreshold: 3, // js-gossipsub doesn't have behaviourPenaltiesThreshold behaviourPenaltyDecay, behaviourPenaltyWeight: gossipScoreThresholds.gossipThreshold / (targetValue * targetValue), behaviourPenaltyThreshold, topicScoreCap, IPColocationFactorWeight: -1 * topicScoreCap, }; return params; } function getAllTopicsScoreParams(config, eth2Context, precomputedParams) { const { epochDurationMs, slotDurationMs } = precomputedParams; const epoch = eth2Context.currentEpoch; const topicsParams = {}; const boundaries = getActiveForkBoundaries(config, epoch); const beaconAttestationSubnetWeight = 1 / ATTESTATION_SUBNET_COUNT; for (const boundary of boundaries) { //first all fixed topics topicsParams[stringifyGossipTopic(config, { type: GossipType.voluntary_exit, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: VOLUNTARY_EXIT_WEIGHT, expectedMessageRate: 4 / SLOTS_PER_EPOCH, firstMessageDecayTime: epochDurationMs * 100, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.bls_to_execution_change, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: BLS_TO_EXECUTION_CHANGE_WEIGHT, expectedMessageRate: 4 / SLOTS_PER_EPOCH, firstMessageDecayTime: epochDurationMs * 100, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.attester_slashing, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: ATTESTER_SLASHING_WEIGHT, expectedMessageRate: 1 / 5 / SLOTS_PER_EPOCH, firstMessageDecayTime: epochDurationMs * 100, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.proposer_slashing, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: PROPOSER_SLASHING_WEIGHT, expectedMessageRate: 1 / 5 / SLOTS_PER_EPOCH, firstMessageDecayTime: epochDurationMs * 100, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.payload_attestation_message, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: PAYLOAD_ATTESTATION_WEIGHT, expectedMessageRate: PTC_SIZE, firstMessageDecayTime: epochDurationMs * 100, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.execution_payload_bid, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: EXECUTION_PAYLOAD_BID_WEIGHT, expectedMessageRate: 1024, // TODO GLOAS: Need an estimate for this firstMessageDecayTime: epochDurationMs * 100, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.proposer_preferences, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: PROPOSER_PREFERENCES_WEIGHT, // Upper bound ~64 messages per epoch: one per proposer across the current + next epoch. expectedMessageRate: 64 / SLOTS_PER_EPOCH, firstMessageDecayTime: epochDurationMs * 100, }); // other topics topicsParams[stringifyGossipTopic(config, { type: GossipType.beacon_block, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: BEACON_BLOCK_WEIGHT, expectedMessageRate: 1, firstMessageDecayTime: epochDurationMs * 20, meshMessageInfo: { decaySlots: SLOTS_PER_EPOCH * 5, capFactor: 3, activationWindow: epochDurationMs, currentSlot: eth2Context.currentSlot, }, }); topicsParams[stringifyGossipTopic(config, { type: GossipType.execution_payload, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: EXECUTION_PAYLOAD_WEIGHT, expectedMessageRate: 1, firstMessageDecayTime: epochDurationMs * 20, meshMessageInfo: { decaySlots: SLOTS_PER_EPOCH * 5, capFactor: 3, activationWindow: epochDurationMs, currentSlot: eth2Context.currentSlot, }, }); const activeValidatorCount = eth2Context.activeValidatorCount; const { aggregatorsPerslot, committeesPerSlot } = expectedAggregatorCountPerSlot(activeValidatorCount); // Checks to prevent unwanted errors in gossipsub // Error: invalid score parameters for topic /eth2/4a26c58b/beacon_attestation_0/ssz_snappy: invalid FirstMessageDeliveriesCap; must be positive // at Object.validatePeerScoreParams (/usr/app/node_modules/libp2p-gossipsub/src/score/peer-score-params.js:62:27) if (activeValidatorCount === 0) throw Error("activeValidatorCount === 0"); if (aggregatorsPerslot === 0) throw Error("aggregatorsPerslot === 0"); const multipleBurstsPerSubnetPerEpoch = committeesPerSlot >= (2 * ATTESTATION_SUBNET_COUNT) / SLOTS_PER_EPOCH; topicsParams[stringifyGossipTopic(config, { type: GossipType.beacon_aggregate_and_proof, boundary, })] = getTopicScoreParams(config, precomputedParams, { topicWeight: BEACON_AGGREGATE_PROOF_WEIGHT, expectedMessageRate: aggregatorsPerslot, firstMessageDecayTime: epochDurationMs, meshMessageInfo: { decaySlots: SLOTS_PER_EPOCH * 2, capFactor: 4, activationWindow: epochDurationMs, currentSlot: eth2Context.currentSlot, }, }); const beaconAttestationParams = getTopicScoreParams(config, precomputedParams, { topicWeight: beaconAttestationSubnetWeight, expectedMessageRate: activeValidatorCount / ATTESTATION_SUBNET_COUNT / SLOTS_PER_EPOCH, firstMessageDecayTime: multipleBurstsPerSubnetPerEpoch ? epochDurationMs : epochDurationMs * 4, meshMessageInfo: { decaySlots: multipleBurstsPerSubnetPerEpoch ? SLOTS_PER_EPOCH * 4 : SLOTS_PER_EPOCH * 16, capFactor: 16, activationWindow: multipleBurstsPerSubnetPerEpoch ? slotDurationMs * (SLOTS_PER_EPOCH / 2 + 1) : epochDurationMs, currentSlot: eth2Context.currentSlot, }, }); for (let subnet = 0; subnet < ATTESTATION_SUBNET_COUNT; subnet++) { const topicStr = stringifyGossipTopic(config, { type: GossipType.beacon_attestation, subnet, boundary, }); topicsParams[topicStr] = beaconAttestationParams; } } return topicsParams; } function getTopicScoreParams(config, { epochDurationMs, slotDurationMs, scoreParameterDecayFn }, { topicWeight, expectedMessageRate, firstMessageDecayTime, meshMessageInfo }) { const params = { ...defaultTopicScoreParams }; params.topicWeight = topicWeight; params.timeInMeshQuantum = slotDurationMs; params.timeInMeshCap = 3600 / (params.timeInMeshQuantum / 1000); params.timeInMeshWeight = 10 / params.timeInMeshCap; params.firstMessageDeliveriesDecay = scoreParameterDecayFn(firstMessageDecayTime); params.firstMessageDeliveriesCap = decayConvergence(params.firstMessageDeliveriesDecay, (2 * expectedMessageRate) / GOSSIP_D); params.firstMessageDeliveriesWeight = 40 / params.firstMessageDeliveriesCap; if (meshMessageInfo) { const { decaySlots, capFactor, activationWindow, currentSlot } = meshMessageInfo; const decayTimeMs = config.SLOT_DURATION_MS * decaySlots; params.meshMessageDeliveriesDecay = scoreParameterDecayFn(decayTimeMs); params.meshMessageDeliveriesThreshold = threshold(params.meshMessageDeliveriesDecay, expectedMessageRate / 50); params.meshMessageDeliveriesCap = Math.max(capFactor * params.meshMessageDeliveriesThreshold, 2); params.meshMessageDeliveriesActivation = activationWindow; // the default in gossipsub is 2s is not enough since lodestar suffers from I/O lag params.meshMessageDeliveriesWindow = 12 * 1000; // 12s params.meshFailurePenaltyDecay = params.meshMessageDeliveriesDecay; params.meshMessageDeliveriesWeight = (-1 * maxPositiveScore) / (params.topicWeight * Math.pow(params.meshMessageDeliveriesThreshold, 2)); params.meshFailurePenaltyWeight = params.meshMessageDeliveriesWeight; if (decaySlots >= currentSlot) { params.meshMessageDeliveriesThreshold = 0; params.meshMessageDeliveriesWeight = 0; } } else { params.meshMessageDeliveriesWeight = 0; params.meshMessageDeliveriesThreshold = 0; params.meshMessageDeliveriesDecay = 0; params.meshMessageDeliveriesCap = 0; params.meshMessageDeliveriesWindow = 0; params.meshMessageDeliveriesActivation = 0; params.meshFailurePenaltyDecay = 0; params.meshFailurePenaltyWeight = 0; } params.invalidMessageDeliveriesWeight = (-1 * maxPositiveScore) / params.topicWeight; params.invalidMessageDeliveriesDecay = scoreParameterDecayFn(epochDurationMs * 50); return params; } function scoreParameterDecayWithBase(decayTimeMs, decayIntervalMs, decayToZero) { const ticks = decayTimeMs / decayIntervalMs; return Math.pow(decayToZero, 1 / ticks); } function expectedAggregatorCountPerSlot(activeValidatorCount) { const committeesPerSlot = computeCommitteeCount(activeValidatorCount); const committeesPerEpoch = committeesPerSlot * SLOTS_PER_EPOCH; const smallerCommitteeSize = Math.floor(activeValidatorCount / committeesPerEpoch); const largerCommiteeeSize = smallerCommitteeSize + 1; const largeCommitteesPerEpoch = activeValidatorCount - smallerCommitteeSize * committeesPerEpoch; const smallCommiteesPerEpoch = committeesPerEpoch - largeCommitteesPerEpoch; const moduloSmaller = Math.max(1, Math.floor(smallerCommitteeSize / TARGET_AGGREGATORS_PER_COMMITTEE)); const moduloLarger = Math.max(1, Math.floor((smallerCommitteeSize + 1) / TARGET_AGGREGATORS_PER_COMMITTEE)); const smallCommitteeAggregatorPerEpoch = Math.floor((smallerCommitteeSize / moduloSmaller) * smallCommiteesPerEpoch); const largeCommitteeAggregatorPerEpoch = Math.floor((largerCommiteeeSize / moduloLarger) * largeCommitteesPerEpoch); return { aggregatorsPerslot: Math.max(1, Math.floor((smallCommitteeAggregatorPerEpoch + largeCommitteeAggregatorPerEpoch) / SLOTS_PER_EPOCH)), committeesPerSlot, }; } function threshold(decay, rate) { return decayConvergence(decay, rate) * decay; } function decayConvergence(decay, rate) { return rate / (1 - decay); } //# sourceMappingURL=scoringParameters.js.map