Skip to content

Commit

Permalink
Implement proof of validation (#304)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pablomendezroyo authored May 31, 2024
1 parent bfc784d commit f098fe2
Show file tree
Hide file tree
Showing 18 changed files with 698 additions and 412 deletions.
13 changes: 9 additions & 4 deletions packages/brain/src/calls/deleteValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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)
Expand All @@ -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;
}
}
25 changes: 13 additions & 12 deletions packages/brain/src/calls/importValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
STADER_POOL_FEE_RECIPIENT_PRATER,
} from "@stakingbrain/common";
import {
cron,
reloadValidatorsCron,
network,
signerApi,
validatorApi,
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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}`);
Expand Down Expand Up @@ -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") {
Expand All @@ -140,7 +141,7 @@ export async function importValidators(
web3signerPostResponse.data.push(...wrongFeeRecipientResponse);

if (validatorsToPost.length === 0) {
cron.start();
reloadValidatorsCron.start();
return web3signerPostResponse;
}

Expand Down Expand Up @@ -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;
}
}
Expand Down
13 changes: 8 additions & 5 deletions packages/brain/src/calls/updateValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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();

Expand All @@ -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) {
Expand Down Expand Up @@ -58,9 +61,9 @@ export async function updateValidators(
);

// IMPORTANT: start the cron
cron.start();
reloadValidatorsCron.start();
} catch (e) {
cron.restart();
reloadValidatorsCron.restart();
throw e;
}
}
36 changes: 27 additions & 9 deletions packages/brain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Beaconchain,
BeaconchaApi,
ValidatorApi,
DappnodeSignatureVerifier,
} from "./modules/apiClients/index.js";
import {
startUiServer,
Expand All @@ -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...`);

Expand All @@ -38,6 +43,9 @@ export const {
signerUrl,
token,
host,
shareDataWithDappnode,
validatorsMonitorUrl,
shareCronInterval,
tlsCert,
} = loadStakerConfig();
logger.debug(
Expand Down Expand Up @@ -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(
Expand All @@ -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();
Expand Down
31 changes: 31 additions & 0 deletions packages/brain/src/modules/apiClients/DappnodeSignatureVerifier.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await this.request({
method: "POST",
endpoint: `${this.dappnodeSignEndpoint}?network=${encodeURIComponent(
this.network.toString()
)}`,
body: JSON.stringify(proofOfValidations),
timeout: 10000,
});
}
}
1 change: 1 addition & 0 deletions packages/brain/src/modules/apiClients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
36 changes: 27 additions & 9 deletions packages/brain/src/modules/apiClients/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
timeout?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): Promise<any> {
let req: http.ClientRequest;
Expand All @@ -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));
Expand Down
Loading

0 comments on commit f098fe2

Please sign in to comment.