@symbioticfi/relay-stats-ts
v0.4.0
Published
TypeScript library for deriving validator sets from Symbiotic network contracts
Readme
@symbioticfi/relay-stats-ts
TypeScript utilities for deriving Symbiotic validator-set data from on-chain contracts. The library mirrors the Go reference implementation and exposes helpers for SSZ encoding, MiMC hashing, aggregator extra data generation, and deterministic scheduler role assignment.
Installation
Local installation
git clone https://github.com/symbioticfi/relay-stats-ts.git
cd relay-stats-ts
pnpm install # install dependencies
pnpm run build # compile TypeScript to dist/You can now import from the src/ or freshly built dist/ folders locally, or run the example script (see examples/README.md).
From npm
pnpm add @symbioticfi/relay-stats-tsRequires Node.js 18 or newer.
Quick Start
Create a deriver that talks to the ValSet driver and fetch the current validator set:
import { ValidatorSetDeriver } from '@symbioticfi/relay-stats-ts';
const deriver = await ValidatorSetDeriver.create({
rpcUrls: ['https://ethereum.publicnode.com'],
driverAddress: {
chainId: 1,
address: '0xDriverAddress',
},
});
const validatorSet = await deriver.getCurrentValidatorSet();
console.log(`Epoch: ${validatorSet.epoch}`);
console.log(`Validators: ${validatorSet.validators.length}`);
console.log(`Settlement status: ${validatorSet.status}`);
console.log(`Integrity: ${validatorSet.integrity}`);Single-call epoch snapshot
getEpochData pulls the validator set, validator roles, network metadata, settlement statuses, log events, and aggregator extras in one request:
const snapshot = await deriver.getEpochData({
epoch,
finalized: true,
includeNetworkData: true,
includeValSetEvent: true,
});
const { validatorSet, validatorRoles, config } = snapshot;
console.log(validatorSet.status, validatorSet.validators.length);
console.log('aggregators:', validatorRoles.aggregatorIndices);
console.log('committers:', validatorRoles.committerIndices);Batch epoch snapshots
getEpochsData returns the same snapshot shape for an epoch range (results in ascending order):
const snapshots = await deriver.getEpochsData({
epochRange: { from: 1, to: 3 },
finalized: true,
});
snapshots.forEach(s => {
console.log(s.epoch, s.validatorSet.status, s.validatorRoles.aggregatorIndices);
});Batch helpers use multicall to minimize RPCs when available.
Validator Roles (aggregator & committer assignments)
Each epoch deterministically assigns aggregator and committer roles to active validators. The validator roles are included in every EpochData snapshot. They are also available standalone:
import { getActiveCommitter } from '@symbioticfi/relay-stats-ts';
const validatorRoles = await deriver.getValidatorRoles({ epoch: currentEpoch });
// indices into validatorSet.validators
const aggregators = validatorRoles.aggregatorIndices.map(i => validatorSet.validators[i]);
const committers = validatorRoles.committerIndices.map(i => validatorSet.validators[i]);
// who is the active committer right now (round-robin time slots)
const active = getActiveCommitter({
committerIndices: validatorRoles.committerIndices,
captureTimestamp: validatorSet.captureTimestamp,
currentTime: Math.floor(Date.now() / 1000),
committerSlotDuration: config.committerSlotDuration,
});
if (active) {
const validator = validatorSet.validators[active.validatorIndex];
console.log(`Active committer: ${validator.operator}`);
console.log(`Slot: ${active.slotStart} – ${active.slotEnd}`);
}getActiveCommitter is a pure function — no RPC calls. Special cases:
committerSlotDuration === 0or single committer → always activecurrentTime < captureTimestamp→ returnsnull- Optional
graceSecondsparameter for early next-slot activation
Aggregator extra data
const extraData = await deriver.getAggregatorsExtraData('zk');
extraData.forEach(({ key, value }) => console.log(key, value));Pass 'simple' for simple mode or provide custom keyTags when you need non-default key selection. Aggregator extra data returned by getEpochData automatically uses the network configuration's verificationType.
Range helpers
For range-style reads (status, settlement logs, timings), use the batch APIs:
const epochRange = { from: 10, to: 12 };
const statuses = await deriver.getValSetStatuses(epochRange, true);
statuses.forEach(({ epoch, status }) => {
console.log(epoch, status.status, status.integrity);
});
const logEvents = await deriver.getValSetLogEventsForEpochs({
epochRange,
finalized: true,
});
logEvents.forEach(({ epoch, logs }) => {
console.log(
epoch,
logs.map(log => log.committed)
);
});
const [starts, durations] = await Promise.all([
deriver.getEpochStarts(epochRange, true),
deriver.getEpochDurations(epochRange, true),
]);
console.log(starts, durations);Validator set events
Validator-set commitment events expose on-chain metadata (block number, block timestamp, transaction hash) together with the parsed header. The deriver only attempts to load the event once the validator set status is committed, so pending epochs return null without additional RPC calls:
const events = await deriver.getValSetLogEvents({ epoch, finalized: true });
events.forEach(({ settlement, committed, event }) => {
console.log(`Settlement ${settlement.address} committed=${committed}`);
if (event) {
console.log(' kind:', event.kind);
console.log(' blockTimestamp:', event.blockTimestamp);
console.log(' txHash:', event.transactionHash);
}
});Caching
ValidatorSetDeriver accepts any cache that conforms to the CacheInterface and only persists finalized data. Cache entries are namespaced by epoch and a string key, allowing multiple values per epoch. The deriver keeps a FIFO list of cached epochs and evicts them with cache.clear(epoch) once maxSavedEpochs is exceeded; non-epoch data (like network metadata) is stored under a persistent epoch sentinel.
import type { CacheInterface } from '@symbioticfi/relay-stats-ts';
class MapCache implements CacheInterface {
private buckets = new Map<number, Map<string, unknown>>();
async get(epoch: number, key: string) {
return this.buckets.get(epoch)?.get(key) ?? null;
}
async set(epoch: number, key: string, value: unknown) {
let bucket = this.buckets.get(epoch);
if (!bucket) {
bucket = new Map();
this.buckets.set(epoch, bucket);
}
bucket.set(key, value);
}
async delete(epoch: number, key: string) {
const bucket = this.buckets.get(epoch);
if (!bucket) return;
bucket.delete(key);
if (bucket.size === 0) {
this.buckets.delete(epoch);
}
}
async clear(epoch: number) {
this.buckets.delete(epoch);
}
}
const deriver = await ValidatorSetDeriver.create({
rpcUrls: ["..."],
driverAddress: {...},
cache: new MapCache(),
});API Highlights
All exports live under @symbioticfi/relay-stats-ts. Key entry points:
ValidatorSetDeriver.create(config)– initialize clients (one per chain) and validate required RPC coverage.getEpochData({ epoch?, finalized?, ... })– single snapshot with validator set, validator roles, optional network metadata, aggregator extras, settlement statuses, and per-settlement log events.getEpochsData({ epochRange?, finalized?, ... })– batch snapshot (array) in ascending order with the same shape asgetEpochData.getValidatorSet(epoch?, finalized?)/getNetworkConfig(epoch?, finalized?)/getNetworkData(settlement?, finalized?)– granular primitives, plus batch variantsgetValidatorSets(epochRange?)andgetNetworkConfigs(epochRange?).getValidatorRoles({ epoch?, finalized? })/getValidatorRolesForEpochs({ epochRange?, finalized? })– deterministic aggregator/committer role assignments.getActiveCommitter(params)– pure helper returning the currently active committer with slot boundaries.getEpochStart(epoch)/getEpochDuration(epoch)– driver timing helpers, plus batch variants.getValSetStatus(epoch)/getValSetSettlementStatuses(...)/getValSetLogEvents(...)– settlement state and events, plus batch variants.getAggregatorsExtraData(mode, ...)/getAggregatorsExtraDataForEpochs(...)– simple/zk aggregator payloads.- Utilities:
getTotalActiveVotingPower,getValidatorSetHeader,abiEncodeValidatorSetHeader,hashValidatorSetHeader,getValidatorSetHeaderHash. - Low-level re-exports:
buildSimpleExtraData,buildZkExtraData,getValidatorRoles, SSZ utilities (serializeValidatorSet,getValidatorSetRoot, etc.).
Refer to src/types/ for full type definitions.
Example
See examples/ for a working demo. Quick start:
cd examples && pnpm install && pnpm startDevelopment
pnpm install
pnpm run checkLicense
MIT
