diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index a505616e4..b17396b49 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -21,7 +21,7 @@ import ./consensus_object_pools/[ blockchain_dag, block_quarantine, exit_pool, attestation_pool, sync_committee_msg_pool], - ./spec/datatypes/base, + ./spec/datatypes/[base, altair], ./sync/[optimistic_sync_light_client, sync_manager, request_manager], ./validators/[action_tracker, validator_monitor, validator_pool], ./rpc/state_ttl_cache @@ -41,6 +41,8 @@ type blocksQueue*: AsyncEventQueue[ForkedTrustedSignedBeaconBlock] headQueue*: AsyncEventQueue[HeadChangeInfoObject] reorgQueue*: AsyncEventQueue[ReorgInfoObject] + finUpdateQueue*: AsyncEventQueue[altair.LightClientFinalityUpdate] + optUpdateQueue*: AsyncEventQueue[altair.LightClientOptimisticUpdate] attestQueue*: AsyncEventQueue[Attestation] contribQueue*: AsyncEventQueue[SignedContributionAndProof] exitQueue*: AsyncEventQueue[SignedVoluntaryExit] diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 583df70f0..cbea71584 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -161,9 +161,9 @@ proc loadChainDag( proc onChainReorg(data: ReorgInfoObject) = eventBus.reorgQueue.emit(data) proc onLightClientFinalityUpdate(data: altair.LightClientFinalityUpdate) = - discard + eventBus.finUpdateQueue.emit(data) proc onLightClientOptimisticUpdate(data: altair.LightClientOptimisticUpdate) = - discard + eventBus.optUpdateQueue.emit(data) let chainDagFlags = @@ -374,6 +374,8 @@ proc init*(T: type BeaconNode, blocksQueue: newAsyncEventQueue[ForkedTrustedSignedBeaconBlock](), headQueue: newAsyncEventQueue[HeadChangeInfoObject](), reorgQueue: newAsyncEventQueue[ReorgInfoObject](), + finUpdateQueue: newAsyncEventQueue[altair.LightClientFinalityUpdate](), + optUpdateQueue: newAsyncEventQueue[altair.LightClientOptimisticUpdate](), attestQueue: newAsyncEventQueue[Attestation](), contribQueue: newAsyncEventQueue[SignedContributionAndProof](), exitQueue: newAsyncEventQueue[SignedVoluntaryExit](), @@ -1315,6 +1317,8 @@ proc installRestHandlers(restServer: RestServerRef, node: BeaconNode) = restServer.router.installNimbusApiHandlers(node) restServer.router.installNodeApiHandlers(node) restServer.router.installValidatorApiHandlers(node) + if node.dag.lightClientDataServe: + restServer.router.installLightClientApiHandlers(node) proc installMessageValidators(node: BeaconNode) = # https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#attestations-and-aggregation diff --git a/beacon_chain/rpc/rest_api.nim b/beacon_chain/rpc/rest_api.nim index aa1f9bb55..ffd0d48e9 100644 --- a/beacon_chain/rpc/rest_api.nim +++ b/beacon_chain/rpc/rest_api.nim @@ -1,22 +1,28 @@ -# Copyright (c) 2018-2021 Status Research & Development GmbH +# beacon_chain +# Copyright (c) 2018-2022 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. +{.push raises: [Defect].} + ## The `rest_api` module is a server implementation for the common REST API for ## Ethereum 2 found at https://ethereum.github.io/eth2.0-APIs/# ## along with several nimbus-specific extensions. It is used by the validator ## client as well as many community utilities. ## A corresponding client can be found in the ## `spec/eth2_apis/rest_beacon_client` module + import "."/[ rest_utils, rest_beacon_api, rest_config_api, rest_debug_api, rest_event_api, - rest_nimbus_api, rest_node_api, rest_validator_api, rest_key_management_api] + rest_key_management_api, rest_light_client_api, rest_nimbus_api, + rest_node_api, rest_validator_api] export rest_utils, rest_beacon_api, rest_config_api, rest_debug_api, rest_event_api, - rest_nimbus_api, rest_node_api, rest_validator_api, rest_key_management_api + rest_key_management_api, rest_light_client_api, rest_nimbus_api, + rest_node_api, rest_validator_api diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index dc12395b2..89d878a91 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -1,3 +1,12 @@ +# beacon_chain +# Copyright (c) 2021-2022 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.push raises: [Defect].} + import ../spec/beacon_time @@ -190,3 +199,19 @@ const "Invalid Authorization Header" PrunedStateError* = "Trying to access a pruned historical state" + InvalidBlockRootValueError* = + "Invalid block root value" + InvalidSyncPeriodError* = + "Invalid sync committee period requested" + InvalidCountError* = + "Invalid count requested" + MissingStartPeriodValueError* = + "Missing `start_period` value" + MissingCountValueError* = + "Missing `count` value" + LCBootstrapUnavailable* = + "LC bootstrap unavailable" + LCFinUpdateUnavailable* = + "LC finality update unavailable" + LCOptUpdateUnavailable* = + "LC optimistic update unavailable" diff --git a/beacon_chain/rpc/rest_event_api.nim b/beacon_chain/rpc/rest_event_api.nim index e44991472..3967dbbab 100644 --- a/beacon_chain/rpc/rest_event_api.nim +++ b/beacon_chain/rpc/rest_event_api.nim @@ -1,9 +1,12 @@ -# Copyright (c) 2018-2020 Status Research & Development GmbH +# beacon_chain +# Copyright (c) 2018-2022 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. +{.push raises: [Defect].} + import stew/results, chronicles, @@ -14,40 +17,20 @@ export rest_utils logScope: topics = "rest_eventapi" -proc validateEventTopics(events: seq[EventTopic]): Result[EventTopics, - cstring] = +proc validateEventTopics(events: seq[EventTopic], + withLightClient: bool): Result[EventTopics, cstring] = const NonUniqueError = cstring("Event topics must be unique") + const UnsupportedError = cstring("Unsupported event topic value") var res: set[EventTopic] for item in events: - case item - of EventTopic.Head: - if EventTopic.Head in res: - return err(NonUniqueError) - res.incl(EventTopic.Head) - of EventTopic.Block: - if EventTopic.Block in res: - return err(NonUniqueError) - res.incl(EventTopic.Block) - of EventTopic.Attestation: - if EventTopic.Attestation in res: - return err(NonUniqueError) - res.incl(EventTopic.Attestation) - of EventTopic.VoluntaryExit: - if EventTopic.VoluntaryExit in res: - return err(NonUniqueError) - res.incl(EventTopic.VoluntaryExit) - of EventTopic.FinalizedCheckpoint: - if EventTopic.FinalizedCheckpoint in res: - return err(NonUniqueError) - res.incl(EventTopic.FinalizedCheckpoint) - of EventTopic.ChainReorg: - if EventTopic.ChainReorg in res: - return err(NonUniqueError) - res.incl(EventTopic.ChainReorg) - of EventTopic.ContributionAndProof: - if EventTopic.ContributionAndProof in res: - return err(NonUniqueError) - res.incl(EventTopic.ContributionAndProof) + if item in res: + return err(NonUniqueError) + if not withLightClient and item in [ + EventTopic.LightClientFinalityUpdate, + EventTopic.LightClientOptimisticUpdate]: + return err(UnsupportedError) + res.incl(item) + if res == {}: err("Empty topics list") else: @@ -105,6 +88,7 @@ proc eventHandler*[T](response: HttpResponseRef, proc installEventApiHandlers*(router: var RestRouter, node: BeaconNode) = # https://ethereum.github.io/beacon-APIs/#/Events/eventstream + # https://github.com/ethereum/beacon-APIs/pull/181 router.api(MethodGet, "/eth/v1/events") do ( topics: seq[EventTopic]) -> RestApiResponse: let eventTopics = @@ -112,7 +96,8 @@ proc installEventApiHandlers*(router: var RestRouter, node: BeaconNode) = if topics.isErr(): return RestApiResponse.jsonError(Http400, "Invalid topics value", $topics.error()) - let res = validateEventTopics(topics.get()) + let res = validateEventTopics(topics.get(), + node.dag.lightClientDataServe) if res.isErr(): return RestApiResponse.jsonError(Http400, "Invalid topics value", $res.error()) @@ -164,6 +149,16 @@ proc installEventApiHandlers*(router: var RestRouter, node: BeaconNode) = let handler = response.eventHandler(node.eventBus.contribQueue, "contribution_and_proof") res.add(handler) + if EventTopic.LightClientFinalityUpdate in eventTopics: + doAssert node.dag.lightClientDataServe + let handler = response.eventHandler(node.eventBus.finUpdateQueue, + "light_client_finality_update_v0") + res.add(handler) + if EventTopic.LightClientOptimisticUpdate in eventTopics: + doAssert node.dag.lightClientDataServe + let handler = response.eventHandler(node.eventBus.optUpdateQueue, + "light_client_optimistic_update_v0") + res.add(handler) res discard await one(handlers) diff --git a/beacon_chain/rpc/rest_light_client_api.nim b/beacon_chain/rpc/rest_light_client_api.nim new file mode 100644 index 000000000..a17f14a78 --- /dev/null +++ b/beacon_chain/rpc/rest_light_client_api.nim @@ -0,0 +1,94 @@ +# beacon_chain +# Copyright (c) 2021-2022 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.push raises: [Defect].} + +import chronicles +import ../beacon_node, + ./rest_utils + +logScope: topics = "rest_light_client" + +proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) = + # https://github.com/ethereum/beacon-APIs/pull/181 + router.api(MethodGet, + "/eth/v0/beacon/light_client/bootstrap/{block_root}") do ( + block_root: Eth2Digest) -> RestApiResponse: + doAssert node.dag.lightClientDataServe + let vroot = block: + if block_root.isErr(): + return RestApiResponse.jsonError(Http400, InvalidBlockRootValueError, + $block_root.error()) + block_root.get() + + let bootstrap = node.dag.getLightClientBootstrap(vroot) + if bootstrap.isOk: + return RestApiResponse.jsonResponse(bootstrap) + else: + return RestApiResponse.jsonError(Http404, LCBootstrapUnavailable) + + # https://github.com/ethereum/beacon-APIs/pull/181 + router.api(MethodGet, + "/eth/v0/beacon/light_client/updates") do ( + start_period: Option[SyncCommitteePeriod], count: Option[uint64] + ) -> RestApiResponse: + doAssert node.dag.lightClientDataServe + let vstart = block: + if start_period.isNone(): + return RestApiResponse.jsonError(Http400, MissingStartPeriodValueError) + let rstart = start_period.get() + if rstart.isErr(): + return RestApiResponse.jsonError(Http400, InvalidSyncPeriodError, + $rstart.error()) + rstart.get() + let vcount = block: + if count.isNone(): + return RestApiResponse.jsonError(Http400, MissingCountValueError) + let rcount = count.get() + if rcount.isErr(): + return RestApiResponse.jsonError(Http400, InvalidCountError, + $rcount.error()) + rcount.get() + let + headPeriod = node.dag.head.slot.sync_committee_period + # Limit number of updates in response + maxSupportedCount = + if vstart > headPeriod: + 0'u64 + else: + min(headPeriod + 1 - vstart, MAX_REQUEST_LIGHT_CLIENT_UPDATES) + numPeriods = min(vcount, maxSupportedCount) + onePastPeriod = vstart + numPeriods + + var updates = newSeqOfCap[LightClientUpdate](numPeriods) + for period in vstart.. RestApiResponse: + doAssert node.dag.lightClientDataServe + let finality_update = node.dag.getLightClientFinalityUpdate() + if finality_update.isSome: + return RestApiResponse.jsonResponse(finality_update) + else: + return RestApiResponse.jsonError(Http404, LCFinUpdateUnavailable) + + # https://github.com/ethereum/beacon-APIs/pull/181 + router.api(MethodGet, + "/eth/v0/beacon/light_client/optimistic_update") do ( + ) -> RestApiResponse: + doAssert node.dag.lightClientDataServe + let optimistic_update = node.dag.getLightClientOptimisticUpdate() + if optimistic_update.isSome: + return RestApiResponse.jsonResponse(optimistic_update) + else: + return RestApiResponse.jsonError(Http404, LCOptUpdateUnavailable) diff --git a/beacon_chain/rpc/rest_utils.nim b/beacon_chain/rpc/rest_utils.nim index 94f2b7ffd..ad09aba51 100644 --- a/beacon_chain/rpc/rest_utils.nim +++ b/beacon_chain/rpc/rest_utils.nim @@ -1,3 +1,12 @@ +# beacon_chain +# Copyright (c) 2022 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.push raises: [Defect].} + import std/[options, macros], stew/byteutils, presto, ../spec/[forks], @@ -37,6 +46,8 @@ proc validate(key: string, value: string): int = 0 of "{validator_id}": 0 + of "{block_root}": + 0 else: 1 diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 0af27282b..cdbcc71f1 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -1,3 +1,4 @@ +# beacon_chain # Copyright (c) 2018-2022 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). @@ -2147,6 +2148,10 @@ proc decodeString*(t: typedesc[EventTopic], ok(EventTopic.ChainReorg) of "contribution_and_proof": ok(EventTopic.ContributionAndProof) + of "light_client_finality_update_v0": + ok(EventTopic.LightClientFinalityUpdate) + of "light_client_optimistic_update_v0": + ok(EventTopic.LightClientOptimisticUpdate) else: err("Incorrect event's topic value") @@ -2186,6 +2191,11 @@ proc decodeString*(t: typedesc[Epoch], value: string): Result[Epoch, cstring] = let res = ? Base10.decode(uint64, value) ok(Epoch(res)) +proc decodeString*(t: typedesc[SyncCommitteePeriod], + value: string): Result[SyncCommitteePeriod, cstring] = + let res = ? Base10.decode(uint64, value) + ok(SyncCommitteePeriod(res)) + proc decodeString*(t: typedesc[uint64], value: string): Result[uint64, cstring] = Base10.decode(uint64, value) diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index a18af6b0a..2693857ff 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -33,7 +33,7 @@ const type EventTopic* {.pure.} = enum Head, Block, Attestation, VoluntaryExit, FinalizedCheckpoint, ChainReorg, - ContributionAndProof + ContributionAndProof, LightClientFinalityUpdate, LightClientOptimisticUpdate EventTopics* = set[EventTopic] diff --git a/beacon_chain/sync/sync_protocol.nim b/beacon_chain/sync/sync_protocol.nim index b6c3a19f8..e49bb6295 100644 --- a/beacon_chain/sync/sync_protocol.nim +++ b/beacon_chain/sync/sync_protocol.nim @@ -19,7 +19,8 @@ import ../spec/[helpers, forks, network], ".."/[beacon_clock], ../networking/eth2_network, - ../consensus_object_pools/blockchain_dag + ../consensus_object_pools/blockchain_dag, + ../rpc/rest_constants logScope: topics = "sync" @@ -548,8 +549,7 @@ p2pProtocol BeaconSync(version = 1, await response.send(bootstrap.get, contextBytes) else: peer.updateRequestQuota(lightClientEmptyResponseCost) - raise newException( - ResourceUnavailableError, "LC bootstrap unavailable") + raise newException(ResourceUnavailableError, LCBootstrapUnavailable) peer.updateRequestQuota(lightClientBootstrapResponseCost) @@ -609,7 +609,7 @@ p2pProtocol BeaconSync(version = 1, peer.awaitNonNegativeRequestQuota() - let finality_update = dag.getLightClientFinalityUpdate + let finality_update = dag.getLightClientFinalityUpdate() if finality_update.isSome: let contextEpoch = finality_update.get.attested_header.slot.epoch @@ -617,8 +617,7 @@ p2pProtocol BeaconSync(version = 1, await response.send(finality_update.get, contextBytes) else: peer.updateRequestQuota(lightClientEmptyResponseCost) - raise newException(ResourceUnavailableError, - "LC finality update unavailable") + raise newException(ResourceUnavailableError, LCFinUpdateUnavailable) peer.updateRequestQuota(lightClientFinalityUpdateResponseCost) @@ -636,7 +635,7 @@ p2pProtocol BeaconSync(version = 1, peer.awaitNonNegativeRequestQuota() - let optimistic_update = dag.getLightClientOptimisticUpdate + let optimistic_update = dag.getLightClientOptimisticUpdate() if optimistic_update.isSome: let contextEpoch = optimistic_update.get.attested_header.slot.epoch @@ -644,8 +643,7 @@ p2pProtocol BeaconSync(version = 1, await response.send(optimistic_update.get, contextBytes) else: peer.updateRequestQuota(lightClientEmptyResponseCost) - raise newException(ResourceUnavailableError, - "LC optimistic update unavailable") + raise newException(ResourceUnavailableError, LCOptUpdateUnavailable) peer.updateRequestQuota(lightClientOptimisticUpdateResponseCost)