# beacon_chain # Copyright (c) 2018-2024 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: [].} import chronicles, chronos, snappy, snappy/codec, ../spec/[helpers, forks, network], ../networking/eth2_network, ../consensus_object_pools/blockchain_dag, ../rpc/rest_constants logScope: topics = "lc_proto" const lightClientBootstrapResponseCost = allowedOpsPerSecondCost(1) ## Only one bootstrap per peer should ever be needed - no need to allow more lightClientUpdateResponseCost = allowedOpsPerSecondCost(1000) ## Updates are tiny - we can allow lots of them lightClientFinalityUpdateResponseCost = allowedOpsPerSecondCost(100) lightClientOptimisticUpdateResponseCost = allowedOpsPerSecondCost(100) type LightClientNetworkState* {.final.} = ref object of RootObj dag*: ChainDAGRef proc readChunkPayload*( conn: Connection, peer: Peer, MsgType: type SomeForkedLightClientObject): Future[NetRes[MsgType]] {.async: (raises: [CancelledError]).} = var contextBytes: ForkDigest try: await conn.readExactly(addr contextBytes, sizeof contextBytes) except CatchableError: return neterr UnexpectedEOF let contextFork = peer.network.forkDigests[].consensusForkForDigest(contextBytes).valueOr: return neterr InvalidContextBytes withLcDataFork(lcDataForkAtConsensusFork(contextFork)): when lcDataFork > LightClientDataFork.None: let res = await eth2_network.readChunkPayload( conn, peer, MsgType.Forky(lcDataFork)) if res.isOk: if contextFork != peer.network.cfg.consensusForkAtEpoch(res.get.contextEpoch): return neterr InvalidContextBytes return ok MsgType.init(res.get) else: return err(res.error) else: return neterr InvalidContextBytes {.pop.} func forkDigestAtEpoch(state: LightClientNetworkState, epoch: Epoch): ForkDigest = state.dag.forkDigests[].atEpoch(epoch, state.dag.cfg) p2pProtocol LightClientSync(version = 1, networkState = LightClientNetworkState): # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.5/specs/altair/light-client/p2p-interface.md#getlightclientbootstrap proc lightClientBootstrap( peer: Peer, blockRoot: Eth2Digest, response: SingleChunkResponse[ForkedLightClientBootstrap]) {.async, libp2pProtocol("light_client_bootstrap", 1).} = trace "Received LC bootstrap request", peer, blockRoot let dag = peer.networkState.dag doAssert dag.lcDataStore.serve let bootstrap = dag.getLightClientBootstrap(blockRoot) withForkyBootstrap(bootstrap): when lcDataFork > LightClientDataFork.None: let contextEpoch = forkyBootstrap.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data # TODO extract from libp2pProtocol peer.awaitQuota( lightClientBootstrapResponseCost, "light_client_bootstrap/1") await response.sendSSZ(forkyBootstrap, contextBytes) else: raise newException(ResourceUnavailableError, LCBootstrapUnavailable) debug "LC bootstrap request done", peer, blockRoot # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.9/specs/altair/light-client/p2p-interface.md#lightclientupdatesbyrange proc lightClientUpdatesByRange( peer: Peer, startPeriod: SyncCommitteePeriod, reqCount: uint64, response: MultipleChunksResponse[ ForkedLightClientUpdate, MAX_REQUEST_LIGHT_CLIENT_UPDATES]) {.async, libp2pProtocol("light_client_updates_by_range", 1).} = trace "Received LC updates by range request", peer, startPeriod, reqCount let dag = peer.networkState.dag doAssert dag.lcDataStore.serve let headPeriod = dag.head.slot.sync_committee_period # Limit number of updates in response maxSupportedCount = if startPeriod > headPeriod: 0'u64 else: min(headPeriod + 1 - startPeriod, MAX_REQUEST_LIGHT_CLIENT_UPDATES) count = min(reqCount, maxSupportedCount) onePastPeriod = startPeriod + count var found = 0 for period in startPeriod.. LightClientDataFork.None: let contextEpoch = forkyUpdate.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data # TODO extract from libp2pProtocol peer.awaitQuota( lightClientUpdateResponseCost, "light_client_updates_by_range/1") await response.writeSSZ(forkyUpdate, contextBytes) inc found else: discard debug "LC updates by range request done", peer, startPeriod, count, found # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.9/specs/altair/light-client/p2p-interface.md#getlightclientfinalityupdate proc lightClientFinalityUpdate( peer: Peer, response: SingleChunkResponse[ForkedLightClientFinalityUpdate]) {.async, libp2pProtocol("light_client_finality_update", 1).} = trace "Received LC finality update request", peer let dag = peer.networkState.dag doAssert dag.lcDataStore.serve let finality_update = dag.getLightClientFinalityUpdate() withForkyFinalityUpdate(finality_update): when lcDataFork > LightClientDataFork.None: let contextEpoch = forkyFinalityUpdate.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data # TODO extract from libp2pProtocol peer.awaitQuota( lightClientFinalityUpdateResponseCost, "light_client_finality_update/1") await response.sendSSZ(forkyFinalityUpdate, contextBytes) else: raise newException(ResourceUnavailableError, LCFinUpdateUnavailable) debug "LC finality update request done", peer # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.9/specs/altair/light-client/p2p-interface.md#getlightclientoptimisticupdate proc lightClientOptimisticUpdate( peer: Peer, response: SingleChunkResponse[ForkedLightClientOptimisticUpdate]) {.async, libp2pProtocol("light_client_optimistic_update", 1).} = trace "Received LC optimistic update request", peer let dag = peer.networkState.dag doAssert dag.lcDataStore.serve let optimistic_update = dag.getLightClientOptimisticUpdate() withForkyOptimisticUpdate(optimistic_update): when lcDataFork > LightClientDataFork.None: let contextEpoch = forkyOptimisticUpdate.contextEpoch contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data # TODO extract from libp2pProtocol peer.awaitQuota( lightClientOptimisticUpdateResponseCost, "light_client_optimistic_update/1") await response.sendSSZ(forkyOptimisticUpdate, contextBytes) else: raise newException(ResourceUnavailableError, LCOptUpdateUnavailable) debug "LC optimistic update request done", peer proc init*(T: type LightClientSync.NetworkState, dag: ChainDAGRef): T = T( dag: dag, )