From de1e1210b46adec73efca5df1bfc9f6e3d6bc952 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 27 May 2021 19:17:49 +0200 Subject: [PATCH] Separate lodestar-api package (#2568) * Refactor REST API definitions * Simplify REST testing * Bump to fastify 3.x.x * Return {} in fastify handler to trigger send * Move REST server to lodestar-api * Improve REST tests * Add eventstream test * Clean up * Bump versions * Fix query string format * Add extra debug routes * Consume lodestar-api package * Fix tests * Revert package.json change * Add HttpClient test * Destroy all active requests immediately on close * Fastify hook handlers must resolve * Fix fastify hook config args * Fix parsing of ValidatorId * Remove e2e script test * Add docs * Simplify req declarations * Review PR * Update license --- packages/api/.babel-register | 15 + packages/api/.babelrc | 3 + packages/api/.gitignore | 10 + packages/api/.mocharc.yaml | 4 + packages/api/.nycrc.json | 3 + packages/api/LICENSE | 201 ++++++++ packages/api/README.md | 52 ++ packages/api/package.json | 64 +++ packages/api/server.d.ts | 1 + packages/api/server.js | 2 + packages/api/src/client/beacon.ts | 13 + packages/api/src/client/config.ts | 13 + packages/api/src/client/debug.ts | 13 + packages/api/src/client/events.ts | 56 +++ packages/api/src/client/index.ts | 30 ++ packages/api/src/client/lightclient.ts | 27 ++ packages/api/src/client/lodestar.ts | 13 + packages/api/src/client/node.ts | 13 + packages/api/src/client/utils/client.ts | 78 +++ packages/api/src/client/utils/httpClient.ts | 145 ++++++ packages/api/src/client/utils/index.ts | 2 + packages/api/src/client/validator.ts | 13 + packages/api/src/index.ts | 5 + packages/api/src/interface.ts | 19 + packages/api/src/routes/beacon/block.ts | 167 +++++++ packages/api/src/routes/beacon/index.ts | 63 +++ packages/api/src/routes/beacon/pool.ts | 161 +++++++ packages/api/src/routes/beacon/state.ts | 279 +++++++++++ packages/api/src/routes/config.ts | 69 +++ packages/api/src/routes/debug.ts | 117 +++++ packages/api/src/routes/events.ts | 139 ++++++ packages/api/src/routes/index.ts | 47 ++ packages/api/src/routes/lightclient.ts | 74 +++ packages/api/src/routes/lodestar.ts | 48 ++ packages/api/src/routes/node.ts | 179 +++++++ packages/api/src/routes/validator.ts | 360 ++++++++++++++ packages/api/src/server/beacon.ts | 8 + packages/api/src/server/config.ts | 8 + packages/api/src/server/debug.ts | 49 ++ packages/api/src/server/events.ts | 66 +++ packages/api/src/server/index.ts | 69 +++ packages/api/src/server/lightclient.ts | 28 ++ packages/api/src/server/lodestar.ts | 8 + packages/api/src/server/node.ts | 8 + packages/api/src/server/utils/index.ts | 1 + packages/api/src/server/utils/server.ts | 79 +++ packages/api/src/server/validator.ts | 8 + .../{types => api}/src/utils/StringType.ts | 2 + packages/api/src/utils/index.ts | 4 + packages/api/src/utils/schema.ts | 116 +++++ packages/api/src/utils/types.ts | 150 ++++++ packages/api/src/utils/urlFormat.ts | 74 +++ packages/api/test/parser.test.ts | 32 ++ packages/api/test/unit/beacon.test.ts | 175 +++++++ packages/api/test/unit/config.test.ts | 28 ++ packages/api/test/unit/debug.test.ts | 67 +++ packages/api/test/unit/events.test.ts | 80 +++ packages/api/test/unit/lightclient.test.ts | 43 ++ packages/api/test/unit/node.test.ts | 62 +++ .../api/test/unit/utils/httpClient.test.ts | 154 ++++++ packages/api/test/unit/validator.test.ts | 93 ++++ packages/api/test/utils/genericServerTest.ts | 56 +++ packages/api/test/utils/utils.ts | 42 ++ packages/api/tsconfig.build.json | 7 + packages/api/tsconfig.json | 4 + .../src/allForks/util/epochContext.ts | 23 +- packages/cli/package.json | 1 + .../validator/slashingProtection/utils.ts | 7 +- packages/cli/src/cmds/dev/handler.ts | 4 +- packages/cli/src/cmds/validator/handler.ts | 4 +- packages/cli/test/e2e/cmds/init.test.ts | 6 +- packages/light-client/LICENSE | 302 +++++++----- packages/light-client/README.md | 4 +- packages/light-client/package.json | 3 +- packages/light-client/src/client/apiClient.ts | 76 --- packages/light-client/src/client/index.ts | 25 +- .../light-client/test/lightclientApiServer.ts | 188 ++------ .../test/lightclientMockServer.ts | 4 +- packages/light-client/tsconfig.build.json | 3 +- packages/light-client/tsconfig.json | 4 +- packages/lodestar/package.json | 10 +- packages/lodestar/src/api/impl/api.ts | 51 +- .../lodestar/src/api/impl/beacon/beacon.ts | 53 -- .../src/api/impl/beacon/blocks/index.ts | 246 +++++----- .../src/api/impl/beacon/blocks/interface.ts | 11 - .../src/api/impl/beacon/blocks/utils.ts | 10 +- .../lodestar/src/api/impl/beacon/index.ts | 35 +- .../lodestar/src/api/impl/beacon/interface.ts | 17 - .../src/api/impl/beacon/pool/index.ts | 136 +++++- .../src/api/impl/beacon/pool/interface.ts | 18 - .../lodestar/src/api/impl/beacon/pool/pool.ts | 144 ------ .../src/api/impl/beacon/state/index.ts | 186 ++++++- .../src/api/impl/beacon/state/interface.ts | 50 -- .../src/api/impl/beacon/state/state.ts | 190 -------- .../src/api/impl/beacon/state/utils.ts | 56 ++- .../lodestar/src/api/impl/config/config.ts | 30 -- .../lodestar/src/api/impl/config/index.ts | 26 +- .../lodestar/src/api/impl/config/interface.ts | 8 - .../src/api/impl/debug/beacon/index.ts | 32 -- .../src/api/impl/debug/beacon/interface.ts | 10 - packages/lodestar/src/api/impl/debug/debug.ts | 26 - packages/lodestar/src/api/impl/debug/index.ts | 44 +- .../lodestar/src/api/impl/debug/interface.ts | 10 - .../lodestar/src/api/impl/events/events.ts | 81 ---- .../lodestar/src/api/impl/events/handlers.ts | 102 ---- .../lodestar/src/api/impl/events/index.ts | 105 +++- .../src/api/impl/events/interfaces.ts | 9 - .../lodestar/src/api/impl/events/types.ts | 48 -- packages/lodestar/src/api/impl/index.ts | 2 +- packages/lodestar/src/api/impl/interface.ts | 51 -- .../src/api/impl/lightclient/index.ts | 89 ++-- .../lodestar/src/api/impl/lodestar/index.ts | 95 ++-- packages/lodestar/src/api/impl/node/index.ts | 82 +++- .../lodestar/src/api/impl/node/interface.ts | 19 - packages/lodestar/src/api/impl/node/node.ts | 72 --- packages/lodestar/src/api/impl/node/utils.ts | 10 +- packages/lodestar/src/api/impl/types.ts | 20 + packages/lodestar/src/api/impl/utils.ts | 24 - .../lodestar/src/api/impl/validator/index.ts | 425 +++++++++++++++- .../src/api/impl/validator/interface.ts | 35 -- .../src/api/impl/validator/validator.ts | 455 ------------------ packages/lodestar/src/api/options.ts | 6 +- .../src/api/rest/beacon/blocks/getBlock.ts | 38 -- .../beacon/blocks/getBlockAttestations.ts | 28 -- .../api/rest/beacon/blocks/getBlockHeader.ts | 26 - .../api/rest/beacon/blocks/getBlockHeaders.ts | 41 -- .../api/rest/beacon/blocks/getBlockRoot.ts | 28 -- .../src/api/rest/beacon/blocks/index.ts | 16 - .../api/rest/beacon/blocks/publishBlock.ts | 30 -- .../src/api/rest/beacon/getGenesis.ts | 14 - .../lodestar/src/api/rest/beacon/index.ts | 12 - .../rest/beacon/pool/getPoolAttestations.ts | 38 -- .../beacon/pool/getPoolAttesterSlashings.ts | 16 - .../beacon/pool/getPoolProposerSlashings.ts | 16 - .../rest/beacon/pool/getPoolVoluntaryExits.ts | 14 - .../src/api/rest/beacon/pool/index.ts | 21 - .../beacon/pool/submitPoolAttestations.ts | 32 -- .../pool/submitPoolAttesterSlashings.ts | 26 - .../pool/submitPoolProposerSlashings.ts | 26 - .../pool/submitPoolSyncCommitteeSignatures.ts | 27 -- .../beacon/pool/submitPoolVoluntaryExit.ts | 26 - .../rest/beacon/state/getEpochCommittees.ts | 51 -- .../beacon/state/getEpochSyncCommittees.ts | 42 -- .../state/getStateFinalityCheckpoints.ts | 34 -- .../src/api/rest/beacon/state/getStateFork.ts | 27 -- .../src/api/rest/beacon/state/getStateRoot.ts | 27 -- .../rest/beacon/state/getStateValidator.ts | 43 -- .../beacon/state/getStateValidatorBalances.ts | 51 -- .../rest/beacon/state/getStateValidators.ts | 60 --- .../src/api/rest/beacon/state/index.ts | 17 - .../src/api/rest/config/getDepositContract.ts | 14 - .../src/api/rest/config/getForkSchedule.ts | 14 - .../lodestar/src/api/rest/config/getSpec.ts | 15 - .../lodestar/src/api/rest/config/index.ts | 5 - .../src/api/rest/debug/connectToPeer.ts | 36 -- .../src/api/rest/debug/disconnectPeer.ts | 26 - .../src/api/rest/debug/getDebugChainHeads.ts | 14 - .../lodestar/src/api/rest/debug/getStates.ts | 47 -- packages/lodestar/src/api/rest/debug/index.ts | 6 - .../lodestar/src/api/rest/errorHandler.ts | 21 - .../src/api/rest/events/getEventStream.ts | 79 --- .../lodestar/src/api/rest/events/index.ts | 3 - packages/lodestar/src/api/rest/index.ts | 182 ++++--- packages/lodestar/src/api/rest/interface.ts | 27 -- .../src/api/rest/lightclient/index.ts | 103 ---- .../lodestar/src/api/rest/lodestar/index.ts | 33 -- packages/lodestar/src/api/rest/logger.ts | 44 -- .../lodestar/src/api/rest/node/getHealth.ts | 19 - .../src/api/rest/node/getNetworkIdentity.ts | 23 - .../src/api/rest/node/getNodeVersion.ts | 15 - .../lodestar/src/api/rest/node/getPeer.ts | 34 -- .../lodestar/src/api/rest/node/getPeers.ts | 52 -- .../src/api/rest/node/getSyncingStatus.ts | 14 - packages/lodestar/src/api/rest/node/index.ts | 8 - packages/lodestar/src/api/rest/options.ts | 18 - packages/lodestar/src/api/rest/routes.ts | 44 -- packages/lodestar/src/api/rest/types.ts | 34 -- packages/lodestar/src/api/rest/utils.ts | 28 -- .../validator/duties/getAttesterDuties.ts | 34 -- .../validator/duties/getProposerDuties.ts | 25 - .../duties/getSyncCommitteeDuties.ts | 34 -- .../validator/getAggregatedAttestation.ts | 41 -- .../lodestar/src/api/rest/validator/index.ts | 26 - .../validator/prepareBeaconCommitteeSubnet.ts | 49 -- .../validator/prepareSyncCommitteeSubnets.ts | 46 -- .../rest/validator/produceAttestationData.ts | 38 -- .../src/api/rest/validator/produceBlock.ts | 68 --- .../produceSyncCommitteeContribution.ts | 47 -- .../validator/publishAggregateAndProof.ts | 27 -- .../validator/publishContributionAndProofs.ts | 27 -- packages/lodestar/src/api/types/index.ts | 1 - packages/lodestar/src/api/types/node.ts | 19 - .../src/chain/factory/duties/index.ts | 5 +- packages/lodestar/src/network/interface.ts | 1 - packages/lodestar/src/node/nodejs.ts | 14 +- packages/lodestar/src/sync/interface.ts | 7 +- packages/lodestar/src/sync/sync.ts | 12 +- .../test/sim/singleNodeSingleThread.test.ts | 4 +- .../test/unit/api/impl/beacon/beacon.test.ts | 22 +- .../api/impl/beacon/blocks/getBlock.test.ts | 2 +- .../impl/beacon/blocks/getBlockHeader.test.ts | 2 - .../beacon/blocks/getBlockHeaders.test.ts | 24 +- .../impl/beacon/blocks/publishBlock.test.ts | 20 +- .../unit/api/impl/beacon/pool/pool.test.ts | 44 +- .../unit/api/impl/beacon/state/fork.test.ts | 20 +- .../unit/api/impl/beacon/state/state.test.ts | 32 +- .../impl/beacon/state/stateValidators.test.ts | 62 ++- .../unit/api/impl/beacon/state/utils.test.ts | 18 +- .../test/unit/api/impl/config/config.test.ts | 12 +- .../api/impl/debug/{beacon => }/index.test.ts | 27 +- .../test/unit/api/impl/events/events.test.ts | 128 ++--- .../lodestar/test/unit/api/impl/index.test.ts | 20 +- .../unit/api/impl/lodestar/lodestar.test.ts | 26 - .../test/unit/api/impl/node/node.test.ts | 52 +- .../impl/validator/duties/proposer.test.ts | 14 +- .../validator/produceAttestationData.test.ts | 10 +- .../api/rest/beacon/blocks/getBlock.test.ts | 34 -- .../blocks/getBlockAttestations.test.ts | 46 -- .../rest/beacon/blocks/getBlockHeader.test.ts | 32 -- .../beacon/blocks/getBlockHeaders.test.ts | 91 ---- .../rest/beacon/blocks/getBlockRoot.test.ts | 34 -- .../rest/beacon/blocks/publishBlock.test.ts | 38 -- .../unit/api/rest/beacon/getGenesis.test.ts | 36 -- .../beacon/pool/getPoolAttestations.test.ts | 23 - .../pool/getPoolAttesterSlashings.test.ts | 21 - .../pool/getPoolProposerSlashings.test.ts | 21 - .../beacon/pool/getPoolVoluntaryExits.test.ts | 21 - .../beacon/pool/submitPoolAttestation.test.ts | 42 -- .../pool/submitPoolAttesterSlashing.test.ts | 42 -- .../pool/submitPoolProposerSlashings.test.ts | 42 -- .../pool/submitPoolVoluntaryExit.test.ts | 42 -- .../beacon/state/getEpochCommittees.test.ts | 88 ---- .../state/getStateFinalityCheckpoints.test.ts | 30 -- .../rest/beacon/state/getStateFork.test.ts | 30 -- .../beacon/state/getStateValidator.test.ts | 64 --- .../beacon/state/getStateValidators.test.ts | 44 -- .../state/getStateValidatorsBalances.test.ts | 66 --- .../rest/config/getDepositContract.test.ts | 31 -- .../api/rest/config/getForkSchedule.test.ts | 20 - .../test/unit/api/rest/config/getSpec.test.ts | 20 - .../api/rest/debug/getDebugChainHeads.test.ts | 28 -- .../test/unit/api/rest/debug/getState.test.ts | 57 --- .../api/rest/events/getEventStream.test.ts | 55 --- .../lodestar/test/unit/api/rest/index.test.ts | 27 -- .../test/unit/api/rest/lodestar/index.test.ts | 21 - .../test/unit/api/rest/node/getHealth.test.ts | 31 -- .../api/rest/node/getNetworkIdentity.test.ts | 33 -- .../test/unit/api/rest/node/getPeer.test.ts | 36 -- .../test/unit/api/rest/node/getPeers.test.ts | 34 -- .../api/rest/node/getSyncingStatus.test.ts | 29 -- .../unit/api/rest/node/getVersion.test.ts | 24 - packages/lodestar/test/unit/api/rest/utils.ts | 11 - .../getAggregatedAttestation.test.ts | 47 -- .../rest/validator/getAttesterDuties.test.ts | 70 --- .../rest/validator/getProposerDuties.test.ts | 40 -- .../prepareBeaconCommitteeSubnet.test.ts | 59 --- .../validator/produceAttestationData.test.ts | 44 -- .../api/rest/validator/produceBlock.test.ts | 52 -- packages/lodestar/test/utils/api.ts | 24 - .../lodestar/test/utils/node/validator.ts | 19 +- packages/lodestar/test/utils/stub/api.ts | 37 -- .../lodestar/test/utils/stub/beaconApi.ts | 24 - .../lodestar/test/utils/stub/configApi.ts | 17 - .../lodestar/test/utils/stub/lodestarApi.ts | 19 - packages/lodestar/test/utils/stub/nodeApi.ts | 23 - packages/lodestar/types/fastify/index.d.ts | 12 - packages/types/src/altair/sszTypes.ts | 36 -- packages/types/src/altair/types/wire.ts | 35 +- packages/types/src/phase0/sszTypes.ts | 171 ------- packages/types/src/phase0/types/api.ts | 146 +----- packages/types/src/phase0/types/misc.ts | 5 - packages/utils/package.json | 1 - packages/utils/src/events/events.ts | 26 - packages/utils/src/events/index.ts | 1 - packages/utils/src/index.ts | 1 - packages/utils/src/objects.ts | 15 +- packages/utils/src/yaml/index.ts | 2 +- packages/validator/package.json | 8 +- packages/validator/src/api/index.ts | 3 - packages/validator/src/api/instance.ts | 17 - packages/validator/src/api/interface.ts | 92 ---- packages/validator/src/api/rest/beacon.ts | 79 --- packages/validator/src/api/rest/config.ts | 14 - packages/validator/src/api/rest/events.ts | 68 --- packages/validator/src/api/rest/index.ts | 30 -- packages/validator/src/api/rest/node.ts | 19 - packages/validator/src/api/rest/validator.ts | 101 ---- packages/validator/src/genesis.ts | 7 +- packages/validator/src/index.ts | 2 - packages/validator/src/options.ts | 14 - .../validator/src/services/attestation.ts | 17 +- .../src/services/attestationDuties.ts | 16 +- packages/validator/src/services/block.ts | 12 +- .../validator/src/services/blockDuties.ts | 10 +- packages/validator/src/services/fork.ts | 6 +- packages/validator/src/services/indices.ts | 9 +- .../validator/src/services/syncCommittee.ts | 28 +- .../src/services/syncCommitteeDuties.ts | 18 +- .../validator/src/services/validatorStore.ts | 9 +- packages/validator/src/util/aggregator.ts | 5 +- packages/validator/src/util/httpClient.ts | 89 ---- packages/validator/src/util/index.ts | 1 - packages/validator/src/validator.ts | 58 ++- packages/validator/src/voluntaryExit.ts | 19 +- .../test/unit/services/attestation.test.ts | 28 +- .../unit/services/attestationDuties.test.ts | 36 +- .../test/unit/services/block.test.ts | 25 +- .../test/unit/services/blockDuties.test.ts | 33 +- .../validator/test/unit/services/fork.test.ts | 14 +- .../unit/services/syncCommitteDuties.test.ts | 37 +- .../test/unit/services/syncCommittee.test.ts | 39 +- .../test/unit/utils/aggregator.test.ts | 4 +- .../test/unit/utils/httpClient.test.ts | 56 --- packages/validator/test/utils/apiStub.ts | 24 +- yarn.lock | 301 ++++++------ 315 files changed, 6354 insertions(+), 7911 deletions(-) create mode 100644 packages/api/.babel-register create mode 100644 packages/api/.babelrc create mode 100644 packages/api/.gitignore create mode 100644 packages/api/.mocharc.yaml create mode 100644 packages/api/.nycrc.json create mode 100644 packages/api/LICENSE create mode 100644 packages/api/README.md create mode 100644 packages/api/package.json create mode 100644 packages/api/server.d.ts create mode 100644 packages/api/server.js create mode 100644 packages/api/src/client/beacon.ts create mode 100644 packages/api/src/client/config.ts create mode 100644 packages/api/src/client/debug.ts create mode 100644 packages/api/src/client/events.ts create mode 100644 packages/api/src/client/index.ts create mode 100644 packages/api/src/client/lightclient.ts create mode 100644 packages/api/src/client/lodestar.ts create mode 100644 packages/api/src/client/node.ts create mode 100644 packages/api/src/client/utils/client.ts create mode 100644 packages/api/src/client/utils/httpClient.ts create mode 100644 packages/api/src/client/utils/index.ts create mode 100644 packages/api/src/client/validator.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/interface.ts create mode 100644 packages/api/src/routes/beacon/block.ts create mode 100644 packages/api/src/routes/beacon/index.ts create mode 100644 packages/api/src/routes/beacon/pool.ts create mode 100644 packages/api/src/routes/beacon/state.ts create mode 100644 packages/api/src/routes/config.ts create mode 100644 packages/api/src/routes/debug.ts create mode 100644 packages/api/src/routes/events.ts create mode 100644 packages/api/src/routes/index.ts create mode 100644 packages/api/src/routes/lightclient.ts create mode 100644 packages/api/src/routes/lodestar.ts create mode 100644 packages/api/src/routes/node.ts create mode 100644 packages/api/src/routes/validator.ts create mode 100644 packages/api/src/server/beacon.ts create mode 100644 packages/api/src/server/config.ts create mode 100644 packages/api/src/server/debug.ts create mode 100644 packages/api/src/server/events.ts create mode 100644 packages/api/src/server/index.ts create mode 100644 packages/api/src/server/lightclient.ts create mode 100644 packages/api/src/server/lodestar.ts create mode 100644 packages/api/src/server/node.ts create mode 100644 packages/api/src/server/utils/index.ts create mode 100644 packages/api/src/server/utils/server.ts create mode 100644 packages/api/src/server/validator.ts rename packages/{types => api}/src/utils/StringType.ts (93%) create mode 100644 packages/api/src/utils/index.ts create mode 100644 packages/api/src/utils/schema.ts create mode 100644 packages/api/src/utils/types.ts create mode 100644 packages/api/src/utils/urlFormat.ts create mode 100644 packages/api/test/parser.test.ts create mode 100644 packages/api/test/unit/beacon.test.ts create mode 100644 packages/api/test/unit/config.test.ts create mode 100644 packages/api/test/unit/debug.test.ts create mode 100644 packages/api/test/unit/events.test.ts create mode 100644 packages/api/test/unit/lightclient.test.ts create mode 100644 packages/api/test/unit/node.test.ts create mode 100644 packages/api/test/unit/utils/httpClient.test.ts create mode 100644 packages/api/test/unit/validator.test.ts create mode 100644 packages/api/test/utils/genericServerTest.ts create mode 100644 packages/api/test/utils/utils.ts create mode 100644 packages/api/tsconfig.build.json create mode 100644 packages/api/tsconfig.json delete mode 100644 packages/light-client/src/client/apiClient.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/beacon.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/blocks/interface.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/interface.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/pool/interface.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/pool/pool.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/state/interface.ts delete mode 100644 packages/lodestar/src/api/impl/beacon/state/state.ts delete mode 100644 packages/lodestar/src/api/impl/config/config.ts delete mode 100644 packages/lodestar/src/api/impl/config/interface.ts delete mode 100644 packages/lodestar/src/api/impl/debug/beacon/index.ts delete mode 100644 packages/lodestar/src/api/impl/debug/beacon/interface.ts delete mode 100644 packages/lodestar/src/api/impl/debug/debug.ts delete mode 100644 packages/lodestar/src/api/impl/debug/interface.ts delete mode 100644 packages/lodestar/src/api/impl/events/events.ts delete mode 100644 packages/lodestar/src/api/impl/events/handlers.ts delete mode 100644 packages/lodestar/src/api/impl/events/interfaces.ts delete mode 100644 packages/lodestar/src/api/impl/events/types.ts delete mode 100644 packages/lodestar/src/api/impl/interface.ts delete mode 100644 packages/lodestar/src/api/impl/node/interface.ts delete mode 100644 packages/lodestar/src/api/impl/node/node.ts create mode 100644 packages/lodestar/src/api/impl/types.ts delete mode 100644 packages/lodestar/src/api/impl/utils.ts delete mode 100644 packages/lodestar/src/api/impl/validator/interface.ts delete mode 100644 packages/lodestar/src/api/impl/validator/validator.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/getBlock.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/getBlockAttestations.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/getBlockHeader.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/getBlockHeaders.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/getBlockRoot.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/index.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/blocks/publishBlock.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/getGenesis.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/index.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/getPoolAttestations.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/getPoolAttesterSlashings.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/getPoolProposerSlashings.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/getPoolVoluntaryExits.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/index.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/submitPoolAttestations.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/submitPoolAttesterSlashings.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/submitPoolProposerSlashings.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/submitPoolSyncCommitteeSignatures.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/pool/submitPoolVoluntaryExit.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getEpochCommittees.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getEpochSyncCommittees.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getStateFinalityCheckpoints.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getStateFork.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getStateRoot.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getStateValidator.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getStateValidatorBalances.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/getStateValidators.ts delete mode 100644 packages/lodestar/src/api/rest/beacon/state/index.ts delete mode 100644 packages/lodestar/src/api/rest/config/getDepositContract.ts delete mode 100644 packages/lodestar/src/api/rest/config/getForkSchedule.ts delete mode 100644 packages/lodestar/src/api/rest/config/getSpec.ts delete mode 100644 packages/lodestar/src/api/rest/config/index.ts delete mode 100644 packages/lodestar/src/api/rest/debug/connectToPeer.ts delete mode 100644 packages/lodestar/src/api/rest/debug/disconnectPeer.ts delete mode 100644 packages/lodestar/src/api/rest/debug/getDebugChainHeads.ts delete mode 100644 packages/lodestar/src/api/rest/debug/getStates.ts delete mode 100644 packages/lodestar/src/api/rest/debug/index.ts delete mode 100644 packages/lodestar/src/api/rest/errorHandler.ts delete mode 100644 packages/lodestar/src/api/rest/events/getEventStream.ts delete mode 100644 packages/lodestar/src/api/rest/events/index.ts delete mode 100644 packages/lodestar/src/api/rest/interface.ts delete mode 100644 packages/lodestar/src/api/rest/lightclient/index.ts delete mode 100644 packages/lodestar/src/api/rest/lodestar/index.ts delete mode 100644 packages/lodestar/src/api/rest/logger.ts delete mode 100644 packages/lodestar/src/api/rest/node/getHealth.ts delete mode 100644 packages/lodestar/src/api/rest/node/getNetworkIdentity.ts delete mode 100644 packages/lodestar/src/api/rest/node/getNodeVersion.ts delete mode 100644 packages/lodestar/src/api/rest/node/getPeer.ts delete mode 100644 packages/lodestar/src/api/rest/node/getPeers.ts delete mode 100644 packages/lodestar/src/api/rest/node/getSyncingStatus.ts delete mode 100644 packages/lodestar/src/api/rest/node/index.ts delete mode 100644 packages/lodestar/src/api/rest/options.ts delete mode 100644 packages/lodestar/src/api/rest/routes.ts delete mode 100644 packages/lodestar/src/api/rest/types.ts delete mode 100644 packages/lodestar/src/api/rest/utils.ts delete mode 100644 packages/lodestar/src/api/rest/validator/duties/getAttesterDuties.ts delete mode 100644 packages/lodestar/src/api/rest/validator/duties/getProposerDuties.ts delete mode 100644 packages/lodestar/src/api/rest/validator/duties/getSyncCommitteeDuties.ts delete mode 100644 packages/lodestar/src/api/rest/validator/getAggregatedAttestation.ts delete mode 100644 packages/lodestar/src/api/rest/validator/index.ts delete mode 100644 packages/lodestar/src/api/rest/validator/prepareBeaconCommitteeSubnet.ts delete mode 100644 packages/lodestar/src/api/rest/validator/prepareSyncCommitteeSubnets.ts delete mode 100644 packages/lodestar/src/api/rest/validator/produceAttestationData.ts delete mode 100644 packages/lodestar/src/api/rest/validator/produceBlock.ts delete mode 100644 packages/lodestar/src/api/rest/validator/produceSyncCommitteeContribution.ts delete mode 100644 packages/lodestar/src/api/rest/validator/publishAggregateAndProof.ts delete mode 100644 packages/lodestar/src/api/rest/validator/publishContributionAndProofs.ts delete mode 100644 packages/lodestar/src/api/types/index.ts delete mode 100644 packages/lodestar/src/api/types/node.ts rename packages/lodestar/test/unit/api/impl/debug/{beacon => }/index.test.ts (58%) delete mode 100644 packages/lodestar/test/unit/api/impl/lodestar/lodestar.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/blocks/getBlock.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockAttestations.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeader.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeaders.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockRoot.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/blocks/publishBlock.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/getGenesis.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttestations.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttesterSlashings.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/getPoolProposerSlashings.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/getPoolVoluntaryExits.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttestation.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttesterSlashing.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolProposerSlashings.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolVoluntaryExit.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/state/getEpochCommittees.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/state/getStateFinalityCheckpoints.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/state/getStateFork.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/state/getStateValidator.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/state/getStateValidators.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/beacon/state/getStateValidatorsBalances.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/config/getDepositContract.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/config/getForkSchedule.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/config/getSpec.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/debug/getDebugChainHeads.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/debug/getState.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/events/getEventStream.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/index.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/lodestar/index.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/node/getHealth.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/node/getNetworkIdentity.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/node/getPeer.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/node/getPeers.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/node/getSyncingStatus.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/node/getVersion.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/utils.ts delete mode 100644 packages/lodestar/test/unit/api/rest/validator/getAggregatedAttestation.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/validator/getAttesterDuties.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/validator/getProposerDuties.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/validator/prepareBeaconCommitteeSubnet.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/validator/produceAttestationData.test.ts delete mode 100644 packages/lodestar/test/unit/api/rest/validator/produceBlock.test.ts delete mode 100644 packages/lodestar/test/utils/api.ts delete mode 100644 packages/lodestar/test/utils/stub/api.ts delete mode 100644 packages/lodestar/test/utils/stub/beaconApi.ts delete mode 100644 packages/lodestar/test/utils/stub/configApi.ts delete mode 100644 packages/lodestar/test/utils/stub/lodestarApi.ts delete mode 100644 packages/lodestar/test/utils/stub/nodeApi.ts delete mode 100644 packages/lodestar/types/fastify/index.d.ts delete mode 100644 packages/utils/src/events/events.ts delete mode 100644 packages/utils/src/events/index.ts delete mode 100644 packages/validator/src/api/index.ts delete mode 100644 packages/validator/src/api/instance.ts delete mode 100644 packages/validator/src/api/interface.ts delete mode 100644 packages/validator/src/api/rest/beacon.ts delete mode 100644 packages/validator/src/api/rest/config.ts delete mode 100644 packages/validator/src/api/rest/events.ts delete mode 100644 packages/validator/src/api/rest/index.ts delete mode 100644 packages/validator/src/api/rest/node.ts delete mode 100644 packages/validator/src/api/rest/validator.ts delete mode 100644 packages/validator/src/options.ts delete mode 100644 packages/validator/src/util/httpClient.ts delete mode 100644 packages/validator/test/unit/utils/httpClient.test.ts diff --git a/packages/api/.babel-register b/packages/api/.babel-register new file mode 100644 index 0000000000..35d91b6f38 --- /dev/null +++ b/packages/api/.babel-register @@ -0,0 +1,15 @@ +/* + See + https://github.com/babel/babel/issues/8652 + https://github.com/babel/babel/pull/6027 + Babel isn't currently configured by default to read .ts files and + can only be configured to do so via cli or configuration below. + + This file is used by mocha to interpret test files using a properly + configured babel. + + This can (probably) be removed in babel 8.x. +*/ +require('@babel/register')({ + extensions: ['.ts'], +}) diff --git a/packages/api/.babelrc b/packages/api/.babelrc new file mode 100644 index 0000000000..633f93f424 --- /dev/null +++ b/packages/api/.babelrc @@ -0,0 +1,3 @@ +{ + "extends": "../../.babelrc" +} diff --git a/packages/api/.gitignore b/packages/api/.gitignore new file mode 100644 index 0000000000..668e0a04c0 --- /dev/null +++ b/packages/api/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +lib +.nyc_output/ +coverage/** +.DS_Store +*.swp +.idea +yarn-error.log +package-lock.json +dist* diff --git a/packages/api/.mocharc.yaml b/packages/api/.mocharc.yaml new file mode 100644 index 0000000000..17f6dc2091 --- /dev/null +++ b/packages/api/.mocharc.yaml @@ -0,0 +1,4 @@ +colors: true +require: ts-node/register +timeout: 2000 +exit: true diff --git a/packages/api/.nycrc.json b/packages/api/.nycrc.json new file mode 100644 index 0000000000..69aa626339 --- /dev/null +++ b/packages/api/.nycrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.nycrc.json" +} diff --git a/packages/api/LICENSE b/packages/api/LICENSE new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/packages/api/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/api/README.md b/packages/api/README.md new file mode 100644 index 0000000000..19db3a74bb --- /dev/null +++ b/packages/api/README.md @@ -0,0 +1,52 @@ +# Lodestar ETH2.0 API + +[![](https://img.shields.io/travis/com/ChainSafe/lodestar/master.svg?label=master&logo=travis "Master Branch (Travis)")](https://travis-ci.com/ChainSafe/lodestar) +[![Discord](https://img.shields.io/discord/593655374469660673.svg?label=Discord&logo=discord)](https://discord.gg/aMxzVcr) +![ETH2.0_Spec_Version 0.12.1](https://img.shields.io/badge/ETH2.0_Spec_Version-0.12.1-2e86c1.svg) +![ES Version](https://img.shields.io/badge/ES-2020-yellow) +![Node Version](https://img.shields.io/badge/node-12.x-green) + +> This package is part of [ChainSafe's Lodestar](https://lodestar.chainsafe.io) project + +Typescript REST client for the [Eth2.0 API spec](https://ethereum.github.io/eth2.0-APIs/) + +## Usage + +```typescript +import {getClient} from "@chainsafe/lodestar-api"; +import {config} from "@chainsafe/lodestar-config/mainnet"; + +const api = getClient(config, { + baseUrl: "http://localhost:9596", +}); + +const res = await api.state.getStateValidator( + "head", + "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95" +); + +console.log("Your balance is:", res.data.balance); +``` + +## Prerequisites + +- [Lerna](https://github.com/lerna/lerna) +- [Yarn](https://yarnpkg.com/) + +## What you need + +You will need to go over the [specification](https://github.com/ethereum/eth2.0-specs). You will also need to have a [basic understanding of sharding](https://github.com/ethereum/wiki/wiki/Sharding-FAQs). + +## Getting started + +- Follow the [installation guide](https://chainsafe.github.io/lodestar/installation) to install Lodestar. +- Quickly try out the whole stack by [starting a local testnet](https://chainsafe.github.io/lodestar/usage). +- View the [typedoc code docs](https://chainsafe.github.io/lodestar/packages). + +## Contributors + +Read our [contributors document](/CONTRIBUTING.md), [submit an issue](https://github.com/ChainSafe/lodestar/issues/new/choose) or talk to us on our [discord](https://discord.gg/yjyvFRP)! + +## License + +Apache-2.0 [ChainSafe Systems](https://chainsafe.io) diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000000..a6a2f23917 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,64 @@ +{ + "name": "@chainsafe/lodestar-api", + "private": true, + "description": "A Typescript implementation of the eth2 light client", + "license": "Apache-2.0", + "author": "ChainSafe Systems", + "homepage": "https://github.com/ChainSafe/lodestar#readme", + "repository": { + "type": "git", + "url": "git+https://github.com:ChainSafe/lodestar.git" + }, + "bugs": { + "url": "https://github.com/ChainSafe/lodestar/issues" + }, + "version": "0.22.0", + "main": "lib/index.js", + "files": [ + "lib/**/*.d.ts", + "lib/**/*.js", + "lib/**/*.js.map" + ], + "scripts": { + "clean": "rm -rf lib && rm -f *.tsbuildinfo", + "build": "concurrently \"yarn build:lib\" \"yarn build:types\"", + "build:typedocs": "typedoc --exclude src/index.ts --out typedocs src", + "build:lib": "babel src -x .ts -d lib --source-maps", + "build:release": "yarn clean && yarn run build && yarn run build:typedocs", + "build:types": "tsc -p tsconfig.build.json", + "check-types": "tsc", + "coverage": "codecov -F lodestar-api", + "lint": "eslint --color --ext .ts src/ test/", + "lint:fix": "yarn run lint --fix", + "pretest": "yarn run check-types", + "test": "yarn test:unit && yarn test:e2e", + "test:unit": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'" + }, + "dependencies": { + "@chainsafe/lodestar-config": "^0.22.0", + "@chainsafe/lodestar-params": "^0.22.0", + "@chainsafe/lodestar-types": "^0.22.0", + "@chainsafe/lodestar-utils": "^0.22.0", + "@chainsafe/persistent-merkle-tree": "^0.3.2", + "@chainsafe/ssz": "^0.8.6", + "abort-controller": "^3.0.0", + "cross-fetch": "^3.1.4", + "eventsource": "^1.1.0", + "qs": "^6.10.1" + }, + "devDependencies": { + "@types/eventsource": "^1.1.5", + "@types/qs": "^6.9.6", + "fastify": "3.15.1" + }, + "peerDependencies": { + "fastify": "3.15.1" + }, + "keywords": [ + "ethereum", + "eth2", + "beacon", + "api", + "blockchain" + ] +} diff --git a/packages/api/server.d.ts b/packages/api/server.d.ts new file mode 100644 index 0000000000..445d71ff89 --- /dev/null +++ b/packages/api/server.d.ts @@ -0,0 +1 @@ +export * from "./lib/server"; diff --git a/packages/api/server.js b/packages/api/server.js new file mode 100644 index 0000000000..a993bf6331 --- /dev/null +++ b/packages/api/server.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports +module.exports = require("./lib/server"); diff --git a/packages/api/src/client/beacon.ts b/packages/api/src/client/beacon.ts new file mode 100644 index 0000000000..f878a7ea32 --- /dev/null +++ b/packages/api/src/client/beacon.ts @@ -0,0 +1,13 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IHttpClient, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/beacon"; + +/** + * REST HTTP client for beacon routes + */ +export function getClient(config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(config); + const returnTypes = getReturnTypes(config); + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); +} diff --git a/packages/api/src/client/config.ts b/packages/api/src/client/config.ts new file mode 100644 index 0000000000..2e673a2727 --- /dev/null +++ b/packages/api/src/client/config.ts @@ -0,0 +1,13 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IHttpClient, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/config"; + +/** + * REST HTTP client for config routes + */ +export function getClient(config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(); + const returnTypes = getReturnTypes(config); + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); +} diff --git a/packages/api/src/client/debug.ts b/packages/api/src/client/debug.ts new file mode 100644 index 0000000000..f70fac02a5 --- /dev/null +++ b/packages/api/src/client/debug.ts @@ -0,0 +1,13 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IHttpClient, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/debug"; + +/** + * REST HTTP client for debug routes + */ +export function getClient(config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(); + const returnTypes = getReturnTypes(config); + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); +} diff --git a/packages/api/src/client/events.ts b/packages/api/src/client/events.ts new file mode 100644 index 0000000000..cb433f1bff --- /dev/null +++ b/packages/api/src/client/events.ts @@ -0,0 +1,56 @@ +import EventSource from "eventsource"; +import qs from "qs"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {Api, BeaconEvent, routesData, getEventSerdes} from "../routes/events"; + +/** + * REST HTTP client for events routes + */ +export function getClient(config: IBeaconConfig, baseUrl: string): Api { + const eventSerdes = getEventSerdes(config); + + return { + eventstream: async (topics, signal, onEvent) => { + const query = qs.stringify({topics}); + // TODO: Use a proper URL formatter + const url = `${baseUrl}${routesData.eventstream.url}?${query}`; + const eventSource = new EventSource(url); + + try { + await new Promise((resolve, reject) => { + for (const topic of topics) { + eventSource.addEventListener(topic, ((event: MessageEvent) => { + const message = eventSerdes.fromJson(topic, JSON.parse(event.data)); + onEvent({type: topic, message} as BeaconEvent); + }) as EventListener); + } + + // EventSource will try to reconnect always on all errors + // `eventSource.onerror` events are informative but don't indicate the EventSource closed + // The only way to abort the connection from the client is via eventSource.close() + eventSource.onerror = function (err) { + const errEs = (err as unknown) as EventSourceError; + // Consider 400 and 500 status errors unrecoverable, close the eventsource + if (errEs.status === 400) { + reject(Error(`400 Invalid topics: ${errEs.message}`)); + } + if (errEs.status === 500) { + reject(Error(`500 Internal Server Error: ${errEs.message}`)); + } + + // TODO: else log the error somewhere + // console.log("eventstream client error", errEs); + }; + + // And abort resolve the promise so finally {} eventSource.close() + signal.addEventListener("abort", () => resolve(), {once: true}); + }); + } finally { + eventSource.close(); + } + }, + }; +} + +// https://github.com/EventSource/eventsource/blob/82e034389bd2c08d532c63172b8e858c5b185338/lib/eventsource.js#L143 +type EventSourceError = {status: number; message: string}; diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts new file mode 100644 index 0000000000..1f6e14914f --- /dev/null +++ b/packages/api/src/client/index.ts @@ -0,0 +1,30 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {Api} from "../interface"; +import {IHttpClient, HttpClient, HttpClientOptions} from "./utils"; + +import * as beacon from "./beacon"; +import * as configApi from "./config"; +import * as debug from "./debug"; +import * as events from "./events"; +import * as lightclient from "./lightclient"; +import * as lodestar from "./lodestar"; +import * as node from "./node"; +import * as validator from "./validator"; + +/** + * REST HTTP client for all routes + */ +export function getClient(config: IBeaconConfig, opts: HttpClientOptions, httpClient?: IHttpClient): Api { + if (!httpClient) httpClient = new HttpClient(opts); + + return { + beacon: beacon.getClient(config, httpClient), + config: configApi.getClient(config, httpClient), + debug: debug.getClient(config, httpClient), + events: events.getClient(config, httpClient.baseUrl), + lightclient: lightclient.getClient(config, httpClient), + lodestar: lodestar.getClient(config, httpClient), + node: node.getClient(config, httpClient), + validator: validator.getClient(config, httpClient), + }; +} diff --git a/packages/api/src/client/lightclient.ts b/packages/api/src/client/lightclient.ts new file mode 100644 index 0000000000..660224e307 --- /dev/null +++ b/packages/api/src/client/lightclient.ts @@ -0,0 +1,27 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {deserializeProof} from "@chainsafe/persistent-merkle-tree"; +import {IHttpClient, getFetchOptsSerializers, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/lightclient"; + +/** + * REST HTTP client for lightclient routes + */ +export function getClient(config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(); + const returnTypes = getReturnTypes(config); + + // Some routes return JSON, use a client auto-generator + const client = generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); + // For `getStateProof()` generate request serializer + const fetchOptsSerializers = getFetchOptsSerializers(routesData, reqSerializers); + + return { + ...client, + + async getStateProof(stateId, paths) { + const buffer = await httpClient.arrayBuffer(fetchOptsSerializers.getStateProof(stateId, paths)); + const proof = deserializeProof(new Uint8Array(buffer)); + return {data: proof}; + }, + }; +} diff --git a/packages/api/src/client/lodestar.ts b/packages/api/src/client/lodestar.ts new file mode 100644 index 0000000000..31f7511c4a --- /dev/null +++ b/packages/api/src/client/lodestar.ts @@ -0,0 +1,13 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IHttpClient, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/lodestar"; + +/** + * REST HTTP client for lodestar routes + */ +export function getClient(_config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(); + const returnTypes = getReturnTypes(); + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); +} diff --git a/packages/api/src/client/node.ts b/packages/api/src/client/node.ts new file mode 100644 index 0000000000..1d78b57316 --- /dev/null +++ b/packages/api/src/client/node.ts @@ -0,0 +1,13 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IHttpClient, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/node"; + +/** + * REST HTTP client for beacon routes + */ +export function getClient(config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(); + const returnTypes = getReturnTypes(config); + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); +} diff --git a/packages/api/src/client/utils/client.ts b/packages/api/src/client/utils/client.ts new file mode 100644 index 0000000000..53a73c4b71 --- /dev/null +++ b/packages/api/src/client/utils/client.ts @@ -0,0 +1,78 @@ +import {Json} from "@chainsafe/ssz"; +import {mapValues} from "@chainsafe/lodestar-utils"; +import {FetchOpts, IHttpClient} from "./httpClient"; +import {compileRouteUrlFormater} from "../../utils/urlFormat"; +import { + RouteDef, + ReqGeneric, + RouteGeneric, + ReturnTypes, + TypeJson, + jsonOpts, + ReqSerializer, + ReqSerializers, + RoutesData, +} from "../../utils/types"; + +// See /packages/api/src/routes/index.ts for reasoning + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Format FetchFn opts from Fn arguments given a route definition and request serializer. + * For routes that return only JSOn use @see getGenericJsonClient + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type +export function getFetchOptsSerializer any, ReqType extends ReqGeneric>( + routeDef: RouteDef, + reqSerializer: ReqSerializer +) { + const urlFormater = compileRouteUrlFormater(routeDef.url); + + return function getFetchOpts(...args: Parameters): FetchOpts { + const req = reqSerializer.writeReq(...args); + return { + url: urlFormater(req.params || {}), + method: routeDef.method, + query: req.query, + body: req.body as unknown, + }; + }; +} + +/** + * Generate `getFetchOptsSerializer()` functions for all routes in `Api` + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type +export function getFetchOptsSerializers< + Api extends Record, + ReqTypes extends {[K in keyof Api]: ReqGeneric} +>(routesData: RoutesData, reqSerializers: ReqSerializers) { + return mapValues(routesData, (routeDef, routeKey) => getFetchOptsSerializer(routeDef, reqSerializers[routeKey])); +} + +/** + * Get a generic JSON client from route definition, request serializer and return types. + */ +export function generateGenericJsonClient< + Api extends Record, + ReqTypes extends {[K in keyof Api]: ReqGeneric} +>( + routesData: RoutesData, + reqSerializers: ReqSerializers, + returnTypes: ReturnTypes, + fetchFn: IHttpClient +): Api { + return mapValues(routesData, (routeDef, routeKey) => { + const fetchOptsSerializer = getFetchOptsSerializer(routeDef, reqSerializers[routeKey]); + const returnType = returnTypes[routeKey as keyof ReturnTypes] as TypeJson | null; + + return async function request(...args: Parameters): Promise { + const res = await fetchFn.json(fetchOptsSerializer(...args)); + if (returnType) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return returnType.fromJson(res, jsonOpts) as ReturnType; + } + }; + }) as Api; +} diff --git a/packages/api/src/client/utils/httpClient.ts b/packages/api/src/client/utils/httpClient.ts new file mode 100644 index 0000000000..2dd22dfffa --- /dev/null +++ b/packages/api/src/client/utils/httpClient.ts @@ -0,0 +1,145 @@ +import {fetch} from "cross-fetch"; +import qs from "qs"; +import {AbortSignal, AbortController} from "abort-controller"; +import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils"; +import {ReqGeneric, RouteDef} from "../../utils"; + +export class HttpError extends Error { + status: number; + url: string; + + constructor(message: string, status: number, url: string) { + super(message); + this.status = status; + this.url = url; + } +} + +export type FetchOpts = { + url: RouteDef["url"]; + method: RouteDef["method"]; + query?: ReqGeneric["query"]; + body?: ReqGeneric["body"]; +}; + +export interface IHttpClient { + baseUrl: string; + json(opts: FetchOpts): Promise; + arrayBuffer(opts: FetchOpts): Promise; +} + +export type HttpClientOptions = { + baseUrl: string; + timeoutMs?: number; + /** Return an AbortSignal to be attached to all requests */ + getAbortSignal?: () => AbortSignal | undefined; + /** Override fetch function */ + fetch?: typeof fetch; +}; + +export class HttpClient implements IHttpClient { + readonly baseUrl: string; + private readonly timeoutMs: number; + private readonly getAbortSignal?: () => AbortSignal | undefined; + private readonly fetch: typeof fetch; + + /** + * timeoutMs = config.params.SECONDS_PER_SLOT * 1000 + */ + constructor(opts: HttpClientOptions) { + this.baseUrl = opts.baseUrl; + this.timeoutMs = opts.timeoutMs ?? 12000; + this.getAbortSignal = opts.getAbortSignal; + this.fetch = opts.fetch ?? fetch; + } + + async json(opts: FetchOpts): Promise { + return await this.request(opts, (res) => res.json() as Promise); + } + + async arrayBuffer(opts: FetchOpts): Promise { + return await this.request(opts, (res) => res.arrayBuffer()); + } + + private async request(opts: FetchOpts, getBody: (res: Response) => Promise): Promise { + // Implement fetch timeout + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + // Attach global signal to this request's controller + const signalGlobal = this.getAbortSignal && this.getAbortSignal(); + if (signalGlobal) { + signalGlobal.addEventListener("abort", () => controller.abort()); + } + + try { + const url = urlJoin(this.baseUrl, opts.url) + (opts.query ? "?" + stringifyQuery(opts.query) : ""); + const bodyArgs = opts.body + ? {headers: {"Content-Type": "application/json"}, body: JSON.stringify(opts.body)} + : {}; + + const res = await this.fetch(url, {method: opts.method, ...bodyArgs, signal: controller.signal}); + + if (!res.ok) { + const errBody = await res.text(); + throw new HttpError(`${res.statusText}: ${getErrorMessage(errBody)}`, res.status, url); + } + + return await getBody(res); + } catch (e) { + if (isAbortedError(e)) { + if (signalGlobal?.aborted) { + throw new ErrorAborted("REST client"); + } else if (controller.signal.aborted) { + throw new TimeoutError("request"); + } else { + throw Error("Unknown aborted error"); + } + } + throw e; + } finally { + clearTimeout(timeout); + if (signalGlobal) { + signalGlobal.removeEventListener("abort", controller.abort); + } + } + } +} + +function isAbortedError(e: Error): boolean { + return e.name === "AbortError" || e.message === "The user aborted a request"; +} + +function getErrorMessage(errBody: string): string { + try { + const errJson = JSON.parse(errBody) as {message: string}; + if (errJson.message) { + return errJson.message; + } else { + return errBody; + } + } catch (e) { + return errBody; + } +} + +/** + * Eth2.0 API requires the query with format: + * - arrayFormat: repeat `topic=topic1&topic=topic2` + */ +export function stringifyQuery(query: unknown): string { + return qs.stringify(query, {arrayFormat: "repeat"}); +} + +/** + * TODO: Optimize, two regex is a bit wasteful + */ +export function urlJoin(...args: string[]): string { + return ( + args + .join("/") + .replace(/([^:]\/)\/+/g, "$1") + // Remove duplicate slashes in the front + .replace(/^(\/)+/, "/") + ); +} diff --git a/packages/api/src/client/utils/index.ts b/packages/api/src/client/utils/index.ts new file mode 100644 index 0000000000..e5fec06a93 --- /dev/null +++ b/packages/api/src/client/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./client"; +export * from "./httpClient"; diff --git a/packages/api/src/client/validator.ts b/packages/api/src/client/validator.ts new file mode 100644 index 0000000000..9ae8881e2c --- /dev/null +++ b/packages/api/src/client/validator.ts @@ -0,0 +1,13 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IHttpClient, generateGenericJsonClient} from "./utils"; +import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "../routes/validator"; + +/** + * REST HTTP client for validator routes + */ +export function getClient(config: IBeaconConfig, httpClient: IHttpClient): Api { + const reqSerializers = getReqSerializers(config); + const returnTypes = getReturnTypes(config); + // All routes return JSON, use a client auto-generator + return generateGenericJsonClient(routesData, reqSerializers, returnTypes, httpClient); +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 0000000000..cdb06e1b2d --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,5 @@ +export * as routes from "./routes"; +export {Api} from "./interface"; +export {getClient} from "./client"; + +// Node: Don't export server here so it's not bundled to all consumers diff --git a/packages/api/src/interface.ts b/packages/api/src/interface.ts new file mode 100644 index 0000000000..7dd0e53c39 --- /dev/null +++ b/packages/api/src/interface.ts @@ -0,0 +1,19 @@ +import {Api as BeaconApi} from "./routes/beacon"; +import {Api as ConfigApi} from "./routes/config"; +import {Api as DebugApi} from "./routes/debug"; +import {Api as EventsApi} from "./routes/events"; +import {Api as LightclientApi} from "./routes/lightclient"; +import {Api as LodestarApi} from "./routes/lodestar"; +import {Api as NodeApi} from "./routes/node"; +import {Api as ValidatorApi} from "./routes/validator"; + +export type Api = { + beacon: BeaconApi; + config: ConfigApi; + debug: DebugApi; + events: EventsApi; + lightclient: LightclientApi; + lodestar: LodestarApi; + node: NodeApi; + validator: ValidatorApi; +}; diff --git a/packages/api/src/routes/beacon/block.ts b/packages/api/src/routes/beacon/block.ts new file mode 100644 index 0000000000..3dd58dc912 --- /dev/null +++ b/packages/api/src/routes/beacon/block.ts @@ -0,0 +1,167 @@ +import {ContainerType, Json} from "@chainsafe/ssz"; +import {ForkName, IBeaconConfig} from "@chainsafe/lodestar-config"; +import {phase0, allForks, Slot, Root} from "@chainsafe/lodestar-types"; +import { + RoutesData, + ReturnTypes, + ArrayOf, + ContainerData, + Schema, + WithVersion, + reqOnlyBody, + TypeJson, + ReqSerializers, + ReqSerializer, +} from "../../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type BlockId = "head" | "genesis" | "finalized" | string | number; + +export type BlockHeaderResponse = { + root: Root; + canonical: boolean; + header: phase0.SignedBeaconBlockHeader; +}; + +export type Api = { + /** + * Get block + * Returns the complete `SignedBeaconBlock` for a given block ID. + * Depending on the `Accept` header it can be returned either as JSON or SSZ-serialized bytes. + * + * @param blockId Block identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \, \. + */ + getBlock(blockId: BlockId): Promise<{data: allForks.SignedBeaconBlock}>; + + /** + * Get block + * Retrieves block details for given block id. + * @param blockId Block identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \, \. + */ + getBlockV2(blockId: BlockId): Promise<{data: allForks.SignedBeaconBlock; version: ForkName}>; + + /** + * Get block attestations + * Retrieves attestation included in requested block. + * @param blockId Block identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \, \. + */ + getBlockAttestations(blockId: BlockId): Promise<{data: phase0.Attestation[]}>; + + /** + * Get block header + * Retrieves block header for given block id. + * @param blockId Block identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \, \. + */ + getBlockHeader(blockId: BlockId): Promise<{data: BlockHeaderResponse}>; + + /** + * Get block headers + * Retrieves block headers matching given query. By default it will fetch current head slot blocks. + * @param slot + * @param parentRoot + */ + getBlockHeaders(filters: Partial<{slot: Slot; parentRoot: string}>): Promise<{data: BlockHeaderResponse[]}>; + + /** + * Get block root + * Retrieves hashTreeRoot of BeaconBlock/BeaconBlockHeader + * @param blockId Block identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \, \. + */ + getBlockRoot(blockId: BlockId): Promise<{data: Root}>; + + /** + * Publish a signed block. + * Instructs the beacon node to broadcast a newly signed beacon block to the beacon network, + * to be included in the beacon chain. The beacon node is not required to validate the signed + * `BeaconBlock`, and a successful response (20X) only indicates that the broadcast has been + * successful. The beacon node is expected to integrate the new block into its state, and + * therefore validate the block internally, however blocks which fail the validation are still + * broadcast but a different status code is returned (202) + * + * @param requestBody The `SignedBeaconBlock` object composed of `BeaconBlock` object (produced by beacon node) and validator signature. + * @returns any The block was validated successfully and has been broadcast. It has also been integrated into the beacon node's database. + */ + publishBlock(block: allForks.SignedBeaconBlock): Promise; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getBlock: {url: "/eth/v1/beacon/blocks/:blockId", method: "GET"}, + getBlockV2: {url: "/eth/v2/beacon/blocks/:blockId", method: "GET"}, + getBlockAttestations: {url: "/eth/v1/beacon/blocks/:blockId/attestations", method: "GET"}, + getBlockHeader: {url: "/eth/v1/beacon/headers/:blockId", method: "GET"}, + getBlockHeaders: {url: "/eth/v1/beacon/headers", method: "GET"}, + getBlockRoot: {url: "/eth/v1/beacon/blocks/:blockId/root", method: "GET"}, + publishBlock: {url: "/eth/v1/beacon/blocks", method: "POST"}, +}; + +type BlockIdOnlyReq = {params: {blockId: string | number}}; + +/* eslint-disable @typescript-eslint/naming-convention */ +export type ReqTypes = { + getBlock: BlockIdOnlyReq; + getBlockV2: BlockIdOnlyReq; + getBlockAttestations: BlockIdOnlyReq; + getBlockHeader: BlockIdOnlyReq; + getBlockHeaders: {query: {slot?: number; parent_root?: string}}; + getBlockRoot: BlockIdOnlyReq; + publishBlock: {body: Json}; +}; + +export function getReqSerializers(config: IBeaconConfig): ReqSerializers { + const blockIdOnlyReq: ReqSerializer = { + writeReq: (blockId) => ({params: {blockId}}), + parseReq: ({params}) => [params.blockId], + schema: {params: {blockId: Schema.StringRequired}}, + }; + + // Compute block type from JSON payload. See https://github.com/ethereum/eth2.0-APIs/pull/142 + const getSignedBeaconBlockType = (data: allForks.SignedBeaconBlock): ContainerType => + config.getForkTypes(data.message.slot).SignedBeaconBlock; + const AllForksSignedBeaconBlock: TypeJson = { + toJson: (data, opts) => getSignedBeaconBlockType(data).toJson(data, opts), + fromJson: (data, opts) => + getSignedBeaconBlockType((data as unknown) as allForks.SignedBeaconBlock).fromJson(data, opts), + }; + + return { + getBlock: blockIdOnlyReq, + getBlockV2: blockIdOnlyReq, + getBlockAttestations: blockIdOnlyReq, + getBlockHeader: blockIdOnlyReq, + getBlockHeaders: { + writeReq: (filters) => ({query: {slot: filters?.slot, parent_root: filters?.parentRoot}}), + parseReq: ({query}) => [{slot: query?.slot, parentRoot: query?.parent_root}], + schema: {query: {slot: Schema.Uint, parent_root: Schema.String}}, + }, + getBlockRoot: blockIdOnlyReq, + publishBlock: reqOnlyBody(AllForksSignedBeaconBlock, Schema.Object), + }; +} + +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + const BeaconHeaderResType = new ContainerType({ + fields: { + root: config.types.Root, + canonical: config.types.Boolean, + header: config.types.phase0.SignedBeaconBlockHeader, + }, + }); + + return { + getBlock: ContainerData(config.types.phase0.SignedBeaconBlock), + getBlockV2: WithVersion((fork) => config.types[fork].SignedBeaconBlock), + getBlockAttestations: ContainerData(ArrayOf(config.types.phase0.Attestation)), + getBlockHeader: ContainerData(BeaconHeaderResType), + getBlockHeaders: ContainerData(ArrayOf(BeaconHeaderResType)), + getBlockRoot: ContainerData(config.types.Root), + }; +} diff --git a/packages/api/src/routes/beacon/index.ts b/packages/api/src/routes/beacon/index.ts new file mode 100644 index 0000000000..4c66a4832a --- /dev/null +++ b/packages/api/src/routes/beacon/index.ts @@ -0,0 +1,63 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {phase0} from "@chainsafe/lodestar-types"; +import {RoutesData, ReturnTypes, reqEmpty, ContainerData} from "../../utils"; +import * as block from "./block"; +import * as pool from "./pool"; +import * as state from "./state"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +// NOTE: We choose to split the block, pool, and state namespaces so the files are not too big. +// However, for a consumer all these methods are within the same service "beacon" + +export {BlockId, BlockHeaderResponse} from "./block"; +export {AttestationFilters} from "./pool"; +// TODO: Review if re-exporting all these types is necessary +export { + StateId, + ValidatorId, + ValidatorStatus, + ValidatorFilters, + CommitteesFilters, + FinalityCheckpoints, + ValidatorResponse, + ValidatorBalance, + EpochCommitteeResponse, + EpochSyncCommitteeResponse, +} from "./state"; + +export type Api = block.Api & + pool.Api & + state.Api & { + getGenesis(): Promise<{data: phase0.Genesis}>; + }; + +export const routesData: RoutesData = { + getGenesis: {url: "/eth/v1/beacon/genesis", method: "GET"}, + ...block.routesData, + ...pool.routesData, + ...state.routesData, +}; + +export type ReqTypes = { + [K in keyof ReturnType]: ReturnType[K]["writeReq"]>; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type +export function getReqSerializers(config: IBeaconConfig) { + return { + getGenesis: reqEmpty, + ...block.getReqSerializers(config), + ...pool.getReqSerializers(config), + ...state.getReqSerializers(), + }; +} + +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + return { + getGenesis: ContainerData(config.types.phase0.Genesis), + ...block.getReturnTypes(config), + ...pool.getReturnTypes(config), + ...state.getReturnTypes(config), + }; +} diff --git a/packages/api/src/routes/beacon/pool.ts b/packages/api/src/routes/beacon/pool.ts new file mode 100644 index 0000000000..a8be0d6d15 --- /dev/null +++ b/packages/api/src/routes/beacon/pool.ts @@ -0,0 +1,161 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {phase0, altair, CommitteeIndex, Slot} from "@chainsafe/lodestar-types"; +import {Json} from "@chainsafe/ssz"; +import { + RoutesData, + ReturnTypes, + ArrayOf, + ContainerData, + Schema, + reqOnlyBody, + ReqSerializers, + reqEmpty, + ReqEmpty, +} from "../../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type AttestationFilters = { + slot: Slot; + committeeIndex: CommitteeIndex; +}; + +export type Api = { + /** + * Get Attestations from operations pool + * Retrieves attestations known by the node but not necessarily incorporated into any block + * @param slot + * @param committeeIndex + * @returns any Successful response + * @throws ApiError + */ + getPoolAttestations(filters?: Partial): Promise<{data: phase0.Attestation[]}>; + + /** + * Get AttesterSlashings from operations pool + * Retrieves attester slashings known by the node but not necessarily incorporated into any block + * @returns any Successful response + * @throws ApiError + */ + getPoolAttesterSlashings(): Promise<{data: phase0.AttesterSlashing[]}>; + + /** + * Get ProposerSlashings from operations pool + * Retrieves proposer slashings known by the node but not necessarily incorporated into any block + * @returns any Successful response + * @throws ApiError + */ + getPoolProposerSlashings(): Promise<{data: phase0.ProposerSlashing[]}>; + + /** + * Get SignedVoluntaryExit from operations pool + * Retrieves voluntary exits known by the node but not necessarily incorporated into any block + * @returns any Successful response + * @throws ApiError + */ + getPoolVoluntaryExits(): Promise<{data: phase0.SignedVoluntaryExit[]}>; + + /** + * Submit Attestation objects to node + * Submits Attestation objects to the node. Each attestation in the request body is processed individually. + * + * If an attestation is validated successfully the node MUST publish that attestation on the appropriate subnet. + * + * If one or more attestations fail validation the node MUST return a 400 error with details of which attestations have failed, and why. + * + * @param requestBody + * @returns any Attestations are stored in pool and broadcast on appropriate subnet + * @throws ApiError + */ + submitPoolAttestations(attestations: phase0.Attestation[]): Promise; + + /** + * Submit AttesterSlashing object to node's pool + * Submits AttesterSlashing object to node's pool and if passes validation node MUST broadcast it to network. + * @param requestBody + * @returns any Success + * @throws ApiError + */ + submitPoolAttesterSlashing(slashing: phase0.AttesterSlashing): Promise; + + /** + * Submit ProposerSlashing object to node's pool + * Submits ProposerSlashing object to node's pool and if passes validation node MUST broadcast it to network. + * @param requestBody + * @returns any Success + * @throws ApiError + */ + submitPoolProposerSlashing(slashing: phase0.ProposerSlashing): Promise; + + /** + * Submit SignedVoluntaryExit object to node's pool + * Submits SignedVoluntaryExit object to node's pool and if passes validation node MUST broadcast it to network. + * @param requestBody + * @returns any Voluntary exit is stored in node and broadcasted to network + * @throws ApiError + */ + submitPoolVoluntaryExit(exit: phase0.SignedVoluntaryExit): Promise; + + /** + * TODO: Add description + */ + submitPoolSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getPoolAttestations: {url: "/eth/v1/beacon/pool/attestations", method: "GET"}, + getPoolAttesterSlashings: {url: "/eth/v1/beacon/pool/attester_slashings", method: "GET"}, + getPoolProposerSlashings: {url: "/eth/v1/beacon/pool/proposer_slashings", method: "GET"}, + getPoolVoluntaryExits: {url: "/eth/v1/beacon/pool/voluntary_exits", method: "GET"}, + submitPoolAttestations: {url: "/eth/v1/beacon/pool/attestations", method: "POST"}, + submitPoolAttesterSlashing: {url: "/eth/v1/beacon/pool/attester_slashings", method: "POST"}, + submitPoolProposerSlashing: {url: "/eth/v1/beacon/pool/proposer_slashings", method: "POST"}, + submitPoolVoluntaryExit: {url: "/eth/v1/beacon/pool/voluntary_exits", method: "POST"}, + submitPoolSyncCommitteeSignatures: {url: "/eth/v1/beacon/pool/sync_committees", method: "POST"}, +}; + +/* eslint-disable @typescript-eslint/naming-convention */ +export type ReqTypes = { + getPoolAttestations: {query: {slot?: number; committee_index?: number}}; + getPoolAttesterSlashings: ReqEmpty; + getPoolProposerSlashings: ReqEmpty; + getPoolVoluntaryExits: ReqEmpty; + submitPoolAttestations: {body: Json}; + submitPoolAttesterSlashing: {body: Json}; + submitPoolProposerSlashing: {body: Json}; + submitPoolVoluntaryExit: {body: Json}; + submitPoolSyncCommitteeSignatures: {body: Json}; +}; + +export function getReqSerializers(config: IBeaconConfig): ReqSerializers { + return { + getPoolAttestations: { + writeReq: (filters) => ({query: {slot: filters?.slot, committee_index: filters?.committeeIndex}}), + parseReq: ({query}) => [{slot: query.slot, committeeIndex: query.committee_index}], + schema: {query: {slot: Schema.Uint, committee_index: Schema.Uint}}, + }, + getPoolAttesterSlashings: reqEmpty, + getPoolProposerSlashings: reqEmpty, + getPoolVoluntaryExits: reqEmpty, + submitPoolAttestations: reqOnlyBody(ArrayOf(config.types.phase0.Attestation), Schema.ObjectArray), + submitPoolAttesterSlashing: reqOnlyBody(config.types.phase0.AttesterSlashing, Schema.Object), + submitPoolProposerSlashing: reqOnlyBody(config.types.phase0.ProposerSlashing, Schema.Object), + submitPoolVoluntaryExit: reqOnlyBody(config.types.phase0.SignedVoluntaryExit, Schema.Object), + submitPoolSyncCommitteeSignatures: reqOnlyBody( + ArrayOf(config.types.altair.SyncCommitteeSignature), + Schema.ObjectArray + ), + }; +} + +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + return { + getPoolAttestations: ContainerData(ArrayOf(config.types.phase0.Attestation)), + getPoolAttesterSlashings: ContainerData(ArrayOf(config.types.phase0.AttesterSlashing)), + getPoolProposerSlashings: ContainerData(ArrayOf(config.types.phase0.ProposerSlashing)), + getPoolVoluntaryExits: ContainerData(ArrayOf(config.types.phase0.SignedVoluntaryExit)), + }; +} diff --git a/packages/api/src/routes/beacon/state.ts b/packages/api/src/routes/beacon/state.ts new file mode 100644 index 0000000000..4c86293135 --- /dev/null +++ b/packages/api/src/routes/beacon/state.ts @@ -0,0 +1,279 @@ +import {ContainerType} from "@chainsafe/ssz"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {phase0, CommitteeIndex, Slot, ValidatorIndex, Epoch, Root, Gwei} from "@chainsafe/lodestar-types"; +import { + RoutesData, + ReturnTypes, + ArrayOf, + ContainerData, + Schema, + StringType, + ReqSerializers, + ReqSerializer, +} from "../../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type StateId = string | "head" | "genesis" | "finalized" | "justified"; +export type ValidatorId = string | number; + +export type ValidatorStatus = + | "active" + | "pending_initialized" + | "pending_queued" + | "active_ongoing" + | "active_exiting" + | "active_slashed" + | "exited_unslashed" + | "exited_slashed" + | "withdrawal_possible" + | "withdrawal_done"; + +export type ValidatorFilters = { + indices?: ValidatorId[]; + statuses?: ValidatorStatus[]; +}; +export type CommitteesFilters = { + epoch?: Epoch; + index?: CommitteeIndex; + slot?: Slot; +}; + +export type FinalityCheckpoints = { + previousJustified: phase0.Checkpoint; + currentJustified: phase0.Checkpoint; + finalized: phase0.Checkpoint; +}; + +export type ValidatorResponse = { + index: ValidatorIndex; + balance: Gwei; + status: ValidatorStatus; + validator: phase0.Validator; +}; + +export type ValidatorBalance = { + index: ValidatorIndex; + balance: Gwei; +}; + +export type EpochCommitteeResponse = { + index: CommitteeIndex; + slot: Slot; + validators: ValidatorIndex[]; +}; + +export type EpochSyncCommitteeResponse = { + /** all of the validator indices in the current sync committee */ + validators: ValidatorIndex[]; + // TODO: This property will likely be deprecated + /** Subcommittee slices of the current sync committee */ + validatorAggregates: ValidatorIndex[]; +}; + +export type Api = { + /** + * Get state SSZ HashTreeRoot + * Calculates HashTreeRoot for state with given 'stateId'. If stateId is root, same value will be returned. + * + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + */ + getStateRoot(stateId: StateId): Promise<{data: Root}>; + + /** + * Get Fork object for requested state + * Returns [Fork](https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/beacon-chain.md#fork) object for state with given 'stateId'. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + */ + getStateFork(stateId: StateId): Promise<{data: phase0.Fork}>; + + /** + * Get state finality checkpoints + * Returns finality checkpoints for state with given 'stateId'. + * In case finality is not yet achieved, checkpoint should return epoch 0 and ZERO_HASH as root. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + */ + getStateFinalityCheckpoints(stateId: StateId): Promise<{data: FinalityCheckpoints}>; + + /** + * Get validators from state + * Returns filterable list of validators with their balance, status and index. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + * @param id Either hex encoded public key (with 0x prefix) or validator index + * @param status [Validator status specification](https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ) + */ + getStateValidators(stateId: StateId, filters?: ValidatorFilters): Promise<{data: ValidatorResponse[]}>; + + /** + * Get validator from state by id + * Returns validator specified by state and id or public key along with status and balance. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + * @param validatorId Either hex encoded public key (with 0x prefix) or validator index + */ + getStateValidator(stateId: StateId, validatorId: ValidatorId): Promise<{data: ValidatorResponse}>; + + /** + * Get validator balances from state + * Returns filterable list of validator balances. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + * @param id Either hex encoded public key (with 0x prefix) or validator index + */ + getStateValidatorBalances(stateId: StateId, indices?: ValidatorId[]): Promise<{data: ValidatorBalance[]}>; + + /** + * Get all committees for a state. + * Retrieves the committees for the given state. + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + * @param epoch Fetch committees for the given epoch. If not present then the committees for the epoch of the state will be obtained. + * @param index Restrict returned values to those matching the supplied committee index. + * @param slot Restrict returned values to those matching the supplied slot. + */ + getEpochCommittees(stateId: StateId, filters?: CommitteesFilters): Promise<{data: EpochCommitteeResponse[]}>; + + getEpochSyncCommittees(stateId: StateId, epoch?: Epoch): Promise<{data: EpochSyncCommitteeResponse}>; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getEpochCommittees: {url: "/eth/v1/beacon/states/:stateId/committees", method: "GET"}, + getEpochSyncCommittees: {url: "/eth/v1/beacon/states/:stateId/sync_committees", method: "GET"}, + getStateFinalityCheckpoints: {url: "/eth/v1/beacon/states/:stateId/finality_checkpoints", method: "GET"}, + getStateFork: {url: "/eth/v1/beacon/states/:stateId/fork", method: "GET"}, + getStateRoot: {url: "/eth/v1/beacon/states/:stateId/root", method: "GET"}, + getStateValidator: {url: "/eth/v1/beacon/states/:stateId/validators/:validatorId", method: "GET"}, + getStateValidators: {url: "/eth/v1/beacon/states/:stateId/validator_balances", method: "GET"}, + getStateValidatorBalances: {url: "/eth/v1/beacon/states/:stateId/validators", method: "GET"}, +}; + +type StateIdOnlyReq = {params: {stateId: string}}; + +export type ReqTypes = { + getEpochCommittees: {params: {stateId: StateId}; query: {slot?: number; epoch?: number; index?: number}}; + getEpochSyncCommittees: {params: {stateId: StateId}; query: {epoch?: number}}; + getStateFinalityCheckpoints: StateIdOnlyReq; + getStateFork: StateIdOnlyReq; + getStateRoot: StateIdOnlyReq; + getStateValidator: {params: {stateId: StateId; validatorId: ValidatorId}}; + getStateValidators: {params: {stateId: StateId}; query: {indices?: ValidatorId[]; statuses?: ValidatorStatus[]}}; + getStateValidatorBalances: {params: {stateId: StateId}; query: {indices?: ValidatorId[]}}; +}; + +export function getReqSerializers(): ReqSerializers { + const stateIdOnlyReq: ReqSerializer = { + writeReq: (stateId) => ({params: {stateId}}), + parseReq: ({params}) => [params.stateId], + schema: {params: {stateId: Schema.StringRequired}}, + }; + + return { + getEpochCommittees: { + writeReq: (stateId, filters) => ({params: {stateId}, query: filters || {}}), + parseReq: ({params, query}) => [params.stateId, query], + schema: { + params: {stateId: Schema.StringRequired}, + query: {slot: Schema.Uint, epoch: Schema.Uint, index: Schema.Uint}, + }, + }, + + getEpochSyncCommittees: { + writeReq: (stateId, epoch) => ({params: {stateId}, query: {epoch}}), + parseReq: ({params, query}) => [params.stateId, query.epoch], + schema: { + params: {stateId: Schema.StringRequired}, + query: {epoch: Schema.Uint}, + }, + }, + + getStateFinalityCheckpoints: stateIdOnlyReq, + getStateFork: stateIdOnlyReq, + getStateRoot: stateIdOnlyReq, + + getStateValidator: { + writeReq: (stateId, validatorId) => ({params: {stateId, validatorId}}), + parseReq: ({params}) => [params.stateId, params.validatorId], + schema: { + params: {stateId: Schema.StringRequired, validatorId: Schema.StringRequired}, + }, + }, + + getStateValidators: { + writeReq: (stateId, filters) => ({params: {stateId}, query: filters || {}}), + parseReq: ({params, query}) => [params.stateId, query], + schema: { + params: {stateId: Schema.StringRequired}, + query: {indices: Schema.UintOrStringArray, statuses: Schema.StringArray}, + }, + }, + + getStateValidatorBalances: { + writeReq: (stateId, indices) => ({params: {stateId}, query: {indices}}), + parseReq: ({params, query}) => [params.stateId, query.indices], + schema: { + params: {stateId: Schema.StringRequired}, + query: {indices: Schema.UintOrStringArray}, + }, + }, + }; +} + +/* eslint-disable @typescript-eslint/naming-convention */ +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + const FinalityCheckpoints = new ContainerType({ + fields: { + previousJustified: config.types.phase0.Checkpoint, + currentJustified: config.types.phase0.Checkpoint, + finalized: config.types.phase0.Checkpoint, + }, + }); + + const ValidatorResponse = new ContainerType({ + fields: { + index: config.types.ValidatorIndex, + balance: config.types.Gwei, + status: new StringType(), + validator: config.types.phase0.Validator, + }, + }); + + const ValidatorBalance = new ContainerType({ + fields: { + index: config.types.ValidatorIndex, + balance: config.types.Gwei, + }, + }); + + const EpochCommitteeResponse = new ContainerType({ + fields: { + index: config.types.CommitteeIndex, + slot: config.types.Slot, + validators: config.types.phase0.CommitteeIndices, + }, + }); + + const EpochSyncCommitteesResponse = new ContainerType({ + fields: { + validators: ArrayOf(config.types.ValidatorIndex), + validatorAggregates: ArrayOf(config.types.ValidatorIndex), + }, + }); + + return { + getStateRoot: ContainerData(config.types.Root), + getStateFork: ContainerData(config.types.phase0.Fork), + getStateFinalityCheckpoints: ContainerData(FinalityCheckpoints), + getStateValidators: ContainerData(ArrayOf(ValidatorResponse)), + getStateValidator: ContainerData(ValidatorResponse), + getStateValidatorBalances: ContainerData(ArrayOf(ValidatorBalance)), + getEpochCommittees: ContainerData(ArrayOf(EpochCommitteeResponse)), + getEpochSyncCommittees: ContainerData(EpochSyncCommitteesResponse), + }; +} diff --git a/packages/api/src/routes/config.ts b/packages/api/src/routes/config.ts new file mode 100644 index 0000000000..3b7412c0cc --- /dev/null +++ b/packages/api/src/routes/config.ts @@ -0,0 +1,69 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IBeaconParams, BeaconParams} from "@chainsafe/lodestar-params"; +import {Bytes32, Number64, phase0} from "@chainsafe/lodestar-types"; +import {mapValues} from "@chainsafe/lodestar-utils"; +import {ByteVectorType, ContainerType} from "@chainsafe/ssz"; +import {ArrayOf, ContainerData, ReqEmpty, reqEmpty, ReturnTypes, ReqSerializers, RoutesData} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type DepositContract = { + chainId: Number64; + address: Bytes32; +}; + +export type Api = { + /** + * Get deposit contract address. + * Retrieve Eth1 deposit contract address and chain ID. + */ + getDepositContract(): Promise<{data: DepositContract}>; + + /** + * Get scheduled upcoming forks. + * Retrieve all scheduled upcoming forks this node is aware of. + */ + getForkSchedule(): Promise<{data: phase0.Fork[]}>; + + /** + * Get spec params. + * Retrieve specification configuration used on this node. + * [Specification params list](https://github.com/ethereum/eth2.0-specs/blob/v1.0.0-rc.0/configs/mainnet/phase0.yaml) + * + * Values are returned with following format: + * - any value starting with 0x in the spec is returned as a hex string + * - numeric values are returned as a quoted integer + */ + getSpec(): Promise<{data: IBeaconParams}>; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getDepositContract: {url: "/eth/v1/config/deposit_contract", method: "GET"}, + getForkSchedule: {url: "/eth/v1/config/fork_schedule", method: "GET"}, + getSpec: {url: "/eth/v1/config/spec", method: "GET"}, +}; + +export type ReqTypes = {[K in keyof Api]: ReqEmpty}; + +export function getReqSerializers(): ReqSerializers { + return mapValues(routesData, () => reqEmpty); +} + +/* eslint-disable @typescript-eslint/naming-convention */ +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + const DepositContract = new ContainerType({ + fields: { + chainId: config.types.Number64, + address: new ByteVectorType({length: 20}), + }, + }); + + return { + getDepositContract: ContainerData(DepositContract), + getForkSchedule: ContainerData(ArrayOf(config.types.phase0.Fork)), + getSpec: ContainerData(BeaconParams), + }; +} diff --git a/packages/api/src/routes/debug.ts b/packages/api/src/routes/debug.ts new file mode 100644 index 0000000000..249954ab62 --- /dev/null +++ b/packages/api/src/routes/debug.ts @@ -0,0 +1,117 @@ +import {ForkName, IBeaconConfig} from "@chainsafe/lodestar-config"; +import {allForks, Slot, Root} from "@chainsafe/lodestar-types"; +import {ContainerType} from "@chainsafe/ssz"; +import {StateId} from "./beacon/state"; +import { + ArrayOf, + ContainerData, + ReturnTypes, + RoutesData, + Schema, + WithVersion, + TypeJson, + reqEmpty, + ReqSerializers, + ReqEmpty, + ReqSerializer, +} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +type SlotRoot = {slot: Slot; root: Root}; + +export type Api = { + /** + * Get fork choice leaves + * Retrieves all possible chain heads (leaves of fork choice tree). + */ + getHeads(): Promise<{data: SlotRoot[]}>; + + /** + * Get full BeaconState object + * Returns full BeaconState object for given stateId. + * Depending on `Accept` header it can be returned either as json or as bytes serialized by SSZ + * + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + */ + getState(stateId: StateId): Promise<{data: allForks.BeaconState}>; + + /** + * Get full BeaconState object + * Returns full BeaconState object for given stateId. + * Depending on `Accept` header it can be returned either as json or as bytes serialized by SSZ + * + * @param stateId State identifier. + * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. + */ + getStateV2(stateId: StateId): Promise<{data: allForks.BeaconState; version: ForkName}>; + + /** + * NOT IN SPEC + * Connect to a peer at the given multiaddr array + */ + connectToPeer(peerIdStr: string, multiaddr: string[]): Promise; + + /** + * NOT IN SPEC + * Disconnect from a peer + */ + disconnectPeer(peerIdStr: string): Promise; +}; + +export const routesData: RoutesData = { + getHeads: {url: "/eth/v1/debug/beacon/heads", method: "GET"}, + getState: {url: "/eth/v1/debug/beacon/states/:stateId", method: "GET"}, + getStateV2: {url: "/eth/v2/debug/beacon/states/:stateId", method: "GET"}, + connectToPeer: {url: "/eth/v1/debug/connect/:peerId", method: "POST"}, + disconnectPeer: {url: "/eth/v1/debug/disconnect/:peerId", method: "POST"}, +}; + +export type ReqTypes = { + getHeads: ReqEmpty; + getState: {params: {stateId: string}}; + getStateV2: {params: {stateId: string}}; + connectToPeer: {params: {peerId: string}; body: string[]}; + disconnectPeer: {params: {peerId: string}}; +}; + +export function getReqSerializers(): ReqSerializers { + const getState: ReqSerializer = { + writeReq: (stateId) => ({params: {stateId}}), + parseReq: ({params}) => [params.stateId], + schema: {params: {stateId: Schema.StringRequired}}, + }; + + return { + getHeads: reqEmpty, + getState: getState, + getStateV2: getState, + connectToPeer: { + writeReq: (peerId, multiaddr) => ({params: {peerId}, body: multiaddr}), + parseReq: ({params, body}) => [params.peerId, body], + schema: {params: {peerId: Schema.StringRequired}, body: Schema.StringArray}, + }, + disconnectPeer: { + writeReq: (peerId) => ({params: {peerId}}), + parseReq: ({params}) => [params.peerId], + schema: {params: {peerId: Schema.StringRequired}}, + }, + }; +} + +/* eslint-disable @typescript-eslint/naming-convention */ +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + const SlotRoot = new ContainerType({ + fields: { + slot: config.types.Slot, + root: config.types.Root, + }, + }); + + return { + getHeads: ContainerData(ArrayOf(SlotRoot)), + getState: ContainerData(config.types.phase0.BeaconState), + getStateV2: WithVersion((fork) => config.types[fork].BeaconState as TypeJson), + }; +} diff --git a/packages/api/src/routes/events.ts b/packages/api/src/routes/events.ts new file mode 100644 index 0000000000..f0ab623bd2 --- /dev/null +++ b/packages/api/src/routes/events.ts @@ -0,0 +1,139 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {Epoch, Number64, phase0, Slot, Root} from "@chainsafe/lodestar-types"; +import {ContainerType, Json, Type} from "@chainsafe/ssz"; +import {jsonOpts, RouteDef, TypeJson} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export enum EventType { + /** + * The node has finished processing, resulting in a new head. previous_duty_dependent_root is + * `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)` and + * current_duty_dependent_root is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)`. + * Both dependent roots use the genesis block root in the case of underflow. + */ + head = "head", + /** The node has received a valid block (from P2P or API) */ + block = "block", + /** The node has received a valid attestation (from P2P or API) */ + attestation = "attestation", + /** The node has received a valid voluntary exit (from P2P or API) */ + voluntaryExit = "voluntary_exit", + /** Finalized checkpoint has been updated */ + finalizedCheckpoint = "finalized_checkpoint", + /** The node has reorganized its chain */ + chainReorg = "chain_reorg", +} + +export type EventData = { + [EventType.head]: { + slot: Slot; + block: Root; + state: Root; + epochTransition: boolean; + previousDutyDependentRoot: Root; + currentDutyDependentRoot: Root; + }; + [EventType.block]: {slot: Slot; block: Root}; + [EventType.attestation]: phase0.Attestation; + [EventType.voluntaryExit]: phase0.SignedVoluntaryExit; + [EventType.finalizedCheckpoint]: {block: Root; state: Root; epoch: Epoch}; + [EventType.chainReorg]: { + slot: Slot; + depth: Number64; + oldHeadBlock: Root; + newHeadBlock: Root; + oldHeadState: Root; + newHeadState: Root; + epoch: Epoch; + }; +}; + +export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType]; + +export type Api = { + /** + * Subscribe to beacon node events + * Provides endpoint to subscribe to beacon node Server-Sent-Events stream. + * Consumers should use [eventsource](https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface) + * implementation to listen on those events. + * + * @param topics Event types to subscribe to + * @returns Opened SSE stream. + */ + eventstream(topics: EventType[], signal: AbortSignal, onEvent: (event: BeaconEvent) => void): void; +}; + +export const routesData: {[K in keyof Api]: RouteDef} = { + eventstream: {url: "/eth/v1/events", method: "GET"}, +}; + +export type ReqTypes = { + eventstream: { + query: {topics: EventType[]}; + }; +}; + +// It doesn't make sense to define a getReqSerializers() here given the exotic argument of eventstream() +// The request is very simple: (topics) => {query: {topics}}, and the test will ensure compatibility server - client + +export function getTypeByEvent(config: IBeaconConfig): {[K in EventType]: Type} { + return { + [EventType.head]: new ContainerType({ + fields: { + slot: config.types.Slot, + block: config.types.Root, + state: config.types.Root, + epochTransition: config.types.Boolean, + previousDutyDependentRoot: config.types.Root, + currentDutyDependentRoot: config.types.Root, + }, + }), + + [EventType.block]: new ContainerType({ + fields: { + slot: config.types.Slot, + block: config.types.Root, + }, + }), + + [EventType.attestation]: config.types.phase0.Attestation, + [EventType.voluntaryExit]: config.types.phase0.SignedVoluntaryExit, + + [EventType.finalizedCheckpoint]: new ContainerType({ + fields: { + block: config.types.Root, + state: config.types.Root, + epoch: config.types.Epoch, + }, + }), + + [EventType.chainReorg]: new ContainerType({ + fields: { + slot: config.types.Slot, + depth: config.types.Number64, + oldHeadBlock: config.types.Root, + newHeadBlock: config.types.Root, + oldHeadState: config.types.Root, + newHeadState: config.types.Root, + epoch: config.types.Epoch, + }, + }), + }; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type +export function getEventSerdes(config: IBeaconConfig) { + const typeByEvent = getTypeByEvent(config); + + return { + toJson: (event: BeaconEvent): Json => { + const eventType = typeByEvent[event.type] as TypeJson; + return eventType.toJson(event.message, jsonOpts); + }, + fromJson: (type: EventType, data: Json): BeaconEvent["message"] => { + const eventType = typeByEvent[type] as TypeJson; + return eventType.fromJson(data, jsonOpts); + }, + }; +} diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts new file mode 100644 index 0000000000..3dcf98e88d --- /dev/null +++ b/packages/api/src/routes/index.ts @@ -0,0 +1,47 @@ +export * as beacon from "./beacon"; +export * as config from "./config"; +export * as debug from "./debug"; +export * as events from "./events"; +export * as lightclient from "./lightclient"; +export * as lodestar from "./lodestar"; +export * as node from "./node"; +export * as validator from "./validator"; + +// Reasoning of the API definitions +// ================================ +// +// An HTTP request to the Lodestar BeaconNode API involves these steps regarding serialization: +// 1. Serialize request: api args => req params +// --- wire +// 2. Deserialize request: req params => api args +// --- exec api +// 3. Serialize api return => res body +// --- wire +// 4. Deserialize res body => api return +// +// In our case we define the client in the exact same interface as the API executor layer. +// Therefore we only need to define how to translate args <-> request, and return <-> response. +// +// All files in the /routes directory provide succint definitions to do those transformations plus: +// - URL + method, for each route ID +// - Runtime schema, for each route ID +// +// Almost all routes receive JSON and return JSON. So both the client and the server can be +// auto-generated from the definitions. Also, the design allows for customizability for the few +// routes that need non-JSON serialization (like debug.getState and lightclient.getProof) +// +// With this approach Typescript help us ensure that the client and server are compatible at build +// time, ensure there are tests for all routes and makes it very cheap to mantain and add new routes. +// +// +// How to add new routes +// ===================== +// +// 1. Add the route function signature to the `Api` type. The function name MUST match the routeId from the spec. +// The arguments should use spec types if approapriate. Non-spec types MUST be defined in before the Api type +// so they are scoped by routes namespace. The all arguments MUST use camelCase casing. +// 2. Add URL + METHOD in `routesData` matching the spec. +// 3. Declare request serializers in `getReqSerializers()`. You MAY use `RouteReqTypeGenerator` to declare the +// ReqTypes and request serializers in the same place. +// 4. Add the return type of the route to `getReturnTypes()` if it has any. The return type doesn't have to be +// a full SSZ type, but just a TypeJson with allows to convert from struct -> json -> struct. diff --git a/packages/api/src/routes/lightclient.ts b/packages/api/src/routes/lightclient.ts new file mode 100644 index 0000000000..7833ac463f --- /dev/null +++ b/packages/api/src/routes/lightclient.ts @@ -0,0 +1,74 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {Path} from "@chainsafe/ssz"; +import {Proof} from "@chainsafe/persistent-merkle-tree"; +import {altair, SyncPeriod} from "@chainsafe/lodestar-types"; +import { + ArrayOf, + reqEmpty, + ReturnTypes, + RoutesData, + Schema, + sameType, + ContainerData, + ReqSerializers, + ReqEmpty, +} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type Api = { + /** TODO: description */ + getStateProof(stateId: string, paths: Path[]): Promise<{data: Proof}>; + /** TODO: description */ + getBestUpdates(from: SyncPeriod, to: SyncPeriod): Promise<{data: altair.LightClientUpdate[]}>; + /** TODO: description */ + getLatestUpdateFinalized(): Promise<{data: altair.LightClientUpdate}>; + /** TODO: description */ + getLatestUpdateNonFinalized(): Promise<{data: altair.LightClientUpdate}>; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getStateProof: {url: "/eth/v1/lightclient/proof/:stateId", method: "POST"}, + getBestUpdates: {url: "/eth/v1/lightclient/best_updates/:periods", method: "GET"}, + getLatestUpdateFinalized: {url: "/eth/v1/lightclient/latest_update_finalized/", method: "GET"}, + getLatestUpdateNonFinalized: {url: "/eth/v1/lightclient/latest_update_nonfinalized/", method: "GET"}, +}; + +export type ReqTypes = { + getStateProof: {params: {stateId: string}; body: Path[]}; + getBestUpdates: {query: {from: number; to: number}}; + getLatestUpdateFinalized: ReqEmpty; + getLatestUpdateNonFinalized: ReqEmpty; +}; + +export function getReqSerializers(): ReqSerializers { + return { + getStateProof: { + writeReq: (stateId, paths) => ({params: {stateId}, body: paths}), + parseReq: ({params, body}) => [params.stateId, body], + schema: {params: {stateId: Schema.StringRequired}, body: Schema.AnyArray}, + }, + + getBestUpdates: { + writeReq: (from, to) => ({query: {from, to}}), + parseReq: ({query}) => [query.from, query.to], + schema: {query: {from: Schema.UintRequired, to: Schema.UintRequired}}, + }, + + getLatestUpdateFinalized: reqEmpty, + getLatestUpdateNonFinalized: reqEmpty, + }; +} + +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + return { + // Just sent the proof JSON as-is + getStateProof: sameType(), + getBestUpdates: ContainerData(ArrayOf(config.types.altair.LightClientUpdate)), + getLatestUpdateFinalized: ContainerData(config.types.altair.LightClientUpdate), + getLatestUpdateNonFinalized: ContainerData(config.types.altair.LightClientUpdate), + }; +} diff --git a/packages/api/src/routes/lodestar.ts b/packages/api/src/routes/lodestar.ts new file mode 100644 index 0000000000..664ada0cf5 --- /dev/null +++ b/packages/api/src/routes/lodestar.ts @@ -0,0 +1,48 @@ +import {Epoch} from "@chainsafe/lodestar-types"; +import {mapValues} from "@chainsafe/lodestar-utils"; +import {jsonType, ReqEmpty, reqEmpty, ReturnTypes, ReqSerializers, RoutesData, sameType} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type SyncChainDebugState = { + targetRoot: string | null; + targetSlot: number | null; + syncType: string; + status: string; + startEpoch: number; + peers: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + batches: any[]; +}; + +export type Api = { + /** TODO: description */ + getWtfNode(): Promise<{data: string}>; + /** TODO: description */ + getLatestWeakSubjectivityCheckpointEpoch(): Promise<{data: Epoch}>; + /** TODO: description */ + getSyncChainsDebugState(): Promise<{data: SyncChainDebugState[]}>; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getWtfNode: {url: "/eth/v1/lodestar/wtfnode/", method: "GET"}, + getLatestWeakSubjectivityCheckpointEpoch: {url: "/eth/v1/lodestar/ws_epoch/", method: "GET"}, + getSyncChainsDebugState: {url: "/eth/v1/lodestar/sync-chains-debug-state", method: "GET"}, +}; + +export type ReqTypes = {[K in keyof Api]: ReqEmpty}; + +export function getReqSerializers(): ReqSerializers { + return mapValues(routesData, () => reqEmpty); +} + +export function getReturnTypes(): ReturnTypes { + return { + getWtfNode: sameType(), + getLatestWeakSubjectivityCheckpointEpoch: sameType(), + getSyncChainsDebugState: jsonType(), + }; +} diff --git a/packages/api/src/routes/node.ts b/packages/api/src/routes/node.ts new file mode 100644 index 0000000000..0fce8cbf80 --- /dev/null +++ b/packages/api/src/routes/node.ts @@ -0,0 +1,179 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {allForks, Slot} from "@chainsafe/lodestar-types"; +import {ContainerType} from "@chainsafe/ssz"; +import { + ArrayOf, + ContainerData, + reqEmpty, + jsonType, + ReturnTypes, + RoutesData, + Schema, + StringType, + ReqSerializers, + ReqEmpty, +} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type NetworkIdentity = { + /** Cryptographic hash of a peer’s public key. [Read more](https://docs.libp2p.io/concepts/peer-id/) */ + peerId: string; + /** Ethereum node record. [Read more](https://eips.ethereum.org/EIPS/eip-778) */ + enr: string; + p2pAddresses: string[]; + discoveryAddresses: string[]; + /** Based on eth2 [Metadata object](https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/p2p-interface.md#metadata) */ + metadata: allForks.Metadata; +}; + +export type PeerState = "disconnected" | "connecting" | "connected" | "disconnecting"; +export type PeerDirection = "inbound" | "outbound"; + +export type NodePeer = { + peerId: string; + enr: string; + lastSeenP2pAddress: string; + state: PeerState; + // the spec does not specify direction for a disconnected peer, lodestar uses null in that case + direction: PeerDirection | null; +}; + +export type PeerCount = { + disconnected: number; + connecting: number; + connected: number; + disconnecting: number; +}; + +export type FilterGetPeers = { + state?: PeerState[]; + direction?: PeerDirection[]; +}; + +export type SyncingStatus = { + /** Head slot node is trying to reach */ + headSlot: Slot; + /** How many slots node needs to process to reach head. 0 if synced. */ + syncDistance: Slot; +}; + +/** + * Read information about the beacon node. + */ +export type Api = { + /** + * Get node network identity + * Retrieves data about the node's network presence + */ + getNetworkIdentity(): Promise<{data: NetworkIdentity}>; + + /** + * Get node network peers + * Retrieves data about the node's network peers. By default this returns all peers. Multiple query params are combined using AND conditions + * @param state + * @param direction + */ + getPeers(filters?: FilterGetPeers): Promise<{data: NodePeer[]; meta: {count: number}}>; + + /** + * Get peer + * Retrieves data about the given peer + * @param peerId + */ + getPeer(peerId: string): Promise<{data: NodePeer}>; + + /** + * Get peer count + * Retrieves number of known peers. + */ + getPeerCount(): Promise<{data: PeerCount}>; + + /** + * Get version string of the running beacon node. + * Requests that the beacon node identify information about its implementation in a format similar to a [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) field. + */ + getNodeVersion(): Promise<{data: {version: string}}>; + + /** + * Get node syncing status + * Requests the beacon node to describe if it's currently syncing or not, and if it is, what block it is up to. + */ + getSyncingStatus(): Promise<{data: SyncingStatus}>; + + /** + * Get health check + * Returns node health status in http status codes. Useful for load balancers. + */ + getHealth(): Promise; +}; + +export const routesData: RoutesData = { + getNetworkIdentity: {url: "/eth/v1/node/identity", method: "GET"}, + getPeers: {url: "/eth/v1/node/peers", method: "GET"}, + getPeer: {url: "/eth/v1/node/peers/:peerId", method: "GET"}, + getPeerCount: {url: "/eth/v1/node/peer_count", method: "GET"}, + getNodeVersion: {url: "/eth/v1/node/version", method: "GET"}, + getSyncingStatus: {url: "/eth/v1/node/syncing", method: "GET"}, + getHealth: {url: "/eth/v1/node/health", method: "GET"}, +}; + +export type ReqTypes = { + getNetworkIdentity: ReqEmpty; + getPeers: {query: {state?: PeerState[]; direction?: PeerDirection[]}}; + getPeer: {params: {peerId: string}}; + getPeerCount: ReqEmpty; + getNodeVersion: ReqEmpty; + getSyncingStatus: ReqEmpty; + getHealth: ReqEmpty; +}; + +export function getReqSerializers(): ReqSerializers { + return { + getNetworkIdentity: reqEmpty, + + getPeers: { + writeReq: (filters) => ({query: filters || {}}), + parseReq: ({query}) => [query], + schema: {query: {state: Schema.StringArray, direction: Schema.StringArray}}, + }, + getPeer: { + writeReq: (peerId) => ({params: {peerId}}), + parseReq: ({params}) => [params.peerId], + schema: {params: {peerId: Schema.StringRequired}}, + }, + + getPeerCount: reqEmpty, + getNodeVersion: reqEmpty, + getSyncingStatus: reqEmpty, + getHealth: reqEmpty, + }; +} + +/* eslint-disable @typescript-eslint/naming-convention */ +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + const stringType = new StringType(); + const NetworkIdentity = new ContainerType({ + fields: { + peerId: stringType, + enr: stringType, + p2pAddresses: ArrayOf(stringType), + discoveryAddresses: ArrayOf(stringType), + metadata: config.types.altair.Metadata, + }, + }); + + return { + // + // TODO: Consider just converting the JSON case without custom types + // + getNetworkIdentity: ContainerData(NetworkIdentity), + // All these types don't contain any BigInt nor Buffer instances. + // Use jsonType() to translate the casing in a generic way. + getPeers: jsonType(), + getPeer: jsonType(), + getPeerCount: jsonType(), + getNodeVersion: jsonType(), + getSyncingStatus: jsonType(), + }; +} diff --git a/packages/api/src/routes/validator.ts b/packages/api/src/routes/validator.ts new file mode 100644 index 0000000000..7fde073f13 --- /dev/null +++ b/packages/api/src/routes/validator.ts @@ -0,0 +1,360 @@ +import {ContainerType, fromHexString, Json, toHexString, Type} from "@chainsafe/ssz"; +import {ForkName, IBeaconConfig} from "@chainsafe/lodestar-config"; +import { + allForks, + altair, + BLSPubkey, + BLSSignature, + CommitteeIndex, + Epoch, + Number64, + phase0, + Root, + Slot, + ValidatorIndex, +} from "@chainsafe/lodestar-types"; +import { + RoutesData, + ReturnTypes, + ArrayOf, + ContainerData, + Schema, + WithVersion, + reqOnlyBody, + ReqSerializers, +} from "../utils"; + +// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes + +export type BeaconCommitteeSubscription = { + validatorIndex: ValidatorIndex; + committeeIndex: number; + committeesAtSlot: number; + slot: Slot; + isAggregator: boolean; +}; + +/** + * From https://github.com/ethereum/eth2.0-APIs/pull/136 + */ +export type SyncCommitteeSubscription = { + validatorIndex: ValidatorIndex; + syncCommitteeIndices: number[]; + untilEpoch: Epoch; +}; + +export type ProposerDuty = { + slot: Slot; + validatorIndex: ValidatorIndex; + pubkey: BLSPubkey; +}; + +export type AttesterDuty = { + // The validator's public key, uniquely identifying them + pubkey: BLSPubkey; + // Index of validator in validator registry + validatorIndex: ValidatorIndex; + committeeIndex: CommitteeIndex; + // Number of validators in committee + committeeLength: Number64; + // Number of committees at the provided slot + committeesAtSlot: Number64; + // Index of validator in committee + validatorCommitteeIndex: Number64; + // The slot at which the validator must attest. + slot: Slot; +}; + +/** + * From https://github.com/ethereum/eth2.0-APIs/pull/134 + */ +export type SyncDuty = { + pubkey: BLSPubkey; + /** Index of validator in validator registry. */ + validatorIndex: ValidatorIndex; + /** The indices of the validator in the sync committee. */ + validatorSyncCommitteeIndices: number[]; +}; + +export type Api = { + /** + * Get attester duties + * Requests the beacon node to provide a set of attestation duties, which should be performed by validators, for a particular epoch. + * Duties should only need to be checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, resulting in a change of duties. For full safety, you should monitor head events and confirm the dependent root in this response matches: + * - event.previous_duty_dependent_root when `compute_epoch_at_slot(event.slot) == epoch` + * - event.current_duty_dependent_root when `compute_epoch_at_slot(event.slot) + 1 == epoch` + * - event.block otherwise + * The dependent_root value is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)` or the genesis block root in the case of underflow. + * @param epoch Should only be allowed 1 epoch ahead + * @param requestBody An array of the validator indices for which to obtain the duties. + * @returns any Success response + * @throws ApiError + */ + getAttesterDuties( + epoch: Epoch, + validatorIndices: ValidatorIndex[] + ): Promise<{data: AttesterDuty[]; dependentRoot: Root}>; + + /** + * Get block proposers duties + * Request beacon node to provide all validators that are scheduled to propose a block in the given epoch. + * Duties should only need to be checked once per epoch, however a chain reorganization could occur that results in a change of duties. For full safety, you should monitor head events and confirm the dependent root in this response matches: + * - event.current_duty_dependent_root when `compute_epoch_at_slot(event.slot) == epoch` + * - event.block otherwise + * The dependent_root value is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)` or the genesis block root in the case of underflow. + * @param epoch + * @returns any Success response + * @throws ApiError + */ + getProposerDuties(epoch: Epoch): Promise<{data: ProposerDuty[]; dependentRoot: Root}>; + + getSyncCommitteeDuties( + epoch: number, + validatorIndices: ValidatorIndex[] + ): Promise<{data: SyncDuty[]; dependentRoot: Root}>; + + /** + * Produce a new block, without signature. + * Requests a beacon node to produce a valid block, which can then be signed by a validator. + * @param slot The slot for which the block should be proposed. + * @param randaoReveal The validator's randao reveal value. + * @param graffiti Arbitrary data validator wants to include in block. + * @returns any Success response + * @throws ApiError + */ + produceBlock( + slot: Slot, + randaoReveal: BLSSignature, + graffiti: string + ): Promise<{data: allForks.BeaconBlock; version: ForkName}>; + + /** + * Produce an attestation data + * Requests that the beacon node produce an AttestationData. + * @param slot The slot for which an attestation data should be created. + * @param committeeIndex The committee index for which an attestation data should be created. + * @returns any Success response + * @throws ApiError + */ + produceAttestationData(index: CommitteeIndex, slot: Slot): Promise<{data: phase0.AttestationData}>; + + produceSyncCommitteeContribution( + slot: Slot, + subcommitteeIndex: number, + beaconBlockRoot: Root + ): Promise<{data: altair.SyncCommitteeContribution}>; + + /** + * Get aggregated attestation + * Aggregates all attestations matching given attestation data root and slot + * @param attestationDataRoot HashTreeRoot of AttestationData that validator want's aggregated + * @param slot + * @returns any Returns aggregated `Attestation` object with same `AttestationData` root. + * @throws ApiError + */ + getAggregatedAttestation(attestationDataRoot: Root, slot: Slot): Promise<{data: phase0.Attestation}>; + + /** + * Publish multiple aggregate and proofs + * Verifies given aggregate and proofs and publishes them on appropriate gossipsub topic. + * @param requestBody + * @returns any Successful response + * @throws ApiError + */ + publishAggregateAndProofs(signedAggregateAndProofs: phase0.SignedAggregateAndProof[]): Promise; + + publishContributionAndProofs(contributionAndProofs: altair.SignedContributionAndProof[]): Promise; + + /** + * Signal beacon node to prepare for a committee subnet + * After beacon node receives this request, + * search using discv5 for peers related to this subnet + * and replace current peers with those ones if necessary + * If validator `is_aggregator`, beacon node must: + * - announce subnet topic subscription on gossipsub + * - aggregate attestations received on that subnet + * + * @param requestBody + * @returns any Slot signature is valid and beacon node has prepared the attestation subnet. + * + * Note that, we cannot be certain Beacon node will find peers for that subnet for various reasons," + * + * @throws ApiError + */ + prepareBeaconCommitteeSubnet(subscriptions: BeaconCommitteeSubscription[]): Promise; + + prepareSyncCommitteeSubnets(subscriptions: SyncCommitteeSubscription[]): Promise; +}; + +/** + * Define javascript values for each route + */ +export const routesData: RoutesData = { + getAttesterDuties: {url: "/eth/v1/validator/duties/attester/:epoch", method: "POST"}, + getProposerDuties: {url: "/eth/v1/validator/duties/proposer/:epoch", method: "GET"}, + getSyncCommitteeDuties: {url: "/eth/v1/validator/duties/sync/:epoch", method: "POST"}, + produceBlock: {url: "/eth/v1/validator/blocks/:slot", method: "GET"}, + produceAttestationData: {url: "/eth/v1/validator/attestation_data", method: "GET"}, + produceSyncCommitteeContribution: {url: "/eth/v1/validator/sync_committee_contribution", method: "GET"}, + getAggregatedAttestation: {url: "/eth/v1/validator/aggregate_attestation", method: "GET"}, + publishAggregateAndProofs: {url: "/eth/v1/validator/aggregate_and_proofs", method: "POST"}, + publishContributionAndProofs: {url: "/eth/v1/validator/contribution_and_proofs", method: "POST"}, + prepareBeaconCommitteeSubnet: {url: "/eth/v1/validator/beacon_committee_subscriptions", method: "POST"}, + prepareSyncCommitteeSubnets: {url: "/eth/v1/validator/sync_committee_subscriptions", method: "POST"}, +}; + +/* eslint-disable @typescript-eslint/naming-convention */ +export type ReqTypes = { + getAttesterDuties: {params: {epoch: Epoch}; body: ValidatorIndex[]}; + getProposerDuties: {params: {epoch: Epoch}}; + getSyncCommitteeDuties: {params: {epoch: Epoch}; body: ValidatorIndex[]}; + produceBlock: {params: {slot: number}; query: {randao_reveal: string; grafitti: string}}; + produceAttestationData: {query: {slot: number; committee_index: number}}; + produceSyncCommitteeContribution: {query: {slot: number; subcommittee_index: number; beacon_block_root: string}}; + getAggregatedAttestation: {query: {attestation_data_root: string; slot: number}}; + publishAggregateAndProofs: {body: Json}; + publishContributionAndProofs: {body: Json}; + prepareBeaconCommitteeSubnet: {body: Json}; + prepareSyncCommitteeSubnets: {body: Json}; +}; + +export function getReqSerializers(config: IBeaconConfig): ReqSerializers { + const BeaconCommitteeSubscription = new ContainerType({ + fields: { + validatorIndex: config.types.ValidatorIndex, + committeeIndex: config.types.CommitteeIndex, + committeesAtSlot: config.types.Slot, + slot: config.types.Slot, + isAggregator: config.types.Boolean, + }, + }); + + const SyncCommitteeSubscription = new ContainerType({ + fields: { + validatorIndex: config.types.ValidatorIndex, + syncCommitteeIndices: ArrayOf(config.types.CommitteeIndex), + untilEpoch: config.types.Epoch, + }, + }); + + return { + getAttesterDuties: { + writeReq: (epoch, validatorIndexes) => ({params: {epoch}, body: validatorIndexes}), + parseReq: ({params, body}) => [params.epoch, body], + schema: { + params: {epoch: Schema.UintRequired}, + body: Schema.UintArray, + }, + }, + + getProposerDuties: { + writeReq: (epoch) => ({params: {epoch}}), + parseReq: ({params}) => [params.epoch], + schema: { + params: {epoch: Schema.UintRequired}, + }, + }, + + getSyncCommitteeDuties: { + writeReq: (epoch, validatorIndexes) => ({params: {epoch}, body: validatorIndexes}), + parseReq: ({params, body}) => [params.epoch, body], + schema: { + params: {epoch: Schema.UintRequired}, + body: Schema.UintArray, + }, + }, + + produceBlock: { + writeReq: (slot, randaoReveal, grafitti) => ({ + params: {slot}, + query: {randao_reveal: toHexString(randaoReveal), grafitti}, + }), + parseReq: ({params, query}) => [params.slot, fromHexString(query.randao_reveal), query.grafitti], + schema: { + params: {slot: Schema.UintRequired}, + query: {randao_reveal: Schema.StringRequired, grafitti: Schema.String}, + }, + }, + + produceAttestationData: { + writeReq: (index, slot) => ({query: {slot, committee_index: index}}), + parseReq: ({query}) => [query.committee_index, query.slot], + schema: { + query: {slot: Schema.UintRequired, committee_index: Schema.UintRequired}, + }, + }, + + produceSyncCommitteeContribution: { + writeReq: (slot, index, root) => ({ + query: {slot, subcommittee_index: index, beacon_block_root: toHexString(root)}, + }), + parseReq: ({query}) => [query.slot, query.subcommittee_index, fromHexString(query.beacon_block_root)], + schema: { + query: { + slot: Schema.UintRequired, + subcommittee_index: Schema.UintRequired, + beacon_block_root: Schema.StringRequired, + }, + }, + }, + + getAggregatedAttestation: { + writeReq: (root, slot) => ({query: {attestation_data_root: toHexString(root), slot}}), + parseReq: ({query}) => [fromHexString(query.attestation_data_root), query.slot], + schema: { + query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired}, + }, + }, + + publishAggregateAndProofs: reqOnlyBody(ArrayOf(config.types.phase0.SignedAggregateAndProof), Schema.ObjectArray), + publishContributionAndProofs: reqOnlyBody( + ArrayOf(config.types.altair.SignedContributionAndProof), + Schema.ObjectArray + ), + prepareBeaconCommitteeSubnet: reqOnlyBody(ArrayOf(BeaconCommitteeSubscription), Schema.ObjectArray), + prepareSyncCommitteeSubnets: reqOnlyBody(ArrayOf(SyncCommitteeSubscription), Schema.ObjectArray), + }; +} + +export function getReturnTypes(config: IBeaconConfig): ReturnTypes { + const WithDependentRoot = (dataType: Type): ContainerType<{data: T; dependentRoot: Root}> => + new ContainerType({fields: {data: dataType, dependentRoot: config.types.Root}}); + + const AttesterDuty = new ContainerType({ + fields: { + pubkey: config.types.BLSPubkey, + validatorIndex: config.types.ValidatorIndex, + committeeIndex: config.types.CommitteeIndex, + committeeLength: config.types.Number64, + committeesAtSlot: config.types.Number64, + validatorCommitteeIndex: config.types.Number64, + slot: config.types.Slot, + }, + }); + + const ProposerDuty = new ContainerType({ + fields: { + slot: config.types.Slot, + validatorIndex: config.types.ValidatorIndex, + pubkey: config.types.BLSPubkey, + }, + }); + + const SyncDuty = new ContainerType({ + fields: { + pubkey: config.types.BLSPubkey, + validatorIndex: config.types.ValidatorIndex, + validatorSyncCommitteeIndices: ArrayOf(config.types.Number64), + }, + }); + + return { + getAttesterDuties: WithDependentRoot(ArrayOf(AttesterDuty)), + getProposerDuties: WithDependentRoot(ArrayOf(ProposerDuty)), + getSyncCommitteeDuties: WithDependentRoot(ArrayOf(SyncDuty)), + produceBlock: WithVersion((fork) => config.types[fork].BeaconBlock), + produceAttestationData: ContainerData(config.types.phase0.AttestationData), + produceSyncCommitteeContribution: ContainerData(config.types.altair.SyncCommitteeContribution), + getAggregatedAttestation: ContainerData(config.types.phase0.Attestation), + }; +} diff --git a/packages/api/src/server/beacon.ts b/packages/api/src/server/beacon.ts new file mode 100644 index 0000000000..e1cfca2ce1 --- /dev/null +++ b/packages/api/src/server/beacon.ts @@ -0,0 +1,8 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/beacon"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + // All routes return JSON, use a server auto-generator + return getGenericJsonServer({routesData, getReturnTypes, getReqSerializers}, config, api); +} diff --git a/packages/api/src/server/config.ts b/packages/api/src/server/config.ts new file mode 100644 index 0000000000..c5fd56c206 --- /dev/null +++ b/packages/api/src/server/config.ts @@ -0,0 +1,8 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/config"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + // All routes return JSON, use a server auto-generator + return getGenericJsonServer({routesData, getReturnTypes, getReqSerializers}, config, api); +} diff --git a/packages/api/src/server/debug.ts b/packages/api/src/server/debug.ts new file mode 100644 index 0000000000..f4e152b74f --- /dev/null +++ b/packages/api/src/server/debug.ts @@ -0,0 +1,49 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {jsonOpts} from "../utils"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/debug"; + +const mimeTypeSSZ = "application/octet-stream"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + const reqSerializers = getReqSerializers(); + const returnTypes = getReturnTypes(config); + + const serverRoutes = getGenericJsonServer( + {routesData, getReturnTypes, getReqSerializers}, + config, + api + ); + + return { + ...serverRoutes, + + // Non-JSON routes. Return JSON or binary depending on "accept" header + getState: { + ...serverRoutes.getState, + handler: async (req) => { + const data = await api.getState(...reqSerializers.getState.parseReq(req)); + const type = config.getForkTypes(data.data.slot).BeaconState; + if (req.headers["accept"] === mimeTypeSSZ) { + // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer + return Buffer.from(type.serialize(data.data)); + } else { + return returnTypes.getState.toJson(data, jsonOpts); + } + }, + }, + getStateV2: { + ...serverRoutes.getStateV2, + handler: async (req) => { + const data = await api.getStateV2(...reqSerializers.getStateV2.parseReq(req)); + const type = config.getForkTypes(data.data.slot).BeaconState; + if (req.headers["accept"] === mimeTypeSSZ) { + // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer + return Buffer.from(type.serialize(data.data)); + } else { + return returnTypes.getStateV2.toJson(data, jsonOpts); + } + }, + }, + }; +} diff --git a/packages/api/src/server/events.ts b/packages/api/src/server/events.ts new file mode 100644 index 0000000000..a16e46a9ae --- /dev/null +++ b/packages/api/src/server/events.ts @@ -0,0 +1,66 @@ +import {AbortController} from "abort-controller"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ServerRoutes} from "./utils"; +import {Api, ReqTypes, routesData, getEventSerdes} from "../routes/events"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + const eventSerdes = getEventSerdes(config); + + return { + // Non-JSON route. Server Sent Events (SSE) + eventstream: { + url: routesData.eventstream.url, + method: routesData.eventstream.method, + id: "eventstream", + + handler: async (req, res) => { + const controller = new AbortController(); + + try { + res.raw.setHeader("Content-Type", "text/event-stream"); + res.raw.setHeader("Cache-Control", "no-cache,no-transform"); + res.raw.setHeader("Connection", "keep-alive"); + // It was reported that chrome and firefox do not play well with compressed event-streams https://github.com/lolo32/fastify-sse/issues/2 + res.raw.setHeader("x-no-compression", 1); + + await new Promise((resolve, reject) => { + api.eventstream(req.query.topics, controller.signal, (event) => { + try { + const data = eventSerdes.toJson(event); + res.raw.write(serializeSSEEvent({event: event.type, data})); + } catch (e) { + reject(e); + } + }); + + // The stream will never end by the server unless the node is stopped. + // In that case the BeaconNode class will call server.close() and end this connection. + + // The client may disconnect and we need to clean the subscriptions. + req.raw.once("close", () => resolve()); + req.raw.once("end", () => resolve()); + req.raw.once("error", (err) => reject(err)); + }); + + // api.eventstream will never stop, so no need to ever call `res.raw.end();` + } finally { + controller.abort(); + } + }, + + // TODO: Bundle this in /routes/events? + schema: { + querystring: { + type: "object", + properties: { + topics: {type: "array", items: {type: "string"}}, + }, + }, + }, + }, + }; +} + +export function serializeSSEEvent(chunk: {event: string; data: unknown}): string { + return [`event: ${chunk.event}`, `data: ${JSON.stringify(chunk.data)}`, "\n"].join("\n"); +} diff --git a/packages/api/src/server/index.ts b/packages/api/src/server/index.ts new file mode 100644 index 0000000000..1eb7efdabd --- /dev/null +++ b/packages/api/src/server/index.ts @@ -0,0 +1,69 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +// eslint-disable-next-line import/no-extraneous-dependencies +import {FastifyInstance} from "fastify"; +import {Api} from "../interface"; +import {ServerRoute} from "./utils"; + +import * as beacon from "./beacon"; +import * as configApi from "./config"; +import * as debug from "./debug"; +import * as events from "./events"; +import * as lightclient from "./lightclient"; +import * as lodestar from "./lodestar"; +import * as node from "./node"; +import * as validator from "./validator"; + +export type RouteConfig = { + operationId: ServerRoute["id"]; +}; + +export function registerRoutes( + server: FastifyInstance, + config: IBeaconConfig, + api: Api, + enabledNamespaces: (keyof Api)[] +): void { + const routesByNamespace: { + // Enforces that we are declaring routes for every routeId in `Api` + [K in keyof Api]: { + // The ReqTypes are enforced in each getRoutes return type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K2 in keyof Api[K]]: ServerRoute; + }; + } = { + // Initializes route types and their definitions + beacon: beacon.getRoutes(config, api.beacon), + config: configApi.getRoutes(config, api.config), + debug: debug.getRoutes(config, api.debug), + events: events.getRoutes(config, api.events), + lightclient: lightclient.getRoutes(config, api.lightclient), + lodestar: lodestar.getRoutes(config, api.lodestar), + node: node.getRoutes(config, api.node), + validator: validator.getRoutes(config, api.validator), + }; + + for (const namespace of enabledNamespaces) { + const routes = routesByNamespace[namespace]; + if (!routes) { + throw Error(`Unknown api namespace ${namespace}`); + } + + registerRoutesGroup(server, routes); + } +} + +export function registerRoutesGroup( + fastify: FastifyInstance, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + routes: Record> +): void { + for (const route of Object.values(routes)) { + fastify.route({ + url: route.url, + method: route.method, + handler: route.handler, + schema: route.schema, + config: {operationId: route.id} as RouteConfig, + }); + } +} diff --git a/packages/api/src/server/lightclient.ts b/packages/api/src/server/lightclient.ts new file mode 100644 index 0000000000..c2dbcdcf23 --- /dev/null +++ b/packages/api/src/server/lightclient.ts @@ -0,0 +1,28 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {serializeProof} from "@chainsafe/persistent-merkle-tree"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/lightclient"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + const reqSerializers = getReqSerializers(); + const serverRoutes = getGenericJsonServer( + {routesData, getReturnTypes, getReqSerializers}, + config, + api + ); + + return { + ...serverRoutes, + + // Non-JSON routes. Return binary + getStateProof: { + ...serverRoutes.getStateProof, + handler: async (req) => { + const args = reqSerializers.getStateProof.parseReq(req); + const {data: proof} = await api.getStateProof(...args); + // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer + return Buffer.from(serializeProof(proof)); + }, + }, + }; +} diff --git a/packages/api/src/server/lodestar.ts b/packages/api/src/server/lodestar.ts new file mode 100644 index 0000000000..fc46948ecd --- /dev/null +++ b/packages/api/src/server/lodestar.ts @@ -0,0 +1,8 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/lodestar"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + // All routes return JSON, use a server auto-generator + return getGenericJsonServer({routesData, getReturnTypes, getReqSerializers}, config, api); +} diff --git a/packages/api/src/server/node.ts b/packages/api/src/server/node.ts new file mode 100644 index 0000000000..bb5865b1d1 --- /dev/null +++ b/packages/api/src/server/node.ts @@ -0,0 +1,8 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/node"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + // All routes return JSON, use a server auto-generator + return getGenericJsonServer({routesData, getReturnTypes, getReqSerializers}, config, api); +} diff --git a/packages/api/src/server/utils/index.ts b/packages/api/src/server/utils/index.ts new file mode 100644 index 0000000000..364a925708 --- /dev/null +++ b/packages/api/src/server/utils/index.ts @@ -0,0 +1 @@ +export * from "./server"; diff --git a/packages/api/src/server/utils/server.ts b/packages/api/src/server/utils/server.ts new file mode 100644 index 0000000000..bea760fd3c --- /dev/null +++ b/packages/api/src/server/utils/server.ts @@ -0,0 +1,79 @@ +import {Json} from "@chainsafe/ssz"; +import {mapValues} from "@chainsafe/lodestar-utils"; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as fastify from "fastify"; +import { + ReqGeneric, + RouteGeneric, + ReturnTypes, + TypeJson, + Resolves, + jsonOpts, + RouteGroupDefinition, +} from "../../utils/types"; +import {getFastifySchema} from "../../utils/schema"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; + +// See /packages/api/src/routes/index.ts for reasoning + +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention */ + +export type ServerRoute = { + url: string; + method: fastify.HTTPMethods; + handler: FastifyHandler; + schema?: fastify.FastifySchema; + /** OperationId as defined in https://github.com/ethereum/eth2.0-APIs/blob/18cb6ff152b33a5f34c377f00611821942955c82/apis/beacon/blocks/attestations.yaml#L2 */ + id: string; +}; + +/** Adaptor for Fastify v3.x.x route type which has a ton of arguments */ +type FastifyHandler = fastify.RouteHandlerMethod< + fastify.RawServerDefault, + fastify.RawRequestDefaultExpression, + fastify.RawReplyDefaultExpression, + { + Body: Req["body"]; + Querystring: Req["query"]; + Params: Req["params"]; + }, + fastify.ContextConfigDefault +>; + +export type ServerRoutes, ReqTypes extends {[K in keyof Api]: ReqGeneric}> = { + [K in keyof Api]: ServerRoute; +}; + +export function getGenericJsonServer< + Api extends Record, + ReqTypes extends {[K in keyof Api]: ReqGeneric} +>( + {routesData, getReqSerializers, getReturnTypes}: RouteGroupDefinition, + config: IBeaconConfig, + api: Api +): ServerRoutes { + const reqSerializers = getReqSerializers(config); + const returnTypes = getReturnTypes(config); + + return mapValues(routesData, (routeDef, routeKey) => { + const routeSerdes = reqSerializers[routeKey]; + const returnType = returnTypes[routeKey as keyof ReturnTypes] as TypeJson | null; + + return { + url: routeDef.url, + method: routeDef.method, + id: routeKey as string, + schema: routeSerdes.schema && getFastifySchema(routeSerdes.schema), + + handler: async function handler(req: ReqGeneric): Promise { + const args: any[] = routeSerdes.parseReq(req as ReqTypes[keyof Api]); + const data = (await api[routeKey](...args)) as Resolves; + if (returnType) { + return returnType.toJson(data, jsonOpts); + } else { + return {}; + } + }, + }; + }); +} diff --git a/packages/api/src/server/validator.ts b/packages/api/src/server/validator.ts new file mode 100644 index 0000000000..a2104405c1 --- /dev/null +++ b/packages/api/src/server/validator.ts @@ -0,0 +1,8 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ServerRoutes, getGenericJsonServer} from "./utils"; +import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/validator"; + +export function getRoutes(config: IBeaconConfig, api: Api): ServerRoutes { + // All routes return JSON, use a server auto-generator + return getGenericJsonServer({routesData, getReturnTypes, getReqSerializers}, config, api); +} diff --git a/packages/types/src/utils/StringType.ts b/packages/api/src/utils/StringType.ts similarity index 93% rename from packages/types/src/utils/StringType.ts rename to packages/api/src/utils/StringType.ts index a969cce97c..f94ff80bc5 100644 --- a/packages/types/src/utils/StringType.ts +++ b/packages/api/src/utils/StringType.ts @@ -1,5 +1,7 @@ import {BasicType} from "@chainsafe/ssz"; +/* eslint-disable @typescript-eslint/naming-convention */ + export class StringType extends BasicType { // eslint-disable-next-line @typescript-eslint/no-unused-vars struct_getSerializedLength(data?: string): number { diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts new file mode 100644 index 0000000000..3017540252 --- /dev/null +++ b/packages/api/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from "./schema"; +export * from "./StringType"; +export * from "./types"; +export * from "./urlFormat"; diff --git a/packages/api/src/utils/schema.ts b/packages/api/src/utils/schema.ts new file mode 100644 index 0000000000..b9b7a14b17 --- /dev/null +++ b/packages/api/src/utils/schema.ts @@ -0,0 +1,116 @@ +import {ReqGeneric} from "./types"; + +// Reasoning: Allows to declare JSON schemas for server routes in a succinct typesafe way. +// The enums exposed here are very feature incomplete but cover the minimum necessary for +// the existing routes. Since the arguments for Eth2.0 server routes are very simple it suffice. + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type JsonSchema = Record; +type JsonSchemaObj = { + type: "object"; + required: string[]; + properties: Record; +}; + +export type SchemaDefinition = { + params?: { + [K in keyof ReqType["params"]]: Schema; + }; + query?: { + [K in keyof ReqType["query"]]: Schema; + }; + body?: Schema; +}; + +export enum Schema { + Uint, + UintRequired, + UintArray, + String, + StringRequired, + StringArray, + UintOrStringRequired, + UintOrStringArray, + Object, + ObjectArray, + AnyArray, +} + +/** + * Return JSON schema from a Schema enum. Useful to declare schemas in a succinct format + */ +function getJsonSchemaItem(schema: Schema): JsonSchema { + switch (schema) { + case Schema.Uint: + case Schema.UintRequired: + return {type: "integer", minimum: 0}; + + case Schema.UintArray: + return {type: "array", items: {type: "integer", minimum: 0}}; + + case Schema.String: + case Schema.StringRequired: + return {type: "string"}; + + case Schema.StringArray: + return {type: "array", items: {type: "string"}}; + + case Schema.UintOrStringRequired: + return {type: ["string", "integer"]}; + case Schema.UintOrStringArray: + return {type: "array", items: {type: ["string", "integer"]}}; + + case Schema.Object: + return {type: "object"}; + + case Schema.ObjectArray: + return {type: "array", items: {type: "object"}}; + + case Schema.AnyArray: + return {type: "array"}; + } +} + +function isRequired(schema: Schema): boolean { + switch (schema) { + case Schema.UintRequired: + case Schema.StringRequired: + case Schema.UintOrStringRequired: + return true; + + default: + return false; + } +} + +export function getFastifySchema(schemaDef: SchemaDefinition): JsonSchema { + const schema: {params?: JsonSchemaObj; querystring?: JsonSchemaObj; body?: JsonSchema} = {}; + + if (schemaDef.body) { + schema.body = getJsonSchemaItem(schemaDef.body); + } + + if (schemaDef.params) { + schema.params = {type: "object", required: [] as string[], properties: {}}; + + for (const [key, def] of Object.entries(schemaDef.params)) { + schema.params.properties[key] = getJsonSchemaItem(def as Schema); + if (isRequired(def as Schema)) { + schema.params.required.push(key); + } + } + } + + if (schemaDef.query) { + schema.querystring = {type: "object", required: [] as string[], properties: {}}; + + for (const [key, def] of Object.entries(schemaDef.query)) { + schema.querystring.properties[key] = getJsonSchemaItem(def as Schema); + if (isRequired(def as Schema)) { + schema.querystring.required.push(key); + } + } + } + + return schema; +} diff --git a/packages/api/src/utils/types.ts b/packages/api/src/utils/types.ts new file mode 100644 index 0000000000..a880c49d34 --- /dev/null +++ b/packages/api/src/utils/types.ts @@ -0,0 +1,150 @@ +import {ContainerType, IJsonOptions, Json, ListType, Type} from "@chainsafe/ssz"; +import {ForkName, IBeaconConfig} from "@chainsafe/lodestar-config"; +import {objectToExpectedCase} from "@chainsafe/lodestar-utils"; +import {Schema, SchemaDefinition} from "./schema"; + +// See /packages/api/src/routes/index.ts for reasoning + +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any */ + +/** All JSON must be sent in snake case */ +export const jsonOpts = {case: "snake" as const}; +/** All JSON inside the JS code must be camel case */ +export const codeCase = "camel" as const; + +export type RouteGroupDefinition< + Api extends Record, + ReqTypes extends {[K in keyof Api]: ReqGeneric} +> = { + routesData: RoutesData; + getReqSerializers: (config: IBeaconConfig) => ReqSerializers; + getReturnTypes: (config: IBeaconConfig) => ReturnTypes; +}; + +export type RouteDef = { + url: string; + method: "GET" | "POST"; +}; + +export type ReqGeneric = { + params?: Record; + query?: Record; + body?: any; +}; + +export type ReqEmpty = ReqGeneric; + +export type RouteGeneric = (...args: any) => PromiseLike | any; + +type ThenArg = T extends PromiseLike ? U : T; +export type Resolves any> = ThenArg>; + +export type TypeJson = { + toJson(val: T, opts?: IJsonOptions): Json; + fromJson(json: Json, opts?: IJsonOptions): T; +}; + +// +// REQ +// + +export type ReqSerializer any, ReqType extends ReqGeneric> = { + writeReq: (...args: Parameters) => ReqType; + parseReq: (arg: ReqType) => Parameters; + schema?: SchemaDefinition; +}; + +export type ReqSerializers< + Api extends Record, + ReqTypes extends {[K in keyof Api]: ReqGeneric} +> = { + [K in keyof Api]: ReqSerializer; +}; + +/** Curried definition to infer only one of the two generic types */ +export type ReqGenArg any, ReqType extends ReqGeneric> = ReqSerializer; + +// +// RETURN +// + +export type KeysOfNonVoidResolveValues> = { + [K in keyof Api]: Resolves extends void ? never : K; +}[keyof Api]; + +export type ReturnTypes> = { + [K in keyof Pick>]: TypeJson>; +}; + +export type RoutesData> = {[K in keyof Api]: RouteDef}; + +// +// Helpers +// + +/** Shortcut for routes that have no params, query nor body */ +export const reqEmpty: ReqSerializer<() => void, ReqEmpty> = { + writeReq: () => ({}), + parseReq: () => [] as [], +}; + +/** Shortcut for routes that have only body */ +export const reqOnlyBody = ( + type: TypeJson, + bodySchema: Schema +): ReqGenArg<(arg: T) => Promise, {body: Json}> => ({ + writeReq: (items) => ({body: type.toJson(items, jsonOpts)}), + parseReq: ({body}) => [type.fromJson(body, jsonOpts)], + schema: {body: bodySchema}, +}); + +/** SSZ factory helper + typed. limit = 1e6 as a big enough random number */ +export function ArrayOf(elementType: Type, limit = 1e6): ListType { + return new ListType({elementType, limit}); +} + +/** + * SSZ factory helper + typed to return responses of type + * ``` + * data: T + * ``` + */ +export function ContainerData(dataType: Type): ContainerType<{data: T}> { + return new ContainerType({fields: {data: dataType}}); +} + +/** + * SSZ factory helper + typed to return responses of type + * ``` + * data: T + * version: ForkName + * ``` + */ +export function WithVersion(getType: (fork: ForkName) => TypeJson): TypeJson<{data: T; version: ForkName}> { + return { + toJson: ({data, version}, opts) => ({ + data: getType(version).toJson(data, opts), + version, + }), + fromJson: ({data, version}: {data: Json; version: string}, opts) => ({ + data: getType(version as ForkName).fromJson(data, opts), + version: version as ForkName, + }), + }; +} + +/** Helper to only translate casing */ +export function jsonType | Record[]>(): TypeJson { + return { + toJson: (val, opts) => objectToExpectedCase(val, opts?.case) as Json, + fromJson: (json) => objectToExpectedCase(json as Record, codeCase) as T, + }; +} + +/** Helper to not do any transformation with the type */ +export function sameType(): TypeJson { + return { + toJson: (val) => (val as unknown) as Json, + fromJson: (json) => (json as unknown) as T, + }; +} diff --git a/packages/api/src/utils/urlFormat.ts b/packages/api/src/utils/urlFormat.ts new file mode 100644 index 0000000000..7481bff97a --- /dev/null +++ b/packages/api/src/utils/urlFormat.ts @@ -0,0 +1,74 @@ +enum TokenType { + String = "String", + Variable = "Variable", +} +type Token = {type: TokenType; start: number}; + +type Args = Record; + +/** + * Compile a route URL formater with syntax `/path/:var1/:var2`. + * Returns a function that expects an object `{var1: 1, var2: 2}`, and returns`/path/1/2`. + * + * It's cheap enough to be neglibible. For the sample input below it costs: + * - compile: 1010 ns / op + * - execute: 105 ns / op + * - execute with template literal: 12 ns / op + * @param path `/eth/v1/validator/:name/attester/:epoch` + */ +export function compileRouteUrlFormater(path: string): (arg: Args) => string { + const tokens: Token[] = []; + + for (let i = 0, len = path.length; i < len; i++) { + const currentToken: Token | undefined = tokens[tokens.length - 1]; + switch (path[i]) { + case ":": { + if (currentToken && currentToken.type === TokenType.Variable) { + throw Error(`Invalid path token ':' not closed: ${path}`); + } + tokens.push({type: TokenType.Variable, start: i}); + break; + } + + case "/": { + if (!currentToken || currentToken.type === TokenType.Variable) { + tokens.push({type: TokenType.String, start: i}); + } + break; + } + + default: { + if (!currentToken) { + tokens.push({type: TokenType.String, start: i}); + } + } + } + } + + // Return a faster function if there's not ':' token + if (tokens.length === 1 && tokens[0].type === TokenType.String) { + return () => path; + } + + const fns = tokens.map((token, i) => { + const ending = tokens[i + 1] ? tokens[i + 1].start : path.length; + const part = path.slice(token.start, ending); + + switch (token.type) { + case TokenType.String: + return () => part; + + case TokenType.Variable: { + const argKey = part.slice(1); // remove prepended ":" + return (args: Args) => args[argKey]; + } + } + }); + + return function (args: Args) { + // Don't use .map() or .join(), it's x3 slower + let s = ""; + for (const fn of fns) s += fn(args); + return s; + }; +} diff --git a/packages/api/test/parser.test.ts b/packages/api/test/parser.test.ts new file mode 100644 index 0000000000..f6753d193e --- /dev/null +++ b/packages/api/test/parser.test.ts @@ -0,0 +1,32 @@ +import {compileRouteUrlFormater} from "../src/utils/urlFormat"; + +/* eslint-disable no-console */ + +describe("route parse", () => { + it.skip("Benchmark compileRouteUrlFormater", () => { + const path = "/eth/v1/validator/:name/attester/:epoch"; + const args = {epoch: 5, name: "HEAD"}; + + console.time("compile"); + for (let i = 0; i < 1e6; i++) { + compileRouteUrlFormater(path); + } + console.timeEnd("compile"); + + const fn = compileRouteUrlFormater(path); + + console.log(fn(args)); + + console.time("execute"); + for (let i = 0; i < 1e6; i++) { + fn(args); + } + console.timeEnd("execute"); + + console.time("execute-template"); + for (let i = 0; i < 1e6; i++) { + `/eth/v1/validator/${args.name}/attester/${args.epoch}`; + } + console.timeEnd("execute-template"); + }); +}); diff --git a/packages/api/test/unit/beacon.test.ts b/packages/api/test/unit/beacon.test.ts new file mode 100644 index 0000000000..64df73a69a --- /dev/null +++ b/packages/api/test/unit/beacon.test.ts @@ -0,0 +1,175 @@ +import {ForkName} from "@chainsafe/lodestar-config"; +import {config} from "@chainsafe/lodestar-config/minimal"; +import {toHexString} from "@chainsafe/ssz"; +import {Api, ReqTypes, BlockHeaderResponse, ValidatorResponse} from "../../src/routes/beacon"; +import {getClient} from "../../src/client/beacon"; +import {getRoutes} from "../../src/server/beacon"; +import {runGenericServerTest} from "../utils/genericServerTest"; + +describe("beacon", () => { + const root = Buffer.alloc(32, 1); + const balance = BigInt(32e9); + const pubkeyHex = toHexString(Buffer.alloc(48, 1)); + + const blockHeaderResponse: BlockHeaderResponse = { + root, + canonical: true, + header: config.types.phase0.SignedBeaconBlockHeader.defaultValue(), + }; + + const validatorResponse: ValidatorResponse = { + index: 1, + balance, + status: "active_ongoing", + validator: config.types.phase0.Validator.defaultValue(), + }; + + runGenericServerTest(config, getClient, getRoutes, { + // block + + getBlock: { + args: ["head"], + res: {data: config.types.phase0.SignedBeaconBlock.defaultValue()}, + }, + getBlockV2: { + args: ["head"], + res: {data: config.types.altair.SignedBeaconBlock.defaultValue(), version: ForkName.altair}, + }, + getBlockAttestations: { + args: ["head"], + res: {data: [config.types.phase0.Attestation.defaultValue()]}, + }, + getBlockHeader: { + args: ["head"], + res: {data: blockHeaderResponse}, + }, + getBlockHeaders: { + args: [{slot: 1, parentRoot: toHexString(root)}], + res: {data: [blockHeaderResponse]}, + }, + getBlockRoot: { + args: ["head"], + res: {data: root}, + }, + publishBlock: { + args: [config.types.phase0.SignedBeaconBlock.defaultValue()], + res: undefined, + }, + + // pool + + getPoolAttestations: { + args: [{slot: 1, committeeIndex: 2}], + res: {data: [config.types.phase0.Attestation.defaultValue()]}, + }, + getPoolAttesterSlashings: { + args: [], + res: {data: [config.types.phase0.AttesterSlashing.defaultValue()]}, + }, + getPoolProposerSlashings: { + args: [], + res: {data: [config.types.phase0.ProposerSlashing.defaultValue()]}, + }, + getPoolVoluntaryExits: { + args: [], + res: {data: [config.types.phase0.SignedVoluntaryExit.defaultValue()]}, + }, + submitPoolAttestations: { + args: [[config.types.phase0.Attestation.defaultValue()]], + res: undefined, + }, + submitPoolAttesterSlashing: { + args: [config.types.phase0.AttesterSlashing.defaultValue()], + res: undefined, + }, + submitPoolProposerSlashing: { + args: [config.types.phase0.ProposerSlashing.defaultValue()], + res: undefined, + }, + submitPoolVoluntaryExit: { + args: [config.types.phase0.SignedVoluntaryExit.defaultValue()], + res: undefined, + }, + submitPoolSyncCommitteeSignatures: { + args: [[config.types.altair.SyncCommitteeSignature.defaultValue()]], + res: undefined, + }, + + // state + + getStateRoot: { + args: ["head"], + res: {data: root}, + }, + getStateFork: { + args: ["head"], + res: {data: config.types.phase0.Fork.defaultValue()}, + }, + getStateFinalityCheckpoints: { + args: ["head"], + res: { + data: { + previousJustified: config.types.phase0.Checkpoint.defaultValue(), + currentJustified: config.types.phase0.Checkpoint.defaultValue(), + finalized: config.types.phase0.Checkpoint.defaultValue(), + }, + }, + }, + getStateValidators: { + args: ["head", {indices: [pubkeyHex, "1300"], statuses: ["active_ongoing"]}], + res: {data: [validatorResponse]}, + }, + getStateValidator: { + args: ["head", pubkeyHex], + res: {data: validatorResponse}, + }, + getStateValidatorBalances: { + args: ["head", ["1300"]], + res: {data: [{index: 1300, balance}]}, + }, + getEpochCommittees: { + args: ["head", {index: 1, slot: 2, epoch: 3}], + res: {data: [{index: 1, slot: 2, validators: [1300]}]}, + }, + getEpochSyncCommittees: { + args: ["head", 1], + res: {data: {validators: [1300], validatorAggregates: [1300]}}, + }, + + // - + + getGenesis: { + args: [], + res: {data: config.types.phase0.Genesis.defaultValue()}, + }, + }); + + // TODO: Extra tests to implement maybe + + // getBlockHeaders + // - fetch without filters + // - parse slot param + // - parse parentRoot param + // - throw validation error on invalid slot + // - throw validation error on invalid parentRoot - not hex + // - throw validation error on invalid parentRoot - incorrect length + // - throw validation error on invalid parentRoot - missing 0x prefix + + // getEpochCommittees + // - succeed without filters + // - succeed with filters + // - throw validation error on string slot + // - throw validation error on negative epoch + + // getStateValidator + // - should get by root + // - should get by index + // - should not found state + + // getStateValidatorsBalances + // - success with indices filter + + // All others: + // - Failed to parse body + // - should not found state +}); diff --git a/packages/api/test/unit/config.test.ts b/packages/api/test/unit/config.test.ts new file mode 100644 index 0000000000..320a387aaa --- /dev/null +++ b/packages/api/test/unit/config.test.ts @@ -0,0 +1,28 @@ +import {config} from "@chainsafe/lodestar-config/minimal"; +import {BeaconParams} from "@chainsafe/lodestar-params"; +import {Api, ReqTypes} from "../../src/routes/config"; +import {getClient} from "../../src/client/config"; +import {getRoutes} from "../../src/server/config"; +import {runGenericServerTest} from "../utils/genericServerTest"; + +describe("config", () => { + runGenericServerTest(config, getClient, getRoutes, { + getDepositContract: { + args: [], + res: { + data: { + chainId: 1, + address: Buffer.alloc(20, 1), + }, + }, + }, + getForkSchedule: { + args: [], + res: {data: [config.types.phase0.Fork.defaultValue()]}, + }, + getSpec: { + args: [], + res: {data: BeaconParams.defaultValue()}, + }, + }); +}); diff --git a/packages/api/test/unit/debug.test.ts b/packages/api/test/unit/debug.test.ts new file mode 100644 index 0000000000..dc952c3bdc --- /dev/null +++ b/packages/api/test/unit/debug.test.ts @@ -0,0 +1,67 @@ +import {ForkName} from "@chainsafe/lodestar-config"; +import {fetch} from "cross-fetch"; +import {config} from "@chainsafe/lodestar-config/minimal"; +import {Api, ReqTypes, routesData} from "../../src/routes/debug"; +import {getClient} from "../../src/client/debug"; +import {getRoutes} from "../../src/server/debug"; +import {runGenericServerTest} from "../utils/genericServerTest"; +import {getMockApi, getTestServer} from "../utils/utils"; +import {registerRoutesGroup} from "../../src/server"; +import {expect} from "chai"; + +const root = Buffer.alloc(32, 1); + +describe("debug", () => { + runGenericServerTest(config, getClient, getRoutes, { + getHeads: { + args: [], + res: {data: [{slot: 1, root}]}, + }, + getState: { + args: ["head"], + res: {data: config.types.phase0.BeaconState.defaultValue()}, + }, + getStateV2: { + args: ["head"], + res: {data: config.types.altair.BeaconState.defaultValue(), version: ForkName.altair}, + }, + connectToPeer: { + args: ["peerId", ["multiaddr1", "multiaddr2"]], + res: undefined, + }, + disconnectPeer: { + args: ["peerId"], + res: undefined, + }, + }); + + // Get state by SSZ + + describe("get SSZ response", () => { + const {baseUrl, server} = getTestServer(); + const mockApi = getMockApi(routesData); + const routes = getRoutes(config, mockApi); + registerRoutesGroup(server, routes); + + it("getState", async () => { + const state = config.types.phase0.BeaconState.defaultValue(); + mockApi.getState.resolves({data: state}); + + const url = baseUrl + routesData.getState.url; + const res = await fetch(url, { + method: routesData.getState.method, + headers: {accept: "application/octet-stream"}, + }); + if (!res.ok) throw Error(res.statusText); + const arrayBuffer = await res.arrayBuffer(); + + expect(res.headers.get("Content-Type")).to.equal("application/octet-stream", "Wrong Content-Type header value"); + + const stateRes = config.types.phase0.BeaconState.deserialize(new Uint8Array(arrayBuffer)); + expect(config.types.phase0.BeaconState.toJson(state)).to.deep.equal( + config.types.phase0.BeaconState.toJson(stateRes), + "returned state value is not equal" + ); + }); + }); +}); diff --git a/packages/api/test/unit/events.test.ts b/packages/api/test/unit/events.test.ts new file mode 100644 index 0000000000..54d193f00e --- /dev/null +++ b/packages/api/test/unit/events.test.ts @@ -0,0 +1,80 @@ +import {AbortController} from "abort-controller"; +import {sleep} from "@chainsafe/lodestar-utils"; +import {config} from "@chainsafe/lodestar-config/minimal"; +import {Api, routesData, EventType, BeaconEvent} from "../../src/routes/events"; +import {getClient} from "../../src/client/events"; +import {getRoutes} from "../../src/server/events"; +import {getMockApi, getTestServer} from "../utils/utils"; +import {registerRoutesGroup} from "../../src/server"; +import {expect} from "chai"; + +const root = Buffer.alloc(32, 1); + +describe("events", () => { + const {baseUrl, server} = getTestServer(); + const mockApi = getMockApi(routesData); + const routes = getRoutes(config, mockApi); + registerRoutesGroup(server, routes); + + let controller: AbortController; + beforeEach(() => (controller = new AbortController())); + afterEach(() => controller.abort()); + + it("Receive events", async () => { + const eventHead1: BeaconEvent = { + type: EventType.head, + message: { + slot: 1, + block: root, + state: root, + epochTransition: false, + previousDutyDependentRoot: root, + currentDutyDependentRoot: root, + }, + }; + const eventHead2: BeaconEvent = { + type: EventType.head, + message: { + slot: 2, + block: root, + state: root, + epochTransition: true, + previousDutyDependentRoot: root, + currentDutyDependentRoot: root, + }, + }; + const eventChainReorg: BeaconEvent = { + type: EventType.chainReorg, + message: { + slot: 3, + depth: 2, + oldHeadBlock: root, + newHeadBlock: root, + oldHeadState: root, + newHeadState: root, + epoch: 1, + }, + }; + + const eventsToSend: BeaconEvent[] = [eventHead1, eventHead2, eventChainReorg]; + const eventsReceived: BeaconEvent[] = []; + + await new Promise((resolve) => { + mockApi.eventstream.callsFake(async (topics, signal, onEvent) => { + for (const event of eventsToSend) { + onEvent(event); + await sleep(5); + } + }); + + // Capture them on the client + const client = getClient(config, baseUrl); + client.eventstream([EventType.head, EventType.chainReorg], controller.signal, (event) => { + eventsReceived.push(event); + if (eventsReceived.length >= eventsToSend.length) resolve(); + }); + }); + + expect(eventsReceived).to.deep.equal(eventsToSend, "Wrong received events"); + }); +}); diff --git a/packages/api/test/unit/lightclient.test.ts b/packages/api/test/unit/lightclient.test.ts new file mode 100644 index 0000000000..a3a8f6b0bc --- /dev/null +++ b/packages/api/test/unit/lightclient.test.ts @@ -0,0 +1,43 @@ +import {config} from "@chainsafe/lodestar-config/minimal"; +import {ProofType} from "@chainsafe/persistent-merkle-tree"; +import {Api, ReqTypes} from "../../src/routes/lightclient"; +import {getClient} from "../../src/client/lightclient"; +import {getRoutes} from "../../src/server/lightclient"; +import {runGenericServerTest} from "../utils/genericServerTest"; + +const root = Uint8Array.from(Buffer.alloc(32, 1)); + +describe("lightclient", () => { + const lightClientUpdate = config.types.altair.LightClientUpdate.defaultValue(); + + runGenericServerTest(config, getClient, getRoutes, { + getStateProof: { + args: [ + "head", + [ + ["validator", 0, "balance"], + ["finalized_checkpoint", "root"], + ], + ], + res: { + data: { + type: ProofType.treeOffset, + offsets: [1, 2, 3], + leaves: [root, root, root, root], + }, + }, + }, + getBestUpdates: { + args: [1, 2], + res: {data: [lightClientUpdate]}, + }, + getLatestUpdateFinalized: { + args: [], + res: {data: lightClientUpdate}, + }, + getLatestUpdateNonFinalized: { + args: [], + res: {data: lightClientUpdate}, + }, + }); +}); diff --git a/packages/api/test/unit/node.test.ts b/packages/api/test/unit/node.test.ts new file mode 100644 index 0000000000..94506e5606 --- /dev/null +++ b/packages/api/test/unit/node.test.ts @@ -0,0 +1,62 @@ +import {config} from "@chainsafe/lodestar-config/minimal"; +import {Api, ReqTypes, NodePeer} from "../../src/routes/node"; +import {getClient} from "../../src/client/node"; +import {getRoutes} from "../../src/server/node"; +import {runGenericServerTest} from "../utils/genericServerTest"; + +describe("node", () => { + const peerIdStr = "peerId"; + const nodePeer: NodePeer = { + peerId: peerIdStr, + enr: "enr", + lastSeenP2pAddress: "lastSeenP2pAddress", + state: "connected", + direction: "inbound", + }; + + runGenericServerTest(config, getClient, getRoutes, { + getNetworkIdentity: { + args: [], + res: { + data: { + peerId: peerIdStr, + enr: "enr", + p2pAddresses: ["p2pAddresses"], + discoveryAddresses: ["discoveryAddresses"], + metadata: config.types.altair.Metadata.defaultValue(), + }, + }, + }, + getPeers: { + args: [{state: ["connected", "disconnected"], direction: ["inbound"]}], + res: {data: [nodePeer], meta: {count: 1}}, + }, + getPeer: { + args: [peerIdStr], + res: {data: nodePeer}, + }, + getPeerCount: { + args: [], + res: { + data: { + disconnected: 1, + connecting: 2, + connected: 3, + disconnecting: 4, + }, + }, + }, + getNodeVersion: { + args: [], + res: {data: {version: "Lodestar/v0.20.0"}}, + }, + getSyncingStatus: { + args: [], + res: {data: {headSlot: 1, syncDistance: 2}}, + }, + getHealth: { + args: [], + res: undefined, + }, + }); +}); diff --git a/packages/api/test/unit/utils/httpClient.test.ts b/packages/api/test/unit/utils/httpClient.test.ts new file mode 100644 index 0000000000..4da91cb0fc --- /dev/null +++ b/packages/api/test/unit/utils/httpClient.test.ts @@ -0,0 +1,154 @@ +import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils"; +import {AbortController} from "abort-controller"; +import chai, {expect} from "chai"; +import chaiAsPromised from "chai-as-promised"; +import fastify, {RouteOptions} from "fastify"; +import {IncomingMessage} from "http"; +import {HttpClient, HttpError} from "../../../src/client/utils"; + +chai.use(chaiAsPromised); + +interface IUser { + id?: number; + name: string; +} + +describe("httpClient test", () => { + const afterEachCallbacks: (() => Promise | any)[] = []; + afterEach(async () => { + while (afterEachCallbacks.length > 0) { + const callback = afterEachCallbacks.pop(); + if (callback) await callback(); + } + }); + + const testRoute = {url: "/test-route", method: "GET" as const}; + + async function getServer(opts: RouteOptions): Promise<{baseUrl: string}> { + const server = fastify({logger: false}); + server.route(opts); + + const reqs = new Set(); + server.addHook("onRequest", async (req) => reqs.add(req.raw)); + afterEachCallbacks.push(async () => { + for (const req of reqs) req.destroy(); + await server.close(); + }); + + return {baseUrl: await server.listen(0)}; + } + + async function getServerWithClient(opts: RouteOptions): Promise { + const {baseUrl} = await getServer(opts); + return new HttpClient({baseUrl}); + } + + it("should handle successful GET request correctly", async () => { + const url = "/test-get"; + const httpClient = await getServerWithClient({ + url, + method: "GET", + handler: async () => ({test: 1}), + }); + + const resBody: IUser = await httpClient.json({url, method: "GET"}); + + expect(resBody).to.deep.equal({test: 1}, "Wrong res body"); + }); + + it("should handle successful POST request correctly", async () => { + const query = {a: "a", b: ["b1", "b2"]}; + const body = {c: 4}; + const resBody = {test: 1}; + let queryReceived: any; + let bodyReceived: any; + + const url = "/test-post"; + const httpClient = await getServerWithClient({ + url, + method: "POST", + handler: async (req) => { + queryReceived = req.query; + bodyReceived = req.body; + return resBody; + }, + }); + + const resBodyReceived: IUser = await httpClient.json({url, method: "POST", query, body}); + + expect(resBodyReceived).to.deep.equal(resBody, "Wrong resBody"); + expect(queryReceived).to.deep.equal(query, "Wrong query"); + expect(bodyReceived).to.deep.equal(body, "Wrong body"); + }); + + it("should handle http status code 404 correctly", async () => { + const httpClient = await getServerWithClient({ + url: "/no-route", + method: "GET", + handler: async () => ({}), + }); + + try { + await httpClient.json(testRoute); + return Promise.reject(Error("did not throw")); // So it doesn't gets catch {} + } catch (e) { + if (!(e instanceof HttpError)) throw Error(`Not an HttpError: ${(e as Error).message}`); + expect(e.message).to.equal("Not Found: Route GET:/test-route not found", "Wrong error message"); + expect(e.status).to.equal(404, "Wrong error status code"); + } + }); + + it("should handle http status code 500 correctly", async () => { + const httpClient = await getServerWithClient({ + ...testRoute, + handler: async () => { + throw Error("Test error"); + }, + }); + + try { + await httpClient.json(testRoute); + return Promise.reject(Error("did not throw")); + } catch (e) { + if (!(e instanceof HttpError)) throw Error(`Not an HttpError: ${(e as Error).message}`); + expect(e.message).to.equal("Internal Server Error: Test error"); + expect(e.status).to.equal(500, "Wrong error status code"); + } + }); + + it("should handle aborting request with timeout", async () => { + const {baseUrl} = await getServer({ + ...testRoute, + handler: async () => new Promise((r) => setTimeout(r, 1000)), + }); + + const httpClient = new HttpClient({baseUrl, timeoutMs: 10}); + + try { + await httpClient.json(testRoute); + return Promise.reject(Error("did not throw")); + } catch (e) { + if (!(e instanceof TimeoutError)) throw Error(`Not an TimeoutError: ${(e as Error).message}`); + } + }); + + it("should handle aborting all request with general AbortController", async () => { + const {baseUrl} = await getServer({ + ...testRoute, + handler: async () => new Promise((r) => setTimeout(r, 1000)), + }); + + const controller = new AbortController(); + const signal = controller.signal; + const httpClient = new HttpClient({baseUrl, getAbortSignal: () => signal}); + + setTimeout(() => controller.abort(), 10); + + try { + await httpClient.json(testRoute); + return Promise.reject(Error("did not throw")); + } catch (e) { + if (!(e instanceof ErrorAborted)) throw Error(`Not an ErrorAborted: ${(e as Error).message}`); + } + }); +}); diff --git a/packages/api/test/unit/validator.test.ts b/packages/api/test/unit/validator.test.ts new file mode 100644 index 0000000000..c2ac07e103 --- /dev/null +++ b/packages/api/test/unit/validator.test.ts @@ -0,0 +1,93 @@ +import {ForkName} from "@chainsafe/lodestar-config"; +import {config} from "@chainsafe/lodestar-config/minimal"; +import {Api, ReqTypes} from "../../src/routes/validator"; +import {getClient} from "../../src/client/validator"; +import {getRoutes} from "../../src/server/validator"; +import {runGenericServerTest} from "../utils/genericServerTest"; + +const ZERO_HASH = Buffer.alloc(32, 0); + +describe("validator", () => { + runGenericServerTest(config, getClient, getRoutes, { + getAttesterDuties: { + args: [1000, [1, 2, 3]], + res: { + data: [ + { + pubkey: Buffer.alloc(48, 1), + validatorIndex: 2, + committeeIndex: 3, + committeeLength: 4, + committeesAtSlot: 5, + validatorCommitteeIndex: 6, + slot: 7, + }, + ], + dependentRoot: ZERO_HASH, + }, + }, + getProposerDuties: { + args: [1000], + res: {data: [{slot: 1, validatorIndex: 2, pubkey: Buffer.alloc(48, 3)}], dependentRoot: ZERO_HASH}, + }, + getSyncCommitteeDuties: { + args: [1000, [1, 2, 3]], + res: { + data: [{pubkey: Buffer.alloc(48, 1), validatorIndex: 2, validatorSyncCommitteeIndices: [3]}], + dependentRoot: ZERO_HASH, + }, + }, + produceBlock: { + args: [32000, Buffer.alloc(96, 1), "graffiti"], + res: {data: config.types.phase0.BeaconBlock.defaultValue(), version: ForkName.phase0}, + }, + produceAttestationData: { + args: [2, 32000], + res: {data: config.types.phase0.AttestationData.defaultValue()}, + }, + produceSyncCommitteeContribution: { + args: [32000, 2, ZERO_HASH], + res: {data: config.types.altair.SyncCommitteeContribution.defaultValue()}, + }, + getAggregatedAttestation: { + args: [ZERO_HASH, 32000], + res: {data: config.types.phase0.Attestation.defaultValue()}, + }, + publishAggregateAndProofs: { + args: [[config.types.phase0.SignedAggregateAndProof.defaultValue()]], + res: undefined, + }, + publishContributionAndProofs: { + args: [[config.types.altair.SignedContributionAndProof.defaultValue()]], + res: undefined, + }, + prepareBeaconCommitteeSubnet: { + args: [[{validatorIndex: 1, committeeIndex: 2, committeesAtSlot: 3, slot: 4, isAggregator: true}]], + res: undefined, + }, + prepareSyncCommitteeSubnets: { + args: [[{validatorIndex: 1, syncCommitteeIndices: [2], untilEpoch: 3}]], + res: undefined, + }, + }); + + // TODO: Extra tests to implement maybe + + // getAttesterDuties + // - throw validation error on invalid epoch "a" + // - throw validation error on no validator indices + // - throw validation error on invalid validator index "a" + + // getProposerDuties + // - throw validation error on invalid epoch "a" + + // prepareBeaconCommitteeSubnet + // - throw validation error on missing param + + // produceAttestationData + // - throw validation error on missing param + + // produceBlock + // - throw validation error on missing randao reveal + // - throw validation error on invalid slot +}); diff --git a/packages/api/test/utils/genericServerTest.ts b/packages/api/test/utils/genericServerTest.ts new file mode 100644 index 0000000000..0538aa19e0 --- /dev/null +++ b/packages/api/test/utils/genericServerTest.ts @@ -0,0 +1,56 @@ +import {expect} from "chai"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {RouteGeneric, ReqGeneric, Resolves} from "../../src/utils"; +import {HttpClient, IHttpClient} from "../../src/client/utils"; +import {ServerRoutes} from "../../src/server/utils"; +import {getMockApi, getTestServer} from "./utils"; +import {registerRoutesGroup} from "../../src/server"; + +type IgnoreVoid = T extends void ? undefined : T; + +export type GenericServerTestCases> = { + [K in keyof Api]: { + args: Parameters; + res: IgnoreVoid>; + }; +}; + +export function runGenericServerTest< + Api extends Record, + ReqTypes extends {[K in keyof Api]: ReqGeneric} +>( + config: IBeaconConfig, + getClient: (config: IBeaconConfig, https: IHttpClient) => Api, + getRoutes: (config: IBeaconConfig, api: Api) => ServerRoutes, + testCases: GenericServerTestCases +): void { + const mockApi = getMockApi(testCases); + const {baseUrl, server} = getTestServer(); + + const httpClient = new HttpClient({baseUrl}); + const client = getClient(config, httpClient); + + const routes = getRoutes(config, mockApi); + registerRoutesGroup(server, routes); + + for (const key of Object.keys(testCases)) { + const routeId = key as keyof Api; + const testCase = testCases[routeId]; + + it(routeId as string, async () => { + // Register mock data for this route + mockApi[routeId].resolves(testCases[routeId].res as any); + + // Do the call + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const res = await (client[routeId] as RouteGeneric)(...(testCase.args as any[])); + + // Assert server handler called with correct args + expect(mockApi[routeId].callCount).to.equal(1, `mockApi[${routeId}] must be called once`); + expect(mockApi[routeId].getCall(0).args).to.deep.equal(testCase.args, `mockApi[${routeId}] wrong args`); + + // Assert returned value is correct + expect(res).to.deep.equal(testCase.res, "Wrong returned value"); + }); + } +} diff --git a/packages/api/test/utils/utils.ts b/packages/api/test/utils/utils.ts new file mode 100644 index 0000000000..f9dfe7a703 --- /dev/null +++ b/packages/api/test/utils/utils.ts @@ -0,0 +1,42 @@ +import fastify, {FastifyInstance} from "fastify"; +import querystring from "querystring"; +import {mapValues} from "@chainsafe/lodestar-utils"; +import Sinon from "sinon"; + +export function getTestServer(): {baseUrl: string; server: FastifyInstance} { + const port = Math.floor(Math.random() * (65535 - 49152)) + 49152; + const baseUrl = `http://127.0.0.1:${port}`; + + const server = fastify({ + ajv: {customOptions: {coerceTypes: "array"}}, + querystringParser: querystring.parse, + }); + + server.addHook("onError", (request, reply, error, done) => { + // eslint-disable-next-line no-console + console.log(error); + done(); + }); + + before("start server", async () => { + await new Promise((resolve, reject) => { + server.listen(port, function (err, address) { + if (err) reject(err); + else resolve(address); + }); + }); + }); + + after("stop server", async () => { + await server.close(); + }); + + return {baseUrl, server}; +} + +/** Type helper to get a Sinon mock object type with Api */ +export function getMockApi>( + routeKeys: Record +): Sinon.SinonStubbedInstance & Api { + return mapValues(routeKeys, () => Sinon.stub()) as Sinon.SinonStubbedInstance & Api; +} diff --git a/packages/api/tsconfig.build.json b/packages/api/tsconfig.build.json new file mode 100644 index 0000000000..92235557ba --- /dev/null +++ b/packages/api/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib" + } +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 0000000000..b29a7b46c4 --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {} +} diff --git a/packages/beacon-state-transition/src/allForks/util/epochContext.ts b/packages/beacon-state-transition/src/allForks/util/epochContext.ts index 76d9c5d720..10290f0ce2 100644 --- a/packages/beacon-state-transition/src/allForks/util/epochContext.ts +++ b/packages/beacon-state-transition/src/allForks/util/epochContext.ts @@ -24,12 +24,25 @@ export type EpochContextOpts = { skipSyncPubkeys?: boolean; }; -export class PubkeyIndexMap extends Map { - get(key: ByteVector): ValidatorIndex | undefined { - return super.get((toHexString(key) as unknown) as ByteVector); +type PubkeyHex = string; + +function toHexStringMaybe(hex: ByteVector | string): string { + return typeof hex === "string" ? hex : toHexString(hex); +} + +export class PubkeyIndexMap { + private readonly map = new Map(); + + get size(): number { + return this.map.size; } - set(key: ByteVector, value: ValidatorIndex): this { - return super.set((toHexString(key) as unknown) as ByteVector, value); + + get(key: ByteVector | PubkeyHex): ValidatorIndex | undefined { + return this.map.get(toHexStringMaybe(key)); + } + + set(key: ByteVector | PubkeyHex, value: ValidatorIndex): void { + this.map.set(toHexStringMaybe(key), value); } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 7b8d4ad8da..862ddde647 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,6 +54,7 @@ "@chainsafe/blst": "^0.2.0", "@chainsafe/discv5": "^0.5.1", "@chainsafe/lodestar": "^0.22.0", + "@chainsafe/lodestar-api": "^0.22.0", "@chainsafe/lodestar-beacon-state-transition": "^0.22.0", "@chainsafe/lodestar-config": "^0.22.0", "@chainsafe/lodestar-db": "^0.22.0", diff --git a/packages/cli/src/cmds/account/cmds/validator/slashingProtection/utils.ts b/packages/cli/src/cmds/account/cmds/validator/slashingProtection/utils.ts index 07638fb638..5028a2d10c 100644 --- a/packages/cli/src/cmds/account/cmds/validator/slashingProtection/utils.ts +++ b/packages/cli/src/cmds/account/cmds/validator/slashingProtection/utils.ts @@ -1,5 +1,6 @@ import {Root} from "@chainsafe/lodestar-types"; -import {ApiClientOverRest, SlashingProtection} from "@chainsafe/lodestar-validator"; +import {getClient} from "@chainsafe/lodestar-api"; +import {SlashingProtection} from "@chainsafe/lodestar-validator"; import {LevelDbController} from "@chainsafe/lodestar-db"; import {YargsError} from "../../../../../util"; import {IGlobalArgs} from "../../../../../options"; @@ -29,11 +30,11 @@ export async function getGenesisValidatorsRoot(args: IGlobalArgs & ISlashingProt const server = args.server; const config = getBeaconConfigFromArgs(args); - const api = ApiClientOverRest(config, server); + const api = getClient(config, {baseUrl: server}); const genesis = await api.beacon.getGenesis(); if (genesis) { - return genesis.genesisValidatorsRoot; + return genesis.data.genesisValidatorsRoot; } else { if (args.force) { return Buffer.alloc(32, 0); diff --git a/packages/cli/src/cmds/dev/handler.ts b/packages/cli/src/cmds/dev/handler.ts index bf556ffd28..a1b7505df4 100644 --- a/packages/cli/src/cmds/dev/handler.ts +++ b/packages/cli/src/cmds/dev/handler.ts @@ -5,7 +5,7 @@ import path from "path"; import {AbortController} from "abort-controller"; import {GENESIS_SLOT} from "@chainsafe/lodestar-params"; import {BeaconNode, BeaconDb, initStateFromAnchorState, createNodeJsLibp2p, nodeUtils} from "@chainsafe/lodestar"; -import {IApiClient, SlashingProtection, Validator} from "@chainsafe/lodestar-validator"; +import {SlashingProtection, Validator} from "@chainsafe/lodestar-validator"; import {LevelDbController} from "@chainsafe/lodestar-db"; import {onGracefulShutdown} from "../../util/process"; import {createEnr, createPeerId} from "../../config"; @@ -104,7 +104,7 @@ export async function devHandler(args: IDevArgs & IGlobalArgs): Promise { const dbPath = path.join(validatorsDbDir, "validators"); fs.mkdirSync(dbPath, {recursive: true}); - const api = args.server === "memory" ? (node.api as IApiClient) : args.server; + const api = args.server === "memory" ? node.api : args.server; const slashingProtection = new SlashingProtection({ config: config, controller: new LevelDbController({name: dbPath}, {logger}), diff --git a/packages/cli/src/cmds/validator/handler.ts b/packages/cli/src/cmds/validator/handler.ts index cab2e207d9..c3b9c3a604 100644 --- a/packages/cli/src/cmds/validator/handler.ts +++ b/packages/cli/src/cmds/validator/handler.ts @@ -1,5 +1,5 @@ import {AbortController} from "abort-controller"; -import {ApiClientOverRest} from "@chainsafe/lodestar-validator"; +import {getClient} from "@chainsafe/lodestar-api"; import {Validator, SlashingProtection} from "@chainsafe/lodestar-validator"; import {LevelDbController} from "@chainsafe/lodestar-db"; import {getBeaconConfigFromArgs} from "../../config"; @@ -44,7 +44,7 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P const controller = new AbortController(); onGracefulShutdownCbs.push(async () => controller.abort()); - const api = ApiClientOverRest(config, args.server); + const api = getClient(config, {baseUrl: args.server}); const slashingProtection = new SlashingProtection({ config: config, controller: new LevelDbController({name: dbPath}, {logger}), diff --git a/packages/cli/test/e2e/cmds/init.test.ts b/packages/cli/test/e2e/cmds/init.test.ts index a103238cfa..80a65127b6 100644 --- a/packages/cli/test/e2e/cmds/init.test.ts +++ b/packages/cli/test/e2e/cmds/init.test.ts @@ -7,7 +7,11 @@ import {getBeaconPaths} from "../../../src/cmds/beacon/paths"; import {depositContractDeployBlock} from "../../../src/networks/pyrmont"; import {testFilesDir} from "../../utils"; import {getLodestarCliTestRunner} from "../commandRunner"; -import {ApiNamespace} from "@chainsafe/lodestar/lib/api"; + +enum ApiNamespace { + DEBUG = "debug", + LODESTAR = "lodestar", +} describe("cmds / init", function () { const lodestar = getLodestarCliTestRunner(); diff --git a/packages/light-client/LICENSE b/packages/light-client/LICENSE index 153d416dc8..f49a4e16e6 100644 --- a/packages/light-client/LICENSE +++ b/packages/light-client/LICENSE @@ -1,165 +1,201 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - 0. Additional Definitions. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - 1. Exception to Section 3 of the GNU GPL. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - 2. Conveying Modified Versions. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - 3. Object Code Incorporating Material from Library Header Files. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. - b) Accompany the object code with a copy of the GNU GPL and this license - document. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - 4. Combined Works. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - d) Do one of the following: + END OF TERMS AND CONDITIONS - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. + APPENDIX: How to apply the Apache License to your work. - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) + Copyright [yyyy] [name of copyright owner] - 5. Combined Libraries. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: + http://www.apache.org/licenses/LICENSE-2.0 - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/light-client/README.md b/packages/light-client/README.md index 94c94aa4cb..4bc2dbc688 100644 --- a/packages/light-client/README.md +++ b/packages/light-client/README.md @@ -1,4 +1,4 @@ -# Lodestar +# Lodestar Light-client [![](https://img.shields.io/travis/com/ChainSafe/lodestar/master.svg?label=master&logo=travis "Master Branch (Travis)")](https://travis-ci.com/ChainSafe/lodestar) [![Discord](https://img.shields.io/discord/593655374469660673.svg?label=Discord&logo=discord)](https://discord.gg/aMxzVcr) @@ -29,4 +29,4 @@ Read our [contributors document](/CONTRIBUTING.md), [submit an issue](https://gi ## License -LGPL-3.0 [ChainSafe Systems](https://chainsafe.io) +Apache-2.0 [ChainSafe Systems](https://chainsafe.io) diff --git a/packages/light-client/package.json b/packages/light-client/package.json index 234a75454f..cbc3839830 100644 --- a/packages/light-client/package.json +++ b/packages/light-client/package.json @@ -2,7 +2,7 @@ "name": "@chainsafe/lodestar-light-client", "private": true, "description": "A Typescript implementation of the eth2 light client", - "license": "LGPL-3.0", + "license": "Apache-2.0", "author": "ChainSafe Systems", "homepage": "https://github.com/ChainSafe/lodestar#readme", "repository": { @@ -37,6 +37,7 @@ }, "dependencies": { "@chainsafe/bls": "6.0.1", + "@chainsafe/lodestar-api": "^0.22.0", "@chainsafe/lodestar-beacon-state-transition": "^0.22.0", "@chainsafe/lodestar-config": "^0.22.0", "@chainsafe/lodestar-params": "^0.22.0", diff --git a/packages/light-client/src/client/apiClient.ts b/packages/light-client/src/client/apiClient.ts deleted file mode 100644 index b119dc2ad5..0000000000 --- a/packages/light-client/src/client/apiClient.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {fetch} from "cross-fetch"; -import {Json} from "@chainsafe/ssz"; -import {deserializeProof, TreeOffsetProof} from "@chainsafe/persistent-merkle-tree"; -import {altair, SyncPeriod, IBeaconSSZTypes} from "@chainsafe/lodestar-types"; - -export type Paths = (string | number)[][]; - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type, @typescript-eslint/naming-convention -export function LightclientApiClient(beaconApiUrl: string, types: IBeaconSSZTypes) { - const prefix = "/eth/v1/lightclient"; - - async function get(url: string): Promise { - const res = await fetch(beaconApiUrl + prefix + url, {method: "GET"}); - const body = (await res.json()) as T; - - if (!res.ok) { - const errorBody = (body as unknown) as {message: string}; - if (typeof errorBody === "object" && errorBody.message) { - throw Error(errorBody.message); - } else { - throw Error(res.statusText); - } - } - - return body; - } - - return { - /** - * GET /eth/v1/lightclient/best_updates/:periods - */ - async getBestUpdates(from: SyncPeriod, to: SyncPeriod): Promise { - const res = await get<{data: Json[]}>(`/best_updates/${from}..${to}`); - return res.data.map((item) => types.altair.LightClientUpdate.fromJson(item, {case: "snake"})); - }, - - /** - * GET /eth/v1/lightclient/latest_update_finalized/ - */ - async getLatestUpdateFinalized(): Promise { - const res = await get<{data: Json}>("/latest_update_finalized/"); - return types.altair.LightClientUpdate.fromJson(res.data, {case: "snake"}); - }, - - /** - * GET /eth/v1/lightclient/latest_update_nonfinalized/ - */ - async getLatestUpdateNonFinalized(): Promise { - const res = await get<{data: Json}>("/latest_update_finalized/"); - return types.altair.LightClientUpdate.fromJson(res.data, {case: "snake"}); - }, - - /** - * POST /eth/v1/lodestar/proof/:stateId - */ - async getStateProof(stateId: string | number, paths: Paths): Promise { - const res = await fetch(beaconApiUrl + prefix + `/proof/${stateId}`, { - method: "POST", - body: JSON.stringify({paths}), - }); - - if (!res.ok) { - const errorBody = (await res.json()) as {message: string}; - if (typeof errorBody === "object" && errorBody.message) { - throw Error(errorBody.message); - } else { - throw Error(res.statusText); - } - } - - const buffer = await res.arrayBuffer(); - - return deserializeProof(new Uint8Array(buffer)) as TreeOffsetProof; - }, - }; -} diff --git a/packages/light-client/src/client/index.ts b/packages/light-client/src/client/index.ts index 4adf7ef996..8f53df6a0a 100644 --- a/packages/light-client/src/client/index.ts +++ b/packages/light-client/src/client/index.ts @@ -1,12 +1,12 @@ import mitt from "mitt"; +import {getClient, Api} from "@chainsafe/lodestar-api"; import {altair, Root, Slot, SyncPeriod} from "@chainsafe/lodestar-types"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {LIGHT_CLIENT_UPDATE_TIMEOUT} from "@chainsafe/lodestar-params"; import {computeSyncPeriodAtSlot, ZERO_HASH} from "@chainsafe/lodestar-beacon-state-transition"; import {TreeOffsetProof} from "@chainsafe/persistent-merkle-tree"; -import {toHexString} from "@chainsafe/ssz"; +import {Path, toHexString} from "@chainsafe/ssz"; import {BeaconBlockHeader} from "@chainsafe/lodestar-types/phase0"; -import {LightclientApiClient, Paths} from "./apiClient"; import {IClock} from "../utils/clock"; import {deserializeSyncCommittee, isEmptyHeader, serializeSyncCommittee, sumBits} from "../utils/utils"; import {LightClientStoreFast} from "./types"; @@ -28,7 +28,7 @@ export type LightclientModules = { const maxPeriodPerRequest = 32; export class Lightclient { - readonly apiClient: ReturnType; + readonly api: Api; readonly emitter: LightclientEmitter = mitt(); readonly config: IBeaconConfig; @@ -42,7 +42,7 @@ export class Lightclient { this.clock = clock; this.genesisValidatorsRoot = genesisValidatorsRoot; this.beaconApiUrl = beaconApiUrl; - this.apiClient = LightclientApiClient(beaconApiUrl, config.types); + this.api = getClient(config, {baseUrl: beaconApiUrl}); this.clock.runEverySlot(this.syncToLatest); } @@ -52,12 +52,13 @@ export class Lightclient { ): Promise { const {config, beaconApiUrl} = modules; const {slot, stateRoot} = trustedRoot; - const apiClient = LightclientApiClient(beaconApiUrl, config.types); + // TODO: Consider initializing only the lightclient namespace + const api = getClient(config, {baseUrl: beaconApiUrl}); const paths = getSyncCommitteesProofPaths(config); - const proof = await apiClient.getStateProof(toHexString(stateRoot), paths); + const proof = await api.lightclient.getStateProof(toHexString(stateRoot), paths); - const state = config.types.altair.BeaconState.createTreeBackedFromProof(stateRoot as Uint8Array, proof); + const state = config.types.altair.BeaconState.createTreeBackedFromProof(stateRoot as Uint8Array, proof.data); const store: LightClientStoreFast = { bestUpdates: new Map(), snapshot: { @@ -99,7 +100,7 @@ export class Lightclient { const currentPeriod = computeSyncPeriodAtSlot(this.config, currentSlot); const periodRanges = chunkifyInclusiveRange(lastPeriod, currentPeriod, maxPeriodPerRequest); for (const [fromPeriod, toPeriod] of periodRanges) { - const updates = await this.apiClient.getBestUpdates(fromPeriod, toPeriod); + const {data: updates} = await this.api.lightclient.getBestUpdates(fromPeriod, toPeriod); for (const update of updates) { this.processLightClientUpdate(update); // Yield to the macro queue, verifying updates is somewhat expensive and we want responsiveness @@ -109,14 +110,16 @@ export class Lightclient { } async syncToLatest(): Promise { - const update = await this.apiClient.getLatestUpdateFinalized(); + const {data: update} = await this.api.lightclient.getLatestUpdateFinalized(); if (update) { this.processLightClientUpdate(update); } } - async getStateProof(paths: Paths): Promise { - return await this.apiClient.getStateProof(toHexString(this.store.snapshot.header.stateRoot), paths); + async getStateProof(paths: Path[]): Promise { + const stateId = toHexString(this.store.snapshot.header.stateRoot); + const res = await this.api.lightclient.getStateProof(stateId, paths); + return res.data as TreeOffsetProof; } onSlot = async (): Promise => { diff --git a/packages/light-client/test/lightclientApiServer.ts b/packages/light-client/test/lightclientApiServer.ts index 3c788579e2..f3db4a412d 100644 --- a/packages/light-client/test/lightclientApiServer.ts +++ b/packages/light-client/test/lightclientApiServer.ts @@ -1,26 +1,16 @@ -import { - DefaultBody, - DefaultHeaders, - DefaultParams, - DefaultQuery, - HTTPMethod, - RequestHandler, - RouteShorthandOptions, -} from "fastify"; -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import {Stream} from "stream"; -import {FastifyRequest} from "fastify"; +import fastify, {FastifyInstance} from "fastify"; +import {Api} from "@chainsafe/lodestar-api"; +import {registerRoutes} from "@chainsafe/lodestar-api/server"; import {ILogger} from "@chainsafe/lodestar-utils"; -import {IncomingMessage, Server, ServerResponse} from "http"; -import fastify, {ServerOptions} from "fastify"; import fastifyCors from "fastify-cors"; import querystring from "querystring"; -import {serializeProof} from "@chainsafe/persistent-merkle-tree"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {LightClientUpdater} from "../src/server/LightClientUpdater"; import {TreeBacked} from "@chainsafe/ssz"; import {altair} from "@chainsafe/lodestar-types"; +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + const maxPeriodsPerRequest = 128; export type IStateRegen = { @@ -39,174 +29,58 @@ export type ServerModules = { stateRegen: IStateRegen; }; -export type ApiController< - Query = DefaultQuery, - Params = DefaultParams, - Body = DefaultBody, - Headers = DefaultHeaders -> = { - url: string; - method: HTTPMethod; - handler: RequestHandler; - schema?: RouteShorthandOptions["schema"]; -}; - -export async function startLightclientApiServer( - opts: ServerOpts, - modules: ServerModules -): Promise { +export async function startLightclientApiServer(opts: ServerOpts, modules: ServerModules): Promise { const server = fastify({ - logger: new FastifyLogger(modules.logger), - ajv: { - customOptions: { - coerceTypes: "array", - }, - }, - querystringParser: querystring.parse as ServerOptions["querystringParser"], + logger: false, + ajv: {customOptions: {coerceTypes: "array"}}, + querystringParser: querystring.parse, }); - server.register(fastifyCors as any, {origin: "*"}); - registerRoutes(server, modules); + const lightclientApi = getLightclientServerApi(modules); + registerRoutes(server, modules.config, {lightclient: lightclientApi} as Api, ["lightclient"]); + + void server.register(fastifyCors, {origin: "*"}); + await server.listen(opts.port, opts.host); return server; } -function registerRoutes(server: fastify.FastifyInstance, modules: ServerModules): void { +function getLightclientServerApi(modules: ServerModules): Api["lightclient"] { const {config, lightClientUpdater, stateRegen} = modules; - const createProof: ApiController = { - url: "/proof/:stateId", - method: "POST", - - handler: async function (req, resp) { - const state = await stateRegen.getStateByRoot(req.params.stateId); - // the body isn't already JSON parsed - const body = JSON.parse((req.body as unknown) as string) as {paths: (string | number)[][]}; + return { + async getStateProof(stateId, paths) { + const state = await stateRegen.getStateByRoot(stateId); const tree = config.types.altair.BeaconState.createTreeBackedFromStruct(state); - const proof = tree.createProof(body.paths); - const serialized = serializeProof(proof); - return resp.status(200).header("Content-Type", "application/octet-stream").send(Buffer.from(serialized)); + return {data: tree.createProof(paths)}; }, - }; - const getBestUpdates: ApiController = { - url: "/best_updates/:periods", - method: "GET", - - handler: async function (req) { - const periods = parsePeriods(req.params.periods); + async getBestUpdates(from, to) { + const periods = linspace(from, to); if (periods.length > maxPeriodsPerRequest) { throw Error("Too many periods requested"); } - const items = await lightClientUpdater.getBestUpdates(periods); - return { - data: items.map((item) => config.types.altair.LightClientUpdate.toJson(item, {case: "snake"})), - }; + return {data: await lightClientUpdater.getBestUpdates(periods)}; }, - }; - const getLatestUpdateFinalized: ApiController = { - url: "/latest_update_finalized/", - method: "GET", - - handler: async function () { + async getLatestUpdateFinalized() { const data = await lightClientUpdater.getLatestUpdateFinalized(); if (!data) throw Error("No update available"); - return { - data: config.types.altair.LightClientUpdate.toJson(data, {case: "snake"}), - }; + return {data}; }, - }; - const getLatestUpdateNonFinalized: ApiController = { - url: "/latest_update_nonfinalized/", - method: "GET", - - handler: async function () { + async getLatestUpdateNonFinalized() { const data = await lightClientUpdater.getLatestUpdateNonFinalized(); if (!data) throw Error("No update available"); - return { - data: config.types.altair.LightClientUpdate.toJson(data, {case: "snake"}), - }; + return {data}; }, }; - - const routes: ApiController[] = [ - createProof, - getBestUpdates, - getLatestUpdateFinalized, - getLatestUpdateNonFinalized, - ]; - - server.register( - async function (fastify) { - for (const route of routes) { - fastify.route({ - url: route.url, - method: route.method, - handler: route.handler, - schema: route.schema, - }); - } - }, - {prefix: "/eth/v1/lightclient"} - ); } -/** - * periods = 1 or = 1..4 - */ -function parsePeriods(periodsArg: string): number[] { - if (periodsArg.includes("..")) { - const [fromStr, toStr] = periodsArg.split(".."); - const from = parseInt(fromStr, 10); - const to = parseInt(toStr, 10); - const periods: number[] = []; - for (let i = from; i <= to; i++) periods.push(i); - return periods; - } else { - const period = parseInt(periodsArg, 10); - return [period]; +function linspace(from: number, to: number): number[] { + const arr: number[] = []; + for (let i = from; i <= to; i++) { + arr.push(i); } -} - -/** - * Logs REST API request/response messages. - */ -export class FastifyLogger { - readonly stream: Stream; - - readonly serializers = { - req: (req: IncomingMessage & FastifyRequest): {msg: string} => { - const url = req.url ? req.url.split("?")[0] : "-"; - return {msg: `Req ${req.id} ${req.ip} ${req.method}:${url}`}; - }, - }; - - private log: ILogger; - - constructor(logger: ILogger) { - this.log = logger; - this.stream = ({ - write: this.handle, - } as unknown) as Stream; - } - - private handle = (chunk: string): void => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const log = JSON.parse(chunk); - if (log.req) { - this.log.debug(log.req.msg); - } else if (log.res) { - this.log.debug(`Res ${log.reqId} - ${log.res.statusCode} ${log.responseTime}`); - } - - if (log.err) { - if (log.level >= 50) { - this.log.error(`Request ${log.reqId} status ${log.res.statusCode}`, {}, log.err); - } else { - this.log.warn(`Request ${log.reqId} status ${log.res.statusCode}`, {}, log.err); - } - } - }; + return arr; } diff --git a/packages/light-client/test/lightclientMockServer.ts b/packages/light-client/test/lightclientMockServer.ts index 042c171760..c8aa5a7e1e 100644 --- a/packages/light-client/test/lightclientMockServer.ts +++ b/packages/light-client/test/lightclientMockServer.ts @@ -1,4 +1,4 @@ -import fastify from "fastify"; +import {FastifyInstance} from "fastify"; import {computeEpochAtSlot, computeSyncPeriodAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {toHexString, TreeBacked} from "@chainsafe/ssz"; @@ -16,7 +16,7 @@ enum ApiStatus { started = "started", stopped = "stopped", } -type ApiState = {status: ApiStatus.started; server: fastify.FastifyInstance} | {status: ApiStatus.stopped}; +type ApiState = {status: ApiStatus.started; server: FastifyInstance} | {status: ApiStatus.stopped}; export class LightclientMockServer { private readonly lightClientUpdater: LightClientUpdater; diff --git a/packages/light-client/tsconfig.build.json b/packages/light-client/tsconfig.build.json index 49aab41014..92235557ba 100644 --- a/packages/light-client/tsconfig.build.json +++ b/packages/light-client/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.build.json", "include": ["src"], "compilerOptions": { - "outDir": "lib", - "typeRoots": ["../../node_modules/@types", "./node_modules/@types"] + "outDir": "lib" } } diff --git a/packages/light-client/tsconfig.json b/packages/light-client/tsconfig.json index c5f850d41f..b29a7b46c4 100644 --- a/packages/light-client/tsconfig.json +++ b/packages/light-client/tsconfig.json @@ -1,6 +1,4 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { - "typeRoots": ["../../node_modules/@types", "./node_modules/@types"] - } + "compilerOptions": {} } diff --git a/packages/lodestar/package.json b/packages/lodestar/package.json index f3b9864d35..92c05fa7cf 100644 --- a/packages/lodestar/package.json +++ b/packages/lodestar/package.json @@ -47,6 +47,7 @@ "dependencies": { "@chainsafe/bls": "6.0.1", "@chainsafe/discv5": "^0.5.1", + "@chainsafe/lodestar-api": "^0.22.0", "@chainsafe/lodestar-beacon-state-transition": "^0.22.0", "@chainsafe/lodestar-config": "^0.22.0", "@chainsafe/lodestar-db": "^0.22.0", @@ -62,13 +63,12 @@ "@types/datastore-level": "^1.1.1", "abort-controller": "^3.0.0", "bl": "^4.0.2", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.4", "datastore-level": "^2.0.0", "deepmerge": "^3.2.0", "es6-promisify": "6.0.2", - "fastify": "2.15.3", - "fastify-cors": "^3.0.3", - "fastify-sse-v2": "^1.0.7", + "fastify": "3.15.1", + "fastify-cors": "^6.0.1", "gc-stats": "^1.4.0", "http-terminator": "^2.0.3", "interface-datastore": "^2.0.0", @@ -105,7 +105,7 @@ "@types/tmp": "^0.2.0", "@types/varint": "^5.0.0", "benchmark": "^2.1.4", - "eventsource": "^1.0.7", + "eventsource": "^1.1.0", "rewiremock": "^3.14.3", "rimraf": "^3.0.2", "tmp": "^0.2.1" diff --git a/packages/lodestar/src/api/impl/api.ts b/packages/lodestar/src/api/impl/api.ts index 193a57e1be..eb42e49abb 100644 --- a/packages/lodestar/src/api/impl/api.ts +++ b/packages/lodestar/src/api/impl/api.ts @@ -1,32 +1,23 @@ -import {IApiOptions} from "../options"; -import {IApi, IApiModules} from "./interface"; -import {IBeaconApi, BeaconApi} from "./beacon"; -import {INodeApi, NodeApi} from "./node"; -import {IValidatorApi, ValidatorApi} from "./validator"; -import {EventsApi, IEventsApi} from "./events"; -import {DebugApi, IDebugApi} from "./debug"; -import {ConfigApi, IConfigApi} from "./config"; -import {LightclientApi, ILightclientApi} from "./lightclient"; -import {LodestarApi, ILodestarApi} from "./lodestar"; +import {Api} from "@chainsafe/lodestar-api"; +import {ApiModules} from "./types"; +import {getBeaconApi} from "./beacon"; +import {getConfigApi} from "./config"; +import {getDebugApi} from "./debug"; +import {getEventsApi} from "./events"; +import {getLightclientApi} from "./lightclient"; +import {getLodestarApi} from "./lodestar"; +import {getNodeApi} from "./node"; +import {getValidatorApi} from "./validator"; -export class Api implements IApi { - beacon: IBeaconApi; - node: INodeApi; - validator: IValidatorApi; - events: IEventsApi; - debug: IDebugApi; - config: IConfigApi; - lightclient: ILightclientApi; - lodestar: ILodestarApi; - - constructor(opts: Partial, modules: IApiModules) { - this.beacon = new BeaconApi(opts, modules); - this.node = new NodeApi(opts, modules); - this.validator = new ValidatorApi(opts, modules); - this.events = new EventsApi(opts, modules); - this.debug = new DebugApi(opts, modules); - this.config = new ConfigApi(opts, modules); - this.lightclient = new LightclientApi(opts, modules); - this.lodestar = new LodestarApi(modules); - } +export function getApi(modules: ApiModules): Api { + return { + beacon: getBeaconApi(modules), + config: getConfigApi(modules), + debug: getDebugApi(modules), + events: getEventsApi(modules), + lightclient: getLightclientApi(modules), + lodestar: getLodestarApi(modules), + node: getNodeApi(modules), + validator: getValidatorApi(modules), + }; } diff --git a/packages/lodestar/src/api/impl/beacon/beacon.ts b/packages/lodestar/src/api/impl/beacon/beacon.ts deleted file mode 100644 index 63117197bd..0000000000 --- a/packages/lodestar/src/api/impl/beacon/beacon.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @module api/rpc - */ - -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {GENESIS_SLOT} from "@chainsafe/lodestar-params"; -import {allForks, phase0} from "@chainsafe/lodestar-types"; -import {LodestarEventIterator} from "@chainsafe/lodestar-utils"; -import {ChainEvent, IBeaconChain} from "../../../chain"; -import {IApiOptions} from "../../options"; -import {ApiNamespace, IApiModules} from "../interface"; -import {BeaconBlockApi, IBeaconBlocksApi} from "./blocks"; -import {IBeaconApi} from "./interface"; -import {BeaconPoolApi, IBeaconPoolApi} from "./pool"; -import {IBeaconStateApi} from "./state/interface"; -import {BeaconStateApi} from "./state/state"; - -export class BeaconApi implements IBeaconApi { - namespace: ApiNamespace; - state: IBeaconStateApi; - blocks: IBeaconBlocksApi; - pool: IBeaconPoolApi; - - private readonly config: IBeaconConfig; - private readonly chain: IBeaconChain; - - constructor(opts: Partial, modules: Pick) { - this.namespace = ApiNamespace.BEACON; - this.config = modules.config; - this.chain = modules.chain; - this.state = new BeaconStateApi(opts, modules); - this.blocks = new BeaconBlockApi(opts, modules); - this.pool = new BeaconPoolApi(opts, modules); - } - - async getGenesis(): Promise { - const genesisForkVersion = this.config.getForkVersion(GENESIS_SLOT); - return { - genesisForkVersion, - genesisTime: BigInt(this.chain.genesisTime), - genesisValidatorsRoot: this.chain.genesisValidatorsRoot, - }; - } - - getBlockStream(): LodestarEventIterator { - return new LodestarEventIterator(({push}) => { - this.chain.emitter.on(ChainEvent.block, push); - return () => { - this.chain.emitter.off(ChainEvent.block, push); - }; - }); - } -} diff --git a/packages/lodestar/src/api/impl/beacon/blocks/index.ts b/packages/lodestar/src/api/impl/beacon/blocks/index.ts index 91e5fd51f3..ec1d4c2bef 100644 --- a/packages/lodestar/src/api/impl/beacon/blocks/index.ts +++ b/packages/lodestar/src/api/impl/beacon/blocks/index.ts @@ -1,136 +1,130 @@ -import {Root, phase0, allForks, Slot} from "@chainsafe/lodestar-types"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; - -import {IBeaconChain} from "../../../../chain"; -import {IBeaconDb} from "../../../../db"; -import {IApiOptions} from "../../../options"; -import {IApiModules} from "../../interface"; -import {BlockId, IBeaconBlocksApi} from "./interface"; +import {routes} from "@chainsafe/lodestar-api"; +import {Api as IBeaconBlocksApi} from "@chainsafe/lodestar-api/lib/routes/beacon/block"; +import {fromHexString} from "@chainsafe/ssz"; +import {ApiModules} from "../../types"; import {resolveBlockId, toBeaconHeaderResponse} from "./utils"; -import {IBeaconSync} from "../../../../sync"; -import {INetwork} from "../../../../network/interface"; -export * from "./interface"; - -export class BeaconBlockApi implements IBeaconBlocksApi { - private readonly config: IBeaconConfig; - private readonly chain: IBeaconChain; - private readonly db: IBeaconDb; - private readonly sync: IBeaconSync; - private readonly network: INetwork; - - constructor(opts: Partial, modules: Pick) { - this.config = modules.config; - this.sync = modules.sync; - this.chain = modules.chain; - this.db = modules.db; - this.network = modules.network; - } - - async getBlockHeaders( - filters: Partial<{slot: Slot; parentRoot: Root}> - ): Promise { - const result: phase0.SignedBeaconHeaderResponse[] = []; - if (filters.parentRoot) { - const finalizedBlock = await this.db.blockArchive.getByParentRoot(filters.parentRoot); - if (finalizedBlock) { - result.push(toBeaconHeaderResponse(this.config, finalizedBlock, true)); - } - const nonFinalizedBlockSummaries = this.chain.forkChoice.getBlockSummariesByParentRoot( - filters.parentRoot.valueOf() as Uint8Array - ); - await Promise.all( - nonFinalizedBlockSummaries.map(async (summary) => { - const block = await this.db.block.get(summary.blockRoot); - if (block) { - const cannonical = this.chain.forkChoice.getCanonicalBlockSummaryAtSlot(block.message.slot); - if (cannonical) { - result.push( - toBeaconHeaderResponse( - this.config, - block, - this.config.types.Root.equals(cannonical.blockRoot, summary.blockRoot) - ) - ); - } - } - }) - ); - return result.filter( - (item) => - // skip if no slot filter - !(filters.slot && filters.slot !== 0) || item.header.message.slot === filters.slot - ); - } - - const headSlot = this.chain.forkChoice.getHead().slot; - if (!filters.parentRoot && !filters.slot && filters.slot !== 0) { - filters.slot = headSlot; - } - - if (filters.slot !== undefined) { - // future slot - if (filters.slot > headSlot) { - return []; - } - - const canonicalBlock = await this.chain.getCanonicalBlockAtSlot(filters.slot); - // skip slot - if (!canonicalBlock) { - return []; - } - const canonicalRoot = this.config - .getForkTypes(canonicalBlock.message.slot) - .BeaconBlock.hashTreeRoot(canonicalBlock.message); - result.push(toBeaconHeaderResponse(this.config, canonicalBlock, true)); - - // fork blocks - await Promise.all( - this.chain.forkChoice.getBlockSummariesAtSlot(filters.slot).map(async (summary) => { - if (!this.config.types.Root.equals(summary.blockRoot, canonicalRoot)) { - const block = await this.db.block.get(summary.blockRoot); +export function getBeaconBlockApi({ + chain, + config, + network, + db, +}: Pick): IBeaconBlocksApi { + return { + async getBlockHeaders(filters) { + const result: routes.beacon.BlockHeaderResponse[] = []; + if (filters.parentRoot) { + const parentRoot = fromHexString(filters.parentRoot); + const finalizedBlock = await db.blockArchive.getByParentRoot(parentRoot); + if (finalizedBlock) { + result.push(toBeaconHeaderResponse(config, finalizedBlock, true)); + } + const nonFinalizedBlockSummaries = chain.forkChoice.getBlockSummariesByParentRoot(parentRoot); + await Promise.all( + nonFinalizedBlockSummaries.map(async (summary) => { + const block = await db.block.get(summary.blockRoot); if (block) { - result.push(toBeaconHeaderResponse(this.config, block)); + const cannonical = chain.forkChoice.getCanonicalBlockSummaryAtSlot(block.message.slot); + if (cannonical) { + result.push( + toBeaconHeaderResponse( + config, + block, + config.types.Root.equals(cannonical.blockRoot, summary.blockRoot) + ) + ); + } } - } - }) - ); - } - - return result; - } - - async getBlockHeader(blockId: BlockId): Promise { - const block = await this.getBlock(blockId); - return toBeaconHeaderResponse(this.config, block, true); - } - - async getBlock(blockId: BlockId): Promise { - return await resolveBlockId(this.chain.forkChoice, this.db, blockId); - } - - async getBlockRoot(blockId: BlockId): Promise { - // Fast path: From head state already available in memory get historical blockRoot - const slot = parseInt(blockId); - if (!Number.isNaN(slot)) { - const head = this.chain.forkChoice.getHead(); - - if (slot === head.slot) { - return head.blockRoot; + }) + ); + return { + data: result.filter( + (item) => + // skip if no slot filter + !(filters.slot && filters.slot !== 0) || item.header.message.slot === filters.slot + ), + }; } - if (slot < head.slot && head.slot <= slot + this.config.params.SLOTS_PER_HISTORICAL_ROOT) { - const state = this.chain.getHeadState(); - return state.blockRoots[slot % this.config.params.SLOTS_PER_HISTORICAL_ROOT]; + const headSlot = chain.forkChoice.getHead().slot; + if (!filters.parentRoot && !filters.slot && filters.slot !== 0) { + filters.slot = headSlot; } - } - // Slow path - const block = await resolveBlockId(this.chain.forkChoice, this.db, blockId); - return this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); - } + if (filters.slot !== undefined) { + // future slot + if (filters.slot > headSlot) { + return {data: []}; + } - async publishBlock(signedBlock: allForks.SignedBeaconBlock): Promise { - await Promise.all([this.chain.receiveBlock(signedBlock), this.network.gossip.publishBeaconBlock(signedBlock)]); - } + const canonicalBlock = await chain.getCanonicalBlockAtSlot(filters.slot); + // skip slot + if (!canonicalBlock) { + return {data: []}; + } + const canonicalRoot = config + .getForkTypes(canonicalBlock.message.slot) + .BeaconBlock.hashTreeRoot(canonicalBlock.message); + result.push(toBeaconHeaderResponse(config, canonicalBlock, true)); + + // fork blocks + await Promise.all( + chain.forkChoice.getBlockSummariesAtSlot(filters.slot).map(async (summary) => { + if (!config.types.Root.equals(summary.blockRoot, canonicalRoot)) { + const block = await db.block.get(summary.blockRoot); + if (block) { + result.push(toBeaconHeaderResponse(config, block)); + } + } + }) + ); + } + + return {data: result}; + }, + + async getBlockHeader(blockId) { + const block = await resolveBlockId(chain.forkChoice, db, blockId); + return {data: toBeaconHeaderResponse(config, block, true)}; + }, + + async getBlock(blockId) { + return {data: await resolveBlockId(chain.forkChoice, db, blockId)}; + }, + + async getBlockV2(blockId) { + const block = await resolveBlockId(chain.forkChoice, db, blockId); + return {data: block, version: config.getForkName(block.message.slot)}; + }, + + async getBlockAttestations(blockId) { + const block = await resolveBlockId(chain.forkChoice, db, blockId); + return {data: Array.from(block.message.body.attestations)}; + }, + + async getBlockRoot(blockId) { + // Fast path: From head state already available in memory get historical blockRoot + const slot = typeof blockId === "string" ? parseInt(blockId) : blockId; + if (!Number.isNaN(slot)) { + const head = chain.forkChoice.getHead(); + + if (slot === head.slot) { + return {data: head.blockRoot}; + } + + if (slot < head.slot && head.slot <= slot + config.params.SLOTS_PER_HISTORICAL_ROOT) { + const state = chain.getHeadState(); + return {data: state.blockRoots[slot % config.params.SLOTS_PER_HISTORICAL_ROOT]}; + } + } + + // Slow path + const block = await resolveBlockId(chain.forkChoice, db, blockId); + return {data: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)}; + }, + + async publishBlock(signedBlock) { + await Promise.all([chain.receiveBlock(signedBlock), network.gossip.publishBeaconBlock(signedBlock)]); + }, + }; } diff --git a/packages/lodestar/src/api/impl/beacon/blocks/interface.ts b/packages/lodestar/src/api/impl/beacon/blocks/interface.ts deleted file mode 100644 index 3b0f38c8e8..0000000000 --- a/packages/lodestar/src/api/impl/beacon/blocks/interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {Root, phase0, allForks, Slot} from "@chainsafe/lodestar-types"; - -export interface IBeaconBlocksApi { - getBlock(blockId: BlockId): Promise; - getBlockHeaders(filters: Partial<{slot: Slot; parentRoot: Root}>): Promise; - getBlockHeader(blockId: BlockId): Promise; - getBlockRoot(blockId: BlockId): Promise; - publishBlock(block: allForks.SignedBeaconBlock): Promise; -} - -export type BlockId = string | "head" | "genesis" | "finalized"; diff --git a/packages/lodestar/src/api/impl/beacon/blocks/utils.ts b/packages/lodestar/src/api/impl/beacon/blocks/utils.ts index 40596eec57..303abf01d2 100644 --- a/packages/lodestar/src/api/impl/beacon/blocks/utils.ts +++ b/packages/lodestar/src/api/impl/beacon/blocks/utils.ts @@ -1,8 +1,8 @@ -import {phase0, allForks} from "@chainsafe/lodestar-types"; +import {allForks} from "@chainsafe/lodestar-types"; +import {routes} from "@chainsafe/lodestar-api"; import {blockToHeader} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; -import {BlockId} from "./interface"; import {IBeaconDb} from "../../../../db"; import {GENESIS_SLOT} from "../../../../constants"; import {fromHexString} from "@chainsafe/ssz"; @@ -12,7 +12,7 @@ export function toBeaconHeaderResponse( config: IBeaconConfig, block: allForks.SignedBeaconBlock, canonical = false -): phase0.SignedBeaconHeaderResponse { +): routes.beacon.BlockHeaderResponse { return { root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message), canonical, @@ -26,7 +26,7 @@ export function toBeaconHeaderResponse( export async function resolveBlockId( forkChoice: IForkChoice, db: IBeaconDb, - blockId: BlockId + blockId: routes.beacon.BlockId ): Promise { const block = await resolveBlockIdOrNull(forkChoice, db, blockId); if (!block) { @@ -39,7 +39,7 @@ export async function resolveBlockId( async function resolveBlockIdOrNull( forkChoice: IForkChoice, db: IBeaconDb, - blockId: BlockId + blockId: routes.beacon.BlockId ): Promise { blockId = String(blockId).toLowerCase(); if (blockId === "head") { diff --git a/packages/lodestar/src/api/impl/beacon/index.ts b/packages/lodestar/src/api/impl/beacon/index.ts index 16d02e85cd..786c71fbc7 100644 --- a/packages/lodestar/src/api/impl/beacon/index.ts +++ b/packages/lodestar/src/api/impl/beacon/index.ts @@ -1,8 +1,31 @@ -/** - * @module api/rpc - */ +import {routes} from "@chainsafe/lodestar-api"; +import {GENESIS_SLOT} from "@chainsafe/lodestar-params"; +import {ApiModules} from "../types"; +import {getBeaconBlockApi} from "./blocks"; +import {getBeaconPoolApi} from "./pool"; +import {getBeaconStateApi} from "./state"; -import {BeaconApi} from "./beacon"; -import {IBeaconApi} from "./interface"; +export function getBeaconApi(modules: Pick): routes.beacon.Api { + const block = getBeaconBlockApi(modules); + const pool = getBeaconPoolApi(modules); + const state = getBeaconStateApi(modules); -export {BeaconApi, IBeaconApi}; + const {chain, config} = modules; + + return { + ...block, + ...pool, + ...state, + + async getGenesis() { + const genesisForkVersion = config.getForkVersion(GENESIS_SLOT); + return { + data: { + genesisForkVersion, + genesisTime: BigInt(chain.genesisTime), + genesisValidatorsRoot: chain.genesisValidatorsRoot, + }, + }; + }, + }; +} diff --git a/packages/lodestar/src/api/impl/beacon/interface.ts b/packages/lodestar/src/api/impl/beacon/interface.ts deleted file mode 100644 index a894cf5e08..0000000000 --- a/packages/lodestar/src/api/impl/beacon/interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @module api/rpc - */ - -import {allForks, phase0} from "@chainsafe/lodestar-types"; -import {IStoppableEventIterable} from "@chainsafe/lodestar-utils"; -import {IBeaconBlocksApi} from "./blocks"; -import {IBeaconPoolApi} from "./pool"; -import {IBeaconStateApi} from "./state/interface"; - -export interface IBeaconApi { - blocks: IBeaconBlocksApi; - state: IBeaconStateApi; - pool: IBeaconPoolApi; - getGenesis(): Promise; - getBlockStream(): IStoppableEventIterable; -} diff --git a/packages/lodestar/src/api/impl/beacon/pool/index.ts b/packages/lodestar/src/api/impl/beacon/pool/index.ts index 4e433db81e..efc09d9a99 100644 --- a/packages/lodestar/src/api/impl/beacon/pool/index.ts +++ b/packages/lodestar/src/api/impl/beacon/pool/index.ts @@ -1,2 +1,134 @@ -export * from "./interface"; -export * from "./pool"; +import {Api as IBeaconPoolApi} from "@chainsafe/lodestar-api/lib/routes/beacon/pool"; +import {Epoch} from "@chainsafe/lodestar-types"; +import {allForks} from "@chainsafe/lodestar-beacon-state-transition"; +import {SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params"; +import {IAttestationJob} from "../../../../chain"; +import {AttestationError, AttestationErrorCode} from "../../../../chain/errors"; +import {validateGossipAttestation} from "../../../../chain/validation"; +import {validateGossipAttesterSlashing} from "../../../../chain/validation/attesterSlashing"; +import {validateGossipProposerSlashing} from "../../../../chain/validation/proposerSlashing"; +import {validateGossipVoluntaryExit} from "../../../../chain/validation/voluntaryExit"; +import {validateSyncCommitteeSigOnly} from "../../../../chain/validation/syncCommittee"; +import {ApiModules} from "../../types"; + +export function getBeaconPoolApi({ + chain, + config, + network, + db, +}: Pick): IBeaconPoolApi { + return { + async getPoolAttestations(filters) { + const attestations = (await db.attestation.values()).filter((attestation) => { + if (filters?.slot && filters?.slot !== attestation.data.slot) { + return false; + } + if (filters?.committeeIndex && filters?.committeeIndex !== attestation.data.index) { + return false; + } + return true; + }); + + return {data: attestations}; + }, + + async getPoolAttesterSlashings() { + return {data: await db.attesterSlashing.values()}; + }, + + async getPoolProposerSlashings() { + return {data: await db.proposerSlashing.values()}; + }, + + async getPoolVoluntaryExits() { + return {data: await db.voluntaryExit.values()}; + }, + + async submitPoolAttestations(attestations) { + for (const attestation of attestations) { + const attestationJob = { + attestation, + validSignature: false, + } as IAttestationJob; + let attestationTargetState; + try { + attestationTargetState = await chain.regen.getCheckpointState(attestation.data.target); + } catch (e) { + throw new AttestationError({ + code: AttestationErrorCode.MISSING_ATTESTATION_TARGET_STATE, + error: e as Error, + job: attestationJob, + }); + } + const subnet = allForks.computeSubnetForAttestation(config, attestationTargetState.epochCtx, attestation); + await validateGossipAttestation(config, chain, db, attestationJob, subnet); + await Promise.all([ + network.gossip.publishBeaconAttestation(attestation, subnet), + db.attestation.add(attestation), + ]); + } + }, + + async submitPoolAttesterSlashing(slashing) { + await validateGossipAttesterSlashing(config, chain, db, slashing); + await Promise.all([network.gossip.publishAttesterSlashing(slashing), db.attesterSlashing.add(slashing)]); + }, + + async submitPoolProposerSlashing(slashing) { + await validateGossipProposerSlashing(config, chain, db, slashing); + await Promise.all([network.gossip.publishProposerSlashing(slashing), db.proposerSlashing.add(slashing)]); + }, + + async submitPoolVoluntaryExit(exit) { + await validateGossipVoluntaryExit(config, chain, db, exit); + await Promise.all([network.gossip.publishVoluntaryExit(exit), db.voluntaryExit.add(exit)]); + }, + + /** + * POST `/eth/v1/beacon/pool/sync_committees` + * + * Submits sync committee signature objects to the node. + * Sync committee signatures are not present in phase0, but are required for Altair networks. + * If a sync committee signature is validated successfully the node MUST publish that sync committee signature on all applicable subnets. + * If one or more sync committee signatures fail validation the node MUST return a 400 error with details of which sync committee signatures have failed, and why. + * + * https://github.com/ethereum/eth2.0-APIs/pull/135 + */ + async submitPoolSyncCommitteeSignatures(signatures) { + // Fetch states for all slots of the `signatures` + const slots = new Set(); + for (const signature of signatures) { + slots.add(signature.slot); + } + + // TODO: Fetch states at signature slots + const state = chain.getHeadState(); + + // TODO: Cache this value + const SYNC_COMMITTEE_SUBNET_SIZE = Math.floor(config.params.SYNC_COMMITTEE_SIZE / SYNC_COMMITTEE_SUBNET_COUNT); + + await Promise.all( + signatures.map(async (signature) => { + const indexesInCommittee = state.currSyncComitteeValidatorIndexMap.get(signature.validatorIndex); + if (indexesInCommittee === undefined || indexesInCommittee.length === 0) { + return; // Not a sync committee member + } + + // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node. + // Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us. + await validateSyncCommitteeSigOnly(chain, state, signature); + + await Promise.all( + indexesInCommittee.map(async (indexInCommittee) => { + // Sync committee subnet members are just sequential in the order they appear in SyncCommitteeIndexes array + const subnet = Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE); + const indexInSubCommittee = indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE; + db.syncCommittee.add(subnet, signature, indexInSubCommittee); + await network.gossip.publishSyncCommitteeSignature(signature, subnet); + }) + ); + }) + ); + }, + }; +} diff --git a/packages/lodestar/src/api/impl/beacon/pool/interface.ts b/packages/lodestar/src/api/impl/beacon/pool/interface.ts deleted file mode 100644 index 0c04033f45..0000000000 --- a/packages/lodestar/src/api/impl/beacon/pool/interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {altair, CommitteeIndex, phase0, Slot} from "@chainsafe/lodestar-types"; - -export interface IAttestationFilters { - slot: Slot; - committeeIndex: CommitteeIndex; -} - -export interface IBeaconPoolApi { - getAttestations(filters?: Partial): Promise; - getAttesterSlashings(): Promise; - getProposerSlashings(): Promise; - getVoluntaryExits(): Promise; - submitAttestations(attestations: phase0.Attestation[]): Promise; - submitAttesterSlashing(slashing: phase0.AttesterSlashing): Promise; - submitProposerSlashing(slashing: phase0.ProposerSlashing): Promise; - submitVoluntaryExit(exit: phase0.SignedVoluntaryExit): Promise; - submitSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise; -} diff --git a/packages/lodestar/src/api/impl/beacon/pool/pool.ts b/packages/lodestar/src/api/impl/beacon/pool/pool.ts deleted file mode 100644 index 7cc8286771..0000000000 --- a/packages/lodestar/src/api/impl/beacon/pool/pool.ts +++ /dev/null @@ -1,144 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {altair, Epoch, phase0} from "@chainsafe/lodestar-types"; -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"; -import {validateGossipAttestation} from "../../../../chain/validation"; -import {validateGossipAttesterSlashing} from "../../../../chain/validation/attesterSlashing"; -import {validateGossipProposerSlashing} from "../../../../chain/validation/proposerSlashing"; -import {validateGossipVoluntaryExit} from "../../../../chain/validation/voluntaryExit"; -import {validateSyncCommitteeSigOnly} from "../../../../chain/validation/syncCommittee"; -import {IBeaconDb} from "../../../../db"; -import {INetwork} from "../../../../network"; -import {IBeaconSync} from "../../../../sync"; -import {IApiOptions} from "../../../options"; -import {IApiModules} from "../../interface"; -import {IAttestationFilters, IBeaconPoolApi} from "./interface"; - -export class BeaconPoolApi implements IBeaconPoolApi { - private readonly config: IBeaconConfig; - private readonly db: IBeaconDb; - private readonly sync: IBeaconSync; - private readonly network: INetwork; - private readonly chain: IBeaconChain; - - constructor(opts: Partial, modules: Pick) { - this.config = modules.config; - this.db = modules.db; - this.sync = modules.sync; - this.network = modules.network; - this.chain = modules.chain; - } - - async getAttestations(filters: Partial = {}): Promise { - return (await this.db.attestation.values()).filter((attestation) => { - if (filters.slot && filters.slot !== attestation.data.slot) { - return false; - } - if (filters.committeeIndex && filters.committeeIndex !== attestation.data.index) { - return false; - } - return true; - }); - } - - async getAttesterSlashings(): Promise { - return this.db.attesterSlashing.values(); - } - - async getProposerSlashings(): Promise { - return this.db.proposerSlashing.values(); - } - - async getVoluntaryExits(): Promise { - return this.db.voluntaryExit.values(); - } - - async submitAttestations(attestations: phase0.Attestation[]): Promise { - for (const attestation of attestations) { - const attestationJob = { - attestation, - validSignature: false, - } as IAttestationJob; - let attestationTargetState; - try { - attestationTargetState = await this.chain.regen.getCheckpointState(attestation.data.target); - } catch (e) { - throw new AttestationError({ - code: AttestationErrorCode.MISSING_ATTESTATION_TARGET_STATE, - error: e as Error, - job: attestationJob, - }); - } - const subnet = allForks.computeSubnetForAttestation(this.config, attestationTargetState.epochCtx, attestation); - await validateGossipAttestation(this.config, this.chain, this.db, attestationJob, subnet); - await Promise.all([ - this.network.gossip.publishBeaconAttestation(attestation, subnet), - this.db.attestation.add(attestation), - ]); - } - } - - async submitAttesterSlashing(slashing: phase0.AttesterSlashing): Promise { - await validateGossipAttesterSlashing(this.config, this.chain, this.db, slashing); - await Promise.all([this.network.gossip.publishAttesterSlashing(slashing), this.db.attesterSlashing.add(slashing)]); - } - - async submitProposerSlashing(slashing: phase0.ProposerSlashing): Promise { - await validateGossipProposerSlashing(this.config, this.chain, this.db, slashing); - await Promise.all([this.network.gossip.publishProposerSlashing(slashing), this.db.proposerSlashing.add(slashing)]); - } - - async submitVoluntaryExit(exit: phase0.SignedVoluntaryExit): Promise { - await validateGossipVoluntaryExit(this.config, this.chain, this.db, exit); - await Promise.all([this.network.gossip.publishVoluntaryExit(exit), this.db.voluntaryExit.add(exit)]); - } - - /** - * POST `/eth/v1/beacon/pool/sync_committees` - * - * Submits sync committee signature objects to the node. - * Sync committee signatures are not present in phase0, but are required for Altair networks. - * If a sync committee signature is validated successfully the node MUST publish that sync committee signature on all applicable subnets. - * If one or more sync committee signatures fail validation the node MUST return a 400 error with details of which sync committee signatures have failed, and why. - * - * https://github.com/ethereum/eth2.0-APIs/pull/135 - */ - async submitSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise { - // Fetch states for all slots of the `signatures` - const slots = new Set(); - for (const signature of signatures) { - slots.add(signature.slot); - } - - // 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); - - await Promise.all( - signatures.map(async (signature) => { - const indexesInCommittee = state.currSyncComitteeValidatorIndexMap.get(signature.validatorIndex); - if (indexesInCommittee === undefined || indexesInCommittee.length === 0) { - return; // Not a sync committee member - } - - // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node. - // Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us. - await validateSyncCommitteeSigOnly(this.chain, state, signature); - - await Promise.all( - indexesInCommittee.map(async (indexInCommittee) => { - // Sync committee subnet members are just sequential in the order they appear in SyncCommitteeIndexes array - const subnet = Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE); - const indexInSubCommittee = indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE; - this.db.syncCommittee.add(subnet, signature, indexInSubCommittee); - await this.network.gossip.publishSyncCommitteeSignature(signature, subnet); - }) - ); - }) - ); - } -} diff --git a/packages/lodestar/src/api/impl/beacon/state/index.ts b/packages/lodestar/src/api/impl/beacon/state/index.ts index e9a74d1ee8..02796a18d0 100644 --- a/packages/lodestar/src/api/impl/beacon/state/index.ts +++ b/packages/lodestar/src/api/impl/beacon/state/index.ts @@ -1,2 +1,184 @@ -export * from "./interface"; -export * from "./state"; +import {routes} from "@chainsafe/lodestar-api"; +import {Api as IBeaconStateApi} from "@chainsafe/lodestar-api/lib/routes/beacon/state"; +import {allForks, altair} from "@chainsafe/lodestar-types"; +import {readonlyValues} from "@chainsafe/ssz"; +import {computeEpochAtSlot, getCurrentEpoch} from "@chainsafe/lodestar-beacon-state-transition"; +import {ApiError} from "../../errors"; +import {ApiModules} from "../../types"; +import { + filterStateValidatorsByStatuses, + getEpochBeaconCommittees, + getStateValidatorIndex, + getSyncCommittees, + getValidatorStatus, + resolveStateId, + toValidatorResponse, +} from "./utils"; + +export function getBeaconStateApi({chain, config, db}: Pick): IBeaconStateApi { + async function getState(stateId: routes.beacon.StateId): Promise { + return await resolveStateId(config, chain, db, stateId); + } + + return { + async getStateRoot(stateId) { + const state = await getState(stateId); + return {data: config.getForkTypes(state.slot).BeaconState.hashTreeRoot(state)}; + }, + + async getStateFork(stateId) { + const state = await getState(stateId); + return {data: state.fork}; + }, + + async getStateFinalityCheckpoints(stateId) { + const state = await getState(stateId); + return { + data: { + currentJustified: state.currentJustifiedCheckpoint, + previousJustified: state.previousJustifiedCheckpoint, + finalized: state.finalizedCheckpoint, + }, + }; + }, + + async getStateValidators(stateId, filters) { + const state = await resolveStateId(config, chain, db, stateId); + const currentEpoch = getCurrentEpoch(config, state); + + const validators: routes.beacon.ValidatorResponse[] = []; + if (filters?.indices) { + for (const id of filters.indices) { + const validatorIndex = getStateValidatorIndex(id, state, chain); + if (validatorIndex != null) { + const validator = state.validators[validatorIndex]; + if (filters.statuses && !filters.statuses.includes(getValidatorStatus(validator, currentEpoch))) { + continue; + } + const validatorResponse = toValidatorResponse( + validatorIndex, + validator, + state.balances[validatorIndex], + currentEpoch + ); + validators.push(validatorResponse); + } + } + return {data: validators}; + } else if (filters?.statuses) { + const validatorsByStatus = filterStateValidatorsByStatuses(filters.statuses, state, chain, currentEpoch); + return {data: validatorsByStatus}; + } + + let index = 0; + const resp: routes.beacon.ValidatorResponse[] = []; + for (const v of readonlyValues(state.validators)) { + resp.push(toValidatorResponse(index, v, state.balances[index], currentEpoch)); + index++; + } + return {data: resp}; + }, + + async getStateValidator(stateId, validatorId) { + const state = await resolveStateId(config, chain, db, stateId); + + const validatorIndex = getStateValidatorIndex(validatorId, state, chain); + if (validatorIndex == null) { + throw new ApiError(404, "Validator not found"); + } + + return { + data: toValidatorResponse( + validatorIndex, + state.validators[validatorIndex], + state.balances[validatorIndex], + getCurrentEpoch(config, state) + ), + }; + }, + + async getStateValidatorBalances(stateId, indices) { + const state = await resolveStateId(config, chain, db, stateId); + + if (indices) { + const headState = chain.getHeadState(); + const balances: routes.beacon.ValidatorBalance[] = []; + for (const id of indices) { + if (typeof id === "number") { + if (state.validators.length <= id) { + continue; + } + balances.push({index: id, balance: state.balances[id]}); + } else { + const index = headState.pubkey2index.get(id); + if (index != null && index <= state.validators.length) { + balances.push({index, balance: state.balances[index]}); + } + } + } + return {data: balances}; + } + + const balances = Array.from(readonlyValues(state.balances), (balance, index) => { + return { + index, + balance, + }; + }); + return {data: balances}; + }, + + async getEpochCommittees(stateId, filters) { + const state = await resolveStateId(config, chain, db, stateId); + + const committes = getEpochBeaconCommittees( + config, + state, + filters?.epoch ?? computeEpochAtSlot(config, state.slot) + ); + const committesFlat = committes.flatMap((slotCommittees, committeeIndex) => { + if (filters?.index && filters.index !== committeeIndex) { + return []; + } + return slotCommittees.flatMap((committee, slot) => { + if (filters?.slot && filters.slot !== slot) { + return []; + } + return [ + { + index: committeeIndex, + slot, + validators: committee, + }, + ]; + }); + }); + + return {data: committesFlat}; + }, + + /** + * Retrieves the sync committees for the given state. + * @param epoch Fetch sync committees for the given epoch. If not present then the sync committees for the epoch of the state will be obtained. + */ + async getEpochSyncCommittees(stateId, epoch) { + // TODO: Should pick a state with the provided epoch too + const state = (await resolveStateId(config, chain, db, stateId)) as altair.BeaconState; + + // TODO: If possible compute the syncCommittees in advance of the fork and expose them here. + // So the validators can prepare and potentially attest the first block. Not critical tho, it's very unlikely + const stateEpoch = computeEpochAtSlot(config, state.slot); + if (stateEpoch < config.params.ALTAIR_FORK_EPOCH) { + throw new ApiError(400, "Requested state before ALTAIR_FORK_EPOCH"); + } + + return { + data: { + validators: getSyncCommittees(config, state, epoch ?? stateEpoch), + // TODO: This is not used by the validator and will be deprecated soon + validatorAggregates: [], + }, + }; + }, + }; +} diff --git a/packages/lodestar/src/api/impl/beacon/state/interface.ts b/packages/lodestar/src/api/impl/beacon/state/interface.ts deleted file mode 100644 index 58b1032114..0000000000 --- a/packages/lodestar/src/api/impl/beacon/state/interface.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - phase0, - altair, - allForks, - BLSPubkey, - CommitteeIndex, - Epoch, - Root, - Slot, - ValidatorIndex, -} from "@chainsafe/lodestar-types"; - -export interface IBeaconStateApi { - getStateRoot(stateId: StateId): Promise; - getState(stateId: StateId): Promise; - getStateFinalityCheckpoints(stateId: StateId): Promise; - getStateValidators(stateId: StateId, filters?: IValidatorFilters): Promise; - getStateValidator(stateId: StateId, validatorId: BLSPubkey | ValidatorIndex): Promise; - getStateValidatorBalances( - stateId: StateId, - indices?: (BLSPubkey | ValidatorIndex)[] - ): Promise; - getStateCommittees(stateId: StateId, filters?: ICommitteesFilters): Promise; - getEpochSyncCommittees(stateId: StateId, epoch?: Epoch): Promise; - getFork(stateId: StateId): Promise; -} - -export type StateId = string | "head" | "genesis" | "finalized" | "justified"; - -export type ValidatorStatus = - | "active" - | "pending_initialized" - | "pending_queued" - | "active_ongoing" - | "active_exiting" - | "active_slashed" - | "exited_unslashed" - | "exited_slashed" - | "withdrawal_possible" - | "withdrawal_done"; - -export interface IValidatorFilters { - indices?: (BLSPubkey | ValidatorIndex)[]; - statuses?: ValidatorStatus[]; -} -export interface ICommitteesFilters { - epoch?: Epoch; - index?: CommitteeIndex; - slot?: Slot; -} diff --git a/packages/lodestar/src/api/impl/beacon/state/state.ts b/packages/lodestar/src/api/impl/beacon/state/state.ts deleted file mode 100644 index 3be6255a2a..0000000000 --- a/packages/lodestar/src/api/impl/beacon/state/state.ts +++ /dev/null @@ -1,190 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {Root, phase0, allForks, BLSPubkey, Epoch, altair} from "@chainsafe/lodestar-types"; -import {List, readonlyValues} from "@chainsafe/ssz"; -import {computeEpochAtSlot, getCurrentEpoch} from "@chainsafe/lodestar-beacon-state-transition"; -import {IBeaconChain} from "../../../../chain/interface"; -import {IBeaconDb} from "../../../../db"; -import {IApiOptions} from "../../../options"; -import {IApiModules} from "../../interface"; -import {getStateValidatorIndex} from "../../utils"; -import {ApiError} from "../../errors"; -import {IBeaconStateApi, ICommitteesFilters, IValidatorFilters, StateId} from "./interface"; -import { - filterStateValidatorsByStatuses, - getEpochBeaconCommittees, - getSyncCommittees, - getValidatorStatus, - resolveStateId, - toValidatorResponse, -} from "./utils"; - -export class BeaconStateApi implements IBeaconStateApi { - private readonly config: IBeaconConfig; - private readonly db: IBeaconDb; - private readonly chain: IBeaconChain; - - constructor(opts: Partial, modules: Pick) { - this.config = modules.config; - this.db = modules.db; - this.chain = modules.chain; - } - - async getStateRoot(stateId: StateId): Promise { - const state = await this.getState(stateId); - return this.config.getForkTypes(state.slot).BeaconState.hashTreeRoot(state); - } - - async getStateFinalityCheckpoints(stateId: StateId): Promise { - const state = await this.getState(stateId); - return { - currentJustified: state.currentJustifiedCheckpoint, - previousJustified: state.previousJustifiedCheckpoint, - finalized: state.finalizedCheckpoint, - }; - } - - async getStateValidators(stateId: StateId, filters?: IValidatorFilters): Promise { - const state = await resolveStateId(this.config, this.chain, this.db, stateId); - const currentEpoch = getCurrentEpoch(this.config, state); - - const validators: phase0.ValidatorResponse[] = []; - if (filters?.indices) { - for (const id of filters.indices) { - const validatorIndex = getStateValidatorIndex(id, state, this.chain); - if (validatorIndex != null) { - const validator = state.validators[validatorIndex]; - if (filters.statuses && !filters.statuses.includes(getValidatorStatus(validator, currentEpoch))) { - continue; - } - const validatorResponse = toValidatorResponse( - validatorIndex, - validator, - state.balances[validatorIndex], - currentEpoch - ); - validators.push(validatorResponse); - } - } - return validators; - } else if (filters?.statuses) { - const validatorsByStatus = filterStateValidatorsByStatuses(filters.statuses, state, this.chain, currentEpoch); - return validatorsByStatus; - } - - let index = 0; - const resp: phase0.ValidatorResponse[] = []; - for (const v of readonlyValues(state.validators)) { - resp.push(toValidatorResponse(index, v, state.balances[index], currentEpoch)); - index++; - } - return resp; - } - - async getStateValidator( - stateId: StateId, - validatorId: phase0.ValidatorIndex | Root - ): Promise { - const state = await resolveStateId(this.config, this.chain, this.db, stateId); - - const validatorIndex = getStateValidatorIndex(validatorId, state, this.chain); - if (validatorIndex == null) { - throw new ApiError(404, "Validator not found"); - } - - return toValidatorResponse( - validatorIndex, - state.validators[validatorIndex], - state.balances[validatorIndex], - getCurrentEpoch(this.config, state) - ); - } - - async getStateValidatorBalances( - stateId: StateId, - indices?: (phase0.ValidatorIndex | BLSPubkey)[] - ): Promise { - const state = await resolveStateId(this.config, this.chain, this.db, stateId); - - if (indices) { - const headState = this.chain.getHeadState(); - const balances: phase0.ValidatorBalance[] = []; - for (const id of indices) { - if (typeof id === "number") { - if (state.validators.length <= id) { - continue; - } - balances.push({index: id, balance: state.balances[id]}); - } else { - const index = headState.pubkey2index.get(id); - if (index != null && index <= state.validators.length) { - balances.push({index, balance: state.balances[index]}); - } - } - } - return balances; - } - return Array.from(readonlyValues(state.balances), (balance, index) => { - return { - index, - balance, - }; - }); - } - - async getStateCommittees(stateId: StateId, filters?: ICommitteesFilters): Promise { - const state = await resolveStateId(this.config, this.chain, this.db, stateId); - - const committes = getEpochBeaconCommittees( - this.config, - state, - filters?.epoch ?? computeEpochAtSlot(this.config, state.slot) - ); - return committes.flatMap((slotCommittees, committeeIndex) => { - if (filters?.index && filters.index !== committeeIndex) { - return []; - } - return slotCommittees.flatMap((committee, slot) => { - if (filters?.slot && filters.slot !== slot) { - return []; - } - return [ - { - index: committeeIndex, - slot, - validators: committee as List, - }, - ]; - }); - }); - } - - /** - * Retrieves the sync committees for the given state. - * @param epoch Fetch sync committees for the given epoch. If not present then the sync committees for the epoch of the state will be obtained. - */ - async getEpochSyncCommittees(stateId: StateId, epoch?: Epoch): Promise { - // TODO: Should pick a state with the provided epoch too - const state = (await resolveStateId(this.config, this.chain, this.db, stateId)) as altair.BeaconState; - - // TODO: If possible compute the syncCommittees in advance of the fork and expose them here. - // So the validators can prepare and potentially attest the first block. Not critical tho, it's very unlikely - const stateEpoch = computeEpochAtSlot(this.config, state.slot); - if (stateEpoch < this.config.params.ALTAIR_FORK_EPOCH) { - throw new ApiError(400, "Requested state before ALTAIR_FORK_EPOCH"); - } - - return { - validators: getSyncCommittees(this.config, state, epoch ?? stateEpoch), - // TODO: This is not used by the validator and will be deprecated soon - validatorAggregates: [], - }; - } - - async getState(stateId: StateId): Promise { - return await resolveStateId(this.config, this.chain, this.db, stateId); - } - - async getFork(stateId: StateId): Promise { - return (await this.getState(stateId))?.fork; - } -} diff --git a/packages/lodestar/src/api/impl/beacon/state/utils.ts b/packages/lodestar/src/api/impl/beacon/state/utils.ts index 5191424d91..4efb707401 100644 --- a/packages/lodestar/src/api/impl/beacon/state/utils.ts +++ b/packages/lodestar/src/api/impl/beacon/state/utils.ts @@ -1,3 +1,4 @@ +import {routes} from "@chainsafe/lodestar-api"; // this will need async once we wan't to resolve archive slot import { GENESIS_SLOT, @@ -12,14 +13,11 @@ import {allForks} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; import {Epoch, ValidatorIndex, Gwei, Slot} from "@chainsafe/lodestar-types"; -import {ValidatorResponse} from "@chainsafe/lodestar-types/phase0"; -import {fromHexString, readonlyValues, TreeBacked} from "@chainsafe/ssz"; +import {ByteVector, fromHexString, readonlyValues, TreeBacked} from "@chainsafe/ssz"; import {IBeaconChain} from "../../../../chain"; import {StateContextCache} from "../../../../chain/stateCache"; import {IBeaconDb} from "../../../../db"; -import {getStateValidatorIndex} from "../../utils"; import {ApiError, ValidationError} from "../../errors"; -import {StateId} from "./interface"; import {sleep, assert} from "@chainsafe/lodestar-utils"; type ResolveStateIdOpts = { @@ -35,7 +33,7 @@ export async function resolveStateId( config: IBeaconConfig, chain: IBeaconChain, db: IBeaconDb, - stateId: StateId, + stateId: routes.beacon.StateId, opts?: ResolveStateIdOpts ): Promise { const state = await resolveStateIdOrNull(config, chain, db, stateId, opts); @@ -50,7 +48,7 @@ async function resolveStateIdOrNull( config: IBeaconConfig, chain: IBeaconChain, db: IBeaconDb, - stateId: StateId, + stateId: routes.beacon.StateId, opts?: ResolveStateIdOpts ): Promise { stateId = stateId.toLowerCase(); @@ -74,32 +72,30 @@ async function resolveStateIdOrNull( * Get the status of the validator * based on conditions outlined in https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ */ -export function getValidatorStatus(validator: phase0.Validator, currentEpoch: Epoch): phase0.ValidatorStatus { +export function getValidatorStatus(validator: phase0.Validator, currentEpoch: Epoch): routes.beacon.ValidatorStatus { // pending if (validator.activationEpoch > currentEpoch) { if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { - return phase0.ValidatorStatus.PENDING_INITIALIZED; + return "pending_initialized"; } else if (validator.activationEligibilityEpoch < FAR_FUTURE_EPOCH) { - return phase0.ValidatorStatus.PENDING_QUEUED; + return "pending_queued"; } } // active if (validator.activationEpoch <= currentEpoch && currentEpoch < validator.exitEpoch) { if (validator.exitEpoch === FAR_FUTURE_EPOCH) { - return phase0.ValidatorStatus.ACTIVE_ONGOING; + return "active_ongoing"; } else if (validator.exitEpoch < FAR_FUTURE_EPOCH) { - return validator.slashed ? phase0.ValidatorStatus.ACTIVE_SLASHED : phase0.ValidatorStatus.ACTIVE_EXITING; + return validator.slashed ? "active_slashed" : "active_exiting"; } } // exited if (validator.exitEpoch <= currentEpoch && currentEpoch < validator.withdrawableEpoch) { - return validator.slashed ? phase0.ValidatorStatus.EXITED_SLASHED : phase0.ValidatorStatus.EXITED_UNSLASHED; + return validator.slashed ? "exited_slashed" : "exited_unslashed"; } // withdrawal if (validator.withdrawableEpoch <= currentEpoch) { - return validator.effectiveBalance !== BigInt(0) - ? phase0.ValidatorStatus.WITHDRAWAL_POSSIBLE - : phase0.ValidatorStatus.WITHDRAWAL_DONE; + return validator.effectiveBalance !== BigInt(0) ? "withdrawal_possible" : "withdrawal_done"; } throw new Error("ValidatorStatus unknown"); } @@ -109,7 +105,7 @@ export function toValidatorResponse( validator: phase0.Validator, balance: Gwei, currentEpoch: Epoch -): phase0.ValidatorResponse { +): routes.beacon.ValidatorResponse { return { index, status: getValidatorStatus(validator, currentEpoch), @@ -175,7 +171,7 @@ async function stateByName( db: IBeaconDb, stateCache: StateContextCache, forkChoice: IForkChoice, - stateId: StateId + stateId: routes.beacon.StateId ): Promise { switch (stateId) { case "head": @@ -194,7 +190,7 @@ async function stateByName( async function stateByRoot( db: IBeaconDb, stateCache: StateContextCache, - stateId: StateId + stateId: routes.beacon.StateId ): Promise { if (stateId.startsWith("0x")) { const stateRoot = fromHexString(stateId); @@ -230,8 +226,8 @@ export function filterStateValidatorsByStatuses( state: allForks.BeaconState, chain: IBeaconChain, currentEpoch: Epoch -): ValidatorResponse[] { - const responses: ValidatorResponse[] = []; +): routes.beacon.ValidatorResponse[] { + const responses: routes.beacon.ValidatorResponse[] = []; const validators = Array.from(state.validators); const filteredValidators = validators.filter((v) => statuses.includes(getValidatorStatus(v, currentEpoch))); for (const validator of readonlyValues(filteredValidators)) { @@ -281,3 +277,23 @@ async function getFinalizedState( } return state; } + +export function getStateValidatorIndex( + id: routes.beacon.ValidatorId | ByteVector, + state: allForks.BeaconState, + chain: IBeaconChain +): number | undefined { + let validatorIndex: ValidatorIndex | undefined; + if (typeof id === "number") { + if (state.validators.length > id) { + validatorIndex = id; + } + } else { + validatorIndex = chain.getHeadState().pubkey2index.get(id) ?? undefined; + // validator added later than given stateId + if (validatorIndex && validatorIndex >= state.validators.length) { + validatorIndex = undefined; + } + } + return validatorIndex; +} diff --git a/packages/lodestar/src/api/impl/config/config.ts b/packages/lodestar/src/api/impl/config/config.ts deleted file mode 100644 index 9b8d692023..0000000000 --- a/packages/lodestar/src/api/impl/config/config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {IBeaconParams} from "@chainsafe/lodestar-params"; -import {phase0} from "@chainsafe/lodestar-types"; -import {IApiModules} from ".."; -import {IApiOptions} from "../../options"; -import {IConfigApi} from "./interface"; - -export class ConfigApi implements IConfigApi { - private readonly config: IBeaconConfig; - - constructor(opts: Partial, modules: Pick) { - this.config = modules.config; - } - - async getForkSchedule(): Promise { - // @TODO: implement the actual fork schedule data get from config params once marin's altair PRs have been merged - return []; - } - - async getDepositContract(): Promise { - return { - chainId: this.config.params.DEPOSIT_CHAIN_ID, - address: this.config.params.DEPOSIT_CONTRACT_ADDRESS, - }; - } - - async getSpec(): Promise { - return this.config.params; - } -} diff --git a/packages/lodestar/src/api/impl/config/index.ts b/packages/lodestar/src/api/impl/config/index.ts index 20a68a9046..b42d41c3e5 100644 --- a/packages/lodestar/src/api/impl/config/index.ts +++ b/packages/lodestar/src/api/impl/config/index.ts @@ -1,2 +1,24 @@ -export * from "./interface"; -export * from "./config"; +import {routes} from "@chainsafe/lodestar-api"; +import {ApiModules} from "../types"; + +export function getConfigApi({config}: Pick): routes.config.Api { + return { + async getForkSchedule() { + // @TODO: implement the actual fork schedule data get from config params once marin's altair PRs have been merged + return {data: []}; + }, + + async getDepositContract() { + return { + data: { + chainId: config.params.DEPOSIT_CHAIN_ID, + address: config.params.DEPOSIT_CONTRACT_ADDRESS, + }, + }; + }, + + async getSpec() { + return {data: config.params}; + }, + }; +} diff --git a/packages/lodestar/src/api/impl/config/interface.ts b/packages/lodestar/src/api/impl/config/interface.ts deleted file mode 100644 index 2ee65429e2..0000000000 --- a/packages/lodestar/src/api/impl/config/interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {IBeaconParams} from "@chainsafe/lodestar-params"; -import {phase0} from "@chainsafe/lodestar-types"; - -export interface IConfigApi { - getForkSchedule(): Promise; - getDepositContract(): Promise; - getSpec(): Promise; -} diff --git a/packages/lodestar/src/api/impl/debug/beacon/index.ts b/packages/lodestar/src/api/impl/debug/beacon/index.ts deleted file mode 100644 index 288c8be1fc..0000000000 --- a/packages/lodestar/src/api/impl/debug/beacon/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {IBlockSummary} from "@chainsafe/lodestar-fork-choice"; -import {allForks, phase0} from "@chainsafe/lodestar-types"; -import {IBeaconChain} from "../../../../chain"; -import {IBeaconDb} from "../../../../db"; -import {IApiOptions} from "../../../options"; -import {StateId} from "../../beacon/state"; -import {resolveStateId} from "../../beacon/state/utils"; -import {IApiModules} from "../../interface"; -import {IDebugBeaconApi} from "./interface"; - -export class DebugBeaconApi implements IDebugBeaconApi { - private readonly chain: IBeaconChain; - private readonly db: IBeaconDb; - private readonly config: IBeaconConfig; - - constructor(opts: Partial, modules: Pick) { - this.chain = modules.chain; - this.db = modules.db; - this.config = modules.config; - } - - async getHeads(): Promise { - return this.chain.forkChoice - .getHeads() - .map((blockSummary: IBlockSummary) => ({slot: blockSummary.slot, root: blockSummary.blockRoot})); - } - - async getState(stateId: StateId): Promise { - return await resolveStateId(this.config, this.chain, this.db, stateId, {regenFinalizedState: true}); - } -} diff --git a/packages/lodestar/src/api/impl/debug/beacon/interface.ts b/packages/lodestar/src/api/impl/debug/beacon/interface.ts deleted file mode 100644 index d14ea884f8..0000000000 --- a/packages/lodestar/src/api/impl/debug/beacon/interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {allForks, phase0} from "@chainsafe/lodestar-types"; -import {StateId} from "../../beacon/state"; - -export interface IDebugBeaconApi { - /** - * API wrapper function for `getHeads` in `@chainsafe/lodestar-fork-choice`. - * */ - getHeads(): Promise; - getState(stateId: StateId): Promise; -} diff --git a/packages/lodestar/src/api/impl/debug/debug.ts b/packages/lodestar/src/api/impl/debug/debug.ts deleted file mode 100644 index d79d0583fa..0000000000 --- a/packages/lodestar/src/api/impl/debug/debug.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Multiaddr from "multiaddr"; -import PeerId from "peer-id"; -import {IApiOptions} from "../../options"; -import {IApiModules} from "../interface"; -import {DebugBeaconApi} from "./beacon"; -import {IDebugBeaconApi} from "./beacon/interface"; -import {IDebugApi} from "./interface"; - -export class DebugApi implements IDebugApi { - beacon: IDebugBeaconApi; - - constructor( - opts: Partial, - private readonly modules: Pick - ) { - this.beacon = new DebugBeaconApi(opts, modules); - } - - async connectToPeer(peer: PeerId, multiaddr: Multiaddr[]): Promise { - await this.modules.network.connectToPeer(peer, multiaddr); - } - - async disconnectPeer(peer: PeerId): Promise { - await this.modules.network.disconnectPeer(peer); - } -} diff --git a/packages/lodestar/src/api/impl/debug/index.ts b/packages/lodestar/src/api/impl/debug/index.ts index a3ae233a38..47edb200b7 100644 --- a/packages/lodestar/src/api/impl/debug/index.ts +++ b/packages/lodestar/src/api/impl/debug/index.ts @@ -1,4 +1,42 @@ -import {IDebugApi} from "./interface"; -import {DebugApi} from "./debug"; +import {routes} from "@chainsafe/lodestar-api"; +import Multiaddr from "multiaddr"; +import {createFromB58String} from "peer-id"; +import {resolveStateId} from "../beacon/state/utils"; +import {ApiModules} from "../types"; -export {IDebugApi, DebugApi}; +export function getDebugApi({ + chain, + config, + db, + network, +}: Pick): routes.debug.Api { + return { + async getHeads() { + const heads = chain.forkChoice.getHeads(); + return { + data: heads.map((blockSummary) => ({slot: blockSummary.slot, root: blockSummary.blockRoot})), + }; + }, + + async getState(stateId) { + const state = await resolveStateId(config, chain, db, stateId, {regenFinalizedState: true}); + return {data: state}; + }, + + async getStateV2(stateId) { + const state = await resolveStateId(config, chain, db, stateId, {regenFinalizedState: true}); + return {data: state, version: config.getForkName(state.slot)}; + }, + + async connectToPeer(peerIdStr, multiaddrStr) { + const peer = createFromB58String(peerIdStr); + const multiaddr = multiaddrStr.map((addr) => new Multiaddr(addr)); + await network.connectToPeer(peer, multiaddr); + }, + + async disconnectPeer(peerIdStr) { + const peer = createFromB58String(peerIdStr); + await network.disconnectPeer(peer); + }, + }; +} diff --git a/packages/lodestar/src/api/impl/debug/interface.ts b/packages/lodestar/src/api/impl/debug/interface.ts deleted file mode 100644 index 6deec1e350..0000000000 --- a/packages/lodestar/src/api/impl/debug/interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Multiaddr from "multiaddr"; -import PeerId from "peer-id"; -import {IDebugBeaconApi} from "./beacon/interface"; - -export interface IDebugApi { - beacon: IDebugBeaconApi; - - connectToPeer(peer: PeerId, multiaddr: Multiaddr[]): Promise; - disconnectPeer(peer: PeerId): Promise; -} diff --git a/packages/lodestar/src/api/impl/events/events.ts b/packages/lodestar/src/api/impl/events/events.ts deleted file mode 100644 index 429f4f055e..0000000000 --- a/packages/lodestar/src/api/impl/events/events.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {IEventsApi} from "./interfaces"; -import {ApiNamespace, IApiModules} from "../interface"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {ChainEvent, IBeaconChain, IChainEvents} from "../../../chain"; -import {IApiOptions} from "../../options"; -import { - ChainEventListener, - handleBeaconAttestationEvent, - handleBeaconBlockEvent, - handleBeaconHeadEvent, - handleChainReorgEvent, - handleFinalizedCheckpointEvent, - handleVoluntaryExitEvent, -} from "./handlers"; -import {LodestarEventIterator} from "@chainsafe/lodestar-utils"; -import {BeaconEventType, BeaconEvent} from "./types"; - -export class EventsApi implements IEventsApi { - namespace: ApiNamespace; - - private readonly config: IBeaconConfig; - private readonly chain: IBeaconChain; - - constructor(opts: Partial, modules: Pick) { - this.namespace = ApiNamespace.EVENTS; - this.config = modules.config; - this.chain = modules.chain; - } - - getEventStream(topics: BeaconEventType[]): LodestarEventIterator { - return new LodestarEventIterator(({push}) => { - const eventHandlerMapping = getEventHandlerMapping(this.config, push); - - for (const topic of topics) { - const eventHandler = eventHandlerMapping[topic]; - if (eventHandler) { - this.chain.emitter.on(eventHandler.chainEvent, eventHandler.handler); - } - } - - return () => { - for (const topic of topics) { - const eventHandler = eventHandlerMapping[topic]; - this.chain.emitter.off(eventHandler.chainEvent, eventHandler.handler as (...args: unknown[]) => void); - } - }; - }); - } -} - -function getEventHandlerMapping( - config: IBeaconConfig, - push: (value: BeaconEvent) => void -): Record}> { - return { - [BeaconEventType.HEAD]: { - chainEvent: ChainEvent.forkChoiceHead, - handler: handleBeaconHeadEvent(config, push), - }, - [BeaconEventType.BLOCK]: { - chainEvent: ChainEvent.block, - handler: handleBeaconBlockEvent(config, push), - }, - [BeaconEventType.ATTESTATION]: { - chainEvent: ChainEvent.attestation, - handler: handleBeaconAttestationEvent(config, push), - }, - [BeaconEventType.VOLUNTARY_EXIT]: { - chainEvent: ChainEvent.block, - handler: handleVoluntaryExitEvent(config, push), - }, - [BeaconEventType.FINALIZED_CHECKPOINT]: { - chainEvent: ChainEvent.finalized, - handler: handleFinalizedCheckpointEvent(config, push), - }, - [BeaconEventType.CHAIN_REORG]: { - chainEvent: ChainEvent.forkChoiceReorg, - handler: handleChainReorgEvent(config, push), - }, - }; -} diff --git a/packages/lodestar/src/api/impl/events/handlers.ts b/packages/lodestar/src/api/impl/events/handlers.ts deleted file mode 100644 index 722ada89b5..0000000000 --- a/packages/lodestar/src/api/impl/events/handlers.ts +++ /dev/null @@ -1,102 +0,0 @@ -import {ChainEvent, IChainEvents} from "../../../chain"; -import {BeaconEventType, BeaconEvent} from "./types"; -import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@chainsafe/lodestar-beacon-state-transition"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; - -type ListenerType = [T] extends [(...args: infer U) => unknown] ? U : [T] extends [void] ? [] : [T]; - -export type ChainEventListener = (...args: ListenerType) => void; - -export function handleBeaconHeadEvent( - config: IBeaconConfig, - callback: (value: BeaconEvent) => void -): ChainEventListener { - return (payload) => { - callback({ - type: BeaconEventType.HEAD, - message: { - block: payload.blockRoot, - epochTransition: computeStartSlotAtEpoch(config, computeEpochAtSlot(config, payload.slot)) === payload.slot, - slot: payload.slot, - state: payload.stateRoot, - }, - }); - }; -} - -export function handleBeaconBlockEvent( - config: IBeaconConfig, - callback: (value: BeaconEvent) => void -): ChainEventListener { - return (payload) => { - callback({ - type: BeaconEventType.BLOCK, - message: { - block: config.getForkTypes(payload.message.slot).BeaconBlock.hashTreeRoot(payload.message), - slot: payload.message.slot, - }, - }); - }; -} - -export function handleBeaconAttestationEvent( - config: IBeaconConfig, - callback: (value: BeaconEvent) => void -): ChainEventListener { - return (payload) => { - callback({ - type: BeaconEventType.ATTESTATION, - message: payload, - }); - }; -} - -export function handleVoluntaryExitEvent( - config: IBeaconConfig, - callback: (value: BeaconEvent) => void -): ChainEventListener { - return (payload) => { - for (const exit of payload.message.body.voluntaryExits) { - callback({ - type: BeaconEventType.VOLUNTARY_EXIT, - message: exit, - }); - } - }; -} - -export function handleFinalizedCheckpointEvent( - config: IBeaconConfig, - callback: (value: BeaconEvent) => void -): ChainEventListener { - return (payload, state) => { - callback({ - type: BeaconEventType.FINALIZED_CHECKPOINT, - message: { - block: payload.root, - epoch: payload.epoch, - state: state.hashTreeRoot(), - }, - }); - }; -} - -export function handleChainReorgEvent( - config: IBeaconConfig, - callback: (value: BeaconEvent) => void -): ChainEventListener { - return (oldHead, newHead, depth) => { - callback({ - type: BeaconEventType.CHAIN_REORG, - message: { - depth, - epoch: computeEpochAtSlot(config, newHead.slot), - slot: newHead.slot, - newHeadBlock: newHead.blockRoot, - oldHeadBlock: oldHead.blockRoot, - newHeadState: newHead.stateRoot, - oldHeadState: oldHead.stateRoot, - }, - }); - }; -} diff --git a/packages/lodestar/src/api/impl/events/index.ts b/packages/lodestar/src/api/impl/events/index.ts index 324e305ff6..4334226219 100644 --- a/packages/lodestar/src/api/impl/events/index.ts +++ b/packages/lodestar/src/api/impl/events/index.ts @@ -1,3 +1,102 @@ -export * from "./events"; -export * from "./interfaces"; -export * from "./types"; +import {ApiModules} from "../types"; +import {ChainEvent, IChainEvents} from "../../../chain"; +import {routes} from "@chainsafe/lodestar-api"; +import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@chainsafe/lodestar-beacon-state-transition"; +import {ZERO_HASH} from "../../../constants"; +import {ApiError} from "../errors"; + +/** + * Mapping of internal `ChainEvents` to API spec events + */ +const chainEventMap = { + [routes.events.EventType.head]: ChainEvent.forkChoiceHead as const, + [routes.events.EventType.block]: ChainEvent.block as const, + [routes.events.EventType.attestation]: ChainEvent.attestation as const, + [routes.events.EventType.voluntaryExit]: ChainEvent.block as const, + [routes.events.EventType.finalizedCheckpoint]: ChainEvent.finalized as const, + [routes.events.EventType.chainReorg]: ChainEvent.forkChoiceReorg as const, +}; + +export function getEventsApi({chain, config}: Pick): routes.events.Api { + /** + * Mapping to convert internal `ChainEvents` payload to the API spec events data + */ + const eventDataTransformers: { + [K in routes.events.EventType]: ( + ...args: Parameters + ) => routes.events.EventData[K][]; + } = { + [routes.events.EventType.head]: (head) => [ + { + block: head.blockRoot, + epochTransition: computeStartSlotAtEpoch(config, computeEpochAtSlot(config, head.slot)) === head.slot, + slot: head.slot, + state: head.stateRoot, + // Todo implement + previousDutyDependentRoot: ZERO_HASH, + currentDutyDependentRoot: ZERO_HASH, + }, + ], + [routes.events.EventType.block]: (block) => [ + { + block: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message), + slot: block.message.slot, + }, + ], + [routes.events.EventType.attestation]: (attestation) => [attestation], + [routes.events.EventType.voluntaryExit]: (block) => Array.from(block.message.body.voluntaryExits), + [routes.events.EventType.finalizedCheckpoint]: (checkpoint, state) => [ + { + block: checkpoint.root, + epoch: checkpoint.epoch, + state: state.hashTreeRoot(), + }, + ], + [routes.events.EventType.chainReorg]: (oldHead, newHead, depth) => [ + { + depth, + epoch: computeEpochAtSlot(config, newHead.slot), + slot: newHead.slot, + newHeadBlock: newHead.blockRoot, + oldHeadBlock: oldHead.blockRoot, + newHeadState: newHead.stateRoot, + oldHeadState: oldHead.stateRoot, + }, + ], + }; + + return { + eventstream(topics, signal, onEvent) { + const onAbortFns: (() => void)[] = []; + + for (const topic of topics) { + const eventDataTransformer = eventDataTransformers[topic]; + const chainEvent = chainEventMap[topic]; + if (!eventDataTransformer || !chainEvent) { + throw new ApiError(400, `Unknown topic ${topic}`); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handler = (...args: any[]): void => { + // TODO: What happens if this handler throws? Does it break the other chain.emitter listeners? + const messages = eventDataTransformer(...args); + for (const message of messages) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + onEvent({type: topic, message: message as any}); + } + }; + + chain.emitter.on(chainEvent, handler); + onAbortFns.push(() => chain.emitter.off(chainEvent, handler)); + } + + signal.addEventListener( + "abort", + () => { + for (const abortFn of onAbortFns) abortFn(); + }, + {once: true} + ); + }, + }; +} diff --git a/packages/lodestar/src/api/impl/events/interfaces.ts b/packages/lodestar/src/api/impl/events/interfaces.ts deleted file mode 100644 index df8f38089e..0000000000 --- a/packages/lodestar/src/api/impl/events/interfaces.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {BeaconEvent, BeaconEventType} from "./types"; -import {IStoppableEventIterable} from "@chainsafe/lodestar-utils"; - -export interface IEventsApi { - /** - * Returns a mapping of beacon node events to an iteratable event stream. - */ - getEventStream(topics: BeaconEventType[]): IStoppableEventIterable; -} diff --git a/packages/lodestar/src/api/impl/events/types.ts b/packages/lodestar/src/api/impl/events/types.ts deleted file mode 100644 index ae1c3ab7c9..0000000000 --- a/packages/lodestar/src/api/impl/events/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; - -export enum BeaconEventType { - HEAD = "head", - BLOCK = "block", - ATTESTATION = "attestation", - VOLUNTARY_EXIT = "voluntary_exit", - CHAIN_REORG = "chain_reorg", - FINALIZED_CHECKPOINT = "finalized_checkpoint", -} - -export type BeaconHeadEvent = { - type: typeof BeaconEventType.HEAD; - message: phase0.ChainHead; -}; - -export type BeaconBlockEvent = { - type: typeof BeaconEventType.BLOCK; - message: phase0.BlockEventPayload; -}; - -export type BeaconAttestationEvent = { - type: typeof BeaconEventType.ATTESTATION; - message: phase0.Attestation; -}; - -export type VoluntaryExitEvent = { - type: typeof BeaconEventType.VOLUNTARY_EXIT; - message: phase0.SignedVoluntaryExit; -}; - -export type FinalizedCheckpointEvent = { - type: typeof BeaconEventType.FINALIZED_CHECKPOINT; - message: phase0.FinalizedCheckpoint; -}; - -export type BeaconChainReorgEvent = { - type: typeof BeaconEventType.CHAIN_REORG; - message: phase0.ChainReorg; -}; - -export type BeaconEvent = - | BeaconHeadEvent - | BeaconBlockEvent - | BeaconAttestationEvent - | VoluntaryExitEvent - | BeaconChainReorgEvent - | FinalizedCheckpointEvent; diff --git a/packages/lodestar/src/api/impl/index.ts b/packages/lodestar/src/api/impl/index.ts index a52df3463d..7c3d3640b9 100644 --- a/packages/lodestar/src/api/impl/index.ts +++ b/packages/lodestar/src/api/impl/index.ts @@ -6,5 +6,5 @@ export * from "./validator"; export * from "./beacon"; export * from "./node"; export * from "./events"; -export * from "./interface"; +export * from "./types"; export * from "./api"; diff --git a/packages/lodestar/src/api/impl/interface.ts b/packages/lodestar/src/api/impl/interface.ts deleted file mode 100644 index a64730e42e..0000000000 --- a/packages/lodestar/src/api/impl/interface.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {ILogger} from "@chainsafe/lodestar-utils"; - -import {IBeaconChain} from "../../chain"; -import {IBeaconDb} from "../../db"; -import {IBeaconSync} from "../../sync"; -import {INetwork} from "../../network"; -import {IEth1ForBlockProduction} from "../../eth1"; - -import {IBeaconApi} from "./beacon"; -import {INodeApi} from "./node"; -import {IValidatorApi} from "./validator"; -import {IEventsApi} from "./events"; -import {IDebugApi} from "./debug/interface"; -import {IConfigApi} from "./config/interface"; -import {ILightclientApi} from "./lightclient"; -import {ILodestarApi} from "./lodestar"; -import {IMetrics} from "../../metrics"; - -export const enum ApiNamespace { - BEACON = "beacon", - VALIDATOR = "validator", - NODE = "node", - EVENTS = "events", - DEBUG = "debug", - CONFIG = "config", - LIGHTCLIENT = "lightclient", - LODESTAR = "lodestar", -} - -export interface IApiModules { - config: IBeaconConfig; - chain: IBeaconChain; - db: IBeaconDb; - eth1: IEth1ForBlockProduction; - logger: ILogger; - metrics: IMetrics | null; - network: INetwork; - sync: IBeaconSync; -} - -export interface IApi { - beacon: IBeaconApi; - node: INodeApi; - validator: IValidatorApi; - events: IEventsApi; - debug: IDebugApi; - config: IConfigApi; - lightclient: ILightclientApi; - lodestar: ILodestarApi; -} diff --git a/packages/lodestar/src/api/impl/lightclient/index.ts b/packages/lodestar/src/api/impl/lightclient/index.ts index c038350abe..e01ff76767 100644 --- a/packages/lodestar/src/api/impl/lightclient/index.ts +++ b/packages/lodestar/src/api/impl/lightclient/index.ts @@ -1,12 +1,8 @@ import {altair, SyncPeriod} from "@chainsafe/lodestar-types"; -import {Path} from "@chainsafe/ssz"; -import {ApiNamespace, IApiModules} from "../interface"; -import {IApiOptions} from "../../options"; -import {Proof} from "@chainsafe/persistent-merkle-tree"; +import {ApiModules} from "../types"; import {resolveStateId} from "../beacon/state/utils"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {IBeaconChain} from "../../../chain"; -import {IBeaconDb} from "../../../db"; +import {routes} from "@chainsafe/lodestar-api"; +import {ApiError} from "../errors"; // TODO: Import from lightclient/server package interface ILightClientUpdater { @@ -15,48 +11,39 @@ interface ILightClientUpdater { getLatestUpdateNonFinalized(): Promise; } -export interface ILightclientApi { - getBestUpdates(from: SyncPeriod, to: SyncPeriod): Promise; - getLatestUpdateFinalized(): Promise; - getLatestUpdateNonFinalized(): Promise; - createStateProof(stateId: string, paths: Path[]): Promise; -} - -export class LightclientApi implements ILightclientApi { - namespace = ApiNamespace.LIGHTCLIENT; - - private readonly config: IBeaconConfig; - private readonly db: IBeaconDb; - private readonly chain: IBeaconChain; - private readonly lightClientUpdater!: ILightClientUpdater; - - constructor(opts: Partial, modules: Pick) { - this.config = modules.config; - this.db = modules.db; - this.chain = modules.chain; - } - - // Sync API - - async getBestUpdates(from: SyncPeriod, to: SyncPeriod): Promise { - return this.lightClientUpdater.getBestUpdates(from, to); - } - - async getLatestUpdateFinalized(): Promise { - return this.lightClientUpdater.getLatestUpdateFinalized(); - } - - async getLatestUpdateNonFinalized(): Promise { - return this.lightClientUpdater.getLatestUpdateNonFinalized(); - } - - // Proofs API - - async createStateProof(stateId: string, paths: Path[]): Promise { - const state = await resolveStateId(this.config, this.chain, this.db, stateId); - const stateTreeBacked = this.config.types.altair.BeaconState.createTreeBackedFromStruct( - state as altair.BeaconState - ); - return stateTreeBacked.createProof(paths); - } +export function getLightclientApi({ + chain, + config, + db, +}: Pick): routes.lightclient.Api { + // TODO: + const lightClientUpdater = {} as ILightClientUpdater; + + return { + // Proofs API + + async getStateProof(stateId, paths) { + const state = await resolveStateId(config, chain, db, stateId); + const stateTreeBacked = config.types.altair.BeaconState.createTreeBackedFromStruct(state as altair.BeaconState); + return {data: stateTreeBacked.createProof(paths)}; + }, + + // Sync API + + async getBestUpdates(from, to) { + return {data: await lightClientUpdater.getBestUpdates(from, to)}; + }, + + async getLatestUpdateFinalized() { + const update = await lightClientUpdater.getLatestUpdateFinalized(); + if (!update) throw new ApiError(404, "No update available"); + return {data: update}; + }, + + async getLatestUpdateNonFinalized() { + const update = await lightClientUpdater.getLatestUpdateNonFinalized(); + if (!update) throw new ApiError(404, "No update available"); + return {data: update}; + }, + }; } diff --git a/packages/lodestar/src/api/impl/lodestar/index.ts b/packages/lodestar/src/api/impl/lodestar/index.ts index 0b5e9800dc..d0b48ae548 100644 --- a/packages/lodestar/src/api/impl/lodestar/index.ts +++ b/packages/lodestar/src/api/impl/lodestar/index.ts @@ -1,64 +1,43 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {Epoch} from "@chainsafe/lodestar-types"; -import {IApiModules} from "../interface"; -import {getLatestWeakSubjectivityCheckpointEpoch} from "../../../../../beacon-state-transition/lib/allForks/util/weakSubjectivity"; -import {IBeaconChain} from "../../../chain"; -import {IBeaconSync} from "../../../sync"; -import {SyncChainDebugState} from "../../../sync/range/chain"; +import {routes} from "@chainsafe/lodestar-api"; +import {getLatestWeakSubjectivityCheckpointEpoch} from "@chainsafe/lodestar-beacon-state-transition/lib/allForks/util/weakSubjectivity"; +import {ApiModules} from "../types"; -export interface ILodestarApi { - getWtfNode(): string; - getLatestWeakSubjectivityCheckpointEpoch(): Promise; - getSyncChainsDebugState(): SyncChainDebugState[]; -} +export function getLodestarApi({ + chain, + config, + sync, +}: Pick): routes.lodestar.Api { + return { + /** + * Get a wtfnode dump of all active handles + * Will only load the wtfnode after the first call, and registers async hooks + * and other listeners to the global process instance + */ + async getWtfNode() { + // Browser interop + if (typeof require !== "function") throw Error("NodeJS only"); -export class LodestarApi implements ILodestarApi { - private readonly config: IBeaconConfig; - private readonly chain: IBeaconChain; - private readonly sync: IBeaconSync; - - constructor(modules: Pick) { - this.config = modules.config; - this.chain = modules.chain; - this.sync = modules.sync; - - // Allows to load wtfnode listeners immedeatelly. Usefull when dockerized, - // so after an unexpected restart wtfnode becomes properly loaded again - if (process?.env?.START_WTF_NODE) { // eslint-disable-next-line - require("wtfnode"); - } - } + const wtfnode = require("wtfnode"); + const logs: string[] = []; + function logger(...args: string[]): void { + for (const arg of args) logs.push(arg); + } + /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ + wtfnode.setLogger("info", logger); + wtfnode.setLogger("warn", logger); + wtfnode.setLogger("error", logger); + wtfnode.dump(); + return {data: logs.join("\n")}; + }, - /** - * Get a wtfnode dump of all active handles - * Will only load the wtfnode after the first call, and registers async hooks - * and other listeners to the global process instance - */ - getWtfNode(): string { - // Browser interop - if (typeof require !== "function") throw Error("NodeJS only"); + async getLatestWeakSubjectivityCheckpointEpoch() { + const state = chain.getHeadState(); + return {data: getLatestWeakSubjectivityCheckpointEpoch(config, state)}; + }, - // eslint-disable-next-line - const wtfnode = require("wtfnode"); - const logs: string[] = []; - function logger(...args: string[]): void { - for (const arg of args) logs.push(arg); - } - /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ - wtfnode.setLogger("info", logger); - wtfnode.setLogger("warn", logger); - wtfnode.setLogger("error", logger); - wtfnode.dump(); - return logs.join("\n"); - } - - async getLatestWeakSubjectivityCheckpointEpoch(): Promise { - const state = this.chain.getHeadState(); - return getLatestWeakSubjectivityCheckpointEpoch(this.config, state); - } - - getSyncChainsDebugState(): SyncChainDebugState[] { - return this.sync.getSyncChainsDebugState(); - } + async getSyncChainsDebugState() { + return {data: sync.getSyncChainsDebugState()}; + }, + }; } diff --git a/packages/lodestar/src/api/impl/node/index.ts b/packages/lodestar/src/api/impl/node/index.ts index 7e6a85a370..b0b6babf80 100644 --- a/packages/lodestar/src/api/impl/node/index.ts +++ b/packages/lodestar/src/api/impl/node/index.ts @@ -1,2 +1,80 @@ -export * from "./interface"; -export * from "./node"; +import {routes} from "@chainsafe/lodestar-api"; +import {createKeypairFromPeerId} from "@chainsafe/discv5"; +import {formatNodePeer} from "./utils"; +import {ApiError} from "../errors"; +import {ApiModules} from "../types"; + +export function getNodeApi({network, sync}: Pick): routes.node.Api { + return { + async getNetworkIdentity() { + const enr = network.getEnr(); + const keypair = createKeypairFromPeerId(network.peerId); + const discoveryAddresses = [ + enr?.getLocationMultiaddr("tcp")?.toString() ?? null, + enr?.getLocationMultiaddr("udp")?.toString() ?? null, + ].filter((addr): addr is string => Boolean(addr)); + + return { + data: { + peerId: network.peerId.toB58String(), + enr: enr?.encodeTxt(keypair.privateKey) || "", + discoveryAddresses, + p2pAddresses: network.localMultiaddrs.map((m) => m.toString()), + metadata: network.metadata, + }, + }; + }, + + async getPeer(peerIdStr) { + const connections = network.getConnectionsByPeer().get(peerIdStr); + if (!connections) { + throw new ApiError(404, "Node has not seen this peer"); + } + return {data: formatNodePeer(peerIdStr, connections)}; + }, + + async getPeers(filters) { + const {state, direction} = filters || {}; + const peers = Array.from(network.getConnectionsByPeer().entries()) + .map(([peerIdStr, connections]) => formatNodePeer(peerIdStr, connections)) + .filter( + (nodePeer) => + (!state || state.length === 0 || state.includes(nodePeer.state)) && + (!direction || direction.length === 0 || (nodePeer.direction && direction.includes(nodePeer.direction))) + ); + + return { + data: peers, + meta: {count: peers.length}, + }; + }, + + async getPeerCount() { + // TODO: Implement + return { + data: { + disconnected: 0, + connecting: 0, + connected: 0, + disconnecting: 0, + }, + }; + }, + + async getNodeVersion() { + return { + data: { + version: `Lodestar/${process.env.npm_package_version || "dev"}`, + }, + }; + }, + + async getSyncingStatus() { + return {data: sync.getSyncStatus()}; + }, + + async getHealth() { + // Ok + }, + }; +} diff --git a/packages/lodestar/src/api/impl/node/interface.ts b/packages/lodestar/src/api/impl/node/interface.ts deleted file mode 100644 index e92e8d8bbe..0000000000 --- a/packages/lodestar/src/api/impl/node/interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; - -import {NodeIdentity, NodePeer} from "../../types"; - -/** - * Read information about the beacon node. - */ -export interface INodeApi { - getNodeIdentity(): Promise; - getPeers(state?: string[], direction?: string[]): Promise; - getPeer(peerId: string): Promise; - /** - * Gets the beacon node version. Format of version string is derived from schema used by other - * eth2 clients. - * */ - getVersion(): Promise; - getSyncingStatus(): Promise; - getNodeStatus(): Promise<"ready" | "syncing" | "error">; -} diff --git a/packages/lodestar/src/api/impl/node/node.ts b/packages/lodestar/src/api/impl/node/node.ts deleted file mode 100644 index 91df9a119f..0000000000 --- a/packages/lodestar/src/api/impl/node/node.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {createKeypairFromPeerId} from "@chainsafe/discv5"; - -import {NodeIdentity, NodePeer} from "../../types"; -import {INetwork, PeerDirection, PeerState} from "../../../network"; -import {IBeaconSync} from "../../../sync"; - -import {IApiOptions} from "../../options"; -import {ApiNamespace, IApiModules} from "../interface"; -import {formatNodePeer} from "./utils"; -import {INodeApi} from "./interface"; -import {ApiError} from "../errors"; - -export class NodeApi implements INodeApi { - namespace = ApiNamespace.NODE; - - private readonly network: INetwork; - private readonly sync: IBeaconSync; - - constructor(opts: Partial, modules: Pick) { - this.namespace = ApiNamespace.BEACON; - this.network = modules.network; - this.sync = modules.sync; - } - - async getNodeIdentity(): Promise { - const enr = this.network.getEnr(); - const keypair = createKeypairFromPeerId(this.network.peerId); - const discoveryAddresses = [ - enr?.getLocationMultiaddr("tcp")?.toString() ?? null, - enr?.getLocationMultiaddr("udp")?.toString() ?? null, - ].filter((addr): addr is string => Boolean(addr)); - - return { - peerId: this.network.peerId.toB58String(), - enr: enr?.encodeTxt(keypair.privateKey) || "", - discoveryAddresses, - p2pAddresses: this.network.localMultiaddrs.map((m) => m.toString()), - metadata: this.network.metadata, - }; - } - - async getNodeStatus(): Promise<"ready" | "syncing" | "error"> { - return this.sync.isSynced() ? "ready" : "syncing"; - } - - async getPeer(peerIdStr: string): Promise { - const connections = this.network.getConnectionsByPeer().get(peerIdStr); - if (!connections) { - throw new ApiError(404, "Node has not seen this peer"); - } - return formatNodePeer(peerIdStr, connections); - } - - async getPeers(state?: PeerState[], direction?: PeerDirection[]): Promise { - return Array.from(this.network.getConnectionsByPeer().entries()) - .map(([peerIdStr, connections]) => formatNodePeer(peerIdStr, connections)) - .filter( - (nodePeer) => - (!state || state.length === 0 || state.includes(nodePeer.state)) && - (!direction || direction.length === 0 || (nodePeer.direction && direction.includes(nodePeer.direction))) - ); - } - - async getSyncingStatus(): Promise { - return this.sync.getSyncStatus(); - } - - async getVersion(): Promise { - return `Lodestar/${process.env.npm_package_version || "dev"}`; - } -} diff --git a/packages/lodestar/src/api/impl/node/utils.ts b/packages/lodestar/src/api/impl/node/utils.ts index e517b94008..6d4d616411 100644 --- a/packages/lodestar/src/api/impl/node/utils.ts +++ b/packages/lodestar/src/api/impl/node/utils.ts @@ -1,11 +1,11 @@ +import {routes} from "@chainsafe/lodestar-api"; import {Connection} from "libp2p"; -import {PeerStatus, PeerState} from "../../../network"; -import {NodePeer} from "../../types"; +import {PeerStatus} from "../../../network"; /** * Format a list of connections from libp2p connections manager into the API's format NodePeer */ -export function formatNodePeer(peerIdStr: string, connections: Connection[]): NodePeer { +export function formatNodePeer(peerIdStr: string, connections: Connection[]): routes.node.NodePeer { const conn = getRevelantConnection(connections); return { @@ -13,7 +13,7 @@ export function formatNodePeer(peerIdStr: string, connections: Connection[]): No // TODO: figure out how to get enr of peer enr: "", lastSeenP2pAddress: conn ? conn.remoteAddr.toString() : "", - direction: conn ? conn.stat.direction : null, + direction: conn ? (conn.stat.direction as routes.node.PeerDirection) : null, state: conn ? getPeerState(conn.stat.status) : "disconnected", }; } @@ -38,7 +38,7 @@ function getRevelantConnection(connections: Connection[]): Connection | null { * Map libp2p connection status to the API's peer state notation * @param status */ -function getPeerState(status: PeerStatus): PeerState { +function getPeerState(status: PeerStatus): routes.node.PeerState { switch (status) { case "open": return "connected"; diff --git a/packages/lodestar/src/api/impl/types.ts b/packages/lodestar/src/api/impl/types.ts new file mode 100644 index 0000000000..7f09b3debe --- /dev/null +++ b/packages/lodestar/src/api/impl/types.ts @@ -0,0 +1,20 @@ +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {ILogger} from "@chainsafe/lodestar-utils"; + +import {IBeaconChain} from "../../chain"; +import {IBeaconDb} from "../../db"; +import {IBeaconSync} from "../../sync"; +import {INetwork} from "../../network"; +import {IEth1ForBlockProduction} from "../../eth1"; +import {IMetrics} from "../../metrics"; + +export type ApiModules = { + config: IBeaconConfig; + chain: IBeaconChain; + db: IBeaconDb; + eth1: IEth1ForBlockProduction; + logger: ILogger; + metrics: IMetrics | null; + network: INetwork; + sync: IBeaconSync; +}; diff --git a/packages/lodestar/src/api/impl/utils.ts b/packages/lodestar/src/api/impl/utils.ts deleted file mode 100644 index e5bc3ac31c..0000000000 --- a/packages/lodestar/src/api/impl/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {allForks} from "@chainsafe/lodestar-types"; -import {ValidatorIndex} from "@chainsafe/lodestar-types/phase0"; -import {ByteVector} from "@chainsafe/ssz"; -import {IBeaconChain} from "../../chain/interface"; - -export function getStateValidatorIndex( - id: number | ByteVector, - state: allForks.BeaconState, - chain: IBeaconChain -): number | undefined { - let validatorIndex: ValidatorIndex | undefined; - if (typeof id === "number") { - if (state.validators.length > id) { - validatorIndex = id; - } - } else { - validatorIndex = chain.getHeadState().pubkey2index.get(id) ?? undefined; - // validator added later than given stateId - if (validatorIndex && validatorIndex >= state.validators.length) { - validatorIndex = undefined; - } - } - return validatorIndex; -} diff --git a/packages/lodestar/src/api/impl/validator/index.ts b/packages/lodestar/src/api/impl/validator/index.ts index 0dd50f3d2a..93d21ad2e8 100644 --- a/packages/lodestar/src/api/impl/validator/index.ts +++ b/packages/lodestar/src/api/impl/validator/index.ts @@ -1,8 +1,425 @@ +import {routes} from "@chainsafe/lodestar-api"; +import bls, {Signature} from "@chainsafe/bls"; +import { + CachedBeaconState, + computeStartSlotAtEpoch, + proposerShufflingDecisionRoot, + attesterShufflingDecisionRoot, + computeSubnetForCommitteesAtSlot, +} from "@chainsafe/lodestar-beacon-state-transition"; +import {GENESIS_SLOT, SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params"; +import {Root, Slot} from "@chainsafe/lodestar-types"; +import {BeaconState} from "@chainsafe/lodestar-types/lib/allForks"; +import {readonlyValues} from "@chainsafe/ssz"; +import {assembleAttestationData} from "../../../chain/factory/attestation"; +import {assembleBlock} from "../../../chain/factory/block"; +import {assembleAttesterDuty} from "../../../chain/factory/duties"; +import {validateGossipAggregateAndProof} from "../../../chain/validation"; +import {ZERO_HASH} from "../../../constants"; +import {SyncState} from "../../../sync"; +import {toGraffitiBuffer} from "../../../util/graffiti"; +import {ApiError} from "../errors"; +import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof"; +import {CommitteeSubscription} from "../../../network/subnets"; +import {getSyncComitteeValidatorIndexMap} from "./utils"; +import {ApiModules} from "../types"; + /** - * @module api/rpc + * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a + * future slot, wait some time instead of rejecting the request because it's in the future */ +const MAX_API_CLOCK_DISPARITY_MS = 1000; -import {ValidatorApi} from "./validator"; -import {IValidatorApi} from "./interface"; +/** + * If the node is within this many epochs from the head, we declare it to be synced regardless of + * the network sync state. + * + * This helps prevent attacks where nodes can convince us that we're syncing some non-existent + * finalized head. + */ +const SYNC_TOLERANCE_EPOCHS = 8; -export {ValidatorApi, IValidatorApi}; +/** + * Server implementation for handling validator duties. + * See `@chainsafe/lodestar-validator/src/api` for the client implementation). + */ +export function getValidatorApi({ + chain, + config, + db, + eth1, + logger, + metrics, + network, + sync, +}: ApiModules): routes.validator.Api { + let genesisBlockRoot: Root | null = null; + + /** Compute and cache the genesis block root */ + async function getGenesisBlockRoot(state: CachedBeaconState): Promise { + if (!genesisBlockRoot) { + // Close to genesis the genesis block may not be available in the DB + if (state.slot < config.params.SLOTS_PER_HISTORICAL_ROOT) { + genesisBlockRoot = state.blockRoots[0]; + } + + const genesisBlock = await chain.getCanonicalBlockAtSlot(GENESIS_SLOT); + if (genesisBlock) { + genesisBlockRoot = config.getForkTypes(genesisBlock.message.slot).SignedBeaconBlock.hashTreeRoot(genesisBlock); + } + } + + // If for some reason the genesisBlockRoot is not able don't prevent validators from + // proposing or attesting. If the genesisBlockRoot is wrong, at worst it may trigger a re-fetch of the duties + return genesisBlockRoot || ZERO_HASH; + } + + /** + * If advancing the local clock `MAX_API_CLOCK_DISPARITY_MS` ticks to the requested slot, wait for its start + * Prevents the validator from getting errors from the API if the clock is a bit advanced + */ + async function waitForSlot(slot: Slot): Promise { + const slotStartSec = chain.genesisTime + slot * config.params.SECONDS_PER_SLOT; + const msToSlot = slotStartSec * 1000 - Date.now(); + if (msToSlot > 0 && msToSlot < MAX_API_CLOCK_DISPARITY_MS) { + await chain.clock.waitForSlot(slot); + } + } + + /** + * If advancing the local clock `MAX_API_CLOCK_DISPARITY_MS` ticks to the next epoch, wait for slot 0 of the next epoch. + * Prevents a validator from not being able to get the attestater duties correctly if the beacon and validator clocks are off + */ + async function waitForNextClosestEpoch(): Promise { + const nextEpoch = chain.clock.currentEpoch + 1; + const secPerEpoch = config.params.SLOTS_PER_EPOCH * config.params.SECONDS_PER_SLOT; + const nextEpochStartSec = chain.genesisTime + nextEpoch * secPerEpoch; + const msToNextEpoch = nextEpochStartSec * 1000 - Date.now(); + if (msToNextEpoch > 0 && msToNextEpoch < MAX_API_CLOCK_DISPARITY_MS) { + await chain.clock.waitForSlot(computeStartSlotAtEpoch(config, nextEpoch)); + } + } + + /** + * Reject any request while the node is syncing + */ + function notWhileSyncing(): void { + // Consider node synced before or close to genesis + if (chain.clock.currentSlot < config.params.SLOTS_PER_EPOCH) { + return; + } + + const syncState = sync.state; + switch (syncState) { + case SyncState.SyncingFinalized: + case SyncState.SyncingHead: { + const currentSlot = chain.clock.currentSlot; + const headSlot = chain.forkChoice.getHead().slot; + if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * config.params.SLOTS_PER_EPOCH) { + throw new ApiError(503, `Node is syncing, headSlot ${headSlot} currentSlot ${currentSlot}`); + } else { + return; + } + } + + case SyncState.Synced: + return; + + case SyncState.Stalled: + throw new ApiError(503, "Node is waiting for peers"); + } + } + + return { + async produceBlock(slot, randaoReveal, graffiti = "") { + notWhileSyncing(); + + await waitForSlot(slot); // Must never request for a future slot > currentSlot + + const block = await assembleBlock( + {config: config, chain: chain, db: db, eth1: eth1, metrics: metrics}, + slot, + randaoReveal, + toGraffitiBuffer(graffiti) + ); + + return {data: block, version: config.getForkName(block.slot)}; + }, + + async produceAttestationData(committeeIndex, slot) { + notWhileSyncing(); + + await waitForSlot(slot); // Must never request for a future slot > currentSlot + + const headRoot = chain.forkChoice.getHeadRoot(); + const state = await chain.regen.getBlockSlotState(headRoot, slot); + return {data: assembleAttestationData(state.config, state, headRoot, slot, committeeIndex)}; + }, + + /** + * GET `/eth/v1/validator/sync_committee_contribution` + * + * Requests that the beacon node produce a sync committee contribution. + * + * https://github.com/ethereum/eth2.0-APIs/pull/138 + * + * @param slot The slot for which a sync committee contribution should be created. + * @param subcommitteeIndex The subcommittee index for which to produce the contribution. + * @param beaconBlockRoot The block root for which to produce the contribution. + */ + async produceSyncCommitteeContribution(slot, subcommitteeIndex, beaconBlockRoot) { + const contribution = db.syncCommittee.getSyncCommitteeContribution(subcommitteeIndex, slot, beaconBlockRoot); + if (!contribution) throw new ApiError(500, "No contribution available"); + return {data: contribution}; + }, + + async getProposerDuties(epoch) { + notWhileSyncing(); + + const startSlot = computeStartSlotAtEpoch(config, epoch); + await waitForSlot(startSlot); // Must never request for a future slot > currentSlot + + const state = await chain.getHeadStateAtCurrentEpoch(); + const duties: routes.validator.ProposerDuty[] = []; + + for (let slot = startSlot; slot < startSlot + config.params.SLOTS_PER_EPOCH; slot++) { + // getBeaconProposer ensures the requested epoch is correct + const blockProposerIndex = state.getBeaconProposer(slot); + duties.push({slot, validatorIndex: blockProposerIndex, pubkey: state.validators[blockProposerIndex].pubkey}); + } + + // Returns `null` on the one-off scenario where the genesis block decides its own shuffling. + // It should be set to the latest block applied to `self` or the genesis block root. + const dependentRoot = proposerShufflingDecisionRoot(config, state) || (await getGenesisBlockRoot(state)); + + return { + data: duties, + dependentRoot, + }; + }, + + async getAttesterDuties(epoch, validatorIndices) { + notWhileSyncing(); + + if (validatorIndices.length === 0) { + throw new ApiError(400, "No validator to get attester duties"); + } + + // May request for an epoch that's in the future + await waitForNextClosestEpoch(); + + // Check if the epoch is in the future after waiting for requested slot + if (epoch > chain.clock.currentEpoch + 1) { + throw new ApiError(400, "Cannot get duties for epoch more than one ahead"); + } + + const state = await chain.getHeadStateAtCurrentEpoch(); + + // TODO: Determine what the current epoch would be if we fast-forward our system clock by + // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. + // + // Most of the time, `tolerantCurrentEpoch` will be equal to `currentEpoch`. However, during + // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerantCurrentEpoch` + // will equal `currentEpoch + 1` + + const duties: routes.validator.AttesterDuty[] = []; + for (const validatorIndex of validatorIndices) { + const validator = state.validators[validatorIndex]; + if (!validator) { + throw new ApiError(400, `Validator index ${validatorIndex} not in state`); + } + const duty = assembleAttesterDuty( + config, + {pubkey: validator.pubkey, index: validatorIndex}, + state.epochCtx, + epoch + ); + if (duty) duties.push(duty); + } + + const dependentRoot = attesterShufflingDecisionRoot(config, state, epoch) || (await getGenesisBlockRoot(state)); + + return { + data: duties, + dependentRoot, + }; + }, + + /** + * `POST /eth/v1/validator/duties/sync/{epoch}` + * + * Requests the beacon node to provide a set of sync committee duties for a particular epoch. + * - Although pubkey can be inferred from the index we return it to keep this call analogous with the one that + * fetches attester duties. + * - `sync_committee_index` is the index of the validator in the sync committee. This can be used to infer the + * subnet to which the contribution should be broadcast. Note, there can be multiple per validator. + * + * https://github.com/ethereum/eth2.0-APIs/pull/134 + * + * @param validatorIndices an array of the validator indices for which to obtain the duties. + */ + async getSyncCommitteeDuties(epoch, validatorIndices) { + notWhileSyncing(); + + if (validatorIndices.length === 0) { + throw new ApiError(400, "No validator to get attester duties"); + } + + // May request for an epoch that's in the future + await waitForNextClosestEpoch(); + + // Note: does not support requesting past duties + const state = chain.getHeadState(); + + // Ensures `epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD <= current_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1` + const syncComitteeValidatorIndexMap = getSyncComitteeValidatorIndexMap(config, state, epoch); + + const duties: routes.validator.SyncDuty[] = validatorIndices.map((validatorIndex) => ({ + pubkey: state.index2pubkey[validatorIndex].toBytes(), + validatorIndex, + validatorSyncCommitteeIndices: syncComitteeValidatorIndexMap.get(validatorIndex) ?? [], + })); + + return { + data: duties, + // TODO: Compute a proper dependentRoot for this syncCommittee shuffling + dependentRoot: ZERO_HASH, + }; + }, + + async getAggregatedAttestation(attestationDataRoot, slot) { + notWhileSyncing(); + + await waitForSlot(slot); // Must never request for a future slot > currentSlot + + const attestations = await db.attestation.getAttestationsByDataRoot(slot, attestationDataRoot); + + if (attestations.length === 0) { + throw Error("No matching attestations found for attestationData"); + } + + // first iterate through collected committee attestations + // expanding each signature and building an aggregated bitlist + const signatures: Signature[] = []; + const aggregationBits = attestations[0].aggregationBits; + for (const attestation of attestations) { + try { + const signature = bls.Signature.fromBytes(attestation.signature.valueOf() as Uint8Array); + signatures.push(signature); + let index = 0; + for (const bit of readonlyValues(attestation.aggregationBits)) { + if (bit) { + aggregationBits[index] = true; + } + index++; + } + } catch (e) { + logger.verbose("Invalid attestation signature", e); + } + } + + // then create/return the aggregate signature + return { + data: { + data: attestations[0].data, + signature: bls.Signature.aggregate(signatures).toBytes(), + aggregationBits, + }, + }; + }, + + async publishAggregateAndProofs(signedAggregateAndProofs) { + notWhileSyncing(); + + await Promise.all( + signedAggregateAndProofs.map(async (signedAggregateAndProof) => { + const attestation = signedAggregateAndProof.message.aggregate; + // TODO: Validate in batch + await validateGossipAggregateAndProof(config, chain, db, signedAggregateAndProof, { + attestation: attestation, + validSignature: false, + }); + await Promise.all([ + db.aggregateAndProof.add(signedAggregateAndProof.message), + db.seenAttestationCache.addAggregateAndProof(signedAggregateAndProof.message), + network.gossip.publishBeaconAggregateAndProof(signedAggregateAndProof), + ]); + }) + ); + }, + + /** + * POST `/eth/v1/validator/contribution_and_proofs` + * + * Publish multiple signed sync committee contribution and proofs + * + * https://github.com/ethereum/eth2.0-APIs/pull/137 + */ + async publishContributionAndProofs(contributionAndProofs) { + notWhileSyncing(); + + await Promise.all( + contributionAndProofs.map(async (contributionAndProof) => { + // TODO: Validate in batch + await validateSyncCommitteeGossipContributionAndProof(config, chain, db, { + contributionAndProof, + validSignature: false, + }); + db.syncCommitteeContribution.add(contributionAndProof.message); + await network.gossip.publishContributionAndProof(contributionAndProof); + }) + ); + }, + + async prepareBeaconCommitteeSubnet(subscriptions) { + notWhileSyncing(); + + network.prepareBeaconCommitteeSubnet( + subscriptions.map(({validatorIndex, slot, isAggregator, committeesAtSlot, committeeIndex}) => ({ + validatorIndex: validatorIndex, + subnet: computeSubnetForCommitteesAtSlot(config, slot, committeesAtSlot, committeeIndex), + slot: slot, + isAggregator: isAggregator, + })) + ); + + // TODO: + // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the + // required subnets. + }, + + /** + * POST `/eth/v1/validator/sync_committee_subscriptions` + * + * Subscribe to a number of sync committee subnets. + * Sync committees are not present in phase0, but are required for Altair networks. + * Subscribing to sync committee subnets is an action performed by VC to enable network participation in Altair networks, + * and only required if the VC has an active validator in an active sync committee. + * + * https://github.com/ethereum/eth2.0-APIs/pull/136 + */ + async prepareSyncCommitteeSubnets(subscriptions) { + notWhileSyncing(); + + // TODO: Cache this value + const SYNC_COMMITTEE_SUBNET_SIZE = Math.floor(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) { + 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(config, sub.untilEpoch + 1), + isAggregator: true, + }); + } + } + + network.prepareSyncCommitteeSubnets(subs); + }, + }; +} diff --git a/packages/lodestar/src/api/impl/validator/interface.ts b/packages/lodestar/src/api/impl/validator/interface.ts deleted file mode 100644 index fc42732a64..0000000000 --- a/packages/lodestar/src/api/impl/validator/interface.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @module api/rpc - */ -import { - BLSSignature, - CommitteeIndex, - Epoch, - Root, - phase0, - allForks, - Slot, - ValidatorIndex, - altair, -} from "@chainsafe/lodestar-types"; - -/** - * The API interface defines the calls that can be made from a Validator - */ -export interface IValidatorApi { - getProposerDuties(epoch: Epoch): Promise; - getAttesterDuties(epoch: Epoch, validatorIndices: ValidatorIndex[]): Promise; - getSyncCommitteeDuties(epoch: number, validatorIndices: ValidatorIndex[]): Promise; - produceBlock(slot: Slot, randaoReveal: BLSSignature, graffiti: string): Promise; - produceAttestationData(index: CommitteeIndex, slot: Slot): Promise; - produceSyncCommitteeContribution( - slot: Slot, - subcommitteeIndex: number, - beaconBlockRoot: Root - ): Promise; - getAggregatedAttestation(attestationDataRoot: Root, slot: Slot): Promise; - publishAggregateAndProofs(signedAggregateAndProofs: phase0.SignedAggregateAndProof[]): Promise; - publishContributionAndProofs(contributionAndProofs: altair.SignedContributionAndProof[]): Promise; - prepareBeaconCommitteeSubnet(subscriptions: phase0.BeaconCommitteeSubscription[]): Promise; - prepareSyncCommitteeSubnets(subscriptions: altair.SyncCommitteeSubscription[]): Promise; -} diff --git a/packages/lodestar/src/api/impl/validator/validator.ts b/packages/lodestar/src/api/impl/validator/validator.ts deleted file mode 100644 index dea47fe327..0000000000 --- a/packages/lodestar/src/api/impl/validator/validator.ts +++ /dev/null @@ -1,455 +0,0 @@ -/** - * @module api/rpc - */ - -import bls, {Signature} from "@chainsafe/bls"; -import { - CachedBeaconState, - computeStartSlotAtEpoch, - proposerShufflingDecisionRoot, - attesterShufflingDecisionRoot, - computeSubnetForCommitteesAtSlot, -} from "@chainsafe/lodestar-beacon-state-transition"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {GENESIS_SLOT, SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params"; -import {Bytes96, Epoch, Root, phase0, allForks, Slot, ValidatorIndex, altair} from "@chainsafe/lodestar-types"; -import {BeaconState} from "@chainsafe/lodestar-types/lib/allForks"; -import {ILogger} from "@chainsafe/lodestar-utils"; -import {readonlyValues} from "@chainsafe/ssz"; -import {IBeaconChain} from "../../../chain"; -import {assembleAttestationData} from "../../../chain/factory/attestation"; -import {assembleBlock} from "../../../chain/factory/block"; -import {assembleAttesterDuty} from "../../../chain/factory/duties"; -import {validateGossipAggregateAndProof} from "../../../chain/validation"; -import {ZERO_HASH} from "../../../constants"; -import {IBeaconDb} from "../../../db"; -import {IEth1ForBlockProduction} from "../../../eth1"; -import {IMetrics} from "../../../metrics"; -import {INetwork} from "../../../network"; -import {IBeaconSync, SyncState} from "../../../sync"; -import {toGraffitiBuffer} from "../../../util/graffiti"; -import {IApiOptions} from "../../options"; -import {ApiError} from "../errors"; -import {ApiNamespace, IApiModules} from "../interface"; -import {IValidatorApi} from "./interface"; -import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof"; -import {CommitteeSubscription} from "../../../network/subnets"; -import {getSyncComitteeValidatorIndexMap} from "./utils"; - -/** - * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a - * future slot, wait some time instead of rejecting the request because it's in the future - */ -const MAX_API_CLOCK_DISPARITY_MS = 1000; - -/** - * If the node is within this many epochs from the head, we declare it to be synced regardless of - * the network sync state. - * - * This helps prevent attacks where nodes can convince us that we're syncing some non-existent - * finalized head. - */ -const SYNC_TOLERANCE_EPOCHS = 8; - -/** - * Server implementation for handling validator duties. - * See `@chainsafe/lodestar-validator/src/api` for the client implementation). - */ -export class ValidatorApi implements IValidatorApi { - namespace: ApiNamespace; - - private config: IBeaconConfig; - private chain: IBeaconChain; - private db: IBeaconDb; - private eth1: IEth1ForBlockProduction; - private network: INetwork; - private sync: IBeaconSync; - private metrics: IMetrics | null; - private logger: ILogger; - // Cached for duties - private genesisBlockRoot: Root | null = null; - - constructor( - opts: Partial, - modules: Pick - ) { - this.namespace = ApiNamespace.VALIDATOR; - this.config = modules.config; - this.chain = modules.chain; - this.db = modules.db; - this.eth1 = modules.eth1; - this.network = modules.network; - this.sync = modules.sync; - this.metrics = modules.metrics; - this.logger = modules.logger; - } - - async produceBlock(slot: Slot, randaoReveal: Bytes96, graffiti = ""): Promise { - this.notWhileSyncing(); - - await this.waitForSlot(slot); // Must never request for a future slot > currentSlot - - return await assembleBlock( - {config: this.config, chain: this.chain, db: this.db, eth1: this.eth1, metrics: this.metrics}, - slot, - randaoReveal, - toGraffitiBuffer(graffiti) - ); - } - - async produceAttestationData(committeeIndex: phase0.CommitteeIndex, slot: Slot): Promise { - this.notWhileSyncing(); - - await this.waitForSlot(slot); // Must never request for a future slot > currentSlot - - const headRoot = this.chain.forkChoice.getHeadRoot(); - const state = await this.chain.regen.getBlockSlotState(headRoot, slot); - return assembleAttestationData(state.config, state, headRoot, slot, committeeIndex); - } - - /** - * GET `/eth/v1/validator/sync_committee_contribution` - * - * Requests that the beacon node produce a sync committee contribution. - * - * https://github.com/ethereum/eth2.0-APIs/pull/138 - * - * @param slot The slot for which a sync committee contribution should be created. - * @param subcommitteeIndex The subcommittee index for which to produce the contribution. - * @param beaconBlockRoot The block root for which to produce the contribution. - */ - async produceSyncCommitteeContribution( - slot: Slot, - subcommitteeIndex: number, - beaconBlockRoot: Root - ): Promise { - const contribution = this.db.syncCommittee.getSyncCommitteeContribution(subcommitteeIndex, slot, beaconBlockRoot); - if (!contribution) throw new ApiError(500, "No contribution available"); - return contribution; - } - - async getProposerDuties(epoch: Epoch): Promise { - this.notWhileSyncing(); - - const startSlot = computeStartSlotAtEpoch(this.config, epoch); - await this.waitForSlot(startSlot); // Must never request for a future slot > currentSlot - - const state = await this.chain.getHeadStateAtCurrentEpoch(); - const duties: phase0.ProposerDuty[] = []; - - for (let slot = startSlot; slot < startSlot + this.config.params.SLOTS_PER_EPOCH; slot++) { - // getBeaconProposer ensures the requested epoch is correct - const blockProposerIndex = state.getBeaconProposer(slot); - duties.push({slot, validatorIndex: blockProposerIndex, pubkey: state.validators[blockProposerIndex].pubkey}); - } - - // Returns `null` on the one-off scenario where the genesis block decides its own shuffling. - // It should be set to the latest block applied to `self` or the genesis block root. - const dependentRoot = proposerShufflingDecisionRoot(this.config, state) || (await this.getGenesisBlockRoot(state)); - - return { - data: duties, - dependentRoot, - }; - } - - async getAttesterDuties(epoch: number, validatorIndices: ValidatorIndex[]): Promise { - this.notWhileSyncing(); - - if (validatorIndices.length === 0) { - throw new ApiError(400, "No validator to get attester duties"); - } - - // May request for an epoch that's in the future - await this.waitForNextClosestEpoch(); - - // Check if the epoch is in the future after waiting for requested slot - if (epoch > this.chain.clock.currentEpoch + 1) { - throw new ApiError(400, "Cannot get duties for epoch more than one ahead"); - } - - const state = await this.chain.getHeadStateAtCurrentEpoch(); - - // TODO: Determine what the current epoch would be if we fast-forward our system clock by - // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. - // - // Most of the time, `tolerantCurrentEpoch` will be equal to `currentEpoch`. However, during - // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerantCurrentEpoch` - // will equal `currentEpoch + 1` - - const duties: phase0.AttesterDuty[] = []; - for (const validatorIndex of validatorIndices) { - const validator = state.validators[validatorIndex]; - if (!validator) { - throw new ApiError(400, `Validator index ${validatorIndex} not in state`); - } - const duty = assembleAttesterDuty( - this.config, - {pubkey: validator.pubkey, index: validatorIndex}, - state.epochCtx, - epoch - ); - if (duty) duties.push(duty); - } - - const dependentRoot = - attesterShufflingDecisionRoot(this.config, state, epoch) || (await this.getGenesisBlockRoot(state)); - - return { - data: duties, - dependentRoot, - }; - } - - /** - * `POST /eth/v1/validator/duties/sync/{epoch}` - * - * Requests the beacon node to provide a set of sync committee duties for a particular epoch. - * - Although pubkey can be inferred from the index we return it to keep this call analogous with the one that - * fetches attester duties. - * - `sync_committee_index` is the index of the validator in the sync committee. This can be used to infer the - * subnet to which the contribution should be broadcast. Note, there can be multiple per validator. - * - * https://github.com/ethereum/eth2.0-APIs/pull/134 - * - * @param validatorIndices an array of the validator indices for which to obtain the duties. - */ - async getSyncCommitteeDuties(epoch: number, validatorIndices: ValidatorIndex[]): Promise { - this.notWhileSyncing(); - - if (validatorIndices.length === 0) { - throw new ApiError(400, "No validator to get attester duties"); - } - - // May request for an epoch that's in the future - await this.waitForNextClosestEpoch(); - - // 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: syncComitteeValidatorIndexMap.get(validatorIndex) ?? [], - })); - - return { - data: duties, - // TODO: Compute a proper dependentRoot for this syncCommittee shuffling - dependentRoot: ZERO_HASH, - }; - } - - async getAggregatedAttestation(attestationDataRoot: Root, slot: Slot): Promise { - this.notWhileSyncing(); - - await this.waitForSlot(slot); // Must never request for a future slot > currentSlot - - const attestations = await this.db.attestation.getAttestationsByDataRoot(slot, attestationDataRoot); - - if (attestations.length === 0) { - throw Error("No matching attestations found for attestationData"); - } - - // first iterate through collected committee attestations - // expanding each signature and building an aggregated bitlist - const signatures: Signature[] = []; - const aggregationBits = attestations[0].aggregationBits; - for (const attestation of attestations) { - try { - const signature = bls.Signature.fromBytes(attestation.signature.valueOf() as Uint8Array); - signatures.push(signature); - let index = 0; - for (const bit of readonlyValues(attestation.aggregationBits)) { - if (bit) { - aggregationBits[index] = true; - } - index++; - } - } catch (e) { - this.logger.verbose("Invalid attestation signature", e); - } - } - - // then create/return the aggregate signature - return { - data: attestations[0].data, - signature: bls.Signature.aggregate(signatures).toBytes(), - aggregationBits, - }; - } - - async publishAggregateAndProofs(signedAggregateAndProofs: phase0.SignedAggregateAndProof[]): Promise { - this.notWhileSyncing(); - - await Promise.all( - signedAggregateAndProofs.map(async (signedAggregateAndProof) => { - const attestation = signedAggregateAndProof.message.aggregate; - // TODO: Validate in batch - await validateGossipAggregateAndProof(this.config, this.chain, this.db, signedAggregateAndProof, { - attestation: attestation, - validSignature: false, - }); - await Promise.all([ - this.db.aggregateAndProof.add(signedAggregateAndProof.message), - this.db.seenAttestationCache.addAggregateAndProof(signedAggregateAndProof.message), - this.network.gossip.publishBeaconAggregateAndProof(signedAggregateAndProof), - ]); - }) - ); - } - - /** - * POST `/eth/v1/validator/contribution_and_proofs` - * - * Publish multiple signed sync committee contribution and proofs - * - * https://github.com/ethereum/eth2.0-APIs/pull/137 - */ - async publishContributionAndProofs(contributionAndProofs: altair.SignedContributionAndProof[]): Promise { - this.notWhileSyncing(); - - await Promise.all( - contributionAndProofs.map(async (contributionAndProof) => { - // TODO: Validate in batch - await validateSyncCommitteeGossipContributionAndProof(this.config, this.chain, this.db, { - contributionAndProof, - validSignature: false, - }); - this.db.syncCommitteeContribution.add(contributionAndProof.message); - await this.network.gossip.publishContributionAndProof(contributionAndProof); - }) - ); - } - - async prepareBeaconCommitteeSubnet(subscriptions: phase0.BeaconCommitteeSubscription[]): Promise { - this.notWhileSyncing(); - - this.network.prepareBeaconCommitteeSubnet( - subscriptions.map(({validatorIndex, slot, isAggregator, committeesAtSlot, committeeIndex}) => ({ - validatorIndex: validatorIndex, - subnet: computeSubnetForCommitteesAtSlot(this.config, slot, committeesAtSlot, committeeIndex), - slot: slot, - isAggregator: isAggregator, - })) - ); - - // TODO: - // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the - // required subnets. - } - - /** - * POST `/eth/v1/validator/sync_committee_subscriptions` - * - * Subscribe to a number of sync committee subnets. - * Sync committees are not present in phase0, but are required for Altair networks. - * Subscribing to sync committee subnets is an action performed by VC to enable network participation in Altair networks, - * and only required if the VC has an active validator in an active sync committee. - * - * https://github.com/ethereum/eth2.0-APIs/pull/136 - */ - async prepareSyncCommitteeSubnets(subscriptions: altair.SyncCommitteeSubscription[]): Promise { - this.notWhileSyncing(); - - // 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) { - 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, - }); - } - } - - this.network.prepareSyncCommitteeSubnets(subs); - } - - /** Compute and cache the genesis block root */ - private async getGenesisBlockRoot(state: CachedBeaconState): Promise { - if (!this.genesisBlockRoot) { - // Close to genesis the genesis block may not be available in the DB - if (state.slot < this.config.params.SLOTS_PER_HISTORICAL_ROOT) { - this.genesisBlockRoot = state.blockRoots[0]; - } - - const genesisBlock = await this.chain.getCanonicalBlockAtSlot(GENESIS_SLOT); - if (genesisBlock) { - this.genesisBlockRoot = this.config - .getForkTypes(genesisBlock.message.slot) - .SignedBeaconBlock.hashTreeRoot(genesisBlock); - } - } - - // If for some reason the genesisBlockRoot is not able don't prevent validators from - // proposing or attesting. If the genesisBlockRoot is wrong, at worst it may trigger a re-fetch of the duties - return this.genesisBlockRoot || ZERO_HASH; - } - - /** - * If advancing the local clock `MAX_API_CLOCK_DISPARITY_MS` ticks to the requested slot, wait for its start - * Prevents the validator from getting errors from the API if the clock is a bit advanced - */ - private async waitForSlot(slot: Slot): Promise { - const slotStartSec = this.chain.genesisTime + slot * this.config.params.SECONDS_PER_SLOT; - const msToSlot = slotStartSec * 1000 - Date.now(); - if (msToSlot > 0 && msToSlot < MAX_API_CLOCK_DISPARITY_MS) { - await this.chain.clock.waitForSlot(slot); - } - } - - /** - * If advancing the local clock `MAX_API_CLOCK_DISPARITY_MS` ticks to the next epoch, wait for slot 0 of the next epoch. - * Prevents a validator from not being able to get the attestater duties correctly if the beacon and validator clocks are off - */ - private async waitForNextClosestEpoch(): Promise { - const nextEpoch = this.chain.clock.currentEpoch + 1; - const secPerEpoch = this.config.params.SLOTS_PER_EPOCH * this.config.params.SECONDS_PER_SLOT; - const nextEpochStartSec = this.chain.genesisTime + nextEpoch * secPerEpoch; - const msToNextEpoch = nextEpochStartSec * 1000 - Date.now(); - if (msToNextEpoch > 0 && msToNextEpoch < MAX_API_CLOCK_DISPARITY_MS) { - await this.chain.clock.waitForSlot(computeStartSlotAtEpoch(this.config, nextEpoch)); - } - } - - /** - * Reject any request while the node is syncing - */ - private notWhileSyncing(): void { - // Consider node synced before or close to genesis - if (this.chain.clock.currentSlot < this.config.params.SLOTS_PER_EPOCH) { - return; - } - - const syncState = this.sync.state; - switch (syncState) { - case SyncState.SyncingFinalized: - case SyncState.SyncingHead: { - const currentSlot = this.chain.clock.currentSlot; - const headSlot = this.chain.forkChoice.getHead().slot; - if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * this.config.params.SLOTS_PER_EPOCH) { - throw new ApiError(503, `Node is syncing, headSlot ${headSlot} currentSlot ${currentSlot}`); - } else { - return; - } - } - - case SyncState.Synced: - return; - - case SyncState.Stalled: - throw new ApiError(503, "Node is waiting for peers"); - } - } -} diff --git a/packages/lodestar/src/api/options.ts b/packages/lodestar/src/api/options.ts index 09ff79ed8f..07db77b105 100644 --- a/packages/lodestar/src/api/options.ts +++ b/packages/lodestar/src/api/options.ts @@ -1,9 +1,9 @@ -import {defaultApiRestOptions, IRestApiOptions} from "./rest/options"; +import {restApiOptionsDefault, RestApiOptions} from "./rest"; export interface IApiOptions { - rest: IRestApiOptions; + rest: RestApiOptions; } export const defaultApiOptions: IApiOptions = { - rest: defaultApiRestOptions, + rest: restApiOptionsDefault, }; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/getBlock.ts b/packages/lodestar/src/api/rest/beacon/blocks/getBlock.ts deleted file mode 100644 index 4b30dd19f1..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/getBlock.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {ApiController} from "../../types"; - -// V2 handler is backwards compatible so re-use it for both versions -const handler: ApiController["handler"] = async function (req) { - const block = await this.api.beacon.blocks.getBlock(req.params.blockId); - return { - version: this.config.getForkName(block.message.slot), - data: this.config.getForkTypes(block.message.slot).SignedBeaconBlock.toJson(block, {case: "snake"}), - }; -}; - -const schema = { - params: { - type: "object", - required: ["blockId"], - properties: { - blockId: { - types: "string", - }, - }, - }, -}; - -export const getBlock: ApiController = { - url: "/eth/v1/beacon/blocks/:blockId", - method: "GET", - id: "getBlock", - handler, - schema, -}; - -export const getBlockV2: ApiController = { - url: "/eth/v2/beacon/blocks/:blockId", - method: "GET", - id: "getBlockV2", - handler, - schema, -}; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/getBlockAttestations.ts b/packages/lodestar/src/api/rest/beacon/blocks/getBlockAttestations.ts deleted file mode 100644 index 313e0d4a40..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/getBlockAttestations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {ApiController} from "../../types"; - -export const getBlockAttestations: ApiController = { - url: "/eth/v1/beacon/blocks/:blockId/attestations", - method: "GET", - id: "getBlockAttestations", - - handler: async function (req) { - const data = await this.api.beacon.blocks.getBlock(req.params.blockId); - return { - data: Array.from(data.message.body.attestations).map((attestations) => { - this.config.types.phase0.Attestation.toJson(attestations, {case: "snake"}); - }), - }; - }, - - schema: { - params: { - type: "object", - required: ["blockId"], - properties: { - blockId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/getBlockHeader.ts b/packages/lodestar/src/api/rest/beacon/blocks/getBlockHeader.ts deleted file mode 100644 index b6ccfd2a25..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/getBlockHeader.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {ApiController} from "../../types"; - -export const getBlockHeader: ApiController = { - url: "/eth/v1/beacon/headers/:blockId", - method: "GET", - id: "getBlockHeader", - - handler: async function (req) { - const data = await this.api.beacon.blocks.getBlockHeader(req.params.blockId); - return { - data: this.config.types.phase0.SignedBeaconHeaderResponse.toJson(data, {case: "snake"}), - }; - }, - - schema: { - params: { - type: "object", - required: ["blockId"], - properties: { - blockId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/getBlockHeaders.ts b/packages/lodestar/src/api/rest/beacon/blocks/getBlockHeaders.ts deleted file mode 100644 index 8fa3f063c0..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/getBlockHeaders.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {Root, Slot} from "@chainsafe/lodestar-types"; -import {ApiController} from "../../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const getBlockHeaders: ApiController<{slot?: string | number; parent_root?: string}> = { - url: "/eth/v1/beacon/headers", - method: "GET", - id: "getBlockHeaders", - - handler: async function (req) { - let slot: Slot | undefined; - if (req.query.slot || req.query.slot === 0) { - slot = this.config.types.Slot.fromJson(req.query.slot); - } - let parentRoot: Root | undefined; - if (req.query.parent_root) { - parentRoot = this.config.types.Root.fromJson(req.query.parent_root); - } - const data = await this.api.beacon.blocks.getBlockHeaders({slot, parentRoot}); - return { - data: data.map((item) => this.config.types.phase0.SignedBeaconHeaderResponse.toJson(item, {case: "snake"})), - }; - }, - - schema: { - querystring: { - type: "object", - required: [], - properties: { - slot: { - type: "number", - minimum: 0, - }, - parent_root: { - type: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/getBlockRoot.ts b/packages/lodestar/src/api/rest/beacon/blocks/getBlockRoot.ts deleted file mode 100644 index c7443d7ca2..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/getBlockRoot.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {ApiController} from "../../types"; - -export const getBlockRoot: ApiController = { - url: "/eth/v1/beacon/blocks/:blockId/root", - method: "GET", - id: "getBlockRoot", - - handler: async function (req) { - const root = await this.api.beacon.blocks.getBlockRoot(req.params.blockId); - return { - data: { - root: this.config.types.Root.toJson(root), - }, - }; - }, - - schema: { - params: { - type: "object", - required: ["blockId"], - properties: { - blockId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/index.ts b/packages/lodestar/src/api/rest/beacon/blocks/index.ts deleted file mode 100644 index 84ab8eaf51..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {getBlock, getBlockV2} from "./getBlock"; -import {getBlockAttestations} from "./getBlockAttestations"; -import {getBlockHeader} from "./getBlockHeader"; -import {getBlockHeaders} from "./getBlockHeaders"; -import {getBlockRoot} from "./getBlockRoot"; -import {publishBlock} from "./publishBlock"; - -export const beaconBlocksRoutes = [ - getBlock, - getBlockV2, - getBlockAttestations, - getBlockHeader, - getBlockHeaders, - getBlockRoot, - publishBlock, -]; diff --git a/packages/lodestar/src/api/rest/beacon/blocks/publishBlock.ts b/packages/lodestar/src/api/rest/beacon/blocks/publishBlock.ts deleted file mode 100644 index 0d889e776d..0000000000 --- a/packages/lodestar/src/api/rest/beacon/blocks/publishBlock.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {allForks} from "@chainsafe/lodestar-types"; -import {SignedBeaconBlock} from "@chainsafe/lodestar-types/lib/allForks"; -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", - id: "publishBlock", - - handler: async function (req) { - let block: allForks.SignedBeaconBlock; - try { - const slot = (req.body as SignedBeaconBlock).message.slot; - block = this.config.getForkTypes(slot).SignedBeaconBlock.fromJson(req.body, {case: "snake"}); - } catch (e) { - throw new ValidationError(`Failed to deserialize block: ${(e as Error).message}`); - } - await this.api.beacon.blocks.publishBlock(block); - return {}; - }, - - schema: { - body: { - type: "object", - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/getGenesis.ts b/packages/lodestar/src/api/rest/beacon/getGenesis.ts deleted file mode 100644 index 8808eb6978..0000000000 --- a/packages/lodestar/src/api/rest/beacon/getGenesis.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ApiController} from "../types"; - -export const getGenesis: ApiController = { - url: "/eth/v1/beacon/genesis", - method: "GET", - id: "getGenesis", - - handler: async function () { - const genesis = await this.api.beacon.getGenesis(); - return { - data: this.config.types.phase0.Genesis.toJson(genesis, {case: "snake"}), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/index.ts b/packages/lodestar/src/api/rest/beacon/index.ts deleted file mode 100644 index b30e12a36e..0000000000 --- a/packages/lodestar/src/api/rest/beacon/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {beaconBlocksRoutes} from "./blocks"; -import {beaconStateRoutes} from "./state"; -import {beaconPoolRoutes} from "./pool"; -import {getGenesis} from "./getGenesis"; - -export const beaconRoutes = [ - // - ...beaconBlocksRoutes, - ...beaconStateRoutes, - ...beaconPoolRoutes, - getGenesis, -]; diff --git a/packages/lodestar/src/api/rest/beacon/pool/getPoolAttestations.ts b/packages/lodestar/src/api/rest/beacon/pool/getPoolAttestations.ts deleted file mode 100644 index a4b8bd2484..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/getPoolAttestations.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {ApiController} from "../../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const getPoolAttestations: ApiController<{slot: string; committee_index: string}> = { - url: "/eth/v1/beacon/pool/attestations", - method: "GET", - id: "getPoolAttestations", - - handler: async function (req) { - const attestations = await this.api.beacon.pool.getAttestations({ - slot: Number(req.query.slot), - committeeIndex: Number(req.query.committee_index), - }); - return { - data: attestations.map((attestation) => - this.config.types.phase0.Attestation.toJson(attestation, {case: "snake"}) - ), - }; - }, - - schema: { - querystring: { - type: "object", - required: [], - properties: { - slot: { - types: "number", - min: 0, - }, - committee_index: { - types: "number", - min: 0, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/getPoolAttesterSlashings.ts b/packages/lodestar/src/api/rest/beacon/pool/getPoolAttesterSlashings.ts deleted file mode 100644 index 0ee4b661ae..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/getPoolAttesterSlashings.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {ApiController} from "../../types"; - -export const getPoolAttesterSlashings: ApiController = { - url: "/eth/v1/beacon/pool/attester_slashings", - method: "GET", - id: "getPoolAttesterSlashings", - - handler: async function () { - const attesterSlashings = await this.api.beacon.pool.getAttesterSlashings(); - return { - data: attesterSlashings.map((slashing) => - this.config.types.phase0.AttesterSlashing.toJson(slashing, {case: "snake"}) - ), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/getPoolProposerSlashings.ts b/packages/lodestar/src/api/rest/beacon/pool/getPoolProposerSlashings.ts deleted file mode 100644 index 6a2a26007a..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/getPoolProposerSlashings.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {ApiController} from "../../types"; - -export const getPoolProposerSlashings: ApiController = { - url: "/eth/v1/beacon/pool/proposer_slashings", - method: "GET", - id: "getPoolProposerSlashings", - - handler: async function () { - const proposerSlashings = await this.api.beacon.pool.getProposerSlashings(); - return { - data: proposerSlashings.map((slashing) => - this.config.types.phase0.ProposerSlashing.toJson(slashing, {case: "snake"}) - ), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/getPoolVoluntaryExits.ts b/packages/lodestar/src/api/rest/beacon/pool/getPoolVoluntaryExits.ts deleted file mode 100644 index 0965abcfb4..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/getPoolVoluntaryExits.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ApiController} from "../../types"; - -export const getPoolVoluntaryExits: ApiController = { - url: "/eth/v1/beacon/pool/voluntary_exits", - method: "GET", - id: "getPoolVoluntaryExits", - - handler: async function () { - const exits = await this.api.beacon.pool.getVoluntaryExits(); - return { - data: exits.map((exit) => this.config.types.phase0.SignedVoluntaryExit.toJson(exit, {case: "snake"})), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/index.ts b/packages/lodestar/src/api/rest/beacon/pool/index.ts deleted file mode 100644 index bc91f80f72..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {getPoolAttestations} from "./getPoolAttestations"; -import {getPoolAttesterSlashings} from "./getPoolAttesterSlashings"; -import {getPoolProposerSlashings} from "./getPoolProposerSlashings"; -import {getPoolVoluntaryExits} from "./getPoolVoluntaryExits"; -import {submitPoolAttestations} from "./submitPoolAttestations"; -import {submitPoolAttesterSlashings} from "./submitPoolAttesterSlashings"; -import {submitPoolProposerSlashings} from "./submitPoolProposerSlashings"; -import {submitPoolSyncCommitteeSignatures} from "./submitPoolSyncCommitteeSignatures"; -import {submitPoolVoluntaryExit} from "./submitPoolVoluntaryExit"; - -export const beaconPoolRoutes = [ - getPoolAttestations, - getPoolAttesterSlashings, - getPoolProposerSlashings, - getPoolVoluntaryExits, - submitPoolAttestations, - submitPoolAttesterSlashings, - submitPoolProposerSlashings, - submitPoolSyncCommitteeSignatures, - submitPoolVoluntaryExit, -]; diff --git a/packages/lodestar/src/api/rest/beacon/pool/submitPoolAttestations.ts b/packages/lodestar/src/api/rest/beacon/pool/submitPoolAttestations.ts deleted file mode 100644 index ac5066c1ad..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/submitPoolAttestations.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Json} from "@chainsafe/ssz"; -import {ValidationError} from "../../../impl/errors"; -import {ApiController} from "../../types"; - -export const submitPoolAttestations: ApiController = { - url: "/eth/v1/beacon/pool/attestations", - method: "POST", - id: "submitPoolAttestations", - - handler: async function (req) { - const attestations = req.body.map((attestation) => { - try { - return this.config.types.phase0.Attestation.fromJson(attestation, {case: "snake"}); - } catch (e) { - throw new ValidationError(`SSZ deserialize error: ${(e as Error).message}`); - } - }); - - await this.api.beacon.pool.submitAttestations(attestations); - return {}; - }, - - schema: { - body: { - type: "array", - minItems: 1, - items: { - type: "object", - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/submitPoolAttesterSlashings.ts b/packages/lodestar/src/api/rest/beacon/pool/submitPoolAttesterSlashings.ts deleted file mode 100644 index 255a843910..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/submitPoolAttesterSlashings.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {ValidationError} from "../../../impl/errors"; -import {ApiController} from "../../types"; - -export const submitPoolAttesterSlashings: ApiController = { - url: "/eth/v1/beacon/pool/attester_slashings", - method: "POST", - id: "submitPoolAttesterSlashings", - - handler: async function (req) { - let slashing: phase0.AttesterSlashing; - try { - slashing = this.config.types.phase0.AttesterSlashing.fromJson(req.body, {case: "snake"}); - } catch (e) { - throw new ValidationError(`SSZ deserialize error: ${(e as Error).message}`); - } - await this.api.beacon.pool.submitAttesterSlashing(slashing); - return {}; - }, - - schema: { - body: { - type: "object", - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/submitPoolProposerSlashings.ts b/packages/lodestar/src/api/rest/beacon/pool/submitPoolProposerSlashings.ts deleted file mode 100644 index 3a526d937a..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/submitPoolProposerSlashings.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {ValidationError} from "../../../impl/errors"; -import {ApiController} from "../../types"; - -export const submitPoolProposerSlashings: ApiController = { - url: "/eth/v1/beacon/pool/proposer_slashings", - method: "POST", - id: "submitPoolProposerSlashings", - - handler: async function (req) { - let slashing: phase0.ProposerSlashing; - try { - slashing = this.config.types.phase0.ProposerSlashing.fromJson(req.body, {case: "snake"}); - } catch (e) { - throw new ValidationError(`SSZ deserialize error: ${(e as Error).message}`); - } - await this.api.beacon.pool.submitProposerSlashing(slashing); - return {}; - }, - - schema: { - body: { - type: "object", - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/submitPoolSyncCommitteeSignatures.ts b/packages/lodestar/src/api/rest/beacon/pool/submitPoolSyncCommitteeSignatures.ts deleted file mode 100644 index 2d1693c64c..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/submitPoolSyncCommitteeSignatures.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Json} from "@chainsafe/ssz"; -import {ApiController} from "../../types"; - -export const submitPoolSyncCommitteeSignatures: ApiController = { - url: "/eth/v1/beacon/pool/sync_committees", - method: "POST", - id: "submitPoolSyncCommitteeSignatures", - - handler: async function (req) { - const signatures = req.body.map((item) => - this.config.types.altair.SyncCommitteeSignature.fromJson(item, {case: "snake"}) - ); - - await this.api.beacon.pool.submitSyncCommitteeSignatures(signatures); - return {}; - }, - - schema: { - body: { - type: "array", - minItems: 1, - items: { - type: "object", - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/pool/submitPoolVoluntaryExit.ts b/packages/lodestar/src/api/rest/beacon/pool/submitPoolVoluntaryExit.ts deleted file mode 100644 index 29419fd172..0000000000 --- a/packages/lodestar/src/api/rest/beacon/pool/submitPoolVoluntaryExit.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {ValidationError} from "../../../impl/errors"; -import {ApiController} from "../../types"; - -export const submitPoolVoluntaryExit: ApiController = { - url: "/eth/v1/beacon/pool/voluntary_exits", - method: "POST", - id: "submitPoolVoluntaryExit", - - handler: async function (req) { - let exit: phase0.SignedVoluntaryExit; - try { - exit = this.config.types.phase0.SignedVoluntaryExit.fromJson(req.body, {case: "snake"}); - } catch (e) { - throw new ValidationError(`SSZ deserialize error: ${(e as Error).message}`); - } - await this.api.beacon.pool.submitVoluntaryExit(exit); - return {}; - }, - - schema: { - body: { - type: "object", - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getEpochCommittees.ts b/packages/lodestar/src/api/rest/beacon/state/getEpochCommittees.ts deleted file mode 100644 index 1041a388b3..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getEpochCommittees.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {StateId} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; - -type Query = { - slot?: number; - epoch?: number; - index?: number; -}; - -export const getEpochCommittees: ApiController = { - url: "/eth/v1/beacon/states/:stateId/committees", - method: "GET", - id: "getEpochCommittees", - - handler: async function (req) { - const committees = await this.api.beacon.state.getStateCommittees(req.params.stateId, {...req.query}); - return { - data: committees.map((c) => this.config.types.phase0.BeaconCommitteeResponse.toJson(c, {case: "snake"})), - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - querystring: { - type: "object", - required: [], - properties: { - epoch: { - type: "number", - minimum: 0, - }, - slot: { - type: "number", - minimum: 0, - }, - index: { - type: "number", - miminimum: 0, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getEpochSyncCommittees.ts b/packages/lodestar/src/api/rest/beacon/state/getEpochSyncCommittees.ts deleted file mode 100644 index 9ed09c1835..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getEpochSyncCommittees.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {StateId} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; - -type Query = { - epoch?: number; -}; - -/** Retrieves the sync committees for the given state. */ -export const getEpochSyncCommittees: ApiController = { - url: "/eth/v1/beacon/states/:stateId/sync_committees", - method: "GET", - id: "getEpochSyncCommittees", - - handler: async function (req) { - const data = await this.api.beacon.state.getEpochSyncCommittees(req.params.stateId, req.query.epoch); - return { - data: this.config.types.altair.SyncCommitteeByValidatorIndices.toJson(data, {case: "snake"}), - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - querystring: { - type: "object", - required: [], - properties: { - epoch: { - type: "number", - minimum: 0, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getStateFinalityCheckpoints.ts b/packages/lodestar/src/api/rest/beacon/state/getStateFinalityCheckpoints.ts deleted file mode 100644 index 0101cd58ed..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getStateFinalityCheckpoints.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {StateId} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const getStateFinalityCheckpoints: ApiController = { - url: "/eth/v1/beacon/states/:stateId/finality_checkpoints", - method: "GET", - id: "getStateFinalityCheckpoints", - - handler: async function (req) { - const state = await this.api.beacon.state.getState(req.params.stateId); - const checkpointType = this.config.types.phase0.Checkpoint; - return { - data: { - previous_justified: checkpointType.toJson(state.previousJustifiedCheckpoint, {case: "snake"}), - current_justified: checkpointType.toJson(state.currentJustifiedCheckpoint, {case: "snake"}), - finalized: checkpointType.toJson(state.finalizedCheckpoint, {case: "snake"}), - }, - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getStateFork.ts b/packages/lodestar/src/api/rest/beacon/state/getStateFork.ts deleted file mode 100644 index a1d6355bfc..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getStateFork.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {StateId} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; - -export const getStateFork: ApiController = { - url: "/eth/v1/beacon/states/:stateId/fork", - method: "GET", - id: "getStateFork", - - handler: async function (req) { - const fork = await this.api.beacon.state.getFork(req.params.stateId); - return { - data: this.config.types.phase0.Fork.toJson(fork, {case: "snake"}), - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getStateRoot.ts b/packages/lodestar/src/api/rest/beacon/state/getStateRoot.ts deleted file mode 100644 index f605fcda1e..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getStateRoot.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {StateId} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; - -export const getStateRoot: ApiController = { - url: "/eth/v1/beacon/states/:stateId/root", - method: "GET", - id: "getStateRoot", - - handler: async function (req) { - const root = await this.api.beacon.state.getStateRoot(req.params.stateId); - return { - data: root, - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getStateValidator.ts b/packages/lodestar/src/api/rest/beacon/state/getStateValidator.ts deleted file mode 100644 index 68939f9b1c..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getStateValidator.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {StateId} from "../../../impl/beacon/state/interface"; -import {ApiController} from "../../types"; - -export const getStateValidator: ApiController = { - url: "/eth/v1/beacon/states/:stateId/validators/:validatorId", - method: "GET", - id: "getStateValidator", - - handler: async function (req) { - let validator: phase0.ValidatorResponse; - if (req.params.validatorId.toLowerCase().startsWith("0x")) { - validator = await this.api.beacon.state.getStateValidator( - req.params.stateId, - this.config.types.BLSPubkey.fromJson(req.params.validatorId) - ); - } else { - validator = await this.api.beacon.state.getStateValidator( - req.params.stateId, - this.config.types.ValidatorIndex.fromJson(req.params.validatorId) - ); - } - - return { - data: this.config.types.phase0.ValidatorResponse.toJson(validator, {case: "snake"}), - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId", "validatorId"], - properties: { - stateId: { - types: "string", - }, - validatorId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getStateValidatorBalances.ts b/packages/lodestar/src/api/rest/beacon/state/getStateValidatorBalances.ts deleted file mode 100644 index d7a7ea1c3a..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getStateValidatorBalances.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {ValidatorIndex, BLSPubkey} from "@chainsafe/lodestar-types"; -import {StateId} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; -import {mapValidatorIndices} from "../../utils"; - -type ValidatorBalancesQuery = { - id?: string[]; -}; - -export const getStateValidatorsBalances: ApiController = { - url: "/eth/v1/beacon/states/:stateId/validator_balances", - method: "GET", - id: "getStateValidatorBalances", - - handler: async function (req) { - let indices: (ValidatorIndex | BLSPubkey)[] | undefined; - if (req.query.id) { - indices = mapValidatorIndices(this.config, req.query.id); - } - const balances = await this.api.beacon.state.getStateValidatorBalances(req.params.stateId, indices); - return { - data: balances.map((b) => this.config.types.phase0.ValidatorBalance.toJson(b, {case: "snake"})), - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - querystring: { - type: "object", - required: [], - properties: { - id: { - types: "array", - uniqueItems: true, - maxItems: 30, - items: { - type: "string", - }, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/getStateValidators.ts b/packages/lodestar/src/api/rest/beacon/state/getStateValidators.ts deleted file mode 100644 index e469d5ac36..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/getStateValidators.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {IValidatorFilters, StateId, ValidatorStatus} from "../../../impl/beacon/state"; -import {ApiController} from "../../types"; -import {mapValidatorIndices} from "../../utils"; - -type ValidatorsQuery = { - indices?: string[]; - statuses?: string[]; -}; - -export const getStateValidators: ApiController = { - url: "/eth/v1/beacon/states/:stateId/validators", - method: "GET", - id: "getStateValidators", - - handler: async function (req) { - const filters: IValidatorFilters = {}; - if (req.query.indices) { - filters.indices = mapValidatorIndices(this.config, req.query.indices); - } - if (req.query.statuses) { - filters.statuses = req.query.statuses as ValidatorStatus[]; - } - const validators = await this.api.beacon.state.getStateValidators(req.params.stateId, filters); - return { - data: validators.map((v) => v && this.config.types.phase0.ValidatorResponse.toJson(v, {case: "snake"})), - }; - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - querystring: { - type: "object", - required: [], - properties: { - indices: { - type: "array", - uniqueItems: true, - items: { - type: "string", - }, - }, - statuses: { - type: "array", - uniqueItems: true, - items: { - type: "string", - }, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/beacon/state/index.ts b/packages/lodestar/src/api/rest/beacon/state/index.ts deleted file mode 100644 index 2af434ac01..0000000000 --- a/packages/lodestar/src/api/rest/beacon/state/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {getEpochCommittees} from "./getEpochCommittees"; -import {getStateFinalityCheckpoints} from "./getStateFinalityCheckpoints"; -import {getStateFork} from "./getStateFork"; -import {getStateRoot} from "./getStateRoot"; -import {getStateValidator} from "./getStateValidator"; -import {getStateValidators} from "./getStateValidators"; -import {getStateValidatorsBalances} from "./getStateValidatorBalances"; - -export const beaconStateRoutes = [ - getEpochCommittees, - getStateFinalityCheckpoints, - getStateFork, - getStateRoot, - getStateValidator, - getStateValidators, - getStateValidatorsBalances, -]; diff --git a/packages/lodestar/src/api/rest/config/getDepositContract.ts b/packages/lodestar/src/api/rest/config/getDepositContract.ts deleted file mode 100644 index 06761c8938..0000000000 --- a/packages/lodestar/src/api/rest/config/getDepositContract.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ApiController} from "../types"; - -export const getDepositContract: ApiController = { - url: "/eth/v1/config/deposit_contract", - method: "GET", - id: "getDepositContract", - - handler: async function () { - const depositContract = await this.api.config.getDepositContract(); - return { - data: this.config.types.phase0.Contract.toJson(depositContract, {case: "snake"}), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/config/getForkSchedule.ts b/packages/lodestar/src/api/rest/config/getForkSchedule.ts deleted file mode 100644 index 190ea706bb..0000000000 --- a/packages/lodestar/src/api/rest/config/getForkSchedule.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ApiController} from "../types"; - -export const getForkSchedule: ApiController = { - url: "/eth/v1/config/fork_schedule", - method: "GET", - id: "getForkSchedule", - - handler: async function () { - const forkSchedule = await this.api.config.getForkSchedule(); - return { - data: forkSchedule.map((fork) => this.config.types.phase0.Fork.toJson(fork, {case: "snake"})), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/config/getSpec.ts b/packages/lodestar/src/api/rest/config/getSpec.ts deleted file mode 100644 index 619b58df94..0000000000 --- a/packages/lodestar/src/api/rest/config/getSpec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {BeaconParams} from "@chainsafe/lodestar-params"; -import {ApiController} from "../types"; - -export const getSpec: ApiController = { - url: "/eth/v1/config/spec", - method: "GET", - id: "getSpec", - - handler: async function () { - const spec = await this.api.config.getSpec(); - return { - data: BeaconParams.toJson(spec), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/config/index.ts b/packages/lodestar/src/api/rest/config/index.ts deleted file mode 100644 index f61a6cd8ad..0000000000 --- a/packages/lodestar/src/api/rest/config/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {getForkSchedule} from "./getForkSchedule"; -import {getDepositContract} from "./getDepositContract"; -import {getSpec} from "./getSpec"; - -export const configRoutes = [getForkSchedule, getDepositContract, getSpec]; diff --git a/packages/lodestar/src/api/rest/debug/connectToPeer.ts b/packages/lodestar/src/api/rest/debug/connectToPeer.ts deleted file mode 100644 index 2dfd60b332..0000000000 --- a/packages/lodestar/src/api/rest/debug/connectToPeer.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Multiaddr from "multiaddr"; -import {createFromB58String} from "peer-id"; -import {ApiController} from "../types"; - -export const connectToPeer: ApiController = { - url: "/eth/v1/debug/connect/:peerId", - method: "POST", - id: "connectToPeer", - - handler: async function (req) { - const multiaddrStr = req.body || []; - const multiaddr = multiaddrStr.map((addr) => new Multiaddr(addr)); - const peer = createFromB58String(req.params.peerId); - await this.api.debug.connectToPeer(peer, multiaddr); - return {}; - }, - - schema: { - params: { - type: "object", - required: ["peerId"], - properties: { - peerId: { - types: "string", - }, - }, - }, - - body: { - type: "array", - items: { - type: "string", - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/debug/disconnectPeer.ts b/packages/lodestar/src/api/rest/debug/disconnectPeer.ts deleted file mode 100644 index 29ac2e503e..0000000000 --- a/packages/lodestar/src/api/rest/debug/disconnectPeer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {createFromB58String} from "peer-id"; -import {ApiController} from "../types"; - -export const disconnectPeer: ApiController = { - url: "/eth/v1/debug/disconnect/:peerId", - method: "POST", - id: "disconnectPeer", - - handler: async function (req) { - const peer = createFromB58String(req.params.peerId); - await this.api.debug.disconnectPeer(peer); - return {}; - }, - - schema: { - params: { - type: "object", - required: ["peerId"], - properties: { - peerId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/debug/getDebugChainHeads.ts b/packages/lodestar/src/api/rest/debug/getDebugChainHeads.ts deleted file mode 100644 index 4efd5e90a7..0000000000 --- a/packages/lodestar/src/api/rest/debug/getDebugChainHeads.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ApiController} from "../types"; - -export const getDebugChainHeads: ApiController = { - url: "/eth/v1/debug/beacon/heads", - method: "GET", - id: "getDebugChainHeads", - - handler: async function () { - const heads = await this.api.debug.beacon.getHeads(); - return { - data: heads.map((head) => this.config.types.phase0.SlotRoot.toJson(head, {case: "snake"})), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/debug/getStates.ts b/packages/lodestar/src/api/rest/debug/getStates.ts deleted file mode 100644 index 1f40cf7416..0000000000 --- a/packages/lodestar/src/api/rest/debug/getStates.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {ApiController, HttpHeader} from "../types"; - -const SSZ_MIME_TYPE = "application/octet-stream"; - -// V2 handler is backwards compatible so re-use it for both versions -const handler: ApiController["handler"] = async function (req, resp) { - const state = await this.api.debug.beacon.getState(req.params.stateId); - const type = this.config.getForkTypes(state.slot).BeaconState; - if (req.headers[HttpHeader.ACCEPT] === SSZ_MIME_TYPE) { - const stateSsz = type.serialize(state); - return resp.status(200).header(HttpHeader.CONTENT_TYPE, SSZ_MIME_TYPE).send(Buffer.from(stateSsz)); - } else { - // Send 200 JSON - return { - version: this.config.getForkName(state.slot), - data: type.toJson(state, {case: "snake"}), - }; - } -}; - -const schema = { - params: { - type: "object", - required: ["stateId"], - properties: { - blockId: { - types: "string", - }, - }, - }, -}; - -export const getState: ApiController = { - url: "/eth/v1/debug/beacon/states/:stateId", - method: "GET", - id: "getState", - handler, - schema, -}; - -export const getStateV2: ApiController = { - url: "/eth/v2/debug/beacon/states/:stateId", - method: "GET", - id: "getStateV2", - handler, - schema, -}; diff --git a/packages/lodestar/src/api/rest/debug/index.ts b/packages/lodestar/src/api/rest/debug/index.ts deleted file mode 100644 index 6f2a72be8c..0000000000 --- a/packages/lodestar/src/api/rest/debug/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {connectToPeer} from "./connectToPeer"; -import {disconnectPeer} from "./disconnectPeer"; -import {getDebugChainHeads} from "./getDebugChainHeads"; -import {getState, getStateV2} from "./getStates"; - -export const debugRoutes = [connectToPeer, disconnectPeer, getDebugChainHeads, getState, getStateV2]; diff --git a/packages/lodestar/src/api/rest/errorHandler.ts b/packages/lodestar/src/api/rest/errorHandler.ts deleted file mode 100644 index c553ad739c..0000000000 --- a/packages/lodestar/src/api/rest/errorHandler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {FastifyError, FastifyReply, FastifyRequest} from "fastify"; -import {ServerResponse} from "http"; -import {ErrorAborted} from "@chainsafe/lodestar-utils"; -import {ApiError} from "../impl/errors"; - -export function errorHandler(e: Error, req: FastifyRequest, resp: FastifyReply): void { - if ((e as FastifyError).validation) { - req.log.warn(`Request ${req.id} failed validation. Reason: ${e.message}`); - resp.status(400).send((e as FastifyError).validation); - return; - } - - // Don't log ErrorAborted errors, they happen on node shutdown and are not usefull - if (!(e instanceof ErrorAborted)) { - const config = (resp.context.config || {}) as {url: string}; - req.log.error(`Request ${req.id} ${config.url} failed with unexpected error: `, e.stack || e.message); - } - - const statusCode = e instanceof ApiError ? (e as ApiError).statusCode : 500; - resp.status(statusCode).send(e); -} diff --git a/packages/lodestar/src/api/rest/events/getEventStream.ts b/packages/lodestar/src/api/rest/events/getEventStream.ts deleted file mode 100644 index 3d7ffe6994..0000000000 --- a/packages/lodestar/src/api/rest/events/getEventStream.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {EventMessage} from "fastify"; -import {BasicType, CompositeType} from "@chainsafe/ssz"; -import {BeaconEvent, BeaconEventType} from "../../impl/events"; -import {ApiController} from "../types"; - -type Query = { - topics?: BeaconEventType[]; -}; - -export const getEventStream: ApiController = { - url: "/eth/v1/events", - method: "GET", - id: "eventstream", - - handler: async function (req, resp) { - resp.sent = true; - const source = this.api.events.getEventStream(req.query.topics ?? Object.values(BeaconEventType)); - for (const event of ["end", "error", "close"]) { - req.req.once(event, () => { - source.stop(); - }); - } - - const config = this.config; - async function* transform(source: AsyncIterable): AsyncIterable { - for await (const event of source) { - switch (event.type) { - case BeaconEventType.HEAD: - yield serializeEvent(config.types.phase0.ChainHead, event); - break; - case BeaconEventType.BLOCK: - yield serializeEvent(config.types.phase0.BlockEventPayload, event); - break; - case BeaconEventType.ATTESTATION: - yield serializeEvent(config.types.phase0.Attestation, event); - break; - case BeaconEventType.FINALIZED_CHECKPOINT: - yield serializeEvent(config.types.phase0.FinalizedCheckpoint, event); - break; - case BeaconEventType.CHAIN_REORG: - yield serializeEvent(config.types.phase0.ChainReorg, event); - break; - default: - req.log.warn("Missing serializer for event " + event.type); - } - } - } - resp - .type("text/event-stream") - .header("Cache-Control", "no-cache") - .header("Connection", "keep-alive") - .sse(transform(source)); - }, - - schema: { - querystring: { - type: "object", - properties: { - topics: { - type: "array", - items: { - type: "string", - enum: Object.values(BeaconEventType), - }, - }, - }, - }, - }, -}; - -function serializeEvent( - type: BasicType | CompositeType, - event: T -): EventMessage { - return { - event: event.type, - data: JSON.stringify(type.toJson(event.message)), - }; -} diff --git a/packages/lodestar/src/api/rest/events/index.ts b/packages/lodestar/src/api/rest/events/index.ts deleted file mode 100644 index f912de22b2..0000000000 --- a/packages/lodestar/src/api/rest/events/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {getEventStream} from "./getEventStream"; - -export const eventsRoutes = [getEventStream]; diff --git a/packages/lodestar/src/api/rest/index.ts b/packages/lodestar/src/api/rest/index.ts index 868a3d93b2..264664a009 100644 --- a/packages/lodestar/src/api/rest/index.ts +++ b/packages/lodestar/src/api/rest/index.ts @@ -1,84 +1,136 @@ -import fastify, {FastifyInstance, ServerOptions} from "fastify"; +import fastify, {FastifyError, FastifyInstance} from "fastify"; import fastifyCors from "fastify-cors"; -import {FastifySSEPlugin} from "fastify-sse-v2"; -import {IncomingMessage, Server, ServerResponse} from "http"; import querystring from "querystring"; -import {IRestApiModules} from "./interface"; -import {FastifyLogger} from "./logger"; -import {defaultApiRestOptions, IRestApiOptions} from "./options"; -import {registerRoutes} from "./routes"; -import {errorHandler} from "./errorHandler"; -import {RouteConfig} from "./types"; +import {IncomingMessage} from "http"; +import {Api} from "@chainsafe/lodestar-api"; +import {registerRoutes, RouteConfig} from "@chainsafe/lodestar-api/server"; +import {ErrorAborted, ILogger} from "@chainsafe/lodestar-utils"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {IMetrics} from "../../metrics"; +import {ApiError} from "../impl/errors"; + +export type RestApiOptions = { + enabled: boolean; + api: (keyof Api)[]; + host: string; + cors: string; + port: number; +}; + +export const restApiOptionsDefault: RestApiOptions = { + enabled: false, + // ApiNamespace "debug" is not turned on by default + api: ["beacon", "config", "events", "node", "validator"], + host: "127.0.0.1", + port: 9596, + cors: "*", +}; + +export interface IRestApiModules { + config: IBeaconConfig; + logger: ILogger; + api: Api; + metrics: IMetrics | null; +} /** * REST API powered by `fastify` server. */ export class RestApi { - server: FastifyInstance; + private readonly opts: RestApiOptions; + private readonly server: FastifyInstance; + private readonly logger: ILogger; + private readonly activeRequests = new Set(); - constructor(server: FastifyInstance) { - this.server = server; - } + constructor(optsArg: Partial, modules: IRestApiModules) { + // Apply opts defaults + const opts = {...restApiOptionsDefault, ...optsArg}; - /** - * Initialize and start the REST API server. - */ - static async init(opts: Partial, modules: IRestApiModules): Promise { - const _opts = {...defaultApiRestOptions, ...opts}; - const api = new RestApi(setupServer(_opts, modules)); - const logger = modules.logger; - if (_opts.enabled) { - try { - const address = await api.server.listen(_opts.port, _opts.host); - logger.info("Started rest api server", {address, namespaces: _opts.api}); - } catch (e) { - logger.error("Failed to start rest api server", {host: _opts.host, port: _opts.port}, e); - throw e; + const server = fastify({ + logger: false, + ajv: {customOptions: {coerceTypes: "array"}}, + querystringParser: querystring.parse, + }); + + // Instantiate and register the routes with matching namespace in `opts.api` + registerRoutes(server, modules.config, modules.api, opts.api); + + // To parse our ApiError -> statusCode + server.setErrorHandler((err, req, res) => { + if ((err as FastifyError).validation) { + void res.status(400).send((err as FastifyError).validation); + } else { + // Convert our custom ApiError into status code + const statusCode = err instanceof ApiError ? err.statusCode : 500; + void res.status(statusCode).send(err); } + }); + + if (opts.cors) { + void server.register(fastifyCors, {origin: opts.cors}); } - return api; + + // Log all incoming request to debug (before parsing). TODO: Should we hook latter in the lifecycle? https://www.fastify.io/docs/latest/Lifecycle/ + // Note: Must be an async method so fastify can continue the release lifecycle. Otherwise we must call done() or the request stalls + server.addHook("onRequest", async (req) => { + this.activeRequests.add(req.raw); + const url = req.raw.url ? req.raw.url.split("?")[0] : "-"; + this.logger.debug(`Req ${req.id} ${req.ip} ${req.raw.method}:${url}`); + }); + + // Log after response + server.addHook("onResponse", async (req, res) => { + this.activeRequests.delete(req.raw); + const {operationId} = res.context.config as RouteConfig; + this.logger.debug(`Res ${req.id} ${operationId} - ${res.raw.statusCode}`); + + if (modules.metrics) { + modules.metrics?.apiRestResponseTime.observe({operationId}, res.getResponseTime() / 1000); + } + }); + + server.addHook("onError", async (req, res, err) => { + this.activeRequests.delete(req.raw); + // Don't log ErrorAborted errors, they happen on node shutdown and are not usefull + if (err instanceof ErrorAborted) return; + + const {operationId} = res.context.config as RouteConfig; + this.logger.error(`Req ${req.id} ${operationId} error`, {}, err); + }); + + this.opts = opts; + this.server = server; + this.logger = modules.logger; } /** - * Close the server instance. + * Start the REST API server. + */ + async listen(): Promise { + // TODO: Consider if necessary. The consumer could just not call this function + if (!this.opts.enabled) return; + + try { + const address = await this.server.listen(this.opts.port, this.opts.host); + this.logger.info("Started REST api server", {address, namespaces: this.opts.api}); + } catch (e) { + this.logger.error("Error starting REST api server", {host: this.opts.host, port: this.opts.port}, e); + throw e; + } + } + + /** + * Close the server instance and terminate all existing connections. */ async close(): Promise { + // In NodeJS land calling close() only causes new connections to be rejected. + // Existing connections can prevent .close() from resolving for potentially forever. + // In Lodestar case when the BeaconNode wants to close we will just abruptly terminate + // all existing connections for a fast shutdown. + // Inspired by https://github.com/gajus/http-terminator/ + for (const req of this.activeRequests) { + req.destroy(Error("Closing")); + } await this.server.close(); } } - -function setupServer(opts: IRestApiOptions, modules: IRestApiModules): FastifyInstance { - const server = fastify({ - logger: new FastifyLogger(modules.logger), - ajv: { - customOptions: { - coerceTypes: "array", - }, - }, - querystringParser: querystring.parse as ServerOptions["querystringParser"], - }); - server.setErrorHandler(errorHandler); - if (opts.cors) { - server.register(fastifyCors as fastify.Plugin>, { - origin: opts.cors, - }); - } - server.register(FastifySSEPlugin); - const api = modules.api; - server.decorate("config", modules.config); - server.decorate("api", api); - // new api - const enabledApiNamespaces = opts.api; - server.register(async function (instance) { - registerRoutes(instance, enabledApiNamespaces); - }); - - if (modules.metrics) { - server.addHook("onResponse", async (request, reply) => { - const config = reply.context.config as RouteConfig; - modules.metrics?.apiRestResponseTime.observe({operationId: config.operationId}, reply.getResponseTime() / 1000); - }); - } - - return server; -} diff --git a/packages/lodestar/src/api/rest/interface.ts b/packages/lodestar/src/api/rest/interface.ts deleted file mode 100644 index 89d21eebb4..0000000000 --- a/packages/lodestar/src/api/rest/interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {FastifyInstance, Plugin} from "fastify"; -import {IncomingMessage, Server, ServerResponse} from "http"; - -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {ILogger} from "@chainsafe/lodestar-utils"; - -import {IApi, IBeaconApi, IValidatorApi} from "../impl"; -import {IMetrics} from "../../metrics"; - -export interface ILodestarApiOpts { - // path prefix - prefix: string; - api: { - beacon: IBeaconApi; - validator: IValidatorApi; - }; - config: IBeaconConfig; -} -export type LodestarApiPlugin = Plugin; -export type LodestarRestApiEndpoint = (server: FastifyInstance, opts: ILodestarApiOpts) => void; - -export interface IRestApiModules { - config: IBeaconConfig; - logger: ILogger; - api: IApi; - metrics: IMetrics | null; -} diff --git a/packages/lodestar/src/api/rest/lightclient/index.ts b/packages/lodestar/src/api/rest/lightclient/index.ts deleted file mode 100644 index 613e7b3ed2..0000000000 --- a/packages/lodestar/src/api/rest/lightclient/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {serializeProof} from "@chainsafe/persistent-merkle-tree"; -import {ApiController, HttpHeader} from "../types"; -import {ApiError} from "../../impl/errors"; - -export const createProof: ApiController = { - url: "/eth/v1/lightclient/proof/:stateId", - method: "POST", - id: "createProof", - - handler: async function (req, resp) { - const proof = await this.api.lightclient.createStateProof(req.params.stateId, req.body.paths); - const serialized = serializeProof(proof); - return resp.status(200).header(HttpHeader.CONTENT_TYPE, "application/octet-stream").send(Buffer.from(serialized)); - }, - - schema: { - params: { - type: "object", - required: ["stateId"], - properties: { - stateId: { - types: "string", - }, - }, - }, - body: { - type: "object", - required: ["paths"], - properties: { - paths: { - type: "array", - }, - }, - }, - }, -}; - -export const getBestUpdates: ApiController = { - url: "/eth/v1/lightclient/best_updates/:periods", - method: "GET", - id: "getBestUpdates", - - handler: async function (req) { - const {from, to} = parsePeriods(req.params.periods); - const items = await this.api.lightclient.getBestUpdates(from, to); - return { - data: items.map((item) => this.config.types.altair.LightClientUpdate.toJson(item, {case: "snake"})), - }; - }, - - schema: { - params: { - type: "object", - required: ["periods"], - properties: { - stateId: { - types: "string", - }, - }, - }, - }, -}; - -export const getLatestUpdateFinalized: ApiController = { - url: "/eth/v1/lightclient/latest_update_finalized/", - method: "GET", - id: "getLatestUpdateFinalized", - - handler: async function () { - const data = await this.api.lightclient.getLatestUpdateFinalized(); - if (!data) throw new ApiError(404, "No update available"); - return { - data: this.config.types.altair.LightClientUpdate.toJson(data, {case: "snake"}), - }; - }, -}; - -export const getLatestUpdateNonFinalized: ApiController = { - url: "/eth/v1/lightclient/latest_update_nonfinalized/", - method: "GET", - id: "getLatestUpdateNonFinalized", - - handler: async function () { - const data = await this.api.lightclient.getLatestUpdateNonFinalized(); - if (!data) throw new ApiError(404, "No update available"); - return { - data: this.config.types.altair.LightClientUpdate.toJson(data, {case: "snake"}), - }; - }, -}; - -/** - * periods = 1 or = 1..4 - */ -function parsePeriods(periods: string): {from: number; to: number} { - if (periods.includes("..")) { - const [from, to] = periods.split(".."); - return {from: parseInt(from, 10), to: parseInt(to, 10)}; - } else { - const period = parseInt(periods, 10); - return {from: period, to: period}; - } -} diff --git a/packages/lodestar/src/api/rest/lodestar/index.ts b/packages/lodestar/src/api/rest/lodestar/index.ts deleted file mode 100644 index 82a31aadfc..0000000000 --- a/packages/lodestar/src/api/rest/lodestar/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {ApiController} from "../types"; - -export const getWtfNode: ApiController = { - url: "/eth/v1/lodestar/wtfnode/", - method: "GET", - id: "getWtfNode", - - handler: async function () { - return this.api.lodestar.getWtfNode(); - }, -}; - -export const getLatestWeakSubjectivityCheckpointEpoch: ApiController = { - url: "/eth/v1/lodestar/ws_epoch/", - method: "GET", - id: "getLatestWeakSubjectivityCheckpointEpoch", - - handler: async function () { - return this.api.lodestar.getLatestWeakSubjectivityCheckpointEpoch(); - }, -}; - -export const getSyncChainsDebugState: ApiController<{paths: (string | number)[][]}, {stateId: string}> = { - url: "/eth/v1/lodestar/sync-chains-debug-state", - method: "GET", - id: "getSyncChainsDebugState", - - handler: async function () { - return this.api.lodestar.getSyncChainsDebugState(); - }, -}; - -export const lodestarRoutes = [getWtfNode, getLatestWeakSubjectivityCheckpointEpoch, getSyncChainsDebugState]; diff --git a/packages/lodestar/src/api/rest/logger.ts b/packages/lodestar/src/api/rest/logger.ts deleted file mode 100644 index b1008fdff5..0000000000 --- a/packages/lodestar/src/api/rest/logger.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import {Stream} from "stream"; -import {IncomingMessage} from "http"; -import {FastifyRequest} from "fastify"; -import {ILogger} from "@chainsafe/lodestar-utils"; - -/** - * Logs REST API request/response messages. - */ -export class FastifyLogger { - readonly stream: Stream; - - readonly serializers = { - req: (req: IncomingMessage & FastifyRequest): {msg: string} => { - const url = req.url ? req.url.split("?")[0] : "-"; - return {msg: `Req ${req.id} ${req.ip} ${req.method}:${url}`}; - }, - }; - - private log: ILogger; - - constructor(logger: ILogger) { - this.log = logger; - this.stream = ({ - write: this.handle, - } as unknown) as Stream; - } - - private handle = (chunk: string): void => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const log = JSON.parse(chunk); - if (log.req) { - this.log.debug(log.req.msg); - } else if (log.res) { - this.log.debug(`Res ${log.reqId} - ${log.res.statusCode} ${log.responseTime}`); - } else { - if (log.level === 50) { - this.log.error(log.msg); - } else { - this.log.warn(log.msg); - } - } - }; -} diff --git a/packages/lodestar/src/api/rest/node/getHealth.ts b/packages/lodestar/src/api/rest/node/getHealth.ts deleted file mode 100644 index ed1f7a4afa..0000000000 --- a/packages/lodestar/src/api/rest/node/getHealth.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {ApiController} from "../types"; - -export const getHealth: ApiController = { - url: "/eth/v1/node/health", - method: "GET", - id: "getHealth", - - handler: async function (req, resp) { - const status = await this.api.node.getNodeStatus(); - switch (status) { - case "ready": - return resp.status(200).send(); - case "syncing": - return resp.status(206).send(); - default: - return resp.status(503).send(); - } - }, -}; diff --git a/packages/lodestar/src/api/rest/node/getNetworkIdentity.ts b/packages/lodestar/src/api/rest/node/getNetworkIdentity.ts deleted file mode 100644 index 81dc80e81a..0000000000 --- a/packages/lodestar/src/api/rest/node/getNetworkIdentity.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const getNetworkIdentity: ApiController = { - url: "/eth/v1/node/identity", - method: "GET", - id: "getNetworkIdentity", - - handler: async function () { - const identity = await this.api.node.getNodeIdentity(); - const metadataJson = this.config.types.phase0.Metadata.toJson(identity.metadata, {case: "snake"}); - return { - data: { - peer_id: identity.peerId, - enr: identity.enr, - p2p_addresses: identity.p2pAddresses, - discovery_addresses: identity.discoveryAddresses, - metadata: metadataJson, - }, - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/node/getNodeVersion.ts b/packages/lodestar/src/api/rest/node/getNodeVersion.ts deleted file mode 100644 index ce0fba27d8..0000000000 --- a/packages/lodestar/src/api/rest/node/getNodeVersion.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {ApiController} from "../types"; - -export const getNodeVersion: ApiController = { - url: "/eth/v1/node/version", - method: "GET", - id: "getNodeVersion", - - handler: async function () { - return { - data: { - version: await this.api.node.getVersion(), - }, - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/node/getPeer.ts b/packages/lodestar/src/api/rest/node/getPeer.ts deleted file mode 100644 index 51cb3d3078..0000000000 --- a/packages/lodestar/src/api/rest/node/getPeer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const getPeer: ApiController = { - url: "/eth/v1/node/peers/:peerId", - method: "GET", - id: "getPeer", - - handler: async function (req) { - const peer = await this.api.node.getPeer(req.params.peerId); - return { - data: { - peer_id: peer.peerId, - enr: peer.enr, - last_seen_p2p_address: peer.lastSeenP2pAddress, - state: peer.state, - direction: peer.direction, - }, - }; - }, - - schema: { - params: { - type: "object", - required: ["peerId"], - properties: { - peerId: { - types: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/node/getPeers.ts b/packages/lodestar/src/api/rest/node/getPeers.ts deleted file mode 100644 index 5dbaf57e54..0000000000 --- a/packages/lodestar/src/api/rest/node/getPeers.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const getPeers: ApiController<{state: string[] | string; direction: string[] | string}> = { - url: "/eth/v1/node/peers", - method: "GET", - id: "getPeers", - - handler: async function (req) { - const peers = await this.api.node.getPeers( - typeof req.query.state === "string" ? [req.query.state] : req.query.state, - typeof req.query.direction === "string" ? [req.query.direction] : req.query.direction - ); - return { - data: peers.map((peer) => ({ - peer_id: peer.peerId, - enr: peer.enr, - last_seen_p2p_address: peer.lastSeenP2pAddress, - state: peer.state, - direction: peer.direction, - })), - }; - }, - - schema: { - querystring: { - type: "object", - required: [], - properties: { - state: { - types: "array", - uniqueItems: true, - maxItems: 4, - items: { - type: "string", - enum: ["disconnected", "connecting", "connected", "disconnecting"], - }, - }, - direction: { - types: "array", - uniqueItems: true, - maxItems: 2, - items: { - type: "string", - enum: ["inbound", "outbound"], - }, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/node/getSyncingStatus.ts b/packages/lodestar/src/api/rest/node/getSyncingStatus.ts deleted file mode 100644 index a962239fc1..0000000000 --- a/packages/lodestar/src/api/rest/node/getSyncingStatus.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ApiController} from "../types"; - -export const getSyncingStatus: ApiController = { - url: "/eth/v1/node/syncing", - method: "GET", - id: "getSyncingStatus", - - handler: async function () { - const status = await this.api.node.getSyncingStatus(); - return { - data: this.config.types.phase0.SyncingStatus.toJson(status, {case: "snake"}), - }; - }, -}; diff --git a/packages/lodestar/src/api/rest/node/index.ts b/packages/lodestar/src/api/rest/node/index.ts deleted file mode 100644 index 8ea8d8a5ad..0000000000 --- a/packages/lodestar/src/api/rest/node/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {getHealth} from "./getHealth"; -import {getNetworkIdentity} from "./getNetworkIdentity"; -import {getPeers} from "./getPeers"; -import {getPeer} from "./getPeer"; -import {getNodeVersion} from "./getNodeVersion"; -import {getSyncingStatus} from "./getSyncingStatus"; - -export const nodeRoutes = [getHealth, getNetworkIdentity, getPeers, getPeer, getNodeVersion, getSyncingStatus]; diff --git a/packages/lodestar/src/api/rest/options.ts b/packages/lodestar/src/api/rest/options.ts deleted file mode 100644 index 74b42ae141..0000000000 --- a/packages/lodestar/src/api/rest/options.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {ApiNamespace} from "../impl"; - -export interface IRestApiOptions { - enabled: boolean; - api: ApiNamespace[]; - host: string; - cors: string; - port: number; -} - -export const defaultApiRestOptions: IRestApiOptions = { - enabled: false, - // ApiNamespace.DEBUG is not turned on by default - api: [ApiNamespace.BEACON, ApiNamespace.CONFIG, ApiNamespace.NODE, ApiNamespace.VALIDATOR, ApiNamespace.EVENTS], - host: "127.0.0.1", - port: 9596, - cors: "*", -}; diff --git a/packages/lodestar/src/api/rest/routes.ts b/packages/lodestar/src/api/rest/routes.ts deleted file mode 100644 index 904cebd33d..0000000000 --- a/packages/lodestar/src/api/rest/routes.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {FastifyInstance} from "fastify"; -import {ApiNamespace} from "../impl"; -import {beaconRoutes} from "./beacon"; -import {configRoutes} from "./config"; -import {debugRoutes} from "./debug"; -import {eventsRoutes} from "./events"; -import {lodestarRoutes} from "./lodestar"; -import {nodeRoutes} from "./node"; -import {ApiController, RouteConfig} from "./types"; -import {validatorRoutes} from "./validator"; - -const routesGroups = [ - {namespace: ApiNamespace.BEACON, routes: beaconRoutes}, - {namespace: ApiNamespace.CONFIG, routes: configRoutes}, - {namespace: ApiNamespace.DEBUG, routes: debugRoutes}, - {namespace: ApiNamespace.EVENTS, routes: eventsRoutes}, - {namespace: ApiNamespace.LODESTAR, routes: lodestarRoutes}, - {namespace: ApiNamespace.NODE, routes: nodeRoutes}, - {namespace: ApiNamespace.VALIDATOR, routes: validatorRoutes}, -]; - -export function registerRoutes(fastify: FastifyInstance, enabledNamespaces: ApiNamespace[]): void { - for (const {namespace, routes} of routesGroups) { - if (enabledNamespaces.includes(namespace)) { - registerRoutesToServer(fastify, routes); - } - } -} - -function registerRoutesToServer( - fastify: FastifyInstance, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - routes: ApiController[] -): void { - for (const route of routes) { - fastify.route({ - url: route.url, - method: route.method, - handler: route.handler, - schema: route.schema, - config: {operationId: route.id} as RouteConfig, - }); - } -} diff --git a/packages/lodestar/src/api/rest/types.ts b/packages/lodestar/src/api/rest/types.ts deleted file mode 100644 index baaa530329..0000000000 --- a/packages/lodestar/src/api/rest/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - DefaultBody, - DefaultHeaders, - DefaultParams, - DefaultQuery, - HTTPMethod, - RequestHandler, - RouteShorthandOptions, -} from "fastify"; -import {IncomingMessage, Server, ServerResponse} from "http"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export interface ApiController< - Query = DefaultQuery, - Params = DefaultParams, - Body = DefaultBody, - Headers = DefaultHeaders -> { - url: string; - method: HTTPMethod; - handler: RequestHandler; - schema?: RouteShorthandOptions["schema"]; - /** OperationId as defined in https://github.com/ethereum/eth2.0-APIs/blob/18cb6ff152b33a5f34c377f00611821942955c82/apis/beacon/blocks/attestations.yaml#L2 */ - id: string; -} - -export enum HttpHeader { - ACCEPT = "accept", - CONTENT_TYPE = "Content-Type", -} - -export type RouteConfig = { - operationId: ApiController["id"]; -}; diff --git a/packages/lodestar/src/api/rest/utils.ts b/packages/lodestar/src/api/rest/utils.ts deleted file mode 100644 index 4789934fbd..0000000000 --- a/packages/lodestar/src/api/rest/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {ValidatorIndex, BLSPubkey} from "@chainsafe/lodestar-types"; -import {FastifyError} from "fastify"; - -/** - * The error handler will decide status code 400 - */ -export function toRestValidationError(field: string, message: string): FastifyError { - return { - message, - validation: [ - { - dataPath: field, - message, - }, - ], - } as FastifyError; -} - -export function mapValidatorIndices(config: IBeaconConfig, data: string[]): (ValidatorIndex | BLSPubkey)[] { - return data.map((id) => { - if (id.toLowerCase().startsWith("0x")) { - return config.types.BLSPubkey.fromJson(id); - } else { - return config.types.ValidatorIndex.fromJson(id); - } - }); -} diff --git a/packages/lodestar/src/api/rest/validator/duties/getAttesterDuties.ts b/packages/lodestar/src/api/rest/validator/duties/getAttesterDuties.ts deleted file mode 100644 index 3fc14d65c9..0000000000 --- a/packages/lodestar/src/api/rest/validator/duties/getAttesterDuties.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {ValidatorIndex} from "@chainsafe/lodestar-types"; -import {ApiController} from "../../types"; - -export const getAttesterDuties: ApiController = { - url: "/eth/v1/validator/duties/attester/:epoch", - method: "POST", - id: "getAttesterDuties", - - handler: async function (req) { - const value = await this.api.validator.getAttesterDuties(req.params.epoch, req.body); - return this.config.types.phase0.AttesterDutiesApi.toJson(value, {case: "snake"}); - }, - - schema: { - params: { - type: "object", - required: ["epoch"], - properties: { - epoch: { - type: "number", - minimum: 0, - }, - }, - }, - body: { - type: "array", - minItems: 1, - items: { - type: "number", - minimum: 0, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/duties/getProposerDuties.ts b/packages/lodestar/src/api/rest/validator/duties/getProposerDuties.ts deleted file mode 100644 index 7a83271c43..0000000000 --- a/packages/lodestar/src/api/rest/validator/duties/getProposerDuties.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {ApiController} from "../../types"; - -export const getProposerDuties: ApiController = { - url: "/eth/v1/validator/duties/proposer/:epoch", - method: "GET", - id: "getProposerDuties", - - handler: async function (req) { - const value = await this.api.validator.getProposerDuties(req.params.epoch); - return this.config.types.phase0.ProposerDutiesApi.toJson(value, {case: "snake"}); - }, - - schema: { - params: { - type: "object", - required: ["epoch"], - properties: { - epoch: { - type: "number", - minimum: 0, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/duties/getSyncCommitteeDuties.ts b/packages/lodestar/src/api/rest/validator/duties/getSyncCommitteeDuties.ts deleted file mode 100644 index 85d059408e..0000000000 --- a/packages/lodestar/src/api/rest/validator/duties/getSyncCommitteeDuties.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {ValidatorIndex} from "@chainsafe/lodestar-types"; -import {ApiController} from "../../types"; - -export const getSyncCommitteeDuties: ApiController = { - url: "/eth/v1/validator/duties/sync/:epoch", - method: "POST", - id: "getSyncCommitteeDuties", - - handler: async function (req) { - const data = await this.api.validator.getSyncCommitteeDuties(req.params.epoch, req.body); - return this.config.types.altair.SyncDutiesApi.toJson(data, {case: "snake"}); - }, - - schema: { - params: { - type: "object", - required: ["epoch"], - properties: { - epoch: { - type: "number", - minimum: 0, - }, - }, - }, - body: { - type: "array", - minItems: 1, - items: { - type: "number", - minimum: 0, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/getAggregatedAttestation.ts b/packages/lodestar/src/api/rest/validator/getAggregatedAttestation.ts deleted file mode 100644 index 21ee3592fa..0000000000 --- a/packages/lodestar/src/api/rest/validator/getAggregatedAttestation.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {fromHex} from "@chainsafe/lodestar-utils"; -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -type Query = { - attestation_data_root: string; - slot: number; -}; - -export const getAggregatedAttestation: ApiController = { - url: "/eth/v1/validator/aggregate_attestation", - method: "GET", - id: "getAggregatedAttestation", - - handler: async function (req) { - const aggregate = await this.api.validator.getAggregatedAttestation( - fromHex(req.query.attestation_data_root), - req.query.slot - ); - return { - data: this.config.types.phase0.Attestation.toJson(aggregate, {case: "snake"}), - }; - }, - - schema: { - querystring: { - type: "object", - required: ["attestation_data_root", "slot"], - properties: { - attestation_data_root: { - type: "string", - }, - slot: { - type: "number", - minimum: 0, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/index.ts b/packages/lodestar/src/api/rest/validator/index.ts deleted file mode 100644 index a80f7ac2c5..0000000000 --- a/packages/lodestar/src/api/rest/validator/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {getAttesterDuties} from "./duties/getAttesterDuties"; -import {getProposerDuties} from "./duties/getProposerDuties"; -import {getSyncCommitteeDuties} from "./duties/getSyncCommitteeDuties"; -import {getAggregatedAttestation} from "./getAggregatedAttestation"; -import {prepareBeaconCommitteeSubnet} from "./prepareBeaconCommitteeSubnet"; -import {prepareSyncCommitteeSubnets} from "./prepareSyncCommitteeSubnets"; -import {produceAttestationData} from "./produceAttestationData"; -import {produceBlock, produceBlockV2} from "./produceBlock"; -import {produceSyncCommitteeContribution} from "./produceSyncCommitteeContribution"; -import {publishAggregateAndProof} from "./publishAggregateAndProof"; -import {publishContributionAndProofs} from "./publishContributionAndProofs"; - -export const validatorRoutes = [ - getAttesterDuties, - getProposerDuties, - getSyncCommitteeDuties, - getAggregatedAttestation, - prepareBeaconCommitteeSubnet, - prepareSyncCommitteeSubnets, - produceAttestationData, - produceBlock, - produceBlockV2, - produceSyncCommitteeContribution, - publishAggregateAndProof, - publishContributionAndProofs, -]; diff --git a/packages/lodestar/src/api/rest/validator/prepareBeaconCommitteeSubnet.ts b/packages/lodestar/src/api/rest/validator/prepareBeaconCommitteeSubnet.ts deleted file mode 100644 index 0c9117ded8..0000000000 --- a/packages/lodestar/src/api/rest/validator/prepareBeaconCommitteeSubnet.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {Json} from "@chainsafe/ssz"; -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const prepareBeaconCommitteeSubnet: ApiController = { - url: "/eth/v1/validator/beacon_committee_subscriptions", - method: "POST", - id: "prepareBeaconCommitteeSubnet", - - handler: async function (req) { - await this.api.validator.prepareBeaconCommitteeSubnet( - req.body.map((item) => this.config.types.phase0.BeaconCommitteeSubscription.fromJson(item, {case: "snake"})) - ); - return {}; - }, - - schema: { - body: { - type: "array", - minItems: 1, - items: { - type: "object", - required: ["validator_index", "committee_index", "committees_at_slot", "slot", "is_aggregator"], - properties: { - validator_index: { - type: "number", - minimum: 0, - }, - committee_index: { - type: "number", - minimum: 0, - }, - committees_at_slot: { - type: "number", - minimum: 0, - }, - slot: { - type: "number", - minimum: 0, - }, - is_aggregator: { - type: "boolean", - }, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/prepareSyncCommitteeSubnets.ts b/packages/lodestar/src/api/rest/validator/prepareSyncCommitteeSubnets.ts deleted file mode 100644 index dc055be095..0000000000 --- a/packages/lodestar/src/api/rest/validator/prepareSyncCommitteeSubnets.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {Json} from "@chainsafe/ssz"; -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export const prepareSyncCommitteeSubnets: ApiController = { - url: "/eth/v1/validator/sync_committee_subscriptions", - method: "POST", - id: "prepareSyncCommitteeSubnets", - - handler: async function (req) { - await this.api.validator.prepareSyncCommitteeSubnets( - req.body.map((item) => this.config.types.altair.SyncCommitteeSubscription.fromJson(item, {case: "snake"})) - ); - return {}; - }, - - schema: { - body: { - type: "array", - minItems: 1, - items: { - type: "object", - required: ["validator_index", "sync_committee_indices", "until_epoch"], - properties: { - validator_index: { - type: "number", - minimum: 0, - }, - sync_committee_indices: { - type: "array", - minItems: 1, - items: { - type: "number", - minimum: 0, - }, - }, - until_epoch: { - type: "number", - minimum: 0, - }, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/produceAttestationData.ts b/packages/lodestar/src/api/rest/validator/produceAttestationData.ts deleted file mode 100644 index 9bf394b6ba..0000000000 --- a/packages/lodestar/src/api/rest/validator/produceAttestationData.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -type Query = { - slot: number; - committee_index: number; -}; - -export const produceAttestationData: ApiController = { - url: "/eth/v1/validator/attestation_data", - method: "GET", - id: "produceAttestationData", - - handler: async function (req) { - const attestationData = await this.api.validator.produceAttestationData(req.query.committee_index, req.query.slot); - return { - data: this.config.types.phase0.AttestationData.toJson(attestationData, {case: "snake"}), - }; - }, - - schema: { - querystring: { - type: "object", - required: ["committee_index", "slot"], - properties: { - slot: { - type: "number", - minimum: 0, - }, - committee_index: { - type: "number", - minimum: 0, - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/produceBlock.ts b/packages/lodestar/src/api/rest/validator/produceBlock.ts deleted file mode 100644 index 8cc57553ad..0000000000 --- a/packages/lodestar/src/api/rest/validator/produceBlock.ts +++ /dev/null @@ -1,68 +0,0 @@ -import {fromHex} from "@chainsafe/lodestar-utils"; -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -type Params = { - slot: number; -}; -type Query = { - randao_reveal: string; - grafitti: string; -}; - -// V2 handler is backwards compatible so re-use it for both versions -const handler: ApiController["handler"] = async function (req) { - const block = await this.api.validator.produceBlock( - req.params.slot, - fromHex(req.query.randao_reveal), - req.query.grafitti - ); - return { - version: this.config.getForkName(block.slot), - data: this.config.getForkTypes(block.slot).BeaconBlock.toJson(block, {case: "snake"}), - }; -}; - -const schema = { - params: { - type: "object", - required: ["slot"], - properties: { - slot: { - type: "number", - minimum: 1, - }, - }, - }, - querystring: { - type: "object", - required: ["randao_reveal"], - properties: { - randao_reveal: { - type: "string", - //TODO: add hex string signature regex - }, - graffiti: { - type: "string", - maxLength: 64, - }, - }, - }, -}; - -export const produceBlock: ApiController = { - url: "/eth/v1/validator/blocks/:slot", - method: "GET", - id: "produceBlock", - handler, - schema, -}; - -export const produceBlockV2: ApiController = { - url: "/eth/v2/validator/blocks/:slot", - method: "GET", - id: "produceBlockV2", - handler, - schema, -}; diff --git a/packages/lodestar/src/api/rest/validator/produceSyncCommitteeContribution.ts b/packages/lodestar/src/api/rest/validator/produceSyncCommitteeContribution.ts deleted file mode 100644 index 3e51c79883..0000000000 --- a/packages/lodestar/src/api/rest/validator/produceSyncCommitteeContribution.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {fromHexString} from "@chainsafe/ssz"; -import {ApiController} from "../types"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -type Query = { - slot: number; - subcommittee_index: number; - beacon_block_root: string; -}; - -export const produceSyncCommitteeContribution: ApiController = { - url: "/eth/v1/validator/sync_committee_contribution", - method: "GET", - id: "produceSyncCommitteeContribution", - - handler: async function (req) { - const data = await this.api.validator.produceSyncCommitteeContribution( - req.query.slot, - req.query.subcommittee_index, - fromHexString(req.query.beacon_block_root) - ); - return { - data: this.config.types.altair.SyncCommitteeContribution.toJson(data, {case: "snake"}), - }; - }, - - schema: { - querystring: { - type: "object", - required: ["slot", "subcommittee_index", "beacon_block_root"], - properties: { - slot: { - type: "number", - minimum: 0, - }, - subcommittee_index: { - type: "number", - minimum: 0, - }, - beacon_block_root: { - type: "string", - }, - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/publishAggregateAndProof.ts b/packages/lodestar/src/api/rest/validator/publishAggregateAndProof.ts deleted file mode 100644 index 9de2b74001..0000000000 --- a/packages/lodestar/src/api/rest/validator/publishAggregateAndProof.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Json} from "@chainsafe/ssz"; -import {ApiController} from "../types"; - -export const publishAggregateAndProof: ApiController = { - url: "/eth/v1/validator/aggregate_and_proofs", - method: "POST", - id: "publishAggregateAndProofs", - - handler: async function (req) { - const signedAggregateAndProofs = req.body.map((item) => - this.config.types.phase0.SignedAggregateAndProof.fromJson(item, {case: "snake"}) - ); - - await this.api.validator.publishAggregateAndProofs(signedAggregateAndProofs); - return {}; - }, - - schema: { - body: { - type: "array", - minItems: 1, - items: { - type: "object", - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/rest/validator/publishContributionAndProofs.ts b/packages/lodestar/src/api/rest/validator/publishContributionAndProofs.ts deleted file mode 100644 index 125a649dc2..0000000000 --- a/packages/lodestar/src/api/rest/validator/publishContributionAndProofs.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Json} from "@chainsafe/ssz"; -import {ApiController} from "../types"; - -export const publishContributionAndProofs: ApiController = { - url: "/eth/v1/validator/contribution_and_proofs", - method: "POST", - id: "publishContributionAndProofs", - - handler: async function (req) { - const items = req.body.map((item) => - this.config.types.altair.SignedContributionAndProof.fromJson(item, {case: "snake"}) - ); - - await this.api.validator.publishContributionAndProofs(items); - return {}; - }, - - schema: { - body: { - type: "array", - minItems: 1, - items: { - type: "object", - }, - }, - }, -}; diff --git a/packages/lodestar/src/api/types/index.ts b/packages/lodestar/src/api/types/index.ts deleted file mode 100644 index 7f6c2c4f96..0000000000 --- a/packages/lodestar/src/api/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./node"; diff --git a/packages/lodestar/src/api/types/node.ts b/packages/lodestar/src/api/types/node.ts deleted file mode 100644 index 9504c758cd..0000000000 --- a/packages/lodestar/src/api/types/node.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {PeerDirection, PeerState} from "../../network"; - -export type NodeIdentity = { - peerId: string; - enr: string; - p2pAddresses: string[]; - discoveryAddresses: string[]; - metadata: phase0.Metadata; -}; - -export type NodePeer = { - peerId: string; - enr: string; - lastSeenP2pAddress: string; - state: PeerState; - // the spec does not specify direction for a disconnected peer, lodestar uses null in that case - direction: PeerDirection | null; -}; diff --git a/packages/lodestar/src/chain/factory/duties/index.ts b/packages/lodestar/src/chain/factory/duties/index.ts index 930310e24c..8a2b6cdba1 100644 --- a/packages/lodestar/src/chain/factory/duties/index.ts +++ b/packages/lodestar/src/chain/factory/duties/index.ts @@ -1,14 +1,15 @@ +import {routes} from "@chainsafe/lodestar-api"; import {readonlyValues} from "@chainsafe/ssz"; import {allForks} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {BLSPubkey, Epoch, ValidatorIndex, phase0} from "@chainsafe/lodestar-types"; +import {BLSPubkey, Epoch, ValidatorIndex} from "@chainsafe/lodestar-types"; export function assembleAttesterDuty( config: IBeaconConfig, validator: {pubkey: BLSPubkey; index: ValidatorIndex}, epochCtx: allForks.EpochContext, epoch: Epoch -): phase0.AttesterDuty | null { +): routes.validator.AttesterDuty | null { const committeeAssignment = epochCtx.getCommitteeAssignment(epoch, validator.index); if (committeeAssignment) { let validatorCommitteeIndex = -1; diff --git a/packages/lodestar/src/network/interface.ts b/packages/lodestar/src/network/interface.ts index 7f0cc970cf..6dcd4a0799 100644 --- a/packages/lodestar/src/network/interface.ts +++ b/packages/lodestar/src/network/interface.ts @@ -55,4 +55,3 @@ export interface INetwork { export type PeerDirection = Connection["stat"]["direction"]; export type PeerStatus = Connection["stat"]["status"]; -export type PeerState = "disconnected" | "connecting" | "connected" | "disconnecting"; diff --git a/packages/lodestar/src/node/nodejs.ts b/packages/lodestar/src/node/nodejs.ts index cee50b7544..0aa048a02d 100644 --- a/packages/lodestar/src/node/nodejs.ts +++ b/packages/lodestar/src/node/nodejs.ts @@ -9,13 +9,14 @@ import {TreeBacked} from "@chainsafe/ssz"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {allForks} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; +import {Api} from "@chainsafe/lodestar-api"; import {IBeaconDb} from "../db"; import {INetwork, Network, ReqRespHandler} from "../network"; import {BeaconSync, IBeaconSync} from "../sync"; import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain"; import {createMetrics, IMetrics, HttpMetricsServer} from "../metrics"; -import {Api, IApi, RestApi} from "../api"; +import {getApi, RestApi} from "../api"; import {TasksService} from "../tasks"; import {IBeaconNodeOptions} from "./options"; import {Eth1ForBlockProduction, Eth1ForBlockProductionDisabled, Eth1Provider} from "../eth1"; @@ -30,7 +31,7 @@ export interface IBeaconNodeModules { metrics: IMetrics | null; network: INetwork; chain: IBeaconChain; - api: IApi; + api: Api; sync: IBeaconSync; chores: TasksService; metricsServer?: HttpMetricsServer; @@ -65,7 +66,7 @@ export class BeaconNode { metricsServer?: HttpMetricsServer; network: INetwork; chain: IBeaconChain; - api: IApi; + api: Api; restApi?: RestApi; sync: IBeaconSync; chores: TasksService; @@ -162,7 +163,7 @@ export class BeaconNode { logger: logger.child(opts.logger.chores), }); - const api = new Api(opts.api, { + const api = getApi({ config, logger: logger.child(opts.logger.api), db, @@ -189,12 +190,15 @@ export class BeaconNode { await metricsServer.start(); } - const restApi = await RestApi.init(opts.api.rest, { + const restApi = new RestApi(opts.api.rest, { config, logger: logger.child(opts.logger.api), api, metrics, }); + if (opts.api.rest.enabled) { + await restApi.listen(); + } await network.start(); chores.start(); diff --git a/packages/lodestar/src/sync/interface.ts b/packages/lodestar/src/sync/interface.ts index 3ad79a4758..5ea5ec0eb2 100644 --- a/packages/lodestar/src/sync/interface.ts +++ b/packages/lodestar/src/sync/interface.ts @@ -1,6 +1,7 @@ import {ILogger} from "@chainsafe/lodestar-utils"; -import {Slot, phase0} from "@chainsafe/lodestar-types"; +import {Slot} from "@chainsafe/lodestar-types"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; +import {routes} from "@chainsafe/lodestar-api"; import {INetwork} from "../network"; import {IBeaconChain} from "../chain"; import {IMetrics} from "../metrics"; @@ -8,10 +9,12 @@ import {IBeaconDb} from "../db"; import {SyncChainDebugState} from "./range/chain"; export {SyncChainDebugState}; +export type SyncingStatus = routes.node.SyncingStatus; + export interface IBeaconSync { state: SyncState; close(): void; - getSyncStatus(): phase0.SyncingStatus; + getSyncStatus(): SyncingStatus; isSynced(): boolean; isSyncing(): boolean; getSyncChainsDebugState(): SyncChainDebugState[]; diff --git a/packages/lodestar/src/sync/sync.ts b/packages/lodestar/src/sync/sync.ts index b7b43700ed..03d8236ad0 100644 --- a/packages/lodestar/src/sync/sync.ts +++ b/packages/lodestar/src/sync/sync.ts @@ -1,5 +1,5 @@ import PeerId from "peer-id"; -import {IBeaconSync, ISyncModules} from "./interface"; +import {IBeaconSync, ISyncModules, SyncingStatus} from "./interface"; import {INetwork, NetworkEvent} from "../network"; import {ILogger} from "@chainsafe/lodestar-utils"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; @@ -66,7 +66,7 @@ export class BeaconSync implements IBeaconSync { this.rangeSync.close(); } - getSyncStatus(): phase0.SyncingStatus { + getSyncStatus(): SyncingStatus { const currentSlot = this.chain.clock.currentSlot; const headSlot = this.chain.forkChoice.getHead().slot; switch (this.state) { @@ -74,13 +74,13 @@ export class BeaconSync implements IBeaconSync { case SyncState.SyncingHead: case SyncState.Stalled: return { - headSlot: BigInt(headSlot), - syncDistance: BigInt(currentSlot - headSlot), + headSlot: headSlot, + syncDistance: currentSlot - headSlot, }; case SyncState.Synced: return { - headSlot: BigInt(headSlot), - syncDistance: BigInt(0), + headSlot: headSlot, + syncDistance: 0, }; default: throw new Error("Node is stopped, cannot get sync status"); diff --git a/packages/lodestar/test/sim/singleNodeSingleThread.test.ts b/packages/lodestar/test/sim/singleNodeSingleThread.test.ts index f369558ee7..adbcc60ddc 100644 --- a/packages/lodestar/test/sim/singleNodeSingleThread.test.ts +++ b/packages/lodestar/test/sim/singleNodeSingleThread.test.ts @@ -4,7 +4,7 @@ import {getDevBeaconNode} from "../utils/node/beacon"; import {waitForEvent} from "../utils/events/resolver"; import {getAndInitDevValidators} from "../utils/node/validator"; import {ChainEvent} from "../../src/chain"; -import {IRestApiOptions} from "../../src/api/rest/options"; +import {RestApiOptions} from "../../src/api/rest"; import {testLogger, TestLoggerOpts, LogLevel} from "../utils/logger"; import {logFilesDir} from "./params"; import {simTestInfoTracker} from "../utils/node/simTest"; @@ -78,7 +78,7 @@ describe("Run single node single thread interop validators (no eth1) until check const bn = await getDevBeaconNode({ params: {...testParams, ALTAIR_FORK_EPOCH: altairForkEpoch}, - options: {api: {rest: {enabled: true} as IRestApiOptions}, sync: {isSingleNode: true}}, + options: {api: {rest: {enabled: true} as RestApiOptions}, sync: {isSingleNode: true}}, validatorCount: validatorClientCount * validatorsPerClient, logger: loggerNodeA, genesisTime, diff --git a/packages/lodestar/test/unit/api/impl/beacon/beacon.test.ts b/packages/lodestar/test/unit/api/impl/beacon/beacon.test.ts index 34dd3c54b4..15137f9e04 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/beacon.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/beacon.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import {BeaconApi} from "../../../../../src/api/impl/beacon"; +import {getBeaconApi} from "../../../../../src/api/impl/beacon"; import sinon from "sinon"; import {StubbedBeaconDb} from "../../../../utils/stub"; import {config} from "@chainsafe/lodestar-config/minimal"; @@ -7,31 +7,27 @@ import {expect} from "chai"; import {setupApiImplTestServer, ApiImplTestModules} from "../index.test"; describe("beacon api implementation", function () { - let api: BeaconApi; let dbStub: StubbedBeaconDb; let server: ApiImplTestModules; before(function () { server = setupApiImplTestServer(); dbStub = new StubbedBeaconDb(sinon); - api = new BeaconApi( - {}, - { - config, - chain: server.chainStub, - db: dbStub, - network: server.networkStub, - sync: server.syncStub, - } - ); }); describe("getGenesis", function () { it("success", async function () { + const api = getBeaconApi({ + config, + chain: server.chainStub, + db: dbStub, + network: server.networkStub, + }); + /** eslint-disable @typescript-eslint/no-unsafe-member-access */ (server.chainStub as any).genesisTime = 0; (server.chainStub as any).genesisValidatorsRoot = Buffer.alloc(32); - const genesis = await api.getGenesis(); + const {data: genesis} = await api.getGenesis(); if (!genesis) throw Error("Genesis is nullish"); expect(genesis.genesisForkVersion).to.not.be.undefined; expect(genesis.genesisTime).to.not.be.undefined; diff --git a/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlock.test.ts b/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlock.test.ts index 776e8488c8..b11777833b 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlock.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlock.test.ts @@ -29,7 +29,7 @@ describe("api - beacon - getBlock", function () { it("success for non finalized block", async function () { resolveBlockIdStub.withArgs(sinon.match.any, sinon.match.any, "head").resolves(generateEmptySignedBlock()); - const result = await server.blockApi.getBlock("head"); + const {data: result} = await server.blockApi.getBlock("head"); expect(result).to.not.be.null; expect(() => config.types.phase0.SignedBeaconBlock.assertValidValue(result)).to.not.throw(); }); diff --git a/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeader.test.ts b/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeader.test.ts index 5ee8a97ab4..fc4b89e865 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeader.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeader.test.ts @@ -1,6 +1,5 @@ import sinon from "sinon"; import * as blockUtils from "../../../../../../src/api/impl/beacon/blocks/utils"; -import {config} from "@chainsafe/lodestar-config/minimal"; import {expect, use} from "chai"; import chaiAsPromised from "chai-as-promised"; import {generateEmptySignedBlock} from "../../../../../utils/block"; @@ -31,6 +30,5 @@ describe("api - beacon - getBlockHeader", function () { resolveBlockIdStub.withArgs(sinon.match.any, sinon.match.any, "head").resolves(generateEmptySignedBlock()); const result = await server.blockApi.getBlockHeader("head"); expect(result).to.not.be.null; - expect(() => config.types.phase0.SignedBeaconHeaderResponse.assertValidValue(result)).to.not.throw(); }); }); diff --git a/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts b/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts index f794e28e79..d6e4bd70e5 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts @@ -10,9 +10,11 @@ import { import deepmerge from "deepmerge"; import {expect} from "chai"; import {setupApiImplTestServer, ApiImplTestModules} from "../../index.test"; +import {toHexString} from "@chainsafe/ssz"; describe("api - beacon - getBlockHeaders", function () { let server: ApiImplTestModules; + const parentRoot = toHexString(Buffer.alloc(32, 1)); beforeEach(function () { server = setupApiImplTestServer(); @@ -31,11 +33,9 @@ describe("api - beacon - getBlockHeaders", function () { ]); server.dbStub.block.get.resolves(deepmerge(generateEmptySignedBlock(), {message: {slot: 3}})); server.dbStub.blockArchive.get.resolves(null); - const blockHeaders = await server.blockApi.getBlockHeaders({}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({}); expect(blockHeaders).to.not.be.null; expect(blockHeaders.length).to.be.equal(2); - expect(() => config.types.phase0.SignedBeaconHeaderResponse.assertValidValue(blockHeaders[0])).to.not.throw; - expect(() => config.types.phase0.SignedBeaconHeaderResponse.assertValidValue(blockHeaders[1])).to.not.throw; expect(blockHeaders.filter((header) => header.canonical).length).to.be.equal(1); expect(server.forkChoiceStub.getHead.calledOnce).to.be.true; expect(server.chainStub.getCanonicalBlockAtSlot.calledOnce).to.be.true; @@ -45,7 +45,7 @@ describe("api - beacon - getBlockHeaders", function () { it("future slot", async function () { server.forkChoiceStub.getHead.returns(generateBlockSummary({slot: 1})); - const blockHeaders = await server.blockApi.getBlockHeaders({slot: 2}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({slot: 2}); expect(blockHeaders.length).to.be.equal(0); }); @@ -53,16 +53,15 @@ describe("api - beacon - getBlockHeaders", function () { server.forkChoiceStub.getHead.returns(generateBlockSummary({slot: 2})); server.chainStub.getCanonicalBlockAtSlot.withArgs(0).resolves(generateEmptySignedBlock()); server.forkChoiceStub.getBlockSummariesAtSlot.withArgs(0).returns([]); - const blockHeaders = await server.blockApi.getBlockHeaders({slot: 0}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({slot: 0}); expect(blockHeaders.length).to.be.equal(1); - expect(() => config.types.phase0.SignedBeaconHeaderResponse.assertValidValue(blockHeaders[0])).to.not.throw; expect(blockHeaders[0].canonical).to.be.true; }); it("skip slot", async function () { server.forkChoiceStub.getHead.returns(generateBlockSummary({slot: 2})); server.chainStub.getCanonicalBlockAtSlot.withArgs(0).resolves(null); - const blockHeaders = await server.blockApi.getBlockHeaders({slot: 0}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({slot: 0}); expect(blockHeaders.length).to.be.equal(0); }); @@ -79,7 +78,7 @@ describe("api - beacon - getBlockHeaders", function () { .returns(generateBlockSummary({blockRoot: config.types.phase0.BeaconBlock.hashTreeRoot(cannonical.message)})); server.dbStub.block.get.onFirstCall().resolves(generateSignedBlock({message: {slot: 1}})); server.dbStub.block.get.onSecondCall().resolves(generateSignedBlock({message: {slot: 2}})); - const blockHeaders = await server.blockApi.getBlockHeaders({parentRoot: Buffer.alloc(32, 1)}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({parentRoot}); expect(blockHeaders.length).to.equal(3); expect(blockHeaders.filter((b) => b.canonical).length).to.equal(2); }); @@ -89,14 +88,14 @@ describe("api - beacon - getBlockHeaders", function () { server.forkChoiceStub.getBlockSummariesByParentRoot.returns([generateBlockSummary({slot: 1})]); server.forkChoiceStub.getCanonicalBlockSummaryAtSlot.withArgs(1).returns(generateBlockSummary()); server.dbStub.block.get.resolves(generateSignedBlock({message: {slot: 1}})); - const blockHeaders = await server.blockApi.getBlockHeaders({parentRoot: Buffer.alloc(32, 1)}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({parentRoot}); expect(blockHeaders.length).to.equal(1); }); it("parent root - no non finalized blocks", async function () { server.dbStub.blockArchive.getByParentRoot.resolves(generateEmptySignedBlock()); server.forkChoiceStub.getBlockSummariesByParentRoot.returns([]); - const blockHeaders = await server.blockApi.getBlockHeaders({parentRoot: Buffer.alloc(32, 1)}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({parentRoot}); expect(blockHeaders.length).to.equal(1); }); @@ -113,7 +112,10 @@ describe("api - beacon - getBlockHeaders", function () { .returns(generateBlockSummary({blockRoot: config.types.phase0.BeaconBlock.hashTreeRoot(cannonical.message)})); server.dbStub.block.get.onFirstCall().resolves(generateSignedBlock({message: {slot: 1}})); server.dbStub.block.get.onSecondCall().resolves(generateSignedBlock({message: {slot: 2}})); - const blockHeaders = await server.blockApi.getBlockHeaders({parentRoot: Buffer.alloc(32, 1), slot: 1}); + const {data: blockHeaders} = await server.blockApi.getBlockHeaders({ + parentRoot: toHexString(Buffer.alloc(32, 1)), + slot: 1, + }); expect(blockHeaders.length).to.equal(1); }); }); diff --git a/packages/lodestar/test/unit/api/impl/beacon/blocks/publishBlock.test.ts b/packages/lodestar/test/unit/api/impl/beacon/blocks/publishBlock.test.ts index fe045f0960..3ed8dab06d 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/blocks/publishBlock.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/blocks/publishBlock.test.ts @@ -1,7 +1,7 @@ import {expect, use} from "chai"; import chaiAsPromised from "chai-as-promised"; import sinon, {SinonStubbedInstance} from "sinon"; -import {BeaconBlockApi} from "../../../../../../src/api/impl/beacon/blocks"; +import {getBeaconBlockApi} from "../../../../../../src/api/impl/beacon/blocks"; import {BeaconChain} from "../../../../../../src/chain"; import {Eth2Gossipsub} from "../../../../../../src/network/gossip"; import {generateEmptySignedBlock} from "../../../../../utils/block"; @@ -14,7 +14,6 @@ use(chaiAsPromised); describe("api - beacon - publishBlock", function () { let gossipStub: SinonStubbedInstance; let block: SignedBeaconBlock; - let blockApi: BeaconBlockApi; let chainStub: SinonStubbedInstance; let syncStub: SinonStubbedInstance; let server: ApiImplTestModules; @@ -30,19 +29,16 @@ describe("api - beacon - publishBlock", function () { server.networkStub.gossip = (gossipStub as unknown) as Eth2Gossipsub; chainStub = server.chainStub; syncStub = server.syncStub; - blockApi = new BeaconBlockApi( - {}, - { - chain: chainStub, - config: server.config, - db: server.dbStub, - network: server.networkStub, - sync: syncStub, - } - ); }); it("successful publish", async function () { + const blockApi = getBeaconBlockApi({ + chain: chainStub, + config: server.config, + db: server.dbStub, + network: server.networkStub, + }); + syncStub.isSynced.returns(true); await expect(blockApi.publishBlock(block)).to.be.fulfilled; expect(chainStub.receiveBlock.calledOnceWith(block)).to.be.true; diff --git a/packages/lodestar/test/unit/api/impl/beacon/pool/pool.test.ts b/packages/lodestar/test/unit/api/impl/beacon/pool/pool.test.ts index 93fac71f9c..7b41774fb6 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/pool/pool.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/pool/pool.test.ts @@ -1,7 +1,7 @@ import {config} from "@chainsafe/lodestar-config/minimal"; import {expect} from "chai"; import sinon from "sinon"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; +import {getBeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; import {Network} from "../../../../../../src/network/network"; import { generateAttestation, @@ -23,7 +23,7 @@ import {setupApiImplTestServer} from "../../index.test"; import {SinonStubFn} from "../../../../../utils/types"; describe("beacon pool api impl", function () { - let poolApi: BeaconPoolApi; + let poolApi: ReturnType; let dbStub: StubbedBeaconDb; let chainStub: SinonStubbedInstance; let networkStub: SinonStubbedInstance; @@ -42,16 +42,12 @@ describe("beacon pool api impl", function () { gossipStub.publishVoluntaryExit = sinon.stub(); networkStub = server.networkStub; networkStub.gossip = (gossipStub as unknown) as Eth2Gossipsub; - poolApi = new BeaconPoolApi( - {}, - { - config, - db: server.dbStub, - sync: server.syncStub, - network: networkStub, - chain: chainStub, - } - ); + poolApi = getBeaconPoolApi({ + config, + db: server.dbStub, + network: networkStub, + chain: chainStub, + }); validateGossipAttesterSlashing = sinon.stub(attesterSlashingValidation, "validateGossipAttesterSlashing"); validateGossipProposerSlashing = sinon.stub(proposerSlashingValidation, "validateGossipProposerSlashing"); validateVoluntaryExit = sinon.stub(voluntaryExitValidation, "validateGossipVoluntaryExit"); @@ -61,10 +57,10 @@ describe("beacon pool api impl", function () { sinon.restore(); }); - describe("getAttestations", function () { + describe("getPoolAttestations", function () { it("no filters", async function () { dbStub.attestation.values.resolves([generateAttestation(), generateAttestation()]); - const attestations = await poolApi.getAttestations(); + const {data: attestations} = await poolApi.getPoolAttestations(); expect(attestations.length).to.be.equal(2); }); @@ -74,12 +70,12 @@ describe("beacon pool api impl", function () { generateAttestation({data: generateAttestationData(0, 1, 1, 0)}), generateAttestation({data: generateAttestationData(0, 1, 3, 2)}), ]); - const attestations = await poolApi.getAttestations({slot: 1, committeeIndex: 0}); + const {data: attestations} = await poolApi.getPoolAttestations({slot: 1, committeeIndex: 0}); expect(attestations.length).to.be.equal(1); }); }); - describe("submitAttesterSlashing", function () { + describe("submitPoolAttesterSlashing", function () { const atterterSlashing: phase0.AttesterSlashing = { attestation1: { attestingIndices: [0] as List, @@ -95,20 +91,20 @@ describe("beacon pool api impl", function () { it("should broadcast and persist to db", async function () { validateGossipAttesterSlashing.resolves(); - await poolApi.submitAttesterSlashing(atterterSlashing); + await poolApi.submitPoolAttesterSlashing(atterterSlashing); expect(gossipStub.publishAttesterSlashing.calledOnceWithExactly(atterterSlashing)).to.be.true; expect(dbStub.attesterSlashing.add.calledOnceWithExactly(atterterSlashing)).to.be.true; }); it("should not broadcast or persist to db", async function () { validateGossipAttesterSlashing.throws(new Error("unit test error")); - await poolApi.submitAttesterSlashing(atterterSlashing).catch(() => ({})); + await poolApi.submitPoolAttesterSlashing(atterterSlashing).catch(() => ({})); expect(gossipStub.publishAttesterSlashing.calledOnce).to.be.false; expect(dbStub.attesterSlashing.add.calledOnce).to.be.false; }); }); - describe("submitProposerSlashing", function () { + describe("submitPoolProposerSlashing", function () { const proposerSlashing: phase0.ProposerSlashing = { signedHeader1: generateEmptySignedBlockHeader(), signedHeader2: generateEmptySignedBlockHeader(), @@ -116,32 +112,32 @@ describe("beacon pool api impl", function () { it("should broadcast and persist to db", async function () { validateGossipProposerSlashing.resolves(); - await poolApi.submitProposerSlashing(proposerSlashing); + await poolApi.submitPoolProposerSlashing(proposerSlashing); expect(gossipStub.publishProposerSlashing.calledOnceWithExactly(proposerSlashing)).to.be.true; expect(dbStub.proposerSlashing.add.calledOnceWithExactly(proposerSlashing)).to.be.true; }); it("should not broadcast or persist to db", async function () { validateGossipProposerSlashing.throws(new Error("unit test error")); - await poolApi.submitProposerSlashing(proposerSlashing).catch(() => ({})); + await poolApi.submitPoolProposerSlashing(proposerSlashing).catch(() => ({})); expect(gossipStub.publishProposerSlashing.calledOnce).to.be.false; expect(dbStub.proposerSlashing.add.calledOnce).to.be.false; }); }); - describe("submitVoluntaryExit", function () { + describe("submitPoolVoluntaryExit", function () { const voluntaryExit = generateEmptySignedVoluntaryExit(); it("should broadcast and persist to db", async function () { validateVoluntaryExit.resolves(); - await poolApi.submitVoluntaryExit(voluntaryExit); + await poolApi.submitPoolVoluntaryExit(voluntaryExit); expect(gossipStub.publishVoluntaryExit.calledOnceWithExactly(voluntaryExit)).to.be.true; expect(dbStub.voluntaryExit.add.calledOnceWithExactly(voluntaryExit)).to.be.true; }); it("should not broadcast or persist to db", async function () { validateVoluntaryExit.throws(new Error("unit test error")); - await poolApi.submitVoluntaryExit(voluntaryExit).catch(() => ({})); + await poolApi.submitPoolVoluntaryExit(voluntaryExit).catch(() => ({})); expect(gossipStub.publishVoluntaryExit.calledOnce).to.be.false; expect(dbStub.voluntaryExit.add.calledOnce).to.be.false; }); diff --git a/packages/lodestar/test/unit/api/impl/beacon/state/fork.test.ts b/packages/lodestar/test/unit/api/impl/beacon/state/fork.test.ts index 65dcdb980c..a956e93185 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/state/fork.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/state/fork.test.ts @@ -1,14 +1,13 @@ -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state/state"; +import {getBeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; import {config} from "@chainsafe/lodestar-config/minimal"; import sinon, {SinonStubbedMember} from "sinon"; -import {IBeaconStateApi} from "../../../../../../src/api/impl/beacon/state/interface"; import * as stateApiUtils from "../../../../../../src/api/impl/beacon/state/utils"; import {generateCachedState} from "../../../../../utils/state"; import {expect} from "chai"; import {setupApiImplTestServer, ApiImplTestModules} from "../../index.test"; describe("beacon api impl - state - get fork", function () { - let api: IBeaconStateApi; + let api: ReturnType; let resolveStateIdStub: SinonStubbedMember; let server: ApiImplTestModules; @@ -18,14 +17,11 @@ describe("beacon api impl - state - get fork", function () { beforeEach(function () { resolveStateIdStub = sinon.stub(stateApiUtils, "resolveStateId"); - api = new BeaconStateApi( - {}, - { - config, - chain: server.chainStub, - db: server.dbStub, - } - ); + api = getBeaconStateApi({ + config, + chain: server.chainStub, + db: server.dbStub, + }); }); afterEach(function () { @@ -34,7 +30,7 @@ describe("beacon api impl - state - get fork", function () { it("should get fork by state id", async function () { resolveStateIdStub.resolves(generateCachedState()); - const fork = await api.getFork("something"); + const {data: fork} = await api.getStateFork("something"); expect(fork).to.not.be.null; }); }); diff --git a/packages/lodestar/test/unit/api/impl/beacon/state/state.test.ts b/packages/lodestar/test/unit/api/impl/beacon/state/state.test.ts index 0f5bb689de..acc4d9a18d 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/state/state.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/state/state.test.ts @@ -1,8 +1,7 @@ -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state/state"; +import {getBeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; import {config} from "@chainsafe/lodestar-config/minimal"; import {StubbedBeaconDb} from "../../../../../utils/stub"; import sinon from "sinon"; -import {IBeaconStateApi} from "../../../../../../src/api/impl/beacon/state/interface"; import * as stateApiUtils from "../../../../../../src/api/impl/beacon/state/utils"; import {generateState} from "../../../../../utils/state"; import {expect} from "chai"; @@ -10,7 +9,7 @@ import {ApiImplTestModules, setupApiImplTestServer} from "../../index.test"; import {SinonStubFn} from "../../../../../utils/types"; describe("beacon api impl - states", function () { - let api: IBeaconStateApi; + let api: ReturnType; let resolveStateIdStub: SinonStubFn; let getEpochBeaconCommitteesStub: SinonStubFn; let server: ApiImplTestModules; @@ -22,14 +21,11 @@ describe("beacon api impl - states", function () { beforeEach(function () { resolveStateIdStub = sinon.stub(stateApiUtils, "resolveStateId"); getEpochBeaconCommitteesStub = sinon.stub(stateApiUtils, "getEpochBeaconCommittees"); - api = new BeaconStateApi( - {}, - { - config, - chain: server.chainStub, - db: new StubbedBeaconDb(sinon, config), - } - ); + api = getBeaconStateApi({ + config, + chain: server.chainStub, + db: new StubbedBeaconDb(sinon, config), + }); }); afterEach(function () { @@ -37,21 +33,13 @@ describe("beacon api impl - states", function () { getEpochBeaconCommitteesStub.restore(); }); - describe("getState", function () { - it("should get state by id", async function () { - resolveStateIdStub.resolves(generateState()); - const state = await api.getState("something"); - expect(state).to.not.be.null; - }); - }); - - describe("getStateCommittes", function () { + describe("getEpochCommittees", function () { const state = generateState(); it("no filters", async function () { resolveStateIdStub.resolves(state); getEpochBeaconCommitteesStub.returns([[[1, 4, 5]], [[2, 3, 6]]]); - const committees = await api.getStateCommittees("blem"); + const {data: committees} = await api.getEpochCommittees("blem"); expect(committees).to.have.length(2); }); it("slot and committee filter", async function () { @@ -63,7 +51,7 @@ describe("beacon api impl - states", function () { [8, 9, 10], ], ]); - const committees = await api.getStateCommittees("blem", {slot: 1, index: 1}); + const {data: committees} = await api.getEpochCommittees("blem", {slot: 1, index: 1}); expect(committees).to.have.length(1); expect(committees[0].index).to.be.equal(1); expect(committees[0].slot).to.be.equal(1); diff --git a/packages/lodestar/test/unit/api/impl/beacon/state/stateValidators.test.ts b/packages/lodestar/test/unit/api/impl/beacon/state/stateValidators.test.ts index bc2d755a91..8721f679f0 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/state/stateValidators.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/state/stateValidators.test.ts @@ -1,11 +1,11 @@ import {config} from "@chainsafe/lodestar-config/minimal"; import {Gwei} from "@chainsafe/lodestar-types"; -import {CachedBeaconState, phase0} from "@chainsafe/lodestar-beacon-state-transition"; -import {List} from "@chainsafe/ssz"; +import {CachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition"; +import {List, toHexString} from "@chainsafe/ssz"; import {expect, use} from "chai"; import chaiAsPromised from "chai-as-promised"; import sinon, {SinonStubbedInstance, SinonStubbedMember} from "sinon"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; +import {getBeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; import * as stateApiUtils from "../../../../../../src/api/impl/beacon/state/utils"; import {generateState} from "../../../../../utils/state"; import {generateValidator, generateValidators} from "../../../../../utils/validator"; @@ -16,6 +16,9 @@ import {allForks} from "@chainsafe/lodestar-beacon-state-transition"; use(chaiAsPromised); +const validatorId1 = toHexString(Buffer.alloc(48, 1)); +const validatorId2 = toHexString(Buffer.alloc(48, 2)); + describe("beacon api impl - state - validators", function () { let resolveStateIdStub: SinonStubbedMember; let toValidatorResponseStub: SinonStubbedMember; @@ -33,7 +36,7 @@ describe("beacon api impl - state - validators", function () { toValidatorResponseStub.returns({ index: 1, balance: BigInt(3200000), - status: phase0.ValidatorStatus.ACTIVE_ONGOING, + status: "active_ongoing", validator: generateValidator(), }); dbStub = server.dbStub; @@ -47,8 +50,8 @@ describe("beacon api impl - state - validators", function () { describe("get validators", function () { it("indices filter", async function () { resolveStateIdStub.resolves(generateState({validators: generateValidators(10)})); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - const validators = await api.getStateValidators("someState", {indices: [0, 1, 123]}); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + const {data: validators} = await api.getStateValidators("someState", {indices: [0, 1, 123]}); expect(validators.length).to.equal(2); }); @@ -58,7 +61,7 @@ describe("beacon api impl - state - validators", function () { toValidatorResponseStub.onFirstCall().returns({ index: 1, balance: BigInt(3200000), - status: phase0.ValidatorStatus.EXITED_SLASHED, + status: "exited_slashed", validator: generateValidator(), }); for (let i = 0; i < 10; i++) { @@ -68,9 +71,9 @@ describe("beacon api impl - state - validators", function () { } as unknown) as allForks.PubkeyIndexMap, } as CachedBeaconState); } - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - const validators = await api.getStateValidators("someState", { - statuses: [phase0.ValidatorStatus.PENDING_INITIALIZED], + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + const {data: validators} = await api.getStateValidators("someState", { + statuses: ["pending_initialized"], }); expect(validators.length).to.equal(9); }); @@ -87,18 +90,18 @@ describe("beacon api impl - state - validators", function () { } as unknown) as allForks.PubkeyIndexMap, } as CachedBeaconState); } - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - const stateValidators = await api.getStateValidators("someState", { + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + const {data: stateValidators} = await api.getStateValidators("someState", { indices: [0, 1, 2, 123], - statuses: [phase0.ValidatorStatus.PENDING_INITIALIZED], + statuses: ["pending_initialized"], }); expect(stateValidators.length).to.equal(3); }); it("success", async function () { resolveStateIdStub.resolves(generateState({validators: generateValidators(10)})); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - const validators = await api.getStateValidators("someState"); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + const {data: validators} = await api.getStateValidators("someState"); expect(validators.length).to.equal(10); }); }); @@ -106,12 +109,12 @@ describe("beacon api impl - state - validators", function () { describe("get validator", function () { it("validator by index not found", async function () { resolveStateIdStub.resolves(generateState({validators: generateValidators(10)})); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); await expect(api.getStateValidator("someState", 15)).to.be.rejectedWith("Validator not found"); }); it("validator by index found", async function () { resolveStateIdStub.resolves(generateState({validators: generateValidators(10)})); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); expect(await api.getStateValidator("someState", 1)).to.not.be.null; }); it("validator by root not found", async function () { @@ -121,8 +124,8 @@ describe("beacon api impl - state - validators", function () { get: () => undefined, } as unknown) as allForks.PubkeyIndexMap, } as CachedBeaconState); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - await expect(api.getStateValidator("someState", Buffer.alloc(32, 1))).to.be.rejectedWith("Validator not found"); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + await expect(api.getStateValidator("someState", validatorId1)).to.be.rejectedWith("Validator not found"); }); it("validator by root found", async function () { resolveStateIdStub.resolves(generateState({validators: generateValidators(10)})); @@ -131,8 +134,8 @@ describe("beacon api impl - state - validators", function () { get: () => 2, } as unknown) as allForks.PubkeyIndexMap, } as CachedBeaconState); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - expect(await api.getStateValidator("someState", Buffer.alloc(32, 1))).to.not.be.null; + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + expect(await api.getStateValidator("someState", validatorId1)).to.not.be.null; }); }); @@ -145,18 +148,13 @@ describe("beacon api impl - state - validators", function () { }) ); const pubkey2IndexStub = sinon.createStubInstance(allForks.PubkeyIndexMap); - pubkey2IndexStub.get.withArgs(Buffer.alloc(32, 1)).returns(3); - pubkey2IndexStub.get.withArgs(Buffer.alloc(32, 2)).returns(25); + pubkey2IndexStub.get.withArgs(validatorId1).returns(3); + pubkey2IndexStub.get.withArgs(validatorId2).returns(25); chainStub.getHeadState.returns({ pubkey2index: (pubkey2IndexStub as unknown) as allForks.PubkeyIndexMap, } as CachedBeaconState); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - const balances = await api.getStateValidatorBalances("somestate", [ - 1, - 24, - Buffer.alloc(32, 1), - Buffer.alloc(32, 2), - ]); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + const {data: balances} = await api.getStateValidatorBalances("somestate", [1, 24, validatorId1, validatorId2]); expect(balances.length).to.equal(2); expect(balances[0].index).to.equal(1); expect(balances[1].index).to.equal(3); @@ -169,8 +167,8 @@ describe("beacon api impl - state - validators", function () { balances: Array.from({length: 10}, () => BigInt(10)) as List, }) ); - const api = new BeaconStateApi({}, {config, db: dbStub, chain: chainStub}); - const balances = await api.getStateValidatorBalances("somestate"); + const api = getBeaconStateApi({config, db: dbStub, chain: chainStub}); + const {data: balances} = await api.getStateValidatorBalances("somestate"); expect(balances.length).to.equal(10); expect(balances[0].index).to.equal(0); expect(balances[0].balance.toString()).to.equal("10"); diff --git a/packages/lodestar/test/unit/api/impl/beacon/state/utils.test.ts b/packages/lodestar/test/unit/api/impl/beacon/state/utils.test.ts index 00860bfa8d..e77cca043a 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/state/utils.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/state/utils.test.ts @@ -131,7 +131,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 0; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.PENDING_INITIALIZED); + expect(status).to.be.equal("pending_initialized"); }); it("should return PENDING_QUEUED", function () { const validator = { @@ -140,7 +140,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 0; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.PENDING_QUEUED); + expect(status).to.be.equal("pending_queued"); }); it("should return ACTIVE_ONGOING", function () { const validator = { @@ -149,7 +149,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 1; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.ACTIVE_ONGOING); + expect(status).to.be.equal("active_ongoing"); }); it("should return ACTIVE_SLASHED", function () { const validator = { @@ -159,7 +159,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 1; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.ACTIVE_SLASHED); + expect(status).to.be.equal("active_slashed"); }); it("should return ACTIVE_EXITING", function () { const validator = { @@ -169,7 +169,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 1; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.ACTIVE_EXITING); + expect(status).to.be.equal("active_exiting"); }); it("should return EXITED_SLASHED", function () { const validator = { @@ -179,7 +179,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 2; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.EXITED_SLASHED); + expect(status).to.be.equal("exited_slashed"); }); it("should return EXITED_UNSLASHED", function () { const validator = { @@ -189,7 +189,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 2; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.EXITED_UNSLASHED); + expect(status).to.be.equal("exited_unslashed"); }); it("should return WITHDRAWAL_POSSIBLE", function () { const validator = { @@ -198,7 +198,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 1; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.WITHDRAWAL_POSSIBLE); + expect(status).to.be.equal("withdrawal_possible"); }); it("should return WITHDRAWAL_DONE", function () { const validator = { @@ -207,7 +207,7 @@ describe("beacon state api utils", function () { } as phase0.Validator; const currentEpoch = 1; const status = getValidatorStatus(validator, currentEpoch); - expect(status).to.be.equal(phase0.ValidatorStatus.WITHDRAWAL_DONE); + expect(status).to.be.equal("withdrawal_done"); }); it("should error", function () { const validator = {} as phase0.Validator; diff --git a/packages/lodestar/test/unit/api/impl/config/config.test.ts b/packages/lodestar/test/unit/api/impl/config/config.test.ts index a5f6d3739e..37e2f2a522 100644 --- a/packages/lodestar/test/unit/api/impl/config/config.test.ts +++ b/packages/lodestar/test/unit/api/impl/config/config.test.ts @@ -1,25 +1,25 @@ import {config} from "@chainsafe/lodestar-config/minimal"; import {expect} from "chai"; -import {ConfigApi} from "../../../../../src/api/impl/config"; +import {getConfigApi} from "../../../../../src/api/impl/config"; describe("config api implementation", function () { - let api: ConfigApi; + let api: ReturnType; beforeEach(function () { - api = new ConfigApi({}, {config}); + api = getConfigApi({config}); }); describe("getForkSchedule", function () { it("should get known scheduled forks", async function () { // @TODO: implement the actual fork schedule data get from config params once marin's altair PRs have been merged - const forkSchedule = await api.getForkSchedule(); + const {data: forkSchedule} = await api.getForkSchedule(); expect(forkSchedule.length).to.equal(0); }); }); describe("getDepositContract", function () { it("should get the deposit contract from config", async function () { - const depositContract = await api.getDepositContract(); + const {data: depositContract} = await api.getDepositContract(); expect(depositContract.address).to.equal(config.params.DEPOSIT_CONTRACT_ADDRESS); expect(depositContract.chainId).to.equal(config.params.DEPOSIT_CHAIN_ID); }); @@ -27,7 +27,7 @@ describe("config api implementation", function () { describe("getSpec", function () { it("should get the spec", async function () { - const spec = await api.getSpec(); + const {data: spec} = await api.getSpec(); expect(spec).to.equal(config.params); }); }); diff --git a/packages/lodestar/test/unit/api/impl/debug/beacon/index.test.ts b/packages/lodestar/test/unit/api/impl/debug/index.test.ts similarity index 58% rename from packages/lodestar/test/unit/api/impl/debug/beacon/index.test.ts rename to packages/lodestar/test/unit/api/impl/debug/index.test.ts index c1b67cf3b3..bc5089fe3d 100644 --- a/packages/lodestar/test/unit/api/impl/debug/beacon/index.test.ts +++ b/packages/lodestar/test/unit/api/impl/debug/index.test.ts @@ -4,20 +4,22 @@ import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; import {expect} from "chai"; import sinon from "sinon"; import {SinonStubbedInstance} from "sinon"; -import * as stateApiUtils from "../../../../../../src/api/impl/beacon/state/utils"; -import {DebugBeaconApi} from "../../../../../../src/api/impl/debug/beacon"; -import {IBeaconChain, LodestarForkChoice} from "../../../../../../src/chain"; -import {generateBlockSummary} from "../../../../../utils/block"; -import {StubbedBeaconDb} from "../../../../../utils/stub"; -import {generateState} from "../../../../../utils/state"; -import {setupApiImplTestServer} from "../../index.test"; -import {SinonStubFn} from "../../../../../utils/types"; +import * as stateApiUtils from "../../../../../src/api/impl/beacon/state/utils"; +import {getDebugApi} from "../../../../../src/api/impl/debug"; +import {INetwork, Network} from "../../../../../src/network"; +import {IBeaconChain, LodestarForkChoice} from "../../../../../src/chain"; +import {generateBlockSummary} from "../../../../utils/block"; +import {StubbedBeaconDb} from "../../../../utils/stub"; +import {generateState} from "../../../../utils/state"; +import {setupApiImplTestServer} from "../index.test"; +import {SinonStubFn} from "../../../../utils/types"; describe("api - debug - beacon", function () { - let debugApi: DebugBeaconApi; + let debugApi: ReturnType; let chainStub: SinonStubbedInstance; let forkchoiceStub: SinonStubbedInstance; let dbStub: StubbedBeaconDb; + let networkStub: SinonStubbedInstance; let resolveStateIdStub: SinonStubFn; beforeEach(function () { @@ -27,7 +29,8 @@ describe("api - debug - beacon", function () { forkchoiceStub = sinon.createStubInstance(LodestarForkChoice); chainStub.forkChoice = forkchoiceStub; dbStub = new StubbedBeaconDb(sinon); - debugApi = new DebugBeaconApi({}, {chain: chainStub, db: dbStub, config}); + networkStub = sinon.createStubInstance(Network); + debugApi = getDebugApi({chain: chainStub, db: dbStub, config, network: networkStub}); }); afterEach(function () { @@ -36,13 +39,13 @@ describe("api - debug - beacon", function () { it("getHeads - should return head", async function () { forkchoiceStub.getHeads.returns([generateBlockSummary({slot: 1000})]); - const heads = await debugApi.getHeads(); + const {data: heads} = await debugApi.getHeads(); expect(heads).to.be.deep.equal([{slot: 1000, root: ZERO_HASH}]); }); it("getState - should return state", async function () { resolveStateIdStub.resolves(generateState()); - const state = await debugApi.getState("something"); + const {data: state} = await debugApi.getState("something"); expect(state).to.not.be.null; }); }); diff --git a/packages/lodestar/test/unit/api/impl/events/events.test.ts b/packages/lodestar/test/unit/api/impl/events/events.test.ts index b7dcf051bb..34a013779c 100644 --- a/packages/lodestar/test/unit/api/impl/events/events.test.ts +++ b/packages/lodestar/test/unit/api/impl/events/events.test.ts @@ -1,18 +1,11 @@ -import { - BeaconAttestationEvent, - BeaconBlockEvent, - BeaconChainReorgEvent, - BeaconEventType, - BeaconHeadEvent, - EventsApi, - FinalizedCheckpointEvent, - VoluntaryExitEvent, -} from "../../../../../src/api/impl/events"; -import {config} from "@chainsafe/lodestar-config/minimal"; -import sinon, {SinonStubbedInstance} from "sinon"; -import {BeaconChain, ChainEvent, ChainEventEmitter, IBeaconChain} from "../../../../../src/chain"; -import {generateBlockSummary, generateEmptySignedBlock, generateSignedBlock} from "../../../../utils/block"; +import {AbortController} from "abort-controller"; import {expect} from "chai"; +import sinon, {SinonStubbedInstance} from "sinon"; +import {routes} from "@chainsafe/lodestar-api"; +import {config} from "@chainsafe/lodestar-config/minimal"; +import {BeaconChain, ChainEvent, ChainEventEmitter, IBeaconChain} from "../../../../../src/chain"; +import {getEventsApi} from "../../../../../src/api/impl/events"; +import {generateBlockSummary, generateEmptySignedBlock, generateSignedBlock} from "../../../../utils/block"; import {generateAttestation, generateEmptySignedVoluntaryExit} from "../../../../utils/attestation"; import {generateCachedState} from "../../../../utils/state"; @@ -20,96 +13,111 @@ describe("Events api impl", function () { describe("beacon event stream", function () { let chainStub: SinonStubbedInstance; let chainEventEmmitter: ChainEventEmitter; - let api: EventsApi; + let api: ReturnType; beforeEach(function () { chainStub = sinon.createStubInstance(BeaconChain); chainEventEmmitter = new ChainEventEmitter(); chainStub.emitter = chainEventEmmitter; - api = new EventsApi({}, {config, chain: chainStub}); + api = getEventsApi({config, chain: chainStub}); }); + let controller: AbortController; + beforeEach(() => (controller = new AbortController())); + afterEach(() => controller.abort()); + + function getEvents(topics: routes.events.EventType[]): routes.events.BeaconEvent[] { + const events: routes.events.BeaconEvent[] = []; + api.eventstream(topics, controller.signal, (event) => { + events.push(event); + }); + return events; + } + it("should ignore not sent topics", async function () { - const stream = api.getEventStream([BeaconEventType.HEAD]); + const events = getEvents([routes.events.EventType.head]); + const headSummary = generateBlockSummary(); chainEventEmmitter.emit(ChainEvent.forkChoiceReorg, headSummary, headSummary, 2); chainEventEmmitter.emit(ChainEvent.forkChoiceHead, headSummary); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as BeaconHeadEvent).type).to.equal(BeaconEventType.HEAD); - expect((event.value as BeaconHeadEvent).message).to.not.be.null; - stream.stop(); + + expect(events).to.have.length(1, "Wrong num of received events"); + expect(events[0].type).to.equal(routes.events.EventType.head); + expect(events[0].message).to.not.be.null; }); it("should process head event", async function () { - const stream = api.getEventStream([BeaconEventType.HEAD]); + const events = getEvents([routes.events.EventType.head]); + const headSummary = generateBlockSummary(); chainEventEmmitter.emit(ChainEvent.forkChoiceHead, headSummary); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as BeaconHeadEvent).type).to.equal(BeaconEventType.HEAD); - expect((event.value as BeaconHeadEvent).message).to.not.be.null; - stream.stop(); + + expect(events).to.have.length(1, "Wrong num of received events"); + expect(events[0].type).to.equal(routes.events.EventType.head); + expect(events[0].message).to.not.be.null; }); it("should process block event", async function () { - const stream = api.getEventStream([BeaconEventType.BLOCK]); + const events = getEvents([routes.events.EventType.block]); + const block = generateSignedBlock(); chainEventEmmitter.emit(ChainEvent.block, block, null as any, null as any); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as BeaconBlockEvent).type).to.equal(BeaconEventType.BLOCK); - expect((event.value as BeaconBlockEvent).message).to.not.be.null; - stream.stop(); + + expect(events).to.have.length(1, "Wrong num of received events"); + expect(events[0].type).to.equal(routes.events.EventType.block); + expect(events[0].message).to.not.be.null; }); it("should process attestation event", async function () { - const stream = api.getEventStream([BeaconEventType.ATTESTATION]); + const events = getEvents([routes.events.EventType.attestation]); + const attestation = generateAttestation(); chainEventEmmitter.emit(ChainEvent.attestation, attestation); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as BeaconAttestationEvent).type).to.equal(BeaconEventType.ATTESTATION); - expect((event.value as BeaconAttestationEvent).message).to.equal(attestation); - stream.stop(); + + expect(events).to.have.length(1, "Wrong num of received events"); + expect(events[0].type).to.equal(routes.events.EventType.attestation); + expect(events[0].message).to.equal(attestation); }); it("should process voluntary exit event", async function () { - const stream = api.getEventStream([BeaconEventType.VOLUNTARY_EXIT]); + const events = getEvents([routes.events.EventType.voluntaryExit]); + const exit = generateEmptySignedVoluntaryExit(); const block = generateEmptySignedBlock(); block.message.body.voluntaryExits.push(exit); chainEventEmmitter.emit(ChainEvent.block, block, null as any, null as any); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as VoluntaryExitEvent).type).to.equal(BeaconEventType.VOLUNTARY_EXIT); - expect((event.value as VoluntaryExitEvent).message).to.equal(exit); - stream.stop(); + + expect(events).to.have.length(1, "Wrong num of received events"); + expect(events[0].type).to.equal(routes.events.EventType.voluntaryExit); + expect(events[0].message).to.equal(exit); }); it("should process finalized checkpoint event", async function () { - const stream = api.getEventStream([BeaconEventType.FINALIZED_CHECKPOINT]); + const events = getEvents([routes.events.EventType.finalizedCheckpoint]); + const state = generateCachedState(); const checkpoint = state.finalizedCheckpoint; chainEventEmmitter.emit(ChainEvent.finalized, checkpoint, state); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as FinalizedCheckpointEvent).type).to.equal(BeaconEventType.FINALIZED_CHECKPOINT); - expect((event.value as FinalizedCheckpointEvent).message).to.not.be.null; - stream.stop(); + + expect(events).to.have.length(1, "Wrong num of received events"); + expect(events[0].type).to.equal(routes.events.EventType.finalizedCheckpoint); + expect(events[0].message).to.not.be.null; }); it("should process chain reorg event", async function () { - const stream = api.getEventStream([BeaconEventType.CHAIN_REORG]); + const events = getEvents([routes.events.EventType.chainReorg]); + + const depth = 3; const oldHead = generateBlockSummary({slot: 4}); const newHead = generateBlockSummary({slot: 3}); - chainEventEmmitter.emit(ChainEvent.forkChoiceReorg, oldHead, newHead, 3); - const event = await stream[Symbol.asyncIterator]().next(); - expect(event?.value).to.not.be.null; - expect((event.value as BeaconChainReorgEvent).type).to.equal(BeaconEventType.CHAIN_REORG); - expect((event.value as BeaconChainReorgEvent).message).to.not.be.null; - expect((event.value as BeaconChainReorgEvent).message.depth).to.equal(3); - stream.stop(); + chainEventEmmitter.emit(ChainEvent.forkChoiceReorg, oldHead, newHead, depth); + + expect(events).to.have.length(1, "Wrong num of received events"); + const event = events[0]; + if (event.type !== routes.events.EventType.chainReorg) throw Error(`Wrong event type ${event.type}`); + expect(events[0].type).to.equal(routes.events.EventType.chainReorg); + expect(event.message).to.not.be.null; + expect(event.message.depth).to.equal(depth, "Wrong depth"); }); }); }); diff --git a/packages/lodestar/test/unit/api/impl/index.test.ts b/packages/lodestar/test/unit/api/impl/index.test.ts index a24aa68fdc..b1c016437c 100644 --- a/packages/lodestar/test/unit/api/impl/index.test.ts +++ b/packages/lodestar/test/unit/api/impl/index.test.ts @@ -1,7 +1,7 @@ import {config} from "@chainsafe/lodestar-config/minimal"; import {SinonSandbox, SinonStubbedInstance} from "sinon"; import sinon from "sinon"; -import {BeaconBlockApi} from "../../../../src/api/impl/beacon/blocks"; +import {getBeaconBlockApi} from "../../../../src/api/impl/beacon/blocks"; import {ForkChoice, BeaconChain} from "../../../../src/chain"; import {Network} from "../../../../src/network"; import {BeaconSync} from "../../../../src/sync"; @@ -15,7 +15,7 @@ export type ApiImplTestModules = { syncStub: SinonStubbedInstance; dbStub: StubbedBeaconDb; networkStub: SinonStubbedInstance; - blockApi: BeaconBlockApi; + blockApi: ReturnType; config: IBeaconConfig; }; @@ -26,16 +26,12 @@ export function setupApiImplTestServer(): ApiImplTestModules { const syncStub = sinon.createStubInstance(BeaconSync); const dbStub = new StubbedBeaconDb(sinon, config); const networkStub = sinon.createStubInstance(Network); - const blockApi = new BeaconBlockApi( - {}, - { - chain: chainStub, - config, - db: dbStub, - network: networkStub, - sync: syncStub, - } - ); + const blockApi = getBeaconBlockApi({ + chain: chainStub, + config, + db: dbStub, + network: networkStub, + }); chainStub.forkChoice = forkChoiceStub; return { sandbox, diff --git a/packages/lodestar/test/unit/api/impl/lodestar/lodestar.test.ts b/packages/lodestar/test/unit/api/impl/lodestar/lodestar.test.ts deleted file mode 100644 index 1a31c9938d..0000000000 --- a/packages/lodestar/test/unit/api/impl/lodestar/lodestar.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {createCachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition"; -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import {SinonStubbedInstance} from "sinon"; -import {ILodestarApi, LodestarApi} from "../../../../../src/api/impl/lodestar"; -import {BeaconChain} from "../../../../../src/chain"; -import {generateState} from "../../../../utils/state"; -import {ApiImplTestModules, setupApiImplTestServer} from "../index.test"; - -describe("Lodestar api impl", function () { - let api: ILodestarApi; - let server: ApiImplTestModules; - let chainStub: SinonStubbedInstance; - - beforeEach(async function () { - server = setupApiImplTestServer(); - chainStub = server.chainStub; - chainStub.getHeadState.returns(createCachedBeaconState(config, generateState())); - api = new LodestarApi({config, chain: chainStub, sync: server.syncStub}); - }); - - it("should get latest weak subjectivity checkpoint epoch", async function () { - const epoch = await api.getLatestWeakSubjectivityCheckpointEpoch(); - expect(epoch).to.be.equal(0); - }); -}); diff --git a/packages/lodestar/test/unit/api/impl/node/node.test.ts b/packages/lodestar/test/unit/api/impl/node/node.test.ts index 2026c46f42..5746b37643 100644 --- a/packages/lodestar/test/unit/api/impl/node/node.test.ts +++ b/packages/lodestar/test/unit/api/impl/node/node.test.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ import {Connection} from "libp2p"; -import {INodeApi} from "../../../../../src/api/impl/node"; -import {NodeApi} from "../../../../../src/api/impl/node/node"; +import {getNodeApi} from "../../../../../src/api/impl/node"; import sinon, {SinonStubbedInstance} from "sinon"; import {createPeerId, INetwork, Network} from "../../../../../src/network"; import {BeaconSync, IBeaconSync} from "../../../../../src/sync"; @@ -13,8 +12,8 @@ import chaiAsPromised from "chai-as-promised"; import Multiaddr from "multiaddr"; import {MetadataController} from "../../../../../src/network/metadata"; import {altair} from "@chainsafe/lodestar-types"; -import {NodePeer} from "../../../../../src/api/types"; import {PeerStatus, PeerDirection} from "../../../../../src/network"; +import {routes} from "@chainsafe/lodestar-api"; use(chaiAsPromised); @@ -25,7 +24,7 @@ interface IPeerSummary { hasP2pAddress: boolean; } -const toPeerSummary = (peer: NodePeer): IPeerSummary => { +const toPeerSummary = (peer: routes.node.NodePeer): IPeerSummary => { return { direction: peer.direction, state: peer.state, @@ -35,7 +34,7 @@ const toPeerSummary = (peer: NodePeer): IPeerSummary => { }; describe("node api implementation", function () { - let api: INodeApi; + let api: ReturnType; let networkStub: SinonStubbedInstance; let syncStub: SinonStubbedInstance; let peerId: PeerId; @@ -43,13 +42,13 @@ describe("node api implementation", function () { beforeEach(async function () { networkStub = sinon.createStubInstance(Network); syncStub = sinon.createStubInstance(BeaconSync); - api = new NodeApi({}, {network: networkStub, sync: syncStub}); + api = getNodeApi({network: networkStub, sync: syncStub}); peerId = await PeerId.create({keyType: "secp256k1"}); sinon.stub(networkStub, "peerId").get(() => peerId); sinon.stub(networkStub, "localMultiaddrs").get(() => [new Multiaddr("/ip4/127.0.0.1/tcp/36000")]); }); - describe("getNodeIdentity", function () { + describe("getNetworkIdentity", function () { it("should get node identity", async function () { const keypair = createKeypairFromPeerId(peerId); const enr = ENR.createV4(keypair.publicKey); @@ -64,7 +63,7 @@ describe("node api implementation", function () { }; }, } as MetadataController; - const identity = await api.getNodeIdentity(); + const {data: identity} = await api.getNetworkIdentity(); expect(identity.peerId.startsWith("16")).to.be.true; expect(identity.enr.startsWith("enr:-")).to.be.true; expect(identity.discoveryAddresses.length).to.equal(1); @@ -76,25 +75,11 @@ describe("node api implementation", function () { it("should get node identity - no enr", async function () { networkStub.getEnr.returns((null as unknown) as ENR); - const identity = await api.getNodeIdentity(); + const {data: identity} = await api.getNetworkIdentity(); expect(identity.enr).equal(""); }); }); - describe("getNodeStatus", function () { - it("syncing", async function () { - syncStub.isSynced.returns(false); - const status = await api.getNodeStatus(); - expect(status).to.equal("syncing"); - }); - - it("ready", async function () { - syncStub.isSynced.resolves(true); - const status = await api.getNodeStatus(); - expect(status).to.equal("ready"); - }); - }); - describe("getPeers", function () { let peer1: PeerId, peer2: PeerId; @@ -110,7 +95,7 @@ describe("node api implementation", function () { ]); networkStub.getConnectionsByPeer.returns(connectionsByPeer); - const peers = await api.getPeers(); + const {data: peers} = await api.getPeers(); expect(peers.length).to.equal(2); expect(peers.map(toPeerSummary)).to.be.deep.equal([ {direction: "outbound", state: "connected", hasP2pAddress: true, hasPeerId: true}, @@ -125,7 +110,7 @@ describe("node api implementation", function () { ]); networkStub.getConnectionsByPeer.returns(connectionsByPeer); - const peers = await api.getPeers(); + const {data: peers} = await api.getPeers(); // expect(peers[0].enr).not.empty; expect(peers.map(toPeerSummary)).to.be.deep.equal([ {direction: "outbound", state: "disconnected", hasPeerId: true, hasP2pAddress: true}, @@ -144,7 +129,7 @@ describe("node api implementation", function () { ]); networkStub.getConnectionsByPeer.returns(connectionsByPeer); - const peer = await api.getPeer(peer1.toB58String()); + const {data: peer} = await api.getPeer(peer1.toB58String()); if (!peer) throw Error("getPeer returned no peer"); expect(peer.peerId).to.equal(peer1.toB58String()); expect(peer.lastSeenP2pAddress).not.empty; @@ -163,20 +148,19 @@ describe("node api implementation", function () { describe("getSyncStatus", function () { it("success", async function () { - syncStub.getSyncStatus.resolves({ - headSlot: BigInt(2), - syncDistance: BigInt(1), + syncStub.getSyncStatus.returns({ + headSlot: 2, + syncDistance: 1, }); - const syncStatus = await api.getSyncingStatus(); - expect(syncStatus.headSlot.toString()).to.equal("2"); - expect(syncStatus.syncDistance.toString()).to.equal("1"); + const {data: syncStatus} = await api.getSyncingStatus(); + expect(syncStatus).to.deep.equal({headSlot: 2, syncDistance: 1}); }); }); describe("getVersion", function () { it("success", async function () { - const version = await api.getVersion(); - expect(version.startsWith("Lodestar")).to.be.true; + const {data} = await api.getNodeVersion(); + expect(data.version.startsWith("Lodestar")).to.be.true; }); }); }); diff --git a/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts index 67bbfa19da..44c7aae2a7 100644 --- a/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts @@ -8,8 +8,8 @@ import {ForkChoice, IBeaconChain} from "../../../../../../src/chain"; import {LocalClock} from "../../../../../../src/chain/clock"; import {FAR_FUTURE_EPOCH} from "../../../../../../src/constants"; import {IEth1ForBlockProduction} from "../../../../../../src/eth1"; -import {IValidatorApi, ValidatorApi} from "../../../../../../src/api/impl/validator"; -import {IApiModules} from "../../../../../../src/api/impl/interface"; +import {getValidatorApi} from "../../../../../../src/api/impl/validator"; +import {ApiModules} from "../../../../../../src/api/impl/types"; import {generateInitialMaxBalances} from "../../../../../utils/balances"; import {generateState} from "../../../../../utils/state"; import {IBeaconSync} from "../../../../../../src/sync"; @@ -28,9 +28,9 @@ describe("get proposers api impl", function () { syncStub: SinonStubbedInstance, dbStub: StubbedBeaconDb; - let api: IValidatorApi; + let api: ReturnType; let server: ApiImplTestModules; - let modules: IApiModules; + let modules: ApiModules; beforeEach(function () { server = setupApiImplTestServer(); @@ -50,7 +50,7 @@ describe("get proposers api impl", function () { sync: syncStub, metrics: null, }; - api = new ValidatorApi({}, modules); + api = getValidatorApi(modules); }); it("should get proposers", async function () { @@ -73,7 +73,7 @@ describe("get proposers api impl", function () { const cachedState = createCachedBeaconState(config, state); chainStub.getHeadStateAtCurrentEpoch.resolves(cachedState); sinon.stub(cachedState.epochCtx, "getBeaconProposer").returns(1); - const result = await api.getProposerDuties(0); - expect(result.data.length).to.be.equal(config.params.SLOTS_PER_EPOCH); + const {data: result} = await api.getProposerDuties(0); + expect(result.length).to.be.equal(config.params.SLOTS_PER_EPOCH); }); }); diff --git a/packages/lodestar/test/unit/api/impl/validator/produceAttestationData.test.ts b/packages/lodestar/test/unit/api/impl/validator/produceAttestationData.test.ts index 4e96486016..4d7683382a 100644 --- a/packages/lodestar/test/unit/api/impl/validator/produceAttestationData.test.ts +++ b/packages/lodestar/test/unit/api/impl/validator/produceAttestationData.test.ts @@ -2,8 +2,8 @@ import {config} from "@chainsafe/lodestar-config/minimal"; import {IBlockSummary} from "@chainsafe/lodestar-fork-choice"; import sinon, {SinonStubbedInstance} from "sinon"; import {IBeaconSync, SyncState} from "../../../../../src/sync/interface"; -import {IApiModules} from "../../../../../src/api/impl/interface"; -import {ValidatorApi} from "../../../../../src/api/impl/validator/validator"; +import {ApiModules} from "../../../../../src/api/impl/types"; +import {getValidatorApi} from "../../../../../src/api/impl/validator"; import {IEth1ForBlockProduction} from "../../../../../src/eth1"; import {LocalClock} from "../../../../../src/chain/clock"; import {testLogger} from "../../../../utils/logger"; @@ -17,7 +17,7 @@ describe("api - validator - produceAttestationData", function () { const logger = testLogger(); let eth1Stub: SinonStubbedInstance; let syncStub: SinonStubbedInstance; - let modules: IApiModules; + let modules: ApiModules; let server: ApiImplTestModules; beforeEach(function () { @@ -44,7 +44,7 @@ describe("api - validator - produceAttestationData", function () { server.forkChoiceStub.getHead.returns({slot: headSlot} as IBlockSummary); // Should not allow any call to validator API - const api = new ValidatorApi({}, modules); + const api = getValidatorApi(modules); await expect(api.produceAttestationData(0, 0)).to.be.rejectedWith("Node is syncing"); }); @@ -54,7 +54,7 @@ describe("api - validator - produceAttestationData", function () { sinon.replaceGetter(syncStub, "state", () => SyncState.Stalled); // Should not allow any call to validator API - const api = new ValidatorApi({}, modules); + const api = getValidatorApi(modules); await expect(api.produceAttestationData(0, 0)).to.be.rejectedWith("Node is waiting for peers"); }); }); diff --git a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlock.test.ts b/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlock.test.ts deleted file mode 100644 index b3e86ef54b..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlock.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {getBlock} from "../../../../../../src/api/rest/beacon/blocks/getBlock"; -import {generateEmptySignedBlock} from "../../../../../utils/block"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {IBeaconBlocksApi} from "../../../../../../src/api/impl/beacon/blocks"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconBlockApi} from "../../../../../../src/api/impl/beacon/blocks"; - -describe("rest - beacon - getBlock", function () { - let beaconBlocksStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - beaconBlocksStub = restApi.server.api.beacon.blocks as SinonStubbedInstance; - }); - - after(async function () { - await restApi.close(); - }); - - it("should succeed", async function () { - beaconBlocksStub.getBlock.withArgs("head").resolves(generateEmptySignedBlock()); - const response = await supertest(restApi.server.server) - .get(getBlock.url.replace(":blockId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockAttestations.test.ts b/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockAttestations.test.ts deleted file mode 100644 index 7196f321c7..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockAttestations.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {List} from "@chainsafe/ssz"; -import {phase0} from "@chainsafe/lodestar-types"; - -import {getBlockAttestations} from "../../../../../../src/api/rest/beacon/blocks/getBlockAttestations"; -import {generateSignedBlock} from "../../../../../utils/block"; -import {generateEmptyAttestation} from "../../../../../utils/attestation"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconBlockApi, IBeaconBlocksApi} from "../../../../../../src/api/impl/beacon/blocks"; - -describe("rest - beacon - getBlockAttestations", function () { - let beaconBlocksStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - beaconBlocksStub = restApi.server.api.beacon.blocks as SinonStubbedInstance; - }); - - after(async function () { - await restApi.close(); - }); - - it("should succeed", async function () { - beaconBlocksStub.getBlock.withArgs("head").resolves( - generateSignedBlock({ - message: { - body: { - attestations: [generateEmptyAttestation(), generateEmptyAttestation()] as List, - }, - }, - }) - ); - const response = await supertest(restApi.server.server) - .get(getBlockAttestations.url.replace(":blockId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data.length).to.equal(2); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeader.test.ts b/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeader.test.ts deleted file mode 100644 index 16342059c5..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeader.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {getBlockHeader} from "../../../../../../src/api/rest/beacon/blocks/getBlockHeader"; -import {generateSignedBeaconHeaderResponse} from "../../../../../utils/api"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconBlockApi, IBeaconBlocksApi} from "../../../../../../src/api/impl/beacon/blocks"; - -describe("rest - beacon - getBlockHeader", function () { - let beaconBlocksStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - beaconBlocksStub = restApi.server.api.beacon.blocks as SinonStubbedInstance; - }); - - after(async function () { - await restApi.close(); - }); - - it("should succeed", async function () { - beaconBlocksStub.getBlockHeader.withArgs("head").resolves(generateSignedBeaconHeaderResponse()); - const response = await supertest(restApi.server.server) - .get(getBlockHeader.url.replace(":blockId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeaders.test.ts b/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeaders.test.ts deleted file mode 100644 index 5189f2e268..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockHeaders.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {toHexString} from "@chainsafe/ssz"; - -import {getBlockHeaders} from "../../../../../../src/api/rest/beacon/blocks/getBlockHeaders"; -import {generateSignedBeaconHeaderResponse} from "../../../../../utils/api"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconBlockApi, IBeaconBlocksApi} from "../../../../../../src/api/impl/beacon/blocks"; - -describe("rest - beacon - getBlockHeaders", function () { - let beaconBlocksStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - beaconBlocksStub = restApi.server.api.beacon.blocks as SinonStubbedInstance; - }); - - after(async function () { - await restApi.close(); - }); - - it("should fetch without filters", async function () { - beaconBlocksStub.getBlockHeaders.resolves([generateSignedBeaconHeaderResponse()]); - const response = await supertest(restApi.server.server) - .get(getBlockHeaders.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); - - it("should parse slot param", async function () { - beaconBlocksStub.getBlockHeaders - .withArgs({slot: 1, parentRoot: undefined}) - .resolves([generateSignedBeaconHeaderResponse()]); - const response = await supertest(restApi.server.server) - .get(getBlockHeaders.url) - .query({slot: "1"}) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); - - it("should parse parentRoot param", async function () { - beaconBlocksStub.getBlockHeaders - .withArgs({slot: undefined, parentRoot: new Uint8Array(32).fill(1)}) - .resolves([generateSignedBeaconHeaderResponse()]); - const response = await supertest(restApi.server.server) - .get(getBlockHeaders.url) - // eslint-disable-next-line @typescript-eslint/naming-convention - .query({parent_root: toHexString(Buffer.alloc(32, 1))}) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); - - it("should throw validation error on invalid slot", async function () { - await supertest(restApi.server.server) - .get(getBlockHeaders.url) - .query({slot: "abc"}) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it.skip("should throw validation error on invalid parentRoot - not hex", async function () { - await supertest(restApi.server.server) - .get(getBlockHeaders.url) - .query({parentRoot: "0xb0e16cdb82ddf08b02aa3898d16a706997b11a69048c80525338d4a7b378d8eg"}) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it.skip("should throw validation error on invalid parentRoot - incorrect length", async function () { - await supertest(restApi.server.server) - .get(getBlockHeaders.url) - .query({parentRoot: "0xb0e"}) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it.skip("should throw validation error on invalid parentRoot - missing 0x prefix", async function () { - await supertest(restApi.server.server) - .get(getBlockHeaders.url) - .query({parentRoot: "b0e16cdb82ddf08b02aa3898d16a706997b11a69048c80525338d4a7b378d8eb"}) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockRoot.test.ts b/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockRoot.test.ts deleted file mode 100644 index e3e59de4c0..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/blocks/getBlockRoot.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {toHexString} from "@chainsafe/ssz"; - -import {getBlockRoot} from "../../../../../../src/api/rest/beacon/blocks/getBlockRoot"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconBlockApi, IBeaconBlocksApi} from "../../../../../../src/api/impl/beacon/blocks"; - -describe("rest - beacon - getBlockRoot", function () { - let beaconBlocksStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - beaconBlocksStub = restApi.server.api.beacon.blocks as SinonStubbedInstance; - }); - - after(async function () { - await restApi.close(); - }); - - it("should succeed", async function () { - 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(root)); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/blocks/publishBlock.test.ts b/packages/lodestar/test/unit/api/rest/beacon/blocks/publishBlock.test.ts deleted file mode 100644 index cfc555bbe0..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/blocks/publishBlock.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import supertest from "supertest"; -import {publishBlock} from "../../../../../../src/api/rest/beacon/blocks/publishBlock"; -import {generateEmptySignedBlock} from "../../../../../utils/block"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconBlockApi, IBeaconBlocksApi} from "../../../../../../src/api/impl/beacon/blocks"; - -describe("rest - beacon - publishBlock", function () { - let beaconBlocksStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconBlocksStub = restApi.server.api.beacon.blocks as SinonStubbedInstance; - }); - - it("should succeed", async function () { - const block = generateEmptySignedBlock(); - beaconBlocksStub.publishBlock.resolves(); - await supertest(restApi.server.server) - .post(publishBlock.url) - .send(config.types.phase0.SignedBeaconBlock.toJson(block, {case: "snake"}) as Record) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it("bad body", async function () { - await supertest(restApi.server.server) - .post(publishBlock.url) - .send({}) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - expect(beaconBlocksStub.publishBlock.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/getGenesis.test.ts b/packages/lodestar/test/unit/api/rest/beacon/getGenesis.test.ts deleted file mode 100644 index 058afb28c3..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/getGenesis.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import supertest from "supertest"; -import {expect} from "chai"; -import {config} from "@chainsafe/lodestar-config/mainnet"; - -import {getGenesis} from "../../../../../src/api/rest/beacon/getGenesis"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {BeaconApi, RestApi} from "../../../../../src/api"; -import {SinonStubbedInstance} from "sinon"; - -describe("rest - beacon - getGenesis", function () { - let beaconStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - beaconStub = restApi.server.api.beacon as SinonStubbedInstance; - }); - - it("should get genesis object", async function () { - beaconStub.getGenesis.resolves({ - genesisForkVersion: config.params.GENESIS_FORK_VERSION, - genesisTime: BigInt(0), - genesisValidatorsRoot: Buffer.alloc(32), - }); - const response = await supertest(restApi.server.server) - .get(getGenesis.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.genesis_time).to.equal("0"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.genesis_validators_root).to.not.be.empty; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttestations.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttestations.test.ts deleted file mode 100644 index ab2210d016..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttestations.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {getPoolAttestations} from "../../../../../../src/api/rest/beacon/pool/getPoolAttestations"; -import {generateAttestation} from "../../../../../utils/attestation"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - getPoolAttestations", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - beaconPoolStub.getAttestations.withArgs({committeeIndex: 1, slot: 1}).resolves([generateAttestation()]); - const response = await supertest(restApi.server.server) - .get(getPoolAttestations.url) - // eslint-disable-next-line @typescript-eslint/naming-convention - .query({slot: "1", committee_index: "1"}) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttesterSlashings.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttesterSlashings.test.ts deleted file mode 100644 index 61963db502..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolAttesterSlashings.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {getPoolAttesterSlashings} from "../../../../../../src/api/rest/beacon/pool/getPoolAttesterSlashings"; -import {generateEmptyAttesterSlashing} from "../../../../../utils/slashings"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - getPoolAttesterSlashings", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - beaconPoolStub.getAttesterSlashings.resolves([generateEmptyAttesterSlashing()]); - const response = await supertest(restApi.server.server) - .get(getPoolAttesterSlashings.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolProposerSlashings.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolProposerSlashings.test.ts deleted file mode 100644 index ac1fb1a219..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolProposerSlashings.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {getPoolProposerSlashings} from "../../../../../../src/api/rest/beacon/pool/getPoolProposerSlashings"; -import {generateEmptyProposerSlashing} from "../../../../../utils/slashings"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - getPoolProposerSlashings", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - beaconPoolStub.getProposerSlashings.resolves([generateEmptyProposerSlashing()]); - const response = await supertest(restApi.server.server) - .get(getPoolProposerSlashings.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolVoluntaryExits.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolVoluntaryExits.test.ts deleted file mode 100644 index 7968710ef3..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/getPoolVoluntaryExits.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {getPoolVoluntaryExits} from "../../../../../../src/api/rest/beacon/pool/getPoolVoluntaryExits"; -import {generateEmptySignedVoluntaryExit} from "../../../../../utils/attestation"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - getPoolVoluntaryExits", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - beaconPoolStub.getVoluntaryExits.withArgs().resolves([generateEmptySignedVoluntaryExit()]); - const response = await supertest(restApi.server.server) - .get(getPoolVoluntaryExits.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttestation.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttestation.test.ts deleted file mode 100644 index 1f071f387f..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttestation.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import supertest from "supertest"; -import {setupRestApiTestServer} from "../../index.test"; -import {generateAttestation} from "../../../../../utils/attestation"; -import {submitPoolAttestations} from "../../../../../../src/api/rest/beacon/pool/submitPoolAttestations"; -import {Attestation} from "@chainsafe/lodestar-types/phase0"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - submitPoolAttestations", function () { - let attestation: Attestation; - let restApi: RestApi; - let beaconPoolStub: SinonStubbedInstance; - - before(function () { - attestation = generateAttestation(); - }); - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - }); - - it("should succeed", async function () { - await supertest(restApi.server.server) - .post(submitPoolAttestations.url) - .send([config.types.phase0.Attestation.toJson(attestation, {case: "snake"}) as Record]) - .expect(200); - expect(beaconPoolStub.submitAttestations.calledOnce).to.be.true; - }); - - it("should fail to parse body", async function () { - await supertest(restApi.server.server) - .post(submitPoolAttestations.url) - .send([config.types.phase0.Attestation.toJson(attestation, {case: "camel"}) as Record]) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - expect(beaconPoolStub.submitAttestations.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttesterSlashing.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttesterSlashing.test.ts deleted file mode 100644 index 16a27780a6..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolAttesterSlashing.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {AttesterSlashing} from "@chainsafe/lodestar-types/phase0"; -import {expect} from "chai"; -import supertest from "supertest"; -import {submitPoolAttesterSlashings} from "../../../../../../src/api/rest/beacon/pool/submitPoolAttesterSlashings"; -import {generateEmptyAttesterSlashing} from "../../../../../utils/slashings"; -import {setupRestApiTestServer} from "../../index.test"; -import {RestApi} from "../../../../../../src/api"; -import {SinonStubbedInstance} from "sinon"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - submitPoolAttesterSlashings", function () { - let slashing: AttesterSlashing; - let restApi: RestApi; - let beaconPoolStub: SinonStubbedInstance; - - before(function () { - slashing = generateEmptyAttesterSlashing(); - }); - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - }); - - it("should succeed", async function () { - await supertest(restApi.server.server) - .post(submitPoolAttesterSlashings.url) - .send(config.types.phase0.AttesterSlashing.toJson(slashing, {case: "snake"}) as Record) - .expect(200); - expect(beaconPoolStub.submitAttesterSlashing.calledOnce).to.be.true; - }); - - it("should fail to parse body", async function () { - await supertest(restApi.server.server) - .post(submitPoolAttesterSlashings.url) - .send(config.types.phase0.AttesterSlashing.toJson(slashing, {case: "camel"}) as Record) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - expect(beaconPoolStub.submitAttesterSlashing.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolProposerSlashings.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolProposerSlashings.test.ts deleted file mode 100644 index 661d5343f7..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolProposerSlashings.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import supertest from "supertest"; -import {setupRestApiTestServer} from "../../index.test"; -import {generateEmptyProposerSlashing} from "../../../../../utils/slashings"; -import {submitPoolProposerSlashings} from "../../../../../../src/api/rest/beacon/pool/submitPoolProposerSlashings"; -import {ProposerSlashing} from "@chainsafe/lodestar-types/phase0"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - submitPoolProposerSlashings", function () { - let slashing: ProposerSlashing; - let restApi: RestApi; - let beaconPoolStub: SinonStubbedInstance; - - before(function () { - slashing = generateEmptyProposerSlashing(); - }); - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - }); - - it("should succeed", async function () { - await supertest(restApi.server.server) - .post(submitPoolProposerSlashings.url) - .send(config.types.phase0.ProposerSlashing.toJson(slashing, {case: "snake"}) as Record) - .expect(200); - expect(beaconPoolStub.submitProposerSlashing.calledOnce).to.be.true; - }); - - it("should fail to parse body", async function () { - await supertest(restApi.server.server) - .post(submitPoolProposerSlashings.url) - .send(config.types.phase0.ProposerSlashing.toJson(slashing, {case: "camel"}) as Record) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - expect(beaconPoolStub.submitProposerSlashing.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolVoluntaryExit.test.ts b/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolVoluntaryExit.test.ts deleted file mode 100644 index 493953b27e..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/pool/submitPoolVoluntaryExit.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {SignedVoluntaryExit} from "@chainsafe/lodestar-types/phase0"; -import {expect} from "chai"; -import supertest from "supertest"; -import {submitPoolVoluntaryExit} from "../../../../../../src/api/rest/beacon/pool/submitPoolVoluntaryExit"; -import {generateEmptySignedVoluntaryExit} from "../../../../../utils/attestation"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconPoolApi} from "../../../../../../src/api/impl/beacon/pool"; - -describe("rest - beacon - submitPoolVoluntaryExit", function () { - let voluntaryExit: SignedVoluntaryExit; - let restApi: RestApi; - let beaconPoolStub: SinonStubbedInstance; - - before(function () { - voluntaryExit = generateEmptySignedVoluntaryExit(); - }); - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconPoolStub = restApi.server.api.beacon.pool as SinonStubbedInstance; - }); - - it("should succeed", async function () { - await supertest(restApi.server.server) - .post(submitPoolVoluntaryExit.url) - .send(config.types.phase0.SignedVoluntaryExit.toJson(voluntaryExit, {case: "snake"}) as Record) - .expect(200); - expect(beaconPoolStub.submitVoluntaryExit.calledOnce).to.be.true; - }); - - it("should fail to parse body", async function () { - await supertest(restApi.server.server) - .post(submitPoolVoluntaryExit.url) - .send(config.types.phase0.SignedVoluntaryExit.toJson(voluntaryExit, {case: "camel"}) as Record) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - expect(beaconPoolStub.submitVoluntaryExit.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/state/getEpochCommittees.test.ts b/packages/lodestar/test/unit/api/rest/beacon/state/getEpochCommittees.test.ts deleted file mode 100644 index 39ea7c78b6..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/state/getEpochCommittees.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import {ValidatorIndex} from "@chainsafe/lodestar-types"; -import {List} from "@chainsafe/ssz"; -import {expect} from "chai"; -import supertest from "supertest"; -import {StateNotFound} from "../../../../../../src/api/impl/errors"; -import {getEpochCommittees} from "../../../../../../src/api/rest/beacon/state/getEpochCommittees"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; - -describe("rest - beacon - getEpochCommittees", function () { - let beaconStateStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconStateStub = restApi.server.api.beacon.state as SinonStubbedInstance; - }); - - it("should succeed without filters", async function () { - beaconStateStub.getStateCommittees.withArgs("head").resolves([ - { - index: 0, - slot: 1, - validators: [1, 2, 3] as List, - }, - ]); - const response = await supertest(restApi.server.server) - .get(getEpochCommittees.url.replace(":stateId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); - - it("should succeed with filters", async function () { - beaconStateStub.getStateCommittees.withArgs("head", {slot: 1, epoch: 0, index: 10}).resolves([ - { - index: 0, - slot: 1, - validators: [1, 2, 3] as List, - }, - ]); - const response = await supertest(restApi.server.server) - .get(getEpochCommittees.url.replace(":stateId", "head")) - .query({ - slot: "1", - epoch: "0", - index: "10", - }) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data.length).to.be.equal(1); - }); - - it("string slot", async function () { - await supertest(restApi.server.server) - .get(getEpochCommittees.url.replace(":stateId", "head")) - .query({ - slot: "1a", - epoch: "0", - index: "10", - }) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it("negative epoch", async function () { - await supertest(restApi.server.server) - .get(getEpochCommittees.url.replace(":stateId", "head")) - .query({ - slot: "1", - epoch: "-2", - index: "10", - }) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it("should not found state", async function () { - beaconStateStub.getStateCommittees.withArgs("4").throws(new StateNotFound()); - await supertest(restApi.server.server).get(getEpochCommittees.url.replace(":stateId", "4")).expect(404); - expect(beaconStateStub.getStateCommittees.calledOnce).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/state/getStateFinalityCheckpoints.test.ts b/packages/lodestar/test/unit/api/rest/beacon/state/getStateFinalityCheckpoints.test.ts deleted file mode 100644 index 0b40aa6a49..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/state/getStateFinalityCheckpoints.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {getStateFinalityCheckpoints} from "../../../../../../src/api/rest/beacon/state/getStateFinalityCheckpoints"; -import {generateState} from "../../../../../utils/state"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; - -describe("rest - beacon - getStateFinalityCheckpoints", function () { - let beaconStateStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconStateStub = restApi.server.api.beacon.state as SinonStubbedInstance; - }); - - it("should succeed", async function () { - beaconStateStub.getState.withArgs("head").resolves(generateState()); - const response = await supertest(restApi.server.server) - .get(getStateFinalityCheckpoints.url.replace(":stateId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.finalized).to.not.be.undefined; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/state/getStateFork.test.ts b/packages/lodestar/test/unit/api/rest/beacon/state/getStateFork.test.ts deleted file mode 100644 index 8ca4aa1860..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/state/getStateFork.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {generateState} from "../../../../../utils/state"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {getStateFork} from "../../../../../../src/api/rest/beacon/state/getStateFork"; -import {SinonStubbedInstance} from "sinon"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; -import {RestApi} from "../../../../../../src/api"; - -describe("rest - beacon - getStateFork", function () { - let beaconStateStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconStateStub = restApi.server.api.beacon.state as SinonStubbedInstance; - }); - - it("should succeed", async function () { - beaconStateStub.getFork.withArgs("head").resolves(generateState().fork); - const response = await supertest(restApi.server.server) - .get(getStateFork.url.replace(":stateId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.current_version).to.not.be.undefined; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidator.test.ts b/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidator.test.ts deleted file mode 100644 index 8424b24c0d..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidator.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {toHexString} from "@chainsafe/ssz"; -import {expect} from "chai"; -import supertest from "supertest"; -import {StateNotFound} from "../../../../../../src/api/impl/errors"; -import {getStateValidator} from "../../../../../../src/api/rest/beacon/state/getStateValidator"; -import {generateValidator} from "../../../../../utils/validator"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {phase0} from "@chainsafe/lodestar-types"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; - -describe("rest - beacon - getStateValidator", function () { - let beaconStateStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconStateStub = restApi.server.api.beacon.state as SinonStubbedInstance; - }); - - it("should get by root", async function () { - const pubkey = toHexString(Buffer.alloc(48, 1)); - beaconStateStub.getStateValidator.withArgs("head", config.types.BLSPubkey.fromJson(pubkey)).resolves({ - index: 1, - balance: BigInt(3200000), - status: phase0.ValidatorStatus.ACTIVE_ONGOING, - validator: generateValidator(), - }); - const response = await supertest(restApi.server.server) - .get(getStateValidator.url.replace(":stateId", "head").replace(":validatorId", pubkey)) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.balance).to.not.be.undefined; - }); - - it("should get by index", async function () { - beaconStateStub.getStateValidator.withArgs("head", 1).resolves({ - index: 1, - balance: BigInt(3200000), - status: phase0.ValidatorStatus.ACTIVE_ONGOING, - validator: generateValidator(), - }); - const response = await supertest(restApi.server.server) - .get(getStateValidator.url.replace(":stateId", "head").replace(":validatorId", "1")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.balance).to.not.be.undefined; - }); - - it("should not found state", async function () { - beaconStateStub.getStateValidator.withArgs("4", 1).throws(new StateNotFound()); - await supertest(restApi.server.server) - .get(getStateValidator.url.replace(":stateId", "4").replace(":validatorId", "1")) - .expect(404); - expect(beaconStateStub.getStateValidator.calledOnce).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidators.test.ts b/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidators.test.ts deleted file mode 100644 index f0985309a4..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidators.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {StateNotFound} from "../../../../../../src/api/impl/errors"; -import {getStateValidators} from "../../../../../../src/api/rest/beacon/state/getStateValidators"; -import {generateValidator} from "../../../../../utils/validator"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {phase0} from "@chainsafe/lodestar-types"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; - -describe("rest - beacon - getStateValidators", function () { - let beaconStateStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconStateStub = restApi.server.api.beacon.state as SinonStubbedInstance; - }); - - it("should success", async function () { - beaconStateStub.getStateValidators.withArgs("head").resolves([ - { - index: 1, - balance: BigInt(3200000), - status: phase0.ValidatorStatus.ACTIVE_ONGOING, - validator: generateValidator(), - }, - ]); - const response = await supertest(restApi.server.server) - .get(getStateValidators.url.replace(":stateId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data.length).to.equal(1); - }); - - it("should not found state", async function () { - beaconStateStub.getStateValidators.withArgs("4").throws(new StateNotFound()); - await supertest(restApi.server.server).get(getStateValidators.url.replace(":stateId", "4")).expect(404); - expect(beaconStateStub.getStateValidators.calledOnce).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidatorsBalances.test.ts b/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidatorsBalances.test.ts deleted file mode 100644 index 5fb8d3342b..0000000000 --- a/packages/lodestar/test/unit/api/rest/beacon/state/getStateValidatorsBalances.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {toHexString} from "@chainsafe/ssz"; -import {expect} from "chai"; -import supertest from "supertest"; -import {StateNotFound} from "../../../../../../src/api/impl/errors"; -import {getStateValidatorsBalances} from "../../../../../../src/api/rest/beacon/state/getStateValidatorBalances"; -import {ApiResponseBody} from "../../utils"; -import {setupRestApiTestServer} from "../../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi} from "../../../../../../src/api"; -import {BeaconStateApi} from "../../../../../../src/api/impl/beacon/state"; - -describe("rest - beacon - getStateValidatorsBalances", function () { - let beaconStateStub: SinonStubbedInstance; - let restApi: RestApi; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - beaconStateStub = restApi.server.api.beacon.state as SinonStubbedInstance; - }); - - it("should succeed", async function () { - beaconStateStub.getStateValidatorBalances.withArgs("head").resolves([ - { - index: 1, - balance: BigInt(32), - }, - ]); - const response = await supertest(restApi.server.server) - .get(getStateValidatorsBalances.url.replace(":stateId", "head")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data.length).to.equal(1); - }); - - it("should success with indices filter", async function () { - const hexPubkey = toHexString(Buffer.alloc(48, 1)); - const pubkey = config.types.BLSPubkey.fromJson(hexPubkey); - beaconStateStub.getStateValidatorBalances.withArgs("head", [1, pubkey]).resolves([ - { - index: 1, - balance: BigInt(32), - }, - { - index: 3, - balance: BigInt(32), - }, - ]); - const response = await supertest(restApi.server.server) - .get(getStateValidatorsBalances.url.replace(":stateId", "head")) - .query({ - id: [1, hexPubkey], - }) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data.length).to.equal(2); - }); - - it("should not found state", async function () { - beaconStateStub.getStateValidatorBalances.withArgs("4").throws(new StateNotFound()); - await supertest(restApi.server.server).get(getStateValidatorsBalances.url.replace(":stateId", "4")).expect(404); - expect(beaconStateStub.getStateValidatorBalances.calledOnce).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/config/getDepositContract.test.ts b/packages/lodestar/test/unit/api/rest/config/getDepositContract.test.ts deleted file mode 100644 index 72f2f5cd48..0000000000 --- a/packages/lodestar/test/unit/api/rest/config/getDepositContract.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import supertest from "supertest"; -import {setupRestApiTestServer} from "../index.test"; -import {getDepositContract} from "../../../../../src/api/rest/config/getDepositContract"; -import {ConfigApi} from "../../../../../src/api/impl/config"; -import {SinonStubbedInstance} from "sinon"; -import {ApiResponseBody} from "../utils"; - -describe("rest - config - getDepositContract", function () { - it("ready", async function () { - const restApi = await setupRestApiTestServer(); - const configStub = restApi.server.api.config as SinonStubbedInstance; - const depositContract = { - chainId: config.params.DEPOSIT_CHAIN_ID, - address: config.params.DEPOSIT_CONTRACT_ADDRESS, - }; - const expectedJson = config.types.phase0.Contract.toJson(depositContract, {case: "snake"}) as Record< - string, - unknown - >; - configStub.getDepositContract.resolves(depositContract); - const response = await supertest(restApi.server.server).get(getDepositContract.url).expect(200); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect(Object.keys((response.body as ApiResponseBody).data).length).to.equal(2); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.chain_id).to.equal(Object.values(expectedJson)[0]); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.address).to.equal(Object.values(expectedJson)[1]); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/config/getForkSchedule.test.ts b/packages/lodestar/test/unit/api/rest/config/getForkSchedule.test.ts deleted file mode 100644 index e60b6d47b5..0000000000 --- a/packages/lodestar/test/unit/api/rest/config/getForkSchedule.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import {expect} from "chai"; -import supertest from "supertest"; -import {setupRestApiTestServer} from "../index.test"; -import {getForkSchedule} from "../../../../../src/api/rest/config/getForkSchedule"; -import {SinonStubbedInstance} from "sinon"; -import {ConfigApi} from "../../../../../src/api/impl/config"; -import {ApiResponseBody} from "../utils"; - -describe("rest - config - getForkSchedule", function () { - it("ready", async function () { - const restApi = await setupRestApiTestServer(); - const configStub = restApi.server.api.config as SinonStubbedInstance; - const expectedData: phase0.Fork[] = []; - configStub.getForkSchedule.resolves(expectedData); - const response = await supertest(restApi.server.server).get(getForkSchedule.url).expect(200); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.deep.equal(expectedData); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/config/getSpec.test.ts b/packages/lodestar/test/unit/api/rest/config/getSpec.test.ts deleted file mode 100644 index 549d043a53..0000000000 --- a/packages/lodestar/test/unit/api/rest/config/getSpec.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {BeaconParams} from "@chainsafe/lodestar-params"; -import {expect} from "chai"; -import supertest from "supertest"; -import {setupRestApiTestServer} from "../index.test"; -import {getSpec} from "../../../../../src/api/rest/config/getSpec"; -import {SinonStubbedInstance} from "sinon"; -import {ConfigApi} from "../../../../../src/api/impl/config"; -import {ApiResponseBody} from "../utils"; - -describe("rest - config - getSpec", function () { - it("ready", async function () { - const restApi = await setupRestApiTestServer(); - const configStub = restApi.server.api.config as SinonStubbedInstance; - configStub.getSpec.resolves(config.params); - const response = await supertest(restApi.server.server).get(getSpec.url).expect(200); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.deep.equal(BeaconParams.toJson(config.params)); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/debug/getDebugChainHeads.test.ts b/packages/lodestar/test/unit/api/rest/debug/getDebugChainHeads.test.ts deleted file mode 100644 index ca272fc260..0000000000 --- a/packages/lodestar/test/unit/api/rest/debug/getDebugChainHeads.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {ZERO_HASH} from "@chainsafe/lodestar-beacon-state-transition"; -import {SinonStubbedInstance} from "sinon"; -import {DebugBeaconApi} from "../../../../../src/api/impl/debug/beacon"; -import {getDebugChainHeads} from "../../../../../src/api/rest/debug/getDebugChainHeads"; -import {RestApi} from "../../../../../src/api"; -import {setupRestApiTestServer} from "../index.test"; -import {ApiResponseBody} from "../utils"; - -describe("rest - debug - getDebugChainHeads", function () { - let debugBeaconStub: SinonStubbedInstance; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - debugBeaconStub = restApi.server.api.debug.beacon as SinonStubbedInstance; - }); - - it("should succeed", async function () { - debugBeaconStub.getHeads.resolves([{slot: 100, root: ZERO_HASH}]); - const response = await supertest(restApi.server.server) - .get(getDebugChainHeads.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/debug/getState.test.ts b/packages/lodestar/test/unit/api/rest/debug/getState.test.ts deleted file mode 100644 index 75e0b5d466..0000000000 --- a/packages/lodestar/test/unit/api/rest/debug/getState.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {config} from "@chainsafe/lodestar-config/minimal"; - -import {ApiNamespace, RestApi} from "../../../../../src/api"; -import {StubbedApi} from "../../../../utils/stub/api"; -import {testLogger} from "../../../../utils/logger"; -import {SinonStubbedInstance} from "sinon"; -import {DebugBeaconApi} from "../../../../../src/api/impl/debug/beacon"; -import {getState} from "../../../../../src/api/rest/debug/getStates"; -import {generateState} from "../../../../utils/state"; -import {ApiResponseBody} from "../utils"; - -describe("rest - debug - getState", function () { - let restApi: RestApi; - let api: StubbedApi; - let debugBeaconStub: SinonStubbedInstance; - - beforeEach(async function () { - api = new StubbedApi(); - restApi = await RestApi.init( - { - api: [ApiNamespace.DEBUG], - cors: "*", - enabled: true, - host: "127.0.0.1", - port: 0, - }, - {config, logger: testLogger(), api, metrics: null} - ); - debugBeaconStub = api.debug.beacon as SinonStubbedInstance; - }); - - afterEach(async function () { - await restApi.close(); - }); - - it("should get state json successfully", async function () { - debugBeaconStub.getState.resolves(generateState()); - const response = await supertest(restApi.server.server) - .get(getState.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - }); - - it("should get state ssz successfully", async function () { - const state = generateState(); - debugBeaconStub.getState.resolves(state); - const response = await supertest(restApi.server.server) - .get(getState.url) - .accept("application/octet-stream") - .expect(200) - .expect("Content-Type", "application/octet-stream"); - expect(response.body).to.be.deep.equal(config.getForkTypes(state.slot).BeaconState.serialize(state)); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/events/getEventStream.test.ts b/packages/lodestar/test/unit/api/rest/events/getEventStream.test.ts deleted file mode 100644 index 22a92d82ea..0000000000 --- a/packages/lodestar/test/unit/api/rest/events/getEventStream.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import EventSource from "eventsource"; -import {URL} from "url"; -import {BeaconEvent, BeaconEventType} from "../../../../../src/api/impl/events"; -import {RestApi} from "../../../../../src/api/rest"; -import {getEventStream} from "../../../../../src/api/rest/events/getEventStream"; -import {generateAttestation} from "../../../../utils/attestation"; -import {expect} from "chai"; -import {AddressInfo} from "net"; -import {LodestarEventIterator} from "@chainsafe/lodestar-utils"; -import {setupRestApiTestServer} from "../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {EventsApi} from "../../../../../src/api"; - -describe("rest - events - getEventStream", function () { - it("should subscribe to topics", async function () { - const restApi = await setupRestApiTestServer(); - const eventsApiStub = restApi.server.api.events as SinonStubbedInstance; - - const mockEventStream = new LodestarEventIterator(({push}) => { - push({ - type: BeaconEventType.BLOCK, - message: {slot: 1, block: Buffer.alloc(32, 0)}, - }); - push({ - type: BeaconEventType.ATTESTATION, - message: generateAttestation(), - }); - }); - - eventsApiStub.getEventStream.returns(mockEventStream); - const eventSource = new EventSource( - getEventStreamUrl([BeaconEventType.BLOCK, BeaconEventType.ATTESTATION], restApi) - ); - const blockEventPromise = new Promise((resolve) => { - eventSource.addEventListener(BeaconEventType.BLOCK, resolve); - }); - const attestationEventPromise = new Promise((resolve) => { - eventSource.addEventListener(BeaconEventType.ATTESTATION, resolve); - }); - - const blockEvent = await blockEventPromise; - const attestationEvent = await attestationEventPromise; - expect(blockEvent).to.not.be.null; - expect(attestationEvent).to.not.be.null; - eventSource.close(); - }); - - function getEventStreamUrl(topics: BeaconEventType[], restApi: RestApi): string { - const addressInfo = restApi.server.server.address() as AddressInfo; - return new URL( - getEventStream.url + "?" + topics.map((t) => "topics=" + t).join("&"), - "http://" + addressInfo.address + ":" + addressInfo.port - ).toString(); - } -}); diff --git a/packages/lodestar/test/unit/api/rest/index.test.ts b/packages/lodestar/test/unit/api/rest/index.test.ts deleted file mode 100644 index f8c15bda0a..0000000000 --- a/packages/lodestar/test/unit/api/rest/index.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/mainnet"; - -import {ApiNamespace, RestApi} from "../../../../src/api"; -import {StubbedApi} from "../../../utils/stub/api"; -import {testLogger} from "../../../utils/logger"; - -export async function setupRestApiTestServer(): Promise { - const api = new StubbedApi(); - return await RestApi.init( - { - api: [ - ApiNamespace.BEACON, - ApiNamespace.CONFIG, - ApiNamespace.DEBUG, - ApiNamespace.EVENTS, - ApiNamespace.NODE, - ApiNamespace.VALIDATOR, - ApiNamespace.LODESTAR, - ], - cors: "*", - enabled: true, - host: "127.0.0.1", - port: 0, - }, - {config, logger: testLogger(), api, metrics: null} - ); -} diff --git a/packages/lodestar/test/unit/api/rest/lodestar/index.test.ts b/packages/lodestar/test/unit/api/rest/lodestar/index.test.ts deleted file mode 100644 index 035fe02460..0000000000 --- a/packages/lodestar/test/unit/api/rest/lodestar/index.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import supertest from "supertest"; - -import {getLatestWeakSubjectivityCheckpointEpoch} from "../../../../../src/api/rest/lodestar"; -import {setupRestApiTestServer} from "../index.test"; -import {RestApi} from "../../../../../src/api"; -import {StubbedLodestarApi} from "../../../../utils/stub/lodestarApi"; - -describe("rest - lodestar - getLatestWeakSubjectivityCheckpointEpoch", function () { - let restApi: RestApi; - let lodestarApiStub: StubbedLodestarApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - lodestarApiStub = restApi.server.api.lodestar as StubbedLodestarApi; - }); - - it("success", async function () { - lodestarApiStub.getLatestWeakSubjectivityCheckpointEpoch.resolves(0); - await supertest(restApi.server.server).get(getLatestWeakSubjectivityCheckpointEpoch.url).expect(200); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/node/getHealth.test.ts b/packages/lodestar/test/unit/api/rest/node/getHealth.test.ts deleted file mode 100644 index ce25f1ad9f..0000000000 --- a/packages/lodestar/test/unit/api/rest/node/getHealth.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import supertest from "supertest"; - -import {getHealth} from "../../../../../src/api/rest/node/getHealth"; -import {setupRestApiTestServer} from "../index.test"; -import {StubbedNodeApi} from "../../../../utils/stub/nodeApi"; -import {RestApi} from "../../../../../src/api"; - -describe("rest - node - getHealth", function () { - let nodeStub: StubbedNodeApi; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - nodeStub = restApi.server.api.node as StubbedNodeApi; - }); - - it("ready", async function () { - nodeStub.getNodeStatus.resolves("ready"); - await supertest(restApi.server.server).get(getHealth.url).expect(200); - }); - - it("syncing", async function () { - nodeStub.getNodeStatus.resolves("syncing"); - await supertest(restApi.server.server).get(getHealth.url).expect(206); - }); - - it("error", async function () { - nodeStub.getNodeStatus.resolves("error"); - await supertest(restApi.server.server).get(getHealth.url).expect(503); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/node/getNetworkIdentity.test.ts b/packages/lodestar/test/unit/api/rest/node/getNetworkIdentity.test.ts deleted file mode 100644 index d51f591926..0000000000 --- a/packages/lodestar/test/unit/api/rest/node/getNetworkIdentity.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {getNetworkIdentity} from "../../../../../src/api/rest/node/getNetworkIdentity"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {StubbedNodeApi} from "../../../../utils/stub/nodeApi"; - -describe("rest - node - getNetworkIdentity", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const nodeStub = restApi.server.api.node as StubbedNodeApi; - - nodeStub.getNodeIdentity.resolves({ - metadata: { - attnets: [true, false], - seqNumber: BigInt(3), - }, - p2pAddresses: ["/ip4/127.0.0.1/tcp/36001"], - peerId: "16", - enr: "enr-", - discoveryAddresses: ["/ip4/127.0.0.1/tcp/36000"], - }); - const response = await supertest(restApi.server.server) - .get(getNetworkIdentity.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.empty; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.p2p_addresses.length).to.equal(1); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/node/getPeer.test.ts b/packages/lodestar/test/unit/api/rest/node/getPeer.test.ts deleted file mode 100644 index 240fb3e13d..0000000000 --- a/packages/lodestar/test/unit/api/rest/node/getPeer.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {getPeer} from "../../../../../src/api/rest/node/getPeer"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {RestApi} from "../../../../../src/api"; -import {StubbedNodeApi} from "../../../../utils/stub/nodeApi"; - -describe("rest - node - getPeer", function () { - let nodeStub: StubbedNodeApi; - let restApi: RestApi; - - before(async function () { - restApi = await setupRestApiTestServer(); - nodeStub = restApi.server.api.node as StubbedNodeApi; - }); - - it("should succeed", async function () { - nodeStub.getPeer.resolves({ - lastSeenP2pAddress: "/ip4/127.0.0.1/tcp/36000", - direction: "inbound", - enr: "enr-", - peerId: "16", - state: "connected", - }); - const response = await supertest(restApi.server.server) - .get(getPeer.url.replace(":peerId", "16")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.empty; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.peer_id).to.equal("16"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/node/getPeers.test.ts b/packages/lodestar/test/unit/api/rest/node/getPeers.test.ts deleted file mode 100644 index 106e8344dc..0000000000 --- a/packages/lodestar/test/unit/api/rest/node/getPeers.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {getPeers} from "../../../../../src/api/rest/node/getPeers"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {StubbedNodeApi} from "../../../../utils/stub/nodeApi"; - -describe("rest - node - getPeers", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const nodeStub = restApi.server.api.node as StubbedNodeApi; - - nodeStub.getPeers.withArgs(["connected"], undefined).resolves([ - { - lastSeenP2pAddress: "/ip4/127.0.0.1/tcp/36000", - direction: "inbound", - enr: "enr-", - peerId: "16", - state: "connected", - }, - ]); - const response = await supertest(restApi.server.server) - .get(getPeers.url) - .query({state: "connected"}) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.empty; - expect((response.body as ApiResponseBody).data.length).to.equal(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data[0].peer_id).to.equal("16"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/node/getSyncingStatus.test.ts b/packages/lodestar/test/unit/api/rest/node/getSyncingStatus.test.ts deleted file mode 100644 index 6af8e95482..0000000000 --- a/packages/lodestar/test/unit/api/rest/node/getSyncingStatus.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {getSyncingStatus} from "../../../../../src/api/rest/node/getSyncingStatus"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {StubbedNodeApi} from "../../../../utils/stub/nodeApi"; - -describe("rest - node - getSyncingStatus", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const nodeStub = restApi.server.api.node as StubbedNodeApi; - - nodeStub.getSyncingStatus.resolves({ - headSlot: BigInt(3), - syncDistance: BigInt(2), - }); - const response = await supertest(restApi.server.server) - .get(getSyncingStatus.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.empty; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.head_slot).to.equal("3"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.sync_distance).to.equal("2"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/node/getVersion.test.ts b/packages/lodestar/test/unit/api/rest/node/getVersion.test.ts deleted file mode 100644 index dd43545d24..0000000000 --- a/packages/lodestar/test/unit/api/rest/node/getVersion.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; - -import {getNodeVersion} from "../../../../../src/api/rest/node/getNodeVersion"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {StubbedNodeApi} from "../../../../utils/stub/nodeApi"; - -describe("rest - node - getNodeVersion", function () { - it("should succeed", async function () { - const restApi = await setupRestApiTestServer(); - const nodeStub = restApi.server.api.node as StubbedNodeApi; - - nodeStub.getVersion.resolves("test"); - const response = await supertest(restApi.server.server) - .get(getNodeVersion.url) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.empty; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.body.data.version).to.equal("test"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/utils.ts b/packages/lodestar/test/unit/api/rest/utils.ts deleted file mode 100644 index 39bc6466bf..0000000000 --- a/packages/lodestar/test/unit/api/rest/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function urlJoin(...args: string[]): string { - return ( - args - .join("/") - .replace(/([^:]\/)\/+/g, "$1") - // Remove duplicate slashes in the front - .replace(/^(\/)+/, "/") - ); -} - -export type ApiResponseBody = {data: [JSON]}; diff --git a/packages/lodestar/test/unit/api/rest/validator/getAggregatedAttestation.test.ts b/packages/lodestar/test/unit/api/rest/validator/getAggregatedAttestation.test.ts deleted file mode 100644 index 95848e1666..0000000000 --- a/packages/lodestar/test/unit/api/rest/validator/getAggregatedAttestation.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {toHexString} from "@chainsafe/ssz"; -import {expect} from "chai"; -import supertest from "supertest"; -import {getAggregatedAttestation} from "../../../../../src/api/rest/validator/getAggregatedAttestation"; -import {generateEmptyAttestation} from "../../../../utils/attestation"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi, ValidatorApi} from "../../../../../src/api"; - -describe("rest - validator - getAggregatedAttestation", function () { - let restApi: RestApi; - let validatorStub: SinonStubbedInstance; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - validatorStub = restApi.server.api.validator as SinonStubbedInstance; - }); - - it("should succeed", async function () { - const root = config.types.Root.defaultValue(); - validatorStub.getAggregatedAttestation.resolves(generateEmptyAttestation()); - const response = await supertest(restApi.server.server) - .get(getAggregatedAttestation.url) - .query({ - // eslint-disable-next-line @typescript-eslint/naming-convention - attestation_data_root: toHexString(root), - slot: 0, - }) - .expect(200); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect(validatorStub.getAggregatedAttestation.withArgs(root, 0).calledOnce).to.be.true; - }); - - it("missing param", async function () { - validatorStub.getAggregatedAttestation.resolves(); - await supertest(restApi.server.server) - .get(getAggregatedAttestation.url) - .query({ - slot: 0, - }) - .expect(400); - expect(validatorStub.getAggregatedAttestation.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/validator/getAttesterDuties.test.ts b/packages/lodestar/test/unit/api/rest/validator/getAttesterDuties.test.ts deleted file mode 100644 index 79eeb5e714..0000000000 --- a/packages/lodestar/test/unit/api/rest/validator/getAttesterDuties.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import supertest from "supertest"; -import {getAttesterDuties} from "../../../../../src/api/rest/validator/duties/getAttesterDuties"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi, ValidatorApi} from "../../../../../src/api"; -import {ZERO_HASH} from "../../../../../src/constants"; - -describe("rest - validator - getAttesterDuties", function () { - let restApi: RestApi; - let validatorStub: SinonStubbedInstance; - - before(async function () { - restApi = await setupRestApiTestServer(); - validatorStub = restApi.server.api.validator as SinonStubbedInstance; - }); - - it("should succeed", async function () { - validatorStub.getAttesterDuties.resolves({ - dependentRoot: ZERO_HASH, - data: [config.types.phase0.AttesterDuty.defaultValue(), config.types.phase0.AttesterDuty.defaultValue()], - }); - const response = await supertest(restApi.server.server) - .post(getAttesterDuties.url.replace(":epoch", "0")) - .send(["1", "4"]) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.be.instanceOf(Array); - expect((response.body as ApiResponseBody).data).to.have.length(2); - expect(validatorStub.getAttesterDuties.withArgs(0, [1, 4]).calledOnce).to.be.true; - }); - - it("invalid epoch", async function () { - validatorStub.getAttesterDuties.resolves({ - dependentRoot: ZERO_HASH, - data: [config.types.phase0.AttesterDuty.defaultValue(), config.types.phase0.AttesterDuty.defaultValue()], - }); - await supertest(restApi.server.server) - .post(getAttesterDuties.url.replace(":epoch", "a")) - .send(["1", "4"]) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it("no validator indices", async function () { - validatorStub.getAttesterDuties.resolves({ - dependentRoot: ZERO_HASH, - data: [config.types.phase0.AttesterDuty.defaultValue(), config.types.phase0.AttesterDuty.defaultValue()], - }); - await supertest(restApi.server.server) - .post(getAttesterDuties.url.replace(":epoch", "1")) - .send([]) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it("invalid validator index", async function () { - validatorStub.getAttesterDuties.resolves({ - dependentRoot: ZERO_HASH, - data: [config.types.phase0.AttesterDuty.defaultValue(), config.types.phase0.AttesterDuty.defaultValue()], - }); - await supertest(restApi.server.server) - .post(getAttesterDuties.url.replace(":epoch", "1")) - .send([1, "a"]) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/validator/getProposerDuties.test.ts b/packages/lodestar/test/unit/api/rest/validator/getProposerDuties.test.ts deleted file mode 100644 index b88ecf6d8c..0000000000 --- a/packages/lodestar/test/unit/api/rest/validator/getProposerDuties.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {config} from "@chainsafe/lodestar-config/minimal"; -import {expect} from "chai"; -import supertest from "supertest"; -import {getProposerDuties} from "../../../../../src/api/rest/validator/duties/getProposerDuties"; -import {setupRestApiTestServer} from "../index.test"; -import {RestApi, ValidatorApi} from "../../../../../src/api"; -import {SinonStubbedInstance} from "sinon"; -import {ProposerDuty} from "@chainsafe/lodestar-types/phase0"; -import {ZERO_HASH} from "../../../../../src/constants"; - -describe("rest - validator - getProposerDuties", function () { - let restApi: RestApi; - let validatorStub: SinonStubbedInstance; - - before(async function () { - restApi = await setupRestApiTestServer(); - validatorStub = restApi.server.api.validator as SinonStubbedInstance; - }); - - it("should succeed", async function () { - validatorStub.getProposerDuties.resolves({ - dependentRoot: ZERO_HASH, - data: [config.types.phase0.ProposerDuty.defaultValue(), config.types.phase0.ProposerDuty.defaultValue()], - }); - const response = await supertest(restApi.server.server) - .get(getProposerDuties.url.replace(":epoch", "1")) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as {data: ProposerDuty}).data).to.be.instanceOf(Array); - expect((response.body as {data: ProposerDuty}).data).to.have.length(2); - expect(validatorStub.getProposerDuties.withArgs(1).calledOnce).to.be.true; - }); - - it("invalid epoch", async function () { - await supertest(restApi.server.server) - .get(getProposerDuties.url.replace(":epoch", "a")) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/validator/prepareBeaconCommitteeSubnet.test.ts b/packages/lodestar/test/unit/api/rest/validator/prepareBeaconCommitteeSubnet.test.ts deleted file mode 100644 index 092bccebab..0000000000 --- a/packages/lodestar/test/unit/api/rest/validator/prepareBeaconCommitteeSubnet.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {setupRestApiTestServer} from "../index.test"; -import {prepareBeaconCommitteeSubnet} from "../../../../../src/api/rest/validator/prepareBeaconCommitteeSubnet"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi, ValidatorApi} from "../../../../../src/api"; - -describe("rest - validator - prepareBeaconCommitteeSubnet", function () { - let restApi: RestApi; - let validatorStub: SinonStubbedInstance; - - before(async function () { - restApi = await setupRestApiTestServer(); - validatorStub = restApi.server.api.validator as SinonStubbedInstance; - }); - - it("should succeed", async function () { - validatorStub.prepareBeaconCommitteeSubnet.resolves(); - await supertest(restApi.server.server) - .post(prepareBeaconCommitteeSubnet.url) - .send([ - { - // eslint-disable-next-line @typescript-eslint/naming-convention - validator_index: 1, - // eslint-disable-next-line @typescript-eslint/naming-convention - committee_index: 2, - // eslint-disable-next-line @typescript-eslint/naming-convention - committees_at_slot: 64, - slot: 0, - // eslint-disable-next-line @typescript-eslint/naming-convention - is_aggregator: false, - }, - ]) - .expect(200); - expect( - validatorStub.prepareBeaconCommitteeSubnet.withArgs([ - { - validatorIndex: 1, - committeeIndex: 2, - committeesAtSlot: 64, - slot: 0, - isAggregator: false, - }, - ]).calledOnce - ).to.be.true; - }); - - it("missing param", async function () { - await supertest(restApi.server.server) - .post(prepareBeaconCommitteeSubnet.url) - .send([ - { - slot: 0, - }, - ]) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/validator/produceAttestationData.test.ts b/packages/lodestar/test/unit/api/rest/validator/produceAttestationData.test.ts deleted file mode 100644 index 34b967e6cf..0000000000 --- a/packages/lodestar/test/unit/api/rest/validator/produceAttestationData.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {expect} from "chai"; -import supertest from "supertest"; -import {produceAttestationData} from "../../../../../src/api/rest/validator/produceAttestationData"; -import {generateEmptyAttestation} from "../../../../utils/attestation"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi, ValidatorApi} from "../../../../../src/api"; - -describe("rest - validator - produceAttestationData", function () { - let restApi: RestApi; - let validatorStub: SinonStubbedInstance; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - validatorStub = restApi.server.api.validator as SinonStubbedInstance; - }); - - it("should succeed", async function () { - validatorStub.produceAttestationData.resolves(generateEmptyAttestation().data); - const response = await supertest(restApi.server.server) - .get(produceAttestationData.url) - .query({ - // eslint-disable-next-line @typescript-eslint/naming-convention - committee_index: 1, - slot: 0, - }) - .expect(200); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect(validatorStub.produceAttestationData.withArgs(1, 0).calledOnce).to.be.true; - }); - - it("missing param", async function () { - validatorStub.getAggregatedAttestation.resolves(); - await supertest(restApi.server.server) - .get(produceAttestationData.url) - .query({ - slot: 0, - }) - .expect(400); - expect(validatorStub.produceAttestationData.notCalled).to.be.true; - }); -}); diff --git a/packages/lodestar/test/unit/api/rest/validator/produceBlock.test.ts b/packages/lodestar/test/unit/api/rest/validator/produceBlock.test.ts deleted file mode 100644 index a09e47385a..0000000000 --- a/packages/lodestar/test/unit/api/rest/validator/produceBlock.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {toHexString} from "@chainsafe/ssz"; -import {expect} from "chai"; -import supertest from "supertest"; -import {produceBlock} from "../../../../../src/api/rest/validator/produceBlock"; -import {generateEmptyBlock} from "../../../../utils/block"; -import {ApiResponseBody} from "../utils"; -import {setupRestApiTestServer} from "../index.test"; -import {SinonStubbedInstance} from "sinon"; -import {RestApi, ValidatorApi} from "../../../../../src/api"; - -describe("rest - validator - produceBlock", function () { - let restApi: RestApi; - let validatorStub: SinonStubbedInstance; - - beforeEach(async function () { - restApi = await setupRestApiTestServer(); - validatorStub = restApi.server.api.validator as SinonStubbedInstance; - }); - - it("should succeed", async function () { - validatorStub.produceBlock.resolves(generateEmptyBlock()); - const response = await supertest(restApi.server.server) - .get(produceBlock.url.replace(":slot", "5")) - .query({ - // eslint-disable-next-line @typescript-eslint/naming-convention - randao_reveal: toHexString(Buffer.alloc(32, 1)), - graffiti: "0x2123", - }) - .expect(200) - .expect("Content-Type", "application/json; charset=utf-8"); - expect((response.body as ApiResponseBody).data).to.not.be.undefined; - expect(validatorStub.produceBlock.withArgs(5, Buffer.alloc(32, 1), "0x2123")); - }); - - it("missing randao reveal", async function () { - await supertest(restApi.server.server) - .get(produceBlock.url.replace(":slot", "5")) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); - - it("invalid slot", async function () { - await supertest(restApi.server.server) - .get(produceBlock.url.replace(":slot", "0")) - .query({ - // eslint-disable-next-line @typescript-eslint/naming-convention - randao_reveal: toHexString(Buffer.alloc(32, 1)), - }) - .expect(400) - .expect("Content-Type", "application/json; charset=utf-8"); - }); -}); diff --git a/packages/lodestar/test/utils/api.ts b/packages/lodestar/test/utils/api.ts deleted file mode 100644 index 1d12844daf..0000000000 --- a/packages/lodestar/test/utils/api.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {phase0} from "@chainsafe/lodestar-types"; -import deepmerge from "deepmerge"; -import {blockToHeader} from "@chainsafe/lodestar-beacon-state-transition"; -import {config} from "@chainsafe/lodestar-config/minimal"; -import {generateEmptySignedBlock} from "./block"; -import {isPlainObject} from "@chainsafe/lodestar-utils"; - -export function generateSignedBeaconHeaderResponse( - override: Partial = {} -): phase0.SignedBeaconHeaderResponse { - const signedBlock = generateEmptySignedBlock(); - return deepmerge( - { - canonical: true, - root: Buffer.alloc(32, 0), - header: { - message: blockToHeader(config, signedBlock.message), - signature: signedBlock.signature, - }, - }, - override, - {isMergeableObject: isPlainObject} - ); -} diff --git a/packages/lodestar/test/utils/node/validator.ts b/packages/lodestar/test/utils/node/validator.ts index 499b10da37..5b7d5292f7 100644 --- a/packages/lodestar/test/utils/node/validator.ts +++ b/packages/lodestar/test/utils/node/validator.ts @@ -1,12 +1,9 @@ import tmp from "tmp"; import {LevelDbController} from "@chainsafe/lodestar-db"; -import {ILogger, LogLevel} from "@chainsafe/lodestar-utils"; import {interopSecretKey} from "@chainsafe/lodestar-beacon-state-transition"; -import {IApiClient, SlashingProtection, Validator} from "@chainsafe/lodestar-validator"; -import {Eth1ForBlockProductionDisabled} from "../../../src/eth1"; +import {SlashingProtection, Validator} from "@chainsafe/lodestar-validator"; import {BeaconNode} from "../../../src/node"; import {testLogger, TestLoggerOpts} from "../logger"; -import {Api} from "../../../src/api/impl"; export async function getAndInitDevValidators({ node, @@ -33,7 +30,7 @@ export async function getAndInitDevValidators({ vcs.push( Validator.initializeFromBeaconNode({ config: node.config, - api: useRestApi ? getNodeApiUrl(node) : getApiInstance(node, logger), + api: useRestApi ? getNodeApiUrl(node) : node.api, slashingProtection: new SlashingProtection({ config: node.config, controller: new LevelDbController({name: tmpDir.name}, {logger}), @@ -52,15 +49,3 @@ function getNodeApiUrl(node: BeaconNode): string { const port = node.opts.api.rest.port || 9596; return `http://${host}:${port}`; } - -function getApiInstance(node: BeaconNode, parentLogger: ILogger): IApiClient { - return new Api( - {}, - { - ...node, - logger: parentLogger.child({module: "api", level: LogLevel.warn}), - eth1: new Eth1ForBlockProductionDisabled(), - } - // TODO: Review why this casting is necessary - ) as IApiClient; -} diff --git a/packages/lodestar/test/utils/stub/api.ts b/packages/lodestar/test/utils/stub/api.ts deleted file mode 100644 index 236acef9b9..0000000000 --- a/packages/lodestar/test/utils/stub/api.ts +++ /dev/null @@ -1,37 +0,0 @@ -import sinon, {SinonSandbox, SinonStubbedInstance} from "sinon"; - -import {IApi, ValidatorApi} from "../../../src/api/impl"; - -import {StubbedNodeApi} from "./nodeApi"; -import {StubbedBeaconApi} from "./beaconApi"; -import {EventsApi} from "../../../src/api/impl/events"; -import {DebugApi} from "../../../src/api/impl/debug"; -import {DebugBeaconApi} from "../../../src/api/impl/debug/beacon"; -import {ConfigApi} from "../../../src/api/impl/config"; -import {LightclientApi} from "../../../src/api/impl/lightclient"; -import {LodestarApi} from "../../../src/api/impl/lodestar"; -import {StubbedConfigApi} from "./configApi"; - -export class StubbedApi implements SinonStubbedInstance { - beacon: StubbedBeaconApi; - node: StubbedNodeApi; - validator: SinonStubbedInstance; - events: SinonStubbedInstance; - debug: SinonStubbedInstance; - config: SinonStubbedInstance; - lightclient: SinonStubbedInstance; - lodestar: SinonStubbedInstance; - - constructor(sandbox: SinonSandbox = sinon) { - this.beacon = new StubbedBeaconApi(sandbox); - this.node = new StubbedNodeApi(sandbox); - this.validator = sandbox.createStubInstance(ValidatorApi); - this.events = sandbox.createStubInstance(EventsApi); - const debugBeacon = sandbox.createStubInstance(DebugBeaconApi); - this.debug = sandbox.createStubInstance(DebugApi); - this.debug.beacon = debugBeacon; - this.config = new StubbedConfigApi(sandbox); - this.lightclient = sandbox.createStubInstance(LightclientApi); - this.lodestar = sandbox.createStubInstance(LodestarApi); - } -} diff --git a/packages/lodestar/test/utils/stub/beaconApi.ts b/packages/lodestar/test/utils/stub/beaconApi.ts deleted file mode 100644 index a2264c891d..0000000000 --- a/packages/lodestar/test/utils/stub/beaconApi.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {IBeaconApi} from "../../../src/api/impl/beacon"; -import Sinon, {SinonSandbox, SinonStubbedInstance} from "sinon"; -import {BeaconBlockApi, IBeaconBlocksApi} from "../../../src/api/impl/beacon/blocks"; -import {ApiNamespace} from "../../../src/api"; -import {BeaconPoolApi, IBeaconPoolApi} from "../../../src/api/impl/beacon/pool"; -import {IBeaconStateApi} from "../../../src/api/impl/beacon/state/interface"; -import {BeaconStateApi} from "../../../src/api/impl/beacon/state/state"; - -export class StubbedBeaconApi implements SinonStubbedInstance { - blocks: SinonStubbedInstance; - state: SinonStubbedInstance; - pool: SinonStubbedInstance; - getBlockStream: Sinon.SinonStubbedMember; - getGenesis: Sinon.SinonStubbedMember; - namespace: ApiNamespace.BEACON = ApiNamespace.BEACON; - - constructor(sandbox: SinonSandbox = Sinon) { - this.state = sandbox.createStubInstance(BeaconStateApi); - this.blocks = sandbox.createStubInstance(BeaconBlockApi); - this.pool = sandbox.createStubInstance(BeaconPoolApi); - this.getBlockStream = sandbox.stub(); - this.getGenesis = sandbox.stub(); - } -} diff --git a/packages/lodestar/test/utils/stub/configApi.ts b/packages/lodestar/test/utils/stub/configApi.ts deleted file mode 100644 index c130e6edd1..0000000000 --- a/packages/lodestar/test/utils/stub/configApi.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Sinon, {SinonSandbox, SinonStubbedInstance} from "sinon"; -import {ApiNamespace} from "../../../src/api"; -import {IConfigApi} from "../../../src/api/impl/config"; - -export class StubbedConfigApi implements SinonStubbedInstance { - namespace: ApiNamespace.CONFIG = ApiNamespace.CONFIG; - - getDepositContract: Sinon.SinonStubbedMember; - getForkSchedule: Sinon.SinonStubbedMember; - getSpec: Sinon.SinonStubbedMember; - - constructor(sandbox: SinonSandbox = Sinon) { - this.getDepositContract = sandbox.stub(); - this.getForkSchedule = sandbox.stub(); - this.getSpec = sandbox.stub(); - } -} diff --git a/packages/lodestar/test/utils/stub/lodestarApi.ts b/packages/lodestar/test/utils/stub/lodestarApi.ts deleted file mode 100644 index 4aba743ad3..0000000000 --- a/packages/lodestar/test/utils/stub/lodestarApi.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Sinon, {SinonSandbox, SinonStubbedInstance} from "sinon"; -import {ApiNamespace} from "../../../src/api"; -import {ILodestarApi} from "../../../src/api/impl/lodestar"; - -export class StubbedLodestarApi implements SinonStubbedInstance { - namespace: ApiNamespace.LODESTAR = ApiNamespace.LODESTAR; - - getWtfNode: Sinon.SinonStubbedMember; - getLatestWeakSubjectivityCheckpointEpoch: Sinon.SinonStubbedMember< - ILodestarApi["getLatestWeakSubjectivityCheckpointEpoch"] - >; - getSyncChainsDebugState: Sinon.SinonStubbedMember; - - constructor(sandbox: SinonSandbox = Sinon) { - this.getWtfNode = sandbox.stub(); - this.getLatestWeakSubjectivityCheckpointEpoch = sandbox.stub(); - this.getSyncChainsDebugState = sandbox.stub(); - } -} diff --git a/packages/lodestar/test/utils/stub/nodeApi.ts b/packages/lodestar/test/utils/stub/nodeApi.ts deleted file mode 100644 index 03b0535b3c..0000000000 --- a/packages/lodestar/test/utils/stub/nodeApi.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Sinon, {SinonSandbox, SinonStubbedInstance} from "sinon"; -import {ApiNamespace} from "../../../src/api"; -import {INodeApi} from "../../../src/api/impl/node"; - -export class StubbedNodeApi implements SinonStubbedInstance { - namespace: ApiNamespace.NODE = ApiNamespace.NODE; - - getNodeIdentity: Sinon.SinonStubbedMember; - getNodeStatus: Sinon.SinonStubbedMember; - getPeer: Sinon.SinonStubbedMember; - getPeers: Sinon.SinonStubbedMember; - getSyncingStatus: Sinon.SinonStubbedMember; - getVersion: Sinon.SinonStubbedMember; - - constructor(sandbox: SinonSandbox = Sinon) { - this.getNodeIdentity = sandbox.stub(); - this.getNodeStatus = sandbox.stub(); - this.getPeer = sandbox.stub(); - this.getPeers = sandbox.stub(); - this.getSyncingStatus = sandbox.stub(); - this.getVersion = sandbox.stub(); - } -} diff --git a/packages/lodestar/types/fastify/index.d.ts b/packages/lodestar/types/fastify/index.d.ts deleted file mode 100644 index 4911f126a0..0000000000 --- a/packages/lodestar/types/fastify/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import "fastify"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {IApi} from "../../src/api/impl"; - -declare module "fastify" { - // eslint-disable-next-line @typescript-eslint/interface-name-prefix - interface FastifyInstance { - //decorated properties on fastify server - config: IBeaconConfig; - api: IApi; - } -} diff --git a/packages/types/src/altair/sszTypes.ts b/packages/types/src/altair/sszTypes.ts index 4f3bec0920..7dfd7f9302 100644 --- a/packages/types/src/altair/sszTypes.ts +++ b/packages/types/src/altair/sszTypes.ts @@ -23,8 +23,6 @@ export function getAltairTypes(params: IBeaconParams, primitive: PrimitiveSSZTyp Number64, Uint64, Slot, - Epoch, - CommitteeIndex, SubCommitteeIndex, ValidatorIndex, Gwei, @@ -111,36 +109,6 @@ export function getAltairTypes(params: IBeaconParams, primitive: PrimitiveSSZTyp }, }); - const SyncCommitteeSubscription = new ContainerType({ - fields: { - validatorIndex: ValidatorIndex, - syncCommitteeIndices: new ListType({elementType: CommitteeIndex, limit: params.SYNC_COMMITTEE_SIZE}), - untilEpoch: Epoch, - }, - }); - - const SyncCommitteeByValidatorIndices = new ContainerType({ - fields: { - validators: new ListType({elementType: ValidatorIndex, limit: params.SYNC_COMMITTEE_SIZE}), - validatorAggregates: new ListType({elementType: ValidatorIndex, limit: params.SYNC_COMMITTEE_SIZE}), - }, - }); - - const SyncDuty = new ContainerType({ - fields: { - pubkey: BLSPubkey, - validatorIndex: ValidatorIndex, - validatorSyncCommitteeIndices: new ListType({elementType: Number64, limit: params.SYNC_COMMITTEE_SIZE}), - }, - }); - - const SyncDutiesApi = new ContainerType({ - fields: { - data: new ListType({elementType: SyncDuty, limit: params.SYNC_COMMITTEE_SIZE}), - dependentRoot: Root, - }, - }); - // Re-declare with the new expanded type const HistoricalBlockRoots = new VectorType>({ elementType: new RootType({expandedType: () => typesRef.get().BeaconBlock}), @@ -278,10 +246,6 @@ export function getAltairTypes(params: IBeaconParams, primitive: PrimitiveSSZTyp SignedContributionAndProof, SyncCommitteeSigningData, SyncAggregate, - SyncCommitteeSubscription, - SyncCommitteeByValidatorIndices, - SyncDuty, - SyncDutiesApi, BeaconBlockBody, BeaconBlock, SignedBeaconBlock, diff --git a/packages/types/src/altair/types/wire.ts b/packages/types/src/altair/types/wire.ts index a31db40ba1..f9409721f1 100644 --- a/packages/types/src/altair/types/wire.ts +++ b/packages/types/src/altair/types/wire.ts @@ -1,6 +1,6 @@ import {BitVector} from "@chainsafe/ssz"; import {AttestationSubnets} from "../../phase0/types/misc"; -import {BLSPubkey, Epoch, Root, Uint64, ValidatorIndex} from "../../primitive/types"; +import {Uint64} from "../../primitive/types"; export type SyncSubnets = BitVector; @@ -9,36 +9,3 @@ export interface Metadata { attnets: AttestationSubnets; syncnets: SyncSubnets; } - -/** - * From https://github.com/ethereum/eth2.0-APIs/pull/136 - */ -export interface SyncCommitteeSubscription { - validatorIndex: ValidatorIndex; - syncCommitteeIndices: number[]; - untilEpoch: Epoch; -} - -export interface SyncCommitteeByValidatorIndices { - /** all of the validator indices in the current sync committee */ - validators: ValidatorIndex[]; - // TODO: This property will likely be deprecated - /** Subcommittee slices of the current sync committee */ - validatorAggregates: ValidatorIndex[]; -} - -/** - * From https://github.com/ethereum/eth2.0-APIs/pull/134 - */ -export interface SyncDuty { - pubkey: BLSPubkey; - /** Index of validator in validator registry. */ - validatorIndex: ValidatorIndex; - /** The indices of the validator in the sync committee. */ - validatorSyncCommitteeIndices: number[]; -} - -export interface SyncDutiesApi { - data: SyncDuty[]; - dependentRoot: Root; -} diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 6013e0ed68..eb477eed74 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -7,9 +7,7 @@ import { } from "@chainsafe/lodestar-params"; import {BitListType, BitVectorType, ContainerType, List, ListType, RootType, Vector, VectorType} from "@chainsafe/ssz"; import {PrimitiveSSZTypes} from "../primitive"; -import {StringType} from "../utils/StringType"; import {LazyVariable} from "../utils/lazyVar"; -import {ValidatorStatus} from "./types"; import * as phase0 from "./types"; // Interface is defined in the return of getPhase0Types(), to de-duplicate info @@ -169,13 +167,6 @@ export function getPhase0Types(params: IPhase0Params, primitive: PrimitiveSSZTyp }, }); - const SlotRoot = new ContainerType({ - fields: { - slot: Slot, - root: Root, - }, - }); - const Validator = new ContainerType({ fields: { pubkey: BLSPubkey, @@ -414,94 +405,6 @@ export function getPhase0Types(params: IPhase0Params, primitive: PrimitiveSSZTyp // Api types // ========= - const AttesterDuty = new ContainerType({ - fields: { - pubkey: BLSPubkey, - validatorIndex: ValidatorIndex, - committeeIndex: CommitteeIndex, - committeeLength: Number64, - committeesAtSlot: Number64, - validatorCommitteeIndex: Number64, - slot: Slot, - }, - }); - - const AttesterDutiesApi = new ContainerType({ - fields: { - data: new ListType({elementType: AttesterDuty, limit: params.VALIDATOR_REGISTRY_LIMIT}), - dependentRoot: Root, - }, - }); - - const BeaconCommitteeResponse = new ContainerType({ - fields: { - index: CommitteeIndex, - slot: Slot, - validators: CommitteeIndices, - }, - }); - - const BeaconCommitteeSubscription = new ContainerType({ - fields: { - validatorIndex: ValidatorIndex, - committeeIndex: CommitteeIndex, - committeesAtSlot: Slot, - slot: Slot, - isAggregator: Boolean, - }, - }); - - const BlockEventPayload = new ContainerType({ - fields: { - slot: Slot, - block: Root, - }, - }); - - const ChainHead = new ContainerType({ - fields: { - slot: Slot, - block: Root, - state: Root, - epochTransition: Boolean, - }, - }); - - const ChainReorg = new ContainerType({ - fields: { - slot: Slot, - depth: Number64, - oldHeadBlock: Root, - newHeadBlock: Root, - oldHeadState: Root, - newHeadState: Root, - epoch: Epoch, - }, - }); - - const Contract = new ContainerType({ - fields: { - chainId: Number64, - address: Bytes32, - }, - }); - - const FinalityCheckpoints = new ContainerType({ - fields: { - previousJustified: Checkpoint, - currentJustified: Checkpoint, - finalized: Checkpoint, - }, - }); - - const FinalizedCheckpoint = new ContainerType({ - fields: { - block: Root, - state: Root, - epoch: Epoch, - }, - }); - const Genesis = new ContainerType({ fields: { genesisValidatorsRoot: Root, @@ -510,61 +413,6 @@ export function getPhase0Types(params: IPhase0Params, primitive: PrimitiveSSZTyp }, }); - const ProposerDuty = new ContainerType({ - fields: { - slot: Slot, - validatorIndex: ValidatorIndex, - pubkey: BLSPubkey, - }, - }); - - const ProposerDutiesApi = new ContainerType({ - fields: { - data: new ListType({elementType: ProposerDuty, limit: params.VALIDATOR_REGISTRY_LIMIT}), - dependentRoot: Root, - }, - }); - - const SignedBeaconHeaderResponse = new ContainerType({ - fields: { - root: Root, - canonical: Boolean, - header: SignedBeaconBlockHeader, - }, - }); - - const SubscribeToCommitteeSubnetPayload = new ContainerType({ - fields: { - slot: Slot, - slotSignature: BLSSignature, - attestationCommitteeIndex: CommitteeIndex, - aggregatorPubkey: BLSPubkey, - }, - }); - - const SyncingStatus = new ContainerType({ - fields: { - headSlot: Uint64, - syncDistance: Uint64, - }, - }); - - const ValidatorBalance = new ContainerType({ - fields: { - index: ValidatorIndex, - balance: Gwei, - }, - }); - - const ValidatorResponse = new ContainerType({ - fields: { - index: ValidatorIndex, - balance: Gwei, - status: new StringType(), - validator: Validator, - }, - }); - // Non-speced types // ================ @@ -599,7 +447,6 @@ export function getPhase0Types(params: IPhase0Params, primitive: PrimitiveSSZTyp ForkData, ENRForkID, Checkpoint, - SlotRoot, Validator, AttestationData, CommitteeIndices, @@ -638,7 +485,6 @@ export function getPhase0Types(params: IPhase0Params, primitive: PrimitiveSSZTyp SignedAggregateAndProof, CommitteeAssignment, // Validator slashing protection - // wire Status, Goodbye, @@ -647,24 +493,7 @@ export function getPhase0Types(params: IPhase0Params, primitive: PrimitiveSSZTyp BeaconBlocksByRangeRequest, BeaconBlocksByRootRequest, // api - SignedBeaconHeaderResponse, - SubscribeToCommitteeSubnetPayload, - SyncingStatus, - AttesterDuty, - ProposerDuty, - AttesterDutiesApi, - ProposerDutiesApi, - BeaconCommitteeSubscription, Genesis, - ChainHead, - BlockEventPayload, - FinalizedCheckpoint, - ChainReorg, - ValidatorBalance, - ValidatorResponse, - FinalityCheckpoints, - BeaconCommitteeResponse, - Contract, // Non-speced types SlashingProtectionBlock, SlashingProtectionAttestation, diff --git a/packages/types/src/phase0/types/api.ts b/packages/types/src/phase0/types/api.ts index f73f30e7f1..2b0bb7b4ad 100644 --- a/packages/types/src/phase0/types/api.ts +++ b/packages/types/src/phase0/types/api.ts @@ -1,151 +1,7 @@ -import {Checkpoint, SignedBeaconBlockHeader, Validator} from "./misc"; -import { - BLSPubkey, - BLSSignature, - Bytes20, - CommitteeIndex, - Epoch, - Gwei, - Number64, - Root, - Slot, - Uint64, - ValidatorIndex, - Version, -} from "../../primitive/types"; -import {List} from "@chainsafe/ssz"; - -export interface SignedBeaconHeaderResponse { - root: Root; - canonical: boolean; - header: SignedBeaconBlockHeader; -} - -export interface SubscribeToCommitteeSubnetPayload { - slot: Slot; - slotSignature: BLSSignature; - attestationCommitteeIndex: CommitteeIndex; - aggregatorPubkey: BLSPubkey; -} - -export interface AttesterDuty { - // The validator's public key, uniquely identifying them - pubkey: BLSPubkey; - // Index of validator in validator registry - validatorIndex: ValidatorIndex; - committeeIndex: CommitteeIndex; - // Number of validators in committee - committeeLength: Number64; - // Number of committees at the provided slot - committeesAtSlot: Number64; - // Index of validator in committee - validatorCommitteeIndex: Number64; - // The slot at which the validator must attest. - slot: Slot; -} - -export interface ProposerDuty { - slot: Slot; - validatorIndex: ValidatorIndex; - pubkey: BLSPubkey; -} - -export interface AttesterDutiesApi { - data: AttesterDuty[]; - dependentRoot: Root; -} - -export interface ProposerDutiesApi { - data: ProposerDuty[]; - dependentRoot: Root; -} - -export interface BeaconCommitteeSubscription { - validatorIndex: ValidatorIndex; - committeeIndex: number; - committeesAtSlot: number; - slot: Slot; - isAggregator: boolean; -} - -export interface SyncingStatus { - // Head slot node is trying to reach - headSlot: Uint64; - // How many slots node needs to process to reach head. 0 if synced. - syncDistance: Uint64; -} +import {Root, Uint64, Version} from "../../primitive/types"; export interface Genesis { genesisTime: Uint64; genesisValidatorsRoot: Root; genesisForkVersion: Version; } - -export interface ChainHead { - slot: Slot; - block: Root; - state: Root; - epochTransition: boolean; -} - -export interface BlockEventPayload { - slot: Slot; - block: Root; -} - -export interface FinalizedCheckpoint { - block: Root; - state: Root; - epoch: Epoch; -} - -export interface ChainReorg { - slot: Slot; - depth: Number64; - oldHeadBlock: Root; - newHeadBlock: Root; - oldHeadState: Root; - newHeadState: Root; - epoch: Epoch; -} - -export interface FinalityCheckpoints { - previousJustified: Checkpoint; - currentJustified: Checkpoint; - finalized: Checkpoint; -} - -export interface ValidatorBalance { - index: ValidatorIndex; - balance: Gwei; -} - -export interface BeaconCommitteeResponse { - index: CommitteeIndex; - slot: Slot; - validators: List; -} - -export enum ValidatorStatus { - PENDING_INITIALIZED = "pending_initialized", - PENDING_QUEUED = "pending_queued", - ACTIVE_ONGOING = "active_ongoing", - ACTIVE_EXITING = "active_exiting", - ACTIVE_SLASHED = "active_slashed", - EXITED_UNSLASHED = "exited_unslashed", - EXITED_SLASHED = "exited_slashed", - WITHDRAWAL_POSSIBLE = "withdrawal_possible", - WITHDRAWAL_DONE = "withdrawal_done", -} - -export interface ValidatorResponse { - index: ValidatorIndex; - balance: Gwei; - status: ValidatorStatus; - validator: Validator; -} - -export interface Contract { - chainId: Number64; - address: Bytes20; -} diff --git a/packages/types/src/phase0/types/misc.ts b/packages/types/src/phase0/types/misc.ts index 8f7db12d5c..6fa56af869 100644 --- a/packages/types/src/phase0/types/misc.ts +++ b/packages/types/src/phase0/types/misc.ts @@ -50,11 +50,6 @@ export interface Checkpoint { root: Root; } -export interface SlotRoot { - slot: Slot; - root: Root; -} - export interface Validator { // BLS public key pubkey: BLSPubkey; diff --git a/packages/utils/package.json b/packages/utils/package.json index 3daf17180e..79eb177d65 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -41,7 +41,6 @@ "any-signal": "2.1.1", "bigint-buffer": "^1.1.5", "chalk": "^2.4.2", - "event-iterator": "^2.0.0", "js-yaml": "^3.13.1", "winston": "^3.3.3", "winston-transport": "^4.3.0" diff --git a/packages/utils/src/events/events.ts b/packages/utils/src/events/events.ts deleted file mode 100644 index e282d10109..0000000000 --- a/packages/utils/src/events/events.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {EventIterator} from "event-iterator"; -import {EventIteratorOptions, ListenHandler} from "event-iterator/src/event-iterator"; - -export interface IStoppableEventIterable extends AsyncIterable { - stop(): void; -} - -export class LodestarEventIterator implements IStoppableEventIterable { - [Symbol.asyncIterator]: () => AsyncIterator; - private stopCallback?: () => void; - - constructor(listenHandler: ListenHandler, options: Partial = {}) { - const handler: ListenHandler = (queue) => { - this.stopCallback = queue.stop; - return listenHandler(queue); - }; - const iterator = new EventIterator(handler, options); - this[Symbol.asyncIterator] = () => iterator[Symbol.asyncIterator](); - } - - stop(): void { - if (this.stopCallback) { - this.stopCallback(); - } - } -} diff --git a/packages/utils/src/events/index.ts b/packages/utils/src/events/index.ts deleted file mode 100644 index 1784004f01..0000000000 --- a/packages/utils/src/events/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./events"; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b5f471e2c0..4fbe38e12d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,3 @@ -export * from "./events"; export * from "./logger"; export * from "./yaml"; export * from "./assert"; diff --git a/packages/utils/src/objects.ts b/packages/utils/src/objects.ts index d2f5e15348..3e650562b0 100644 --- a/packages/utils/src/objects.ts +++ b/packages/utils/src/objects.ts @@ -47,16 +47,16 @@ export function mapValues( return output; } -export function objectToExpectedCase( - obj: Record, +export function objectToExpectedCase | Record[]>( + obj: T, expectedCase: "snake" | "camel" = "camel" -): Record { +): T { if (Array.isArray(obj)) { const newArr: unknown[] = []; for (let i = 0; i < obj.length; i++) { newArr[i] = objectToExpectedCase(obj[i], expectedCase); } - return (newArr as unknown) as Record; + return (newArr as unknown) as T; } if (Object(obj) === obj) { @@ -67,9 +67,12 @@ export function objectToExpectedCase( throw new Error(`object already has a ${newName} property`); } - newObj[newName] = objectToExpectedCase(obj[name] as Record, expectedCase); + newObj[newName] = objectToExpectedCase( + (obj as Record)[name] as Record, + expectedCase + ); } - return newObj; + return newObj as T; } return obj; diff --git a/packages/utils/src/yaml/index.ts b/packages/utils/src/yaml/index.ts index baaebf191b..ac0cb1394e 100644 --- a/packages/utils/src/yaml/index.ts +++ b/packages/utils/src/yaml/index.ts @@ -3,7 +3,7 @@ import {schema} from "./schema"; import {objectToExpectedCase} from "../objects"; export function loadYaml(yaml: string): Record { - return objectToExpectedCase(load(yaml, {schema})); + return objectToExpectedCase>(load(yaml, {schema})); } export function dumpYaml(yaml: unknown): string { diff --git a/packages/validator/package.json b/packages/validator/package.json index 993dafeda3..a9e1c4b6e3 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -45,6 +45,7 @@ ], "dependencies": { "@chainsafe/bls": "6.0.1", + "@chainsafe/lodestar-api": "^0.22.0", "@chainsafe/lodestar-beacon-state-transition": "^0.22.0", "@chainsafe/lodestar-config": "^0.22.0", "@chainsafe/lodestar-db": "^0.22.0", @@ -53,15 +54,10 @@ "@chainsafe/lodestar-utils": "^0.22.0", "@chainsafe/ssz": "^0.8.6", "abort-controller": "^3.0.0", - "axios": "^0.21.1", - "axios-mock-adapter": "^1.17.0", - "bigint-buffer": "^1.1.5", - "eventsource": "^1.0.7", - "strict-event-emitter-types": "^2.0.0" + "bigint-buffer": "^1.1.5" }, "devDependencies": { "@chainsafe/slashing-protection-interchange-tests": "^4.0.0", - "@types/eventsource": "^1.1.5", "bigint-buffer": "^1.1.5" } } diff --git a/packages/validator/src/api/index.ts b/packages/validator/src/api/index.ts deleted file mode 100644 index 95baea3ebd..0000000000 --- a/packages/validator/src/api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./instance"; -export * from "./rest"; -export * from "./interface"; diff --git a/packages/validator/src/api/instance.ts b/packages/validator/src/api/instance.ts deleted file mode 100644 index be11d0fc24..0000000000 --- a/packages/validator/src/api/instance.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {IApiClient, IApiClientValidator} from "./interface"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function ApiClientOverInstance(api: IApiClient): IApiClientValidator { - return { - beacon: api.beacon, - validator: api.validator, - events: api.events, - node: api.node, - config: api.config, - - url: "inmemory", - registerAbortSignal() { - // Does not support aborting - }, - }; -} diff --git a/packages/validator/src/api/interface.ts b/packages/validator/src/api/interface.ts deleted file mode 100644 index 06b92841fc..0000000000 --- a/packages/validator/src/api/interface.ts +++ /dev/null @@ -1,92 +0,0 @@ -import {EventEmitter} from "events"; -import StrictEventEmitter from "strict-event-emitter-types"; -import { - allForks, - phase0, - altair, - Epoch, - Slot, - Root, - ValidatorIndex, - BLSSignature, - CommitteeIndex, -} 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", - CHAIN_REORG = "chain_reorg", - HEAD = "head", -} - -export type BeaconBlockEvent = {type: typeof BeaconEventType.BLOCK; message: phase0.BlockEventPayload}; -export type BeaconChainReorgEvent = {type: typeof BeaconEventType.CHAIN_REORG; message: phase0.ChainReorg}; -export type HeadEvent = {type: typeof BeaconEventType.HEAD; message: phase0.ChainHead}; -export type BeaconEvent = BeaconBlockEvent | BeaconChainReorgEvent | HeadEvent; - -export interface IApiClientEvents { - [BeaconEventType.BLOCK]: (evt: BeaconBlockEvent["message"]) => void; - [BeaconEventType.CHAIN_REORG]: (evt: BeaconChainReorgEvent["message"]) => void; - [BeaconEventType.HEAD]: (evt: HeadEvent["message"]) => void; -} - -export type ApiClientEventEmitter = StrictEventEmitter; - -export interface IApiClient { - beacon: { - state: { - getFork(stateId: StateId): Promise; - getStateValidators(stateId: StateId, filters?: IValidatorFilters): Promise; - }; - blocks: { - getBlockRoot(blockId: BlockId): Promise; - publishBlock(block: allForks.SignedBeaconBlock): Promise; - }; - pool: { - submitAttestations(attestation: phase0.Attestation[]): Promise; - submitVoluntaryExit(signedVoluntaryExit: phase0.SignedVoluntaryExit): Promise; - submitSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise; - }; - getGenesis(): Promise; - }; - - config: { - getForkSchedule(): Promise; - }; - - node: { - getVersion(): Promise; - getSyncingStatus(): Promise; - }; - - events: { - getEventStream(topics: BeaconEventType[]): IStoppableEventIterable; - }; - - validator: { - getProposerDuties(epoch: Epoch): Promise; - getAttesterDuties(epoch: Epoch, validatorIndices: ValidatorIndex[]): Promise; - getSyncCommitteeDuties(epoch: number, validatorIndices: ValidatorIndex[]): Promise; - produceBlock(slot: Slot, randaoReveal: BLSSignature, graffiti: string): Promise; - produceAttestationData(index: CommitteeIndex, slot: Slot): Promise; - produceSyncCommitteeContribution( - slot: Slot, - subcommitteeIndex: number, - beaconBlockRoot: Root - ): Promise; - getAggregatedAttestation(attestationDataRoot: Root, slot: Slot): Promise; - publishAggregateAndProofs(signedAggregateAndProofs: phase0.SignedAggregateAndProof[]): Promise; - publishContributionAndProofs(contributionAndProofs: altair.SignedContributionAndProof[]): Promise; - prepareBeaconCommitteeSubnet(subscriptions: phase0.BeaconCommitteeSubscription[]): Promise; - prepareSyncCommitteeSubnets(subscriptions: altair.SyncCommitteeSubscription[]): Promise; - }; -} - -export interface IApiClientValidator extends IApiClient { - url: string; - registerAbortSignal(signal: AbortSignal): void; -} diff --git a/packages/validator/src/api/rest/beacon.ts b/packages/validator/src/api/rest/beacon.ts deleted file mode 100644 index 13aba86101..0000000000 --- a/packages/validator/src/api/rest/beacon.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {allForks, altair, BLSPubkey, phase0, Root, ValidatorIndex} from "@chainsafe/lodestar-types"; -import {Json, toHexString} from "@chainsafe/ssz"; -import {HttpClient, IValidatorFilters} from "../../util"; -import {BlockId, IApiClient} from "../interface"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function BeaconApi(config: IBeaconConfig, client: HttpClient): IApiClient["beacon"] { - return { - async getGenesis(): Promise { - const res = await client.get<{data: Json}>("/eth/v1/beacon/genesis"); - return config.types.phase0.Genesis.fromJson(res.data, {case: "snake"}); - }, - - state: { - async getFork(stateId: "head"): Promise { - const res = await client.get<{data: Json}>(`/eth/v1/beacon/states/${stateId}/fork`); - return config.types.phase0.Fork.fromJson(res.data, {case: "snake"}); - }, - - async getStateValidators(stateId: "head", filters?: IValidatorFilters): Promise { - const query = { - indices: (filters?.indices || []).map(formatIndex), - ...(filters?.statuses && {statuses: filters.statuses as string[]}), - }; - - const res = await client.get<{data: Json[]}>(`/eth/v1/beacon/states/${stateId}/validators`, query); - return res.data.map((value) => config.types.phase0.ValidatorResponse.fromJson(value, {case: "snake"})); - }, - }, - - blocks: { - async publishBlock(block: allForks.SignedBeaconBlock): Promise { - await client.post( - "/eth/v1/beacon/blocks", - config.getForkTypes(block.message.slot).SignedBeaconBlock.toJson(block, {case: "snake"}) - ); - }, - - async getBlockRoot(blockId: BlockId): Promise { - const res = await client.get<{data: {root: Json}}>(`/eth/v1/beacon/blocks/${blockId}/root`); - return config.types.Root.fromJson(res.data.root, {case: "snake"}); - }, - }, - - pool: { - async submitAttestations(attestations: phase0.Attestation[]): Promise { - return client.post( - "/eth/v1/beacon/pool/attestations", - attestations.map((attestation) => config.types.phase0.Attestation.toJson(attestation, {case: "snake"})) - ); - }, - - async submitVoluntaryExit(signedVoluntaryExit: phase0.SignedVoluntaryExit): Promise { - await client.post( - "/eth/v1/beacon/pool/voluntary_exits", - config.types.phase0.SignedVoluntaryExit.toJson(signedVoluntaryExit, {case: "snake"}) - ); - }, - - async submitSyncCommitteeSignatures(signatures: altair.SyncCommitteeSignature[]): Promise { - await client.post( - "/eth/v1/beacon/pool/sync_committees", - signatures.map((item) => config.types.altair.SyncCommitteeSignature.toJson(item, {case: "snake"})) - ); - }, - }, - }; -} - -function formatIndex(validatorId: ValidatorIndex | BLSPubkey): string { - if (typeof validatorId === "number") { - return validatorId.toString(); - } else if (typeof validatorId === "string") { - return validatorId; - } else { - return toHexString(validatorId); - } -} diff --git a/packages/validator/src/api/rest/config.ts b/packages/validator/src/api/rest/config.ts deleted file mode 100644 index 3744396424..0000000000 --- a/packages/validator/src/api/rest/config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {IBeaconSSZTypes, phase0} from "@chainsafe/lodestar-types"; -import {Json} from "@chainsafe/ssz"; -import {HttpClient} from "../../util"; -import {IApiClient} from "../interface"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function ConfigApi(types: IBeaconSSZTypes, client: HttpClient): IApiClient["config"] { - return { - async getForkSchedule(): Promise { - const res = await client.get<{data: Json[]}>("/eth/v1/config/fork_schedule"); - return res.data.map((fork) => types.phase0.Fork.fromJson(fork)); - }, - }; -} diff --git a/packages/validator/src/api/rest/events.ts b/packages/validator/src/api/rest/events.ts deleted file mode 100644 index 5fc19a6b5f..0000000000 --- a/packages/validator/src/api/rest/events.ts +++ /dev/null @@ -1,68 +0,0 @@ -import EventSource from "eventsource"; -import {IStoppableEventIterable, LodestarEventIterator} from "@chainsafe/lodestar-utils"; -import {HttpClient, urlJoin} from "../../util"; -import {BeaconEvent, BeaconEventType, IApiClient} from "../interface"; -import {ContainerType} from "@chainsafe/ssz"; -import {ApiClientEventEmitter} from "../interface"; -import {IBeaconSSZTypes} from "@chainsafe/lodestar-types"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function EventsApi(types: IBeaconSSZTypes, client: HttpClient): IApiClient["events"] { - return { - getEventStream(topics: BeaconEventType[]): IStoppableEventIterable { - const query = topics.map((topic) => `topics=${topic}`).join("&"); - const url = `${urlJoin(client.baseUrl, "/eth/v1/events")}?${query}`; - - const eventSource = new EventSource(url); - return new LodestarEventIterator(({push}) => { - for (const evt of [BeaconEventType.BLOCK, BeaconEventType.CHAIN_REORG, BeaconEventType.HEAD]) { - eventSource.addEventListener(evt, ((event: MessageEvent) => { - if (topics.includes(event.type as BeaconEventType)) { - push(deserializeBeaconEventMessage(types, event)); - } - }) as EventListener); - } - return () => { - eventSource.close(); - }; - }); - }, - }; -} - -function deserializeBeaconEventMessage(types: IBeaconSSZTypes, msg: MessageEvent): BeaconEvent { - switch (msg.type) { - case BeaconEventType.BLOCK: - return { - type: BeaconEventType.BLOCK, - message: deserializeEventData(types.phase0.BlockEventPayload, msg.data), - }; - case BeaconEventType.CHAIN_REORG: - return { - type: BeaconEventType.CHAIN_REORG, - message: deserializeEventData(types.phase0.ChainReorg, msg.data), - }; - case BeaconEventType.HEAD: - return { - type: BeaconEventType.HEAD, - message: deserializeEventData(types.phase0.ChainHead, msg.data), - }; - default: - throw new Error("Unsupported beacon event type " + msg.type); - } -} - -function deserializeEventData(type: ContainerType, data: string): T { - return type.fromJson(JSON.parse(data)); -} - -export async function pipeToEmitter< - T extends BeaconEvent["type"] = BeaconEventType.BLOCK | BeaconEventType.HEAD | BeaconEventType.CHAIN_REORG ->(stream: IStoppableEventIterable, emitter: ApiClientEventEmitter): Promise { - for await (const evt of stream) { - emitter.emit( - evt.type, - evt.message as ({type: T} extends BeaconEvent ? BeaconEvent : never)["message"] - ); - } -} diff --git a/packages/validator/src/api/rest/index.ts b/packages/validator/src/api/rest/index.ts deleted file mode 100644 index c3bcd7d7ff..0000000000 --- a/packages/validator/src/api/rest/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {AbortSignal} from "abort-controller"; -import {HttpClient} from "../../util"; -import {IApiClientValidator} from "../interface"; -import {BeaconApi} from "./beacon"; -import {ConfigApi} from "./config"; -import {EventsApi} from "./events"; -import {NodeApi} from "./node"; -import {ValidatorApi} from "./validator"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function ApiClientOverRest(config: IBeaconConfig, baseUrl: string): IApiClientValidator { - const client = new HttpClient({ - baseUrl: baseUrl, - timeout: config.params.SECONDS_PER_SLOT * 1000, - }); - - return { - beacon: BeaconApi(config, client), - config: ConfigApi(config.types, client), - node: NodeApi(config.types, client), - events: EventsApi(config.types, client), - validator: ValidatorApi(config, client), - - url: baseUrl, - registerAbortSignal(signal: AbortSignal) { - client.registerAbortSignal(signal); - }, - }; -} diff --git a/packages/validator/src/api/rest/node.ts b/packages/validator/src/api/rest/node.ts deleted file mode 100644 index b4b2b27b04..0000000000 --- a/packages/validator/src/api/rest/node.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {HttpClient} from "../../util"; -import {IApiClient} from "../interface"; -import {IBeaconSSZTypes, phase0} from "@chainsafe/lodestar-types"; -import {Json} from "@chainsafe/ssz"; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function NodeApi(types: IBeaconSSZTypes, client: HttpClient): IApiClient["node"] { - return { - async getVersion(): Promise { - const res = await client.get<{data: {version: string}}>("/eth/v1/node/version"); - return res.data.version; - }, - - async getSyncingStatus(): Promise { - const res = await client.get<{data: Json}>("/eth/v1/node/syncing"); - return types.phase0.SyncingStatus.fromJson(res.data, {case: "snake"}); - }, - }; -} diff --git a/packages/validator/src/api/rest/validator.ts b/packages/validator/src/api/rest/validator.ts deleted file mode 100644 index 8d8f2ea1b2..0000000000 --- a/packages/validator/src/api/rest/validator.ts +++ /dev/null @@ -1,101 +0,0 @@ -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {allForks, phase0, altair, CommitteeIndex, Epoch, Root, Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; -import {Json, toHexString} from "@chainsafe/ssz"; -import {HttpClient} from "../../util"; -import {IApiClient} from "../interface"; - -/* eslint-disable @typescript-eslint/naming-convention */ - -export function ValidatorApi(config: IBeaconConfig, client: HttpClient): IApiClient["validator"] { - return { - async getProposerDuties(epoch: Epoch): Promise { - const res = await client.get<{data: Json[]; dependentRoot: string}>( - `/eth/v1/validator/duties/proposer/${epoch.toString()}` - ); - return config.types.phase0.ProposerDutiesApi.fromJson(res, {case: "snake"}); - }, - - async getAttesterDuties(epoch: Epoch, indices: ValidatorIndex[]): Promise { - const res = await client.post( - `/eth/v1/validator/duties/attester/${epoch.toString()}`, - indices.map((index) => config.types.ValidatorIndex.toJson(index) as string) - ); - return config.types.phase0.AttesterDutiesApi.fromJson(res, {case: "snake"}); - }, - - async getSyncCommitteeDuties(epoch: number, indices: ValidatorIndex[]): Promise { - const res = await client.post( - `/eth/v1/validator/duties/sync/${epoch.toString()}`, - indices.map((index) => config.types.ValidatorIndex.toJson(index) as string) - ); - return config.types.altair.SyncDutiesApi.fromJson(res, {case: "snake"}); - }, - - async produceBlock(slot: Slot, randaoReveal: Uint8Array, graffiti: string): Promise { - const res = await client.get<{data: Json}>(`/eth/v1/validator/blocks/${slot}`, { - randao_reveal: toHexString(randaoReveal), - graffiti, - }); - return config.getForkTypes(slot).BeaconBlock.fromJson(res.data, {case: "snake"}); - }, - - async produceAttestationData(index: CommitteeIndex, slot: Slot): Promise { - const res = await client.get<{data: Json[]}>("/eth/v1/validator/attestation_data", { - committee_index: index, - slot, - }); - return config.types.phase0.AttestationData.fromJson(res.data, {case: "snake"}); - }, - - async produceSyncCommitteeContribution( - slot: Slot, - subcommitteeIndex: number, - beaconBlockRoot: Root - ): Promise { - const res = await client.get<{data: Json}>("/eth/v1/validator/sync_committee_contribution", { - subcommittee_index: subcommitteeIndex, - slot, - beacon_block_root: toHexString(beaconBlockRoot), - }); - return config.types.altair.SyncCommitteeContribution.fromJson(res.data, {case: "snake"}); - }, - - async getAggregatedAttestation(attestationDataRoot: Root, slot: Slot): Promise { - const res = await client.get<{data: Json[]}>("/eth/v1/validator/aggregate_attestation", { - attestation_data_root: config.types.Root.toJson(attestationDataRoot) as string, - slot, - }); - return config.types.phase0.Attestation.fromJson(res.data, {case: "snake"}); - }, - - async publishAggregateAndProofs(signedAggregateAndProofs: phase0.SignedAggregateAndProof[]): Promise { - await client.post( - "/eth/v1/validator/aggregate_and_proofs", - signedAggregateAndProofs.map((a) => config.types.phase0.SignedAggregateAndProof.toJson(a, {case: "snake"})) - ); - }, - - async publishContributionAndProofs(contributionAndProofs: altair.SignedContributionAndProof[]): Promise { - await client.post( - "/eth/v1/validator/contribution_and_proofs", - contributionAndProofs.map((item) => - config.types.altair.SignedContributionAndProof.toJson(item, {case: "snake"}) - ) - ); - }, - - async prepareBeaconCommitteeSubnet(subscriptions: phase0.BeaconCommitteeSubscription[]): Promise { - await client.post( - "/eth/v1/validator/beacon_committee_subscriptions", - subscriptions.map((s) => config.types.phase0.BeaconCommitteeSubscription.toJson(s, {case: "snake"})) - ); - }, - - async prepareSyncCommitteeSubnets(subscriptions: altair.SyncCommitteeSubscription[]): Promise { - await client.post( - "/eth/v1/validator/sync_committee_subscriptions", - subscriptions.map((item) => config.types.altair.SyncCommitteeSubscription.toJson(item, {case: "snake"})) - ); - }, - }; -} diff --git a/packages/validator/src/genesis.ts b/packages/validator/src/genesis.ts index 61c0835d74..178752b23f 100644 --- a/packages/validator/src/genesis.ts +++ b/packages/validator/src/genesis.ts @@ -1,16 +1,17 @@ import {AbortSignal} from "abort-controller"; import {Genesis} from "@chainsafe/lodestar-types/phase0"; import {ILogger, sleep} from "@chainsafe/lodestar-utils"; -import {IApiClient} from "./api"; +import {Api} from "@chainsafe/lodestar-api"; /** The time between polls when waiting for genesis */ const WAITING_FOR_GENESIS_POLL_MS = 12 * 1000; -export async function waitForGenesis(apiClient: IApiClient, logger: ILogger, signal?: AbortSignal): Promise { +export async function waitForGenesis(api: Api, logger: ILogger, signal?: AbortSignal): Promise { // eslint-disable-next-line no-constant-condition while (true) { try { - return await apiClient.beacon.getGenesis(); + const res = await api.beacon.getGenesis(); + return res.data; } catch (e) { // TODO: Search for a 404 error which indicates that genesis has not yet occurred. // Note: Lodestar API does not become online after genesis is found diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index ff22a170fc..25c17cf7ae 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -4,6 +4,4 @@ export * from "./validator"; export * from "./genesis"; -export * from "./options"; -export * from "./api"; export * from "./slashingProtection"; diff --git a/packages/validator/src/options.ts b/packages/validator/src/options.ts deleted file mode 100644 index 8422371ef9..0000000000 --- a/packages/validator/src/options.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {IApiClient} from "./api"; -import {SecretKey} from "@chainsafe/bls"; -import {ILogger} from "@chainsafe/lodestar-utils"; -import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {ISlashingProtection} from "./slashingProtection"; - -export interface IValidatorOptions { - slashingProtection: ISlashingProtection; - config: IBeaconConfig; - api: IApiClient | string; - secretKeys: SecretKey[]; - logger: ILogger; - graffiti?: string; -} diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index bc8c407099..25c8b07bd0 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -3,7 +3,7 @@ import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {phase0, Slot, CommitteeIndex} from "@chainsafe/lodestar-types"; import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; import {ILogger, prettyBytes, sleep} from "@chainsafe/lodestar-utils"; -import {IApiClient} from "../api"; +import {Api} from "@chainsafe/lodestar-api"; import {extendError, notAborted, IClock} from "../util"; import {ValidatorStore} from "./validatorStore"; import {AttestationDutiesService, AttDutyAndProof} from "./attestationDuties"; @@ -19,12 +19,12 @@ export class AttestationService { constructor( private readonly config: IBeaconConfig, private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, private readonly clock: IClock, private readonly validatorStore: ValidatorStore, indicesService: IndicesService ) { - this.dutiesService = new AttestationDutiesService(config, logger, apiClient, clock, validatorStore, indicesService); + this.dutiesService = new AttestationDutiesService(config, logger, api, clock, validatorStore, indicesService); // At most every slot, check existing duties from AttestationDutiesService and run tasks clock.runEverySlot(this.runAttestationTasks); @@ -85,9 +85,10 @@ export class AttestationService { const logCtx = {slot, index: committeeIndex}; // Produce one attestation data per slot and committeeIndex - const attestation = await this.apiClient.validator.produceAttestationData(committeeIndex, slot).catch((e) => { + const attestationRes = await this.api.validator.produceAttestationData(committeeIndex, slot).catch((e) => { throw extendError(e, "Error producing attestation"); }); + const attestation = attestationRes.data; const currentEpoch = computeEpochAtSlot(this.config, slot); const signedAttestations: phase0.Attestation[] = []; @@ -104,7 +105,7 @@ export class AttestationService { if (signedAttestations.length > 0) { try { - await this.apiClient.beacon.pool.submitAttestations(signedAttestations); + await this.api.beacon.submitPoolAttestations(signedAttestations); this.logger.info("Published attestations", {...logCtx, count: signedAttestations.length}); } catch (e) { if (notAborted(e)) this.logger.error("Error publishing attestations", logCtx, e); @@ -136,7 +137,7 @@ export class AttestationService { } this.logger.verbose("Aggregating attestations", logCtx); - const aggregate = await this.apiClient.validator + const aggregate = await this.api.validator .getAggregatedAttestation(this.config.types.phase0.AttestationData.hashTreeRoot(attestation), attestation.slot) .catch((e) => { throw extendError(e, "Error producing aggregateAndProofs"); @@ -150,7 +151,7 @@ export class AttestationService { // Produce signed aggregates only for validators that are subscribed aggregators. if (selectionProof !== null) { signedAggregateAndProofs.push( - await this.validatorStore.signAggregateAndProof(duty, selectionProof, aggregate) + await this.validatorStore.signAggregateAndProof(duty, selectionProof, aggregate.data) ); this.logger.debug("Signed aggregateAndProofs", logCtxValidator); } @@ -161,7 +162,7 @@ export class AttestationService { if (signedAggregateAndProofs.length > 0) { try { - await this.apiClient.validator.publishAggregateAndProofs(signedAggregateAndProofs); + await this.api.validator.publishAggregateAndProofs(signedAggregateAndProofs); this.logger.info("Published aggregateAndProofs", {...logCtx, count: signedAggregateAndProofs.length}); } catch (e) { if (notAborted(e)) this.logger.error("Error publishing aggregateAndProofs", logCtx, e); diff --git a/packages/validator/src/services/attestationDuties.ts b/packages/validator/src/services/attestationDuties.ts index f6e7783b79..aecb2f6a91 100644 --- a/packages/validator/src/services/attestationDuties.ts +++ b/packages/validator/src/services/attestationDuties.ts @@ -1,10 +1,10 @@ import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {BLSSignature, Epoch, phase0, Root, Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; +import {BLSSignature, Epoch, Root, Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; +import {Api, routes} from "@chainsafe/lodestar-api"; import {toHexString} from "@chainsafe/ssz"; import {IndicesService} from "./indices"; -import {IApiClient} from "../api"; import {extendError, isAttestationAggregator, notAborted} from "../util"; import {IClock} from "../util/clock"; import {ValidatorStore} from "./validatorStore"; @@ -14,7 +14,7 @@ const HISTORICAL_DUTIES_EPOCHS = 2; /** Neatly joins the server-generated `AttesterData` with the locally-generated `selectionProof`. */ export type AttDutyAndProof = { - duty: phase0.AttesterDuty; + duty: routes.validator.AttesterDuty; /** This value is only set to not null if the proof indicates that the validator is an aggregator. */ selectionProof: BLSSignature | null; }; @@ -29,7 +29,7 @@ export class AttestationDutiesService { constructor( private readonly config: IBeaconConfig, private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, clock: IClock, private readonly validatorStore: ValidatorStore, private readonly indicesService: IndicesService @@ -99,7 +99,7 @@ export class AttestationDutiesService { }); } - const beaconCommitteeSubscriptions: phase0.BeaconCommitteeSubscription[] = []; + const beaconCommitteeSubscriptions: routes.validator.BeaconCommitteeSubscription[] = []; // For this epoch and the next epoch, produce any beacon committee subscriptions. // @@ -128,7 +128,7 @@ export class AttestationDutiesService { // If there are any subscriptions, push them out to the beacon node. if (beaconCommitteeSubscriptions.length > 0) { // TODO: Should log or throw? - await this.apiClient.validator.prepareBeaconCommitteeSubnet(beaconCommitteeSubscriptions).catch((e) => { + await this.api.validator.prepareBeaconCommitteeSubnet(beaconCommitteeSubscriptions).catch((e) => { throw extendError(e, "Failed to subscribe to beacon committee subnets"); }); } @@ -144,7 +144,7 @@ export class AttestationDutiesService { } // TODO: Implement dependentRoot logic - const attesterDuties = await this.apiClient.validator.getAttesterDuties(epoch, indexArr).catch((e) => { + const attesterDuties = await this.api.validator.getAttesterDuties(epoch, indexArr).catch((e) => { throw extendError(e, "Failed to obtain attester duty"); }); const dependentRoot = attesterDuties.dependentRoot; @@ -189,7 +189,7 @@ export class AttestationDutiesService { } } - private async getDutyAndProof(duty: phase0.AttesterDuty): Promise { + private async getDutyAndProof(duty: routes.validator.AttesterDuty): Promise { const selectionProof = await this.validatorStore.signAttestationSelectionProof(duty.pubkey, duty.slot); const isAggregator = isAttestationAggregator(this.config, duty, selectionProof); diff --git a/packages/validator/src/services/block.ts b/packages/validator/src/services/block.ts index ba17b317a9..019ef53a05 100644 --- a/packages/validator/src/services/block.ts +++ b/packages/validator/src/services/block.ts @@ -2,7 +2,7 @@ import {BLSPubkey, Slot} from "@chainsafe/lodestar-types"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {ILogger, prettyBytes} from "@chainsafe/lodestar-utils"; import {toHexString} from "@chainsafe/ssz"; -import {IApiClient} from "../api"; +import {Api} from "@chainsafe/lodestar-api"; import {extendError, notAborted} from "../util"; import {ValidatorStore} from "./validatorStore"; import {BlockDutiesService, GENESIS_SLOT} from "./blockDuties"; @@ -17,7 +17,7 @@ export class BlockProposingService { constructor( config: IBeaconConfig, private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, clock: IClock, private readonly validatorStore: ValidatorStore, private readonly graffiti?: string @@ -25,7 +25,7 @@ export class BlockProposingService { this.dutiesService = new BlockDutiesService( config, logger, - apiClient, + api, clock, validatorStore, this.notifyBlockProductionFn @@ -62,13 +62,13 @@ export class BlockProposingService { const graffiti = this.graffiti || ""; this.logger.debug("Producing block", logCtx); - const block = await this.apiClient.validator.produceBlock(slot, randaoReveal, graffiti).catch((e) => { + const block = await this.api.validator.produceBlock(slot, randaoReveal, graffiti).catch((e) => { throw extendError(e, "Failed to produce block"); }); this.logger.debug("Produced block", logCtx); - const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot); - await this.apiClient.beacon.blocks.publishBlock(signedBlock).catch((e) => { + const signedBlock = await this.validatorStore.signBlock(pubkey, block.data, slot); + await this.api.beacon.publishBlock(signedBlock).catch((e) => { throw extendError(e, "Failed to publish block"); }); this.logger.info("Published block", {...logCtx, graffiti}); diff --git a/packages/validator/src/services/blockDuties.ts b/packages/validator/src/services/blockDuties.ts index 7acc211923..cec524ab5a 100644 --- a/packages/validator/src/services/blockDuties.ts +++ b/packages/validator/src/services/blockDuties.ts @@ -1,9 +1,9 @@ import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {BLSPubkey, Epoch, phase0, Root, Slot} from "@chainsafe/lodestar-types"; +import {BLSPubkey, Epoch, Root, Slot} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; import {toHexString} from "@chainsafe/ssz"; -import {IApiClient} from "../api"; +import {Api, routes} from "@chainsafe/lodestar-api"; import {extendError, notAborted} from "../util"; import {IClock} from "../util/clock"; import {differenceHex} from "../util/difference"; @@ -15,7 +15,7 @@ const HISTORICAL_DUTIES_EPOCHS = 2; const GENESIS_EPOCH = 0; export const GENESIS_SLOT = 0; -type BlockDutyAtEpoch = {dependentRoot: Root; data: phase0.ProposerDuty[]}; +type BlockDutyAtEpoch = {dependentRoot: Root; data: routes.validator.ProposerDuty[]}; type NotifyBlockProductionFn = (slot: Slot, proposers: BLSPubkey[]) => void; export class BlockDutiesService { @@ -28,7 +28,7 @@ export class BlockDutiesService { constructor( private readonly config: IBeaconConfig, private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, clock: IClock, private readonly validatorStore: ValidatorStore, notifyBlockProductionFn: NotifyBlockProductionFn @@ -138,7 +138,7 @@ export class BlockDutiesService { return; } - const proposerDuties = await this.apiClient.validator.getProposerDuties(epoch).catch((e) => { + const proposerDuties = await this.api.validator.getProposerDuties(epoch).catch((e) => { throw extendError(e, "Error on getProposerDuties"); }); const dependentRoot = proposerDuties.dependentRoot; diff --git a/packages/validator/src/services/fork.ts b/packages/validator/src/services/fork.ts index de8abea093..e6e5309399 100644 --- a/packages/validator/src/services/fork.ts +++ b/packages/validator/src/services/fork.ts @@ -1,6 +1,6 @@ import {Epoch, phase0} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; -import {IApiClient} from "../api"; +import {Api} from "@chainsafe/lodestar-api"; import {notAborted} from "../util"; import {IClock} from "../util/clock"; @@ -22,7 +22,7 @@ export class ForkService implements IForkService { /** Prevent calling updateFork() more than once at the same time */ private forkPromisePending = false; - constructor(private readonly provider: IApiClient, private readonly logger: ILogger, clock: IClock) { + constructor(private readonly api: Api, private readonly logger: ILogger, clock: IClock) { clock.runEveryEpoch(this.updateFork); } @@ -45,7 +45,7 @@ export class ForkService implements IForkService { try { this.forkPromisePending = true; - this.forkPromise = this.provider.beacon.state.getFork("head"); + this.forkPromise = this.api.beacon.getStateFork("head").then((res) => res.data); this.fork = await this.forkPromise; } catch (e) { if (notAborted(e)) this.logger.error("Error updating fork", {}, e as Error); diff --git a/packages/validator/src/services/indices.ts b/packages/validator/src/services/indices.ts index 43c488e978..0846a7a904 100644 --- a/packages/validator/src/services/indices.ts +++ b/packages/validator/src/services/indices.ts @@ -1,7 +1,7 @@ import {ValidatorIndex} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; import {toHexString} from "@chainsafe/ssz"; -import {IApiClient} from "../api"; +import {Api} from "@chainsafe/lodestar-api"; import {ValidatorStore} from "./validatorStore"; // To assist with readability @@ -16,7 +16,7 @@ export class IndicesService { constructor( private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, private readonly validatorStore: ValidatorStore ) {} @@ -58,10 +58,11 @@ export class IndicesService { } // Query the remote BN to resolve a pubkey to a validator index. - const validatorsState = await this.apiClient.beacon.state.getStateValidators("head", {indices: pubkeysToPoll}); + const pubkeysHex = pubkeysToPoll.map((pubkey) => toHexString(pubkey)); + const validatorsState = await this.api.beacon.getStateValidators("head", {indices: pubkeysHex}); const newIndices = []; - for (const validatorState of validatorsState) { + for (const validatorState of validatorsState.data) { const pubkeyHex = toHexString(validatorState.validator.pubkey); if (!this.pubkey2index.has(pubkeyHex)) { this.logger.debug("Discovered validator", {pubkey: pubkeyHex, index: validatorState.index}); diff --git a/packages/validator/src/services/syncCommittee.ts b/packages/validator/src/services/syncCommittee.ts index ea4dd30e4b..46590b2b27 100644 --- a/packages/validator/src/services/syncCommittee.ts +++ b/packages/validator/src/services/syncCommittee.ts @@ -2,7 +2,7 @@ import {AbortSignal} from "abort-controller"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {Slot, CommitteeIndex, altair, Root} from "@chainsafe/lodestar-types"; import {ILogger, prettyBytes, sleep} from "@chainsafe/lodestar-utils"; -import {IApiClient} from "../api"; +import {Api} from "@chainsafe/lodestar-api"; import {extendError, notAborted, IClock} from "../util"; import {ValidatorStore} from "./validatorStore"; import {SyncCommitteeDutiesService, SyncDutyAndProof} from "./syncCommitteeDuties"; @@ -19,19 +19,12 @@ export class SyncCommitteeService { constructor( private readonly config: IBeaconConfig, private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, private readonly clock: IClock, private readonly validatorStore: ValidatorStore, indicesService: IndicesService ) { - this.dutiesService = new SyncCommitteeDutiesService( - config, - logger, - apiClient, - clock, - validatorStore, - indicesService - ); + this.dutiesService = new SyncCommitteeDutiesService(config, logger, api, clock, validatorStore, indicesService); // At most every slot, check existing duties from SyncCommitteeDutiesService and run tasks clock.runEverySlot(this.runSyncCommitteeTasks); @@ -93,17 +86,18 @@ export class SyncCommitteeService { // Produce one attestation data per slot and subcommitteeIndex // Spec: the validator should prepare a SyncCommitteeSignature for the previous slot (slot - 1) // as soon as they have determined the head block of slot - 1 - const beaconBlockRoot = await this.apiClient.beacon.blocks.getBlockRoot(slot).catch((e) => { + const beaconBlockRoot = await this.api.beacon.getBlockRoot(slot).catch((e) => { throw extendError(e, "Error producing SyncCommitteeSignature"); }); + const blockRoot = beaconBlockRoot.data; const signatures: altair.SyncCommitteeSignature[] = []; for (const {duty} of duties) { const logCtxValidator = {...logCtx, validator: prettyBytes(duty.pubkey)}; try { signatures.push( - await this.validatorStore.signSyncCommitteeSignature(duty.pubkey, duty.validatorIndex, slot, beaconBlockRoot) + await this.validatorStore.signSyncCommitteeSignature(duty.pubkey, duty.validatorIndex, slot, blockRoot) ); this.logger.debug("Signed SyncCommitteeSignature", logCtxValidator); } catch (e) { @@ -113,14 +107,14 @@ export class SyncCommitteeService { if (signatures.length > 0) { try { - await this.apiClient.beacon.pool.submitSyncCommitteeSignatures(signatures); + await this.api.beacon.submitPoolSyncCommitteeSignatures(signatures); this.logger.info("Published SyncCommitteeSignature", {...logCtx, count: signatures.length}); } catch (e) { if (notAborted(e)) this.logger.error("Error publishing SyncCommitteeSignature", logCtx, e); } } - return beaconBlockRoot; + return blockRoot; } /** @@ -147,7 +141,7 @@ export class SyncCommitteeService { } this.logger.verbose("Producing SyncCommitteeContribution", logCtx); - const contribution = await this.apiClient.validator + const contribution = await this.api.validator .produceSyncCommitteeContribution(slot, subcommitteeIndex, beaconBlockRoot) .catch((e) => { throw extendError(e, "Error producing SyncCommitteeContribution"); @@ -161,7 +155,7 @@ export class SyncCommitteeService { // Produce signed contributions only for validators that are subscribed aggregators. if (selectionProof !== null) { signedContributions.push( - await this.validatorStore.signContributionAndProof(duty, selectionProof, contribution) + await this.validatorStore.signContributionAndProof(duty, selectionProof, contribution.data) ); this.logger.debug("Signed SyncCommitteeContribution", logCtxValidator); } @@ -172,7 +166,7 @@ export class SyncCommitteeService { if (signedContributions.length > 0) { try { - await this.apiClient.validator.publishContributionAndProofs(signedContributions); + await this.api.validator.publishContributionAndProofs(signedContributions); this.logger.info("Published SyncCommitteeContribution", {...logCtx, count: signedContributions.length}); } catch (e) { if (notAborted(e)) this.logger.error("Error publishing SyncCommitteeContribution", logCtx, e); diff --git a/packages/validator/src/services/syncCommitteeDuties.ts b/packages/validator/src/services/syncCommitteeDuties.ts index bf39312d49..5d3c9b405e 100644 --- a/packages/validator/src/services/syncCommitteeDuties.ts +++ b/packages/validator/src/services/syncCommitteeDuties.ts @@ -1,10 +1,10 @@ import {computeSyncPeriodAtEpoch, computeSyncPeriodAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {altair, BLSSignature, Epoch, Root, Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; +import {BLSSignature, Epoch, Root, Slot, ValidatorIndex} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; import {toHexString} from "@chainsafe/ssz"; +import {Api, routes} from "@chainsafe/lodestar-api"; import {IndicesService} from "./indices"; -import {IApiClient} from "../api"; import {extendError, isSyncCommitteeAggregator, notAborted} from "../util"; import {IClock} from "../util/clock"; import {ValidatorStore} from "./validatorStore"; @@ -18,8 +18,8 @@ const ALTAIR_FORK_LOOKAHEAD_EPOCHS = 1; const SUBSCRIPTIONS_LOOKAHEAD_EPOCHS = 2; export type SyncDutySubCommittee = { - pubkey: altair.SyncDuty["pubkey"]; - validatorIndex: altair.SyncDuty["validatorIndex"]; + pubkey: routes.validator.SyncDuty["pubkey"]; + validatorIndex: routes.validator.SyncDuty["validatorIndex"]; /** A single index of the validator in the sync committee. */ validatorSyncCommitteeIndex: number; }; @@ -33,7 +33,7 @@ export type SyncDutyAndProof = { }; // To assist with readability -type DutyAtPeriod = {dependentRoot: Root; duty: altair.SyncDuty}; +type DutyAtPeriod = {dependentRoot: Root; duty: routes.validator.SyncDuty}; /** * Validators are part of a static long (~27h) sync committee, and part of static subnets. @@ -46,7 +46,7 @@ export class SyncCommitteeDutiesService { constructor( private readonly config: IBeaconConfig, private readonly logger: ILogger, - private readonly apiClient: IApiClient, + private readonly api: Api, clock: IClock, private readonly validatorStore: ValidatorStore, private readonly indicesService: IndicesService @@ -139,7 +139,7 @@ export class SyncCommitteeDutiesService { } const currentPeriod = computeSyncPeriodAtEpoch(this.config, currentEpoch); - const syncCommitteeSubscriptions: altair.SyncCommitteeSubscription[] = []; + const syncCommitteeSubscriptions: routes.validator.SyncCommitteeSubscription[] = []; // For this and the next period, produce any beacon committee subscriptions. // @@ -171,7 +171,7 @@ export class SyncCommitteeDutiesService { // If there are any subscriptions, push them out to the beacon node. if (syncCommitteeSubscriptions.length > 0) { // TODO: Should log or throw? - await this.apiClient.validator.prepareSyncCommitteeSubnets(syncCommitteeSubscriptions).catch((e) => { + await this.api.validator.prepareSyncCommitteeSubnets(syncCommitteeSubscriptions).catch((e) => { throw extendError(e, "Failed to subscribe to sync committee subnets"); }); } @@ -186,7 +186,7 @@ export class SyncCommitteeDutiesService { return; } - const syncDuties = await this.apiClient.validator.getSyncCommitteeDuties(epoch, indexArr).catch((e) => { + const syncDuties = await this.api.validator.getSyncCommitteeDuties(epoch, indexArr).catch((e) => { throw extendError(e, "Failed to obtain SyncDuties"); }); const dependentRoot = syncDuties.dependentRoot; diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index e5dfecd2b1..c8f5ca76bf 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -19,6 +19,7 @@ import { } from "@chainsafe/lodestar-types"; import {Genesis, ValidatorIndex} from "@chainsafe/lodestar-types/phase0"; import {List, toHexString} from "@chainsafe/ssz"; +import {routes} from "@chainsafe/lodestar-api"; import {ISlashingProtection} from "../slashingProtection"; import {BLSKeypair, PubkeyHex} from "../types"; import {IForkService} from "./fork"; @@ -91,7 +92,7 @@ export class ValidatorStore { } async signAttestation( - duty: phase0.AttesterDuty, + duty: routes.validator.AttesterDuty, attestationData: phase0.AttestationData, currentEpoch: Epoch ): Promise { @@ -127,7 +128,7 @@ export class ValidatorStore { } async signAggregateAndProof( - duty: phase0.AttesterDuty, + duty: routes.validator.AttesterDuty, selectionProof: BLSSignature, aggregate: phase0.Attestation ): Promise { @@ -177,7 +178,7 @@ export class ValidatorStore { } async signContributionAndProof( - duty: Pick, + duty: Pick, selectionProof: BLSSignature, contribution: altair.SyncCommitteeContribution ): Promise { @@ -254,7 +255,7 @@ export class ValidatorStore { } /** Prevent signing bad data sent by the Beacon node */ - private validateAttestationDuty(duty: phase0.AttesterDuty, data: phase0.AttestationData): void { + private validateAttestationDuty(duty: routes.validator.AttesterDuty, data: phase0.AttestationData): void { if (duty.slot !== data.slot) { throw Error(`Inconsistent duties during signing: duty.slot ${duty.slot} != att.slot ${data.slot}`); } diff --git a/packages/validator/src/util/aggregator.ts b/packages/validator/src/util/aggregator.ts index 186144c9d3..e991e5602a 100644 --- a/packages/validator/src/util/aggregator.ts +++ b/packages/validator/src/util/aggregator.ts @@ -1,12 +1,13 @@ import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params"; -import {phase0, BLSSignature} from "@chainsafe/lodestar-types"; +import {BLSSignature} from "@chainsafe/lodestar-types"; import {bytesToBigInt, intDiv} from "@chainsafe/lodestar-utils"; +import {routes} from "@chainsafe/lodestar-api"; import {hash} from "@chainsafe/ssz"; export function isAttestationAggregator( config: IBeaconConfig, - duty: Pick, + duty: Pick, slotSignature: BLSSignature ): boolean { const modulo = Math.max(1, intDiv(duty.committeeLength, config.params.TARGET_AGGREGATORS_PER_COMMITTEE)); diff --git a/packages/validator/src/util/httpClient.ts b/packages/validator/src/util/httpClient.ts deleted file mode 100644 index 1a4389801b..0000000000 --- a/packages/validator/src/util/httpClient.ts +++ /dev/null @@ -1,89 +0,0 @@ -import Axios, {AxiosError, AxiosInstance, AxiosResponse, CancelToken, Method} from "axios"; -import querystring from "querystring"; -import {AbortSignal} from "abort-controller"; -import {BLSPubkey, ValidatorIndex} from "@chainsafe/lodestar-types"; -import {ValidatorStatus} from "@chainsafe/lodestar-types/phase0"; -import {ErrorAborted} from "@chainsafe/lodestar-utils"; - -/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */ - -export interface IValidatorFilters { - indices?: (BLSPubkey | ValidatorIndex)[]; - statuses?: ValidatorStatus[]; -} - -export interface IHttpClientOptions { - baseUrl: string; - timeout?: number; -} - -export interface IHttpQuery { - [key: string]: string | number | boolean | string[] | number[]; -} - -export class HttpClient { - readonly baseUrl: string; - private client: AxiosInstance; - private cancelToken?: CancelToken; - - constructor(opt: IHttpClientOptions) { - this.baseUrl = opt.baseUrl; - this.client = Axios.create({ - baseURL: opt.baseUrl, - timeout: opt.timeout, - }); - } - - registerAbortSignal(signal: AbortSignal): void { - const source = Axios.CancelToken.source(); - signal.addEventListener("abort", () => source.cancel("Aborted"), {once: true}); - this.cancelToken = source.token; - } - - async get(url: string, query?: IHttpQuery): Promise { - return this.request(url, "GET", query); - } - - async post(url: string, data: T, query?: IHttpQuery): Promise { - return this.request(url, "POST", query, data); - } - - async request(url: string, method: Method, query?: IHttpQuery, data?: T): Promise { - try { - if (query) { - url += "?" + querystring.stringify(query); - } - - const result: AxiosResponse = await this.client.request({ - method, - url, - data, - cancelToken: this.cancelToken, - }); - return result.data; - } catch (e) { - if (Axios.isCancel(e)) { - throw new ErrorAborted("Validator REST client"); - } - throw this.handleError(e); - } - } - - private handleError = (error: AxiosError & NodeJS.ErrnoException): Error => { - if (error.response) { - if (error.response.status === 404) { - error.message = "Endpoint not found"; - if (error.request && error.request.path) { - error.message += `: ${error.request.path}`; - } - } else { - error.message = error.response.data.message || "Request failed with response status " + error.response.status; - } - } else if (error.request) { - if (error.syscall && error.errno) - error.message = error.syscall + " " + error.errno + " " + error.request._currentUrl; - } - error.stack = ""; - return error; - }; -} diff --git a/packages/validator/src/util/index.ts b/packages/validator/src/util/index.ts index 9f8b2fa272..a6f06f3855 100644 --- a/packages/validator/src/util/index.ts +++ b/packages/validator/src/util/index.ts @@ -1,5 +1,4 @@ export * from "./aggregator"; export * from "./clock"; export * from "./error"; -export * from "./httpClient"; export * from "./url"; diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index a7ab1fb6f3..ee3d35c3b3 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -3,9 +3,7 @@ import {SecretKey} from "@chainsafe/bls"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {Genesis} from "@chainsafe/lodestar-types/phase0"; import {fromHex, ILogger} from "@chainsafe/lodestar-utils"; -import {ApiClientOverInstance, IApiClientValidator} from "./api"; -import {ApiClientOverRest} from "./api/rest"; -import {IValidatorOptions} from "./options"; +import {getClient, Api} from "@chainsafe/lodestar-api"; import {Clock, IClock} from "./util/clock"; import {signAndSubmitVoluntaryExit} from "./voluntaryExit"; import {waitForGenesis} from "./genesis"; @@ -15,6 +13,16 @@ import {BlockProposingService} from "./services/block"; import {AttestationService} from "./services/attestation"; import {IndicesService} from "./services/indices"; import {SyncCommitteeService} from "./services/syncCommittee"; +import {ISlashingProtection} from "./slashingProtection"; + +export type ValidatorOptions = { + slashingProtection: ISlashingProtection; + config: IBeaconConfig; + api: Api | string; + secretKeys: SecretKey[]; + logger: ILogger; + graffiti?: string; +}; // TODO: Extend the timeout, and let it be customizable /// The global timeout for HTTP requests to the beacon node. @@ -32,36 +40,46 @@ type State = {status: Status.running; controller: AbortController} | {status: St */ export class Validator { private readonly config: IBeaconConfig; - private readonly apiClient: IApiClientValidator; + private readonly api: Api; private readonly secretKeys: SecretKey[]; private readonly clock: IClock; private readonly logger: ILogger; private state: State = {status: Status.stopped}; - constructor(opts: IValidatorOptions, genesis: Genesis) { + constructor(opts: ValidatorOptions, genesis: Genesis) { const {config, logger, slashingProtection, secretKeys, graffiti} = opts; - const apiClient = - typeof opts.api === "string" ? ApiClientOverRest(config, opts.api) : ApiClientOverInstance(opts.api); + const api = + typeof opts.api === "string" + ? getClient(config, { + baseUrl: opts.api, + timeoutMs: config.params.SECONDS_PER_SLOT * 1000, + getAbortSignal: this.getAbortSignal, + }) + : opts.api; + const clock = new Clock(config, logger, {genesisTime: Number(genesis.genesisTime)}); - const forkService = new ForkService(apiClient, logger, clock); + const forkService = new ForkService(api, logger, clock); const validatorStore = new ValidatorStore(config, forkService, slashingProtection, secretKeys, genesis); - const indicesService = new IndicesService(logger, apiClient, validatorStore); - new BlockProposingService(config, logger, apiClient, clock, validatorStore, graffiti); - new AttestationService(config, logger, apiClient, clock, validatorStore, indicesService); - new SyncCommitteeService(config, logger, apiClient, clock, validatorStore, indicesService); + const indicesService = new IndicesService(logger, api, validatorStore); + new BlockProposingService(config, logger, api, clock, validatorStore, graffiti); + new AttestationService(config, logger, api, clock, validatorStore, indicesService); + new SyncCommitteeService(config, logger, api, clock, validatorStore, indicesService); this.config = config; this.logger = logger; - this.apiClient = apiClient; + this.api = api; this.clock = clock; this.secretKeys = secretKeys; } /** Waits for genesis and genesis time */ - static async initializeFromBeaconNode(opts: IValidatorOptions, signal?: AbortSignal): Promise { - const apiClient = typeof opts.api === "string" ? ApiClientOverRest(opts.config, opts.api) : opts.api; - const genesis = await waitForGenesis(apiClient, opts.logger, signal); + static async initializeFromBeaconNode(opts: ValidatorOptions, signal?: AbortSignal): Promise { + const api = + typeof opts.api === "string" + ? getClient(opts.config, {baseUrl: opts.api, timeoutMs: 12000, getAbortSignal: () => signal}) + : opts.api; + const genesis = await waitForGenesis(api, opts.logger, signal); opts.logger.info("Genesis available"); return new Validator(opts, genesis); } @@ -75,7 +93,6 @@ export class Validator { this.state = {status: Status.running, controller}; this.clock.start(controller.signal); - this.apiClient.registerAbortSignal(controller.signal); } /** @@ -96,7 +113,12 @@ export class Validator { ); if (!secretKey) throw new Error(`No matching secret key found for public key ${publicKey}`); - await signAndSubmitVoluntaryExit(publicKey, exitEpoch, secretKey, this.apiClient, this.config); + await signAndSubmitVoluntaryExit(publicKey, exitEpoch, secretKey, this.api, this.config); this.logger.info(`Submitted voluntary exit for ${publicKey} to the network`); } + + /** Provide the current AbortSignal to the api instance */ + private getAbortSignal = (): AbortSignal | undefined => { + return this.state.status === Status.running ? this.state.controller.signal : undefined; + }; } diff --git a/packages/validator/src/voluntaryExit.ts b/packages/validator/src/voluntaryExit.ts index 880245e13e..05c2d471fb 100644 --- a/packages/validator/src/voluntaryExit.ts +++ b/packages/validator/src/voluntaryExit.ts @@ -7,7 +7,7 @@ import { } from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {phase0} from "@chainsafe/lodestar-types"; -import {IApiClient} from "./api"; +import {Api} from "@chainsafe/lodestar-api"; /** * Perform a voluntary exit for the given validator by its key. @@ -16,25 +16,24 @@ export async function signAndSubmitVoluntaryExit( publicKey: string, exitEpoch: number, secretKey: SecretKey, - apiClient: IApiClient, + api: Api, config: IBeaconConfig ): Promise { - const [stateValidator] = await apiClient.beacon.state.getStateValidators("head", { - indices: [config.types.BLSPubkey.fromJson(publicKey)], - }); + const stateValidatorRes = await api.beacon.getStateValidators("head", {indices: [publicKey]}); + const stateValidator = stateValidatorRes.data[0]; if (!stateValidator) { throw new Error("Validator not found in beacon chain."); } - const fork = await apiClient.beacon.state.getFork("head"); + const {data: fork} = await api.beacon.getStateFork("head"); if (!fork) { throw new Error("VoluntaryExit: Fork not found"); } - const genesis = await apiClient.beacon.getGenesis(); - const genesisValidatorsRoot = genesis.genesisValidatorsRoot; - const currentSlot = getCurrentSlot(config, Number(genesis.genesisTime)); + const genesisRes = await api.beacon.getGenesis(); + const {genesisValidatorsRoot, genesisTime} = genesisRes.data; + const currentSlot = getCurrentSlot(config, Number(genesisTime)); const currentEpoch = computeEpochAtSlot(config, currentSlot); const voluntaryExit = { @@ -50,5 +49,5 @@ export async function signAndSubmitVoluntaryExit( signature: secretKey.sign(signingRoot).toBytes(), }; - await apiClient.beacon.pool.submitVoluntaryExit(signedVoluntaryExit); + await api.beacon.submitPoolVoluntaryExit(signedVoluntaryExit); } diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index f5ee1da96d..98502ccd33 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -10,7 +10,7 @@ import { import {AttestationService} from "../../../src/services/attestation"; import {AttDutyAndProof} from "../../../src/services/attestationDuties"; import {ValidatorStore} from "../../../src/services/validatorStore"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {ClockMock} from "../../utils/clock"; import {IndicesService} from "../../../src/services/indices"; @@ -20,7 +20,7 @@ describe("AttestationService", function () { const logger = testLogger(); const ZERO_HASH = Buffer.alloc(32, 0); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & sinon.SinonStubbedInstance; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized @@ -45,8 +45,8 @@ describe("AttestationService", function () { it("Should produce, sign, and publish an attestation + aggregate", async () => { const clock = new ClockMock(); - const indicesService = new IndicesService(logger, apiClient, validatorStore); - const attestationService = new AttestationService(config, logger, apiClient, clock, validatorStore, indicesService); + const indicesService = new IndicesService(logger, api, validatorStore); + const attestationService = new AttestationService(config, logger, api, clock, validatorStore, indicesService); const attestation = generateEmptyAttestation(); const aggregate = generateEmptySignedAggregateAndProof(); @@ -66,18 +66,18 @@ describe("AttestationService", function () { ]; // Return empty replies to duties service - apiClient.beacon.state.getStateValidators.resolves([]); - apiClient.validator.getAttesterDuties.resolves({dependentRoot: ZERO_HASH, data: []}); + api.beacon.getStateValidators.resolves({data: []}); + api.validator.getAttesterDuties.resolves({dependentRoot: ZERO_HASH, data: []}); // Mock duties service to return some duties directly attestationService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties); // Mock beacon's attestation and aggregates endpoints - apiClient.validator.produceAttestationData.resolves(attestation.data); - apiClient.validator.getAggregatedAttestation.resolves(attestation); - apiClient.beacon.pool.submitAttestations.resolves(); - apiClient.validator.publishAggregateAndProofs.resolves(); + api.validator.produceAttestationData.resolves({data: attestation.data}); + api.validator.getAggregatedAttestation.resolves({data: attestation}); + api.beacon.submitPoolAttestations.resolves(); + api.validator.publishAggregateAndProofs.resolves(); // Mock signing service validatorStore.signAttestation.resolves(attestation); @@ -87,18 +87,18 @@ describe("AttestationService", function () { await clock.tickSlotFns(0, controller.signal); // Must submit the attestation received through produceAttestationData() - expect(apiClient.beacon.pool.submitAttestations.callCount).to.equal(1, "submitAttestations() must be called once"); - expect(apiClient.beacon.pool.submitAttestations.getCall(0).args).to.deep.equal( + 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(apiClient.validator.publishAggregateAndProofs.callCount).to.equal( + expect(api.validator.publishAggregateAndProofs.callCount).to.equal( 1, "publishAggregateAndProofs() must be called once" ); - expect(apiClient.validator.publishAggregateAndProofs.getCall(0).args).to.deep.equal( + expect(api.validator.publishAggregateAndProofs.getCall(0).args).to.deep.equal( [[aggregate]], // 1 arg, = aggregate[] "wrong publishAggregateAndProofs() args" ); diff --git a/packages/validator/test/unit/services/attestationDuties.test.ts b/packages/validator/test/unit/services/attestationDuties.test.ts index 48f6a6d857..dca97c3af6 100644 --- a/packages/validator/test/unit/services/attestationDuties.test.ts +++ b/packages/validator/test/unit/services/attestationDuties.test.ts @@ -4,11 +4,11 @@ import {expect} from "chai"; import sinon from "sinon"; import bls from "@chainsafe/bls"; import {config} from "@chainsafe/lodestar-config/mainnet"; -import {phase0} from "@chainsafe/lodestar-types"; import {toHexString} from "@chainsafe/ssz"; +import {routes} from "@chainsafe/lodestar-api"; import {AttestationDutiesService} from "../../../src/services/attestationDuties"; import {ValidatorStore} from "../../../src/services/validatorStore"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {ClockMock} from "../../utils/clock"; import {IndicesService} from "../../../src/services/indices"; @@ -18,15 +18,20 @@ describe("AttestationDutiesService", function () { const logger = testLogger(); const ZERO_HASH = Buffer.alloc(32, 0); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & sinon.SinonStubbedInstance; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized // Sample validator - const defaultValidator = config.types.phase0.ValidatorResponse.defaultValue(); const index = 4; - defaultValidator.index = index; + // Sample validator + const defaultValidator: routes.beacon.ValidatorResponse = { + index, + balance: BigInt(32e9), + status: "active", + validator: config.types.phase0.Validator.defaultValue(), + }; before(() => { const secretKeys = [bls.SecretKey.fromBytes(toBufferBE(BigInt(98), 32))]; @@ -47,11 +52,11 @@ describe("AttestationDutiesService", function () { index, validator: {...defaultValidator.validator, pubkey: pubkeys[0]}, }; - apiClient.beacon.state.getStateValidators.resolves([validatorResponse]); + api.beacon.getStateValidators.resolves({data: [validatorResponse]}); // Reply with some duties const slot = 1; - const duty: phase0.AttesterDuty = { + const duty: routes.validator.AttesterDuty = { slot: slot, committeeIndex: 1, committeeLength: 120, @@ -60,22 +65,15 @@ describe("AttestationDutiesService", function () { validatorIndex: index, pubkey: pubkeys[0], }; - apiClient.validator.getAttesterDuties.resolves({dependentRoot: ZERO_HASH, data: [duty]}); + api.validator.getAttesterDuties.resolves({dependentRoot: ZERO_HASH, data: [duty]}); // Accept all subscriptions - apiClient.validator.prepareBeaconCommitteeSubnet.resolves(); + api.validator.prepareBeaconCommitteeSubnet.resolves(); // Clock will call runAttesterDutiesTasks() immediatelly const clock = new ClockMock(); - const indicesService = new IndicesService(logger, apiClient, validatorStore); - const dutiesService = new AttestationDutiesService( - config, - logger, - apiClient, - clock, - validatorStore, - indicesService - ); + const indicesService = new IndicesService(logger, api, validatorStore); + const dutiesService = new AttestationDutiesService(config, logger, api, clock, validatorStore, indicesService); // Trigger clock onSlot for slot 0 await clock.tickEpochFns(0, controller.signal); @@ -101,7 +99,7 @@ describe("AttestationDutiesService", function () { "Wrong getAttestersAtSlot()" ); - expect(apiClient.validator.prepareBeaconCommitteeSubnet.callCount).to.equal( + expect(api.validator.prepareBeaconCommitteeSubnet.callCount).to.equal( 1, "prepareBeaconCommitteeSubnet() must be called once after getting the duties" ); diff --git a/packages/validator/test/unit/services/block.test.ts b/packages/validator/test/unit/services/block.test.ts index f1cccfb863..fef69be48f 100644 --- a/packages/validator/test/unit/services/block.test.ts +++ b/packages/validator/test/unit/services/block.test.ts @@ -3,23 +3,25 @@ import {expect} from "chai"; import sinon from "sinon"; import bls from "@chainsafe/bls"; import {config} from "@chainsafe/lodestar-config/mainnet"; -import {phase0, Root} from "@chainsafe/lodestar-types"; +import {Root} from "@chainsafe/lodestar-types"; import {sleep} from "@chainsafe/lodestar-utils"; +import {routes} from "@chainsafe/lodestar-api"; import {generateEmptySignedBlock} from "@chainsafe/lodestar/test/utils/block"; import {BlockProposingService} from "../../../src/services/block"; import {ValidatorStore} from "../../../src/services/validatorStore"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {ClockMock} from "../../utils/clock"; +import {ForkName} from "@chainsafe/lodestar-config"; -type ProposerDutiesRes = {dependentRoot: Root; data: phase0.ProposerDuty[]}; +type ProposerDutiesRes = {dependentRoot: Root; data: routes.validator.ProposerDuty[]}; describe("BlockDutiesService", function () { const sandbox = sinon.createSandbox(); const logger = testLogger(); const ZERO_HASH = Buffer.alloc(32, 0); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & sinon.SinonStubbedInstance; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized @@ -41,16 +43,16 @@ describe("BlockDutiesService", function () { dependentRoot: ZERO_HASH, data: [{slot: slot, validatorIndex: 0, pubkey: pubkeys[0]}], }; - apiClient.validator.getProposerDuties.resolves(duties); + api.validator.getProposerDuties.resolves(duties); const clock = new ClockMock(); - const blockService = new BlockProposingService(config, logger, apiClient, clock, validatorStore); + const blockService = new BlockProposingService(config, logger, api, clock, validatorStore); const signedBlock = generateEmptySignedBlock(); validatorStore.signRandao.resolves(signedBlock.message.body.randaoReveal); validatorStore.signBlock.callsFake(async (_, block) => ({message: block, signature: signedBlock.signature})); - apiClient.validator.produceBlock.resolves(signedBlock.message); - apiClient.beacon.blocks.publishBlock.resolves(); + api.validator.produceBlock.resolves({data: signedBlock.message, version: ForkName.phase0}); + api.beacon.publishBlock.resolves(); // Triger block production for slot 1 const notifyBlockProductionFn = blockService["dutiesService"]["notifyBlockProductionFn"]; @@ -60,10 +62,7 @@ describe("BlockDutiesService", function () { await sleep(20, controller.signal); // Must have submited the block received on signBlock() - expect(apiClient.beacon.blocks.publishBlock.callCount).to.equal(1, "publishBlock() must be called once"); - expect(apiClient.beacon.blocks.publishBlock.getCall(0).args).to.deep.equal( - [signedBlock], - "wrong publishBlock() args" - ); + expect(api.beacon.publishBlock.callCount).to.equal(1, "publishBlock() must be called once"); + expect(api.beacon.publishBlock.getCall(0).args).to.deep.equal([signedBlock], "wrong publishBlock() args"); }); }); diff --git a/packages/validator/test/unit/services/blockDuties.test.ts b/packages/validator/test/unit/services/blockDuties.test.ts index b56663caf3..d185813cb6 100644 --- a/packages/validator/test/unit/services/blockDuties.test.ts +++ b/packages/validator/test/unit/services/blockDuties.test.ts @@ -3,21 +3,22 @@ import {expect} from "chai"; import sinon from "sinon"; import bls from "@chainsafe/bls"; import {config} from "@chainsafe/lodestar-config/mainnet"; -import {phase0, Root} from "@chainsafe/lodestar-types"; +import {Root} from "@chainsafe/lodestar-types"; +import {routes} from "@chainsafe/lodestar-api"; import {BlockDutiesService} from "../../../src/services/blockDuties"; import {ValidatorStore} from "../../../src/services/validatorStore"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {ClockMock} from "../../utils/clock"; -type ProposerDutiesRes = {dependentRoot: Root; data: phase0.ProposerDuty[]}; +type ProposerDutiesRes = {dependentRoot: Root; data: routes.validator.ProposerDuty[]}; describe("BlockDutiesService", function () { const sandbox = sinon.createSandbox(); const logger = testLogger(); const ZERO_HASH = Buffer.alloc(32, 0); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & sinon.SinonStubbedInstance; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized @@ -41,19 +42,12 @@ describe("BlockDutiesService", function () { dependentRoot: ZERO_HASH, data: [{slot: slot, validatorIndex: 0, pubkey: pubkeys[0]}], }; - apiClient.validator.getProposerDuties.resolves(duties); + api.validator.getProposerDuties.resolves(duties); const notifyBlockProductionFn = sinon.stub(); // Returns void const clock = new ClockMock(); - const dutiesService = new BlockDutiesService( - config, - logger, - apiClient, - clock, - validatorStore, - notifyBlockProductionFn - ); + const dutiesService = new BlockDutiesService(config, logger, api, clock, validatorStore, notifyBlockProductionFn); // Trigger clock onSlot for slot 0 await clock.tickSlotFns(0, controller.signal); @@ -88,21 +82,14 @@ describe("BlockDutiesService", function () { // Clock will call runAttesterDutiesTasks() immediatelly const clock = new ClockMock(); - const dutiesService = new BlockDutiesService( - config, - logger, - apiClient, - clock, - validatorStore, - notifyBlockProductionFn - ); + const dutiesService = new BlockDutiesService(config, logger, api, clock, validatorStore, notifyBlockProductionFn); // Trigger clock onSlot for slot 0 - apiClient.validator.getProposerDuties.resolves(dutiesBeforeReorg); + api.validator.getProposerDuties.resolves(dutiesBeforeReorg); await clock.tickSlotFns(0, controller.signal); // Trigger clock onSlot for slot 1 - Return different duties for slot 1 - apiClient.validator.getProposerDuties.resolves(dutiesAfterReorg); + api.validator.getProposerDuties.resolves(dutiesAfterReorg); await clock.tickSlotFns(1, controller.signal); // Should persist the dutiesAfterReorg diff --git a/packages/validator/test/unit/services/fork.test.ts b/packages/validator/test/unit/services/fork.test.ts index dee97eaa81..4daffa3acf 100644 --- a/packages/validator/test/unit/services/fork.test.ts +++ b/packages/validator/test/unit/services/fork.test.ts @@ -2,7 +2,7 @@ import sinon from "sinon"; import {AbortController} from "abort-controller"; import {config} from "@chainsafe/lodestar-config/mainnet"; import {ForkService} from "../../../src/services/fork"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {expect} from "chai"; import {ClockMock} from "../../utils/clock"; @@ -10,7 +10,7 @@ import {ClockMock} from "../../utils/clock"; describe("ForkService", () => { const sandbox = sinon.createSandbox(); const logger = testLogger(); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); let controller: AbortController; // To stop clock beforeEach(() => (controller = new AbortController())); @@ -18,22 +18,22 @@ describe("ForkService", () => { it("Should only fetch the fork once", async () => { const clock = new ClockMock(); - const forkService = new ForkService(apiClient, logger, clock); + const forkService = new ForkService(api, logger, clock); const fork = config.types.phase0.Fork.defaultValue(); - apiClient.beacon.state.getFork.resolves(fork); + api.beacon.getStateFork.resolves({data: fork}); // Trigger clock onSlot for slot 0 // Don't resolve the promise immediatelly to check the promise caching mechanism void clock.tickEpochFns(0, controller.signal); - // Call getFork() multiple times at once + // Call getStateFork() multiple times at once await Promise.all( Array.from({length: 3}).map(async () => { - expect(await forkService.getFork()).to.equal(fork, "Wrong resolved value on forkService.getFork()"); + expect(await forkService.getFork()).to.equal(fork, "Wrong resolved value on forkService.getStateFork()"); }) ); - expect(apiClient.beacon.state.getFork.callCount).to.equal(1, "getFork must only be called once"); + expect(api.beacon.getStateFork.callCount).to.equal(1, "getStateFork must only be called once"); }); }); diff --git a/packages/validator/test/unit/services/syncCommitteDuties.test.ts b/packages/validator/test/unit/services/syncCommitteDuties.test.ts index d6e05990a2..5f20adfc29 100644 --- a/packages/validator/test/unit/services/syncCommitteDuties.test.ts +++ b/packages/validator/test/unit/services/syncCommitteDuties.test.ts @@ -5,11 +5,11 @@ import sinon from "sinon"; import bls from "@chainsafe/bls"; import {createIBeaconConfig} from "@chainsafe/lodestar-config"; import {config as mainnetConfig} from "@chainsafe/lodestar-config/mainnet"; -import {altair} from "@chainsafe/lodestar-types"; import {toHexString} from "@chainsafe/ssz"; +import {routes} from "@chainsafe/lodestar-api"; import {SyncCommitteeDutiesService} from "../../../src/services/syncCommitteeDuties"; import {ValidatorStore} from "../../../src/services/validatorStore"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {ClockMock} from "../../utils/clock"; import {IndicesService} from "../../../src/services/indices"; @@ -21,7 +21,7 @@ describe("SyncCommitteeDutiesService", function () { const logger = testLogger(); const ZERO_HASH = Buffer.alloc(32, 0); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & sinon.SinonStubbedInstance; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized @@ -31,10 +31,14 @@ describe("SyncCommitteeDutiesService", function () { ALTAIR_FORK_EPOCH: 0, // Activate Altair immediatelly }); - // Sample validator - const defaultValidator = config.types.phase0.ValidatorResponse.defaultValue(); const index = 4; - defaultValidator.index = index; + // Sample validator + const defaultValidator: routes.beacon.ValidatorResponse = { + index, + balance: BigInt(32e9), + status: "active", + validator: config.types.phase0.Validator.defaultValue(), + }; before(() => { const secretKeys = [bls.SecretKey.fromBytes(toBufferBE(BigInt(98), 32))]; @@ -56,31 +60,24 @@ describe("SyncCommitteeDutiesService", function () { index, validator: {...defaultValidator.validator, pubkey: pubkeys[0]}, }; - apiClient.beacon.state.getStateValidators.resolves([validatorResponse]); + api.beacon.getStateValidators.resolves({data: [validatorResponse]}); // Reply with some duties const slot = 1; - const duty: altair.SyncDuty = { + const duty: routes.validator.SyncDuty = { pubkey: pubkeys[0], validatorIndex: index, validatorSyncCommitteeIndices: [7], }; - apiClient.validator.getSyncCommitteeDuties.resolves({dependentRoot: ZERO_HASH, data: [duty]}); + api.validator.getSyncCommitteeDuties.resolves({dependentRoot: ZERO_HASH, data: [duty]}); // Accept all subscriptions - apiClient.validator.prepareSyncCommitteeSubnets.resolves(); + api.validator.prepareSyncCommitteeSubnets.resolves(); // Clock will call runAttesterDutiesTasks() immediatelly const clock = new ClockMock(); - const indicesService = new IndicesService(logger, apiClient, validatorStore); - const dutiesService = new SyncCommitteeDutiesService( - config, - logger, - apiClient, - clock, - validatorStore, - indicesService - ); + const indicesService = new IndicesService(logger, api, validatorStore); + const dutiesService = new SyncCommitteeDutiesService(config, logger, api, clock, validatorStore, indicesService); // Trigger clock onSlot for slot 0 await clock.tickEpochFns(0, controller.signal); @@ -115,7 +112,7 @@ describe("SyncCommitteeDutiesService", function () { "Wrong getAttestersAtSlot()" ); - expect(apiClient.validator.prepareSyncCommitteeSubnets.callCount).to.equal( + expect(api.validator.prepareSyncCommitteeSubnets.callCount).to.equal( 1, "prepareSyncCommitteeSubnets() must be called once after getting the duties" ); diff --git a/packages/validator/test/unit/services/syncCommittee.test.ts b/packages/validator/test/unit/services/syncCommittee.test.ts index a1c02afb88..00f1f9ce85 100644 --- a/packages/validator/test/unit/services/syncCommittee.test.ts +++ b/packages/validator/test/unit/services/syncCommittee.test.ts @@ -7,7 +7,7 @@ import {config as mainnetConfig} from "@chainsafe/lodestar-config/mainnet"; import {SyncCommitteeService} from "../../../src/services/syncCommittee"; import {SyncDutyAndProof} from "../../../src/services/syncCommitteeDuties"; import {ValidatorStore} from "../../../src/services/validatorStore"; -import {ApiClientStub} from "../../utils/apiStub"; +import {getApiClientStub} from "../../utils/apiStub"; import {testLogger} from "../../utils/logger"; import {ClockMock} from "../../utils/clock"; import {IndicesService} from "../../../src/services/indices"; @@ -19,7 +19,7 @@ describe("SyncCommitteeService", function () { const logger = testLogger(); const ZERO_HASH = Buffer.alloc(32, 0); - const apiClient = ApiClientStub(sandbox); + const api = getApiClientStub(sandbox); const validatorStore = sinon.createStubInstance(ValidatorStore) as ValidatorStore & sinon.SinonStubbedInstance; let pubkeys: Uint8Array[]; // Initialize pubkeys in before() so bls is already initialized @@ -46,15 +46,8 @@ describe("SyncCommitteeService", function () { it("Should produce, sign, and publish a sync committee + contribution", async () => { const clock = new ClockMock(); - const indicesService = new IndicesService(logger, apiClient, validatorStore); - const syncCommitteeService = new SyncCommitteeService( - config, - logger, - apiClient, - clock, - validatorStore, - indicesService - ); + const indicesService = new IndicesService(logger, api, validatorStore); + const syncCommitteeService = new SyncCommitteeService(config, logger, api, clock, validatorStore, indicesService); const beaconBlockRoot = Buffer.alloc(32, 0x4d); const syncCommitteeSignature = config.types.altair.SyncCommitteeSignature.defaultValue(); @@ -73,18 +66,18 @@ describe("SyncCommitteeService", function () { ]; // Return empty replies to duties service - apiClient.beacon.state.getStateValidators.resolves([]); - apiClient.validator.getSyncCommitteeDuties.resolves({dependentRoot: ZERO_HASH, data: []}); + api.beacon.getStateValidators.resolves({data: []}); + api.validator.getSyncCommitteeDuties.resolves({dependentRoot: ZERO_HASH, data: []}); // Mock duties service to return some duties directly syncCommitteeService["dutiesService"].getDutiesAtSlot = sinon.stub().returns(duties); // Mock beacon's sync committee and contribution routes - apiClient.beacon.blocks.getBlockRoot.resolves(beaconBlockRoot); - apiClient.beacon.pool.submitSyncCommitteeSignatures.resolves(); - apiClient.validator.produceSyncCommitteeContribution.resolves(contribution); - apiClient.validator.publishContributionAndProofs.resolves(); + api.beacon.getBlockRoot.resolves({data: beaconBlockRoot}); + api.beacon.submitPoolSyncCommitteeSignatures.resolves(); + api.validator.produceSyncCommitteeContribution.resolves({data: contribution}); + api.validator.publishContributionAndProofs.resolves(); // Mock signing service validatorStore.signSyncCommitteeSignature.resolves(syncCommitteeSignature); @@ -94,21 +87,21 @@ describe("SyncCommitteeService", function () { await clock.tickSlotFns(0, controller.signal); // Must submit the signature received through signSyncCommitteeSignature() - expect(apiClient.beacon.pool.submitSyncCommitteeSignatures.callCount).to.equal( + expect(api.beacon.submitPoolSyncCommitteeSignatures.callCount).to.equal( 1, - "submitSyncCommitteeSignatures() must be called once" + "submitPoolSyncCommitteeSignatures() must be called once" ); - expect(apiClient.beacon.pool.submitSyncCommitteeSignatures.getCall(0).args).to.deep.equal( + expect(api.beacon.submitPoolSyncCommitteeSignatures.getCall(0).args).to.deep.equal( [[syncCommitteeSignature]], // 1 arg, = syncCommitteeSignature[] - "wrong submitSyncCommitteeSignatures() args" + "wrong submitPoolSyncCommitteeSignatures() args" ); // Must submit the aggregate received through produceSyncCommitteeContribution() then signContributionAndProof() - expect(apiClient.validator.publishContributionAndProofs.callCount).to.equal( + expect(api.validator.publishContributionAndProofs.callCount).to.equal( 1, "publishContributionAndProofs() must be called once" ); - expect(apiClient.validator.publishContributionAndProofs.getCall(0).args).to.deep.equal( + expect(api.validator.publishContributionAndProofs.getCall(0).args).to.deep.equal( [[contributionAndProof]], // 1 arg, = contributionAndProof[] "wrong publishContributionAndProofs() args" ); diff --git a/packages/validator/test/unit/utils/aggregator.test.ts b/packages/validator/test/unit/utils/aggregator.test.ts index 27bb9da3e3..fa7a91f5e7 100644 --- a/packages/validator/test/unit/utils/aggregator.test.ts +++ b/packages/validator/test/unit/utils/aggregator.test.ts @@ -1,7 +1,7 @@ import {expect} from "chai"; -import {phase0} from "@chainsafe/lodestar-types"; import {config} from "@chainsafe/lodestar-config/mainnet"; import {fromHexString} from "@chainsafe/ssz"; +import {routes} from "@chainsafe/lodestar-api"; import {isAttestationAggregator, isSyncCommitteeAggregator} from "../../../src/util/aggregator"; import {createIBeaconConfig} from "@chainsafe/lodestar-config"; import {SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params"; @@ -9,7 +9,7 @@ import {SYNC_COMMITTEE_SUBNET_COUNT} from "@chainsafe/lodestar-params"; /* eslint-disable @typescript-eslint/naming-convention */ describe("isAttestationAggregator", function () { - const duty: phase0.AttesterDuty = { + const duty: routes.validator.AttesterDuty = { slot: 1, committeeIndex: 2, committeeLength: 130, diff --git a/packages/validator/test/unit/utils/httpClient.test.ts b/packages/validator/test/unit/utils/httpClient.test.ts deleted file mode 100644 index 797edcd117..0000000000 --- a/packages/validator/test/unit/utils/httpClient.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {assert} from "chai"; -import Axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import {HttpClient} from "../../../src/util"; - -interface IUser { - id?: number; - name: string; -} - -describe("httpClient test", () => { - let mock: MockAdapter; - let httpClient: HttpClient; - - beforeEach(() => { - mock = new MockAdapter(Axios); - httpClient = new HttpClient({baseUrl: ""}); - }); - - it("should handle successful GET request correctly", async () => { - mock.onGet("/users/1").reply(200, {id: 1, name: "John Smith"}); - const user: IUser = await httpClient.get("/users/1"); - assert.equal(user.id, 1); - assert.equal(user.name, "John Smith"); - }); - - it("should handle successful GET request with query correctly", async () => { - mock.onGet("/users?id=1").reply(200, {id: 1, name: "John Smith"}); - const user: IUser = await httpClient.get("/users", {id: 1}); - assert.equal(user.id, 1); - assert.equal(user.name, "John Smith"); - }); - - it("should handle successful POST request correctly", async () => { - mock.onPost("/users", {name: "New comer"}).reply(200, "The user 'New comer' was saved successfully"); - const result: string = await httpClient.post("/users", {name: "New comer"}); - assert.equal(result, "The user 'New comer' was saved successfully"); - }); - - it("should handle http status code 404 correctly", async () => { - try { - await httpClient.get("/wrong_url"); - } catch (e) { - assert.equal((e as Error).message, "Endpoint not found"); - } - }); - - it("should handle http status code 500 correctly", async () => { - mock.onGet("/users/!").reply(500, "internal server error"); - try { - await httpClient.get("/users/!"); - } catch (e) { - assert.equal((e as Error).message, "Request failed with response status 500"); - } - }); -}); diff --git a/packages/validator/test/utils/apiStub.ts b/packages/validator/test/utils/apiStub.ts index 0410a0d73c..5278b4f083 100644 --- a/packages/validator/test/utils/apiStub.ts +++ b/packages/validator/test/utils/apiStub.ts @@ -1,20 +1,20 @@ import sinon, {SinonSandbox} from "sinon"; -import {ApiClientOverRest} from "../../src/api"; +import {getClient, Api} from "@chainsafe/lodestar-api"; import {config} from "@chainsafe/lodestar-config/mainnet"; -// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type -export function ApiClientStub(sandbox: SinonSandbox = sinon) { - const api = ApiClientOverRest(config, ""); +export function getApiClientStub( + sandbox: SinonSandbox = sinon +): Api & {[K in keyof Api]: sinon.SinonStubbedInstance} { + const api = getClient(config, {baseUrl: ""}); + return { - beacon: { - ...sandbox.stub(api.beacon), - state: sandbox.stub(api.beacon.state), - blocks: sandbox.stub(api.beacon.blocks), - pool: sandbox.stub(api.beacon.pool), - }, + beacon: sandbox.stub(api.beacon), + config: sandbox.stub(api.config), + debug: sandbox.stub(api.debug), + events: sandbox.stub(api.events), + lightclient: sandbox.stub(api.lightclient), + lodestar: sandbox.stub(api.lodestar), node: sandbox.stub(api.node), validator: sandbox.stub(api.validator), - events: sandbox.stub(api.events), - config: sandbox.stub(api.config), }; } diff --git a/yarn.lock b/yarn.lock index fba367c780..f1b1e5df7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2533,6 +2533,19 @@ unique-filename "^1.1.1" which "^1.3.1" +"@fastify/forwarded@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@fastify/forwarded/-/forwarded-1.0.0.tgz#cc4a3bc1f02856e56e67d6d655026e8d8c2e7429" + integrity sha512-VoO+6WD0aRz8bwgJZ8pkkxjq7o/782cQ1j945HWg0obZMgIadYW3Pew0+an+k1QL7IPZHM3db5WF6OP6x4ymMA== + +"@fastify/proxy-addr@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@fastify/proxy-addr/-/proxy-addr-3.0.0.tgz#7d3bb6474a01b206010329291f4edf6af8af582d" + integrity sha512-ty7wnUd/GeSqKTC2Jozsl5xGbnxUnEFC0On2/zPv/8ixywipQmVZwuWvNGnBoitJ2wixwVqofwXNua8j6Y62lQ== + dependencies: + "@fastify/forwarded" "^1.0.0" + ipaddr.js "^2.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -3915,6 +3928,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/qs@^6.9.6": + version "6.9.6" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" + integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== + "@types/react-dom@^16.8.4": version "16.9.12" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.12.tgz#55cd6b17e73922edb9545e5355a0016c1734e6f4" @@ -4497,7 +4515,7 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.12.0, ajv@^6.12.2: +ajv@^6.12.2: version "6.12.3" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== @@ -4945,14 +4963,15 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -avvio@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-6.5.0.tgz#d2cf119967fe90d2156afc29de350ced800cdaab" - integrity sha512-BmzcZ7gFpyFJsW8G+tfQw8vJNUboA9SDkkHLZ9RAALhvw/rplfWwni8Ee1rA11zj/J7/E5EvZmweusVvTHjWCA== +avvio@^7.1.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.2.tgz#58e00e7968870026cd7b7d4f689d596db629e251" + integrity sha512-XW2CMCmZaCmCCsIaJaLKxAzPwF37fXi1KGxNOvedOpeisLdmxZnblGc3hpHWYnlP+KOUxZsazh43WXNHgXpbqw== dependencies: archy "^1.0.0" debug "^4.0.0" - fastq "^1.6.0" + fastq "^1.6.1" + queue-microtask "^1.1.2" aws-sign2@~0.7.0: version "0.7.0" @@ -4964,15 +4983,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== -axios-mock-adapter@^1.17.0: - version "1.18.1" - resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.18.1.tgz#a2ba2638ef513d954793f96bde3e26bd4a1b7940" - integrity sha512-kFBZsG1Ma5yxjRGHq5KuuL55mPb7WzFULhypquEhzPg8SH5CXICb+qwC2CCA5u+GQVpiqGPwKSRkd3mBCs6gdw== - dependencies: - fast-deep-equal "^3.1.1" - is-buffer "^2.0.3" - -axios@^0.21.0, axios@^0.21.1: +axios@^0.21.0: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== @@ -5464,7 +5475,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.2.1, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== @@ -6437,13 +6448,6 @@ cross-env@^7.0.2: dependencies: cross-spawn "^7.0.1" -cross-fetch@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" - integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== - dependencies: - node-fetch "2.6.1" - cross-fetch@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" @@ -7715,11 +7719,6 @@ ethers@^5.0.2: "@ethersproject/web" "^5.0.0" "@ethersproject/wordlists" "^5.0.0" -event-iterator@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/event-iterator/-/event-iterator-2.0.0.tgz#10f06740cc1e9fd6bc575f334c2bc1ae9d2dbf62" - integrity sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ== - event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -7752,6 +7751,13 @@ eventsource@^1.0.7: dependencies: original "^1.0.0" +eventsource@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" + integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== + dependencies: + original "^1.0.0" + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -7938,7 +7944,7 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-decode-uri-component@^1.0.0: +fast-decode-uri-component@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== @@ -7953,6 +7959,11 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -7992,13 +8003,14 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-json-stringify@^1.18.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-1.21.0.tgz#51bc8c6d77d8c7b2cc7e5fa754f7f909f9e1262f" - integrity sha512-xY6gyjmHN3AK1Y15BCbMpeO9+dea5ePVsp3BouHCdukcx0hOHbXwFhRodhcI0NpZIgDChSeAKkHW9YjKvhwKBA== +fast-json-stringify@^2.5.2: + version "2.7.5" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.5.tgz#fdd348d204ba8fc81b3ab4bb947d10a50d953e48" + integrity sha512-VClYNkPo7tyZr0BMrRWraDMTJwjH6dIaHc/b/BiA4Z2MpxpKZBu45akYVb0dOVwQbF22zUMmhdg1WjrUjzAN2g== dependencies: ajv "^6.11.0" deepmerge "^4.2.2" + rfdc "^1.2.0" string-similarity "^4.0.1" fast-levenshtein@^2.0.6: @@ -8006,10 +8018,10 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-redact@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-2.0.0.tgz#17bb8f5e1f56ecf4a38c8455985e5eab4c478431" - integrity sha512-zxpkULI9W9MNTK2sJ3BpPQrTEXFNESd2X6O1tXMFpK/XM0G5c5Rll2EVYZH2TqI3xRGK/VaJ+eEOt7pnENJpeA== +fast-redact@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.1.tgz#d6015b971e933d03529b01333ba7f22c29961e92" + integrity sha512-kYpn4Y/valC9MdrISg47tZOpYBNoTXKgT9GYXFpHN/jYFs+lFkPoisY+LcBODdKVMY96ATzvzsWv+ES/4Kmufw== fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: version "2.0.7" @@ -8028,48 +8040,50 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== -fastify-cors@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/fastify-cors/-/fastify-cors-3.0.3.tgz#c1b2227983d7b02feff73fd642d81041adfbe124" - integrity sha512-SDMa+GtyTTAU7pWZwY4fukb/VwCZ4c30p0oEaE7/d/+VCvceB1+NzW2udp2dSZZfWR7J1kUookCpw2dLmtAsSw== +fastify-cors@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/fastify-cors/-/fastify-cors-6.0.1.tgz#300e0f1cbeedda19d9de284e9cf05c65842c182b" + integrity sha512-eeNTdQNmBsqHL87we+X74n9+H0hTDX0cXGVdyZjGf9om2pZfigAZwuSxaUUE2pLP9tp5+rEd5kejKQ8+ZCvAoA== dependencies: - fastify-plugin "^1.6.0" + fastify-plugin "^3.0.0" vary "^1.1.2" -fastify-plugin@^1.6.0, fastify-plugin@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-1.6.1.tgz#122f5a5eeb630d55c301713145a9d188e6d5dd5b" - integrity sha512-APBcb27s+MjaBIerFirYmBLatoPCgmHZM6XP0K+nDL9k0yX8NJPWDY1RAC3bh6z+AB5ULS2j31BUfLMT3uaZ4A== - dependencies: - semver "^6.3.0" +fastify-error@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.1.tgz#8eb993e15e3cf57f0357fc452af9290f1c1278d2" + integrity sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ== -fastify-sse-v2@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/fastify-sse-v2/-/fastify-sse-v2-1.0.7.tgz#c53997252b76236e23d3e740d29752eebf9347be" - integrity sha512-/wBqfvx6nt0sMG51LSFeeP7jDvCl01HeRHaRrsAdjrtjrjQXYOB4hOJY9mES2qgDidphU0lVhjohrb6hJV1GBQ== - dependencies: - fastify-plugin "^1.6.1" - it-to-stream "^0.1.1" +fastify-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca" + integrity sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w== -fastify@2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-2.15.3.tgz#ba941e9b62175f053ef01c3eea9fa76e91fffed1" - integrity sha512-2O+A9SjHpbH/SgDDMA+xIznhx/rDeNuwPIiZSFVU7fwOiiFfQjHmfu21jp22wMmsZ5PYKYFR+pze2TzoAUmOtw== +fastify-warning@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f" + integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw== + +fastify@3.15.1: + version "3.15.1" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.15.1.tgz#6d257cd7d04938d97e8fcd5d727bc363e5e4dabe" + integrity sha512-QZBGrSOwcR+IJF5OwYTZ5662wEd68SqC6sG4aMu0GncKbYlG9GF88EF2PzN2HfXCCD9K0d/+ZNowuF8S893mOg== dependencies: + "@fastify/proxy-addr" "^3.0.0" abstract-logging "^2.0.0" - ajv "^6.12.0" - avvio "^6.5.0" - fast-json-stringify "^1.18.0" - find-my-way "^2.2.2" + ajv "^6.12.2" + avvio "^7.1.2" + fast-json-stringify "^2.5.2" + fastify-error "^0.3.0" + fastify-warning "^0.2.0" + find-my-way "^4.0.0" flatstr "^1.0.12" - light-my-request "^3.7.3" - middie "^4.1.0" - pino "^5.17.0" - proxy-addr "^2.0.6" - readable-stream "^3.6.0" - rfdc "^1.1.2" - secure-json-parse "^2.1.0" - tiny-lru "^7.0.2" + light-my-request "^4.2.0" + pino "^6.2.1" + readable-stream "^3.4.0" + rfdc "^1.1.4" + secure-json-parse "^2.0.0" + semver "^7.3.2" + tiny-lru "^7.0.0" fastq@^1.6.0: version "1.8.0" @@ -8078,6 +8092,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fastq@^1.6.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" @@ -8176,12 +8197,13 @@ find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-my-way@^2.2.2: - version "2.2.5" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-2.2.5.tgz#86ce825266fa28cd962e538a45ec2aaa84c3d514" - integrity sha512-GjRZZlGcGmTh9t+6Xrj5K0YprpoAFCAiCPgmAH9Kb09O4oX6hYuckDfnDipYj+Q7B1GtYWSzDI5HEecNYscLQg== +find-my-way@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-4.1.0.tgz#e70aa10b3670cc8be96eb251357705644ea5087b" + integrity sha512-UBD94MdO6cBi6E97XA0fBA9nwqw+xG5x1TYIPHats33gEi/kNqy7BWHAWx8QHCQQRSU5Txc0JiD8nzba39gvMQ== dependencies: - fast-decode-uri-component "^1.0.0" + fast-decode-uri-component "^1.0.1" + fast-deep-equal "^3.1.3" safe-regex2 "^2.0.0" semver-store "^0.3.0" @@ -9470,6 +9492,11 @@ ipaddr.js@1.9.1, ipaddr.js@^1.9.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +ipaddr.js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.0.tgz#77ccccc8063ae71ab65c55f21b090698e763fc6e" + integrity sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w== + ipfs-utils@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/ipfs-utils/-/ipfs-utils-2.3.1.tgz#999951da4461b5901a5ad38329e247b5c14b7bd1" @@ -9575,16 +9602,16 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.3, is-buffer@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== - is-buffer@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== +is-buffer@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + is-callable@^1.1.4, is-callable@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" @@ -10219,18 +10246,6 @@ it-take@1.0.0: resolved "https://registry.yarnpkg.com/it-take/-/it-take-1.0.0.tgz#2319a39d91463b4bf6151289126aa44889eda903" integrity sha512-zfr2iAtekTGhHVWzCqqqgDnHhmzdzfCW92L0GvbaSFlvc3n2Ep/sponzmlNl2Kg39N5Py+02v+Aypc+i2c+9og== -it-to-stream@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/it-to-stream/-/it-to-stream-0.1.1.tgz#3fb4a9c4df868cd8f4aaf2071eba5ada5a3fad2a" - integrity sha512-QQx/58JBvT189imr6fD234F8aVf8EdyQHJR0MxXAOShEWK1NWyahPYIQt/tQG7PId0ZG/6/3tUiVCfw2cq+e1w== - dependencies: - buffer "^5.2.1" - fast-fifo "^1.0.0" - get-iterator "^1.0.2" - p-defer "^3.0.0" - p-fifo "^1.0.0" - readable-stream "^3.4.0" - it-to-stream@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/it-to-stream/-/it-to-stream-0.1.2.tgz#7163151f75b60445e86b8ab1a968666acaacfe7b" @@ -10858,14 +10873,15 @@ libp2p@^0.30.2: varint "^5.0.0" xsalsa20 "^1.0.2" -light-my-request@^3.7.3: - version "3.8.0" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-3.8.0.tgz#7da96786e4d479371b25cfd524ee05d5d583dae8" - integrity sha512-cIOWmNsgoStysmkzcv2EwvLwMb2hEm6oqKMerG/b5ey9F0we2Qony8cAZgBktmGPYUvPyKsDCzMcYU6fXbpWew== +light-my-request@^4.2.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.4.1.tgz#bfa2220316eef4f6465bf2f0667780da6b7f6a71" + integrity sha512-FDNRF2mYjthIRWE7O8d/X7AzDx4otQHl4/QXbu3Q/FRwBFcgb+ZoDaUd5HwN53uQXLAiw76osN+Va0NEaOW6rQ== dependencies: - ajv "^6.10.2" + ajv "^6.12.2" cookie "^0.4.0" - readable-stream "^3.4.0" + fastify-warning "^0.2.0" + readable-stream "^3.6.0" set-cookie-parser "^2.4.1" load-json-file@^1.0.0: @@ -11389,14 +11405,6 @@ micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" -middie@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/middie/-/middie-4.1.0.tgz#0fe986c83d5081489514ca1a2daba5ca36263436" - integrity sha512-eylPpZA+K3xO9kpDjagoPkEUkNcWV3EAo5OEz0MqsekUpT7KbnQkk8HNZkh4phx2vvOAmNNZuLRWF9lDDHPpVQ== - dependencies: - path-to-regexp "^4.0.0" - reusify "^1.0.2" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -12978,11 +12986,6 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" -path-to-regexp@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-4.0.5.tgz#2d4fd140af9a369bf7b68f77a7fdc340490f4239" - integrity sha512-l+fTaGG2N9ZRpCEUj5fG1VKdDLaiqwCIvPngpnxzREhcdobhZC4ou4w984HBu72DqAJ5CfcdV6tjqNOunfpdsQ== - path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -13113,22 +13116,22 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= -pino-std-serializers@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.4.2.tgz#cb5e3e58c358b26f88969d7e619ae54bdfcc1ae1" - integrity sha512-WaL504dO8eGs+vrK+j4BuQQq6GLKeCCcHaMB2ItygzVURcL1CycwNEUHTD/lHFHs/NL5qAz2UKrjYWXKSf4aMQ== +pino-std-serializers@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671" + integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg== -pino@^5.17.0: - version "5.17.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-5.17.0.tgz#b9def314e82402154f89a25d76a31f20ca84b4c8" - integrity sha512-LqrqmRcJz8etUjyV0ddqB6OTUutCgQULPFg2b4dtijRHUsucaAdBgSUW58vY6RFSX+NT8963F+q0tM6lNwGShA== +pino@^6.2.1: + version "6.11.3" + resolved "https://registry.yarnpkg.com/pino/-/pino-6.11.3.tgz#0c02eec6029d25e6794fdb6bbea367247d74bc29" + integrity sha512-drPtqkkSf0ufx2gaea3TryFiBHdNIdXKf5LN0hTM82SXI4xVIve2wLwNg92e1MT6m3jASLu6VO7eGY6+mmGeyw== dependencies: - fast-redact "^2.0.0" + fast-redact "^3.0.0" fast-safe-stringify "^2.0.7" flatstr "^1.0.12" - pino-std-serializers "^2.4.2" - quick-format-unescaped "^3.0.3" - sonic-boom "^0.7.5" + pino-std-serializers "^3.1.0" + quick-format-unescaped "^4.0.3" + sonic-boom "^1.0.2" pirates@^4.0.0: version "4.0.1" @@ -13425,7 +13428,7 @@ protons@^2.0.0: uint8arrays "^1.0.0" varint "^5.0.0" -proxy-addr@^2.0.6, proxy-addr@~2.0.5: +proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== @@ -13518,6 +13521,13 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + qs@^6.5.1: version "6.9.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" @@ -13551,10 +13561,15 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -quick-format-unescaped@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-3.0.3.tgz#fb3e468ac64c01d22305806c39f121ddac0d1fb9" - integrity sha512-dy1yjycmn9blucmJLXOfZDx1ikZJUi6E8bBZLnhPG5gBrVhHXx2xVyqqgKBubVNEXmx51dBACMHpoMQK/N/AXQ== +queue-microtask@^1.1.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-format-unescaped@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652" + integrity sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg== quick-lru@^1.0.0: version "1.1.0" @@ -14132,7 +14147,7 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -reusify@^1.0.2, reusify@^1.0.4: +reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== @@ -14151,10 +14166,10 @@ rewiremock@^3.14.3: wipe-node-cache "^2.1.2" wipe-webpack-cache "^2.1.0" -rfdc@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" - integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== +rfdc@^1.1.4, rfdc@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" @@ -14385,10 +14400,10 @@ secp256k1@^4.0.1: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" -secure-json-parse@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20" - integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA== +secure-json-parse@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85" + integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg== select-hose@^2.0.0: version "2.0.0" @@ -14808,10 +14823,10 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" -sonic-boom@^0.7.5: - version "0.7.7" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-0.7.7.tgz#d921de887428208bfa07b0ae32c278de043f350a" - integrity sha512-Ei5YOo5J64GKClHIL/5evJPgASXFVpfVYbJV9PILZQytTK6/LCwHvsZJW2Ig4p9FMC2OrBrMnXKgRN/OEoAWfg== +sonic-boom@^1.0.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" + integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg== dependencies: atomic-sleep "^1.0.0" flatstr "^1.0.12" @@ -15682,7 +15697,7 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tiny-lru@^7.0.2: +tiny-lru@^7.0.0: version "7.0.6" resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24" integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==