owned this note
owned this note
Published
Linked with GitHub
---
tags: keys-api
title: Keys API Service
---
# Keys API Service
## Intro
Simple Lido keys and validators HTTP API. It will be possible to launch it on someone's machine and fetch actual Lido keys from all sources. API will also provide HTTP methods for [Automating Validator Exits](https://hackmd.io/0xndfK-ISxaOGuEY8FVGSw?view#Generating-and-Signing-Exit-Messages).
## Glossary
- `NodeOperatorRegistry` is a Lido contract that manages a сurated set of node operators, stores their signing keys;
- `Module` is a contract in charge of different validators subsets;
- Module state is a keys, operators and other data for some specific EL block;
- Possible validators subsets are `Curated` (modified version of `NodeOperatorRegistry`), `Community`, `DVT`, `Offchain` or `L2` upgrade for Curated subset;
- Module type is a value from Staking Module that help to match Module with our code base;
- `StakingRouter` is a new contract that is responsible for modules management, reserve allocation and rewards distribution.
## Problem statement
Lido node operators keys and validators are used in a lot of services: [Oracle](https://github.com/lidofinance/lido-oracle), [Council Daemon](https://github.com/lidofinance/lido-council-daemon), MEV Monitoring, BalVal, Node Operators Widget. Also Node Operators need fetching keys for automating exits. Currently Lido supports only `Curated` validators subset handled by `NodeOperatorRegistry` contract. But in the future validators set can be expanded with `DVT` and `Community` validators. The `StakingRouter` contract was proposed for implementing modular approach of managing different subsets of validators. It will support any number of `Curated`, `Community` and `DVT` modules. These modules could store keys on-chain, off-chain, on L2 or in any other way. Services will have to support fetching keys from all these modules.
## Assumptions
1. `StakingRouter` will be implemented;
2. `StakingRouter` could support any number `Curated`, `Community`, `DVT`, off-chain and L2 `Curated` modules;
3. Every module will have an `L1` contract;
4. That `L1` contract address will not be changed;
5. Every module can be uniquely identified by it's contract address;
6. Every module will have a type and function to get it;
7. Every module will have `keysOpIndex` or other value to understand that keys list was changed (will be renamed to `nonce`);
8. Curated module will have `operator id`;
9. `Community`/`DVT` modules will have some group id for keys to determine ownership of keys;
10. Used keys will not be removed from any module storage.
## Requirements
1. Keys API should keep all data of modules in a consistent state. In other words Keys API should keep state of all modules identical (`blockHash` in metadata of each module should be equal);
2. Keys API will return it's latest fetched `EL` state. It will NOT return any arbitrary state for arbitrary `blockNumber`. Keys API is NOT an indexer.
## Technical solution
### Overview
Keys API service will work with all `StakingRouter` modules. We chose modular way of implementation. For each type of module we will have separate library. Modules data will be stored in separate tables too. For example, we will have `CuratedKey` and `CommunityKey` tables. Every library will contain its own `{ModuleName}Meta` table with EL data. Meta will include `keyOpIndex` (`nonce`) value. API will run cron job to update keys in db for all modules to `latest EL block`. At the moment API supports only `NodeOperatorRegistry` keys.
API will have endpoint for [Automating Validator Exits](https://hackmd.io/0xndfK-ISxaOGuEY8FVGSw?view#Generating-and-Signing-Exit-Messages). Information about validators state will be fetched from beacon node and stored in database. We will implement this algorithm as `ValidatorState` library.
In the next sections we will describe technical details of each part.
### Modules libraries interface
In this section we will describe general interface of libraries for `StakingRouter` modules. Each library will contain methods for fetching keys and other data from modules storages, methods for writing and reading data to the database.
```typescript=
// information that shows data relevance
interface BlockMetadata {
blockNumber: number;
blockHash: string;
timestamp: number;
}
// information from staking router module
interface ModuleMetadata {
// address of module
stakingModuleAddress: string;
// module type
type: string;
// unique id of the module
moduleId: number;
// will be renamed as a nonce
keysOpIndex: number;
// reward fee of the module
moduleFee: number;
// treasury fee
treasuryFee: number;
// target percent of total keys in protocol, in BP
targetShare: number;
// module status if module can not accept the deposits or can participate in further reward distribution
status: number;
// name of module
name: string;
// block.timestamp of the last deposit of the module
lastDepositAt: number;
// block.number of the last deposit of the module
lastDepositBlock: number;
}
interface Metadata {
blockMetadata: BlockMetadata;
moduleMetadata: ModuleMetadata;
}
interface Key {
index: number;
used: boolean;
operatorIndex: number;
depositSignature: string;
key: string;
}
interface Operator {
index: number;
active: boolean;
name: string;
rewardAddress: string;
stakingLimit: number;
stoppedValidators: number;
totalSigningKeys: number;
usedSigningKeys: number;
}
interface Data<Op extends Operator = Operator,PKey extends Key = Key> {
keys: PKey[],
operators: Op[],
meta: Metadata
}
interface AbstractModule<Op extends Operator = Operator,PKey extends Key = Key> {
stakingModuleAddress: string;
type: string;
keysOpIndex: number;
// method for updating data in database from contract
update(blockHash: string): Promise<Metadata>;
// methods for reading data from database
getKeysFromStorage(): Promise<{keys: PKey[]; meta: Metadata}>;
getOperatorsFromStorage(): Promise<{operators: Op[]; meta: Metadata}>;
getKeysAndOperatorsFromStorage(): Promise<{keys: PKey[]; operators: Op[]; meta: Metadata}>;
getMetadataFromStorage(): Promise<Metadata>;
}
```
### Updating keys in Keys API service
As was mentioned in [requirements section](https://hackmd.io/fv8btyNTTOGLZI6LqYyYIg?both#Requirements) Keys API service should keep modules data in storage in consistent state. To fulfill this condition we will fetch data from modules storages for the same `blockHash` and write it in database in transaction.
```typescript=
// some function that returns all modules
async function getModules(): Promise<AbstractModule[]> {
...
}
// somewhere in cron jobs
const modules = await getModules();
const block = await provider.getBlock('latest');
const blockHash = block.hash;
// get all keys, operators, meta
entityManager.transactional(async () => {
const metadatas = Promise.all(modules.map(async (module) => {
return await module.update(blockHash);
}));
if (!checkMetadatas(metadatas)) {
throw new Error('Inconsistent metadatas');
}
});
// reading keys somewhere in HTTP controller
const keys = await module.getKeysFromStorage();
```
### Curated module library
As described earlier, at the moment Lido stores keys on-chain in `NodeOperatorRegistry` contract. We have already implemented `Registry` library for fetching keys and operators from this contract, store in database and update database state to `latest EL block`.
#### DB Models
`Registry` library contains `RegistryKey`, `RegistryOperator`, `RegistryMeta` tables.
```typescript=
interface RegistryKey {
index: number;
key: string;
depositSignature: string;
used: boolean;
operatorIndex: number;
}
interface RegistryOperator {
index: number;
active: boolean;
name: string;
rewardAddress: string;
stackingLimit: number;
stoppedValidators: number;
totalSigningKeys: number;
usedSigningKeys: number;
}
interface RegistryMeta {
blockNumber: number;
blockHash: string;
timestamp: number;
keysOpIndex: number;
}
```
#### Updating keys
Updating keys in `Registry` library is based on comparing meta information in contract and database. We read `blockHash`,`blockNumber` values of `latest` block from `EL` and fetch `keyOpIndex` from `NodeOperatorRegistry` by `blockHash`. If current `blockNumber` is less than previous or `keyOpIndex` wasn't changed, keys in database will not be updated. Otherwise, keys and operators will be updated in library's database.
```typescript=
async update(blockHashOrBlockTag: string) {
const prevMeta: RegistryMeta = await getMetaDataFromStorage();
const currMeta = await getMetaDataFromContract(blockHashOrBlockTag);
if (prevMeta.blockNumber > currMeta.blockNumber) {
return;
}
if (prevMeta.keyOpIndex === currMeta.keyOpIndex) {
return;
}
updateOperatorsAndKeysInDB(currMeta.blockHash);
}
async getMetaDataFromContract(blockHashOrBlockTag): Promise<RegistryMeta> {
const block = await provider.getBlock(blockHashOrBlockTag);
const blockHash = block.hash;
const blockTag = { blockHash };
const keysOpIndex = await registryContract.getKeysOpIndex({ blockTag });
return {
blockNumber: block.number,
blockHash,
keysOpIndex,
timestamp: block.timestamp,
};
}
```
The same algorithm we consider to have in other modules libraries.
### Validator state library
Keys API will also provide endpoints for working with validators. This library will fetch keys from beacon node, store data in database and support it in actual state.
#### DB Models
Library will contain `Validator`, `Meta` tables.
```typescript=
enum VALIDATOR_STATUS {
PENDING_INITIALIZED = 'pending_initialized',
PENDING_QUEUED = 'pending_queued',
ACTIVE_ONGOING = 'active_ongoing',
ACTIVE_EXITING = 'active_exiting'
ACTIVE_SLASHED = 'active_slashed',
EXITED_UNSLASHED = 'exited_unslashed',
EXITED_SLASHED = 'exited_slashed',
WITHDRAWAL_POSSIBLE = 'withdrawal_possible',
WITHDRAWAL_DONE = 'withdrawal_done'
}
interface Validator {
id: number;
status: VALIDATOR_STATUS;
key: string;
}
interface Meta {
slot: integer;
slotRoot: string;
// execution layer data
blockNumber: integer;
blockHash: string;
timestamp: integer;
}
```
#### Validators approximate update algorithm
Algorithm of fetching validators, update and store in database:
1. Fetch `CL` finalized block by [request](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlockHeaders) to Beacon node to get `root` and `slot` of last finalized block
```json
{
"finalized": true
"data": {
"root": "string",
"header": {
"message": {
"slot": "number"
}
}
}
}
```
2. Compare `slot` from `Meta` table and received `slot` on 1 step. If `Meta` is empty or slot number in database less than received `slot` on 1 step, go on 3 step. Otherwise stop algorithm execution.
3. Fetch EL block information by [request](https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2) to Beacon node by slot root parameter. This information is needed to check modules' keys relevance compare to validators in API's database:
```
{
"data": {
"message": {
"description": "The BeaconBlock object from the CL Bellatrix spec.",
"body": {
"execution_payload": {
"description": "The ExecutionPayload object from the CL Bellatrix spec.",
"block_number": "number",
"timestamp": "string"
"block_hash": "string"
}
}
}
}
}
```
Based on [10 assumption](https://hackmd.io/fv8btyNTTOGLZI6LqYyYIg?both#Assumptions) we need `RegistryMeta.block_number` be newer than or equal to `ValidatorMeta.block_number`.
4. Fetch all validators for `slot_root` received on 1 step by [request](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getStateValidators).
5. Write `Meta` and `Validators` in transaction in database.
Cron job with this algorithm will be run every 10 seconds.
```typescript=
// validator's library part:
interface ConsesusValidatorsAndMetadata {
validators: Validator[];
meta: Meta;
}
class ValidatorsStateService {
update() {
const CONSENSUS_NODE = 'http://127.0.0.1:5050';
const BLOCK_ID = 'finalized';
const CONSENSUS_BLOCK_HEADERS_ENDPOINT = `${CONSENSUS_NODE}/eth/v1/beacon/headers/${BLOCK_ID}`;
const headersResp = await fetch(CONSENSUS_BLOCK_HEADERS_ENDPOINT);
const headersJson = await headersResp.json();
const {
data: {
root: currSlotRoot,
header: {
message: { slot: currSlot },
},
},
} = headersJson;
// Get meta from database
const { slot: prevSlot } = await getMetaDataFromStorage();
if ( prevSlot >= currSlot ) {
// We have already had validators for this slot
return;
}
const CONSENSUS_BLOCK_DETAILS_ENDPOINT = `${CONSENSUS_NODE}/eth/v2/beacon/blocks/${currSlotRoot}`;
// Get EL block information
// This information is needed to check RegistryKey data relevance compare to validators in API's database
const blockDetailsResp = await fetch(CONSENSUS_BLOCK_DETAILS_ENDPOINT);
const blockDetailsJson = await blockDetailsResp.json();
const {
data: {
message: {
body: {
execution_payload: { block_number, timestamp, block_hash },
},
},
},
} = blockDetailsJson;
const CONSENSUS_STATE_VALIDATORS_ENDPOINT = `${CONSENSUS_NODE}/eth/v1/beacon/states/${currSlotRoot}/validators`;
const validators = await fetch(CONSENSUS_STATE_VALIDATORS_ENDPOINT);
const validatorsJson = await validators.json();
// save in database
const clMeta = {
root: currSlotRoot,
slot: currSlot,
blockNumber: block_number,
timestamp,
blockHash: block_hash
};
storeValidatorsAndMetaTrans(validatorsJson, clMeta);
}
getValidators(pubkeys: string[]): Promise<ConsesusValidatorsAndMetadata> {
...
}
getLidoOldestValidators(pubkeys: string[], statuses: string[], validatorPercent: number): Promise<ConsesusValidatorsAndMetadata> {
...
}
}
// Keys API
// cron job
const job = new CronJob(
CronExpression.EVERY_10_SECONDS,
() => validatorsStateService.update()
);
job.start();
...
// HTTP request
getLidoCuratedValidators(operatorId) {
const { pubkeys, meta: elMeta } = await registryStorage.getKeysAndMeta({operatorIndex: operatorId, used: true});
const { validators, meta: clMeta} = await validatorsStateService.getValidators(pubkeys);
// base on 10 assumption
if (elMeta.blockNumber < clMeta.blockNumber ) {
throw new InternalServerErrorException();
}
return { validators };
}
```
## API
### /keys
**GET** `/v1/keys`
Endpoint returns list of keys for all modules.
Query:
- `used` - filter for used/unused keys. Possible values: true/false;
- `operatorIndex` - filter for keys of operator with index `operatorIndex`;
```typescript=
interface ELBlockSnapshot {
blockNumber: number;
blockHash: string;
blockTimestamp: number;
}
interface Key {
key: string;
depositSignature: string;
used: boolean;
operatorIndex: number;
}
interface ModuleKey extends Key {
moduleAddress: string;
}
class Response {
data: ModuleKey[];
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/keys' \
-H 'accept: application/json'
```
:::warning
Response of this endpoint could be very large but we can’t have a pagination here since data could be updated in the process.
:::
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/keys/{pubkey}`
Return key by public key with basic fields. `pubkey` should be in lowercase.
```typescript=
class Response {
data: ModuleKey[];
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3005/v1/keys/pubkey' \
-H 'accept: application/json'
```
**POST** `/v1/keys/find`
Returns all keys found in db.
Request body:
```typescript=
interface RequestBody {
// public keys in lowercase
pubkeys: string[];
}
```
Response:
```typescript=
class Response {
data: ModuleKey[];
meta: {
elBlockSnapshot: ElBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
```
Example:
```
curl -X 'POST' \
'http://localhost:3000/v1/keys/find' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"pubkeys": ["pubkey0 "]}'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
### /modules
**GET** `/v1/modules`
Endpoint returns list of staking router modules.
```typescript=
interface Module {
nonce: number;
type: string;
// unique id of the module
id: number;
// address of module
stakingModuleAddress: string;
// rewarf fee of the module
moduleFee: number;
// treasury fee
treasuryFee: number;
// target percent of total keys in protocol, in BP
targetShare: number;
// module status if module can not accept the deposits or can participate in further reward distribution
status: number;
// name of module
name: string;
// block.timestamp of the last deposit of the module
lastDepositAt: number;
// block.number of the last deposit of the module
lastDepositBlock: number;
}
interface ELBlockSnapshot {
blockNumber: number,
blockHash: string,
blockTimestamp: number,
}
class Reponse {
data: Module[];
elBlockSnapshot: ElBlockSnapshot;
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/modules/{module_id}`
`module_id` - staking router module contact address or id;
Endpoint return information about staking router module;
```typescript=
interface Module {
nonce: number;
type: string;
/// @notice unique id of the module
id: number;
/// @notice address of module
stakingModuleAddress: string;
/// @notice rewarf fee of the module
moduleFee: number;
/// @notice treasury fee
treasuryFee: number;
/// @notice target percent of total keys in protocol, in BP
targetShare: number;
/// @notice module status if module can not accept the deposits or can participate in further reward distribution
status: number;
/// @notice name of module
name: string;
/// @notice block.timestamp of the last deposit of the module
lastDepositAt: number;
/// @notice block.number of the last deposit of the module
lastDepositBlock: number;
}
interface ELBlockSnapshot {
blockNumber: number;
blockHash: string;
blockTimestamp: number;
}
class Reponse {
data: Module;
elBlockSnapshot: ElBlockSnapshot;
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundResponse implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules/1' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
### /modules/keys/
**GET** `/v1/modules/keys/`
Return keys for all modules grouped by staking router module.
Query:
- `used` - filter for used/unused keys. Possible values: true/false;
- `operatorIndex` - filter for keys of operator with index `operatorIndex`;
```typescript=
interface Module {
// current KeyOpIndex
nonce: number;
// type of module
type: string;
/// @notice unique id of the module
id: number;
/// @notice address of module
stakingModuleAddress: string;
/// @notice rewarf fee of the module
moduleFee: number;
/// @notice treasury fee
treasuryFee: number;
/// @notice target percent of total keys in protocol, in BP
targetShare: number;
/// @notice module status if module can not accept the deposits or can participate in further reward distribution
status: number;
/// @notice name of module
name: string;
/// @notice block.timestamp of the last deposit of the module
lastDepositAt: number;
/// @notice block.number of the last deposit of the module
lastDepositBlock: number;
}
class Response{
data: {
keys: Key[];
module: Module;
}[];
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules/keys?used=true&operatorIndex=1' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/modules/{module_id}/keys`
`module_id` - staking router module contact address or id;
Endpoint returns list of keys for module.
Query:
- `used` - filter for used/unused keys. Possible values: true/false;
- `operatorIndex` - filter for keys of operator with index `operatorIndex`;
Response:
Response depends on `module type`
```typescript=
interface Key {
key: string;
depositSignature: string;
used: boolean;
operatorIndex: number;
}
interface RegistryKey extends Key {
index: number;
}
interface CommunityKey extends Key {
}
class Response {
data: {
keys: RegistryKey[] | CommunityKey[];
module: Module;
};
meta: {
elBlockSnapshot: ELBlockSnapshot
}
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules/1/keys?used=true&operatorIndex=1' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**POST** `/v1/modules/{module_id}/keys/find`
`module_id` - staking router module contact address or id;
Returns all keys found in db.
Request body:
```typescript
interface RequestBody {
// public keys in lowercase
pubkeys: string[];
}
```
Response:
```typescript=
interface Key {
key: string;
depositSignature: string;
used: boolean;
operatorIndex: number;
}
interface RegistryKey extends Key {
index: number;
}
interface CommunityKey extends Key {
}
class Response {
data: {
keys: RegistryKey[] | CommunityKey[];
module: Module;
};
meta: {
elBlockSnapshot: ElBlockSnapshot
}
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'POST' \
'http://localhost:3000/v1/modules/1/keys/find' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"pubkeys": [
"pubkey"
]
}'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
### /validators
**GET** `/v1/modules/{module_id}/validators/validator-exits-to-prepare/{operator_id}`
`module_id` - staking router module contact address or id;
This endpoint will return N of oldest lido validators for earliest epoch when voluntary exit can be processed for specific node operator and specific `StakingRouter` module. Node operator will use this validators list for preparing pre-sign exit messages. API will find `used` keys of node operator and find for these public keys N oldest validators in `Validator` table. We consider active validators (`active_ongoing` status) or validators in `pending_initialized`, `pending_queued` statuses. Module tables state fetched from `EL` should be newer than `Validator` table state fetched from `CL`. Otherwise API will return error on request.
Query:
Only one filter is available. If both parameters are provided, `percent` has a high priority.
- `percent` - Percent of validators to exit. Default value is 10.
- `max_amount` - Number of validators to exit. If validators number less than amount, return all validators.
```typescript=
interface Validator {
validatorIndex: number;
key: string;
}
interface CLBlockSnapshot {
epoch: number;
root: number;
slot: number;
blockNumber: number;
timestamp: number;
blockHash: string;
};
class Response {
data: Validator[];
meta: {
clBlockSnapshot: CLBlockSnapshot;
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
class InternalServerErrorExceptionNotActualData implements HttpException {
statusCode: number = 500;
message: string = 'Last Execution Layer block number in our database older than last Consensus Layer';
}
class InternalServerErrorExceptionDisable implements HttpException {
statusCode: number = 500;
message: string = 'Validators Registry is disabled. Check environment variables';
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules/1/validators/validator-exits-to-prepare/1?percent=10' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/modules/{module_id}/validators/generate-unsigned-exit-messages/{operator_id}`
`module_id` - staking router module contact address or id;
Return unsigned exit messages for N oldest validators for earliest epoch when voluntary exit can be processed.
Query:
Only one filter is available. If both parameters are provided, `percent` has a high priority.
- `percent` - Percent of validators to exit. Default value is 10.
- `max_amount` - Number of validators to exit. If validators number less than amount, return all validators.
```typescript=
interface ExitPresignMessage {
validator_index: string;
epoch: string;
}
interface CLBlockSnapshot {
epoch: number;
root: string;
slot: number;
blockNumber: number;
timestamp: number;
blockHash: string;
};
class Response {
data: ExitPresignMessage[];
meta: {
clBlockSnapshot: CLBlockSnapshot;
}
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
class InternalServerErrorExceptionNotActualData implements HttpException {
statusCode: number = 500;
message: string = 'Last Execution Layer block number in our database older than last Consensus Layer';
}
class InternalServerErrorExceptionDisable implements HttpException {
statusCode: number = 500;
message: string = 'Validators Registry is disabled. Check environment variables';
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules/1/validators/generate-unsigned-exit-messages/1?percent=10' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
Approximate algorithm:
```typescript=
getExitsPresign(operatorId: number, messages: boolean) {
const { keys: pubkeys, meta: elMeta } = await registryStorage.getKeysAndMeta({operatorIndex: operatorId, used: true});
const { validators, meta: clMeta } = await ValidatorsLibrary.getLidoOldestValidators(pubkeys, ['active_ongoing', 'pending_initialized', 'pending_queued'], 10)
if (elMeta.blockNumber < clMeta.blockNumber ) {
throw new InternalServerErrorException();
}
if (messages) {
return validators.map((v) => ({ validator_index: v.id, epoch: calculateEpoch(clMeta.slot) })
}
return validators.map((v) => ({ validator_index: v.id, key: v.key}));
}
calculateEpoch(slot) {}
```
### /operators
**GET** `/v1/operators`
List of operators grouped by staking router module
Query
```typescript=
interface Operator {
index: number;
active: boolean;
}
interface CuratedOperator extends Operator {
name: string;
rewardAddress: string;
stakingLimit: number;
stoppedValidators: number;
totalSigningKeys: number;
usedSigningKeys: number;
}
interface CommunityOperator extends Operator {
}
class Response {
data: {
operators: CuratedOperator[] | CommunityOperator[];
module: Module;
}[];
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/operators' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/modules/{module_id}/operators/`
`module_id` - staking router module contact address or id;
List of SR module operators
```typescript=
class Response {
data: {
operators: CuratedOperator[] | CommunityOperator[];
module: Module;
};
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3000/v1/modules/1/operators' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/modules/{module_id}/operators/{operator_id}`
`module_id` - staking router module contact address or id;
`operator_id` - operator index;
List of SR module operators
```typescript=
class Response {
data: {
operators: CuratedOperator | CommunityOperator;
module: Module;
};
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3005/v1/modules/1/operators/1' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
**GET** `/v1/modules/{module_id}/operators/keys`
`module_id` - staking router module contact address or id;
Query:
- `used` - filter for used/unused keys. Possible values: true/false;
- `operatorIndex` - filter for keys of operator with index `operatorIndex`;
```typescript=
class Response {
data: {
keys: RegistryKey[] | CommunityKey[];
operators: CuratedOperator[] | CommunityOperator[];
module: Module;
};
meta: {
elBlockSnapshot: ELBlockSnapshot
};
}
interface HttpException {
statusCode: number;
message: string;
}
class TooEarlyResponse implements HttpException {
statusCode: number = 425;
message: string = "Too early response";
}
class NotFoundException implements HttpException {
statusCode: number = 404;
}
```
Example:
```
curl -X 'GET' \
'http://localhost:3005/v1/modules/1/operators/1' \
-H 'accept: application/json'
```
:::warning
If API returns 425 code, it means database is not ready for work
:::
### /status
**GET** /v1/status
```typescript=
class Response {
// keys api version
appVersion: string;
chainId: number;
elBlockSnapshot: ELBlockSnapshot;
clBlockSnapshot: CLBlockSnapshot;
}
```
## Prometheus metrics
## Roadmap
#### Future iterations
- Make modules optional;
- Research possibility of gRPC as a transport if Keys API is used as a microservice;
- Implement absraction for staking router ;(possibility to have flexible list of modules, choose chain etc);
- Use this service without db as CLI;