From f098fe2b48bb1d89f105314ee4036b525383fb0d Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Fri, 31 May 2024 10:53:52 +0200 Subject: [PATCH] Implement proof of validation (#304) * Implement signDappnodeProofOfValidation * split cron * implement proover * fix cron test imports * use of modules exports * add envs * fix apis * add typ cast * fix proof of attestation cron * add headers to api standard * add pubkey to sign request * object destructuring * implement network as query paramter * fix typo * remove log * fix send query parameter * mark review fixes: rename proofOfValidation and timeout * rename to validation and collect stader tag * rename to DappnodeSignatureVerifier * move endpoint to params * add public to cron methods * add interval positive number check * add log if return in send proof of validation * do not throw error in loading envs * convert reloadValidators to function * fix test * remove path module * only send stakder keys --- packages/brain/src/calls/deleteValidators.ts | 13 +- packages/brain/src/calls/importValidators.ts | 25 +- packages/brain/src/calls/updateValidators.ts | 13 +- packages/brain/src/index.ts | 36 +- .../apiClients/DappnodeSignatureVerifier.ts | 31 ++ .../brain/src/modules/apiClients/index.ts | 1 + .../brain/src/modules/apiClients/standard.ts | 36 +- .../src/modules/apiClients/web3signer.ts | 57 ++- packages/brain/src/modules/cron/cron.ts | 35 ++ packages/brain/src/modules/cron/index.ts | 346 +----------------- .../src/modules/cron/reloadValidators.ts | 315 ++++++++++++++++ .../modules/cron/sendProofsOfValidation.ts | 99 +++++ packages/brain/src/modules/envs/index.ts | 43 ++- packages/brain/src/params.ts | 2 + .../unit/modules/apiClients/cron.unit.test.ts | 27 +- packages/brain/tsconfig.json | 1 + .../common/src/types/api/standard/types.ts | 1 + .../common/src/types/api/web3signer/types.ts | 29 +- 18 files changed, 698 insertions(+), 412 deletions(-) create mode 100644 packages/brain/src/modules/apiClients/DappnodeSignatureVerifier.ts create mode 100644 packages/brain/src/modules/cron/cron.ts create mode 100644 packages/brain/src/modules/cron/reloadValidators.ts create mode 100644 packages/brain/src/modules/cron/sendProofsOfValidation.ts diff --git a/packages/brain/src/calls/deleteValidators.ts b/packages/brain/src/calls/deleteValidators.ts index 170c8c01..62668d82 100644 --- a/packages/brain/src/calls/deleteValidators.ts +++ b/packages/brain/src/calls/deleteValidators.ts @@ -2,7 +2,12 @@ import { Web3signerDeleteRequest, Web3signerDeleteResponse, } from "@stakingbrain/common"; -import { cron, validatorApi, signerApi, brainDb } from "../index.js"; +import { + reloadValidatorsCron, + validatorApi, + signerApi, + brainDb, +} from "../index.js"; import logger from "../modules/logger/index.js"; /** @@ -20,7 +25,7 @@ export async function deleteValidators( try { // IMPORTANT: stop the cron. This removes the scheduled cron task from the task queue // and prevents the cron from running while we are deleting validators - cron.stop(); + reloadValidatorsCron.stop(); // Delete feeRecipient on Validator API for (const pubkey of deleteRequest.pubkeys) @@ -45,10 +50,10 @@ export async function deleteValidators( brainDb.deleteValidators(deleteRequest.pubkeys); // IMPORTANT: start the cron - cron.start(); + reloadValidatorsCron.start(); return web3signerDeleteResponse; } catch (e) { - cron.restart(); + reloadValidatorsCron.restart(); throw e; } } diff --git a/packages/brain/src/calls/importValidators.ts b/packages/brain/src/calls/importValidators.ts index 80ad3ea9..b9c43a77 100644 --- a/packages/brain/src/calls/importValidators.ts +++ b/packages/brain/src/calls/importValidators.ts @@ -13,7 +13,7 @@ import { STADER_POOL_FEE_RECIPIENT_PRATER, } from "@stakingbrain/common"; import { - cron, + reloadValidatorsCron, network, signerApi, validatorApi, @@ -46,7 +46,7 @@ export async function importValidators( try { // IMPORTANT: stop the cron. This removes the scheduled cron task from the task queue // and prevents the cron from running while we are importing validators - cron.stop(); + reloadValidatorsCron.stop(); const validators: ValidatorImportRequest[] = []; const validatorsToPost: ValidatorImportRequest[] = []; @@ -62,13 +62,13 @@ export async function importValidators( try { const feeRecipient = !["gnosis", "lukso"].includes(network) && - !isFeeRecipientEditable(validator.tag, postRequest.importFrom) + !isFeeRecipientEditable(validator.tag, postRequest.importFrom) ? await getNonEditableFeeRecipient( - pubkey, - validator.tag as NonEditableFeeRecipientTag, - network, - validator.feeRecipient - ) + pubkey, + validator.tag as NonEditableFeeRecipientTag, + network, + validator.feeRecipient + ) : validator.feeRecipient; logger.info(`Setting ${feeRecipient} as fee recipient for ${pubkey}`); @@ -126,7 +126,8 @@ export async function importValidators( web3signerPostResponse.data[index].message += ". Check that the keystore file format is valid and the password is correct."; logger.error( - `Error importing keystore for pubkey ${shortenPubkey(pubkey)}: ${web3signerPostResponse.data[index].message + `Error importing keystore for pubkey ${shortenPubkey(pubkey)}: ${ + web3signerPostResponse.data[index].message }` ); } else if (postStatus === "duplicate") { @@ -140,7 +141,7 @@ export async function importValidators( web3signerPostResponse.data.push(...wrongFeeRecipientResponse); if (validatorsToPost.length === 0) { - cron.start(); + reloadValidatorsCron.start(); return web3signerPostResponse; } @@ -191,10 +192,10 @@ export async function importValidators( ); // IMPORTANT: start the cron - cron.start(); + reloadValidatorsCron.start(); return web3signerPostResponse; } catch (e) { - cron.restart(); + reloadValidatorsCron.restart(); throw e; } } diff --git a/packages/brain/src/calls/updateValidators.ts b/packages/brain/src/calls/updateValidators.ts index 77b6f371..1d476408 100644 --- a/packages/brain/src/calls/updateValidators.ts +++ b/packages/brain/src/calls/updateValidators.ts @@ -4,7 +4,7 @@ import { isFeeRecipientEditable, PubkeyDetails, } from "@stakingbrain/common"; -import { cron, brainDb, validatorApi } from "../index.js"; +import { reloadValidatorsCron, brainDb, validatorApi } from "../index.js"; import logger from "../modules/logger/index.js"; import { ActionRequestOrigin } from "@stakingbrain/common"; @@ -21,7 +21,7 @@ export async function updateValidators( try { // IMPORTANT: stop the cron. This removes the scheduled cron task from the task queue // and prevents the cron from running while we are importing validators - cron.stop(); + reloadValidatorsCron.stop(); const dbData = brainDb.getData(); @@ -30,7 +30,10 @@ export async function updateValidators( customValidatorUpdateRequest.filter( (validator) => dbData[prefix0xPubkey(validator.pubkey)] && - isFeeRecipientEditable(dbData[prefix0xPubkey(validator.pubkey)].tag, requestFrom) + isFeeRecipientEditable( + dbData[prefix0xPubkey(validator.pubkey)].tag, + requestFrom + ) ); if (editableValidators.length === 0) { @@ -58,9 +61,9 @@ export async function updateValidators( ); // IMPORTANT: start the cron - cron.start(); + reloadValidatorsCron.start(); } catch (e) { - cron.restart(); + reloadValidatorsCron.restart(); throw e; } } diff --git a/packages/brain/src/index.ts b/packages/brain/src/index.ts index 41b9ebfd..c0d153ff 100644 --- a/packages/brain/src/index.ts +++ b/packages/brain/src/index.ts @@ -7,6 +7,7 @@ import { Beaconchain, BeaconchaApi, ValidatorApi, + DappnodeSignatureVerifier, } from "./modules/apiClients/index.js"; import { startUiServer, @@ -15,7 +16,11 @@ import { import * as dotenv from "dotenv"; import process from "node:process"; import { params } from "./params.js"; -import { Cron } from "./modules/cron/index.js"; +import { + CronJob, + reloadValidators, + sendProofsOfValidation, +} from "./modules/cron/index.js"; logger.info(`Starting brain...`); @@ -38,6 +43,9 @@ export const { signerUrl, token, host, + shareDataWithDappnode, + validatorsMonitorUrl, + shareCronInterval, tlsCert, } = loadStakerConfig(); logger.debug( @@ -69,6 +77,10 @@ export const beaconchainApi = new Beaconchain( { baseUrl: beaconchainUrl }, network ); +export const dappnodeSignatureVerifierApi = new DappnodeSignatureVerifier( + network, + validatorsMonitorUrl +); // Create DB instance export const brainDb = new BrainDataBase( @@ -88,19 +100,25 @@ await brainDb.initialize(signerApi, validatorApi); logger.debug(brainDb.data); // CRON -export const cron = new Cron( - 60 * 1000, - signerApi, - signerUrl, - validatorApi, - brainDb +export const reloadValidatorsCron = new CronJob(60 * 1000, () => + reloadValidators(signerApi, signerUrl, validatorApi, brainDb) +); +reloadValidatorsCron.start(); +const proofOfValidationCron = new CronJob(shareCronInterval, () => + sendProofsOfValidation( + signerApi, + brainDb, + dappnodeSignatureVerifierApi, + shareDataWithDappnode + ) ); -cron.start(); +proofOfValidationCron.start(); // Graceful shutdown function handle(signal: string): void { logger.info(`${signal} received. Shutting down...`); - cron.stop(); + reloadValidatorsCron.stop(); + proofOfValidationCron.stop(); brainDb.close(); uiServer.close(); launchpadServer.close(); diff --git a/packages/brain/src/modules/apiClients/DappnodeSignatureVerifier.ts b/packages/brain/src/modules/apiClients/DappnodeSignatureVerifier.ts new file mode 100644 index 00000000..82c69f9f --- /dev/null +++ b/packages/brain/src/modules/apiClients/DappnodeSignatureVerifier.ts @@ -0,0 +1,31 @@ +import { StandardApi } from "./index.js"; +import { + Network, + DappnodeSignatureVerifierPostRequest, +} from "@stakingbrain/common"; + +export class DappnodeSignatureVerifier extends StandardApi { + private dappnodeSignEndpoint = "/signatures"; + + constructor(network: Network, validatorsMonitorUrl: string) { + super( + { + baseUrl: validatorsMonitorUrl, + }, + network + ); + } + + public async sendProofsOfValidation( + proofOfValidations: DappnodeSignatureVerifierPostRequest[] + ): Promise { + await this.request({ + method: "POST", + endpoint: `${this.dappnodeSignEndpoint}?network=${encodeURIComponent( + this.network.toString() + )}`, + body: JSON.stringify(proofOfValidations), + timeout: 10000, + }); + } +} diff --git a/packages/brain/src/modules/apiClients/index.ts b/packages/brain/src/modules/apiClients/index.ts index 0f75dfc3..80a40731 100644 --- a/packages/brain/src/modules/apiClients/index.ts +++ b/packages/brain/src/modules/apiClients/index.ts @@ -3,4 +3,5 @@ export { Beaconchain } from "./beaconchain.js"; export { ValidatorApi } from "./validator.js"; export { StandardApi } from "./standard.js"; export { Web3SignerApi } from "./web3signer.js"; +export { DappnodeSignatureVerifier } from "./DappnodeSignatureVerifier.js"; export { ApiError } from "./error.js"; diff --git a/packages/brain/src/modules/apiClients/standard.ts b/packages/brain/src/modules/apiClients/standard.ts index 6387da11..832cf34f 100644 --- a/packages/brain/src/modules/apiClients/standard.ts +++ b/packages/brain/src/modules/apiClients/standard.ts @@ -54,13 +54,15 @@ export class StandardApi { method, endpoint, body, - setOrigin = false + headers, + timeout, }: { - method: AllowedMethods, - endpoint: string, + method: AllowedMethods; + endpoint: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - body?: any, - setOrigin?: boolean + body?: any; + headers?: Record; + timeout?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any }): Promise { let req: http.ClientRequest; @@ -72,10 +74,26 @@ export class StandardApi { req = https.request(this.requestOptions); } else req = http.request(this.requestOptions); - if (setOrigin) - req.setHeader("Origin", this.network === "mainnet" - ? "http://brain.web3signer.dappnode" - : `http://brain.web3signer-${this.network}.dappnode`); + if (timeout) { + req.setTimeout(timeout, () => { + const error = new ApiError({ + name: "TimeoutError", + message: `Request to ${endpoint} timed out.`, + errno: -1, + code: "ETIMEDOUT", + path: endpoint, + syscall: method, + hostname: this.requestOptions.hostname || undefined, + }); + req.destroy(error); + }); + } + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + req.setHeader(key, value); + } + } if (body) { req.setHeader("Content-Length", Buffer.byteLength(body)); diff --git a/packages/brain/src/modules/apiClients/web3signer.ts b/packages/brain/src/modules/apiClients/web3signer.ts index 5d57c45e..fc6db359 100644 --- a/packages/brain/src/modules/apiClients/web3signer.ts +++ b/packages/brain/src/modules/apiClients/web3signer.ts @@ -5,9 +5,11 @@ import { Web3signerDeleteResponse, Web3signerGetResponse, Web3signerHealthcheckResponse, + Web3signerPostSignDappnodeRequest, prefix0xPubkey, Web3SignerPostSignvoluntaryexitRequest, Web3SignerPostSignvoluntaryexitResponse, + Web3signerPostSignDappnodeResponse, } from "@stakingbrain/common"; import { StandardApi } from "./standard.js"; import path from "node:path"; @@ -23,6 +25,12 @@ export class Web3SignerApi extends StandardApi { */ private signEndpoint = "/api/v1/eth2/sign"; + /** + * Signs proof of validation with timestamp and platform + * @see TODO: not in doc yet + */ + private signExtEndpoint = "/api/v1/eth2/ext/sign"; + /** * Local Key Manager endpoint * @see https://ethereum.github.io/keymanager-APIs/#/Local%20Key%20Manager/ @@ -36,7 +44,17 @@ export class Web3SignerApi extends StandardApi { private serverStatusEndpoint = "/healthcheck"; /** - * + * Origine header required by web3signer + */ + private originHeader = { + Origin: + this.network === "mainnet" + ? "http://brain.web3signer.dappnode" + : `http://brain.web3signer-${this.network}.dappnode`, + }; + + /** + * Signs a voluntary exit for the validator with the specified public key */ public async signVoluntaryExit({ signerVoluntaryExitRequest, @@ -50,7 +68,7 @@ export class Web3SignerApi extends StandardApi { method: "POST", endpoint: path.join(this.signEndpoint, pubkey), body: JSON.stringify(signerVoluntaryExitRequest), - setOrigin: true, + headers: this.originHeader, }); } catch (e) { e.message += `Error signing (POST) voluntary exit for validator index ${signerVoluntaryExitRequest.voluntary_exit.validator_index}. `; @@ -58,6 +76,33 @@ export class Web3SignerApi extends StandardApi { } } + /** + * Signs a proof of validation for the validator with the specified public key + */ + public async signDappnodeProofOfValidation({ + signerDappnodeSignRequest, + pubkey, + }: { + signerDappnodeSignRequest: Web3signerPostSignDappnodeRequest; + pubkey: string; + }): Promise { + try { + return await this.request({ + method: "POST", + endpoint: path.join(this.signExtEndpoint, pubkey), + body: JSON.stringify(signerDappnodeSignRequest), + headers: { + ...this.originHeader, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + } catch (e) { + e.message += `Error signing (POST) proof of validation for validator ${pubkey}. `; + throw e; + } + } + /** * Import remote keys for the validator client to request duties for. * @see https://ethereum.github.io/keymanager-APIs/#/Local%20Key%20Manager/ListKeys @@ -71,7 +116,7 @@ export class Web3SignerApi extends StandardApi { method: "POST", endpoint: this.localKeymanagerEndpoint, body: JSON.stringify(postRequest), - setOrigin: true, + headers: this.originHeader, })) as Web3signerPostResponse; } catch (e) { e.message += `Error importing (POST) keystores to remote signer. `; @@ -98,7 +143,7 @@ export class Web3SignerApi extends StandardApi { method: "DELETE", endpoint: this.localKeymanagerEndpoint, body: data, - setOrigin: true, + headers: this.originHeader, })) as Web3signerDeleteResponse; } catch (e) { e.message += `Error deleting (DELETE) keystores from remote signer. `; @@ -115,7 +160,7 @@ export class Web3SignerApi extends StandardApi { return (await this.request({ method: "GET", endpoint: this.localKeymanagerEndpoint, - setOrigin: true, + headers: this.originHeader, })) as Web3signerGetResponse; } catch (e) { e.message += `Error getting (GET) keystores from remote signer. `; @@ -132,7 +177,7 @@ export class Web3SignerApi extends StandardApi { return (await this.request({ method: "GET", endpoint: this.serverStatusEndpoint, - setOrigin: true, + headers: this.originHeader, })) as Web3signerHealthcheckResponse; } catch (e) { e.message += `Error getting (GET) server status. Is Web3Signer running? `; diff --git a/packages/brain/src/modules/cron/cron.ts b/packages/brain/src/modules/cron/cron.ts new file mode 100644 index 00000000..ef0a2de6 --- /dev/null +++ b/packages/brain/src/modules/cron/cron.ts @@ -0,0 +1,35 @@ +import { setInterval, clearInterval } from "timers"; +import logger from "../logger/index.js"; + +export class CronJob { + private task: NodeJS.Timeout | null = null; + private interval: number; + + constructor(interval: number, private jobFunction: () => Promise) { + if (interval <= 0) throw Error("Interval must be a positive number."); + this.interval = interval; + } + + public start(): void { + if (this.task === null) { + logger.info(`Starting cron job with interval ${this.interval}ms`); + this.task = setInterval(async () => { + await this.jobFunction(); + }, this.interval); + } else + logger.warn("Task is already running. Use restart to restart the job."); + } + + public stop(): void { + if (this.task !== null) { + logger.info("Stopping cron job."); + clearInterval(this.task); + this.task = null; + } else logger.warn("Task is not running."); + } + + public restart(): void { + this.stop(); + this.start(); + } +} diff --git a/packages/brain/src/modules/cron/index.ts b/packages/brain/src/modules/cron/index.ts index de7890c3..27dd4f76 100644 --- a/packages/brain/src/modules/cron/index.ts +++ b/packages/brain/src/modules/cron/index.ts @@ -1,343 +1,3 @@ -import { StakingBrainDb } from "@stakingbrain/common"; -import { ValidatorApi, Web3SignerApi, ApiError } from "../apiClients/index.js"; -import { BrainDataBase } from "../db/index.js"; -import logger from "../logger/index.js"; - -export class Cron { - private defaultInterval: number; - private timer: NodeJS.Timer | undefined; - private signerApi: Web3SignerApi; - private signerUrl: string; - private validatorApi: ValidatorApi; - private brainDb: BrainDataBase; - - constructor( - defaultInterval: number, - signerApi: Web3SignerApi, - signerUrl: string, - validatorApi: ValidatorApi, - brainDb: BrainDataBase - ) { - this.defaultInterval = defaultInterval; - logger.debug( - `Cron initialized with interval: ${defaultInterval / 1000} seconds` - ); - this.signerApi = signerApi; - this.signerUrl = signerUrl; - this.validatorApi = validatorApi; - this.brainDb = brainDb; - } - - public start(interval?: number): void { - logger.debug(`Starting cron...`); - this.timer = setInterval(async () => { - await this.reloadValidators(); - }, interval || this.defaultInterval); - } - - public stop(): void { - logger.debug(`Stopping cron...`); - if (this.timer) { - clearInterval(this.timer); - } - } - - public restart(): void { - this.stop(); - this.start(); - } - - /** - * Reload db data based on truth sources: validator and signer APIs: - * - GET signer API pubkeys - * - GET validator API pubkeys and fee recipients - * - DELETE from signer API pubkeys that are not in DB - * - DELETE from DB pubkeys that are not in signer API - * - DELETE to validator API pubkeys that are in validator API and not in DB - * - POST to validator API fee recipients that are in DB and not in validator API - * - */ - public async reloadValidators(): Promise { - try { - logger.debug(`Reloading data...`); - - // 0. GET status - const signerApiStatus = await this.signerApi.getStatus(); - - // If web3signer API is not UP, skip data reload and further steps. - // This is done to avoid unintended DB modifications when the API is down. - // Status can be "UP" | "DOWN" | "UNKNOWN" | "LOADING" | "ERROR"; - if (signerApiStatus.status !== "UP") { - logger.warn( - `Web3Signer is ${ - signerApiStatus.status - }. Skipping data reload until Web3Signer is UP. Trying again in ${ - this.defaultInterval / 1000 - } seconds` - ); - return; - } - - // 1. GET data - const dbPubkeys = Object.keys(this.brainDb.getData()); - const signerPubkeys = (await this.signerApi.getKeystores()).data.map( - (keystore) => keystore.validating_pubkey - ); - - // 2. DELETE from signer API pubkeys that are not in DB - await this.deleteSignerPubkeysNotInDB({ signerPubkeys, dbPubkeys }); - - // 3. DELETE from DB pubkeys that are not in signer API - await this.deleteDbPubkeysNotInSigner({ dbPubkeys, signerPubkeys }); - - const validatorPubkeys = ( - await this.validatorApi.getRemoteKeys() - ).data.map((keystore) => keystore.pubkey); - - // 4. POST to validator API pubkeys that are in DB and not in validator API - await this.postValidatorPubkeysFromDb({ - brainDbPubkeysToAdd: dbPubkeys.filter( - (pubkey) => !validatorPubkeys.includes(pubkey) - ), - validatorPubkeys, - }); - - // 5. DELETE to validator API pubkeys that are in validator API and not in DB - await this.deleteValidatorPubkeysNotInDB({ - validatorPubkeys, - validatorPubkeysToRemove: validatorPubkeys.filter( - (pubkey) => !dbPubkeys.includes(pubkey) - ), - }); - - // 6. POST to validator API fee recipients that are in DB and not in validator API - await this.postValidatorsFeeRecipientsFromDb({ - dbData: this.brainDb.getData(), - validatorPubkeysFeeRecipients: await this.getValidatorsFeeRecipients({ - validatorPubkeys, - }), - }); - - logger.debug(`Finished reloading data`); - } catch (e) { - if (e instanceof ApiError && e.code) { - switch (e.code) { - case "ECONNREFUSED": - e.message += `Connection refused by the server ${e.hostname}. Make sure the port is open and the server is running`; - break; - case "ECONNRESET": - e.message += `Connection reset by the server ${e.hostname}, check server logs`; - break; - case "ENOTFOUND": - e.message += `Host ${e.hostname} not found. Make sure the server is running and the hostname is correct`; - break; - case "ERR_HTTP": - e.message += `HTTP error code ${e.errno}`; - break; - default: - e.message += `Unknown error`; - break; - } - - logger.error(`Error reloading data`, e); - } else { - logger.error(`Unknown error reloading data`, e); - } - } - } - - /** - * Get the validators fee recipients from the validator API for the given pubkeys - */ - private async getValidatorsFeeRecipients({ - validatorPubkeys, - }: { - validatorPubkeys: string[]; - }): Promise<{ pubkey: string; feeRecipient: string }[]> { - const validatorData = []; - - for (const pubkey of validatorPubkeys) { - validatorData.push({ - pubkey, - feeRecipient: (await this.validatorApi.getFeeRecipient(pubkey)).data - .ethaddress, - }); - } - - return validatorData; - } - - /** - * Delete from the validator API the pubkeys that are in the validator API and not in the DB - */ - private async deleteSignerPubkeysNotInDB({ - signerPubkeys, - dbPubkeys, - }: { - signerPubkeys: string[]; - dbPubkeys: string[]; - }): Promise { - const signerPubkeysToRemove = signerPubkeys.filter( - (pubkey) => !dbPubkeys.includes(pubkey) - ); - - if (signerPubkeysToRemove.length > 0) { - logger.debug( - `Found ${signerPubkeysToRemove.length} validators to remove from signer` - ); - - const signerDeleteResponse = await this.signerApi.deleteKeystores({ - pubkeys: signerPubkeysToRemove, - }); - - for (const [index, pubkeyToRemove] of signerPubkeysToRemove.entries()) { - const signerDeleteStatus = signerDeleteResponse.data[index].status; - if ( - signerDeleteStatus === "deleted" || - signerDeleteStatus === "not_found" - ) - signerPubkeys.splice(signerPubkeys.indexOf(pubkeyToRemove), 1); - else - logger.error( - `Error deleting pubkey ${pubkeyToRemove} from signer API: ${signerDeleteResponse.data[index].message}` - ); - } - } - } - - /** - * Delete from the signer API the pubkeys that are in the DB and not in the signer API - */ - private async deleteDbPubkeysNotInSigner({ - dbPubkeys, - signerPubkeys, - }: { - dbPubkeys: string[]; - signerPubkeys: string[]; - }): Promise { - const dbPubkeysToRemove = dbPubkeys.filter( - (pubkey) => !signerPubkeys.includes(pubkey) - ); - - if (dbPubkeysToRemove.length > 0) { - logger.debug( - `Found ${dbPubkeysToRemove.length} validators to remove from DB` - ); - this.brainDb.deleteValidators(dbPubkeysToRemove); - dbPubkeys.splice( - 0, - dbPubkeys.length, - ...dbPubkeys.filter((pubkey) => !dbPubkeysToRemove.includes(pubkey)) - ); - } - } - - /** - * Post pubkeys that are in the DB and not in the validator API - */ - private async postValidatorPubkeysFromDb({ - brainDbPubkeysToAdd, - validatorPubkeys, - }: { - brainDbPubkeysToAdd: string[]; - validatorPubkeys: string[]; - }): Promise { - if (brainDbPubkeysToAdd.length > 0) { - logger.debug( - `Found ${brainDbPubkeysToAdd.length} validators to add to validator API` - ); - const postKeysResponse = await this.validatorApi.postRemoteKeys({ - remote_keys: brainDbPubkeysToAdd.map((pubkey) => ({ - pubkey, - url: this.signerUrl, - })), - }); - - for (const [index, pubkeyToAdd] of brainDbPubkeysToAdd.entries()) { - const postKeyStatus = postKeysResponse.data[index].status; - if (postKeyStatus === "imported" || postKeyStatus === "duplicate") - validatorPubkeys.push(pubkeyToAdd); - else - logger.error( - `Error adding pubkey ${pubkeyToAdd} to validator API: ${postKeyStatus} ${postKeysResponse.data[index].message}` - ); - } - } - } - - /** - * Delete from the validator API the pubkeys that are in the validator API and not in the DB - */ - private async deleteValidatorPubkeysNotInDB({ - validatorPubkeys, - validatorPubkeysToRemove, - }: { - validatorPubkeys: string[]; - validatorPubkeysToRemove: string[]; - }): Promise { - if (validatorPubkeysToRemove.length > 0) { - logger.debug( - `Found ${validatorPubkeysToRemove.length} validators to remove from validator API` - ); - - const deleteValidatorKeysResponse = - await this.validatorApi.deleteRemoteKeys({ - pubkeys: validatorPubkeysToRemove, - }); - - for (const [ - index, - pubkeyToRemove, - ] of validatorPubkeysToRemove.entries()) { - const deleteValidatorKeyStatus = - deleteValidatorKeysResponse.data[index].status; - - if ( - deleteValidatorKeyStatus === "deleted" || - deleteValidatorKeyStatus === "not_found" - ) - validatorPubkeys.splice(validatorPubkeys.indexOf(pubkeyToRemove), 1); - else - logger.error( - `Error deleting pubkey ${pubkeyToRemove} from validator API: ${deleteValidatorKeyStatus} ${deleteValidatorKeysResponse.data[index].message}` - ); - } - } - } - - /** - * Post in the validator API fee recipients that are in the DB and not in the validator API - */ - private async postValidatorsFeeRecipientsFromDb({ - dbData, - validatorPubkeysFeeRecipients, - }: { - dbData: StakingBrainDb; - validatorPubkeysFeeRecipients: { pubkey: string; feeRecipient: string }[]; - }): Promise { - const feeRecipientsToPost = validatorPubkeysFeeRecipients - .filter( - (validator) => - validator.feeRecipient !== dbData[validator.pubkey].feeRecipient - ) - .map((validator) => ({ - pubkey: validator.pubkey, - feeRecipient: dbData[validator.pubkey].feeRecipient, - })); - - if (feeRecipientsToPost.length > 0) { - logger.debug( - `Found ${feeRecipientsToPost.length} fee recipients to add/update to validator API` - ); - for (const { pubkey, feeRecipient } of feeRecipientsToPost) - await this.validatorApi - .setFeeRecipient(feeRecipient, pubkey) - .catch((e) => - logger.error( - `Error adding fee recipient ${feeRecipient} to validator API for pubkey ${pubkey}`, - e - ) - ); - } - } -} +export { CronJob } from "./cron.js"; +export { reloadValidators } from "./reloadValidators.js"; +export { sendProofsOfValidation } from "./sendProofsOfValidation.js"; diff --git a/packages/brain/src/modules/cron/reloadValidators.ts b/packages/brain/src/modules/cron/reloadValidators.ts new file mode 100644 index 00000000..a957946e --- /dev/null +++ b/packages/brain/src/modules/cron/reloadValidators.ts @@ -0,0 +1,315 @@ +import { StakingBrainDb } from "@stakingbrain/common"; +import { ApiError } from "../apiClients/error.js"; +import { ValidatorApi, Web3SignerApi } from "../apiClients/index.js"; +import { BrainDataBase } from "../db/index.js"; +import logger from "../logger/index.js"; + +/** + * Reload db data based on truth sources: validator and signer APIs: + * - GET signer API pubkeys + * - GET validator API pubkeys and fee recipients + * - DELETE from signer API pubkeys that are not in DB + * - DELETE from DB pubkeys that are not in signer API + * - DELETE to validator API pubkeys that are in validator API and not in DB + * - POST to validator API fee recipients that are in DB and not in validator API + * + */ +export async function reloadValidators( + signerApi: Web3SignerApi, + signerUrl: string, + validatorApi: ValidatorApi, + brainDb: BrainDataBase +): Promise { + try { + logger.debug(`Reloading data...`); + + // 0. GET status + const signerApiStatus = await signerApi.getStatus(); + + // If web3signer API is not UP, skip data reload and further steps. + // This is done to avoid unintended DB modifications when the API is down. + // Status can be "UP" | "DOWN" | "UNKNOWN" | "LOADING" | "ERROR"; + if (signerApiStatus.status !== "UP") { + logger.warn( + `Web3Signer is ${signerApiStatus.status}. Skipping data reload until Web3Signer is UP. Trying again in next jobexecution` + ); + return; + } + + // 1. GET data + const dbPubkeys = Object.keys(brainDb.getData()); + const signerPubkeys = (await signerApi.getKeystores()).data.map( + (keystore) => keystore.validating_pubkey + ); + + // 2. DELETE from signer API pubkeys that are not in DB + await deleteSignerPubkeysNotInDB({ signerApi, signerPubkeys, dbPubkeys }); + + // 3. DELETE from DB pubkeys that are not in signer API + await deleteDbPubkeysNotInSigner({ brainDb, dbPubkeys, signerPubkeys }); + + const validatorPubkeys = (await validatorApi.getRemoteKeys()).data.map( + (keystore) => keystore.pubkey + ); + + // 4. POST to validator API pubkeys that are in DB and not in validator API + await postValidatorPubkeysFromDb({ + validatorApi, + signerUrl, + brainDbPubkeysToAdd: dbPubkeys.filter( + (pubkey) => !validatorPubkeys.includes(pubkey) + ), + validatorPubkeys, + }); + + // 5. DELETE to validator API pubkeys that are in validator API and not in DB + await deleteValidatorPubkeysNotInDB({ + validatorApi, + validatorPubkeys, + validatorPubkeysToRemove: validatorPubkeys.filter( + (pubkey) => !dbPubkeys.includes(pubkey) + ), + }); + + // 6. POST to validator API fee recipients that are in DB and not in validator API + await postValidatorsFeeRecipientsFromDb({ + validatorApi, + dbData: brainDb.getData(), + validatorPubkeysFeeRecipients: await getValidatorsFeeRecipients({ + validatorApi, + validatorPubkeys, + }), + }); + + logger.debug(`Finished reloading data`); + } catch (e) { + if (e instanceof ApiError && e.code) { + switch (e.code) { + case "ECONNREFUSED": + e.message += `Connection refused by the server ${e.hostname}. Make sure the port is open and the server is running`; + break; + case "ECONNRESET": + e.message += `Connection reset by the server ${e.hostname}, check server logs`; + break; + case "ENOTFOUND": + e.message += `Host ${e.hostname} not found. Make sure the server is running and the hostname is correct`; + break; + case "ERR_HTTP": + e.message += `HTTP error code ${e.errno}`; + break; + default: + e.message += `Unknown error`; + break; + } + + logger.error(`Error reloading data`, e); + } else { + logger.error(`Unknown error reloading data`, e); + } + } +} + +/** + * Get the validators fee recipients from the validator API for the given pubkeys + */ +async function getValidatorsFeeRecipients({ + validatorApi, + validatorPubkeys, +}: { + validatorApi: ValidatorApi; + validatorPubkeys: string[]; +}): Promise<{ pubkey: string; feeRecipient: string }[]> { + const validatorData = []; + + for (const pubkey of validatorPubkeys) { + validatorData.push({ + pubkey, + feeRecipient: (await validatorApi.getFeeRecipient(pubkey)).data + .ethaddress, + }); + } + + return validatorData; +} + +/** + * Delete from the validator API the pubkeys that are in the validator API and not in the DB + */ +async function deleteSignerPubkeysNotInDB({ + signerApi, + signerPubkeys, + dbPubkeys, +}: { + signerApi: Web3SignerApi; + signerPubkeys: string[]; + dbPubkeys: string[]; +}): Promise { + const signerPubkeysToRemove = signerPubkeys.filter( + (pubkey) => !dbPubkeys.includes(pubkey) + ); + + if (signerPubkeysToRemove.length > 0) { + logger.debug( + `Found ${signerPubkeysToRemove.length} validators to remove from signer` + ); + + const signerDeleteResponse = await signerApi.deleteKeystores({ + pubkeys: signerPubkeysToRemove, + }); + + for (const [index, pubkeyToRemove] of signerPubkeysToRemove.entries()) { + const signerDeleteStatus = signerDeleteResponse.data[index].status; + if ( + signerDeleteStatus === "deleted" || + signerDeleteStatus === "not_found" + ) + signerPubkeys.splice(signerPubkeys.indexOf(pubkeyToRemove), 1); + else + logger.error( + `Error deleting pubkey ${pubkeyToRemove} from signer API: ${signerDeleteResponse.data[index].message}` + ); + } + } +} + +/** + * Delete from the signer API the pubkeys that are in the DB and not in the signer API + */ +async function deleteDbPubkeysNotInSigner({ + brainDb, + dbPubkeys, + signerPubkeys, +}: { + brainDb: BrainDataBase; + dbPubkeys: string[]; + signerPubkeys: string[]; +}): Promise { + const dbPubkeysToRemove = dbPubkeys.filter( + (pubkey) => !signerPubkeys.includes(pubkey) + ); + + if (dbPubkeysToRemove.length > 0) { + logger.debug( + `Found ${dbPubkeysToRemove.length} validators to remove from DB` + ); + brainDb.deleteValidators(dbPubkeysToRemove); + dbPubkeys.splice( + 0, + dbPubkeys.length, + ...dbPubkeys.filter((pubkey) => !dbPubkeysToRemove.includes(pubkey)) + ); + } +} + +/** + * Post pubkeys that are in the DB and not in the validator API + */ +async function postValidatorPubkeysFromDb({ + validatorApi, + signerUrl, + brainDbPubkeysToAdd, + validatorPubkeys, +}: { + validatorApi: ValidatorApi; + signerUrl: string; + brainDbPubkeysToAdd: string[]; + validatorPubkeys: string[]; +}): Promise { + if (brainDbPubkeysToAdd.length > 0) { + logger.debug( + `Found ${brainDbPubkeysToAdd.length} validators to add to validator API` + ); + const postKeysResponse = await validatorApi.postRemoteKeys({ + remote_keys: brainDbPubkeysToAdd.map((pubkey) => ({ + pubkey, + url: signerUrl, + })), + }); + + for (const [index, pubkeyToAdd] of brainDbPubkeysToAdd.entries()) { + const postKeyStatus = postKeysResponse.data[index].status; + if (postKeyStatus === "imported" || postKeyStatus === "duplicate") + validatorPubkeys.push(pubkeyToAdd); + else + logger.error( + `Error adding pubkey ${pubkeyToAdd} to validator API: ${postKeyStatus} ${postKeysResponse.data[index].message}` + ); + } + } +} + +/** + * Delete from the validator API the pubkeys that are in the validator API and not in the DB + */ +async function deleteValidatorPubkeysNotInDB({ + validatorApi, + validatorPubkeys, + validatorPubkeysToRemove, +}: { + validatorApi: ValidatorApi; + validatorPubkeys: string[]; + validatorPubkeysToRemove: string[]; +}): Promise { + if (validatorPubkeysToRemove.length > 0) { + logger.debug( + `Found ${validatorPubkeysToRemove.length} validators to remove from validator API` + ); + + const deleteValidatorKeysResponse = await validatorApi.deleteRemoteKeys({ + pubkeys: validatorPubkeysToRemove, + }); + + for (const [index, pubkeyToRemove] of validatorPubkeysToRemove.entries()) { + const deleteValidatorKeyStatus = + deleteValidatorKeysResponse.data[index].status; + + if ( + deleteValidatorKeyStatus === "deleted" || + deleteValidatorKeyStatus === "not_found" + ) + validatorPubkeys.splice(validatorPubkeys.indexOf(pubkeyToRemove), 1); + else + logger.error( + `Error deleting pubkey ${pubkeyToRemove} from validator API: ${deleteValidatorKeyStatus} ${deleteValidatorKeysResponse.data[index].message}` + ); + } + } +} + +/** + * Post in the validator API fee recipients that are in the DB and not in the validator API + */ +async function postValidatorsFeeRecipientsFromDb({ + validatorApi, + dbData, + validatorPubkeysFeeRecipients, +}: { + validatorApi: ValidatorApi; + dbData: StakingBrainDb; + validatorPubkeysFeeRecipients: { pubkey: string; feeRecipient: string }[]; +}): Promise { + const feeRecipientsToPost = validatorPubkeysFeeRecipients + .filter( + (validator) => + validator.feeRecipient !== dbData[validator.pubkey].feeRecipient + ) + .map((validator) => ({ + pubkey: validator.pubkey, + feeRecipient: dbData[validator.pubkey].feeRecipient, + })); + + if (feeRecipientsToPost.length > 0) { + logger.debug( + `Found ${feeRecipientsToPost.length} fee recipients to add/update to validator API` + ); + for (const { pubkey, feeRecipient } of feeRecipientsToPost) + await validatorApi + .setFeeRecipient(feeRecipient, pubkey) + .catch((e) => + logger.error( + `Error adding fee recipient ${feeRecipient} to validator API for pubkey ${pubkey}`, + e + ) + ); + } +} diff --git a/packages/brain/src/modules/cron/sendProofsOfValidation.ts b/packages/brain/src/modules/cron/sendProofsOfValidation.ts new file mode 100644 index 00000000..d7406194 --- /dev/null +++ b/packages/brain/src/modules/cron/sendProofsOfValidation.ts @@ -0,0 +1,99 @@ +import { + DappnodeSignatureVerifierPostRequest, + Web3signerPostSignDappnodeRequest, + Web3signerPostSignDappnodeResponse, +} from "@stakingbrain/common"; +import { + Web3SignerApi, + DappnodeSignatureVerifier, +} from "../apiClients/index.js"; +import { BrainDataBase } from "../db/index.js"; +import logger from "../logger/index.js"; +import { isEmpty } from "lodash-es"; + +/** + * Send the proof of validation to the dappnode-signatures.io domain + */ +export async function sendProofsOfValidation( + signerApi: Web3SignerApi, + brainDb: BrainDataBase, + DappnodeSignatureVerifier: DappnodeSignatureVerifier, + shareDataWithDappnode: boolean +): Promise { + try { + // Get the proofs of validation from the signer + const proofsOfValidations = await getProofsOfValidation( + signerApi, + brainDb, + shareDataWithDappnode + ); + if (proofsOfValidations.length === 0) { + logger.debug(`No proofs of validation to send`); + return; + } + logger.debug(`Sending ${proofsOfValidations.length} proofs of validations`); + await DappnodeSignatureVerifier.sendProofsOfValidation(proofsOfValidations); + } catch (e) { + logger.error(`Error sending proof of validation: ${e.message}`); + } +} + +/** + * Get the proofs of validation from the signer + * for all the pubkeys in the db + */ +async function getProofsOfValidation( + signerApi: Web3SignerApi, + brainDb: BrainDataBase, + shareDataWithDappnode: boolean +): Promise { + const signerDappnodeSignRequest: Web3signerPostSignDappnodeRequest = { + type: "PROOF_OF_VALIDATION", + platform: "dappnode", + timestamp: Date.now().toString(), + }; + // get pubkeys detauls from db + const dbPubkeysDetails = brainDb.getData(); + + // only send proof of validation if the user has enabled it + // or if there is a stader pubkey + const dbPubkeysDetailsFiltered = Object.fromEntries( + Object.entries(dbPubkeysDetails).filter( + ([pubkey, details]) => shareDataWithDappnode || details.tag === "stader" + ) + ); + if (isEmpty(dbPubkeysDetailsFiltered)) return []; + // For each pubkey, get the proof of validation from the signer + const proofsOfValidations = await Promise.all( + Object.keys(dbPubkeysDetailsFiltered).map(async (pubkey) => { + try { + const { payload, signature }: Web3signerPostSignDappnodeResponse = + await signerApi.signDappnodeProofOfValidation({ + signerDappnodeSignRequest, + pubkey, + }); + return { + payload, + pubkey, + signature, + tag: dbPubkeysDetailsFiltered[pubkey].tag, + }; + } catch (e) { + logger.error( + `Error getting proof of validation for pubkey ${pubkey}. Error: ${e.message}` + ); + return null; + } + }) + ); + + return filterNotNullishFromArray(proofsOfValidations); +} + +/** + * TODO: update typescript to 5.5 to fix the type predicate issue + * @see https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-beta/#inferred-type-predicates + */ +function filterNotNullishFromArray(array: (T | null | undefined)[]): T[] { + return array.filter((item) => item !== null && item !== undefined) as T[]; +} diff --git a/packages/brain/src/modules/envs/index.ts b/packages/brain/src/modules/envs/index.ts index ebbdfed4..c4083c24 100644 --- a/packages/brain/src/modules/envs/index.ts +++ b/packages/brain/src/modules/envs/index.ts @@ -58,6 +58,9 @@ export function loadStakerConfig(): { signerUrl: string; token: string; host: string; + shareDataWithDappnode: boolean; + validatorsMonitorUrl: string; + shareCronInterval: number; tlsCert?: Buffer; } { const network = process.env.NETWORK as Network; @@ -69,6 +72,13 @@ export function loadStakerConfig(): { )}` ); + const shareDataWithDappnode = process.env.SHARE_DATA_WITH_DAPPNODE === "true"; + const validatorsMonitorUrl = + process.env.VALIDATORS_MONITOR_URL || params.defaultValidatorsMonitorUrl; + const shareCronInterval = process.env.SHARE_CRON_INTERVAL + ? parseInt(process.env.SHARE_CRON_INTERVAL) + : params.defaultProofsOfValidationCron; + const certDir = path.join(__dirname, params.certDirName); let executionClientUrl: string, @@ -78,7 +88,8 @@ export function loadStakerConfig(): { tlsCert: Buffer | undefined; if (network === "mainnet") { - const { executionClient, consensusClient, isMevBoostSet } = loadEnvs("mainnet"); + const { executionClient, consensusClient, isMevBoostSet } = + loadEnvs("mainnet"); switch (executionClient) { case "geth.dnp.dappnode.eth": executionClientUrl = `http://geth.dappnode:8545`; @@ -143,10 +154,14 @@ export function loadStakerConfig(): { signerUrl: `http://web3signer.web3signer.dappnode:9000`, token, host: `brain.web3signer.dappnode`, + shareDataWithDappnode, + validatorsMonitorUrl, + shareCronInterval, tlsCert, }; } else if (network === "gnosis") { - const { executionClient, consensusClient, isMevBoostSet } = loadEnvs("gnosis"); + const { executionClient, consensusClient, isMevBoostSet } = + loadEnvs("gnosis"); switch (executionClient) { case "nethermind-xdai.dnp.dappnode.eth": executionClientUrl = `http://nethermind-xdai.dappnode:8545`; @@ -205,10 +220,14 @@ export function loadStakerConfig(): { signerUrl: `http://web3signer.web3signer-gnosis.dappnode:9000`, token, host: `brain.web3signer-gnosis.dappnode`, + shareDataWithDappnode, + validatorsMonitorUrl, + shareCronInterval, tlsCert, }; } else if (network === "prater") { - const { executionClient, consensusClient, isMevBoostSet } = loadEnvs("prater"); + const { executionClient, consensusClient, isMevBoostSet } = + loadEnvs("prater"); switch (executionClient) { case "goerli-nethermind.dnp.dappnode.eth": executionClientUrl = `http://goerli-nethermind.dappnode:8545`; @@ -273,10 +292,14 @@ export function loadStakerConfig(): { signerUrl: `http://web3signer.web3signer-prater.dappnode:9000`, token, host: `web3signer.web3signer-prater.dappnode`, + shareDataWithDappnode, + validatorsMonitorUrl, + shareCronInterval, tlsCert, }; } else if (network === "lukso") { - const { executionClient, consensusClient, isMevBoostSet } = loadEnvs("lukso"); + const { executionClient, consensusClient, isMevBoostSet } = + loadEnvs("lukso"); switch (executionClient) { case "lukso-erigon.dnp.dappnode.eth": executionClientUrl = `http://lukso-erigon.dappnode:8545`; @@ -335,10 +358,14 @@ export function loadStakerConfig(): { signerUrl: `http://web3signer.web3signer-lukso.dappnode:9000`, token, host: `web3signer.web3signer-lukso.dappnode`, + shareDataWithDappnode, + validatorsMonitorUrl, + shareCronInterval, tlsCert, }; } else if (network === "holesky") { - const { executionClient, consensusClient, isMevBoostSet } = loadEnvs("holesky"); + const { executionClient, consensusClient, isMevBoostSet } = + loadEnvs("holesky"); switch (executionClient) { case "holesky-nethermind.dnp.dappnode.eth": executionClientUrl = `http://holesky-nethermind.dappnode:8545`; @@ -403,6 +430,9 @@ export function loadStakerConfig(): { signerUrl: `http://web3signer.web3signer-holesky.dappnode:9000`, token, host: `web3signer.web3signer-holesky.dappnode`, + shareDataWithDappnode, + validatorsMonitorUrl, + shareCronInterval, tlsCert, }; } else { @@ -428,7 +458,8 @@ function loadEnvs( const consensusClient = process.env[`_DAPPNODE_GLOBAL_CONSENSUS_CLIENT_${network.toUpperCase()}`]; const isMevBoostSet = - process.env[`_DAPPNODE_GLOBAL_MEVBOOST_${network.toUpperCase()}`] === "true"; + process.env[`_DAPPNODE_GLOBAL_MEVBOOST_${network.toUpperCase()}`] === + "true"; switch (network) { case "mainnet": diff --git a/packages/brain/src/params.ts b/packages/brain/src/params.ts index 068f6d67..a978cb53 100644 --- a/packages/brain/src/params.ts +++ b/packages/brain/src/params.ts @@ -8,4 +8,6 @@ export const params = { defaultTag: "solo" as Tag, uiPort: 80, launchpadPort: 3000, + defaultValidatorsMonitorUrl: "https://dappnode-signatures.io", + defaultProofsOfValidationCron: 24 * 60 * 60 * 1000, // 1 day in ms }; diff --git a/packages/brain/tests/unit/modules/apiClients/cron.unit.test.ts b/packages/brain/tests/unit/modules/apiClients/cron.unit.test.ts index 054ddd26..49e27702 100644 --- a/packages/brain/tests/unit/modules/apiClients/cron.unit.test.ts +++ b/packages/brain/tests/unit/modules/apiClients/cron.unit.test.ts @@ -8,7 +8,7 @@ import { execSync } from "node:child_process"; import { BrainDataBase } from "../../../../src/modules/db/index.js"; import fs from "fs"; import path from "path"; -import { Cron } from "../../../../src/modules/cron/index.js"; +import { reloadValidators } from "../../../../src/modules/cron/index.js"; import { Network, PubkeyDetails } from "@stakingbrain/common"; describe.skip("Cron: Prater", () => { @@ -65,7 +65,7 @@ describe.skip("Cron: Prater", () => { let validatorApi: ValidatorApi; let signerApi: Web3SignerApi; let brainDb: BrainDataBase; - let cron: Cron; + let signerUrl: string; const testDbName = "testDb.json"; @@ -100,17 +100,10 @@ describe.skip("Cron: Prater", () => { }, stakerSpecs.network ); + signerUrl = `http://${signerIp}:9000`; if (fs.existsSync(testDbName)) fs.unlinkSync(testDbName); brainDb = new BrainDataBase(testDbName); - - cron = new Cron( - 60 * 1000, - signerApi, - `http://${signerIp}:9000`, - validatorApi, - brainDb - ); }); beforeEach(async function () { @@ -144,7 +137,7 @@ describe.skip("Cron: Prater", () => { }); //Check that fee recipient has changed in validator - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); const validatorFeeRecipient = await validatorApi.getFeeRecipient( pubkeyToTest @@ -158,7 +151,7 @@ describe.skip("Cron: Prater", () => { addSampleValidatorsToDB(1); await addSampleKeystoresToSigner(2); - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); const signerPubkeys = await signerApi.getKeystores(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -175,7 +168,7 @@ describe.skip("Cron: Prater", () => { addSampleValidatorsToDB(2); await addSampleKeystoresToSigner(1); - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); const signerPubkeys = await signerApi.getKeystores(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -195,7 +188,7 @@ describe.skip("Cron: Prater", () => { brainDb.deleteValidators([pubkeys[0]]); await signerApi.deleteKeystores({ pubkeys: [pubkeys[1]] }); - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); const signerPubkeys = await signerApi.getKeystores(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -208,7 +201,7 @@ describe.skip("Cron: Prater", () => { addSampleValidatorsToDB(2); await addSampleKeystoresToSigner(2); - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); const signerPubkeys = await signerApi.getKeystores(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -226,7 +219,7 @@ describe.skip("Cron: Prater", () => { console.log("Added pubkeys to validator"); - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); console.log("Validators reloaded"); @@ -243,7 +236,7 @@ describe.skip("Cron: Prater", () => { const pubkeysToTest = pubkeys.slice(0, 2); - await cron.reloadValidators(); + await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); const validatorPubkeys = await validatorApi.getRemoteKeys(); diff --git a/packages/brain/tsconfig.json b/packages/brain/tsconfig.json index 611632e3..f2c51f44 100644 --- a/packages/brain/tsconfig.json +++ b/packages/brain/tsconfig.json @@ -6,6 +6,7 @@ "outDir": "dist", "skipLibCheck": true, "moduleResolution": "NodeNext", + "module": "NodeNext", "useUnknownInCatchVariables": false }, "ts-node": { diff --git a/packages/common/src/types/api/standard/types.ts b/packages/common/src/types/api/standard/types.ts index c64b537f..28f8ba30 100644 --- a/packages/common/src/types/api/standard/types.ts +++ b/packages/common/src/types/api/standard/types.ts @@ -25,6 +25,7 @@ export interface ErrnoException extends Error { } export type ErrorCode = + | "ETIMEDOUT" | "ENOTFOUND" | "ECONNREFUSED" | "ECONNRESET" diff --git a/packages/common/src/types/api/web3signer/types.ts b/packages/common/src/types/api/web3signer/types.ts index 14cad5c1..e2e5bf3d 100644 --- a/packages/common/src/types/api/web3signer/types.ts +++ b/packages/common/src/types/api/web3signer/types.ts @@ -1,5 +1,9 @@ +import { Tag } from "../../db/types.js"; + export type Web3SignerStatus = "UP" | "DOWN" | "UNKNOWN" | "LOADING" | "ERROR"; +// keymanager + export interface Web3signerGetResponse { data: { validating_pubkey: string; @@ -28,6 +32,9 @@ export interface Web3signerDeleteResponse { }[]; slashing_protection?: string; } + +// healthcheck + export interface Web3signerHealthcheckResponse { status: Web3SignerStatus; checks: { @@ -37,6 +44,8 @@ export interface Web3signerHealthcheckResponse { outcome: string; } +// Signing + export interface Web3SignerPostSignvoluntaryexitRequest { type: "VOLUNTARY_EXIT"; fork_info: { @@ -56,4 +65,22 @@ export interface Web3SignerPostSignvoluntaryexitRequest { export interface Web3SignerPostSignvoluntaryexitResponse { signature: string; -} \ No newline at end of file +} + +export interface Web3signerPostSignDappnodeRequest { + type: "PROOF_OF_VALIDATION"; + platform: "dappnode"; + timestamp: string; +} + +export interface Web3signerPostSignDappnodeResponse { + signature: string; + payload: string; +} + +export interface DappnodeSignatureVerifierPostRequest { + payload: string; + pubkey: string; + signature: string; + tag: Tag; +}