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 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);
/**
 * 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,
        });
        // 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