From e5d9810214d7838d68b9a1ac6010e3f37e492e97 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Tue, 16 Jan 2024 16:24:31 +0100 Subject: [PATCH] test: migrate validator tests to vitest (#6301) * Migrate validator tests to vitest * Increase hook timeout for e2e test --- packages/validator/.mocharc.yaml | 6 - packages/validator/.nycrc.json | 3 - packages/validator/package.json | 4 +- .../validator/test/e2e/web3signer.test.ts | 13 +- .../test/unit/services/attestation.test.ts | 97 +++++------- .../unit/services/attestationDuties.test.ts | 103 ++++++------- .../test/unit/services/block.test.ts | 104 ++++++------- .../test/unit/services/blockDuties.test.ts | 69 +++------ .../test/unit/services/doppelganger.test.ts | 6 +- .../test/unit/services/indicesService.test.ts | 28 ++-- .../unit/services/syncCommitteDuties.test.ts | 141 ++++++++---------- .../test/unit/services/syncCommittee.test.ts | 90 +++++------ .../test/unit/services/utils.test.ts | 8 +- .../interchange/index.test.ts | 4 +- .../minMaxSurround/surroundTests.test.ts | 4 +- .../minMaxSurround/updateSpans.test.ts | 4 +- .../unit/slashingProtection/utils.test.ts | 10 +- .../validator/test/unit/utils/batch.test.ts | 4 +- .../validator/test/unit/utils/clock.test.ts | 59 ++++---- .../test/unit/utils/difference.test.ts | 6 +- .../validator/test/unit/utils/format.test.ts | 4 +- .../validator/test/unit/utils/metrics.test.ts | 6 +- .../validator/test/unit/utils/params.test.ts | 4 +- .../test/unit/validatorStore.test.ts | 53 +++---- packages/validator/test/utils/apiStub.ts | 47 +++--- packages/validator/test/utils/types.ts | 5 - 26 files changed, 383 insertions(+), 499 deletions(-) delete mode 100644 packages/validator/.mocharc.yaml delete mode 100644 packages/validator/.nycrc.json diff --git a/packages/validator/.mocharc.yaml b/packages/validator/.mocharc.yaml deleted file mode 100644 index d4114a3f2397..000000000000 --- a/packages/validator/.mocharc.yaml +++ /dev/null @@ -1,6 +0,0 @@ -colors: true -extension: ["ts"] -require: - - ./test/setup.ts -node-option: - - "loader=ts-node/esm" diff --git a/packages/validator/.nycrc.json b/packages/validator/.nycrc.json deleted file mode 100644 index 69aa626339a0..000000000000 --- a/packages/validator/.nycrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../.nycrc.json" -} diff --git a/packages/validator/package.json b/packages/validator/package.json index f46b11332938..14ee1002b214 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -28,10 +28,10 @@ "lint": "eslint --color --ext .ts src/ test/", "lint:fix": "yarn run lint --fix", "pretest": "yarn run check-types", - "test:unit": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'", + "test:unit": "vitest --run --dir test/unit --coverage", "test": "yarn test:unit", "test:spec": "vitest --run --config vitest.config.spec.ts --dir test/spec/", - "test:e2e": "mocha 'test/e2e/**/*.test.ts'", + "test:e2e": "LODESTAR_PRESET=mainnet vitest --run --poolOptions.threads.singleThread true --dir test/e2e", "download-spec-tests": "node --loader=ts-node/esm test/spec/downloadTests.ts", "coverage": "codecov -F lodestar-validator", "check-readme": "typescript-docs-verifier" diff --git a/packages/validator/test/e2e/web3signer.test.ts b/packages/validator/test/e2e/web3signer.test.ts index 933e59d800ad..c93f1786dfb4 100644 --- a/packages/validator/test/e2e/web3signer.test.ts +++ b/packages/validator/test/e2e/web3signer.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {expect, describe, it, vi, beforeAll, afterAll} from "vitest"; import {fromHex, toHex} from "@lodestar/utils"; import {config} from "@lodestar/config/default"; import {computeStartSlotAtEpoch, interopSecretKey, interopSecretKeys} from "@lodestar/state-transition"; @@ -13,7 +13,7 @@ import {IndicesService} from "../../src/services/indices.js"; import {testLogger} from "../utils/logger.js"; describe("web3signer signature test", function () { - this.timeout("60s"); + vi.setConfig({testTimeout: 60_000, hookTimeout: 60_000}); const altairSlot = 2375711; const epoch = 0; @@ -39,7 +39,7 @@ describe("web3signer signature test", function () { pubkey: pubkeyBytes, }; - before("set up validator stores", async () => { + beforeAll(async () => { validatorStoreLocal = await getValidatorStore({type: SignerType.Local, secretKey: secretKey}); const password = "password"; @@ -57,15 +57,16 @@ describe("web3signer signature test", function () { }); }); - after("stop external signer container", async () => { + afterAll(async () => { await externalSigner.container.stop(); }); for (const fork of config.forksAscendingEpochOrder) { - it(`signBlock ${fork.name}`, async function () { + it(`signBlock ${fork.name}`, async ({skip}) => { // Only test till the fork the signer version supports if (ForkSeq[fork.name] > externalSigner.supportedForkSeq) { - this.skip(); + skip(); + return; } const block = ssz[fork.name].BeaconBlock.defaultValue(); diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index 754b9a133ff7..c8010fe1bdf0 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect, beforeAll, beforeEach, afterEach, vi} from "vitest"; import sinon from "sinon"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; @@ -14,32 +14,36 @@ import {ChainHeaderTracker} from "../../../src/services/chainHeaderTracker.js"; import {ValidatorEventEmitter} from "../../../src/services/emitter.js"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../utils/types.js"; +vi.mock("../../../src/services/validatorStore.js"); +vi.mock("../../../src/services/emitter.js"); +vi.mock("../../../src/services/chainHeaderTracker.js"); + describe("AttestationService", function () { - const sandbox = sinon.createSandbox(); - - const api = getApiClientStub(sandbox); - const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & - sinon.SinonStubbedInstance; - const emitter = sinon.createStubInstance(ValidatorEventEmitter) as ValidatorEventEmitter & - sinon.SinonStubbedInstance; - const chainHeadTracker = sinon.createStubInstance(ChainHeaderTracker) as ChainHeaderTracker & - sinon.SinonStubbedInstance; + const api = getApiClientStub(); + // @ts-expect-error - Mocked class don't need parameters + const validatorStore = vi.mocked(new ValidatorStore()); + const emitter = vi.mocked(new ValidatorEventEmitter()); + // @ts-expect-error - Mocked class don't need parameters + const chainHeadTracker = vi.mocked(new ChainHeaderTracker()); + let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized - before(() => { + beforeAll(() => { const secretKeys = Array.from({length: 1}, (_, i) => bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1))); pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes()); - validatorStore.votingPubkeys.returns(pubkeys.map(toHexString)); - validatorStore.hasVotingPubkey.returns(true); - validatorStore.hasSomeValidators.returns(true); - validatorStore.signAttestationSelectionProof.resolves(ZERO_HASH); + validatorStore.votingPubkeys.mockReturnValue(pubkeys.map(toHexString)); + validatorStore.hasVotingPubkey.mockReturnValue(true); + validatorStore.hasSomeValidators.mockReturnValue(true); + validatorStore.signAttestationSelectionProof.mockResolvedValue(ZERO_HASH); }); let controller: AbortController; // To stop clock - beforeEach(() => (controller = new AbortController())); + beforeEach(() => { + controller = new AbortController(); + }); afterEach(() => { controller.abort(); - sandbox.resetHistory(); + vi.resetAllMocks(); }); const testContexts: [string, AttestationServiceOpts][] = [ @@ -49,7 +53,7 @@ describe("AttestationService", function () { ]; for (const [title, opts] of testContexts) { - context(title, () => { + describe(title, () => { it("Should produce, sign, and publish an attestation + aggregate", async () => { const clock = new ClockMock(); const attestationService = new AttestationService( @@ -82,12 +86,12 @@ describe("AttestationService", function () { ]; // Return empty replies to duties service - api.beacon.getStateValidators.resolves({ + api.beacon.getStateValidators.mockResolvedValue({ response: {executionOptimistic: false, data: []}, ok: true, status: HttpStatusCode.OK, }); - api.validator.getAttesterDuties.resolves({ + api.validator.getAttesterDuties.mockResolvedValue({ response: {dependentRoot: ZERO_HASH_HEX, executionOptimistic: false, data: []}, ok: true, status: HttpStatusCode.OK, @@ -98,22 +102,22 @@ describe("AttestationService", function () { // Mock beacon's attestation and aggregates endpoints - api.validator.produceAttestationData.resolves({ + api.validator.produceAttestationData.mockResolvedValue({ response: {data: attestation.data}, ok: true, status: HttpStatusCode.OK, }); - api.validator.getAggregatedAttestation.resolves({ + api.validator.getAggregatedAttestation.mockResolvedValue({ response: {data: attestation}, ok: true, status: HttpStatusCode.OK, }); - api.beacon.submitPoolAttestations.resolves({ + api.beacon.submitPoolAttestations.mockResolvedValue({ response: undefined, ok: true, status: HttpStatusCode.OK, }); - api.validator.publishAggregateAndProofs.resolves({ + api.validator.publishAggregateAndProofs.mockResolvedValue({ response: undefined, ok: true, status: HttpStatusCode.OK, @@ -122,13 +126,13 @@ describe("AttestationService", function () { if (opts.distributedAggregationSelection) { // Mock distributed validator middleware client selections endpoint // and return a selection proof that passes `is_aggregator` test - api.validator.submitBeaconCommitteeSelections.resolves({ + api.validator.submitBeaconCommitteeSelections.mockResolvedValue({ response: {data: [{validatorIndex: 0, slot: 0, selectionProof: Buffer.alloc(1, 0x10)}]}, ok: true, status: HttpStatusCode.OK, }); // Accept all subscriptions - api.validator.prepareBeaconCommitteeSubnet.resolves({ + api.validator.prepareBeaconCommitteeSubnet.mockResolvedValue({ response: undefined, ok: true, status: HttpStatusCode.OK, @@ -136,8 +140,8 @@ describe("AttestationService", function () { } // Mock signing service - validatorStore.signAttestation.resolves(attestation); - validatorStore.signAggregateAndProof.resolves(aggregate); + validatorStore.signAttestation.mockResolvedValue(attestation); + validatorStore.signAggregateAndProof.mockResolvedValue(aggregate); // Trigger clock onSlot for slot 0 await clock.tickSlotFns(0, controller.signal); @@ -149,14 +153,8 @@ describe("AttestationService", function () { slot: 0, selectionProof: ZERO_HASH, }; - expect(api.validator.submitBeaconCommitteeSelections.callCount).to.equal( - 1, - "submitBeaconCommitteeSelections() must be called once" - ); - expect(api.validator.submitBeaconCommitteeSelections.getCall(0).args).to.deep.equal( - [[selection]], // 1 arg, = selection[] - "wrong submitBeaconCommitteeSelections() args" - ); + expect(api.validator.submitBeaconCommitteeSelections).toHaveBeenCalledOnce(); + expect(api.validator.submitBeaconCommitteeSelections).toHaveBeenCalledWith([selection]); // Must resubscribe validator as aggregator on beacon committee subnet const subscription: routes.validator.BeaconCommitteeSubscription = { @@ -166,32 +164,17 @@ describe("AttestationService", function () { slot: 0, isAggregator: true, }; - expect(api.validator.prepareBeaconCommitteeSubnet.callCount).to.equal( - 1, - "prepareBeaconCommitteeSubnet() must be called once" - ); - expect(api.validator.prepareBeaconCommitteeSubnet.getCall(0).args).to.deep.equal( - [[subscription]], // 1 arg, = subscription[] - "wrong prepareBeaconCommitteeSubnet() args" - ); + expect(api.validator.prepareBeaconCommitteeSubnet).toHaveBeenCalledOnce(); + expect(api.validator.prepareBeaconCommitteeSubnet).toHaveBeenCalledWith([subscription]); } // Must submit the attestation received through produceAttestationData() - expect(api.beacon.submitPoolAttestations.callCount).to.equal(1, "submitAttestations() must be called once"); - expect(api.beacon.submitPoolAttestations.getCall(0).args).to.deep.equal( - [[attestation]], // 1 arg, = attestation[] - "wrong submitAttestations() args" - ); + expect(api.beacon.submitPoolAttestations).toHaveBeenCalledOnce(); + expect(api.beacon.submitPoolAttestations).toHaveBeenCalledWith([attestation]); // Must submit the aggregate received through getAggregatedAttestation() then createAndSignAggregateAndProof() - expect(api.validator.publishAggregateAndProofs.callCount).to.equal( - 1, - "publishAggregateAndProofs() must be called once" - ); - expect(api.validator.publishAggregateAndProofs.getCall(0).args).to.deep.equal( - [[aggregate]], // 1 arg, = aggregate[] - "wrong publishAggregateAndProofs() args" - ); + expect(api.validator.publishAggregateAndProofs).toHaveBeenCalledOnce(); + expect(api.validator.publishAggregateAndProofs).toHaveBeenCalledWith([aggregate]); }); }); } diff --git a/packages/validator/test/unit/services/attestationDuties.test.ts b/packages/validator/test/unit/services/attestationDuties.test.ts index 0a4a1b2c2fe9..3edd091d3fcd 100644 --- a/packages/validator/test/unit/services/attestationDuties.test.ts +++ b/packages/validator/test/unit/services/attestationDuties.test.ts @@ -1,6 +1,5 @@ +import {describe, it, expect, beforeAll, vi, Mocked, beforeEach, afterEach} from "vitest"; import {toBufferBE} from "bigint-buffer"; -import {expect} from "chai"; -import sinon from "sinon"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {chainConfig} from "@lodestar/config/default"; @@ -16,14 +15,15 @@ import {initValidatorStore} from "../../utils/validatorStore.js"; import {ChainHeaderTracker} from "../../../src/services/chainHeaderTracker.js"; import {ZERO_HASH_HEX} from "../../utils/types.js"; +vi.mock("../../../src/services/chainHeaderTracker.js"); + describe("AttestationDutiesService", function () { - const sandbox = sinon.createSandbox(); - const api = getApiClientStub(sandbox); + const api = getApiClientStub(); let validatorStore: ValidatorStore; - const chainHeadTracker = sinon.createStubInstance(ChainHeaderTracker) as ChainHeaderTracker & - sinon.SinonStubbedInstance; + // @ts-expect-error - Mocked class don't need parameters + const chainHeadTracker = new ChainHeaderTracker() as Mocked; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized // Sample validator @@ -36,14 +36,16 @@ describe("AttestationDutiesService", function () { validator: ssz.phase0.Validator.defaultValue(), }; - before(async () => { + beforeAll(async () => { const secretKeys = [bls.SecretKey.fromBytes(toBufferBE(BigInt(98), 32))]; pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes()); validatorStore = await initValidatorStore(secretKeys, api, chainConfig); }); let controller: AbortController; // To stop clock - beforeEach(() => (controller = new AbortController())); + beforeEach(() => { + controller = new AbortController(); + }); afterEach(() => controller.abort()); it("Should fetch indexes and duties", async function () { @@ -53,7 +55,7 @@ describe("AttestationDutiesService", function () { index, validator: {...defaultValidator.validator, pubkey: pubkeys[0]}, }; - api.beacon.getStateValidators.resolves({ + api.beacon.getStateValidators.mockResolvedValue({ response: {data: [validatorResponse], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, @@ -71,14 +73,18 @@ describe("AttestationDutiesService", function () { validatorIndex: index, pubkey: pubkeys[0], }; - api.validator.getAttesterDuties.resolves({ + api.validator.getAttesterDuties.mockResolvedValue({ response: {dependentRoot: ZERO_HASH_HEX, data: [duty], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); // Accept all subscriptions - api.validator.prepareBeaconCommitteeSubnet.resolves(); + api.validator.prepareBeaconCommitteeSubnet.mockResolvedValue({ + response: undefined, + ok: true, + status: HttpStatusCode.OK, + }); // Clock will call runAttesterDutiesTasks() immediately const clock = new ClockMock(); @@ -88,38 +94,24 @@ describe("AttestationDutiesService", function () { await clock.tickEpochFns(0, controller.signal); // Validator index should be persisted - expect(validatorStore.getAllLocalIndices()).to.deep.equal([index], "Wrong local indices"); - expect(validatorStore.getPubkeyOfIndex(index)).equals(toHexString(pubkeys[0]), "Wrong pubkey"); + expect(validatorStore.getAllLocalIndices()).toEqual([index]); + expect(validatorStore.getPubkeyOfIndex(index)).toBe(toHexString(pubkeys[0])); // Duties for this and next epoch should be persisted - expect( - Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(epoch)?.dutiesByIndex || new Map()) - ).to.deep.equal( - { - // Since the ZERO_HASH won't pass the isAggregator test, selectionProof is null - [index]: {duty, selectionProof: null}, - }, - "Wrong dutiesService.attesters Map at current epoch" - ); + expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(epoch)?.dutiesByIndex || new Map())).toEqual({ + // Since the ZERO_HASH won't pass the isAggregator test, selectionProof is null + [index]: {duty, selectionProof: null}, + }); expect( Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(epoch + 1)?.dutiesByIndex || new Map()) - ).to.deep.equal( - { - // Since the ZERO_HASH won't pass the isAggregator test, selectionProof is null - [index]: {duty, selectionProof: null}, - }, - "Wrong dutiesService.attesters Map at next epoch" - ); - - expect(dutiesService.getDutiesAtSlot(slot)).to.deep.equal( - [{duty, selectionProof: null}], - "Wrong getAttestersAtSlot()" - ); - - expect(api.validator.prepareBeaconCommitteeSubnet.callCount).to.equal( - 1, - "prepareBeaconCommitteeSubnet() must be called once after getting the duties" - ); + ).toEqual({ + // Since the ZERO_HASH won't pass the isAggregator test, selectionProof is null + [index]: {duty, selectionProof: null}, + }); + + expect(dutiesService.getDutiesAtSlot(slot)).toEqual([{duty, selectionProof: null}]); + + expect(api.validator.prepareBeaconCommitteeSubnet).toHaveBeenCalledOnce(); }); it("Should remove signer from attestation duties", async function () { @@ -129,7 +121,7 @@ describe("AttestationDutiesService", function () { index, validator: {...defaultValidator.validator, pubkey: pubkeys[0]}, }; - api.beacon.getStateValidators.resolves({ + api.beacon.getStateValidators.mockResolvedValue({ response: {data: [validatorResponse], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, @@ -146,14 +138,18 @@ describe("AttestationDutiesService", function () { validatorIndex: index, pubkey: pubkeys[0], }; - api.validator.getAttesterDuties.resolves({ + api.validator.getAttesterDuties.mockResolvedValue({ response: {data: [duty], dependentRoot: ZERO_HASH_HEX, executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); // Accept all subscriptions - api.validator.prepareBeaconCommitteeSubnet.resolves(); + api.validator.prepareBeaconCommitteeSubnet.mockResolvedValue({ + ok: true, + status: HttpStatusCode.OK, + response: undefined, + }); // Clock will call runAttesterDutiesTasks() immediately const clock = new ClockMock(); @@ -163,23 +159,14 @@ describe("AttestationDutiesService", function () { await clock.tickEpochFns(0, controller.signal); // first confirm duties for this and next epoch should be persisted - expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(0)?.dutiesByIndex || new Map())).to.deep.equal( - { - 4: {duty: duty, selectionProof: null}, - }, - "Wrong dutiesService.attesters Map at current epoch" - ); - expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(1)?.dutiesByIndex || new Map())).to.deep.equal( - { - 4: {duty: duty, selectionProof: null}, - }, - "Wrong dutiesService.attesters Map at current epoch" - ); + expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(0)?.dutiesByIndex || new Map())).toEqual({ + 4: {duty: duty, selectionProof: null}, + }); + expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"].get(1)?.dutiesByIndex || new Map())).toEqual({ + 4: {duty: duty, selectionProof: null}, + }); // then remove dutiesService.removeDutiesForKey(toHexString(pubkeys[0])); - expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"])).to.deep.equal( - {}, - "Wrong dutiesService.attesters Map at current epoch after removal" - ); + expect(Object.fromEntries(dutiesService["dutiesByIndexByEpoch"])).toEqual({}); }); }); diff --git a/packages/validator/test/unit/services/block.test.ts b/packages/validator/test/unit/services/block.test.ts index 3677cdac3a7a..bcfc57eb8674 100644 --- a/packages/validator/test/unit/services/block.test.ts +++ b/packages/validator/test/unit/services/block.test.ts @@ -1,5 +1,4 @@ -import {expect} from "chai"; -import sinon from "sinon"; +import {describe, it, expect, beforeAll, beforeEach, afterEach, vi} from "vitest"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {createChainForkConfig} from "@lodestar/config"; @@ -15,30 +14,32 @@ import {loggerVc} from "../../utils/logger.js"; import {ClockMock} from "../../utils/clock.js"; import {ZERO_HASH_HEX} from "../../utils/types.js"; -describe("BlockDutiesService", function () { - const sandbox = sinon.createSandbox(); +vi.mock("../../../src/services/validatorStore.js"); - const api = getApiClientStub(sandbox); - const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & - sinon.SinonStubbedInstance; +describe("BlockDutiesService", function () { + const api = getApiClientStub(); + // @ts-expect-error - Mocked class don't need parameters + const validatorStore = vi.mocked(new ValidatorStore()); let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized const config = createChainForkConfig(mainnetConfig); - before(() => { + beforeAll(() => { const secretKeys = Array.from({length: 2}, (_, i) => bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1))); pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes()); - validatorStore.votingPubkeys.returns(pubkeys.map(toHexString)); + validatorStore.votingPubkeys.mockReturnValue(pubkeys.map(toHexString)); }); let controller: AbortController; // To stop clock - beforeEach(() => (controller = new AbortController())); + beforeEach(() => { + controller = new AbortController(); + }); afterEach(() => controller.abort()); it("Should produce, sign, and publish a block", async function () { // Reply with some duties const slot = 0; // genesisTime is right now, so test with slot = currentSlot - api.validator.getProposerDuties.resolves({ + api.validator.getProposerDuties.mockResolvedValue({ response: { dependentRoot: ZERO_HASH_HEX, executionOptimistic: false, @@ -57,17 +58,20 @@ describe("BlockDutiesService", function () { }); const signedBlock = ssz.phase0.SignedBeaconBlock.defaultValue(); - validatorStore.signRandao.resolves(signedBlock.message.body.randaoReveal); - validatorStore.signBlock.callsFake(async (_, block) => ({message: block, signature: signedBlock.signature})); - validatorStore.getBuilderSelectionParams.returns({ + validatorStore.signRandao.mockResolvedValue(signedBlock.message.body.randaoReveal); + validatorStore.signBlock.mockImplementation(async (_, block) => ({ + message: block, + signature: signedBlock.signature, + })); + validatorStore.getBuilderSelectionParams.mockReturnValue({ selection: routes.validator.BuilderSelection.MaxProfit, boostFactor: BigInt(100), }); - validatorStore.getGraffiti.returns("aaaa"); - validatorStore.getFeeRecipient.returns("0x00"); - validatorStore.strictFeeRecipientCheck.returns(false); + validatorStore.getGraffiti.mockReturnValue("aaaa"); + validatorStore.getFeeRecipient.mockReturnValue("0x00"); + validatorStore.strictFeeRecipientCheck.mockReturnValue(false); - api.validator.produceBlockV3.resolves({ + api.validator.produceBlockV3.mockResolvedValue({ response: { data: signedBlock.message, version: ForkName.bellatrix, @@ -79,7 +83,7 @@ describe("BlockDutiesService", function () { ok: true, status: HttpStatusCode.OK, }); - api.beacon.publishBlockV2.resolves(); + api.beacon.publishBlockV2.mockResolvedValue({ok: true, status: HttpStatusCode.OK, response: undefined}); // Trigger block production for slot 1 const notifyBlockProductionFn = blockService["dutiesService"]["notifyBlockProductionFn"]; @@ -89,35 +93,32 @@ describe("BlockDutiesService", function () { await sleep(20, controller.signal); // Must have submitted the block received on signBlock() - expect(api.beacon.publishBlockV2.callCount).to.equal(1, "publishBlock() must be called once"); - expect(api.beacon.publishBlockV2.getCall(0).args).to.deep.equal( - [signedBlock, {broadcastValidation: routes.beacon.BroadcastValidation.consensus}], - "wrong publishBlock() args" - ); + expect(api.beacon.publishBlockV2).toHaveBeenCalledOnce(); + expect(api.beacon.publishBlockV2.mock.calls[0]).toEqual([ + signedBlock, + {broadcastValidation: routes.beacon.BroadcastValidation.consensus}, + ]); // ProduceBlockV3 is called with all correct arguments - expect(api.validator.produceBlockV3.getCall(0).args).to.deep.equal( - [ - 1, - signedBlock.message.body.randaoReveal, - "aaaa", - false, - { - feeRecipient: "0x00", - builderSelection: routes.validator.BuilderSelection.MaxProfit, - strictFeeRecipientCheck: false, - blindedLocal: false, - builderBoostFactor: BigInt(100), - }, - ], - "wrong produceBlockV3() args" - ); + expect(api.validator.produceBlockV3.mock.calls[0]).toEqual([ + 1, + signedBlock.message.body.randaoReveal, + "aaaa", + false, + { + feeRecipient: "0x00", + builderSelection: routes.validator.BuilderSelection.MaxProfit, + strictFeeRecipientCheck: false, + blindedLocal: false, + builderBoostFactor: BigInt(100), + }, + ]); }); it("Should produce, sign, and publish a blinded block", async function () { // Reply with some duties const slot = 0; // genesisTime is right now, so test with slot = currentSlot - api.validator.getProposerDuties.resolves({ + api.validator.getProposerDuties.mockResolvedValue({ response: { dependentRoot: ZERO_HASH_HEX, executionOptimistic: false, @@ -136,9 +137,12 @@ describe("BlockDutiesService", function () { }); const signedBlock = ssz.bellatrix.SignedBlindedBeaconBlock.defaultValue(); - validatorStore.signRandao.resolves(signedBlock.message.body.randaoReveal); - validatorStore.signBlock.callsFake(async (_, block) => ({message: block, signature: signedBlock.signature})); - api.validator.produceBlockV3.resolves({ + validatorStore.signRandao.mockResolvedValue(signedBlock.message.body.randaoReveal); + validatorStore.signBlock.mockImplementation(async (_, block) => ({ + message: block, + signature: signedBlock.signature, + })); + api.validator.produceBlockV3.mockResolvedValue({ response: { data: signedBlock.message, version: ForkName.bellatrix, @@ -150,7 +154,7 @@ describe("BlockDutiesService", function () { ok: true, status: HttpStatusCode.OK, }); - api.beacon.publishBlindedBlockV2.resolves(); + api.beacon.publishBlindedBlockV2.mockResolvedValue({ok: true, status: HttpStatusCode.OK, response: undefined}); // Trigger block production for slot 1 const notifyBlockProductionFn = blockService["dutiesService"]["notifyBlockProductionFn"]; @@ -160,10 +164,10 @@ describe("BlockDutiesService", function () { await sleep(20, controller.signal); // Must have submitted the block received on signBlock() - expect(api.beacon.publishBlindedBlockV2.callCount).to.equal(1, "publishBlindedBlockV2() must be called once"); - expect(api.beacon.publishBlindedBlockV2.getCall(0).args).to.deep.equal( - [signedBlock, {broadcastValidation: routes.beacon.BroadcastValidation.consensus}], - "wrong publishBlock() args" - ); + expect(api.beacon.publishBlindedBlockV2).toHaveBeenCalledOnce(); + expect(api.beacon.publishBlindedBlockV2.mock.calls[0]).toEqual([ + signedBlock, + {broadcastValidation: routes.beacon.BroadcastValidation.consensus}, + ]); }); }); diff --git a/packages/validator/test/unit/services/blockDuties.test.ts b/packages/validator/test/unit/services/blockDuties.test.ts index 93540b0c2794..45dd99a80e77 100644 --- a/packages/validator/test/unit/services/blockDuties.test.ts +++ b/packages/validator/test/unit/services/blockDuties.test.ts @@ -1,5 +1,4 @@ -import {expect} from "chai"; -import sinon from "sinon"; +import {describe, it, expect, beforeAll, beforeEach, afterEach, vi} from "vitest"; import {toBufferBE} from "bigint-buffer"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; @@ -18,20 +17,20 @@ import {ZERO_HASH_HEX} from "../../utils/types.js"; type ProposerDutiesRes = {dependentRoot: RootHex; data: routes.validator.ProposerDuty[]}; describe("BlockDutiesService", function () { - const sandbox = sinon.createSandbox(); - - const api = getApiClientStub(sandbox); + const api = getApiClientStub(); let validatorStore: ValidatorStore; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized - before(async () => { + beforeAll(async () => { const secretKeys = Array.from({length: 3}, (_, i) => bls.SecretKey.fromBytes(toBufferBE(BigInt(i + 1), 32))); pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes()); validatorStore = await initValidatorStore(secretKeys, api); }); let controller: AbortController; // To stop clock - beforeEach(() => (controller = new AbortController())); + beforeEach(() => { + controller = new AbortController(); + }); afterEach(() => controller.abort()); it("Should fetch and persist block duties", async function () { @@ -41,13 +40,13 @@ describe("BlockDutiesService", function () { dependentRoot: ZERO_HASH_HEX, data: [{slot: slot, validatorIndex: 0, pubkey: pubkeys[0]}], }; - api.validator.getProposerDuties.resolves({ + api.validator.getProposerDuties.mockResolvedValue({ response: {...duties, executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); - const notifyBlockProductionFn = sinon.stub(); // Returns void + const notifyBlockProductionFn = vi.fn(); // Returns void const clock = new ClockMock(); const dutiesService = new BlockDutiesService( @@ -64,17 +63,11 @@ describe("BlockDutiesService", function () { await clock.tickSlotFns(0, controller.signal); // Duties for this epoch should be persisted - expect(Object.fromEntries(dutiesService["proposers"])).to.deep.equal( - {0: duties}, - "Wrong dutiesService.proposers Map" - ); + expect(Object.fromEntries(dutiesService["proposers"])).toEqual({0: duties}); - expect(dutiesService.getblockProposersAtSlot(slot)).to.deep.equal([pubkeys[0]], "Wrong getblockProposersAtSlot()"); + expect(dutiesService.getblockProposersAtSlot(slot)).toEqual([pubkeys[0]]); - expect(notifyBlockProductionFn.callCount).to.equal( - 1, - "notifyBlockProductionFn() must be called once after getting the duties" - ); + expect(notifyBlockProductionFn).toHaveBeenCalledOnce(); }); it("Should call notifyBlockProductionFn again on duties re-org", async () => { @@ -89,7 +82,7 @@ describe("BlockDutiesService", function () { data: [{slot: 1, validatorIndex: 1, pubkey: pubkeys[1]}], }; - const notifyBlockProductionFn = sinon.stub(); // Returns void + const notifyBlockProductionFn = vi.fn(); // Returns void // Clock will call runAttesterDutiesTasks() immediately const clock = new ClockMock(); @@ -104,7 +97,7 @@ describe("BlockDutiesService", function () { ); // Trigger clock onSlot for slot 0 - api.validator.getProposerDuties.resolves({ + api.validator.getProposerDuties.mockResolvedValue({ response: {...dutiesBeforeReorg, executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, @@ -112,7 +105,7 @@ describe("BlockDutiesService", function () { await clock.tickSlotFns(0, controller.signal); // Trigger clock onSlot for slot 1 - Return different duties for slot 1 - api.validator.getProposerDuties.resolves({ + api.validator.getProposerDuties.mockResolvedValue({ response: {...dutiesAfterReorg, executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, @@ -120,24 +113,12 @@ describe("BlockDutiesService", function () { await clock.tickSlotFns(1, controller.signal); // Should persist the dutiesAfterReorg - expect(Object.fromEntries(dutiesService["proposers"])).to.deep.equal( - {0: dutiesAfterReorg}, - "dutiesService.proposers must persist dutiesAfterReorg" - ); + expect(Object.fromEntries(dutiesService["proposers"])).toEqual({0: dutiesAfterReorg}); - expect(notifyBlockProductionFn.callCount).to.equal( - 2, - "Must call notifyBlockProductionFn twice, before and after the re-org" - ); + expect(notifyBlockProductionFn).toBeCalledTimes(2); - expect(notifyBlockProductionFn.getCall(0).args).to.deep.equal( - [1, [pubkeys[0]]], - "First call to notifyBlockProductionFn() before the re-org with pubkey[0]" - ); - expect(notifyBlockProductionFn.getCall(1).args).to.deep.equal( - [1, [pubkeys[1]]], - "Second call to notifyBlockProductionFn() after the re-org with pubkey[1]" - ); + expect(notifyBlockProductionFn.mock.calls[0]).toEqual([1, [pubkeys[0]]]); + expect(notifyBlockProductionFn.mock.calls[1]).toEqual([1, [pubkeys[1]]]); }); it("Should remove signer from duty", async function () { @@ -159,13 +140,13 @@ describe("BlockDutiesService", function () { {slot: 33, validatorIndex: 2, pubkey: pubkeys[2]}, ], }; - api.validator.getProposerDuties.resolves({ + api.validator.getProposerDuties.mockResolvedValue({ response: {...duties, executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); - const notifyBlockProductionFn = sinon.stub(); // Returns void + const notifyBlockProductionFn = vi.fn(); // Returns void const clock = new ClockMock(); const dutiesService = new BlockDutiesService( @@ -183,18 +164,12 @@ describe("BlockDutiesService", function () { await clock.tickSlotFns(32, controller.signal); // first confirm the duties for the epochs was persisted - expect(Object.fromEntries(dutiesService["proposers"])).to.deep.equal( - {0: duties, 1: duties}, - "Wrong dutiesService.proposers Map" - ); + expect(Object.fromEntries(dutiesService["proposers"])).toEqual({0: duties, 1: duties}); // then remove a signers public key dutiesService.removeDutiesForKey(toHexString(pubkeys[0])); // confirm that the duties no longer contain the signers public key - expect(Object.fromEntries(dutiesService["proposers"])).to.deep.equal( - {0: dutiesRemoved, 1: dutiesRemoved}, - "Wrong dutiesService.proposers Map" - ); + expect(Object.fromEntries(dutiesService["proposers"])).toEqual({0: dutiesRemoved, 1: dutiesRemoved}); }); }); diff --git a/packages/validator/test/unit/services/doppelganger.test.ts b/packages/validator/test/unit/services/doppelganger.test.ts index f3507be690f6..b3943f619494 100644 --- a/packages/validator/test/unit/services/doppelganger.test.ts +++ b/packages/validator/test/unit/services/doppelganger.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {Epoch, Slot, ValidatorIndex} from "@lodestar/types"; import {sleep} from "@lodestar/utils"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; @@ -130,7 +130,7 @@ describe("doppelganger service", () => { // Assert doppelganger status const status = doppelganger.getStatus(pubkeyHex); - expect(status).equal(expectedStatus, `Wrong status at step ${step}`); + expect(status).toBe(expectedStatus); } }); } @@ -172,7 +172,7 @@ describe("doppelganger service", () => { // Assert doppelganger status right away const status = doppelganger.getStatus(pubkeyHex); - expect(status).equal(DoppelgangerStatus.VerifiedSafe); + expect(status).toBe(DoppelgangerStatus.VerifiedSafe); }); }); diff --git a/packages/validator/test/unit/services/indicesService.test.ts b/packages/validator/test/unit/services/indicesService.test.ts index 45ee665eb941..b94ec6fa398a 100644 --- a/packages/validator/test/unit/services/indicesService.test.ts +++ b/packages/validator/test/unit/services/indicesService.test.ts @@ -1,6 +1,5 @@ +import {describe, it, expect, beforeAll} from "vitest"; import {toBufferBE} from "bigint-buffer"; -import {expect} from "chai"; -import sinon from "sinon"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {getApiClientStub} from "../../utils/apiStub.js"; @@ -8,13 +7,12 @@ import {testLogger} from "../../utils/logger.js"; import {IndicesService} from "../../../src/services/indices.js"; describe("IndicesService", function () { - const sandbox = sinon.createSandbox(); const logger = testLogger(); - const api = getApiClientStub(sandbox); + const api = getApiClientStub(); let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized - before(() => { + beforeAll(() => { const secretKeys = [ bls.SecretKey.fromBytes(toBufferBE(BigInt(98), 32)), bls.SecretKey.fromBytes(toBufferBE(BigInt(99), 32)), @@ -39,18 +37,12 @@ describe("IndicesService", function () { // remove pubkey2 indicesService.removeForKey(pubkey2); - expect(Object.fromEntries(indicesService.index2pubkey)).to.deep.equal( - { - "0": `${pubkey1}`, - }, - "Wrong indicesService.index2pubkey Map" - ); - - expect(Object.fromEntries(indicesService.pubkey2index)).to.deep.equal( - { - [`${pubkey1}`]: 0, - }, - "Wrong indicesService.pubkey2index Map" - ); + expect(Object.fromEntries(indicesService.index2pubkey)).toEqual({ + "0": `${pubkey1}`, + }); + + expect(Object.fromEntries(indicesService.pubkey2index)).toEqual({ + [`${pubkey1}`]: 0, + }); }); }); diff --git a/packages/validator/test/unit/services/syncCommitteDuties.test.ts b/packages/validator/test/unit/services/syncCommitteDuties.test.ts index af5734ffdcca..bca0dd67cdc9 100644 --- a/packages/validator/test/unit/services/syncCommitteDuties.test.ts +++ b/packages/validator/test/unit/services/syncCommitteDuties.test.ts @@ -1,6 +1,6 @@ +import {describe, it, expect, beforeAll, beforeEach, afterEach} from "vitest"; +import {when} from "vitest-when"; import {toBufferBE} from "bigint-buffer"; -import {expect} from "chai"; -import sinon from "sinon"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {createChainForkConfig} from "@lodestar/config"; @@ -22,9 +22,7 @@ import {syncCommitteeIndicesToSubnets} from "../../../src/services/utils.js"; /* eslint-disable @typescript-eslint/naming-convention */ describe("SyncCommitteeDutiesService", function () { - const sandbox = sinon.createSandbox(); - - const api = getApiClientStub(sandbox); + const api = getApiClientStub(); let validatorStore: ValidatorStore; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized @@ -43,7 +41,7 @@ describe("SyncCommitteeDutiesService", function () { validator: ssz.phase0.Validator.defaultValue(), }; - before(async () => { + beforeAll(async () => { const secretKeys = [ bls.SecretKey.fromBytes(toBufferBE(BigInt(98), 32)), bls.SecretKey.fromBytes(toBufferBE(BigInt(99), 32)), @@ -61,7 +59,7 @@ describe("SyncCommitteeDutiesService", function () { index: indices[i], validator: {...defaultValidator.validator, pubkey: pubkeys[i]}, })); - api.beacon.getStateValidators.resolves({ + api.beacon.getStateValidators.mockResolvedValue({ response: {data: validatorResponses, executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, @@ -77,14 +75,18 @@ describe("SyncCommitteeDutiesService", function () { validatorIndex: indices[0], validatorSyncCommitteeIndices: [7], }; - api.validator.getSyncCommitteeDuties.resolves({ + api.validator.getSyncCommitteeDuties.mockResolvedValue({ response: {data: [duty], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); // Accept all subscriptions - api.validator.prepareSyncCommitteeSubnets.resolves(); + api.validator.prepareSyncCommitteeSubnets.mockResolvedValue({ + ok: true, + status: HttpStatusCode.OK, + response: undefined, + }); // Clock will call runAttesterDutiesTasks() immediately const clock = new ClockMock(); @@ -95,9 +97,9 @@ describe("SyncCommitteeDutiesService", function () { // Validator index should be persisted // Validator index should be persisted - expect(validatorStore.getAllLocalIndices()).to.deep.equal(indices, "Wrong local indices"); + expect(validatorStore.getAllLocalIndices()).toEqual(indices); for (let i = 0; i < indices.length; i++) { - expect(validatorStore.getPubkeyOfIndex(indices[i])).equals(toHexString(pubkeys[i]), `Wrong pubkey[${i}]`); + expect(validatorStore.getPubkeyOfIndex(indices[i])).toBe(toHexString(pubkeys[i])); } // Duties for this and next epoch should be persisted @@ -108,25 +110,16 @@ describe("SyncCommitteeDutiesService", function () { ]) ); - expect(dutiesByIndexByPeriodObj).to.deep.equal( - { - 0: {[indices[0]]: {duty: toSyncDutySubnet(duty)}}, - 1: {[indices[0]]: {duty: toSyncDutySubnet(duty)}}, - } as typeof dutiesByIndexByPeriodObj, - "Wrong dutiesService.dutiesByIndexByPeriod Map" - ); + expect(dutiesByIndexByPeriodObj).toEqual({ + 0: {[indices[0]]: {duty: toSyncDutySubnet(duty)}}, + 1: {[indices[0]]: {duty: toSyncDutySubnet(duty)}}, + } as typeof dutiesByIndexByPeriodObj); - expect(await dutiesService.getDutiesAtSlot(slot)).to.deep.equal( - [ - {duty: toSyncDutySubnet(duty), selectionProofs: [{selectionProof: null, subcommitteeIndex: 0}]}, - ] as SyncDutyAndProofs[], - "Wrong getAttestersAtSlot()" - ); + expect(await dutiesService.getDutiesAtSlot(slot)).toEqual([ + {duty: toSyncDutySubnet(duty), selectionProofs: [{selectionProof: null, subcommitteeIndex: 0}]}, + ] as SyncDutyAndProofs[]); - expect(api.validator.prepareSyncCommitteeSubnets.callCount).to.equal( - 1, - "prepareSyncCommitteeSubnets() must be called once after getting the duties" - ); + expect(api.validator.prepareSyncCommitteeSubnets).toHaveBeenCalledOnce(); }); /** @@ -139,24 +132,24 @@ describe("SyncCommitteeDutiesService", function () { validatorIndex: indices[0], validatorSyncCommitteeIndices: [7], }; - api.validator.getSyncCommitteeDuties - .withArgs(0, sinon.match.any) - .resolves({response: {data: [duty], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); + when(api.validator.getSyncCommitteeDuties) + .calledWith(0, expect.any(Array)) + .thenResolve({response: {data: [duty], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); // sync period 1 should all return empty - api.validator.getSyncCommitteeDuties - .withArgs(256, sinon.match.any) - .resolves({response: {data: [], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); - api.validator.getSyncCommitteeDuties - .withArgs(257, sinon.match.any) - .resolves({response: {data: [], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); + when(api.validator.getSyncCommitteeDuties) + .calledWith(256, expect.any(Array)) + .thenResolve({response: {data: [], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); + when(api.validator.getSyncCommitteeDuties) + .calledWith(257, expect.any(Array)) + .thenResolve({response: {data: [], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); const duty2: routes.validator.SyncDuty = { pubkey: pubkeys[1], validatorIndex: indices[1], validatorSyncCommitteeIndices: [5], }; - api.validator.getSyncCommitteeDuties - .withArgs(1, sinon.match.any) - .resolves({response: {data: [duty2], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); + when(api.validator.getSyncCommitteeDuties) + .calledWith(1, expect.any(Array)) + .thenResolve({response: {data: [duty2], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); // Clock will call runAttesterDutiesTasks() immediately const clock = new ClockMock(); @@ -172,13 +165,10 @@ describe("SyncCommitteeDutiesService", function () { Object.fromEntries(dutiesByIndex), ]) ); - expect(dutiesByIndexByPeriodObj).to.deep.equal( - { - 0: {[indices[0]]: {duty: toSyncDutySubnet(duty)}}, - 1: {}, - } as typeof dutiesByIndexByPeriodObj, - "Wrong dutiesService.dutiesByIndexByPeriod Map" - ); + expect(dutiesByIndexByPeriodObj).toEqual({ + 0: {[indices[0]]: {duty: toSyncDutySubnet(duty)}}, + 1: {}, + } as typeof dutiesByIndexByPeriodObj); await clock.tickEpochFns(1, controller.signal); @@ -188,13 +178,10 @@ describe("SyncCommitteeDutiesService", function () { Object.fromEntries(dutiesByIndex), ]) ); - expect(dutiesByIndexByPeriodObj).to.deep.equal( - { - 0: {[indices[1]]: {duty: toSyncDutySubnet(duty2)}}, - 1: {}, - } as typeof dutiesByIndexByPeriodObj, - "Wrong dutiesService.dutiesByIndexByPeriod Map" - ); + expect(dutiesByIndexByPeriodObj).toEqual({ + 0: {[indices[1]]: {duty: toSyncDutySubnet(duty2)}}, + 1: {}, + } as typeof dutiesByIndexByPeriodObj); }); it("Should remove signer from sync committee duties", async function () { @@ -209,12 +196,16 @@ describe("SyncCommitteeDutiesService", function () { validatorIndex: indices[1], validatorSyncCommitteeIndices: [7], }; - api.validator.getSyncCommitteeDuties - .withArgs(sinon.match.any, sinon.match.any) - .resolves({response: {data: [duty1, duty2], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); + when(api.validator.getSyncCommitteeDuties) + .calledWith(expect.any(Number), expect.any(Array)) + .thenResolve({response: {data: [duty1, duty2], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK}); // Accept all subscriptions - api.validator.prepareSyncCommitteeSubnets.resolves(); + api.validator.prepareSyncCommitteeSubnets.mockResolvedValue({ + ok: true, + status: HttpStatusCode.OK, + response: undefined, + }); // Clock will call runAttesterDutiesTasks() immediately const clock = new ClockMock(); @@ -231,19 +222,16 @@ describe("SyncCommitteeDutiesService", function () { ]) ); - expect(dutiesByIndexByPeriodObj).to.deep.equal( - { - 0: { - [indices[0]]: {duty: toSyncDutySubnet(duty1)}, - [indices[1]]: {duty: toSyncDutySubnet(duty2)}, - }, - 1: { - [indices[0]]: {duty: toSyncDutySubnet(duty1)}, - [indices[1]]: {duty: toSyncDutySubnet(duty2)}, - }, - } as typeof dutiesByIndexByPeriodObj, - "Wrong dutiesService.dutiesByIndexByPeriod Map" - ); + expect(dutiesByIndexByPeriodObj).toEqual({ + 0: { + [indices[0]]: {duty: toSyncDutySubnet(duty1)}, + [indices[1]]: {duty: toSyncDutySubnet(duty2)}, + }, + 1: { + [indices[0]]: {duty: toSyncDutySubnet(duty1)}, + [indices[1]]: {duty: toSyncDutySubnet(duty2)}, + }, + } as typeof dutiesByIndexByPeriodObj); // then remove signer with pubkeys[0] dutiesService.removeDutiesForKey(toHexString(pubkeys[0])); @@ -254,13 +242,10 @@ describe("SyncCommitteeDutiesService", function () { Object.fromEntries(dutiesByIndex), ]) ); - expect(dutiesByIndexByPeriodObjAfterRemoval).to.deep.equal( - { - 0: {[indices[1]]: {duty: toSyncDutySubnet(duty2)}}, - 1: {[indices[1]]: {duty: toSyncDutySubnet(duty2)}}, - } as typeof dutiesByIndexByPeriodObjAfterRemoval, - "Wrong dutiesService.dutiesByIndexByPeriod Map" - ); + expect(dutiesByIndexByPeriodObjAfterRemoval).toEqual({ + 0: {[indices[1]]: {duty: toSyncDutySubnet(duty2)}}, + 1: {[indices[1]]: {duty: toSyncDutySubnet(duty2)}}, + } as typeof dutiesByIndexByPeriodObjAfterRemoval); }); }); diff --git a/packages/validator/test/unit/services/syncCommittee.test.ts b/packages/validator/test/unit/services/syncCommittee.test.ts index 9316f11eb483..20697f651ca9 100644 --- a/packages/validator/test/unit/services/syncCommittee.test.ts +++ b/packages/validator/test/unit/services/syncCommittee.test.ts @@ -1,5 +1,4 @@ -import {expect} from "chai"; -import sinon from "sinon"; +import {describe, it, expect, beforeAll, beforeEach, afterEach, vi} from "vitest"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {createChainForkConfig} from "@lodestar/config"; @@ -16,18 +15,19 @@ import {ChainHeaderTracker} from "../../../src/services/chainHeaderTracker.js"; import {ZERO_HASH} from "../../utils/types.js"; import {ValidatorEventEmitter} from "../../../src/services/emitter.js"; +vi.mock("../../../src/services/validatorStore.js"); +vi.mock("../../../src/services/emitter.js"); +vi.mock("../../../src/services/chainHeaderTracker.js"); + /* eslint-disable @typescript-eslint/naming-convention */ describe("SyncCommitteeService", function () { - const sandbox = sinon.createSandbox(); - - const api = getApiClientStub(sandbox); - const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & - sinon.SinonStubbedInstance; - const emitter = sinon.createStubInstance(ValidatorEventEmitter) as ValidatorEventEmitter & - sinon.SinonStubbedInstance; - const chainHeaderTracker = sinon.createStubInstance(ChainHeaderTracker) as ChainHeaderTracker & - sinon.SinonStubbedInstance; + const api = getApiClientStub(); + // @ts-expect-error - Mocked class don't need parameters + const validatorStore = vi.mocked(new ValidatorStore()); + const emitter = vi.mocked(new ValidatorEventEmitter()); + // @ts-expect-error - Mocked class don't need parameters + const chainHeaderTracker = vi.mocked(new ChainHeaderTracker()); let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized const config = createChainForkConfig({ @@ -36,20 +36,22 @@ describe("SyncCommitteeService", function () { ALTAIR_FORK_EPOCH: 0, // Activate Altair immediately }); - before(() => { + beforeAll(() => { const secretKeys = Array.from({length: 1}, (_, i) => bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1))); pubkeys = secretKeys.map((sk) => sk.toPublicKey().toBytes()); - validatorStore.votingPubkeys.returns(pubkeys.map(toHexString)); - validatorStore.hasVotingPubkey.returns(true); - validatorStore.hasSomeValidators.returns(true); - validatorStore.signAttestationSelectionProof.resolves(ZERO_HASH); + validatorStore.votingPubkeys.mockReturnValue(pubkeys.map(toHexString)); + validatorStore.hasVotingPubkey.mockReturnValue(true); + validatorStore.hasSomeValidators.mockReturnValue(true); + validatorStore.signAttestationSelectionProof.mockResolvedValue(ZERO_HASH); }); let controller: AbortController; // To stop clock - beforeEach(() => (controller = new AbortController())); + beforeEach(() => { + controller = new AbortController(); + }); afterEach(() => { controller.abort(); - sandbox.resetHistory(); + vi.resetAllMocks(); }); const testContexts: [string, SyncCommitteeServiceOpts][] = [ @@ -58,7 +60,7 @@ describe("SyncCommitteeService", function () { ]; for (const [title, opts] of testContexts) { - context(title, () => { + describe(title, () => { it("Should produce, sign, and publish a sync committee + contribution", async () => { const clock = new ClockMock(); const syncCommitteeService = new SyncCommitteeService( @@ -95,34 +97,34 @@ describe("SyncCommitteeService", function () { ]; // Return empty replies to duties service - api.beacon.getStateValidators.resolves({ + api.beacon.getStateValidators.mockResolvedValue({ response: {data: [], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); - api.validator.getSyncCommitteeDuties.resolves({ + api.validator.getSyncCommitteeDuties.mockResolvedValue({ response: {data: [], executionOptimistic: false}, ok: true, status: HttpStatusCode.OK, }); // Mock duties service to return some duties directly - syncCommitteeService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties); + syncCommitteeService["dutiesService"].getDutiesAtSlot = vi.fn().mockReturnValue(duties); // Mock beacon's sync committee and contribution routes - chainHeaderTracker.getCurrentChainHead.returns(beaconBlockRoot); - api.beacon.submitPoolSyncCommitteeSignatures.resolves({ + chainHeaderTracker.getCurrentChainHead.mockReturnValue(beaconBlockRoot); + api.beacon.submitPoolSyncCommitteeSignatures.mockResolvedValue({ response: undefined, ok: true, status: HttpStatusCode.OK, }); - api.validator.produceSyncCommitteeContribution.resolves({ + api.validator.produceSyncCommitteeContribution.mockResolvedValue({ response: {data: contribution}, ok: true, status: HttpStatusCode.OK, }); - api.validator.publishContributionAndProofs.resolves({ + api.validator.publishContributionAndProofs.mockResolvedValue({ response: undefined, ok: true, status: HttpStatusCode.OK, @@ -131,7 +133,7 @@ describe("SyncCommitteeService", function () { if (opts.distributedAggregationSelection) { // Mock distributed validator middleware client selections endpoint // and return a selection proof that passes `is_sync_committee_aggregator` test - api.validator.submitSyncCommitteeSelections.resolves({ + api.validator.submitSyncCommitteeSelections.mockResolvedValue({ response: { data: [{validatorIndex: 0, slot: 0, subcommitteeIndex: 0, selectionProof: Buffer.alloc(1, 0x19)}], }, @@ -141,8 +143,8 @@ describe("SyncCommitteeService", function () { } // Mock signing service - validatorStore.signSyncCommitteeSignature.resolves(syncCommitteeSignature); - validatorStore.signContributionAndProof.resolves(contributionAndProof); + validatorStore.signSyncCommitteeSignature.mockResolvedValue(syncCommitteeSignature); + validatorStore.signContributionAndProof.mockResolvedValue(contributionAndProof); // Trigger clock onSlot for slot 0 await clock.tickSlotFns(0, controller.signal); @@ -155,35 +157,17 @@ describe("SyncCommitteeService", function () { subcommitteeIndex: 0, selectionProof: ZERO_HASH, }; - expect(api.validator.submitSyncCommitteeSelections.callCount).to.equal( - 1, - "submitSyncCommitteeSelections() must be called once" - ); - expect(api.validator.submitSyncCommitteeSelections.getCall(0).args).to.deep.equal( - [[selection]], // 1 arg, = selection[] - "wrong submitSyncCommitteeSelections() args" - ); + expect(api.validator.submitSyncCommitteeSelections).toHaveBeenCalledOnce(); + expect(api.validator.submitSyncCommitteeSelections).toHaveBeenCalledWith([selection]); } // Must submit the signature received through signSyncCommitteeSignature() - expect(api.beacon.submitPoolSyncCommitteeSignatures.callCount).to.equal( - 1, - "submitPoolSyncCommitteeSignatures() must be called once" - ); - expect(api.beacon.submitPoolSyncCommitteeSignatures.getCall(0).args).to.deep.equal( - [[syncCommitteeSignature]], // 1 arg, = syncCommitteeSignature[] - "wrong submitPoolSyncCommitteeSignatures() args" - ); + expect(api.beacon.submitPoolSyncCommitteeSignatures).toHaveBeenCalledOnce(); + expect(api.beacon.submitPoolSyncCommitteeSignatures).toHaveBeenCalledWith([syncCommitteeSignature]); // Must submit the aggregate received through produceSyncCommitteeContribution() then signContributionAndProof() - expect(api.validator.publishContributionAndProofs.callCount).to.equal( - 1, - "publishContributionAndProofs() must be called once" - ); - expect(api.validator.publishContributionAndProofs.getCall(0).args).to.deep.equal( - [[contributionAndProof]], // 1 arg, = contributionAndProof[] - "wrong publishContributionAndProofs() args" - ); + expect(api.validator.publishContributionAndProofs).toHaveBeenCalledOnce(); + expect(api.validator.publishContributionAndProofs).toHaveBeenCalledWith([contributionAndProof]); }); }); } diff --git a/packages/validator/test/unit/services/utils.test.ts b/packages/validator/test/unit/services/utils.test.ts index 43f160ab226d..f4bb9e57cd04 100644 --- a/packages/validator/test/unit/services/utils.test.ts +++ b/packages/validator/test/unit/services/utils.test.ts @@ -1,10 +1,10 @@ -import {expect} from "chai"; +import {describe, it, expect, beforeAll} from "vitest"; import {SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; import {syncCommitteeIndicesToSubnets} from "../../../src/services/utils.js"; describe("services / utils / syncCommitteeIndicesToSubnets", () => { - before("Check SYNC_COMMITTEE_SUBNET_SIZE", () => { - expect(SYNC_COMMITTEE_SUBNET_SIZE).equals(128); + beforeAll(() => { + expect(SYNC_COMMITTEE_SUBNET_SIZE).toBe(128); }); const testCases: {indexes: number[]; subnets: number[]}[] = [ @@ -19,7 +19,7 @@ describe("services / utils / syncCommitteeIndicesToSubnets", () => { for (const {indexes, subnets} of testCases) { it(indexes.join(","), () => { - expect(syncCommitteeIndicesToSubnets(indexes)).deep.equals(subnets); + expect(syncCommitteeIndicesToSubnets(indexes)).toEqual(subnets); }); } }); diff --git a/packages/validator/test/unit/slashingProtection/interchange/index.test.ts b/packages/validator/test/unit/slashingProtection/interchange/index.test.ts index 95137e7d0b71..af2694458368 100644 --- a/packages/validator/test/unit/slashingProtection/interchange/index.test.ts +++ b/packages/validator/test/unit/slashingProtection/interchange/index.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {toHexString} from "@chainsafe/ssz"; import {Root, ssz} from "@lodestar/types"; import { @@ -29,6 +29,6 @@ describe("interchange", () => { const interchangeLodestar = parseInterchange(interchange, expectedGenesisValidatorsRoot); const serializedInterchange = serializeInterchange(interchangeLodestar, {format: "complete", version: "4"}); // Stringify and parse to simulate writing and reading. It ignores undefined values - expect(JSON.parse(JSON.stringify(serializedInterchange))).to.deep.equal(interchange); + expect(JSON.parse(JSON.stringify(serializedInterchange))).toEqual(interchange); }); }); diff --git a/packages/validator/test/unit/slashingProtection/minMaxSurround/surroundTests.test.ts b/packages/validator/test/unit/slashingProtection/minMaxSurround/surroundTests.test.ts index 280404c7f913..6bdc49fb7f0c 100644 --- a/packages/validator/test/unit/slashingProtection/minMaxSurround/surroundTests.test.ts +++ b/packages/validator/test/unit/slashingProtection/minMaxSurround/surroundTests.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import { MinMaxSurround, MinMaxSurroundAttestation, @@ -204,7 +204,7 @@ describe("surroundTests", () => { } catch (e) { if (e instanceof SurroundAttestationError) { if (slashableEpoch !== undefined) { - expect(e.type.attestation2Target).to.equal(slashableEpoch, "Wrong slashableEpoch"); + expect(e.type.attestation2Target).toBe(slashableEpoch); } } else { throw Error(`Wrong error type: ${(e as Error).stack}`); diff --git a/packages/validator/test/unit/slashingProtection/minMaxSurround/updateSpans.test.ts b/packages/validator/test/unit/slashingProtection/minMaxSurround/updateSpans.test.ts index dd370325a101..62f1ea5e1b4f 100644 --- a/packages/validator/test/unit/slashingProtection/minMaxSurround/updateSpans.test.ts +++ b/packages/validator/test/unit/slashingProtection/minMaxSurround/updateSpans.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {MinMaxSurroundAttestation, MinMaxSurround} from "../../../../src/slashingProtection/minMaxSurround/index.js"; import {DistanceStoreMemory, storeToSpansPerEpoch, emptyPubkey} from "./utils.js"; @@ -39,7 +39,7 @@ describe("Update spans test", () => { await minMaxSurround.insertAttestation(emptyPubkey, att); const spansByEpochResult = await storeToSpansPerEpoch(store); - expect(spansByEpochResult).to.deep.equal(spansByEpoch); + expect(spansByEpochResult).toEqual(spansByEpoch); }); } }); diff --git a/packages/validator/test/unit/slashingProtection/utils.test.ts b/packages/validator/test/unit/slashingProtection/utils.test.ts index a77f2961f219..2e0e4859436d 100644 --- a/packages/validator/test/unit/slashingProtection/utils.test.ts +++ b/packages/validator/test/unit/slashingProtection/utils.test.ts @@ -1,20 +1,20 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {minEpoch} from "../../../src/slashingProtection/utils.js"; describe("slashingProtection / utils / minEpoch", () => { it("should return the minimum epoch from an array of epochs", () => { - expect(minEpoch([15, 10, 20, 30, 5, 1, 50])).to.equal(1); + expect(minEpoch([15, 10, 20, 30, 5, 1, 50])).toBe(1); }); it("should return the only epoch if epochs array only contains one element", () => { - expect(minEpoch([10])).to.equal(10); + expect(minEpoch([10])).toBe(10); }); it("should return null if epochs array is empty", () => { - expect(minEpoch([])).to.equal(null); + expect(minEpoch([])).toBe(null); }); it("should not throw 'RangeError: Maximum call stack size exceeded' for huge epoch arrays", () => { - expect(() => minEpoch(Array.from({length: 1e6}, (_, index) => index))).to.not.throw(RangeError); + expect(() => minEpoch(Array.from({length: 1e6}, (_, index) => index))).not.toThrow(RangeError); }); }); diff --git a/packages/validator/test/unit/utils/batch.test.ts b/packages/validator/test/unit/utils/batch.test.ts index c34486a17a27..821d27c1a43a 100644 --- a/packages/validator/test/unit/utils/batch.test.ts +++ b/packages/validator/test/unit/utils/batch.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {batchItems} from "../../../src/util/index.js"; describe("util / batch", function () { @@ -36,7 +36,7 @@ describe("util / batch", function () { for (const {items: pubkeys, expected} of testCases) { it(`Batch ${pubkeys.length} items`, () => { - expect(batchItems(pubkeys, {batchSize: 2, maxBatches: 3})).to.deep.equal(expected); + expect(batchItems(pubkeys, {batchSize: 2, maxBatches: 3})).toEqual(expected); }); } }); diff --git a/packages/validator/test/unit/utils/clock.test.ts b/packages/validator/test/unit/utils/clock.test.ts index 5194a5e4f942..8bb12e6ef8ae 100644 --- a/packages/validator/test/unit/utils/clock.test.ts +++ b/packages/validator/test/unit/utils/clock.test.ts @@ -1,5 +1,4 @@ -import sinon from "sinon"; -import {expect} from "chai"; +import {describe, it, expect, beforeEach, afterEach, vi} from "vitest"; import {config} from "@lodestar/config/default"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconConfig} from "@lodestar/config"; @@ -9,54 +8,54 @@ import {testLogger} from "../../utils/logger.js"; describe("util / Clock", function () { const logger = testLogger(); let controller: AbortController; - let fakeClock: sinon.SinonFakeTimers; beforeEach(() => { controller = new AbortController(); - fakeClock = sinon.useFakeTimers(); + vi.useFakeTimers(); }); + afterEach(() => { controller.abort(); - fakeClock.restore(); + vi.useRealTimers(); }); it("Should call on slot", async () => { const genesisTime = Math.floor(Date.now() / 1000) - config.SECONDS_PER_SLOT / 2; const clock = new Clock(config, logger, {genesisTime}); - const onSlot = sinon.stub().resolves(); + const onSlot = vi.fn().mockResolvedValue(undefined); clock.runEverySlot(onSlot); clock.start(controller.signal); // Must run once immediately - expect(onSlot.callCount).to.equal(1, "runEverySlot(cb) must be called immediately"); - expect(onSlot.getCall(0).args[0]).to.equal(0, "Wrong arg on runEverySlot(cb) call 0"); + expect(onSlot).toHaveBeenCalledOnce(); + expect(onSlot).toHaveBeenNthCalledWith(1, 0, expect.any(AbortSignal)); - await fakeClock.tickAsync(config.SECONDS_PER_SLOT * 1000); - expect(onSlot.callCount).to.equal(2, "runEverySlot(cb) must be called after after slot 1"); - expect(onSlot.getCall(1).args[0]).to.equal(1, "Wrong arg on runEverySlot(cb) call 1"); + await vi.advanceTimersByTimeAsync(config.SECONDS_PER_SLOT * 1000); + expect(onSlot).toHaveBeenCalledTimes(2); + expect(onSlot).toHaveBeenNthCalledWith(2, 1, expect.any(AbortSignal)); - await fakeClock.tickAsync(config.SECONDS_PER_SLOT * 1000); - expect(onSlot.callCount).to.equal(3, "runEverySlot(cb) must be called again after slot 2"); - expect(onSlot.getCall(2).args[0]).to.equal(2, "Wrong arg on runEverySlot(cb) call 2"); + await vi.advanceTimersByTimeAsync(config.SECONDS_PER_SLOT * 1000); + expect(onSlot).toHaveBeenCalledTimes(3); + expect(onSlot).toHaveBeenNthCalledWith(3, 2, expect.any(AbortSignal)); }); it("Should stop calling on slot after stop()", async () => { const genesisTime = Math.floor(Date.now() / 1000) - config.SECONDS_PER_SLOT / 2; const clock = new Clock(config, logger, {genesisTime}); - const onSlot = sinon.stub().resolves(); + const onSlot = vi.fn().mockResolvedValue(undefined); clock.runEverySlot(onSlot); clock.start(controller.signal); - await fakeClock.tickAsync(config.SECONDS_PER_SLOT * 1000); - expect(onSlot.callCount).to.equal(2, "runEverySlot(cb) must be called after after slot 1"); - expect(onSlot.getCall(1).args[0]).to.equal(1, "Wrong arg on runEverySlot(cb) call 1"); + await vi.advanceTimersByTimeAsync(config.SECONDS_PER_SLOT * 1000); + expect(onSlot).toBeCalledTimes(2); + expect(onSlot).toHaveBeenNthCalledWith(2, 1, expect.any(AbortSignal)); // Stop clock controller.abort(); - await fakeClock.tickAsync(config.SECONDS_PER_SLOT * 1000); - expect(onSlot.callCount).to.equal(2, "runEverySlot(cb) should not be called again"); + await vi.advanceTimersByTimeAsync(config.SECONDS_PER_SLOT * 1000); + expect(onSlot).toBeCalledTimes(2); }); it("Should call on epoch", async () => { @@ -65,20 +64,20 @@ describe("util / Clock", function () { const clock = new Clock(config, logger, {genesisTime}); - const onEpoch = sinon.stub().resolves(); + const onEpoch = vi.fn().mockResolvedValue(undefined); clock.runEveryEpoch(onEpoch); clock.start(controller.signal); // Must run once immediately - expect(onEpoch.callCount).to.equal(1, "runEverySlot(cb) must be called immediately"); - expect(onEpoch.getCall(0).args[0]).to.equal(0, "Wrong arg on runEverySlot(cb) call 0"); + expect(onEpoch).toHaveBeenCalledOnce(); + expect(onEpoch).toHaveBeenCalledWith(0, expect.any(AbortSignal)); - await fakeClock.tickAsync(config.SECONDS_PER_SLOT * 1000); - expect(onEpoch.callCount).to.equal(1, "runEverySlot(cb) must not be called again after a slot"); + await vi.advanceTimersByTimeAsync(config.SECONDS_PER_SLOT * 1000); + expect(onEpoch).toHaveBeenCalledOnce(); - await fakeClock.tickAsync(SLOTS_PER_EPOCH * config.SECONDS_PER_SLOT * 1000); - expect(onEpoch.callCount).to.equal(2, "runEverySlot(cb) must be called again after an epoch"); - expect(onEpoch.getCall(1).args[0]).to.equal(1, "Wrong arg on runEverySlot(cb) call 1"); + await vi.advanceTimersByTimeAsync(SLOTS_PER_EPOCH * config.SECONDS_PER_SLOT * 1000); + expect(onEpoch).toHaveBeenCalledTimes(2); + expect(onEpoch).toHaveBeenNthCalledWith(2, 1, expect.any(AbortSignal)); }); describe("getCurrentSlot", function () { @@ -93,8 +92,8 @@ describe("util / Clock", function () { for (const {name, delta} of testCase) { it(name, async function () { const currentSlot = getCurrentSlotAround(testConfig, genesisTime); - fakeClock.tick(delta * 1000); - expect(getCurrentSlotAround(testConfig, genesisTime)).to.be.equal(currentSlot + 1, name); + vi.advanceTimersByTime(delta * 1000); + expect(getCurrentSlotAround(testConfig, genesisTime)).toBe(currentSlot + 1); }); } }); diff --git a/packages/validator/test/unit/utils/difference.test.ts b/packages/validator/test/unit/utils/difference.test.ts index 9d35a39b1fdf..023a6144877a 100644 --- a/packages/validator/test/unit/utils/difference.test.ts +++ b/packages/validator/test/unit/utils/difference.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {differenceHex} from "../../../src/util/difference.js"; describe("utils / differenceHex", () => { @@ -9,11 +9,11 @@ describe("utils / differenceHex", () => { it("Return new hex items", () => { const additionalRoots = differenceHex([root0, root1a], [root1b, root2]); - expect(additionalRoots).to.deep.equal([root2]); + expect(additionalRoots).toEqual([root2]); }); it("Return no new hex items", () => { const additionalRoots = differenceHex([root0, root1a], [root1b]); - expect(additionalRoots).to.deep.equal([]); + expect(additionalRoots).toEqual([]); }); }); diff --git a/packages/validator/test/unit/utils/format.test.ts b/packages/validator/test/unit/utils/format.test.ts index 84139f5fd51e..ab75c8fa1b72 100644 --- a/packages/validator/test/unit/utils/format.test.ts +++ b/packages/validator/test/unit/utils/format.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {formatBigDecimal} from "../../../src/util/format.js"; describe("util / formatBigDecimal", function () { @@ -15,7 +15,7 @@ describe("util / formatBigDecimal", function () { ]; for (const [numerator, denominator, decimalFactor, expectedString] of testCases) { it(`format ${numerator} / ${denominator} correctly to ${expectedString}`, () => { - expect(formatBigDecimal(numerator, denominator, decimalFactor)).to.be.equal(expectedString); + expect(formatBigDecimal(numerator, denominator, decimalFactor)).toBe(expectedString); }); } }); diff --git a/packages/validator/test/unit/utils/metrics.test.ts b/packages/validator/test/unit/utils/metrics.test.ts index cf3ebc960ad5..695e8731b7f6 100644 --- a/packages/validator/test/unit/utils/metrics.test.ts +++ b/packages/validator/test/unit/utils/metrics.test.ts @@ -1,10 +1,8 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {BeaconHealth, renderEnumNumeric} from "../../../src/metrics.js"; describe("renderEnumNumeric", () => { it("BeaconHealth", () => { - expect(renderEnumNumeric(BeaconHealth)).equals( - "READY=0, SYNCING=1, NOT_INITIALIZED_OR_ISSUES=2, UNKNOWN=3, ERROR=4" - ); + expect(renderEnumNumeric(BeaconHealth)).toBe("READY=0, SYNCING=1, NOT_INITIALIZED_OR_ISSUES=2, UNKNOWN=3, ERROR=4"); }); }); diff --git a/packages/validator/test/unit/utils/params.test.ts b/packages/validator/test/unit/utils/params.test.ts index 10e814e4d501..47b743e0ce5e 100644 --- a/packages/validator/test/unit/utils/params.test.ts +++ b/packages/validator/test/unit/utils/params.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {describe, it, expect} from "vitest"; import {chainConfigToJson, ChainConfig} from "@lodestar/config"; import {chainConfig} from "@lodestar/config/default"; import {networksChainConfig} from "@lodestar/config/networks"; @@ -28,7 +28,7 @@ describe("utils / params / assertEqualParams", () => { // Force ALTAIR_FORK_EPOCH value to be different const otherConfig = {...chainConfigJson, ALTAIR_FORK_EPOCH: String(ALTAIR_FORK_EPOCH + 1)}; - expect(() => assertEqualParams(localConfig, otherConfig)).to.throw(NotEqualParamsError); + expect(() => assertEqualParams(localConfig, otherConfig)).toThrow(NotEqualParamsError); }); it("should fill missing remote values with default and be equal", () => { diff --git a/packages/validator/test/unit/validatorStore.test.ts b/packages/validator/test/unit/validatorStore.test.ts index 37cae9c2b558..3f7f0792f378 100644 --- a/packages/validator/test/unit/validatorStore.test.ts +++ b/packages/validator/test/unit/validatorStore.test.ts @@ -1,6 +1,5 @@ +import {describe, it, expect, beforeEach, afterEach, vi} from "vitest"; import {toBufferBE} from "bigint-buffer"; -import {expect} from "chai"; -import sinon from "sinon"; import bls from "@chainsafe/bls"; import {toHexString, fromHexString} from "@chainsafe/ssz"; import {chainConfig} from "@lodestar/config/default"; @@ -11,18 +10,15 @@ import {ValidatorStore} from "../../src/services/validatorStore.js"; import {getApiClientStub} from "../utils/apiStub.js"; import {initValidatorStore} from "../utils/validatorStore.js"; import {ValidatorProposerConfig} from "../../src/services/validatorStore.js"; -import {SinonStubFn} from "..//utils/types.js"; describe("ValidatorStore", function () { - const sandbox = sinon.createSandbox(); - const api = getApiClientStub(sandbox); + const api = getApiClientStub(); let validatorStore: ValidatorStore; let valProposerConfig: ValidatorProposerConfig; - let signValidatorStub: SinonStubFn; - before(async () => { + beforeEach(async () => { valProposerConfig = { proposerConfig: { [toHexString(pubkeys[0])]: { @@ -46,43 +42,37 @@ describe("ValidatorStore", function () { }; validatorStore = await initValidatorStore(secretKeys, api, chainConfig, valProposerConfig); - signValidatorStub = sinon.stub(validatorStore, "signValidatorRegistration"); }); - after(() => { - sandbox.restore(); + afterEach(() => { + vi.resetAllMocks(); }); it("Should validate graffiti,feeRecipient etc. from valProposerConfig and ValidatorStore", async function () { //pubkeys[0] values - expect(validatorStore.getGraffiti(toHexString(pubkeys[0]))).to.be.equal( + expect(validatorStore.getGraffiti(toHexString(pubkeys[0]))).toBe( valProposerConfig.proposerConfig[toHexString(pubkeys[0])].graffiti ); - expect(validatorStore.getFeeRecipient(toHexString(pubkeys[0]))).to.be.equal( + expect(validatorStore.getFeeRecipient(toHexString(pubkeys[0]))).toBe( valProposerConfig.proposerConfig[toHexString(pubkeys[0])].feeRecipient ); - expect(validatorStore.strictFeeRecipientCheck(toHexString(pubkeys[0]))).to.be.equal( + expect(validatorStore.strictFeeRecipientCheck(toHexString(pubkeys[0]))).toBe( valProposerConfig.proposerConfig[toHexString(pubkeys[0])].strictFeeRecipientCheck ); - expect(validatorStore.getGasLimit(toHexString(pubkeys[0]))).to.be.equal( + expect(validatorStore.getGasLimit(toHexString(pubkeys[0]))).toBe( valProposerConfig.proposerConfig[toHexString(pubkeys[0])].builder?.gasLimit ); // default values - expect(validatorStore.getGraffiti(toHexString(pubkeys[1]))).to.be.equal(valProposerConfig.defaultConfig.graffiti); - expect(validatorStore.getFeeRecipient(toHexString(pubkeys[1]))).to.be.equal( - valProposerConfig.defaultConfig.feeRecipient - ); - expect(validatorStore.strictFeeRecipientCheck(toHexString(pubkeys[1]))).to.be.equal( + expect(validatorStore.getGraffiti(toHexString(pubkeys[1]))).toBe(valProposerConfig.defaultConfig.graffiti); + expect(validatorStore.getFeeRecipient(toHexString(pubkeys[1]))).toBe(valProposerConfig.defaultConfig.feeRecipient); + expect(validatorStore.strictFeeRecipientCheck(toHexString(pubkeys[1]))).toBe( valProposerConfig.defaultConfig.strictFeeRecipientCheck ); - expect(validatorStore.getGasLimit(toHexString(pubkeys[1]))).to.be.equal( - valProposerConfig.defaultConfig.builder?.gasLimit - ); + expect(validatorStore.getGasLimit(toHexString(pubkeys[1]))).toBe(valProposerConfig.defaultConfig.builder?.gasLimit); }); it("Should create/update builder data and return from cache next time", async () => { - let signCallCount = 0; let slot = 0; const testCases: [bellatrix.SignedValidatorRegistrationV1, string, number][] = [ [valRegF00G100, "0x00", 100], @@ -90,19 +80,14 @@ describe("ValidatorStore", function () { [valRegF10G200, "0x10", 200], ]; for (const [valReg, feeRecipient, gasLimit] of testCases) { - signValidatorStub.resolves(valReg); + vi.spyOn(validatorStore, "signValidatorRegistration").mockResolvedValue(valReg); + const val1 = await validatorStore.getValidatorRegistration(pubkeys[0], {feeRecipient, gasLimit}, slot++); - expect(JSON.stringify(val1)).to.be.eql(JSON.stringify(valReg)); - expect(signValidatorStub.callCount).to.equal( - ++signCallCount, - `signValidatorRegistration() must be updated for new feeRecipient=${feeRecipient} gasLimit=${gasLimit} combo ` - ); + expect(JSON.stringify(val1)).toEqual(JSON.stringify(valReg)); + expect(validatorStore.signValidatorRegistration).toHaveBeenCalledOnce(); const val2 = await validatorStore.getValidatorRegistration(pubkeys[0], {feeRecipient, gasLimit}, slot++); - expect(JSON.stringify(val2)).to.be.eql(JSON.stringify(valReg)); - expect(signValidatorStub.callCount).to.equal( - signCallCount, - `signValidatorRegistration() must be updated for same feeRecipient=${feeRecipient} gasLimit=${gasLimit} combo ` - ); + expect(JSON.stringify(val2)).toEqual(JSON.stringify(valReg)); + expect(validatorStore.signValidatorRegistration).toHaveBeenCalledOnce(); } }); }); diff --git a/packages/validator/test/utils/apiStub.ts b/packages/validator/test/utils/apiStub.ts index 55b4a370569c..521abfae171d 100644 --- a/packages/validator/test/utils/apiStub.ts +++ b/packages/validator/test/utils/apiStub.ts @@ -1,24 +1,29 @@ -import sinon, {SinonSandbox} from "sinon"; -import {getClient, Api} from "@lodestar/api"; -import {config} from "@lodestar/config/default"; - -export function getApiClientStub( - sandbox: SinonSandbox = sinon -): Api & {[K in keyof Api]: sinon.SinonStubbedInstance} { - const api = getClient({baseUrl: "http://localhost:9596"}, {config}); +import {vi, Mocked} from "vitest"; +import {Api} from "@lodestar/api"; +export function getApiClientStub(): {[K in keyof Api]: Mocked} { return { - beacon: sandbox.stub(api.beacon), - config: sandbox.stub(api.config), - // Typescript errors due to the multiple return types of debug.getState() - // Since the return type of this function is typed, casting to any to patch the error quickly - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - debug: sandbox.stub(api.debug) as any, - events: sandbox.stub(api.events), - lightclient: sandbox.stub(api.lightclient), - lodestar: sandbox.stub(api.lodestar), - node: sandbox.stub(api.node), - proof: sandbox.stub(api.proof), - validator: sandbox.stub(api.validator), - }; + beacon: { + getStateValidators: vi.fn(), + publishBlindedBlockV2: vi.fn(), + publishBlockV2: vi.fn(), + submitPoolSyncCommitteeSignatures: vi.fn(), + submitPoolAttestations: vi.fn(), + }, + validator: { + getProposerDuties: vi.fn(), + getAttesterDuties: vi.fn(), + prepareBeaconCommitteeSubnet: vi.fn(), + produceBlockV3: vi.fn(), + getSyncCommitteeDuties: vi.fn(), + prepareSyncCommitteeSubnets: vi.fn(), + produceSyncCommitteeContribution: vi.fn(), + publishContributionAndProofs: vi.fn(), + submitSyncCommitteeSelections: vi.fn(), + produceAttestationData: vi.fn(), + getAggregatedAttestation: vi.fn(), + publishAggregateAndProofs: vi.fn(), + submitBeaconCommitteeSelections: vi.fn(), + }, + } as unknown as {[K in keyof Api]: Mocked}; } diff --git a/packages/validator/test/utils/types.ts b/packages/validator/test/utils/types.ts index 3ccba6c69918..4704a8142dfc 100644 --- a/packages/validator/test/utils/types.ts +++ b/packages/validator/test/utils/types.ts @@ -1,9 +1,4 @@ -import {SinonStub} from "sinon"; import {toHex} from "@lodestar/utils"; export const ZERO_HASH = Buffer.alloc(32, 0); export const ZERO_HASH_HEX = toHex(ZERO_HASH); - -export type SinonStubFn any> = T extends (...args: infer TArgs) => infer TReturnValue - ? SinonStub - : never;