Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Altair VC client #2520

Merged
merged 14 commits into from
May 14, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export function createEpochContext(
currSyncCommitteeIndexes,
nextSyncCommitteeIndexes,
currSyncComitteeValidatorIndexMap: computeSyncComitteeMap(currSyncCommitteeIndexes),
nextSyncComitteeValidatorIndexMap: computeSyncComitteeMap(nextSyncCommitteeIndexes),
});
}

Expand Down Expand Up @@ -188,7 +189,8 @@ export function rotateEpochs(
const nextPeriodEpoch = currEpoch + epochCtx.config.params.EPOCHS_PER_SYNC_COMMITTEE_PERIOD;
epochCtx.currSyncCommitteeIndexes = epochCtx.nextSyncCommitteeIndexes;
epochCtx.nextSyncCommitteeIndexes = getSyncCommitteeIndices(epochCtx.config, state, nextPeriodEpoch);
epochCtx.currSyncComitteeValidatorIndexMap = computeSyncComitteeMap(epochCtx.currSyncCommitteeIndexes);
epochCtx.currSyncComitteeValidatorIndexMap = epochCtx.nextSyncComitteeValidatorIndexMap;
epochCtx.nextSyncComitteeValidatorIndexMap = computeSyncComitteeMap(epochCtx.nextSyncCommitteeIndexes);
}

// If crossing through the altair fork the caches will be empty, fill them up
Expand All @@ -197,6 +199,7 @@ export function rotateEpochs(
epochCtx.currSyncCommitteeIndexes = getSyncCommitteeIndices(epochCtx.config, state, currEpoch);
epochCtx.nextSyncCommitteeIndexes = getSyncCommitteeIndices(epochCtx.config, state, nextPeriodEpoch);
epochCtx.currSyncComitteeValidatorIndexMap = computeSyncComitteeMap(epochCtx.currSyncCommitteeIndexes);
epochCtx.nextSyncComitteeValidatorIndexMap = computeSyncComitteeMap(epochCtx.nextSyncCommitteeIndexes);
}
}

Expand All @@ -212,6 +215,7 @@ interface IEpochContextData {
currSyncCommitteeIndexes: ValidatorIndex[];
nextSyncCommitteeIndexes: ValidatorIndex[];
currSyncComitteeValidatorIndexMap: SyncComitteeValidatorIndexMap;
nextSyncComitteeValidatorIndexMap: SyncComitteeValidatorIndexMap;
}

/**
Expand All @@ -238,16 +242,13 @@ export class EpochContext {
* Memory cost: 1024 Number integers.
*/
currSyncCommitteeIndexes: ValidatorIndex[];
/**
* Update freq: every ~ 27h.
* Memory cost: 1024 Number integers.
*/
nextSyncCommitteeIndexes: ValidatorIndex[];
/**
* Update freq: every ~ 27h.
* Memory cost: Map of Number -> Number with 1024 entries.
*/
currSyncComitteeValidatorIndexMap: SyncComitteeValidatorIndexMap;
nextSyncComitteeValidatorIndexMap: SyncComitteeValidatorIndexMap;
config: IBeaconConfig;

constructor(data: IEpochContextData) {
Expand All @@ -261,6 +262,7 @@ export class EpochContext {
this.currSyncCommitteeIndexes = data.currSyncCommitteeIndexes;
this.nextSyncCommitteeIndexes = data.nextSyncCommitteeIndexes;
this.currSyncComitteeValidatorIndexMap = data.currSyncComitteeValidatorIndexMap;
this.nextSyncComitteeValidatorIndexMap = data.nextSyncComitteeValidatorIndexMap;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions packages/lodestar/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {BlockId, IBeaconBlocksApi} from "./interface";
import {resolveBlockId, toBeaconHeaderResponse} from "./utils";
import {IBeaconSync} from "../../../../sync";
import {INetwork} from "../../../../network/interface";
import {getBlockType} from "../../../../util/multifork";

export * from "./interface";

Expand Down Expand Up @@ -107,6 +108,21 @@ export class BeaconBlockApi implements IBeaconBlocksApi {
return await resolveBlockId(this.chain.forkChoice, this.db, blockId);
}

async getBlockRoot(blockId: BlockId): Promise<Root> {
// Fast path: From head state already available in memory get historical blockRoot
const slot = parseInt(blockId);
if (!Number.isNaN(slot)) {
const state = this.chain.getHeadState();
if (slot < state.slot && state.slot <= slot + this.config.params.SLOTS_PER_HISTORICAL_ROOT) {
return state.blockRoots[slot % this.config.params.SLOTS_PER_HISTORICAL_ROOT];
}
}

// Slow path
const block = await resolveBlockId(this.chain.forkChoice, this.db, blockId);
return getBlockType(this.config, block.message).hashTreeRoot(block.message);
}

async publishBlock(signedBlock: phase0.SignedBeaconBlock): Promise<void> {
await Promise.all([this.chain.receiveBlock(signedBlock), this.network.gossip.publishBeaconBlock(signedBlock)]);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/lodestar/src/api/impl/beacon/blocks/interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Root, phase0, allForks, Slot} from "@chainsafe/lodestar-types";

export interface IBeaconBlocksApi {
getBlock(blockId: BlockId): Promise<allForks.SignedBeaconBlock>;
getBlockHeaders(filters: Partial<{slot: Slot; parentRoot: Root}>): Promise<phase0.SignedBeaconHeaderResponse[]>;
getBlockHeader(blockId: BlockId): Promise<phase0.SignedBeaconHeaderResponse>;
getBlock(blockId: BlockId): Promise<allForks.SignedBeaconBlock>;
getBlockRoot(blockId: BlockId): Promise<Root>;
publishBlock(block: phase0.SignedBeaconBlock): Promise<void>;
}

Expand Down
12 changes: 6 additions & 6 deletions packages/lodestar/src/api/impl/beacon/pool/pool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {altair, Epoch, phase0} from "@chainsafe/lodestar-types";
import {computeEpochAtSlot, allForks} from "@chainsafe/lodestar-beacon-state-transition";
import {allForks} from "@chainsafe/lodestar-beacon-state-transition";
import {SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params";
import {IAttestationJob, IBeaconChain} from "../../../../chain";
import {AttestationError, AttestationErrorCode} from "../../../../chain/errors";
Expand Down Expand Up @@ -106,14 +106,14 @@ export class BeaconPoolApi implements IBeaconPoolApi {
* https://github.com/ethereum/eth2.0-APIs/pull/135
*/
async submitSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise<void> {
// Fetch states for all epochs of the `signatures`
const epochs = new Set<Epoch>();
// Fetch states for all slots of the `signatures`
const slots = new Set<Epoch>();
for (const signature of signatures) {
epochs.add(computeEpochAtSlot(this.config, signature.slot));
slots.add(signature.slot);
}

// TODO: Fetch states at signature epochs
const state = await this.chain.getHeadStateAtCurrentEpoch();
// TODO: Fetch states at signature slots
const state = this.chain.getHeadState();

// TODO: Cache this value
const SYNC_COMMITTEE_SUBNET_SIZE = Math.floor(this.config.params.SYNC_COMMITTEE_SIZE / SYNC_COMMITTEE_SUBNET_COUNT);
Expand Down
30 changes: 30 additions & 0 deletions packages/lodestar/src/api/impl/validator/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
CachedBeaconState,
computeEpochAtSlot,
computeSyncCommitteePeriod,
} from "@chainsafe/lodestar-beacon-state-transition";
import {IBeaconConfig} from "@chainsafe/lodestar-config";
import {allForks, altair, Epoch, ValidatorIndex} from "@chainsafe/lodestar-types";
import {ApiError} from "../errors";

export function getSyncComitteeValidatorIndexMap(
config: IBeaconConfig,
state: allForks.BeaconState | CachedBeaconState<allForks.BeaconState>,
requestedEpoch: Epoch
): Map<ValidatorIndex, number[]> {
const statePeriod = computeSyncCommitteePeriod(config, computeEpochAtSlot(config, state.slot));
const requestPeriod = computeSyncCommitteePeriod(config, requestedEpoch);

if ((state as CachedBeaconState<allForks.BeaconState>).epochCtx) {
switch (requestPeriod) {
case statePeriod:
return (state as CachedBeaconState<altair.BeaconState>).currSyncComitteeValidatorIndexMap;
case statePeriod + 1:
return (state as CachedBeaconState<altair.BeaconState>).nextSyncComitteeValidatorIndexMap;
default:
throw new ApiError(400, "Epoch out of bounds");
}
}

throw new ApiError(400, "No CachedBeaconState available");
}
35 changes: 16 additions & 19 deletions packages/lodestar/src/api/impl/validator/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {ApiNamespace, IApiModules} from "../interface";
import {IValidatorApi} from "./interface";
import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof";
import {CommitteeSubscription} from "../../../network/subnetsService";
import {getSyncComitteeValidatorIndexMap} from "./utils";

/**
* Validator clock may be advanced from beacon's clock. If the validator requests a resource in a
Expand Down Expand Up @@ -229,15 +230,16 @@ export class ValidatorApi implements IValidatorApi {
// May request for an epoch that's in the future
await this.waitForNextClosestEpoch();

// TODO: Get state at `epoch`
const state = await this.chain.getHeadStateAtCurrentEpoch();
// TODO: ensures `epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD <= current_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1`
// const syncCommittee = getSyncCommittees(this.config, state, epoch);
// Note: does not support requesting past duties
const state = this.chain.getHeadState();

// Ensures `epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD <= current_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1`
const syncComitteeValidatorIndexMap = getSyncComitteeValidatorIndexMap(this.config, state, epoch);

const duties: altair.SyncDuty[] = validatorIndices.map((validatorIndex) => ({
pubkey: state.index2pubkey[validatorIndex].toBytes(),
validatorIndex,
validatorSyncCommitteeIndices: state.currSyncComitteeValidatorIndexMap.get(validatorIndex) ?? [],
validatorSyncCommitteeIndices: syncComitteeValidatorIndexMap.get(validatorIndex) ?? [],
}));

return {
Expand Down Expand Up @@ -359,26 +361,21 @@ export class ValidatorApi implements IValidatorApi {
async prepareSyncCommitteeSubnets(subscriptions: altair.SyncCommitteeSubscription[]): Promise<void> {
this.notWhileSyncing();

const state = await this.chain.getHeadStateAtCurrentEpoch();

// TODO: Cache this value
const SYNC_COMMITTEE_SUBNET_SIZE = Math.floor(this.config.params.SYNC_COMMITTEE_SIZE / SYNC_COMMITTEE_SUBNET_COUNT);

// A `validatorIndex` can be in multiple subnets, so compute the CommitteeSubscription with double for loop
const subs: CommitteeSubscription[] = [];
for (const sub of subscriptions) {
const committeeIndices = state.currSyncComitteeValidatorIndexMap.get(sub.validatorIndex);
if (committeeIndices) {
for (const committeeIndex of committeeIndices) {
const subnet = Math.floor(committeeIndex / SYNC_COMMITTEE_SUBNET_SIZE);
subs.push({
validatorIndex: sub.validatorIndex,
subnet: subnet,
// Subscribe until the end of `untilEpoch`: https://github.com/ethereum/eth2.0-APIs/pull/136#issuecomment-840315097
slot: computeStartSlotAtEpoch(this.config, sub.untilEpoch + 1),
isAggregator: true,
});
}
for (const committeeIndex of sub.syncCommitteeIndices) {
const subnet = Math.floor(committeeIndex / SYNC_COMMITTEE_SUBNET_SIZE);
subs.push({
validatorIndex: sub.validatorIndex,
subnet: subnet,
// Subscribe until the end of `untilEpoch`: https://github.com/ethereum/eth2.0-APIs/pull/136#issuecomment-840315097
slot: computeStartSlotAtEpoch(this.config, sub.untilEpoch + 1),
isAggregator: true,
});
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/lodestar/src/api/rest/beacon/blocks/getBlockRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export const getBlockRoot: ApiController<null, {blockId: string}> = {
id: "getBlockRoot",

handler: async function (req) {
const data = await this.api.beacon.blocks.getBlock(req.params.blockId);
const root = await this.api.beacon.blocks.getBlockRoot(req.params.blockId);
return {
data: {
root: this.config.types.Root.toJson(this.config.types.phase0.BeaconBlock.hashTreeRoot(data.message)),
root: this.config.types.Root.toJson(root),
},
};
},
Expand Down
7 changes: 6 additions & 1 deletion packages/lodestar/src/api/rest/beacon/blocks/publishBlock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {phase0} from "@chainsafe/lodestar-types";
import {SignedBeaconBlock} from "@chainsafe/lodestar-types/lib/allForks";
import {getSignedBlockType} from "../../../../util/multifork";
import {ValidationError} from "../../../impl/errors";
import {ApiController} from "../../types";

// TODO: Watch https://github.com/ethereum/eth2.0-APIs/pull/142 for resolution on how to upgrade this route

export const publishBlock: ApiController = {
url: "/eth/v1/beacon/blocks",
method: "POST",
Expand All @@ -10,7 +14,8 @@ export const publishBlock: ApiController = {
handler: async function (req) {
let block: phase0.SignedBeaconBlock;
try {
block = this.config.types.phase0.SignedBeaconBlock.fromJson(req.body, {case: "snake"});
const type = getSignedBlockType(this.config, req.body as SignedBeaconBlock);
block = type.fromJson(req.body, {case: "snake"});
} catch (e) {
throw new ValidationError(`Failed to deserialize block: ${(e as Error).message}`);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/lodestar/src/util/multifork.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ForkName, IBeaconConfig} from "@chainsafe/lodestar-config";
import {allForks, phase0, Slot} from "@chainsafe/lodestar-types";
import {allForks, Slot} from "@chainsafe/lodestar-types";
import {bytesToInt} from "@chainsafe/lodestar-utils";
import {ContainerType} from "@chainsafe/ssz";

Expand Down Expand Up @@ -35,8 +35,8 @@ const SLOT_BYTES_POSITION_IN_BLOCK = 100;
*/
const SLOT_BYTES_POSITION_IN_STATE = 40;

type BlockType = ContainerType<phase0.BeaconBlock>;
type SignedBlockType = ContainerType<phase0.SignedBeaconBlock>;
type BlockType = ContainerType<allForks.BeaconBlock>;
type SignedBlockType = ContainerType<allForks.SignedBeaconBlock>;
type StateType = ContainerType<allForks.BeaconState>;

// Block
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {expect} from "chai";
import supertest from "supertest";
import {toHexString} from "@chainsafe/ssz";
import {config} from "@chainsafe/lodestar-config/minimal";

import {getBlockRoot} from "../../../../../../src/api/rest/beacon/blocks/getBlockRoot";
import {generateEmptySignedBlock} from "../../../../../utils/block";
import {setupRestApiTestServer} from "../../index.test";
import {SinonStubbedInstance} from "sinon";
import {RestApi} from "../../../../../../src/api";
Expand All @@ -24,15 +22,13 @@ describe("rest - beacon - getBlockRoot", function () {
});

it("should succeed", async function () {
const block = generateEmptySignedBlock();
beaconBlocksStub.getBlock.withArgs("head").resolves(block);
const root = Buffer.alloc(32, 0x4d);
beaconBlocksStub.getBlockRoot.withArgs("head").resolves(root);
const response = await supertest(restApi.server.server)
.get(getBlockRoot.url.replace(":blockId", "head"))
.expect(200)
.expect("Content-Type", "application/json; charset=utf-8");
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(response.body.data.root).to.be.equal(
toHexString(config.types.phase0.BeaconBlock.hashTreeRoot(block.message))
);
expect(response.body.data.root).to.be.equal(toHexString(root));
});
});
1 change: 1 addition & 0 deletions packages/validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@chainsafe/lodestar-beacon-state-transition": "^0.21.0",
"@chainsafe/lodestar-config": "^0.21.0",
"@chainsafe/lodestar-db": "^0.21.0",
"@chainsafe/lodestar-params": "^0.21.0",
"@chainsafe/lodestar-types": "^0.21.0",
"@chainsafe/lodestar-utils": "^0.21.0",
"@chainsafe/ssz": "^0.8.4",
Expand Down
22 changes: 21 additions & 1 deletion packages/validator/src/api/interface.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import {EventEmitter} from "events";
import StrictEventEmitter from "strict-event-emitter-types";
import {Epoch, Slot, Root, phase0, ValidatorIndex, BLSSignature, CommitteeIndex} from "@chainsafe/lodestar-types";
import {
Epoch,
Slot,
Root,
phase0,
ValidatorIndex,
BLSSignature,
CommitteeIndex,
altair,
} from "@chainsafe/lodestar-types";
import {IStoppableEventIterable} from "@chainsafe/lodestar-utils";
import {IValidatorFilters} from "../util";

export type StateId = "head";
export type BlockId = "head" | Slot;

export enum BeaconEventType {
BLOCK = "block",
Expand Down Expand Up @@ -32,11 +42,13 @@ export interface IApiClient {
getStateValidators(stateId: StateId, filters?: IValidatorFilters): Promise<phase0.ValidatorResponse[]>;
};
blocks: {
getBlockRoot(blockId: BlockId): Promise<Root>;
publishBlock(block: phase0.SignedBeaconBlock): Promise<void>;
};
pool: {
submitAttestations(attestation: phase0.Attestation[]): Promise<void>;
submitVoluntaryExit(signedVoluntaryExit: phase0.SignedVoluntaryExit): Promise<void>;
submitSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise<void>;
};
getGenesis(): Promise<phase0.Genesis>;
};
Expand All @@ -57,11 +69,19 @@ export interface IApiClient {
validator: {
getProposerDuties(epoch: Epoch): Promise<phase0.ProposerDutiesApi>;
getAttesterDuties(epoch: Epoch, validatorIndices: ValidatorIndex[]): Promise<phase0.AttesterDutiesApi>;
getSyncCommitteeDuties(epoch: number, validatorIndices: ValidatorIndex[]): Promise<altair.SyncDutiesApi>;
produceBlock(slot: Slot, randaoReveal: BLSSignature, graffiti: string): Promise<phase0.BeaconBlock>;
produceAttestationData(index: CommitteeIndex, slot: Slot): Promise<phase0.AttestationData>;
produceSyncCommitteeContribution(
slot: Slot,
subcommitteeIndex: number,
beaconBlockRoot: Root
): Promise<altair.SyncCommitteeContribution>;
getAggregatedAttestation(attestationDataRoot: Root, slot: Slot): Promise<phase0.Attestation>;
publishAggregateAndProofs(signedAggregateAndProofs: phase0.SignedAggregateAndProof[]): Promise<void>;
publishContributionAndProofs(contributionAndProofs: altair.SignedContributionAndProof[]): Promise<void>;
prepareBeaconCommitteeSubnet(subscriptions: phase0.BeaconCommitteeSubscription[]): Promise<void>;
prepareSyncCommitteeSubnets(subscriptions: altair.SyncCommitteeSubscription[]): Promise<void>;
};
}

Expand Down
Loading