Skip to content

Commit 7459651

Browse files
authored
Merge pull request #445 from dappnode/fix/validator-settings-updates
Restore missing validator keys and simplify settings updates
2 parents f56146b + 57c3390 commit 7459651

15 files changed

Lines changed: 249 additions & 120 deletions

File tree

‎packages/brain/src/modules/apiServers/launchpad/routes/feeRecipients/route.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ import { ValidatorApi } from "../../../../apiClients/validator/index.js";
4141
export const createFeeRecipientsRouter = ({
4242
brainDb,
4343
signerApi,
44+
signerUrl,
4445
validatorApi,
4546
beaconchainApi,
4647
reloadValidatorsCronTask
4748
}: {
4849
brainDb: BrainDataBase;
4950
signerApi: Web3SignerApi;
51+
signerUrl: string;
5052
validatorApi: ValidatorApi;
5153
beaconchainApi: BeaconchainApi;
5254
reloadValidatorsCronTask: CronJob;
@@ -168,6 +170,7 @@ export const createFeeRecipientsRouter = ({
168170
brainDb,
169171
validatorApi,
170172
customValidatorUpdateRequest: validatorsToUpdate.map(({ pubkey, feeRecipient }) => ({ pubkey, feeRecipient })),
173+
signerUrl,
171174
requestFrom: "api"
172175
});
173176

‎packages/brain/src/modules/apiServers/launchpad/startLaunchpadApi.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function startLaunchpadApi({
4040
createFeeRecipientsRouter({
4141
brainDb,
4242
signerApi,
43+
signerUrl,
4344
validatorApi,
4445
beaconchainApi,
4546
reloadValidatorsCronTask

‎packages/brain/src/modules/apiServers/ui/calls/importValidators.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,7 @@ export async function importValidators({
169169

170170
brainDb.deleteValidators(rejectedNewKeys);
171171
if (confirmedKeys.length > 0)
172-
brainDb.updateValidators({
173-
validators: Object.fromEntries(confirmedKeys.map((pubkey) => [pubkey, { importPending: undefined }]))
174-
});
172+
brainDb.updateValidators(confirmedKeys.map((pubkey) => ({ pubkey, importPending: undefined })));
175173

176174
if (validatorsToPost.length === 0) return results;
177175

‎packages/brain/src/modules/apiServers/ui/calls/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const createRpcMethods = ({
7777
brainDb,
7878
validatorApi,
7979
customValidatorUpdateRequest,
80+
signerUrl,
8081
requestFrom
8182
}),
8283
getValidators: async () =>
Lines changed: 107 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,129 @@
1-
import { withValidatorOperationLock } from "../../../db/withValidatorOperationLock.js";
21
import { prefix0xPubkey, isFeeRecipientEditable, ActionRequestOrigin } from "@stakingbrain/common";
3-
import logger from "../../../../modules/logger/index.js";
2+
import { withValidatorOperationLock } from "../../../db/withValidatorOperationLock.js";
3+
import logger from "../../../logger/index.js";
44
import { CustomValidatorUpdateRequest } from "./types.js";
5-
import { StakingBrainDbUpdate } from "../../../../modules/db/types.js";
5+
import { StakingBrainDb } from "../../../db/types.js";
66
import { CronJob } from "../../../cron/cron.js";
77
import { BrainDataBase } from "../../../db/index.js";
88
import { ValidatorApi } from "../../../apiClients/validator/index.js";
99
import { applyValidatorBuilderConfig } from "../../../cron/reloadValidators/postValidatorsBuilderConfigsFromDb.js";
1010

11+
type UpdateValidatorsOptions = {
12+
reloadValidatorsCronTask: CronJob;
13+
brainDb: BrainDataBase;
14+
validatorApi: ValidatorApi;
15+
signerUrl: string;
16+
customValidatorUpdateRequest: CustomValidatorUpdateRequest[];
17+
requestFrom?: ActionRequestOrigin;
18+
};
19+
1120
/** Persist desired settings first. Periodic reload retries client failures. */
1221
export async function updateValidators({
1322
reloadValidatorsCronTask,
1423
brainDb,
1524
validatorApi,
16-
customValidatorUpdateRequest,
25+
signerUrl,
26+
customValidatorUpdateRequest: requests,
1727
requestFrom
18-
}: {
19-
reloadValidatorsCronTask: CronJob;
20-
brainDb: BrainDataBase;
21-
validatorApi: ValidatorApi;
22-
customValidatorUpdateRequest: CustomValidatorUpdateRequest[];
23-
requestFrom?: ActionRequestOrigin;
24-
}): Promise<void> {
28+
}: UpdateValidatorsOptions): Promise<void> {
2529
return withValidatorOperationLock(brainDb, async () => {
2630
reloadValidatorsCronTask.stop();
2731
try {
28-
if (!Array.isArray(customValidatorUpdateRequest) || customValidatorUpdateRequest.length === 0)
29-
throw new Error("Select at least one validator to update");
30-
const dbData = brainDb.getData();
31-
const updates: StakingBrainDbUpdate = {};
32-
for (const validator of customValidatorUpdateRequest) {
33-
if (!validator || typeof validator.pubkey !== "string") throw new Error("Validator public key is required");
34-
const pubkey = prefix0xPubkey(validator.pubkey);
35-
if (!dbData[pubkey]) throw new Error(`Validator ${pubkey} is not in the database`);
36-
if (updates[pubkey]) throw new Error(`Duplicate validator ${pubkey}`);
37-
if (Object.keys(validator).some((field) => !["pubkey", "feeRecipient", "builderConfig"].includes(field)))
38-
throw new Error("Unsupported validator setting");
39-
const update: StakingBrainDbUpdate[string] = {};
40-
if ("feeRecipient" in validator && isFeeRecipientEditable(dbData[pubkey].tag, requestFrom))
41-
update.feeRecipient = validator.feeRecipient;
42-
if ("builderConfig" in validator) update.builderConfig = validator.builderConfig;
43-
if (Object.keys(update).length > 0) updates[pubkey] = update;
44-
}
45-
if (Object.keys(updates).length === 0) throw new Error("The fee recipient can't be updated for these validators");
32+
const currentValidators = brainDb.getData();
33+
const updates = prepareUpdates(requests, currentValidators, requestFrom);
4634

47-
// Validate the entire batch before changing any persisted settings.
48-
brainDb.updateValidators({ validators: updates });
49-
const failures: string[] = [];
50-
for (const [pubkey, update] of Object.entries(updates)) {
51-
if (update.feeRecipient !== undefined)
52-
await validatorApi.setFeeRecipient(update.feeRecipient, pubkey).catch((err) => {
53-
logger.error("Error setting validator feeRecipient", err);
54-
failures.push(`Fee recipient application failed for ${pubkey}`);
55-
});
56-
if ("builderConfig" in update) {
57-
try {
58-
await applyValidatorBuilderConfig(validatorApi, pubkey, update.builderConfig);
59-
} catch (error) {
60-
logger.error(error);
61-
failures.push(error.message);
62-
}
63-
}
64-
}
65-
if (failures.length > 0)
66-
throw new Error(
67-
`Settings saved; client configuration application failed and will be retried on reload. ${failures.join("; ")}`
68-
);
35+
// The database validates the entire batch before persisting any settings.
36+
brainDb.updateValidators(updates);
37+
const importedKeys = await importMissingBuilderKeys(validatorApi, signerUrl, updates);
38+
await applyUpdates(validatorApi, updates, currentValidators, importedKeys);
6939
} finally {
7040
reloadValidatorsCronTask.start();
7141
}
7242
});
7343
}
44+
45+
function prepareUpdates(
46+
requests: CustomValidatorUpdateRequest[],
47+
currentValidators: StakingBrainDb,
48+
requestFrom?: ActionRequestOrigin
49+
): CustomValidatorUpdateRequest[] {
50+
if (!Array.isArray(requests) || requests.length === 0) throw new Error("Select at least one validator to update");
51+
52+
const updates: CustomValidatorUpdateRequest[] = [];
53+
const seen = new Set<string>();
54+
for (const request of requests) {
55+
if (!request || typeof request.pubkey !== "string") throw new Error("Validator public key is required");
56+
const pubkey = prefix0xPubkey(request.pubkey);
57+
const current = currentValidators[pubkey];
58+
if (!current) throw new Error(`Validator ${pubkey} is not in the database`);
59+
if (seen.has(pubkey)) throw new Error(`Duplicate validator ${pubkey}`);
60+
if (Object.keys(request).some((field) => !["pubkey", "feeRecipient", "builderConfig"].includes(field)))
61+
throw new Error("Unsupported validator setting");
62+
63+
const { feeRecipient, builderConfig } = request;
64+
const update: CustomValidatorUpdateRequest = { pubkey };
65+
if (feeRecipient !== undefined && isFeeRecipientEditable(current.tag, requestFrom))
66+
update.feeRecipient = feeRecipient;
67+
if (builderConfig !== undefined) update.builderConfig = builderConfig;
68+
if (update.feeRecipient !== undefined || update.builderConfig !== undefined) {
69+
updates.push(update);
70+
seen.add(pubkey);
71+
}
72+
}
73+
74+
if (updates.length === 0) throw new Error("The fee recipient can't be updated for these validators");
75+
return updates;
76+
}
77+
78+
/** A restart may have cleared keys before the next cron reload. */
79+
async function importMissingBuilderKeys(
80+
validatorApi: ValidatorApi,
81+
signerUrl: string,
82+
updates: readonly CustomValidatorUpdateRequest[]
83+
): Promise<Set<string>> {
84+
const builderPubkeys = updates.filter(({ builderConfig }) => builderConfig !== undefined).map(({ pubkey }) => pubkey);
85+
if (builderPubkeys.length === 0) return new Set();
86+
87+
const remoteKeys = await validatorApi.getRemoteKeys();
88+
const loaded = new Set(remoteKeys.data.map(({ pubkey }) => pubkey.toLowerCase()));
89+
const missing = builderPubkeys.filter((pubkey) => !loaded.has(pubkey.toLowerCase()));
90+
if (missing.length > 0)
91+
await validatorApi.postRemoteKeys({ remote_keys: missing.map((pubkey) => ({ pubkey, url: signerUrl })) });
92+
return new Set(missing);
93+
}
94+
95+
async function applyUpdates(
96+
validatorApi: ValidatorApi,
97+
updates: readonly CustomValidatorUpdateRequest[],
98+
currentValidators: StakingBrainDb,
99+
importedKeys: Set<string>
100+
): Promise<void> {
101+
const failures: string[] = [];
102+
for (const update of updates) {
103+
const { pubkey } = update;
104+
let feeRecipient = update.feeRecipient;
105+
if (feeRecipient === undefined && importedKeys.has(pubkey)) feeRecipient = currentValidators[pubkey].feeRecipient;
106+
107+
if (feeRecipient !== undefined) {
108+
try {
109+
await validatorApi.setFeeRecipient(feeRecipient, pubkey);
110+
} catch (error) {
111+
logger.error("Error setting validator feeRecipient", error);
112+
failures.push(`Fee recipient application failed for ${pubkey}`);
113+
}
114+
}
115+
if (update.builderConfig !== undefined) {
116+
try {
117+
await applyValidatorBuilderConfig(validatorApi, pubkey, update.builderConfig);
118+
} catch (error) {
119+
logger.error(error);
120+
failures.push(error.message);
121+
}
122+
}
123+
}
124+
125+
if (failures.length > 0)
126+
throw new Error(
127+
`Settings saved; client configuration application failed and will be retried on reload. ${failures.join("; ")}`
128+
);
129+
}

‎packages/brain/src/modules/config/importBuilderIds.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,12 @@ export function importBuilderIds({
3535
};
3636

3737
const validators = Object.entries(brainDb.getData());
38-
const updates = Object.fromEntries(
39-
validators
40-
.filter(([, details]) => details.builderConfig === undefined)
41-
.map(([pubkey]) => [pubkey, { builderConfig }])
42-
);
43-
const imported = Object.keys(updates).length;
38+
const updates = validators
39+
.filter(([, details]) => details.builderConfig === undefined)
40+
.map(([pubkey]) => ({ pubkey, builderConfig }));
41+
const imported = updates.length;
4442
const skipped = validators.length - imported;
45-
if (imported > 0) brainDb.updateValidators({ validators: updates });
43+
if (imported > 0) brainDb.updateValidators(updates);
4644

4745
// Mark completion only after saving. Retries preserve existing settings and edits.
4846
fs.writeFileSync(markerPath, "", { mode: 0o600, flag: "wx" });

‎packages/brain/src/modules/cron/reloadValidators/index.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ export async function reloadValidators(
4949
(pubkey) => data[pubkey].importPending && signerKeys.has(pubkey.toLowerCase())
5050
);
5151
if (confirmedImports.length > 0)
52-
brainDb.updateValidators({
53-
validators: Object.fromEntries(confirmedImports.map((pubkey) => [pubkey, { importPending: undefined }]))
54-
});
52+
brainDb.updateValidators(confirmedImports.map((pubkey) => ({ pubkey, importPending: undefined })));
5553
// Preserve pending records, but do not activate keys the signer has not confirmed.
5654
const activeData = Object.fromEntries(
5755
Object.entries(brainDb.getData()).filter(([pubkey]) => signerKeys.has(pubkey.toLowerCase()))

‎packages/brain/src/modules/cron/trackValidatorsPerformance/setActiveValidatorsLoadedInBrain.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async function getValidatorIndexesAndSaveInDb({
7878
const { pubkey } = validatorData.validator;
7979
const index = validatorData.index;
8080
validatorIndexes.push(index);
81-
brainDb.updateValidators({ validators: { [pubkey]: { index: parseInt(index) } } });
81+
brainDb.updateValidators([{ pubkey, index: parseInt(index) }]);
8282
}
8383
}
8484

‎packages/brain/src/modules/db/index.ts‎

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StakingBrainDb, StakingBrainDbUpdate } from "./types.js";
1+
import { StakingBrainDb, ValidatorUpdate } from "./types.js";
22
import { LowSync } from "lowdb";
33
import { JSONFileSync } from "lowdb/node";
44
import fs from "fs";
@@ -115,26 +115,25 @@ export class BrainDataBase extends LowSync<StakingBrainDb> {
115115
/**
116116
* Update supplied validator settings without erasing omitted settings.
117117
*/
118-
public updateValidators({ validators }: { validators: StakingBrainDbUpdate }): void {
118+
public updateValidators(validators: readonly ValidatorUpdate[]): void {
119119
try {
120120
this.validateDb();
121121
this.validateUpdateValidators(validators);
122122
const updatedData = { ...this.data };
123-
if (this.data)
124-
for (const pubkey of Object.keys(validators)) {
125-
if (!this.data[pubkey]) {
126-
// Remove pubkeys that don't exist
127-
logger.warn(`Pubkey ${pubkey} not found in the database`);
128-
delete validators[pubkey];
129-
} else {
130-
updatedData[pubkey] = { ...this.data[pubkey], ...validators[pubkey] };
131-
}
123+
for (const { pubkey, ...changes } of validators) {
124+
if (!this.data[pubkey]) {
125+
logger.warn(`Pubkey ${pubkey} not found in the database`);
126+
continue;
132127
}
128+
updatedData[pubkey] = { ...this.data[pubkey], ...changes };
129+
}
133130
this.ensureDbMaxSize(updatedData, true);
134131
this.data = updatedData;
135132
this.write();
136133
} catch (e) {
137-
throw new BrainDbError(`Unable to update pubkeys ${Object.keys(validators).join(", ")}. ${e.message}`);
134+
throw new BrainDbError(
135+
`Unable to update pubkeys ${validators.map(({ pubkey }) => pubkey).join(", ")}. ${e.message}`
136+
);
138137
}
139138
}
140139

@@ -285,32 +284,31 @@ export class BrainDataBase extends LowSync<StakingBrainDb> {
285284
this.read();
286285
}
287286

288-
private validateUpdateValidators(validators: StakingBrainDbUpdate): void {
287+
private validateUpdateValidators(validators: readonly ValidatorUpdate[]): void {
289288
const errors: string[] = [];
290-
Object.keys(validators).forEach((pubkey) => {
291-
const pubkeyDetails = validators[pubkey];
289+
const seen = new Set<string>();
290+
for (const validator of validators) {
291+
const { pubkey } = validator;
292292

293293
// create substring of pubkey to be used in error message
294294
const pubkeySubstr = shortenPubkey(pubkey);
295295

296296
// Validate Ethereum address
297297
if (!isValidBlsPubkey(pubkey)) errors.push(`\n pubkey ${pubkeySubstr}: bls is invalid`);
298298

299-
if (!pubkeyDetails) {
300-
errors.push(`\n pubkey ${pubkeySubstr}: pubkey details are missing`);
301-
return;
302-
}
299+
if (seen.has(pubkey)) errors.push(`\n pubkey ${pubkeySubstr}: duplicate update`);
300+
seen.add(pubkey);
303301

304302
// FeeRecipient
305-
if ("feeRecipient" in pubkeyDetails) {
306-
if (typeof pubkeyDetails.feeRecipient !== "string")
303+
if ("feeRecipient" in validator) {
304+
if (typeof validator.feeRecipient !== "string")
307305
errors.push(`\n pubkey ${pubkeySubstr}: feeRecipient address is invalid, must be in string format`);
308-
if (typeof pubkeyDetails.feeRecipient !== "string" || !isValidEcdsaPubkey(pubkeyDetails.feeRecipient))
306+
if (typeof validator.feeRecipient !== "string" || !isValidEcdsaPubkey(validator.feeRecipient))
309307
errors.push(`\n pubkey ${pubkeySubstr}: fee recipient is invalid`);
310308
}
311-
if ("builderConfig" in pubkeyDetails && pubkeyDetails.builderConfig !== null)
312-
validateBuilderConfig(pubkeyDetails.builderConfig);
313-
});
309+
if ("builderConfig" in validator && validator.builderConfig !== null)
310+
validateBuilderConfig(validator.builderConfig);
311+
}
314312
if (errors.length > 0) throw new BrainDbError(errors.join("\n"));
315313
}
316314

‎packages/brain/src/modules/db/types.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export interface StakingBrainDb {
1212
[pubkey: string]: PubkeyDetails;
1313
}
1414

15-
export interface StakingBrainDbUpdate {
16-
[pubkey: string]: Partial<Omit<PubkeyDetails, "automaticImport" | "tag">>;
15+
export interface ValidatorUpdate extends Partial<Omit<PubkeyDetails, "automaticImport" | "tag">> {
16+
pubkey: string;
1717
}
1818

1919
export interface CustomValidators extends PubkeyDetails {

0 commit comments

Comments
 (0)