KAPI was created for easy fetching keys of all staking modules. Staking Module interface contains type that was specially designed for KAPI to easy understand that implementation it should use for working with module's keys.
For example:
```
simple-dvt-onchain-v1 -> RegistryModule
curated-onchain-v1 -> RegistryModule
```
There can be few modules with the same type. They all should have the same implementation (contract's ABI). For KAPI it means for these modules it can reuse the same algorithm for fetching and saving keys. Also different types of modules can have the same implementation as in the example above.
At the moment KAPI knows only one type: `curated-onchain-v1`.
```
export enum STAKING_MODULE_TYPE {
CURATED_ONCHAIN_V1_TYPE = 'curated-onchain-v1',
}
```
List of all modules available in SR contract. And KAPI already uses this method to get list of all staking Modules. If KAPI doesnt know type it will exit with error.
In the nearest future will be implemented DVT module and Community module. DVT module will be inherited from Curated module. So it should have the same implementation. Module type possibly will be simple-dvt-onchain-v1. Community module will have different implementation.
This proposal will address two problems:
- The need to prepare an interface for easy integration of any staking modules;
- The need to adapt code from the registry library to work with modules that have an identical implementation;
For convenience [registry code was moved to KAPI ](https://github.com/lidofinance/lido-keys-api/pull/131).
## Changes in registry implementation
Registry library currently work only with NOR contract. We need to change the way we store data (keys, meta and operators) and how we fetch data from contract.
### Storage
Currently, the `registry_key` table only contains keys from the Curated module. The keys from the `Simple DVT` module will have the same fields. Therefore, it is feasible to store the keys of these two modules, along with others that have the same implementation, in the same table. It will allow the reuse of "registry" code that works with the database.
However, we should have the ability to differentiate keys of modules from each other. To achieve this, we can add a contract address field to the table.
Adding this field to the table also simplifies the process of generating the response to the query [/v1/keys](https://keys-api.lido.fi/api/static/index.html#/keys/KeysController_get), as we can retrieve the contract address directly from the database instead of having to iterate through the list of keys and add the address to each one separately.
The same improvement is proposed for the `registry_operator` table.
The most interesting question here is the storage of meta information. Currently, the `registry_meta` table contains only one row with information about the execution layer block from which that information was retrieved and nonce value of curated staking module. In the most of endpoints [we return in meta only ElBlockSnapshot](https://keys-api.lido.fi/api/static/index.html#/keys/KeysController_getByPubkey) we return `ElBlockSnapshot`. But `nonce` we use only when return information about [modules](https://keys-api.lido.fi/api/static/index.html#/modules/SRModulesController_getModules). Since we store data for numerous modules within the same `EL block` (block hash) and update them in a single transaction, it is unnecessary to duplicate EL data for each module in separate tables.
To achieve this, we can split the current `registry_meta` table into two tables: `el_meta` and `staking_module`. The `el_meta` table will contain `block_number, block_hash, and timestamp` as it did in the original `registry_meta`, and will always have a single row. The `staking_module` table will store information about all the modules we retrieve from the `Staking Router`, along with their nonce values (the nonce obtained from the module's contract and other module information from the `SR` contract). Storing module information in a database will help in future iterations to separate the `API` component from the jobs that `update keys` and `validators`, since we currently cache staking module information. Also currently to response on [/v1/modules](https://keys-api.lido.fi/api/static/index.html#/modules/SRModulesController_getModules) request we get module's data from cache and nonce from `meta` table. In the proposed version we will just read `staking_module` table.
Visualization https://miro.com/app/board/uXjVM1RSULU=/?share_link_id=488170147188
### Keys' fetching from blockchain
The second part is to make the registry work with all contracts that have the same implementation.
For example methods for fetching keys from contract will be changed like this:
```typescript=
private getSRContract(contractAddress: string) {
return Registry__factory.connect(contractAddress, this.provider);
}
public async fetchOne(contractAddress: string, operatorIndex: number, keyIndex: number, overrides: CallOverrides = {}): Promise<RegistryKey> {
const keyData = await this.getSRContract(contractAddress).getSigningKey(operatorIndex, keyIndex, overrides as any);
const { key, depositSignature, used } = keyData;
return {
operatorIndex,
index: keyIndex,
key,
depositSignature,
used,
};
}
```
## Interface
Current API already support multiple staking modules (https://hackmd.io/fv8btyNTTOGLZI6LqYyYIg?view#API). However endpoints' code uses `if clauses` to check type of modules and work only with `curated-onchain-type`.
We want to add config that will match type with implementation:
```typescript=
type StakingModuleImpl = typeof CuratedModuleService;
export const config: // = {
Record<STAKING_MODULE_TYPE, StakingModuleImpl> = {
[STAKING_MODULE_TYPE.CURATED_ONCHAIN_V1_TYPE]: CuratedModuleService,
// In future will be added dvt staking module with the same implementation
// now kapi will now correctly work with it as module contract address is hardcoded
// [STAKING_MODULE_TYPE.DVT_ONCHAIN_V1_TYPE]: CuratedModuleService,
};
```
Simplify the code of endpoints by transferring all logic related to working with modules to the StakingRouterService module. This module will provide an update method to actualize the KAPI database, as well as different methods to fetch data from all modules or a specific module (https://github.com/lidofinance/lido-keys-api/pull/135/files#diff-3f6afce38894c19134490a8ec1c439a8decdefe4f3bef75373198591b0e446d0).
Provide common interface for working with staking modules in KAPI:
``` typescript=
export interface StakingModuleInterface {
updateKeys(contractAddress: string, blockHashOrBlockTag: string | number): Promise<void>;
getKeysWithMetaByPubkey(contractAddress: string, pubkey: string): Promise<{ keys: KeyEntity[]; meta: MetaEntity | null }>;
getKeysWithMetaByPubkeys(contractAddress: string, pubkeys: string[]): Promise<{ keys: KeyEntity[]; meta: MetaEntity | null }>;
getKeysWithMeta(contractAddress: string, filters: KeysFilter): Promise<{ keys: KeyEntity[]; meta: MetaEntity | null }>;
getMetaDataFromStorage(contractAddress: string): Promise<MetaEntity | null>;
getOperatorsWithMeta(contractAddress: string): Promise<{ operators: OperatorEntity[]; meta: MetaEntity | null }>;
getOperatorByIndex(contractAddress: string, index: number): Promise<{ operator: OperatorEntity | null; meta: MetaEntity | null }>;
getData(contractAddress: string, filters: KeysFilter): Promise<{
operators: OperatorEntity[];
keys: KeyEntity[];
meta: MetaEntity | null;
}>;
}
```
## Questions
1. If we are reading the list of modules from the SR and a module is missing that we have data for in the database, what should we do?
2. Do we need to get rid of the module ID?
3. How can foreign key (FK) constraints help us, and can they?
## How data update will look for all modules ?
we should figure out how this code will be changed :
```typescript=
const prevMeta = await this.getMetaDataFromStorage();
const currMeta = await this.getMetaDataFromContract(blockHashOrBlockTag);
// now nonce we save in
const isSameContractState = compareMeta(prevMeta, currMeta);
this.logger.log('Collected metadata', { prevMeta, currMeta });
const previousBlockNumber = prevMeta?.blockNumber ?? -1;
const currentBlockNumber = currMeta.blockNumber;
// TODO: maybe blockhash instead blocknumber?
if (previousBlockNumber > currentBlockNumber) {
this.logger.warn('Previous data is newer than current data');
return;
}
if (isSameContractState) {
this.logger.debug?.('Same state, no data update required', { currMeta });
await this.entityManager.transactional(async (entityManager) => {
await entityManager.nativeDelete(RegistryMeta, {});
await entityManager.persist(new RegistryMeta(currMeta));
});
this.logger.debug?.('Updated metadata in the DB', { currMeta });
return;
}
```