diff --git a/.eslintrc b/.eslintrc index 26faacf9046e..ce75e43d6df5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,7 @@ "rules": { "@typescript-eslint/indent": ["error", 2], "@typescript-eslint/no-require-imports": "error", + "@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" diff --git a/src/chain/index.ts b/src/chain/index.ts index f6e23c1e920e..0eee2e5cbd71 100644 --- a/src/chain/index.ts +++ b/src/chain/index.ts @@ -2,7 +2,7 @@ import assert from "assert"; import {EventEmitter} from "events"; import {hashTreeRoot} from "@chainsafe/ssz"; -import {BeaconBlock, BeaconState, Deposit, Eth1Data, number64} from "../types"; +import {BeaconBlock, BeaconState, bytes48, Deposit, Epoch, Eth1Data, number64, Slot, ValidatorIndex} from "../types"; import {GENESIS_SLOT, SECONDS_PER_SLOT} from "../constants"; import {DB} from "../db"; @@ -20,6 +20,7 @@ import {getBlockRoot, getEpochStartSlot} from "./stateTransition/util"; */ export class BeaconChain extends EventEmitter { public chain: string; + public genesisTime: number64; private db: DB; private eth1: Eth1Notifier; private _latestBlock: BeaconBlock; @@ -57,6 +58,7 @@ export class BeaconChain extends EventEmitter { const genesisState = getGenesisBeaconState(genesisDeposits, genesisTime, genesisEth1Data); const genesisBlock = getEmptyBlock(); genesisBlock.stateRoot = hashTreeRoot(genesisState, BeaconState); + this.genesisTime = genesisTime; await this.db.setBlock(genesisBlock); await this.db.setChainHead(genesisState, genesisBlock); await this.db.setJustifiedBlock(genesisBlock); diff --git a/src/chain/stateTransition/util/validator.ts b/src/chain/stateTransition/util/validator.ts index 52b542ed46de..b0e025db9931 100644 --- a/src/chain/stateTransition/util/validator.ts +++ b/src/chain/stateTransition/util/validator.ts @@ -1,9 +1,14 @@ +import assert from "assert"; import { BeaconState, - Epoch, + Epoch, Slot, Validator, ValidatorIndex, } from "../../../types"; +import {getBeaconProposerIndex, getCrosslinkCommitteesAtSlot, getPreviousEpoch, slotToEpoch} from "./index"; +import {CommitteeAssignment} from "../../../validator/types"; +import {getCurrentEpoch, getEpochStartSlot} from "./epoch"; +import {SLOTS_PER_EPOCH} from "../../../constants"; /** @@ -44,3 +49,58 @@ export function getActiveValidatorIndices(state: BeaconState, epoch: Epoch): Val return indices; }, []); } + +/** + * Return the committee assignment in the ``epoch`` for ``validator_index`` and ``registry_change``. + * ``assignment`` returned is a tuple of the following form: + * ``assignment[0]`` is the list of validators in the committee + * ``assignment[1]`` is the shard to which the committee is assigned + * ``assignment[2]`` is the slot at which the committee is assigned + * a beacon block at the assigned slot. + * @param {BeaconState} state + * @param {Epoch} epoch + * @param {ValidatorIndex} validatorIndex + * @param {boolean} registryChange + * @returns {{validators: ValidatorIndex[]; shard: Shard; slot: number; isProposer: boolean}} + */ +export function getCommitteeAssignment( + state: BeaconState, + epoch: Epoch, + validatorIndex: ValidatorIndex): CommitteeAssignment { + + const previousEpoch = getPreviousEpoch(state); + const nextEpoch = getCurrentEpoch(state) + 1; + assert(previousEpoch <= epoch && epoch <= nextEpoch); + + const epochStartSlot = getEpochStartSlot(epoch); + const loopEnd = epochStartSlot + SLOTS_PER_EPOCH; + + for (let slot = epochStartSlot; slot < loopEnd; slot++) { + const crosslinkCommittees = getCrosslinkCommitteesAtSlot(state, slot); + const selectedCommittees = crosslinkCommittees.map((committee) => committee[0].includes(validatorIndex)); + + if (selectedCommittees.length > 0) { + const validators = selectedCommittees[0][0]; + const shard = selectedCommittees[0][1]; + return {validators, shard, slot}; + } + } +} + +/** + * Checks if a validator is supposed to propose a block + * @param {BeaconState} state + * @param {Slot} slot + * @param {ValidatorIndex} validatorIndex + * @returns {Boolean} + */ +export function isProposerAtSlot( + state: BeaconState, + slot: Slot, + validatorIndex: ValidatorIndex): boolean { + + const currentEpoch = getCurrentEpoch(state); + assert(slotToEpoch(slot) === currentEpoch); + + return getBeaconProposerIndex(state) === validatorIndex; +} diff --git a/src/node/index.ts b/src/node/index.ts index de14196028e9..d51202d50bbc 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -11,7 +11,7 @@ import {BeaconChain} from "../chain"; import {OpPool} from "../opPool"; import {JSONRPC} from "../rpc/protocol"; import {WSServer} from "../rpc/transport"; -import {BeaconAPI} from "../rpc/api"; +import {ValidatorApi} from "../rpc/api"; interface Service { start(): Promise; @@ -65,7 +65,7 @@ class BeaconNode { }); this.rpc = new JSONRPC(this.conf.rpc, { transport: new WSServer(this.conf.rpc), - api: new BeaconAPI(this.conf.rpc, { + api: new ValidatorApi(this.conf.rpc, { chain: this.chain, db: this.db, opPool: this.opPool, diff --git a/src/rpc/api/api.ts b/src/rpc/api/api.ts deleted file mode 100644 index 59347f264120..000000000000 --- a/src/rpc/api/api.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {Attestation, AttestationData, BeaconBlock, bytes32, Deposit, Shard, Slot, Eth1Data} from "../../types"; -import {DB} from "../../db"; -import {BeaconChain} from "../../chain"; -import {OpPool} from "../../opPool"; - -import {API} from "./interface"; - -export class BeaconAPI implements API { - private chain: BeaconChain; - private db: DB; - private opPool: OpPool; - - public constructor(opts, {chain, db, opPool}) { - this.chain = chain; - this.db = db; - this.opPool = opPool; - } - - public async getChainHead(): Promise { - return await this.db.getChainHead(); - } - - public async getPendingAttestations(): Promise { - return this.opPool.getAttestations(); - } - - public async getPendingDeposits(): Promise { - return []; - } - - public async getEth1Data(): Promise { - // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion - return {} as Eth1Data; - } - - public async computeStateRoot(block: BeaconBlock): Promise { - return Buffer.alloc(32); - } - - public async getAttestationData(slot: Slot, shard: Shard): Promise { - // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion - return {} as AttestationData; - } - - public async putAttestation(attestation: Attestation): Promise { - await this.opPool.receiveAttestation(attestation); - } - - public async putBlock(block: BeaconBlock): Promise { - await this.chain.receiveBlock(block); - } -} diff --git a/src/rpc/api/index.ts b/src/rpc/api/index.ts index b069ca92315d..c23d239883a8 100644 --- a/src/rpc/api/index.ts +++ b/src/rpc/api/index.ts @@ -1,3 +1 @@ -export * from "./interface"; -export * from "./mock"; -export * from "./api"; +export * from "./validator"; diff --git a/src/rpc/api/interface.ts b/src/rpc/api/interface.ts index da453b1a9cee..4df21dc7ec2f 100644 --- a/src/rpc/api/interface.ts +++ b/src/rpc/api/interface.ts @@ -1,46 +1,7 @@ -import {Attestation, AttestationData, BeaconBlock, bytes32, Deposit, Shard, Slot, Eth1Data} from "../../types"; - -/** - * The API interface defines the calls that can be made externally - */ -export interface API { - /** - * Return the current chain head - */ - getChainHead(): Promise; - - /** - * Return a list of attestations ready for inclusion in the next block - */ - getPendingAttestations(): Promise; - - /** - * Return a list of deposits ready for inclusion in the next block - */ - getPendingDeposits(): Promise; - +export interface IApi { /** - * Return the Eth1Data to be included in the next block + * Name space for API commands */ - getEth1Data(): Promise; - - /** - * Return the state root after the block has been run through the state transition - */ - computeStateRoot(block: BeaconBlock): Promise; - - /** - * Return the attestation data for a slot and shard based on the current head - */ - getAttestationData(slot: Slot, shard: Shard): Promise; - - /** - * Submit an attestation for processing - */ - putAttestation(attestation: Attestation): Promise; - - /** - * Submit a block for processing - */ - putBlock(block: BeaconBlock): Promise; + namespace: string; } + diff --git a/src/rpc/api/mock.ts b/src/rpc/api/mock.ts deleted file mode 100644 index 6a8b20c50dcb..000000000000 --- a/src/rpc/api/mock.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {Attestation, AttestationData, BeaconBlock, bytes32, Deposit, Shard, Slot, Eth1Data} from "../../types"; - -import {getEmptyBlock} from "../../chain/genesis"; - -import {API} from "./interface"; - -export interface MockAPIOpts { - head?: BeaconBlock; - pendingAttestations?: Attestation[]; - getPendingDeposits?: Deposit[]; - Eth1Data?: Eth1Data; - attestationData?: AttestationData; -} - -export class MockAPI implements API { - private head; - private attestations; - public constructor(opts?: MockAPIOpts) { - this.attestations = opts && opts.pendingAttestations || []; - this.head = opts && opts.head || getEmptyBlock(); - } - public async getChainHead(): Promise { - return this.head; - } - - public async getPendingAttestations(): Promise { - return this.attestations; - } - - public async getPendingDeposits(): Promise { - return []; - } - - public async getEth1Data(): Promise { - // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion - return {} as Eth1Data; - } - - public async computeStateRoot(block: BeaconBlock): Promise { - return Buffer.alloc(32); - } - - public async getAttestationData(slot: Slot, shard: Shard): Promise { - // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion - return {} as AttestationData; - } - - public async putAttestation(attestation: Attestation): Promise { - this.attestations.push(attestation); - } - - public async putBlock(block: BeaconBlock): Promise { - this.head = block; - } -} diff --git a/src/rpc/api/validator/index.ts b/src/rpc/api/validator/index.ts new file mode 100644 index 000000000000..3e45c3457afc --- /dev/null +++ b/src/rpc/api/validator/index.ts @@ -0,0 +1,7 @@ +import {ValidatorApi} from "./validator"; +import {IValidatorApi} from "./interface"; + +export { + ValidatorApi, + IValidatorApi +}; diff --git a/src/rpc/api/validator/interface.ts b/src/rpc/api/validator/interface.ts new file mode 100644 index 000000000000..eb59abe8aed8 --- /dev/null +++ b/src/rpc/api/validator/interface.ts @@ -0,0 +1,88 @@ +/** + * The API interface defines the calls that can be made from a Validator + */ +import { + Attestation, AttestationData, + BeaconBlock, bytes, bytes32, bytes48, Epoch, Fork, IndexedAttestation, number64, Shard, Slot, SyncingStatus, uint64, + ValidatorDuty, ValidatorIndex +} from "../../../types/index"; +import {IApi} from "../interface"; +import {CommitteeAssignment} from "../../../validator/types"; + +export interface IValidatorApi extends IApi { + /** + * Requests that the BeaconNode identify information about its implementation in a format similar to a HTTP User-Agent field. + * @returns {Promise} An ASCII-encoded hex string which uniquely defines the implementation of the BeaconNode and its current software version. + */ + getClientVersion(): Promise; + + /** + * Requests the BeaconNode to provide which fork version it is currently on. + * @returns {Promise<{fork: Fork; chainId: uint64}>} + */ + getFork(): Promise; + + /** + * Requests the genesis_time parameter from the BeaconNode, which should be consistent across all BeaconNodes that follow the same beacon chain. + * @returns {Promise} The genesis_time, which is a fairly static configuration option for the BeaconNode. + */ + getGenesisTime(): Promise; + + /** + * Requests the BeaconNode to describe if it's currently syncing or not, and if it is, what block it is up to. This is modelled after the Eth1.0 JSON-RPC eth_syncing call. + * @returns {Promise} Either false if the node is not syncing, or a SyncingStatus object if it is. + */ + getSyncingStatus(): Promise; + + /** + * Requests the BeaconNode to provide a set of “duties”, which are actions that should be performed by ValidatorClients. This API call should be polled at every slot, to ensure that any chain reorganisations are catered for, and to ensure that the currently connected BeaconNode is properly synchronised. + * @param {bytes48[]} validatorPubkey + * @returns {Promise<{currentVersion: bytes4; validatorDuty: ValidatorDuty}>} A list of unique validator public keys, where each item is a 0x encoded hex string. + */ + getDuties(validatorPubkey: bytes48): Promise<{currentVersion: Fork; validatorDuty: ValidatorDuty}>; + + /** + * Requests to check if a validator should propose for a given slot. + * @param {bytes48} validatorPubkey + * @param {Slot} slot + * @returns {Promise<{slot: Slot, proposer: boolean}} + */ + isProposer(index: ValidatorIndex, slot: Slot): Promise; + + /** + * Requests a validators committeeAssignment, can be used for past, current and one epoch in the future + * @param {ValidatorIndex} index + * @param {Epoch} epoch + */ + getCommitteeAssignment(index: ValidatorIndex, epoch: Epoch): Promise; + + /** + * Requests a BeaconNode to produce a valid block, which can then be signed by a ValidatorClient. + * @param {Slot} slot + * @param {bytes} randaoReveal + * @returns {Promise} A proposed BeaconBlock object, but with the signature field left blank. + */ + produceBlock(slot: Slot, randaoReveal: bytes): Promise; + + /** + * Requests that the BeaconNode produce an IndexedAttestation, with a blank signature field, which the ValidatorClient will then sign. + * @param {Slot} slot + * @param {Shard} shard + * @returns {Promise} + */ + produceAttestation(slot: Slot, shard: Shard): Promise; + + /** + * Instructs the BeaconNode to publish a newly signed beacon block to the beacon network, to be included in the beacon chain. + * @param {BeaconBlock} beaconBlock + * @returns {Promise} + */ + publishBlock(beaconBlock: BeaconBlock): Promise; + + /** + * Instructs the BeaconNode to publish a newly signed IndexedAttestation object, to be incorporated into the beacon chain. + * @param {Attestation} attestation + * @returns {Promise} + */ + publishAttestation(attestation: Attestation): Promise; +} diff --git a/src/rpc/api/validator/validator.ts b/src/rpc/api/validator/validator.ts new file mode 100644 index 000000000000..1bc94c8d573d --- /dev/null +++ b/src/rpc/api/validator/validator.ts @@ -0,0 +1,76 @@ +import { + Attestation, AttestationData, BeaconBlock, bytes32, Deposit, Shard, Slot, Eth1Data, uint64, + Fork, SyncingStatus, ValidatorDuty, bytes48, bytes, IndexedAttestation, number64, BeaconState, ValidatorIndex, Epoch +} from "../../../types"; +import {DB} from "../../../db"; +import {BeaconChain} from "../../../chain"; +import {OpPool} from "../../../opPool"; + +import {IValidatorApi} from "./interface"; +import {getCommitteeAssignment, isProposerAtSlot} from "../../../chain/stateTransition/util"; +import {CommitteeAssignment} from "../../../validator/types"; + +export class ValidatorApi implements IValidatorApi { + public namespace: string; + private chain: BeaconChain; + private db: DB; + private opPool: OpPool; + + public constructor(opts, {chain, db, opPool}) { + this.namespace = "validator"; + this.chain = chain; + this.db = db; + this.opPool = opPool; + } + + public async getClientVersion(): Promise { + return Buffer.alloc(32); + } + + public async getFork(): Promise { + const state: BeaconState = await this.db.getState(); + return state.fork; + } + + public async getGenesisTime(): Promise { + return await this.chain.genesisTime; + } + + public async getSyncingStatus(): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as boolean | SyncingStatus; + } + + public async getDuties(validatorPubkey: bytes48): Promise<{currentVersion: Fork; validatorDuty: ValidatorDuty}> { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as {currentVersion: Fork; validatorDuty: ValidatorDuty}; + } + + public async produceBlock(slot: Slot, randaoReveal: bytes): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as BeaconBlock; + } + + public async isProposer(index: ValidatorIndex, slot: Slot): Promise { + const state: BeaconState = await this.db.getState(); + return isProposerAtSlot(state, slot, index); + } + + public async getCommitteeAssignment(index: ValidatorIndex, epoch: Epoch): Promise { + const state: BeaconState = await this.db.getState(); + return getCommitteeAssignment(state, epoch, index); + } + + public async produceAttestation(slot: Slot, shard: Shard): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as AttestationData; + } + + public async publishBlock(block: BeaconBlock): Promise { + await this.chain.receiveBlock(block); + } + + public async publishAttestation(attestation: Attestation): Promise { + await this.opPool.receiveAttestation(attestation); + } +} diff --git a/src/rpc/protocol/jsonRpc.ts b/src/rpc/protocol/jsonRpc.ts index ca0c8a508938..3b18918b5e07 100644 --- a/src/rpc/protocol/jsonRpc.ts +++ b/src/rpc/protocol/jsonRpc.ts @@ -1,7 +1,7 @@ import * as jsonRpc from "noice-json-rpc"; -import {API} from "../api"; +import {IValidatorApi} from "../api"; export interface LikeSocketServer extends jsonRpc.LikeSocketServer { start(): Promise; @@ -10,15 +10,13 @@ export interface LikeSocketServer extends jsonRpc.LikeSocketServer { /** * JSON-RPC over some transport - * - * */ export class JSONRPC { private rpcServer: jsonRpc.Server; private transport: LikeSocketServer; private jsonRpcApi; - public constructor(opts, {transport, api}: {transport: LikeSocketServer; api: API}) { + public constructor(opts, {transport, api}: {transport: LikeSocketServer; api: IValidatorApi}) { this.transport = transport; // attach the json-rpc server to underlying transport this.rpcServer = new jsonRpc.Server(this.transport); @@ -30,11 +28,13 @@ export class JSONRPC { methods[name] = api[name].bind(api); } } - this.jsonRpcApi.BeaconChain.expose(methods); + this.jsonRpcApi[api.namespace].expose(methods); } + public async start(): Promise { await this.transport.start(); } + public async stop(): Promise { await this.transport.stop(); } diff --git a/src/types/index.ts b/src/types/index.ts index 60e0f619812c..ccc3f40e58cd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,3 +25,4 @@ export * from "./misc"; export * from "./operations"; export * from "./block"; export * from "./state"; +export * from "./validator"; diff --git a/src/types/validator.ts b/src/types/validator.ts new file mode 100644 index 000000000000..b8d71df268d6 --- /dev/null +++ b/src/types/validator.ts @@ -0,0 +1,42 @@ +import {bytes48, Shard, Slot, uint64} from "./primitive"; +import {SimpleContainerType} from "@chainsafe/ssz"; + +export interface ValidatorDuty { + // The validator's public key, uniquely identifying them + validatorPubkey: bytes48; + // The index of the validator in the committee + committeeIndex: uint64; + // The slot at which the validator must attest + attestationSlot: Slot; + // The shard in which the validator must attest + attestationShard: Shard; + // The slot in which a validator must propose a block, this field can be Null + blockProductionSlot: Slot; +} +export const ValidatorDuty: SimpleContainerType = { + name: "ValidatorDuty", + fields: [ + ["validatorPubkey", bytes48], + ["committeeIndex", uint64], + ["attestationSlot", Slot], + ["attestationShard", Shard], + ["blockProductionSlot", Slot], + ], +}; + +export interface SyncingStatus { + // The block at which syncing started (will only be reset, after the sync reached his head) + startingBlock: uint64; + // Current Block + currentBlock: uint64; + // The estimated highest block, or current target block number + highestBlock: uint64; +} +export const SyncingStatus: SimpleContainerType = { + name: "SyncingStatus", + fields: [ + ["startingBlock", uint64], + ["currentBlock", uint64], + ["highestBlock", uint64], + ], +}; diff --git a/src/validator/utils/assignments.ts b/src/validator/utils/assignments.ts deleted file mode 100644 index 2bca2e1b80c2..000000000000 --- a/src/validator/utils/assignments.ts +++ /dev/null @@ -1,76 +0,0 @@ -import assert from "assert"; - -import { - BeaconState, Epoch, Slot, - ValidatorIndex, -} from "../../types"; - -import { - SLOTS_PER_EPOCH, -} from "../../constants"; - -import { - getBeaconProposerIndex, getCrosslinkCommitteesAtSlot, - getCurrentEpoch, - getPreviousEpoch, - getEpochStartSlot, - slotToEpoch, -} from "../../chain/stateTransition/util"; - -import {CommitteeAssignment} from "../types"; - - -/** - * Return the committee assignment in the ``epoch`` for ``validator_index`` and ``registry_change``. - * ``assignment`` returned is a tuple of the following form: - * ``assignment[0]`` is the list of validators in the committee - * ``assignment[1]`` is the shard to which the committee is assigned - * ``assignment[2]`` is the slot at which the committee is assigned - * a beacon block at the assigned slot. - * @param {BeaconState} state - * @param {Epoch} epoch - * @param {ValidatorIndex} validatorIndex - * @param {boolean} registryChange - * @returns {{validators: ValidatorIndex[]; shard: Shard; slot: number; isProposer: boolean}} - */ -export function getCommitteeAssignment( - state: BeaconState, - epoch: Epoch, - validatorIndex: ValidatorIndex): CommitteeAssignment { - - const previousEpoch = getPreviousEpoch(state); - const nextEpoch = getCurrentEpoch(state) + 1; - assert(previousEpoch <= epoch && epoch <= nextEpoch); - - const epochStartSlot = getEpochStartSlot(epoch); - const loopEnd = epochStartSlot + SLOTS_PER_EPOCH; - - for (let slot = epochStartSlot; slot < loopEnd; slot++) { - const crosslinkCommittees = getCrosslinkCommitteesAtSlot(state, slot); - const selectedCommittees = crosslinkCommittees.map((committee) => committee[0].includes(validatorIndex)); - - if (selectedCommittees.length > 0) { - const validators = selectedCommittees[0][0]; - const shard = selectedCommittees[0][1]; - return {validators, shard, slot}; - } - } -} - -/** - * Checks if a validator is supposed to propose a block - * @param {BeaconState} state - * @param {Slot} slot - * @param {ValidatorIndex} validatorIndex - * @returns {Boolean} - */ -export function isProposerAtSlot( - state: BeaconState, - slot: Slot, - validatorIndex: ValidatorIndex): boolean { - - const currentEpoch = getCurrentEpoch(state); - assert(slotToEpoch(slot) === currentEpoch); - - return getBeaconProposerIndex(state) === validatorIndex; -} diff --git a/test/unit/rpc/api.test.ts b/test/unit/rpc/api.test.ts index 2b405533fe9f..0718c0b5a9ad 100644 --- a/test/unit/rpc/api.test.ts +++ b/test/unit/rpc/api.test.ts @@ -9,7 +9,7 @@ import { import {BeaconChain} from "../../../src/chain"; import {OpPool} from "../../../src/opPool"; import {LevelDB} from "../../../src/db"; -import {BeaconAPI} from "../../../src/rpc"; +// import {BeaconAPI} from "../../../src/rpc"; import { generateEmptyBlock } from "../../utils/block"; import { generateEmptyAttestation } from "../../utils/attestation"; diff --git a/test/unit/rpc/jsonRpcOverWs.test.ts b/test/unit/rpc/jsonRpcOverWs.test.ts deleted file mode 100644 index b83f081c5c7f..000000000000 --- a/test/unit/rpc/jsonRpcOverWs.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { assert } from "chai"; -import * as jsonRpc from "noice-json-rpc"; -import Websocket from "ws"; -import {MockAPI, JSONRPC, API, WSServer} from "../../../src/rpc"; -import { generateEmptyBlock } from "../../utils/block"; -import { generateEmptyAttestation } from "../../utils/attestation"; - -describe("Json RPC over WS", () => { - const rpc = new JSONRPC({}, {transport: new WSServer({port: 32420}), api: new MockAPI()}); - let client; - let ws; - let clientApi: {BeaconChain: API}; - before(async () => { - await rpc.start(); - ws = new Websocket("ws://localhost:32420"); - client = new jsonRpc.Client(ws); - clientApi = client.api(); - }) - after(async () => { - await rpc.stop(); - }) - it("should get the chain head", async () => { - const head = await clientApi.BeaconChain.getChainHead(); - assert.ok(head); - }) - it("should get pending attestations", async () => { - const attestations = await clientApi.BeaconChain.getPendingAttestations(); - assert.ok(attestations); - }) - it("should get pending deposits", async () => { - const deposits = await clientApi.BeaconChain.getPendingDeposits(); - assert.ok(deposits); - }) - it("should get eth1 data", async () => { - const eth1Data = await clientApi.BeaconChain.getEth1Data(); - assert.ok(eth1Data); - }) - it("should compute the state root", async () => { - const root = await clientApi.BeaconChain.computeStateRoot(generateEmptyBlock()); - assert.ok(root); - }) - it("should get attestation data", async () => { - const data = await clientApi.BeaconChain.getAttestationData(0, 0); - assert.ok(data); - }) - it("should accept an attestation submission", async () => { - await clientApi.BeaconChain.putAttestation(generateEmptyAttestation()); - assert.ok(true); - }) - it("should accept a block submission", async () => { - await clientApi.BeaconChain.putBlock(generateEmptyBlock()); - assert.ok(true); - }) - it("should fail for unknown methods", async () => { - try { - await (clientApi.BeaconChain as any).foo(); - assert.fail('Unknown/undefined method should fail'); - } catch (e) {} - }) - -}); diff --git a/test/unit/rpc/jsonRpcOverHttp.test.ts b/test/unit/rpc/validator/jsonRpcOverHttp.test.ts similarity index 78% rename from test/unit/rpc/jsonRpcOverHttp.test.ts rename to test/unit/rpc/validator/jsonRpcOverHttp.test.ts index 1aa4750083e7..28bb95f93e1a 100644 --- a/test/unit/rpc/jsonRpcOverHttp.test.ts +++ b/test/unit/rpc/validator/jsonRpcOverHttp.test.ts @@ -1,9 +1,10 @@ import {assert} from "chai"; import * as request from "supertest"; -import {JSONRPC, MockAPI} from "../../../src/rpc"; -import HttpServer from "../../../src/rpc/transport/http"; -import {generateRPCCall} from "../../utils/rpcCall"; -import logger from "../../../src/logger/winston"; +import {JSONRPC} from "../../../../src/rpc/index"; +import {MockValidatorApi} from "../../../utils/mocks/rpc/validator"; +import HttpServer from "../../../../src/rpc/transport/http"; +import {generateRPCCall} from "../../../utils/rpcCall"; +import logger from "../../../../src/logger/winston"; describe("Json RPC over http", () => { let rpc; @@ -12,17 +13,17 @@ describe("Json RPC over http", () => { logger.silent(true); const rpcServer = new HttpServer({port: 32421}); server = rpcServer.server; - rpc = new JSONRPC({}, {transport: rpcServer, api: new MockAPI()}) + rpc = new JSONRPC({}, {transport: rpcServer, api: new MockValidatorApi()}); await rpc.start(); }); after(async () => { await rpc.stop(); logger.silent(false); }); - it("should get the chain head", (done) => { + it("should get the version", (done) => { request.default(server) .post('/') - .send(generateRPCCall('BeaconChain.getChainHead', [])) + .send(generateRPCCall('validator.getFork', [])) .set('Accept', 'application/json') .expect('Content-Type', /json/) .expect(200) @@ -33,11 +34,11 @@ describe("Json RPC over http", () => { } done(); }); - }) + }); it("should fail for unknown methods", (done) => { request.default(server) .post('/') - .send(generateRPCCall('BeaconChain.notExistingMethod', [])) + .send(generateRPCCall('validator.notExistingMethod', [])) .set('Accept', 'application/json') .expect('Content-Type', /json/) .expect(200) @@ -49,7 +50,7 @@ describe("Json RPC over http", () => { assert.fail('Should not be successfull'); done(); }); - }) + }); it("should fail for methods other than POST", (done) => { request.default(server) .get('/') @@ -58,9 +59,9 @@ describe("Json RPC over http", () => { .end((err) => { done(err); }); - }) + }); it("should fail to start on existing port", (done) => { - const rpc = new JSONRPC({}, {transport: new HttpServer({port: 32421}), api: new MockAPI()}); + const rpc = new JSONRPC({}, {transport: new HttpServer({port: 32421}), api: new MockValidatorApi()}); rpc.start() .then(async () => { await rpc.stop(); diff --git a/test/unit/rpc/validator/jsonRpcOverWs.test.ts b/test/unit/rpc/validator/jsonRpcOverWs.test.ts new file mode 100644 index 000000000000..152bdc1a6e68 --- /dev/null +++ b/test/unit/rpc/validator/jsonRpcOverWs.test.ts @@ -0,0 +1,65 @@ +import { assert } from "chai"; +import * as jsonRpc from "noice-json-rpc"; +import Websocket from "ws"; +import {JSONRPC, IValidatorApi, WSServer} from "../../../../src/rpc/index"; +import { generateEmptyBlock } from "../../../utils/block"; +import {MockValidatorApi} from "../../../utils/mocks/rpc/validator"; +import { generateEmptyAttestation } from "../../../utils/attestation"; + +describe("Json RPC over WS", () => { + const rpc = new JSONRPC({}, {transport: new WSServer({port: 32420}), api: new MockValidatorApi()}); + let client; + let ws; + let clientApi: {validator: IValidatorApi}; + before(async () => { + await rpc.start(); + ws = new Websocket("ws://localhost:32420"); + client = new jsonRpc.Client(ws); + clientApi = client.api(); + }); + after(async () => { + await rpc.stop(); + }); + it("should get the client version", async () => { + const version = await clientApi.validator.getClientVersion(); + assert.ok(version); + }); + it("should get the fork version", async () => { + const fork = await clientApi.validator.getFork(); + assert.ok(fork); + }); + it("should get the genesis time", async () => { + const time = await clientApi.validator.getGenesisTime(); + assert.ok(time); + }); + it("should get the sync status", async () => { + const status = await clientApi.validator.getSyncingStatus(); + assert.ok(status); + }); + it("should get validator duties", async () => { + const duties = await clientApi.validator.getDuties(Buffer.alloc(48)); + assert.ok(duties); + }); + it("should produce a block for the validator", async () => { + const block = await clientApi.validator.produceBlock(0, Buffer.alloc(0)); + assert.ok(block); + }); + it("should produce an attestation", async () => { + await clientApi.validator.produceAttestation(0,1); + assert.ok(true); + }); + it("should accept an attestation submission", async () => { + await clientApi.validator.publishAttestation(generateEmptyAttestation()); + assert.ok(true); + }); + it("should accept a block submission", async () => { + await clientApi.validator.publishBlock(generateEmptyBlock()); + assert.ok(true); + }); + it("should fail for unknown methods", async () => { + try { + await (clientApi.validator as any).foo(); + assert.fail('Unknown/undefined method should fail'); + } catch (e) {} + }) +}); diff --git a/test/utils/mocks/rpc/validator.ts b/test/utils/mocks/rpc/validator.ts new file mode 100644 index 000000000000..4b0bd1b65943 --- /dev/null +++ b/test/utils/mocks/rpc/validator.ts @@ -0,0 +1,89 @@ +import { + Attestation, AttestationData, BeaconBlock, bytes32, Deposit, Shard, Slot, Eth1Data, + BeaconState, ValidatorIndex, Epoch +} from "../../../../src/types"; + +import {getEmptyBlock} from "../../../../src/chain/genesis"; + +import {IValidatorApi} from "../../../../src/rpc/api/validator"; +import {bytes, bytes48, Fork, number64, SyncingStatus, uint64, ValidatorDuty} from "../../../../src/types"; +import {getCommitteeAssignment, isProposerAtSlot} from "../../../../src/chain/stateTransition/util"; +import {CommitteeAssignment} from "../../../../src/validator/types"; + +export interface MockAPIOpts { + head?: BeaconBlock; + version?: bytes32; + fork?: Fork; + chainId?: number64; + pendingAttestations?: Attestation[]; + getPendingDeposits?: Deposit[]; + Eth1Data?: Eth1Data; + attestationData?: AttestationData; +} + +export class MockValidatorApi implements IValidatorApi { + public namespace: string; + private version: bytes32; + private fork: Fork; + private chainId: number64; + private attestations; + private head: BeaconBlock; + public constructor(opts?: MockAPIOpts) { + this.namespace = "validator"; + this.attestations = opts && opts.pendingAttestations || []; + this.head = opts && opts.head || getEmptyBlock(); + this.version = opts && opts.version || Buffer.alloc(0); + this.fork = opts && opts.fork || {previousVersion: Buffer.alloc(0), currentVersion: Buffer.alloc(0), epoch: 0} + this.chainId = opts && opts.chainId || 0; + } + + public async getClientVersion(): Promise { + return this.version; + } + + public async getFork(): Promise { + return this.fork; + } + + public async getGenesisTime(): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as number64; + } + + public async getSyncingStatus(): Promise { + return false; + } + + public async getDuties(validatorPubkey: bytes48): Promise<{currentVersion: Fork; validatorDuty: ValidatorDuty}> { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as {currentVersion: Fork; validatorDuty: ValidatorDuty}; + } + + public async produceBlock(slot: Slot, randaoReveal: bytes): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as BeaconBlock; + } + + public async produceAttestation(slot: Slot, shard: Shard): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as AttestationData; + } + + public async isProposer(index: ValidatorIndex, slot: Slot): Promise { + return true; + } + + public async getCommitteeAssignment(index: ValidatorIndex, epoch: Epoch): Promise { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + return {} as CommitteeAssignment; + } + + public async publishBlock(block: BeaconBlock): Promise { + this.head = block; + } + + public async publishAttestation(attestation: Attestation): Promise { + this.attestations.push(Attestation); + } + +}