Skip to content

Commit

Permalink
Add unit tests for distributed sync committee aggregation selection
Browse files Browse the repository at this point in the history
  • Loading branch information
nflaig committed Apr 14, 2023
1 parent 47b734d commit 78155a7
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 84 deletions.
2 changes: 1 addition & 1 deletion packages/validator/src/services/syncCommittee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {groupSyncDutiesBySubcommitteeIndex, SubcommitteeDuty} from "./utils.js";
import {ChainHeaderTracker} from "./chainHeaderTracker.js";
import {ValidatorEventEmitter} from "./emitter.js";

type SyncCommitteeServiceOpts = {
export type SyncCommitteeServiceOpts = {
scAfterBlockDelaySlotFraction?: number;
distributedAggregationSelection?: boolean;
};
Expand Down
223 changes: 140 additions & 83 deletions packages/validator/test/unit/services/syncCommittee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {toHexString} from "@chainsafe/ssz";
import {createChainForkConfig} from "@lodestar/config";
import {config as mainnetConfig} from "@lodestar/config/default";
import {ssz} from "@lodestar/types";
import {HttpStatusCode} from "@lodestar/api";
import {SyncCommitteeService} from "../../../src/services/syncCommittee.js";
import {HttpStatusCode, routes} from "@lodestar/api";
import {SyncCommitteeService, SyncCommitteeServiceOpts} from "../../../src/services/syncCommittee.js";
import {SyncDutyAndProofs} from "../../../src/services/syncCommitteeDuties.js";
import {ValidatorStore} from "../../../src/services/validatorStore.js";
import {getApiClientStub} from "../../utils/apiStub.js";
Expand Down Expand Up @@ -47,87 +47,144 @@ describe("SyncCommitteeService", function () {

let controller: AbortController; // To stop clock
beforeEach(() => (controller = new AbortController()));
afterEach(() => controller.abort());

it("Should produce, sign, and publish a sync committee + contribution", async () => {
const clock = new ClockMock();
const syncCommitteeService = new SyncCommitteeService(
config,
loggerVc,
api,
clock,
validatorStore,
emitter,
chainHeaderTracker,
null
);

const beaconBlockRoot = Buffer.alloc(32, 0x4d);
const syncCommitteeSignature = ssz.altair.SyncCommitteeMessage.defaultValue();
const contribution = ssz.altair.SyncCommitteeContribution.defaultValue();
const contributionAndProof = ssz.altair.SignedContributionAndProof.defaultValue();
const duties: SyncDutyAndProofs[] = [
{
duty: {
pubkey: toHexString(pubkeys[0]),
validatorIndex: 0,
subnets: [0],
},
selectionProofs: [{selectionProof: ZERO_HASH, subcommitteeIndex: 0}],
},
];

// Return empty replies to duties service
api.beacon.getStateValidators.resolves({
response: {data: [], executionOptimistic: false},
ok: true,
status: HttpStatusCode.OK,
});
api.validator.getSyncCommitteeDuties.resolves({
response: {data: [], executionOptimistic: false},
ok: true,
status: HttpStatusCode.OK,
});

// Mock duties service to return some duties directly
syncCommitteeService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties);

// Mock beacon's sync committee and contribution routes
afterEach(() => {
controller.abort();
sandbox.resetHistory();
});

chainHeaderTracker.getCurrentChainHead.returns(beaconBlockRoot);
api.beacon.submitPoolSyncCommitteeSignatures.resolves();
api.validator.produceSyncCommitteeContribution.resolves({
response: {data: contribution},
ok: true,
status: HttpStatusCode.OK,
const testContexts: [string, SyncCommitteeServiceOpts][] = [
["With default configuration", {}],
["With distributed aggregation selection enabled", {distributedAggregationSelection: true}],
];

for (const [title, opts] of testContexts) {
context(title, () => {
it("Should produce, sign, and publish a sync committee + contribution", async () => {
const clock = new ClockMock();
const syncCommitteeService = new SyncCommitteeService(
config,
loggerVc,
api,
clock,
validatorStore,
emitter,
chainHeaderTracker,
null,
opts
);

const beaconBlockRoot = Buffer.alloc(32, 0x4d);
const syncCommitteeSignature = ssz.altair.SyncCommitteeMessage.defaultValue();
const contribution = ssz.altair.SyncCommitteeContribution.defaultValue();
const contributionAndProof = ssz.altair.SignedContributionAndProof.defaultValue();
const duties: SyncDutyAndProofs[] = [
{
duty: {
pubkey: toHexString(pubkeys[0]),
validatorIndex: 0,
subnets: [0],
},
selectionProofs: [
{
selectionProof: opts.distributedAggregationSelection ? null : ZERO_HASH,
partialSelectionProof: opts.distributedAggregationSelection ? ZERO_HASH : undefined,
subcommitteeIndex: 0,
},
],
},
];

// Return empty replies to duties service
api.beacon.getStateValidators.resolves({
response: {data: [], executionOptimistic: false},
ok: true,
status: HttpStatusCode.OK,
});
api.validator.getSyncCommitteeDuties.resolves({
response: {data: [], executionOptimistic: false},
ok: true,
status: HttpStatusCode.OK,
});

// Mock duties service to return some duties directly
syncCommitteeService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties);

// Mock beacon's sync committee and contribution routes

chainHeaderTracker.getCurrentChainHead.returns(beaconBlockRoot);
api.beacon.submitPoolSyncCommitteeSignatures.resolves({
response: undefined,
ok: true,
status: HttpStatusCode.OK,
});
api.validator.produceSyncCommitteeContribution.resolves({
response: {data: contribution},
ok: true,
status: HttpStatusCode.OK,
});
api.validator.publishContributionAndProofs.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_sync_committee_aggregator` test
api.validator.submitSyncCommitteeSelections.resolves({
response: {
data: [{validatorIndex: 0, slot: 0, subcommitteeIndex: 0, selectionProof: Buffer.alloc(1, 0x19)}],
},
ok: true,
status: HttpStatusCode.OK,
});
}

// Mock signing service
validatorStore.signSyncCommitteeSignature.resolves(syncCommitteeSignature);
validatorStore.signContributionAndProof.resolves(contributionAndProof);

// Trigger clock onSlot for slot 0
await clock.tickSlotFns(0, controller.signal);

if (opts.distributedAggregationSelection) {
// Must submit partial sync committee selection proof based on duty
const selection: routes.validator.SyncCommitteeSelection = {
validatorIndex: 0,
slot: 0,
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"
);
}

// 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"
);

// 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"
);
});
});
api.validator.publishContributionAndProofs.resolves();

// Mock signing service
validatorStore.signSyncCommitteeSignature.resolves(syncCommitteeSignature);
validatorStore.signContributionAndProof.resolves(contributionAndProof);

// Trigger clock onSlot for slot 0
await clock.tickSlotFns(0, controller.signal);

// 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"
);

// 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"
);
});
}
});

0 comments on commit 78155a7

Please sign in to comment.