From 47b734d6e950c71fc6c5998f884674f33b1e6a46 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 14 Apr 2023 16:04:44 +0200 Subject: [PATCH] Add unit tests for distributed attestation aggregation selection --- .../test/unit/services/attestation.test.ts | 251 +++++++++++------- 1 file changed, 152 insertions(+), 99 deletions(-) diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index a53314dd359e..754b9a133ff7 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -3,7 +3,7 @@ import sinon from "sinon"; import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; -import {HttpStatusCode} from "@lodestar/api"; +import {HttpStatusCode, routes} from "@lodestar/api"; import {AttestationService, AttestationServiceOpts} from "../../../src/services/attestation.js"; import {AttDutyAndProof} from "../../../src/services/attestationDuties.js"; import {ValidatorStore} from "../../../src/services/validatorStore.js"; @@ -42,104 +42,157 @@ describe("AttestationService", function () { sandbox.resetHistory(); }); - context("With attestation grouping enabled", () => { - const opts: AttestationServiceOpts = {disableAttestationGrouping: false}; - - it("Should produce, sign, and publish an attestation + aggregate", async () => { - await testAttestationTasks(opts); - }); - }); - - context("With attestation grouping disabled", () => { - const opts: AttestationServiceOpts = {disableAttestationGrouping: true}; - - it("Should produce, sign, and publish an attestation + aggregate", async () => { - await testAttestationTasks(opts); - }); - }); - - async function testAttestationTasks(opts?: AttestationServiceOpts): Promise { - const clock = new ClockMock(); - const attestationService = new AttestationService( - loggerVc, - api, - clock, - validatorStore, - emitter, - chainHeadTracker, - null, - opts - ); - - const attestation = ssz.phase0.Attestation.defaultValue(); - const aggregate = ssz.phase0.SignedAggregateAndProof.defaultValue(); - const duties: AttDutyAndProof[] = [ - { - duty: { - slot: 0, - committeeIndex: attestation.data.index, - committeeLength: 120, - committeesAtSlot: 120, - validatorCommitteeIndex: 1, - validatorIndex: 0, - pubkey: pubkeys[0], - }, - selectionProof: ZERO_HASH, - }, - ]; - - // Return empty replies to duties service - api.beacon.getStateValidators.resolves({ - response: {executionOptimistic: false, data: []}, - ok: true, - status: HttpStatusCode.OK, - }); - api.validator.getAttesterDuties.resolves({ - response: {dependentRoot: ZERO_HASH_HEX, executionOptimistic: false, data: []}, - ok: true, - status: HttpStatusCode.OK, - }); - - // Mock duties service to return some duties directly - attestationService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties); - - // Mock beacon's attestation and aggregates endpoints - - api.validator.produceAttestationData.resolves({ - response: {data: attestation.data}, - ok: true, - status: HttpStatusCode.OK, - }); - api.validator.getAggregatedAttestation.resolves({ - response: {data: attestation}, - ok: true, - status: HttpStatusCode.OK, + const testContexts: [string, AttestationServiceOpts][] = [ + ["With default configuration", {}], + ["With attestation grouping disabled", {disableAttestationGrouping: true}], + ["With distributed aggregation selection enabled", {distributedAggregationSelection: true}], + ]; + + for (const [title, opts] of testContexts) { + context(title, () => { + it("Should produce, sign, and publish an attestation + aggregate", async () => { + const clock = new ClockMock(); + const attestationService = new AttestationService( + loggerVc, + api, + clock, + validatorStore, + emitter, + chainHeadTracker, + null, + opts + ); + + const attestation = ssz.phase0.Attestation.defaultValue(); + const aggregate = ssz.phase0.SignedAggregateAndProof.defaultValue(); + const duties: AttDutyAndProof[] = [ + { + duty: { + slot: 0, + committeeIndex: attestation.data.index, + committeeLength: 120, + committeesAtSlot: 120, + validatorCommitteeIndex: 1, + validatorIndex: 0, + pubkey: pubkeys[0], + }, + selectionProof: opts.distributedAggregationSelection ? null : ZERO_HASH, + partialSelectionProof: opts.distributedAggregationSelection ? ZERO_HASH : undefined, + }, + ]; + + // Return empty replies to duties service + api.beacon.getStateValidators.resolves({ + response: {executionOptimistic: false, data: []}, + ok: true, + status: HttpStatusCode.OK, + }); + api.validator.getAttesterDuties.resolves({ + response: {dependentRoot: ZERO_HASH_HEX, executionOptimistic: false, data: []}, + ok: true, + status: HttpStatusCode.OK, + }); + + // Mock duties service to return some duties directly + attestationService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties); + + // Mock beacon's attestation and aggregates endpoints + + api.validator.produceAttestationData.resolves({ + response: {data: attestation.data}, + ok: true, + status: HttpStatusCode.OK, + }); + api.validator.getAggregatedAttestation.resolves({ + response: {data: attestation}, + ok: true, + status: HttpStatusCode.OK, + }); + api.beacon.submitPoolAttestations.resolves({ + response: undefined, + ok: true, + status: HttpStatusCode.OK, + }); + api.validator.publishAggregateAndProofs.resolves({ + response: undefined, + ok: true, + status: HttpStatusCode.OK, + }); + + if (opts.distributedAggregationSelection) { + // Mock distributed validator middleware client selections endpoint + // and return a selection proof that passes `is_aggregator` test + api.validator.submitBeaconCommitteeSelections.resolves({ + response: {data: [{validatorIndex: 0, slot: 0, selectionProof: Buffer.alloc(1, 0x10)}]}, + ok: true, + status: HttpStatusCode.OK, + }); + // Accept all subscriptions + api.validator.prepareBeaconCommitteeSubnet.resolves({ + response: undefined, + ok: true, + status: HttpStatusCode.OK, + }); + } + + // Mock signing service + validatorStore.signAttestation.resolves(attestation); + validatorStore.signAggregateAndProof.resolves(aggregate); + + // Trigger clock onSlot for slot 0 + await clock.tickSlotFns(0, controller.signal); + + if (opts.distributedAggregationSelection) { + // Must submit partial beacon committee selection proof based on duty + const selection: routes.validator.BeaconCommitteeSelection = { + validatorIndex: 0, + 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" + ); + + // Must resubscribe validator as aggregator on beacon committee subnet + const subscription: routes.validator.BeaconCommitteeSubscription = { + validatorIndex: 0, + committeeIndex: 0, + committeesAtSlot: 120, + 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" + ); + } + + // 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" + ); + + // 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" + ); + }); }); - api.beacon.submitPoolAttestations.resolves(); - api.validator.publishAggregateAndProofs.resolves(); - - // Mock signing service - validatorStore.signAttestation.resolves(attestation); - validatorStore.signAggregateAndProof.resolves(aggregate); - - // Trigger clock onSlot for slot 0 - await clock.tickSlotFns(0, controller.signal); - - // 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" - ); - - // 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" - ); } });