|
1 | | -import { withValidatorOperationLock } from "../../../db/withValidatorOperationLock.js"; |
2 | 1 | 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"; |
4 | 4 | import { CustomValidatorUpdateRequest } from "./types.js"; |
5 | | -import { StakingBrainDbUpdate } from "../../../../modules/db/types.js"; |
| 5 | +import { StakingBrainDb } from "../../../db/types.js"; |
6 | 6 | import { CronJob } from "../../../cron/cron.js"; |
7 | 7 | import { BrainDataBase } from "../../../db/index.js"; |
8 | 8 | import { ValidatorApi } from "../../../apiClients/validator/index.js"; |
9 | 9 | import { applyValidatorBuilderConfig } from "../../../cron/reloadValidators/postValidatorsBuilderConfigsFromDb.js"; |
10 | 10 |
|
| 11 | +type UpdateValidatorsOptions = { |
| 12 | + reloadValidatorsCronTask: CronJob; |
| 13 | + brainDb: BrainDataBase; |
| 14 | + validatorApi: ValidatorApi; |
| 15 | + signerUrl: string; |
| 16 | + customValidatorUpdateRequest: CustomValidatorUpdateRequest[]; |
| 17 | + requestFrom?: ActionRequestOrigin; |
| 18 | +}; |
| 19 | + |
11 | 20 | /** Persist desired settings first. Periodic reload retries client failures. */ |
12 | 21 | export async function updateValidators({ |
13 | 22 | reloadValidatorsCronTask, |
14 | 23 | brainDb, |
15 | 24 | validatorApi, |
16 | | - customValidatorUpdateRequest, |
| 25 | + signerUrl, |
| 26 | + customValidatorUpdateRequest: requests, |
17 | 27 | requestFrom |
18 | | -}: { |
19 | | - reloadValidatorsCronTask: CronJob; |
20 | | - brainDb: BrainDataBase; |
21 | | - validatorApi: ValidatorApi; |
22 | | - customValidatorUpdateRequest: CustomValidatorUpdateRequest[]; |
23 | | - requestFrom?: ActionRequestOrigin; |
24 | | -}): Promise<void> { |
| 28 | +}: UpdateValidatorsOptions): Promise<void> { |
25 | 29 | return withValidatorOperationLock(brainDb, async () => { |
26 | 30 | reloadValidatorsCronTask.stop(); |
27 | 31 | 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); |
46 | 34 |
|
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); |
69 | 39 | } finally { |
70 | 40 | reloadValidatorsCronTask.start(); |
71 | 41 | } |
72 | 42 | }); |
73 | 43 | } |
| 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 | +} |
0 commit comments