UNPKG

@substrate/api-sidecar

Version:

REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.

351 lines 18.2 kB
"use strict"; // Copyright 2017-2025 Parity Technologies (UK) Ltd. // This file is part of Substrate API Sidecar. // // Substrate API Sidecar is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PalletsStakingProgressService = void 0; const bn_js_1 = __importDefault(require("bn.js")); const http_errors_1 = require("http-errors"); const apiRegistry_1 = require("../../../src/apiRegistry"); const chains_config_1 = require("../../chains-config"); const badStakingBlocks_1 = require("../../util/badStakingBlocks"); const AbstractService_1 = require("../AbstractService"); class PalletsStakingProgressService extends AbstractService_1.AbstractService { /** * Fetch and derive generalized staking information at a given block. * * @param hash `BlockHash` to make call at */ async derivePalletStakingProgress(hash, options = {}) { var _a; const { api } = this; const RCApiPromise = this.assetHubInfo.isAssetHub ? apiRegistry_1.ApiPromiseRegistry.getApiByType('relay') : null; if (this.assetHubInfo.isAssetHub && !(RCApiPromise === null || RCApiPromise === void 0 ? void 0 : RCApiPromise.length)) { throw new Error('Relay chain API not found'); } const historicApi = await api.at(hash); if (historicApi.query.staking === undefined) { throw new Error('Staking pallet not found for queried runtime'); } const sessionValidators = this.assetHubInfo.isAssetHub && !options.isRcCall ? RCApiPromise[0].api.query.session.validators : historicApi.query.session.validators; if (!sessionValidators) { throw new Error('Session pallet not found for queried runtime'); } const [validatorCount, forceEra, validators, { number }] = await Promise.all([ historicApi.query.staking.validatorCount(), historicApi.query.staking.forceEra(), sessionValidators(), api.rpc.chain.getHeader(hash), ]); if ((0, badStakingBlocks_1.isBadStakingBlock)(this.specName, number.unwrap().toNumber())) { let chainName = this.specName; switch (this.specName) { case 'westmint': chainName = 'Westend Asset Hub'; break; } throw new Error(`Post migration, there were some interruptions to staking on ${chainName}, Block ${number.unwrap().toString(10)} is in the list of known bad staking blocks in ${chainName}`); } let eraElectionPromise; /** * Polkadot runtimes v0.8.30 and above do not support eraElectionStatus, so we check * to see if eraElectionStatus is mounted to the api, and if were running on a * runtime less than v0.8.30 it will return a successful result. If it doesn't * we do nothing and let `eraElectionStatus` stay undefined. */ if (historicApi.query.staking.eraElectionStatus) { eraElectionPromise = await historicApi.query.staking.eraElectionStatus(); } let deriveSessionAndEra; if (this.assetHubInfo.isAssetHub && this.assetHubInfo.isAssetHubMigrated && !options.isRcCall) { deriveSessionAndEra = this.deriveSessionAndEraProgressAssetHub(historicApi, RCApiPromise === null || RCApiPromise === void 0 ? void 0 : RCApiPromise[0].api); } else { deriveSessionAndEra = this.deriveSessionAndEraProgress(historicApi); } const [eraElectionStatus, { eraLength, eraProgress, sessionLength, sessionProgress, activeEra }] = await Promise.all([eraElectionPromise, deriveSessionAndEra]); const unappliedSlashesAtActiveEra = await historicApi.query.staking.unappliedSlashes.entries(); const currentBlockNumber = number.toBn(); const nextSession = sessionLength.sub(sessionProgress).add(currentBlockNumber); const baseResponse = { at: { hash: hash.toJSON(), height: currentBlockNumber.toString(10), }, activeEra: activeEra.toString(10), forceEra: forceEra.toJSON(), nextSessionEstimate: nextSession.toString(10), unappliedSlashes: Array.isArray(unappliedSlashesAtActiveEra) && unappliedSlashesAtActiveEra.length > 0 ? unappliedSlashesAtActiveEra.flatMap(([key, slashOption]) => { var _a; if (!slashOption || !slashOption.isSome) return []; const slashes = slashOption.unwrap(); const era = (_a = key.args[0]) === null || _a === void 0 ? void 0 : _a.toString(); return Array.isArray(slashes) ? slashes.map((slash) => ({ era, ...slash.toJSON(), })) : [ { era, ...slashes.toJSON(), }, ]; }) : [], validatorSet: validators && validators.length ? validators.map((accountId) => accountId.toString()) : [], }; if (forceEra.isForceNone) { // Most likely we are in a PoA network with no elections. Things // like `ValidatorCount` and `Validators` are hardcoded from genesis // to support a transition into NPoS, but are irrelevant here and would be // confusing to include. Thus, we craft a response excluding those values. return baseResponse; } const nextActiveEra = forceEra.isForceAlways ? nextSession // there is a new era every session : eraLength.sub(eraProgress).add(currentBlockNumber); // the nextActiveEra is at the end of this era const electionLookAhead = await this.deriveElectionLookAhead(historicApi, RCApiPromise === null || RCApiPromise === void 0 ? void 0 : RCApiPromise[0].api); const nextCurrentEra = nextActiveEra.sub(currentBlockNumber).sub(sessionLength).gt(new bn_js_1.default(0)) ? nextActiveEra.sub(sessionLength) // current era simply one session before active era : nextActiveEra.add(eraLength).sub(sessionLength); // we are in the last session of an active era let toggle; if (electionLookAhead.eq(new bn_js_1.default(0))) { // no offchain solutions accepted toggle = null; } else if (eraElectionStatus === null || eraElectionStatus === void 0 ? void 0 : eraElectionStatus.isClose) { // election window is yet to open toggle = nextCurrentEra.sub(electionLookAhead); } else { // election window closes at the end of the current era toggle = nextCurrentEra; } return { ...baseResponse, nextActiveEraEstimate: nextActiveEra.toString(10), electionStatus: eraElectionStatus ? { status: eraElectionStatus.toJSON(), toggleEstimate: (_a = toggle === null || toggle === void 0 ? void 0 : toggle.toString(10)) !== null && _a !== void 0 ? _a : null, } : 'Deprecated, see docs', idealValidatorCount: validatorCount.toString(10), validatorSet: validators.map((accountId) => accountId.toString()), }; } /** * Derive session and era progress for Asset Hub using time-based BABE calculations * * This method calculates session and era progress for Asset Hub by deriving all values * from scratch using time-based BABE formulas. This approach is necessary because: * * 1. **Historical Support**: Asset Hub cannot access historical BABE pallet constants * from the relay chain, so we need to calculate everything from time * 2. **Time-Based Nature**: BABE slots and epochs are purely time-based and can be * calculated deterministically without chain state * 3. **Relay Chain Dependency**: Asset Hub needs to query the relay chain for * skipped epochs, but can calculate slots/epochs locally * * **Calculations:** * - **Current Slot**: `timestamp / slot_duration` (6 seconds for all networks) * - **Epoch Index**: `(current_slot - genesis_slot) / epoch_duration` * - **Session Index**: Derived from epoch index using SkippedEpochs mapping * - **Session Progress**: `current_slot - epoch_start_slot` * - **Era Progress**: `(current_session - era_start_session) * session_length + session_progress` * * **Data Sources:** * - **Time-based**: Current timestamp, hardcoded BABE parameters * - **Chain state**: Active era, bonded eras, session index, skipped epochs * * @param historicApi - Asset Hub API for staking queries * @param RCApi - Relay chain API for BABE queries (session index, skipped epochs) * @returns Session and era progress information */ async deriveSessionAndEraProgressAssetHub(historicApi, RCApi) { const [activeEraOption, activeEraStartSessionIndexVec, currentTimestamp, skippedEpochs] = await Promise.all([ historicApi.query.staking.activeEra(), historicApi.query.staking.bondedEras(), historicApi.query.timestamp.now(), RCApi === null || RCApi === void 0 ? void 0 : RCApi.query.babe.skippedEpochs(), ]); if (activeEraOption.isNone) { throw new http_errors_1.InternalServerError('ActiveEra is None when Some was expected.'); } const { index: activeEra } = activeEraOption.unwrap(); let activeEraStartSessionIndex; for (const [era, idx] of activeEraStartSessionIndexVec) { if (era && era.eq(activeEra) && idx) { activeEraStartSessionIndex = idx; } } if (!activeEraStartSessionIndex) { throw new http_errors_1.InternalServerError('EraStartSessionIndex is None when Some was expected.'); } const specNameBabeValues = chains_config_1.assetHubToBabe[this.specName]; const eraLength = historicApi.consts.staking.sessionsPerEra.mul(specNameBabeValues.epochDuration); const currentSlot = currentTimestamp.div(specNameBabeValues.slotDurationMs); // Calculate epoch index: (current_slot - genesis_slot) / epoch_duration const epochIndex = currentSlot.sub(specNameBabeValues.genesisSlot).div(specNameBabeValues.epochDuration); const currentIndex = this.calculateSessionIndexFromSkippedEpochs(epochIndex, skippedEpochs); // Calculate epoch start slot const epochStartSlot = epochIndex.mul(specNameBabeValues.epochDuration).add(specNameBabeValues.genesisSlot); // Calculate session progress within current epoch const sessionProgress = currentSlot.sub(epochStartSlot); // Calculate era progress const eraProgress = currentIndex .sub(activeEraStartSessionIndex) .mul(specNameBabeValues.epochDuration) .add(sessionProgress); return { eraLength, eraProgress, sessionLength: specNameBabeValues.epochDuration, sessionProgress, activeEra, }; } /** * Derive information on the progress of the current session and era. * * @param api ApiPromise with ensured metadata * @param hash `BlockHash` to make call at */ async deriveSessionAndEraProgress(historicApi) { const [currentSlot, epochIndex, genesisSlot, currentIndex, activeEraOption] = await Promise.all([ historicApi.query.babe.currentSlot(), historicApi.query.babe.epochIndex(), historicApi.query.babe.genesisSlot(), historicApi.query.session.currentIndex(), historicApi.query.staking.activeEra(), ]); if (activeEraOption.isNone) { // TODO refactor to newer error type throw new http_errors_1.InternalServerError('ActiveEra is None when Some was expected.'); } const { index: activeEra } = activeEraOption.unwrap(); const activeEraStartSessionIndexVec = await historicApi.query.staking.bondedEras(); let activeEraStartSessionIndex; for (const [era, idx] of activeEraStartSessionIndexVec) { if (era && era.eq(activeEra) && idx) { activeEraStartSessionIndex = idx; } } if (!activeEraStartSessionIndex) { throw new http_errors_1.InternalServerError('EraStartSessionIndex is None when Some was expected.'); } const { epochDuration: sessionLength } = historicApi.consts.babe; const eraLength = historicApi.consts.staking.sessionsPerEra.mul(sessionLength); const epochStartSlot = epochIndex.mul(sessionLength).add(genesisSlot); const sessionProgress = currentSlot.sub(epochStartSlot); const eraProgress = currentIndex.sub(activeEraStartSessionIndex).mul(sessionLength).add(sessionProgress); return { eraLength, eraProgress, sessionLength, sessionProgress, activeEra, }; } /** * Get electionLookAhead as a const if available. Otherwise derive * `electionLookAhead` based on the `specName` & `epochDuration`. * N.B. Values are hardcoded based on `specName`s polkadot, kusama, and westend. * There are no guarantees that this will return the expected values for * other `specName`s. * * @param api ApiPromise with ensured metadata * @param hash `BlockHash` to make call at */ async deriveElectionLookAhead(historicApi, RCApi) { if (historicApi.consts.staking.electionLookahead) { return historicApi.consts.staking.electionLookahead; } let specName = this.specName; if (RCApi) { specName = await RCApi.rpc.state.getRuntimeVersion().then(({ specName }) => specName.toString()); } const { epochDuration } = RCApi ? RCApi.consts.babe : historicApi.consts.babe; // TODO - create a configurable epochDivisor env for a more generic solution const epochDurationDivisor = specName.toString() === 'polkadot' ? new bn_js_1.default(16) // polkadot electionLookAhead = epochDuration / 16 : new bn_js_1.default(4); // kusama, westend, `substrate/bin/node` electionLookAhead = epochDuration / 4 return epochDuration.div(epochDurationDivisor); } /** * Calculate session index from epoch index using SkippedEpochs mapping * * In BABE consensus, epochs advance based on time and can be "skipped" if the chain * is offline, but sessions (which represent authority set changes) must happen * sequentially and cannot be skipped. * * When epochs are skipped, BABE stores the mapping in `SkippedEpochs` to track * the permanent offset between epoch and session indices. * * **Example:** * - Normal: Epoch 10 → Session 10, Epoch 11 → Session 11 * - Chain offline during epochs 12-15 * - After downtime: Epoch 16 → Session 12 (sessions 13-16 were "lost") * - `SkippedEpochs` stores: [(16, 12)] indicating epoch 16 maps to session 12 * - Future epochs: Epoch 17 → Session 13, Epoch 18 → Session 14, etc. * * **Algorithm:** * 1. Find the closest skipped epoch ≤ current epoch * 2. Calculate permanent offset: `skipped_epoch - skipped_session` * 3. Apply offset: `session_index = current_epoch - permanent_offset` * * @param epochIndex - Current epoch index (time-based) * @param skippedEpochs - BABE SkippedEpochs storage: Vec<(u64, u32)> mapping epoch to session * @returns Current session index that accounts for skipped epochs */ calculateSessionIndexFromSkippedEpochs(epochIndex, skippedEpochs) { if (!skippedEpochs || skippedEpochs.isEmpty) { // No skipped epochs - session index equals epoch index return epochIndex; } const skippedArray = skippedEpochs.toArray(); skippedArray.sort((a, b) => a[0].toNumber() - b[0].toNumber()); // Find the closest skipped epoch that's <= current epoch let closestSkippedEpoch = null; for (const skipped of skippedArray) { if (skipped[0].lte(epochIndex)) { closestSkippedEpoch = skipped; } else { break; } } if (!closestSkippedEpoch) { // No skipped epochs before current epoch - session = epoch return epochIndex; } // Calculate the permanent offset from the closest skipped epoch const [skippedEpochIndex, skippedSessionIndex] = closestSkippedEpoch; const permanentOffset = skippedEpochIndex.sub(skippedSessionIndex); // Apply the permanent offset to current epoch to get session index const sessionIndex = epochIndex.sub(permanentOffset); return sessionIndex; } } exports.PalletsStakingProgressService = PalletsStakingProgressService; //# sourceMappingURL=PalletsStakingProgressService.js.map