UNPKG

@symbioticfi/relay-stats-ts

Version:

TypeScript library for deriving validator sets from Symbiotic network contracts

134 lines 5.76 kB
import { encodeAbiParameters, keccak256 } from 'viem'; import { sszTreeRoot } from './encoding.js'; import { SSZ_MAX_VALIDATORS, SSZ_MAX_VAULTS } from './constants.js'; export const totalActiveVotingPower = (validatorSet) => validatorSet.validators.reduce((sum, validator) => sum + (validator.isActive ? validator.votingPower : 0n), 0n); export const createValidatorSetHeader = (validatorSet) => ({ version: validatorSet.version, requiredKeyTag: validatorSet.requiredKeyTag, epoch: validatorSet.epoch, captureTimestamp: validatorSet.captureTimestamp, quorumThreshold: validatorSet.quorumThreshold, totalVotingPower: totalActiveVotingPower(validatorSet), validatorsSszMRoot: sszTreeRoot(validatorSet), }); export const encodeValidatorSetHeader = (header) => encodeAbiParameters([ { name: 'version', type: 'uint8' }, { name: 'requiredKeyTag', type: 'uint8' }, { name: 'epoch', type: 'uint48' }, { name: 'captureTimestamp', type: 'uint48' }, { name: 'quorumThreshold', type: 'uint256' }, { name: 'totalVotingPower', type: 'uint256' }, { name: 'validatorsSszMRoot', type: 'bytes32' }, ], [ header.version, header.requiredKeyTag, header.epoch, header.captureTimestamp, header.quorumThreshold, header.totalVotingPower, header.validatorsSszMRoot, ]); export const hashValidatorSetHeader = (header) => keccak256(encodeValidatorSetHeader(header)); export const hashValidatorSet = (validatorSet) => hashValidatorSetHeader(createValidatorSetHeader(validatorSet)); const limitAndSortVaults = (validator) => { if (validator.vaults.length <= SSZ_MAX_VAULTS) { validator.vaults.sort((a, b) => a.vault.toLowerCase().localeCompare(b.vault.toLowerCase())); return; } validator.vaults.sort((left, right) => { const diff = right.votingPower - left.votingPower; if (diff !== 0n) { return diff > 0n ? 1 : -1; } return left.vault.toLowerCase().localeCompare(right.vault.toLowerCase()); }); validator.vaults = validator.vaults.slice(0, SSZ_MAX_VAULTS); validator.votingPower = validator.vaults.reduce((sum, vault) => sum + vault.votingPower, 0n); validator.vaults.sort((a, b) => a.vault.toLowerCase().localeCompare(b.vault.toLowerCase())); }; const applyValidatorKeys = (validators, keys) => { for (const keyRecord of keys) { const operatorAddr = keyRecord.operator.toLowerCase(); const validator = validators.get(operatorAddr); if (validator) { validator.keys = keyRecord.keys; } } }; const markValidatorsActive = (config, validators) => { let totalActive = 0; for (const validator of validators) { if (validator.votingPower < config.minInclusionVotingPower) { break; } if (validator.keys.length === 0) { continue; } totalActive++; validator.isActive = true; if (config.maxVotingPower !== 0n && validator.votingPower > config.maxVotingPower) { validator.votingPower = config.maxVotingPower; } if (config.maxValidatorsCount !== 0n && totalActive >= Number(config.maxValidatorsCount)) { break; } } }; export const composeValidators = (config, votingPowers, operatorKeys) => { const validators = new Map(); for (const chainVotingPower of votingPowers) { for (const votingPower of chainVotingPower.votingPowers) { const operatorAddr = votingPower.operator.toLowerCase(); if (!validators.has(operatorAddr)) { validators.set(operatorAddr, { operator: votingPower.operator, votingPower: 0n, isActive: false, keys: [], vaults: [], }); } const validator = validators.get(operatorAddr); for (const vault of votingPower.vaults) { validator.votingPower += vault.votingPower; validator.vaults.push({ vault: vault.vault, votingPower: vault.votingPower, chainId: chainVotingPower.chainId, }); } } } for (const validator of validators.values()) { limitAndSortVaults(validator); } applyValidatorKeys(validators, operatorKeys); let mappedValidators = Array.from(validators.values()); mappedValidators.sort((left, right) => { const diff = right.votingPower - left.votingPower; if (diff !== 0n) { return diff > 0n ? 1 : -1; } return left.operator.toLowerCase().localeCompare(right.operator.toLowerCase()); }); if (mappedValidators.length > SSZ_MAX_VALIDATORS) { mappedValidators = mappedValidators.slice(0, SSZ_MAX_VALIDATORS); } markValidatorsActive(config, mappedValidators); mappedValidators.sort((left, right) => left.operator.toLowerCase().localeCompare(right.operator.toLowerCase())); return mappedValidators; }; export const calculateQuorumThreshold = (config, totalVotingPower) => { const threshold = config.quorumThresholds.find((entry) => entry.keyTag === config.requiredHeaderKeyTag); if (!threshold) { throw new Error(`No quorum threshold for key tag ${config.requiredHeaderKeyTag}`); } if (threshold.quorumThreshold === 0n) { throw new Error(`Quorum threshold for key tag ${config.requiredHeaderKeyTag} is zero`); } const maxThreshold = 1000000000000000000n; const multiplied = totalVotingPower * threshold.quorumThreshold; const divided = multiplied / maxThreshold; return divided + 1n; }; //# sourceMappingURL=validator_set.js.map