UNPKG

@symbioticfi/relay-stats-ts

Version:

TypeScript library for deriving validator sets from Symbiotic network contracts

245 lines (203 loc) 7.39 kB
import { encodeAbiParameters, keccak256, type Hex } from 'viem'; import type { AggregatorExtraDataEntry, KeyTag, ValidatorSet } from './types.js'; import { KeyType, getKeyType } from './types.js'; import { compressAggregatedG1, compressRawG1, parseKeyToPoint } from './bls_bn254.js'; import { FIELD_MODULUS, mimcBn254Hash } from './mimc_bn254.js'; import { hexToBytes } from './encoding.js'; import { EXTRA_NAME, EXTRA_PREFIX } from './constants.js'; import { bigintToBytes32, bytesToBigint, bytesToLimbs, sortHexAsc } from './utils.js'; const keccakName = (name: string): Hex => { const hex = `0x${Buffer.from(name, 'utf8').toString('hex')}` as Hex; return keccak256(hex); }; const computeExtraDataKey = (verificationType: number, name: string): Hex => { const encoded = encodeAbiParameters( [ { name: 'vt', type: 'uint32' }, { name: 'name', type: 'bytes32' }, ], [verificationType, keccakName(name)], ); return keccak256(encoded); }; const computeExtraDataKeyTagged = (verificationType: number, tag: number, name: string): Hex => { const prefix = keccakName(EXTRA_PREFIX.TAG); const nameHash = keccakName(name); const encoded = encodeAbiParameters( [ { name: 'vt', type: 'uint32' }, { name: 'prefix', type: 'bytes32' }, { name: 'tag', type: 'uint8' }, { name: 'name', type: 'bytes32' }, ], [verificationType, prefix, tag, nameHash], ); return keccak256(encoded); }; const modField = (value: bigint): bigint => { const remainder = value % FIELD_MODULUS; return remainder >= 0n ? remainder : remainder + FIELD_MODULUS; }; const filterBlsBn254Tags = (keyTags: readonly KeyTag[]): readonly KeyTag[] => { const tags: KeyTag[] = []; for (const tag of keyTags) { if (getKeyType(tag) === KeyType.KeyTypeBlsBn254) { tags.push(tag); } } return tags; }; type ValidatorTuple = { keySerialized: Hex; votingPower: bigint }; const collectValidatorsForSimple = ( validatorSet: ValidatorSet, tag: number, ): { validatorTuples: ValidatorTuple[]; aggregatedKeyCompressed: Hex } => { const tuples: ValidatorTuple[] = []; const activeKeys: Hex[] = []; for (const validator of validatorSet.validators) { if (!validator.isActive) continue; let payload: Hex | null = null; for (const key of validator.keys) { if (key.tag === tag) { payload = key.payload; break; } } if (!payload) { throw new Error(`Failed to find key by keyTag ${tag} for validator ${validator.operator}`); } tuples.push({ keySerialized: compressRawG1(payload), votingPower: validator.votingPower, }); activeKeys.push(payload); } tuples.sort((left, right) => left.keySerialized < right.keySerialized ? -1 : left.keySerialized > right.keySerialized ? 1 : 0, ); return { validatorTuples: tuples, aggregatedKeyCompressed: compressAggregatedG1(activeKeys), }; }; type ZkValidatorTuple = ValidatorTuple & { x: bigint; y: bigint }; const collectValidatorsForZk = ( validatorSet: ValidatorSet, tag: number, ): readonly ZkValidatorTuple[] => { const tuples: ZkValidatorTuple[] = []; for (const validator of validatorSet.validators) { if (!validator.isActive) continue; let payload: Hex | null = null; for (const key of validator.keys) { if (key.tag === tag) { payload = key.payload; break; } } if (!payload) { throw new Error(`Failed to find key by keyTag ${tag} for validator ${validator.operator}`); } const point = parseKeyToPoint(payload); if (point === null) { throw new Error(`Validator ${validator.operator} key resolves to point at infinity`); } tuples.push({ keySerialized: compressRawG1(payload), votingPower: validator.votingPower, x: modField(point.x), y: modField(point.y), }); } tuples.sort((left, right) => { if (left.x === right.x && left.y === right.y) return 0; const leftBeforeRight = left.x < right.x || left.y < right.y; return leftBeforeRight ? -1 : 1; }); return tuples; }; const keccakValidatorsData = (tuples: readonly ValidatorTuple[]): Hex => { const encoded = encodeAbiParameters( [ { name: 'validators', type: 'tuple[]', components: [ { name: 'keySerialized', type: 'bytes' }, { name: 'VotingPower', type: 'uint256' }, ], }, ], [ tuples.map((tuple) => ({ keySerialized: tuple.keySerialized, VotingPower: tuple.votingPower, })), ], ); const encodedBytes = hexToBytes(encoded); const tail = encodedBytes.slice(32); return keccak256(tail); }; const mimcHashValidators = async (tuples: readonly ZkValidatorTuple[]): Promise<Hex> => { if (tuples.length === 0) { return `0x${'0'.repeat(64)}` as Hex; } let state = 0n; for (const validator of tuples) { if (validator.x === 0n && validator.y === 0n) break; const xLimbs = bytesToLimbs(bigintToBytes32(validator.x), 8); const yLimbs = bytesToLimbs(bigintToBytes32(validator.y), 8); const votingPowerField = bytesToBigint(bigintToBytes32(validator.votingPower)); for (const limb of [...xLimbs, ...yLimbs]) { state = modField(mimcBn254Hash(state, limb)); } state = modField(mimcBn254Hash(state, votingPowerField)); } const finalHash = modField(state); const bytes = bigintToBytes32(finalHash); return `0x${Buffer.from(bytes).toString('hex')}` as Hex; }; export const buildSimpleExtraData = ( validatorSet: ValidatorSet, keyTags: readonly number[], ): AggregatorExtraDataEntry[] => { const entries: AggregatorExtraDataEntry[] = []; const filteredTags = filterBlsBn254Tags(keyTags); for (const tag of filteredTags) { const { validatorTuples, aggregatedKeyCompressed } = collectValidatorsForSimple( validatorSet, tag, ); if (validatorTuples.length === 0) continue; const validatorsKeccak = keccakValidatorsData(validatorTuples); const validatorKey = computeExtraDataKeyTagged(1, tag, EXTRA_NAME.SIMPLE_VALIDATORS_KECCAK); entries.push({ key: validatorKey, value: validatorsKeccak }); const aggregatedKey = computeExtraDataKeyTagged(1, tag, EXTRA_NAME.SIMPLE_AGG_G1); entries.push({ key: aggregatedKey, value: aggregatedKeyCompressed }); } return sortHexAsc(entries); }; export const buildZkExtraData = async ( validatorSet: ValidatorSet, keyTags: readonly number[], ): Promise<AggregatorExtraDataEntry[]> => { const entries: AggregatorExtraDataEntry[] = []; const totalActive = validatorSet.validators.filter((validator) => validator.isActive).length; const totalActiveKey = computeExtraDataKey(0, EXTRA_NAME.ZK_TOTAL_ACTIVE); const totalActiveBytes = `0x${totalActive.toString(16).padStart(64, '0')}` as Hex; entries.push({ key: totalActiveKey, value: totalActiveBytes }); const filteredTags = filterBlsBn254Tags(keyTags); for (const tag of filteredTags) { const tuples = collectValidatorsForZk(validatorSet, tag); if (tuples.length === 0) continue; const mimcAccumulator = await mimcHashValidators(tuples); const validatorsKey = computeExtraDataKeyTagged(0, tag, EXTRA_NAME.ZK_VALIDATORS_MIMC); entries.push({ key: validatorsKey, value: mimcAccumulator }); } return sortHexAsc(entries); };