diff --git a/packages/brain/src/modules/cron/index.ts b/packages/brain/src/modules/cron/index.ts index f55c5a80..aa1615e7 100644 --- a/packages/brain/src/modules/cron/index.ts +++ b/packages/brain/src/modules/cron/index.ts @@ -49,7 +49,7 @@ export const getCrons = ({ }); }), reloadValidatorsCronTask: new CronJob(60 * 1000, () => - reloadValidators(signerApi, signerUrl, validatorApi, brainDb) + reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb) ) }; }; diff --git a/packages/brain/src/modules/cron/reloadValidators/index.ts b/packages/brain/src/modules/cron/reloadValidators/index.ts index 8519a9a7..34f52924 100644 --- a/packages/brain/src/modules/cron/reloadValidators/index.ts +++ b/packages/brain/src/modules/cron/reloadValidators/index.ts @@ -1,4 +1,4 @@ -import { Web3SignerApi, ValidatorApi } from "../../apiClients/index.js"; +import { Web3SignerApi, ValidatorApi, BeaconchainApi } from "../../apiClients/index.js"; import { BrainDataBase } from "../../db/index.js"; import logger from "../../logger/index.js"; import { deleteDbPubkeysNotInSigner } from "./deleteDbPubkeysNotInSigner.js"; @@ -7,6 +7,7 @@ import { deleteValidatorPubkeysNotInDB } from "./deleteValidatorPubkeysNotInDb.j import { logPrefix } from "./logPrefix.js"; import { postValidatorPubkeysFromDb } from "./postValidatorPubkeysFromDb.js"; import { postValidatorsFeeRecipientsFromDb } from "./postValidatorsFeeRecipientsFromDb.js"; +import { persistValidatorIndices } from "./persistValidatorIndices.js"; /** * Reload db data based on truth sources: validator and signer APIs: @@ -16,12 +17,14 @@ import { postValidatorsFeeRecipientsFromDb } from "./postValidatorsFeeRecipients * - 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 + * - FETCH and PERSIST validator indices from Beacon API for pubkeys without indices * */ export async function reloadValidators( signerApi: Web3SignerApi, signerUrl: string, validatorApi: ValidatorApi, + beaconchainApi: BeaconchainApi, brainDb: BrainDataBase ): Promise { try { @@ -63,6 +66,12 @@ export async function reloadValidators( dbData: brainDb.getData() }); + // 7. FETCH and PERSIST validator indices from Beacon API for all pubkeys in DB + await persistValidatorIndices({ + beaconchainApi, + brainDb + }); + logger.debug(`${logPrefix}Finished reloading data`); } catch (e) { logger.error(`${logPrefix}Error reloading data`, e); diff --git a/packages/brain/src/modules/cron/reloadValidators/persistValidatorIndices.ts b/packages/brain/src/modules/cron/reloadValidators/persistValidatorIndices.ts new file mode 100644 index 00000000..2d55a220 --- /dev/null +++ b/packages/brain/src/modules/cron/reloadValidators/persistValidatorIndices.ts @@ -0,0 +1,106 @@ +import { BeaconchainApi } from "../../apiClients/index.js"; +import { BrainDataBase } from "../../db/index.js"; +import { ValidatorStatus } from "../../apiClients/beaconchain/types.js"; +import { shortenPubkey } from "@stakingbrain/common"; +import logger from "../../logger/index.js"; +import { logPrefix } from "./logPrefix.js"; +import { StakingBrainDb } from "../../db/types.js"; + +interface ValidatorUpdate { + index: number; + status: ValidatorStatus; + feeRecipient: string; +} + +interface UpdateResult { + validatorsToUpdate: Record; + newIndicesCount: number; + statusChangesCount: number; +} + +/** + * Fetches validator indices and statuses from the Beacon API for all validators + * in the database and persists any changes. + */ +export async function persistValidatorIndices({ + beaconchainApi, + brainDb +}: { + beaconchainApi: BeaconchainApi; + brainDb: BrainDataBase; +}): Promise { + try { + const dbData = brainDb.getData(); + const allPubkeys = Object.keys(dbData); + + if (allPubkeys.length === 0) { + logger.debug(`${logPrefix}No validators in database to fetch data for`); + return; + } + + logger.debug(`${logPrefix}Fetching indices and statuses for ${allPubkeys.length} validators`); + + const response = await beaconchainApi.postStateValidators({ + stateId: "head", + body: { ids: allPubkeys, statuses: [] } + }); + + const { validatorsToUpdate, newIndicesCount, statusChangesCount } = processValidatorResponse(response.data, dbData); + + const updateCount = Object.keys(validatorsToUpdate).length; + if (updateCount > 0) { + brainDb.updateValidators({ validators: validatorsToUpdate }); + logger.debug( + `${logPrefix}Persisted ${updateCount} validator updates (${newIndicesCount} new indices, ${statusChangesCount} status changes)` + ); + } + } catch (e) { + logger.error(`${logPrefix}Error persisting validator indices and statuses`, e); + } +} + +/** + * Processes the beacon API response and identifies validators that need updating. + */ +function processValidatorResponse( + responseData: { index: string; status: ValidatorStatus; validator: { pubkey: string } }[], + dbData: StakingBrainDb +): UpdateResult { + const validatorsToUpdate: Record = {}; + let newIndicesCount = 0; + let statusChangesCount = 0; + + for (const validatorData of responseData) { + const pubkey = validatorData.validator.pubkey; + const dbEntry = dbData[pubkey]; + + if (!dbEntry) continue; + + const newIndex = parseInt(validatorData.index); + const newStatus = validatorData.status; + const indexChanged = dbEntry.index !== newIndex; + const statusChanged = dbEntry.status !== newStatus; + + if (!indexChanged && !statusChanged) continue; + + validatorsToUpdate[pubkey] = { + index: newIndex, + status: newStatus, + feeRecipient: dbEntry.feeRecipient + }; + + if (dbEntry.index === undefined) { + newIndicesCount++; + logger.info(`${logPrefix}Validator ${shortenPubkey(pubkey)} assigned index ${newIndex} with status ${newStatus}`); + } + + if (dbEntry.status !== undefined && statusChanged) { + statusChangesCount++; + logger.info( + `${logPrefix}Validator ${shortenPubkey(pubkey)} (index ${newIndex}) status changed: ${dbEntry.status} → ${newStatus}` + ); + } + } + + return { validatorsToUpdate, newIndicesCount, statusChangesCount }; +} diff --git a/packages/brain/src/modules/db/index.ts b/packages/brain/src/modules/db/index.ts index 87ee6c22..010cea83 100644 --- a/packages/brain/src/modules/db/index.ts +++ b/packages/brain/src/modules/db/index.ts @@ -96,7 +96,7 @@ export class BrainDataBase extends LowSync { } /** - * Updates 1 or more validators in db. The fields available to update are feeRecipient and index + * Updates 1 or more validators in db. The fields available to update are feeRecipient, index, and status */ public updateValidators({ validators }: { validators: StakingBrainDbUpdate }): void { try { @@ -110,7 +110,14 @@ export class BrainDataBase extends LowSync { delete validators[pubkey]; } else { this.data[pubkey].feeRecipient = validators[pubkey].feeRecipient; - this.data[pubkey].index = validators[pubkey].index; + // Optional fields. Only update if provided so we dont overwrite existing data with undefined + // Index cant change once defined by ethereum and status should change only a few times in a validator lifetime + if (validators[pubkey].index !== undefined) { + this.data[pubkey].index = validators[pubkey].index; + } + if (validators[pubkey].status !== undefined) { + this.data[pubkey].status = validators[pubkey].status; + } } } diff --git a/packages/brain/src/modules/db/types.ts b/packages/brain/src/modules/db/types.ts index 2a400efe..387c1bf0 100644 --- a/packages/brain/src/modules/db/types.ts +++ b/packages/brain/src/modules/db/types.ts @@ -1,4 +1,5 @@ import { Tag } from "@stakingbrain/common"; +import { ValidatorStatus } from "../apiClients/beaconchain/types.js"; /** * DbSlot represents the line in the database for a given public key: @@ -25,6 +26,7 @@ export interface PubkeyDetails { feeRecipient: string; automaticImport: boolean; index?: number; // index of the validator. Only available if the validator is active. + status?: ValidatorStatus; // status of the validator from the Beacon API. } export const nonEditableFeeRecipientTags = ["rocketpool", "stader", "stakewise", "lido"] as const; diff --git a/packages/brain/test/unit/modules/apiClients/cron.unit.test.ts b/packages/brain/test/unit/modules/apiClients/cron.unit.test.ts index 67fd4310..e6f04c1c 100644 --- a/packages/brain/test/unit/modules/apiClients/cron.unit.test.ts +++ b/packages/brain/test/unit/modules/apiClients/cron.unit.test.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { before } from "mocha"; -import { ValidatorApi, Web3SignerApi } from "../../../../src/modules/apiClients/index.js"; +import { ValidatorApi, Web3SignerApi, BeaconchainApi } from "../../../../src/modules/apiClients/index.js"; import { execSync } from "node:child_process"; import { BrainDataBase } from "../../../../src/modules/db/index.js"; import fs from "fs"; @@ -60,6 +60,7 @@ describe.skip("Cron: Prater", () => { describe(`Consensus client: ${consensusClient.name}`, () => { let validatorApi: ValidatorApi; let signerApi: Web3SignerApi; + let beaconchainApi: BeaconchainApi; let brainDb: BrainDataBase; let signerUrl: string; @@ -97,6 +98,15 @@ describe.skip("Cron: Prater", () => { ); signerUrl = `http://${signerIp}:9000`; + // Mock BeaconchainApi - postStateValidators returns empty array (validators not active yet) + beaconchainApi = { + postStateValidators: async () => ({ + execution_optimistic: false, + finalized: true, + data: [] // No validators found on beacon chain yet + }) + } as unknown as BeaconchainApi; + if (fs.existsSync(testDbName)) fs.unlinkSync(testDbName); brainDb = new BrainDataBase(testDbName); }); @@ -132,7 +142,7 @@ describe.skip("Cron: Prater", () => { }); //Check that fee recipient has changed in validator - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); const validatorFeeRecipient = await validatorApi.getFeeRecipient(pubkeyToTest); @@ -144,7 +154,7 @@ describe.skip("Cron: Prater", () => { addSampleValidatorsToDB(1); await addSampleKeystoresToSigner(2); - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); const signerPubkeys = await signerApi.listRemoteKeys(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -159,7 +169,7 @@ describe.skip("Cron: Prater", () => { addSampleValidatorsToDB(2); await addSampleKeystoresToSigner(1); - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); const signerPubkeys = await signerApi.listRemoteKeys(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -177,7 +187,7 @@ describe.skip("Cron: Prater", () => { brainDb.deleteValidators([pubkeys[0]]); await signerApi.deleteRemoteKeys({ pubkeys: [pubkeys[1]] }); - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); const signerPubkeys = await signerApi.listRemoteKeys(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -190,7 +200,7 @@ describe.skip("Cron: Prater", () => { addSampleValidatorsToDB(2); await addSampleKeystoresToSigner(2); - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); const signerPubkeys = await signerApi.listRemoteKeys(); const dbPubkeys = Object.keys(brainDb.getData()); @@ -208,7 +218,7 @@ describe.skip("Cron: Prater", () => { console.log("Added pubkeys to validator"); - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); console.log("Validators reloaded"); @@ -225,7 +235,7 @@ describe.skip("Cron: Prater", () => { const pubkeysToTest = pubkeys.slice(0, 2); - await reloadValidators(signerApi, signerUrl, validatorApi, brainDb); + await reloadValidators(signerApi, signerUrl, validatorApi, beaconchainApi, brainDb); const validatorPubkeys = await validatorApi.getRemoteKeys(); diff --git a/packages/brain/test/unit/modules/cron/persistValidatorIndices.unit.test.ts b/packages/brain/test/unit/modules/cron/persistValidatorIndices.unit.test.ts new file mode 100644 index 00000000..5b1987c9 --- /dev/null +++ b/packages/brain/test/unit/modules/cron/persistValidatorIndices.unit.test.ts @@ -0,0 +1,407 @@ +import { expect } from "chai"; +import { describe, it, beforeEach } from "mocha"; +import { BrainDataBase } from "../../../../src/modules/db/index.js"; +import { BeaconchainApi } from "../../../../src/modules/apiClients/index.js"; +import { persistValidatorIndices } from "../../../../src/modules/cron/reloadValidators/persistValidatorIndices.js"; +import { ValidatorStatus } from "../../../../src/modules/apiClients/beaconchain/types.js"; +import fs from "fs"; + +type MockBeaconchainApi = Pick; + +describe("persistValidatorIndices", () => { + const testDbName = "test-persist-validator-indices.json"; + let brainDb: BrainDataBase; + + // Use valid BLS pubkeys from the integration tests + const mockPubkey1 = + "0xa2cc280ce811bb680cba309103e23dc3c9902f2a08541c6737e8adfe8198e796023b959fc8aadfad39499b56ec3dd184"; + const mockPubkey2 = + "0x86d25af52627204ab822a20ac70da6767952841edbcb0b83c84a395205313661de5f7f76efa475a46f45fa89d95c1dd7"; + const mockPubkey3 = + "0x821a80380122281580ba8a56cd21956933d43c62fdc8f5b4ec31b2c620e8534e80b6b816c9a2cc8d25568dc4ebcfd47a"; + + beforeEach(() => { + // Clean up test database + if (fs.existsSync(testDbName)) { + fs.unlinkSync(testDbName); + } + brainDb = new BrainDataBase(testDbName); + }); + + afterEach(() => { + // Clean up test database + if (fs.existsSync(testDbName)) { + fs.unlinkSync(testDbName); + } + }); + + it("should fetch and persist indices and statuses for all validators", async () => { + // Add validators to database + brainDb.addValidators({ + validators: { + [mockPubkey1]: { + tag: "solo", + feeRecipient: "0x1111111111111111111111111111111111111111", + automaticImport: true + }, + [mockPubkey2]: { + tag: "solo", + feeRecipient: "0x2222222222222222222222222222222222222222", + automaticImport: false + } + } + }); + + // Mock BeaconchainApi + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async ({ stateId, body }) => { + expect(stateId).to.equal("head"); + expect(body.ids).to.deep.equal([mockPubkey1, mockPubkey2]); + expect(body.statuses).to.deep.equal([]); + + return { + execution_optimistic: false, + finalized: true, + data: [ + { + index: "123456", + balance: "32000000000", + status: ValidatorStatus.ACTIVE_ONGOING, + validator: { + pubkey: mockPubkey1, + withdrawal_credentials: "0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12", + effective_balance: "32000000000", + slashed: false, + activation_eligibility_epoch: "0", + activation_epoch: "100", + exit_epoch: "9999999999", + withdrawable_epoch: "9999999999" + } + }, + { + index: "654321", + balance: "32000000000", + status: ValidatorStatus.PENDING_QUEUED, + validator: { + pubkey: mockPubkey2, + withdrawal_credentials: "0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12", + effective_balance: "32000000000", + slashed: false, + activation_eligibility_epoch: "0", + activation_epoch: "9999999999", + exit_epoch: "9999999999", + withdrawable_epoch: "9999999999" + } + } + ] + }; + } + }; + + // Call the function + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Verify the data was persisted + const dbData = brainDb.getData(); + + expect(dbData[mockPubkey1].index).to.equal(123456); + expect(dbData[mockPubkey1].status).to.equal(ValidatorStatus.ACTIVE_ONGOING); + expect(dbData[mockPubkey1].feeRecipient).to.equal("0x1111111111111111111111111111111111111111"); + + expect(dbData[mockPubkey2].index).to.equal(654321); + expect(dbData[mockPubkey2].status).to.equal(ValidatorStatus.PENDING_QUEUED); + expect(dbData[mockPubkey2].feeRecipient).to.equal("0x2222222222222222222222222222222222222222"); + }); + + it("should update existing indices and statuses", async () => { + // Add validators with existing indices + brainDb.addValidators({ + validators: { + [mockPubkey1]: { + tag: "solo", + feeRecipient: "0x1111111111111111111111111111111111111111", + automaticImport: true + } + } + }); + + // Manually set initial index and status + brainDb.updateValidators({ + validators: { + [mockPubkey1]: { + feeRecipient: "0x1111111111111111111111111111111111111111", + index: 123456, + status: ValidatorStatus.PENDING_QUEUED + } + } + }); + + // Mock BeaconchainApi with updated status + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async () => ({ + execution_optimistic: false, + finalized: true, + data: [ + { + index: "123456", + balance: "32000000000", + status: ValidatorStatus.ACTIVE_ONGOING, // Status changed from PENDING_QUEUED to ACTIVE_ONGOING + validator: { + pubkey: mockPubkey1, + withdrawal_credentials: "0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12", + effective_balance: "32000000000", + slashed: false, + activation_eligibility_epoch: "0", + activation_epoch: "100", + exit_epoch: "9999999999", + withdrawable_epoch: "9999999999" + } + } + ] + }) + }; + + // Call the function + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Verify the status was updated + const dbData = brainDb.getData(); + expect(dbData[mockPubkey1].index).to.equal(123456); + expect(dbData[mockPubkey1].status).to.equal(ValidatorStatus.ACTIVE_ONGOING); + }); + + it("should handle validators not found on beacon chain", async () => { + // Add validators to database + brainDb.addValidators({ + validators: { + [mockPubkey1]: { + tag: "solo", + feeRecipient: "0x1111111111111111111111111111111111111111", + automaticImport: true + }, + [mockPubkey2]: { + tag: "solo", + feeRecipient: "0x2222222222222222222222222222222222222222", + automaticImport: false + } + } + }); + + // Mock BeaconchainApi - only returns one validator + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async () => ({ + execution_optimistic: false, + finalized: true, + data: [ + { + index: "123456", + balance: "32000000000", + status: ValidatorStatus.ACTIVE_ONGOING, + validator: { + pubkey: mockPubkey1, + withdrawal_credentials: "0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12", + effective_balance: "32000000000", + slashed: false, + activation_eligibility_epoch: "0", + activation_epoch: "100", + exit_epoch: "9999999999", + withdrawable_epoch: "9999999999" + } + } + // mockPubkey2 not returned (not deposited yet) + ] + }) + }; + + // Call the function + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Verify only the found validator was updated + const dbData = brainDb.getData(); + + expect(dbData[mockPubkey1].index).to.equal(123456); + expect(dbData[mockPubkey1].status).to.equal(ValidatorStatus.ACTIVE_ONGOING); + + // mockPubkey2 should have no index or status + expect(dbData[mockPubkey2].index).to.be.undefined; + expect(dbData[mockPubkey2].status).to.be.undefined; + }); + + it("should handle empty database gracefully", async () => { + // Mock BeaconchainApi + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async () => { + throw new Error("Should not be called with empty database"); + } + }; + + // Call the function with empty database (should return early) + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Should complete without error + expect(Object.keys(brainDb.getData()).length).to.equal(0); + }); + + it("should handle API errors gracefully", async () => { + // Add validators to database + brainDb.addValidators({ + validators: { + [mockPubkey1]: { + tag: "solo", + feeRecipient: "0x1111111111111111111111111111111111111111", + automaticImport: true + } + } + }); + + // Mock BeaconchainApi that throws an error + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async () => { + throw new Error("Beacon API connection failed"); + } + }; + + // Call the function (should not throw) + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Verify data is unchanged + const dbData = brainDb.getData(); + expect(dbData[mockPubkey1].index).to.be.undefined; + expect(dbData[mockPubkey1].status).to.be.undefined; + }); + + it("should not update validators when index and status haven't changed", async () => { + // Add validators with existing indices and statuses + brainDb.addValidators({ + validators: { + [mockPubkey1]: { + tag: "solo", + feeRecipient: "0x1111111111111111111111111111111111111111", + automaticImport: true + } + } + }); + + // Set initial index and status + brainDb.updateValidators({ + validators: { + [mockPubkey1]: { + feeRecipient: "0x1111111111111111111111111111111111111111", + index: 123456, + status: ValidatorStatus.ACTIVE_ONGOING + } + } + }); + + let updateValidatorsCalled = false; + const originalUpdateValidators = brainDb.updateValidators.bind(brainDb); + brainDb.updateValidators = function (...args) { + updateValidatorsCalled = true; + return originalUpdateValidators(...args); + }; + + // Mock BeaconchainApi returns same data (no changes) + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async () => ({ + execution_optimistic: false, + finalized: true, + data: [ + { + index: "123456", // Same index + balance: "32000000000", + status: ValidatorStatus.ACTIVE_ONGOING, // Same status + validator: { + pubkey: mockPubkey1, + withdrawal_credentials: "0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12", + effective_balance: "32000000000", + slashed: false, + activation_eligibility_epoch: "0", + activation_epoch: "100", + exit_epoch: "9999999999", + withdrawable_epoch: "9999999999" + } + } + ] + }) + }; + + // Call the function + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Verify updateValidators was NOT called (no changes to persist) + expect(updateValidatorsCalled).to.be.false; + + // Verify data remains the same + const dbData = brainDb.getData(); + expect(dbData[mockPubkey1].index).to.equal(123456); + expect(dbData[mockPubkey1].status).to.equal(ValidatorStatus.ACTIVE_ONGOING); + }); + + it("should handle response with unknown pubkeys gracefully", async () => { + // Add validators to database + brainDb.addValidators({ + validators: { + [mockPubkey1]: { + tag: "solo", + feeRecipient: "0x1111111111111111111111111111111111111111", + automaticImport: true + } + } + }); + + // Mock BeaconchainApi returns a different pubkey + const mockBeaconchainApi: MockBeaconchainApi = { + postStateValidators: async () => ({ + execution_optimistic: false, + finalized: true, + data: [ + { + index: "999999", + balance: "32000000000", + status: ValidatorStatus.ACTIVE_ONGOING, + validator: { + pubkey: mockPubkey3, // Different pubkey not in our database + withdrawal_credentials: "0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12", + effective_balance: "32000000000", + slashed: false, + activation_eligibility_epoch: "0", + activation_epoch: "100", + exit_epoch: "9999999999", + withdrawable_epoch: "9999999999" + } + } + ] + }) + }; + + // Call the function + await persistValidatorIndices({ + beaconchainApi: mockBeaconchainApi as BeaconchainApi, + brainDb + }); + + // Verify our validator was not updated (unknown pubkey ignored) + const dbData = brainDb.getData(); + expect(dbData[mockPubkey1].index).to.be.undefined; + expect(dbData[mockPubkey1].status).to.be.undefined; + expect(dbData[mockPubkey3]).to.be.undefined; // Not added to database + }); +});