# 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 std/[typetraits, sequtils, sets], stew/base10, chronicles, metrics, ./rest_utils, ./state_ttl_cache, ../beacon_node, ../consensus_object_pools/[blockchain_dag, spec_cache, validator_change_pool], ../spec/[deposit_snapshots, eth2_merkleization, forks, network, validator], ../spec/mev/[bellatrix_mev, capella_mev], ../validators/message_router_mev export rest_utils logScope: topics = "rest_beaconapi" proc validateBeaconApiQueries*(key: string, value: string): int = ## This is rough validation procedure which should be simple and fast, ## because it will be used for query routing. case key of "{epoch}": 0 of "{slot}": 0 of "{peer_id}": 0 of "{state_id}": 0 of "{block_id}": 0 of "{validator_id}": 0 of "{block_root}": 0 of "{pubkey}": int(value.len != 98) else: 1 const AllValidatorFilterKinds = { ValidatorFilterKind.PendingInitialized, ValidatorFilterKind.PendingQueued, ValidatorFilterKind.ActiveOngoing, ValidatorFilterKind.ActiveExiting, ValidatorFilterKind.ActiveSlashed, ValidatorFilterKind.ExitedUnslashed, ValidatorFilterKind.ExitedSlashed, ValidatorFilterKind.WithdrawalPossible, ValidatorFilterKind.WithdrawalDone } proc validateFilter(filters: seq[ValidatorFilter]): Result[ValidatorFilter, cstring] = var res: ValidatorFilter for item in filters: if res * item != {}: return err("Validator status must be unique") res.incl(item) if res == {}: return ok(AllValidatorFilterKinds) ok(res) proc getStatus(validator: Validator, current_epoch: Epoch): Result[ValidatorFilterKind, cstring] = if validator.activation_epoch > current_epoch: # pending if validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH: ok(ValidatorFilterKind.PendingInitialized) else: # validator.activation_eligibility_epoch < FAR_FUTURE_EPOCH: ok(ValidatorFilterKind.PendingQueued) elif (validator.activation_epoch <= current_epoch) and (current_epoch < validator.exit_epoch): # active if validator.exit_epoch == FAR_FUTURE_EPOCH: ok(ValidatorFilterKind.ActiveOngoing) elif not validator.slashed: # validator.exit_epoch < FAR_FUTURE_EPOCH ok(ValidatorFilterKind.ActiveExiting) else: # validator.exit_epoch < FAR_FUTURE_EPOCH and validator.slashed: ok(ValidatorFilterKind.ActiveSlashed) elif (validator.exit_epoch <= current_epoch) and (current_epoch < validator.withdrawable_epoch): # exited if not validator.slashed: ok(ValidatorFilterKind.ExitedUnslashed) else: # validator.slashed ok(ValidatorFilterKind.ExitedSlashed) elif validator.withdrawable_epoch <= current_epoch: # withdrawal if validator.effective_balance != 0.Gwei: ok(ValidatorFilterKind.WithdrawalPossible) else: # validator.effective_balance == 0.Gwei ok(ValidatorFilterKind.WithdrawalDone) else: err("Invalid validator status") proc toString*(kind: ValidatorFilterKind): string = case kind of ValidatorFilterKind.PendingInitialized: "pending_initialized" of ValidatorFilterKind.PendingQueued: "pending_queued" of ValidatorFilterKind.ActiveOngoing: "active_ongoing" of ValidatorFilterKind.ActiveExiting: "active_exiting" of ValidatorFilterKind.ActiveSlashed: "active_slashed" of ValidatorFilterKind.ExitedUnslashed: "exited_unslashed" of ValidatorFilterKind.ExitedSlashed: "exited_slashed" of ValidatorFilterKind.WithdrawalPossible: "withdrawal_possible" of ValidatorFilterKind.WithdrawalDone: "withdrawal_done" proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4881.md router.api2(MethodGet, "/eth/v1/beacon/deposit_snapshot") do ( ) -> RestApiResponse: let snapshot = node.db.getDepositContractSnapshot().valueOr: # This can happen in a very short window after the client is started, # but the snapshot record still haven't been upgraded in the database. # Returning 404 should be easy to handle for the clients - they just need # to retry. return RestApiResponse.jsonError(Http404, NoFinalizedSnapshotAvailableError) RestApiResponse.jsonResponse(snapshot.getTreeSnapshot()) # https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis router.api2(MethodGet, "/eth/v1/beacon/genesis") do () -> RestApiResponse: RestApiResponse.jsonResponse( ( genesis_time: getStateField(node.dag.headState, genesis_time), genesis_validators_root: getStateField(node.dag.headState, genesis_validators_root), genesis_fork_version: node.dag.cfg.GENESIS_FORK_VERSION ) ) # https://ethereum.github.io/beacon-APIs/#/Beacon/getStateRoot router.api2(MethodGet, "/eth/v1/beacon/states/{state_id}/root") do ( state_id: StateIdent) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) node.withStateForBlockSlotId(bslot): return RestApiResponse.jsonResponseFinalized( (root: stateRoot), node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getStateFork router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/fork", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) node.withStateForBlockSlotId(bslot): return RestApiResponse.jsonResponseFinalized( ( previous_version: getStateField(state, fork).previous_version, current_version: getStateField(state, fork).current_version, epoch: getStateField(state, fork).epoch ), node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getStateFinalityCheckpoints router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/finality_checkpoints", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) node.withStateForBlockSlotId(bslot): return RestApiResponse.jsonResponseFinalized( ( previous_justified: getStateField(state, previous_justified_checkpoint), current_justified: getStateField(state, current_justified_checkpoint), finalized: getStateField(state, finalized_checkpoint) ), node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) proc getIndices( node: BeaconNode, validatorIds: openArray[ValidatorIdent], state: ForkedHashedBeaconState ): Result[seq[ValidatorIndex], RestErrorMessage] = var keyset: HashSet[ValidatorPubKey] indexset: HashSet[ValidatorIndex] let validatorsCount = lenu64(getStateField(state, validators)) for item in validatorIds: case item.kind of ValidatorQueryKind.Key: # Test for uniqueness of value. if keyset.containsOrIncl(item.key): return err(RestErrorMessage.init( Http400, NonUniqueValidatorIdError, $item.key)) of ValidatorQueryKind.Index: let vindex = item.index.toValidatorIndex().valueOr: case error of ValidatorIndexError.TooHighValue: return err(RestErrorMessage.init( Http400, TooHighValidatorIndexValueError)) of ValidatorIndexError.UnsupportedValue: return err(RestErrorMessage.init( Http500, UnsupportedValidatorIndexValueError)) if uint64(vindex) < validatorsCount: # We're only adding validator indices which are present in # validators list at this moment. if indexset.containsOrIncl(vindex): return err(RestErrorMessage.init( Http400, NonUniqueValidatorIdError, Base10.toString(uint64(vindex)))) if len(keyset) > 0: let optIndices = keysToIndices(node.restKeysCache, state, keyset.toSeq()) # Remove all the duplicates. for item in optIndices: # We ignore missing keys. if item.isSome(): indexset.incl(item.get()) ok(indexset.toSeq()) proc getValidators( node: BeaconNode, bslot: BlockSlotId, validatorsMask: ValidatorFilter, validatorIds: openArray[ValidatorIdent] ): RestApiResponse = node.withStateForBlockSlotId(bslot): let stateEpoch = getStateField(state, slot).epoch() indices = node.getIndices(validatorIds, state).valueOr: return RestApiResponse.jsonError(error) response = block: var res: seq[RestValidator] if len(indices) == 0: # Case when `len(indices) == 0 and len(validatorIds) != 0` means # that we can't find validator identifiers in state, so we should # return empty response. if len(validatorIds) == 0: # There are no indices, so we're going to filter all the # validators. for index, validator in getStateField(state, validators): let balance = getStateField(state, balances).item(index) status = validator.getStatus(stateEpoch).valueOr: return RestApiResponse.jsonError( Http400, ValidatorStatusNotFoundError, $error) if status in validatorsMask: res.add(RestValidator.init(ValidatorIndex(index), balance, toString(status), validator)) else: for index in indices: let validator = getStateField(state, validators).item(index) balance = getStateField(state, balances).item(index) status = validator.getStatus(stateEpoch).valueOr: return RestApiResponse.jsonError( Http400, ValidatorStatusNotFoundError, $error) if status in validatorsMask: res.add(RestValidator.init(index, balance, toString(status), validator)) res return RestApiResponse.jsonResponseFinalized( response, node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) proc getBalances( node: BeaconNode, bslot: BlockSlotId, validatorIds: openArray[ValidatorIdent] ): RestApiResponse = node.withStateForBlockSlotId(bslot): let indices = node.getIndices(validatorIds, state).valueOr: return RestApiResponse.jsonError(error) response = block: var res: seq[RestValidatorBalance] if len(indices) == 0: # Case when `len(indices) == 0 and len(validatorIds) != 0` means # that we can't find validator identifiers in state, so we should # return empty response. if len(validatorIds) == 0: # There are no indices, so we're going to return balances of all # known validators. for index, balance in getStateField(state, balances): res.add(RestValidatorBalance.init(ValidatorIndex(index), balance)) else: for index in indices: let balance = getStateField(state, balances).item(index) res.add(RestValidatorBalance.init(index, balance)) res return RestApiResponse.jsonResponseFinalized( response, node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidators router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/validators", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, id: seq[ValidatorIdent], status: seq[ValidatorFilter]) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError( Http404, StateNotFoundError, $error) validatorIds = block: if id.isErr(): return RestApiResponse.jsonError( Http400, InvalidValidatorIdValueError) let ires = id.get() if len(ires) > ServerMaximumValidatorIds: return RestApiResponse.jsonError( Http414, MaximumNumberOfValidatorIdsError) ires validatorsMask = block: if status.isErr(): return RestApiResponse.jsonError(Http400, InvalidValidatorStatusValueError) validateFilter(status.get()).valueOr: return RestApiResponse.jsonError( Http400, InvalidValidatorStatusValueError, $error) getValidators(node, bslot, validatorsMask, validatorIds) # https://ethereum.github.io/beacon-APIs/#/Beacon/postStateValidators router.metricsApi2( MethodPost, "/eth/v1/beacon/states/{state_id}/validators", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, contentBody: Option[ContentBody]) -> RestApiResponse: let (validatorIds, validatorsMask) = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let request = decodeBody(RestValidatorRequest, contentBody.get()).valueOr: return RestApiResponse.jsonError( Http400, InvalidRequestBodyError, $error) let ids = request.ids.valueOr: @[] filter = if request.status.isNone() or len(request.status.get) == 0: AllValidatorFilterKinds else: request.status.get (ids, filter) sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) getValidators(node, bslot, validatorsMask, validatorIds) # https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidator router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/validators/{validator_id}", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, validator_id: ValidatorIdent) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) vid = validator_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidValidatorIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) node.withStateForBlockSlotId(bslot): let current_epoch = getStateField(state, slot).epoch() validatorsCount = lenu64(getStateField(state, validators)) let vindex = block: case vid.kind of ValidatorQueryKind.Key: let optIndices = keysToIndices(node.restKeysCache, state, [vid.key]) if optIndices[0].isNone(): return RestApiResponse.jsonError(Http404, ValidatorNotFoundError) optIndices[0].get() of ValidatorQueryKind.Index: let vres = vid.index.toValidatorIndex() if vres.isErr(): case vres.error of ValidatorIndexError.TooHighValue: return RestApiResponse.jsonError(Http400, TooHighValidatorIndexValueError) of ValidatorIndexError.UnsupportedValue: return RestApiResponse.jsonError(Http500, UnsupportedValidatorIndexValueError) let index = vres.get() if uint64(index) >= validatorsCount: return RestApiResponse.jsonError(Http404, ValidatorNotFoundError) index let validator = getStateField(state, validators).item(vindex) balance = getStateField(state, balances).item(vindex) status = block: let sres = validator.getStatus(current_epoch) if sres.isErr(): return RestApiResponse.jsonError(Http400, ValidatorStatusNotFoundError, $sres.get()) toString(sres.get()) return RestApiResponse.jsonResponseFinalized( RestValidator.init(vindex, balance, status, validator), node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidatorBalances router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/validator_balances", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, id: seq[ValidatorIdent]) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) validatorIds = block: if id.isErr(): return RestApiResponse.jsonError( Http400, InvalidValidatorIdValueError) let ires = id.get() if len(ires) > ServerMaximumValidatorIds: return RestApiResponse.jsonError( Http400, MaximumNumberOfValidatorIdsError) ires getBalances(node, bslot, validatorIds) # https://ethereum.github.io/beacon-APIs/#/Beacon/postStateValidatorBalances router.metricsApi2( MethodPost, "/eth/v1/beacon/states/{state_id}/validator_balances", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, contentBody: Option[ContentBody]) -> RestApiResponse: let validatorIds = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let body = contentBody.get() decodeBody(seq[ValidatorIdent], body).valueOr: return RestApiResponse.jsonError( Http400, InvalidValidatorIdValueError, $error) sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) getBalances(node, bslot, validatorIds) # https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/committees", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, epoch: Option[Epoch], index: Option[CommitteeIndex], slot: Option[Slot]) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) let vepoch = if epoch.isSome(): let repoch = epoch.get() if repoch.isErr(): return RestApiResponse.jsonError(Http400, InvalidEpochValueError, $repoch.error) let res = repoch.get() if res > bslot.slot.epoch + MIN_SEED_LOOKAHEAD: return RestApiResponse.jsonError( Http400, InvalidEpochValueError, "Requested epoch more than 1 epoch past state epoch") if res + EPOCHS_PER_HISTORICAL_VECTOR < bslot.slot.epoch + MIN_SEED_LOOKAHEAD: return RestApiResponse.jsonError( Http400, InvalidEpochValueError, "Requested epoch earlier than what committees can be computed for") some(res) else: none[Epoch]() let vindex = if index.isSome(): let rindex = index.get() if rindex.isErr(): return RestApiResponse.jsonError(Http400, InvalidCommitteeIndexValueError, $rindex.error) some(rindex.get()) else: none[CommitteeIndex]() let vslot = if slot.isSome(): let rslot = slot.get() if rslot.isErr(): return RestApiResponse.jsonError(Http400, InvalidSlotValueError, $rslot.error) let res = rslot.get() if vepoch.isSome(): if res.epoch != vepoch.get(): return RestApiResponse.jsonError( Http400, InvalidSlotValueError, "Slot does not match requested epoch") else: if res.epoch > bslot.slot.epoch + 1: return RestApiResponse.jsonError( Http400, InvalidEpochValueError, "Requested slot more than 1 epoch past state epoch") if res.epoch + EPOCHS_PER_HISTORICAL_VECTOR < bslot.slot.epoch + MIN_SEED_LOOKAHEAD: return RestApiResponse.jsonError( Http400, InvalidEpochValueError, "Requested slot earlier than what committees can be computed for") some(res) else: none[Slot]() node.withStateForBlockSlotId(bslot): proc getCommittee(slot: Slot, index: CommitteeIndex): RestBeaconStatesCommittees = let validators = get_beacon_committee(state, slot, index, cache) RestBeaconStatesCommittees(index: index, slot: slot, validators: validators) proc forSlot(slot: Slot, cindex: Option[CommitteeIndex], res: var seq[RestBeaconStatesCommittees]) = let committees_per_slot = get_committee_count_per_slot( state, slot.epoch, cache) if cindex.isNone: for committee_index in get_committee_indices(committees_per_slot): res.add(getCommittee(slot, committee_index)) else: let idx = cindex.get() if idx < committees_per_slot: res.add(getCommittee(slot, idx)) var res: seq[RestBeaconStatesCommittees] let qepoch = if vepoch.isNone: epoch(getStateField(state, slot)) else: vepoch.get() if vslot.isNone(): for slot in qepoch.slots(): forSlot(slot, vindex, res) else: forSlot(vslot.get(), vindex, res) return RestApiResponse.jsonResponseFinalized( res, node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochSyncCommittees router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/sync_committees", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, epoch: Option[Epoch]) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) let qepoch = if epoch.isSome(): let repoch = epoch.get() if repoch.isErr(): return RestApiResponse.jsonError(Http400, InvalidEpochValueError, $repoch.error) let res = repoch.get() if res > MaxEpoch: return RestApiResponse.jsonError(Http400, EpochOverflowValueError) if res < node.dag.cfg.ALTAIR_FORK_EPOCH: return RestApiResponse.jsonError(Http400, EpochFromTheIncorrectForkError) res else: # If ``epoch`` not present then the sync committees for the epoch of # the state will be obtained. bslot.slot.epoch() node.withStateForBlockSlotId(bslot): let keys = block: let res = syncCommitteeParticipants(state, qepoch) if res.isErr(): return RestApiResponse.jsonError(Http400, $res.error) let kres = res.get() if len(kres) == 0: return RestApiResponse.jsonError(Http500, InternalServerError, "List of sync committee participants is empty") kres let indices = block: var res: seq[ValidatorIndex] let optIndices = keysToIndices(node.restKeysCache, state, keys) # Remove all the duplicates. for item in optIndices: if item.isNone(): # This should not be happened, because keys are from state. return RestApiResponse.jsonError(Http500, InternalServerError, "Could not get validator indices") res.add(item.get()) res let aggregates = block: var res: seq[seq[ValidatorIndex]] offset = 0 while true: let length = min(SYNC_SUBCOMMITTEE_SIZE, len(indices) - offset) if length == 0: break res.add(@(indices.toOpenArray(offset, offset + length - 1))) offset.inc(length) res return RestApiResponse.jsonResponseFinalized( RestEpochSyncCommittee(validators: indices, validator_aggregates: aggregates), node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getStateRandao # https://github.com/ethereum/beacon-APIs/blob/b3c4defa238aaa74bf22aa602aa1b24b68a4c78e/apis/beacon/states/randao.yaml router.metricsApi2( MethodGet, "/eth/v1/beacon/states/{state_id}/randao", {RestServerMetricsType.Status, Response}) do ( state_id: StateIdent, epoch: Option[Epoch]) -> RestApiResponse: let sid = state_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidStateIdValueError, $error) bslot = node.getBlockSlotId(sid).valueOr: if sid.kind == StateQueryKind.Root: # TODO (cheatfate): Its impossible to retrieve state by `state_root` # in current version of database. return RestApiResponse.jsonError(Http500, NoImplementationError) return RestApiResponse.jsonError(Http404, StateNotFoundError, $error) let qepoch = if epoch.isSome(): let repoch = epoch.get() if repoch.isErr(): return RestApiResponse.jsonError(Http400, InvalidEpochValueError, $repoch.error) let res = repoch.get() if res > MaxEpoch: return RestApiResponse.jsonError(Http400, EpochOverflowValueError) if res < node.dag.cfg.ALTAIR_FORK_EPOCH: return RestApiResponse.jsonError(Http400, EpochFromTheIncorrectForkError) if res > bslot.slot.epoch() + 1: return RestApiResponse.jsonError(Http400, EpochFromFutureError) res else: # If ``epoch`` not present then the RANDAO mix for the epoch of # the state will be obtained. bslot.slot.epoch() # Try to obtain RANDAO in an accelerated way let bsi = node.dag.atSlot(bslot.bid, (qepoch + 1).start_slot - 1) if bsi.isSome: let mix = node.dag.computeRandaoMix(bsi.get.bid) if mix.isSome: return RestApiResponse.jsonResponseWOpt( RestEpochRandao(randao: mix.get), node.getBidOptimistic(bsi.get.bid) ) # Fall back to full state computation node.withStateForBlockSlotId(bslot): withState(state): return RestApiResponse.jsonResponseFinalized( RestEpochRandao(randao: get_randao_mix(forkyState.data, qepoch)), node.getStateOptimistic(state), node.dag.isFinalized(bslot.bid) ) RestApiResponse.jsonError(Http404, StateNotFoundError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeaders router.api2(MethodGet, "/eth/v1/beacon/headers") do ( slot: Option[Slot], parent_root: Option[Eth2Digest]) -> RestApiResponse: # TODO (cheatfate): This call is incomplete, because structure # of database do not allow to query blocks by `parent_root`. let qslot = if slot.isSome(): let rslot = slot.get() if rslot.isErr(): return RestApiResponse.jsonError(Http400, InvalidSlotValueError, $rslot.error) rslot.get() else: node.dag.head.slot if parent_root.isSome(): let rroot = parent_root.get() if rroot.isErr(): return RestApiResponse.jsonError(Http400, InvalidParentRootValueError, $rroot.error) return RestApiResponse.jsonError(Http500, NoImplementationError) let bdata = node.getForkedBlock(BlockIdent.init(qslot)).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) withBlck(bdata): let bid = BlockId(root: forkyBlck.root, slot: forkyBlck.message.slot) RestApiResponse.jsonResponseFinalized( [ ( root: forkyBlck.root, canonical: node.dag.isCanonical(bid), header: ( message: forkyBlck.toBeaconBlockHeader, signature: forkyBlck.signature ) ) ], node.getBlockOptimistic(bdata), node.dag.isFinalized(bid) ) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeader router.api2(MethodGet, "/eth/v1/beacon/headers/{block_id}") do ( block_id: BlockIdent) -> RestApiResponse: let bid = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bdata = node.getForkedBlock(bid).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) withBlck(bdata): let bid = BlockId(root: forkyBlck.root, slot: forkyBlck.message.slot) RestApiResponse.jsonResponseFinalized( ( root: forkyBlck.root, canonical: node.dag.isCanonical(bid), header: ( message: forkyBlck.toBeaconBlockHeader, signature: forkyBlck.signature ) ), node.getBlockOptimistic(bdata), node.dag.isFinalized(bid) ) # https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlock router.api(MethodPost, "/eth/v1/beacon/blocks") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let res = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let body = contentBody.get() currentEpochFork = node.dag.cfg.consensusForkAtEpoch(node.currentSlot().epoch()) rawVersion = request.headers.getString("eth-consensus-version") # The V1 endpoint doesn't require the version to be specified but the # only fork which works is the current gossip fork. Either it can use # and broadcast a block in that fork or that broadcast will not prove # useful anyway, so allow it to fail at the decoding stage. version = if rawVersion == "": currentEpochFork.toString else: rawVersion let restBlock = decodeBody( RestPublishedSignedBlockContents, body, version).valueOr: return RestApiResponse.jsonError(error) withForkyBlck(restBlock): if restBlock.kind != node.dag.cfg.consensusForkAtEpoch( forkyBlck.message.slot.epoch): doAssert strictVerification notin node.dag.updateFlags return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) when consensusFork >= ConsensusFork.Deneb: await node.router.routeSignedBeaconBlock( forkyBlck, Opt.some( forkyBlck.create_blob_sidecars(kzg_proofs, blobs)), checkValidator = true) else: await node.router.routeSignedBeaconBlock( forkyBlck, Opt.none(seq[BlobSidecar]), checkValidator = true) if res.isErr(): return RestApiResponse.jsonError( Http503, BeaconNodeInSyncError, $res.error) if res.get().isNone(): return RestApiResponse.jsonError(Http202, BlockValidationError) RestApiResponse.jsonMsgResponse(BlockValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlockV2 router.api(MethodPost, "/eth/v2/beacon/blocks") do ( broadcast_validation: Option[BroadcastValidationType], contentBody: Option[ContentBody]) -> RestApiResponse: let res = block: let version = request.headers.getString("eth-consensus-version") validation = block: let res = if broadcast_validation.isNone(): BroadcastValidationType.Gossip else: broadcast_validation.get().valueOr: return RestApiResponse.jsonError(Http400, InvalidBroadcastValidationType) # TODO (henridf): support 'consensus' and # 'consensus_and_equivocation' broadcast_validation types. if res != BroadcastValidationType.Gossip: return RestApiResponse.jsonError(Http500, "Only `gossip` broadcast_validation option supported") res body = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) contentBody.get() restBlock = decodeBody( RestPublishedSignedBlockContents, body, version).valueOr: return RestApiResponse.jsonError(error) withForkyBlck(restBlock): # TODO (henridf): handle broadcast_validation flag if restBlock.kind != node.dag.cfg.consensusForkAtEpoch( forkyBlck.message.slot.epoch): doAssert strictVerification notin node.dag.updateFlags return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) when consensusFork >= ConsensusFork.Deneb: await node.router.routeSignedBeaconBlock( forkyBlck, Opt.some( forkyBlck.create_blob_sidecars(kzg_proofs, blobs)), checkValidator = true) else: await node.router.routeSignedBeaconBlock( forkyBlck, Opt.none(seq[BlobSidecar]), checkValidator = true) if res.isErr(): return RestApiResponse.jsonError( Http503, BeaconNodeInSyncError, $res.error) if res.get().isNone(): return RestApiResponse.jsonError(Http202, BlockValidationError) RestApiResponse.jsonMsgResponse(BlockValidationSuccess) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.2#/Beacon/getBlindedBlock # https://github.com/ethereum/beacon-APIs/blob/v2.4.2/apis/beacon/blocks/blinded_block.yaml router.api2(MethodGet, "/eth/v1/beacon/blinded_blocks/{block_id}") do ( block_id: BlockIdent) -> RestApiResponse: let blockIdent = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bid = node.getBlockId(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) contentType = block: let res = preferredContentType(jsonMediaType, sszMediaType) if res.isErr(): return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) res.get() bdata = node.dag.getForkedBlock(bid).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) template respondSszOrJson( signedMaybeBlindedBlck: auto, consensusFork: ConsensusFork): untyped = if contentType == sszMediaType: RestApiResponse.sszResponse( signedMaybeBlindedBlck, [("eth-consensus-version", consensusFork.toString())]) elif contentType == jsonMediaType: RestApiResponse.jsonResponseBlock( signedMaybeBlindedBlck, consensusFork, node.getBlockOptimistic(bdata), node.dag.isFinalized(bid) ) else: RestApiResponse.jsonError(Http500, InvalidAcceptError) withBlck(bdata.asSigned()): when consensusFork <= ConsensusFork.Altair: respondSszOrJson(forkyBlck, consensusFork) else: respondSszOrJson(toSignedBlindedBeaconBlock(forkyBlck), consensusFork) # https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlindedBlock # https://github.com/ethereum/beacon-APIs/blob/v2.4.0/apis/beacon/blocks/blinded_blocks.yaml router.api(MethodPost, "/eth/v1/beacon/blinded_blocks") do ( contentBody: Option[ContentBody]) -> RestApiResponse: ## Instructs the beacon node to use the components of the ## `SignedBlindedBeaconBlock` to construct and publish a ## `SignedBeaconBlock` by swapping out the transactions_root for the ## corresponding full list of transactions. The beacon node should ## broadcast a newly constructed `SignedBeaconBlock` 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. if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let currentEpochFork = node.dag.cfg.consensusForkAtEpoch(node.currentSlot().epoch()) rawVersion = request.headers.getString("eth-consensus-version") body = contentBody.get() # The V1 endpoint doesn't require the version to be specified but the # only fork which works is the current gossip fork. Either it can use # and broadcast a block in that fork or that broadcast will not prove # useful anyway, so allow it to fail at the decoding stage. version = if rawVersion == "": currentEpochFork.toString else: rawVersion if (body.contentType == OctetStreamMediaType) and (currentEpochFork.toString != version): return RestApiResponse.jsonError(Http400, BlockIncorrectFork) withConsensusFork(currentEpochFork): when consensusFork >= ConsensusFork.Deneb: let restBlock = decodeBodyJsonOrSsz( consensusFork.SignedBlindedBeaconBlock, body).valueOr: return RestApiResponse.jsonError(error) payloadBuilderClient = node.getPayloadBuilderClient( restBlock.message.proposer_index).valueOr: return RestApiResponse.jsonError( Http400, "Unable to initialize payload builder client: " & $error) res = await node.unblindAndRouteBlockMEV( payloadBuilderClient, restBlock) if res.isErr(): return RestApiResponse.jsonError( Http500, InternalServerError, $res.error) if res.get().isNone(): return RestApiResponse.jsonError(Http202, BlockValidationError) return RestApiResponse.jsonMsgResponse(BlockValidationSuccess) elif consensusFork >= ConsensusFork.Bellatrix: return RestApiResponse.jsonError( Http400, $consensusFork & " builder API unsupported") else: # Pre-Bellatrix, this endpoint will accept a `SignedBeaconBlock`. # # This is mostly the same as /eth/v1/beacon/blocks for phase 0 and # altair. var restBlock = decodeBody( RestPublishedSignedBeaconBlock, body, version).valueOr: return RestApiResponse.jsonError(error) forked = ForkedSignedBeaconBlock(restBlock) if forked.kind != node.dag.cfg.consensusForkAtEpoch( getForkedBlockField(forked, slot).epoch): return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) let res = withBlck(forked): forkyBlck.root = hash_tree_root(forkyBlck.message) await node.router.routeSignedBeaconBlock( forkyBlck, Opt.none(seq[BlobSidecar]), checkValidator = true) if res.isErr(): return RestApiResponse.jsonError( Http503, BeaconNodeInSyncError, $res.error) elif res.get().isNone(): return RestApiResponse.jsonError(Http202, BlockValidationError) RestApiResponse.jsonMsgResponse(BlockValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/publishBlindedBlockV2 router.api(MethodPost, "/eth/v2/beacon/blinded_blocks") do ( broadcast_validation: Option[BroadcastValidationType], contentBody: Option[ContentBody]) -> RestApiResponse: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let currentEpochFork = node.dag.cfg.consensusForkAtEpoch(node.currentSlot().epoch()) version = request.headers.getString("eth-consensus-version") validation = if broadcast_validation.isNone(): BroadcastValidationType.Gossip else: let res = broadcast_validation.get().valueOr: return RestApiResponse.jsonError(Http400, InvalidBroadcastValidationType) # TODO (cheatfate): support 'consensus' and # 'consensus_and_equivocation' broadcast_validation types. if res != BroadcastValidationType.Gossip: return RestApiResponse.jsonError(Http500, "Only `gossip` broadcast_validation option supported") res body = contentBody.get() if (body.contentType == OctetStreamMediaType) and (currentEpochFork.toString != version): return RestApiResponse.jsonError(Http400, BlockIncorrectFork) withConsensusFork(currentEpochFork): # TODO (cheatfate): handle broadcast_validation flag when consensusFork >= ConsensusFork.Deneb: let restBlock = decodeBodyJsonOrSsz( consensusFork.SignedBlindedBeaconBlock, body).valueOr: return RestApiResponse.jsonError(error) payloadBuilderClient = node.getPayloadBuilderClient( restBlock.message.proposer_index).valueOr: return RestApiResponse.jsonError( Http400, "Unable to initialize payload builder client: " & $error) res = await node.unblindAndRouteBlockMEV( payloadBuilderClient, restBlock) if res.isErr(): return RestApiResponse.jsonError( Http500, InternalServerError, $res.error) if res.get().isNone(): return RestApiResponse.jsonError(Http202, BlockValidationError) return RestApiResponse.jsonMsgResponse(BlockValidationSuccess) elif consensusFork >= ConsensusFork.Bellatrix: return RestApiResponse.jsonError( Http400, $consensusFork & " builder API unsupported") else: # Pre-Bellatrix, this endpoint will accept a `SignedBeaconBlock`. # # This is mostly the same as /eth/v1/beacon/blocks for phase 0 and # altair. var restBlock = decodeBody( RestPublishedSignedBeaconBlock, body, version).valueOr: return RestApiResponse.jsonError(error) forked = ForkedSignedBeaconBlock(restBlock) if forked.kind != node.dag.cfg.consensusForkAtEpoch( getForkedBlockField(forked, slot).epoch): return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) let res = withBlck(forked): forkyBlck.root = hash_tree_root(forkyBlck.message) await node.router.routeSignedBeaconBlock( forkyBlck, Opt.none(seq[BlobSidecar]), checkValidator = true) if res.isErr(): return RestApiResponse.jsonError( Http503, BeaconNodeInSyncError, $res.error) elif res.get().isNone(): return RestApiResponse.jsonError(Http202, BlockValidationError) RestApiResponse.jsonMsgResponse(BlockValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlock router.api2(MethodGet, "/eth/v1/beacon/blocks/{block_id}") do ( block_id: BlockIdent) -> RestApiResponse: RestApiResponse.jsonError( Http410, DeprecatedRemovalBeaconBlocksDebugStateV1) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2 router.api2(MethodGet, "/eth/v2/beacon/blocks/{block_id}") do ( block_id: BlockIdent) -> RestApiResponse: let blockIdent = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bid = node.getBlockId(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) let contentType = block: let res = preferredContentType(jsonMediaType, sszMediaType) if res.isErr(): return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) res.get() if contentType == sszMediaType: var data: seq[byte] if not node.dag.getBlockSSZ(bid, data): return RestApiResponse.jsonError(Http404, BlockNotFoundError) let fork = node.dag.cfg.consensusForkAtEpoch(bid.slot.epoch) headers = [("eth-consensus-version", fork.toString())] RestApiResponse.sszResponsePlain(data, headers) elif contentType == jsonMediaType: let bdata = node.dag.getForkedBlock(bid).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) RestApiResponse.jsonResponseBlock( bdata.asSigned(), node.getBlockOptimistic(bdata), node.dag.isFinalized(bid) ) else: RestApiResponse.jsonError(Http500, InvalidAcceptError) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockRoot router.api2(MethodGet, "/eth/v1/beacon/blocks/{block_id}/root") do ( block_id: BlockIdent) -> RestApiResponse: let blockIdent = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bid = node.getBlockId(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) bdata = node.dag.getForkedBlock(bid).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) RestApiResponse.jsonResponseFinalized( (root: bid.root), node.getBlockOptimistic(bdata), node.dag.isFinalized(bid) ) # https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations router.api2(MethodGet, "/eth/v1/beacon/blocks/{block_id}/attestations") do ( block_id: BlockIdent) -> RestApiResponse: let blockIdent = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bdata = node.getForkedBlock(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) withBlck(bdata): let bid = BlockId(root: forkyBlck.root, slot: forkyBlck.message.slot) RestApiResponse.jsonResponseFinalized( forkyBlck.message.body.attestations.asSeq(), node.getBlockOptimistic(bdata), node.dag.isFinalized(bid) ) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getBlockAttestationsV2 router.api2(MethodGet, "/eth/v2/beacon/blocks/{block_id}/attestations") do ( block_id: BlockIdent) -> RestApiResponse: let blockIdent = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bdata = node.getForkedBlock(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) withBlck(bdata): let bid = BlockId(root: forkyBlck.root, slot: forkyBlck.message.slot) RestApiResponse.jsonResponseFinalizedWVersion( forkyBlck.message.body.attestations.asSeq(), node.getBlockOptimistic(bdata), node.dag.isFinalized(bid), consensusFork ) # https://ethereum.github.io/beacon-APIs/#/Beacon/getPoolAttestations router.api2(MethodGet, "/eth/v1/beacon/pool/attestations") do ( slot: Option[Slot], committee_index: Option[CommitteeIndex]) -> RestApiResponse: let vindex = if committee_index.isSome(): let rindex = committee_index.get() if rindex.isErr(): return RestApiResponse.jsonError(Http400, InvalidCommitteeIndexValueError, $rindex.error) Opt.some(rindex.get()) else: Opt.none(CommitteeIndex) let vslot = if slot.isSome(): let rslot = slot.get() if rslot.isErr(): return RestApiResponse.jsonError(Http400, InvalidSlotValueError, $rslot.error) Opt.some(rslot.get()) else: Opt.none(Slot) var res: seq[phase0.Attestation] for item in node.attestationPool[].attestations(vslot, vindex): res.add(item) RestApiResponse.jsonResponse(res) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getPoolAttestationsV2 router.api2(MethodGet, "/eth/v2/beacon/pool/attestations") do ( slot: Option[Slot], committee_index: Option[CommitteeIndex]) -> RestApiResponse: let vindex = if committee_index.isSome(): let rindex = committee_index.get() if rindex.isErr(): return RestApiResponse.jsonError(Http400, InvalidCommitteeIndexValueError, $rindex.error) Opt.some(rindex.get()) else: Opt.none(CommitteeIndex) let vslot = if slot.isSome(): let rslot = slot.get() if rslot.isErr(): return RestApiResponse.jsonError(Http400, InvalidSlotValueError, $rslot.error) Opt.some(rslot.get()) else: Opt.none(Slot) let consensusFork = if vslot.isNone(): node.dag.cfg.consensusForkAtEpoch(node.currentSlot().epoch()) else: node.dag.cfg.consensusForkAtEpoch(vslot.get().epoch) if consensusFork < ConsensusFork.Electra: return RestApiResponse.jsonResponseWVersion( toSeq(node.attestationPool[].attestations(vslot, vindex)), consensusFork) else: return RestApiResponse.jsonResponseWVersion( toSeq(node.attestationPool[].electraAttestations(vslot, vindex)), consensusFork) # https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolAttestations router.api2(MethodPost, "/eth/v1/beacon/pool/attestations") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let attestations = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let dres = decodeBody(seq[phase0.Attestation], contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidAttestationObjectError, $dres.error) dres.get() # Since our validation logic supports batch processing, we will submit all # attestations for validation. let pending = mapIt(attestations, node.router.routeAttestation(it)) let failures = block: var res: seq[RestIndexedErrorMessageItem] await allFutures(pending) for index, future in pending: if future.completed(): let fres = future.value() if fres.isErr(): let failure = RestIndexedErrorMessageItem(index: index, message: $fres.error) res.add(failure) elif future.failed() or future.cancelled(): # This is unexpected failure, so we log the error message. let exc = future.error() let failure = RestIndexedErrorMessageItem(index: index, message: $exc.msg) res.add(failure) res if len(failures) > 0: RestApiResponse.jsonErrorList(Http400, AttestationValidationError, failures) else: RestApiResponse.jsonMsgResponse(AttestationValidationSuccess) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/submitPoolAttestationsV2 router.api2(MethodPost, "/eth/v2/beacon/pool/attestations") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let headerVersion = request.headers.getString("Eth-Consensus-Version") consensusVersion = ConsensusFork.init(headerVersion) if consensusVersion.isNone(): return RestApiResponse.jsonError(Http400, FailedToObtainConsensusForkError) if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) var pendingAttestations: seq[Future[SendResult]] template decodeAttestations(AttestationType: untyped) = let dres = decodeBody(seq[AttestationType], contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidAttestationObjectError, $dres.error) # Since our validation logic supports batch processing, we will submit all # attestations for validation. for attestation in dres.get(): when AttestationType is electra.Attestation: let attester_indices = toSeq( get_attesting_indices(node.dag, attestation, true)) if len(attester_indices) != 1: return RestApiResponse.jsonError(Http400, InvalidAttestationObjectError, $dres.error) let committee_index = get_committee_index_one( attestation.committee_bits).valueOr: return RestApiResponse.jsonError(Http400, InvalidAttestationObjectError, $dres.error) pendingAttestations.add(node.router.routeAttestation( SingleAttestation( committee_index: committee_index.distinctBase, attester_index: attester_indices[0].uint64, data: attestation.data, signature: attestation.signature))) else: pendingAttestations.add(node.router.routeAttestation(attestation)) case consensusVersion.get(): of ConsensusFork.Phase0 .. ConsensusFork.Deneb: decodeAttestations(phase0.Attestation) of ConsensusFork.Electra .. ConsensusFork.Fulu: decodeAttestations(electra.Attestation) let failures = block: var res: seq[RestIndexedErrorMessageItem] await allFutures(pendingAttestations) for index, future in pendingAttestations: if future.completed(): let fres = future.value() if fres.isErr(): let failure = RestIndexedErrorMessageItem(index: index, message: $fres.error) res.add(failure) elif future.failed() or future.cancelled(): # This is unexpected failure, so we log the error message. let exc = future.error() let failure = RestIndexedErrorMessageItem(index: index, message: $exc.msg) res.add(failure) res if len(failures) > 0: RestApiResponse.jsonErrorList(Http400, AttestationValidationError, failures) else: RestApiResponse.jsonMsgResponse(AttestationValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/getPoolAttesterSlashings router.api2(MethodGet, "/eth/v1/beacon/pool/attester_slashings") do ( ) -> RestApiResponse: RestApiResponse.jsonResponse( toSeq(node.validatorChangePool.phase0_attester_slashings)) # https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolAttesterSlashings router.api(MethodPost, "/eth/v1/beacon/pool/attester_slashings") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let slashing = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let dres = decodeBody(phase0.AttesterSlashing, contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidAttesterSlashingObjectError, $dres.error) dres.get() let res = await node.router.routeAttesterSlashing(slashing) if res.isErr(): return RestApiResponse.jsonError(Http400, AttesterSlashingValidationError, $res.error) RestApiResponse.jsonMsgResponse(AttesterSlashingValidationSuccess) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getPoolAttesterSlashingsV2 router.api2(MethodGet, "/eth/v2/beacon/pool/attester_slashings") do ( ) -> RestApiResponse: let contextFork = node.dag.cfg.consensusForkAtEpoch(node.currentSlot.epoch) withConsensusFork(contextFork): when consensusFork < ConsensusFork.Electra: RestApiResponse.jsonResponseWVersion( toSeq(node.validatorChangePool.phase0_attester_slashings), contextFork) else: RestApiResponse.jsonResponseWVersion( toSeq(node.validatorChangePool.electra_attester_slashings), contextFork) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/submitPoolAttesterSlashingsV2 router.api(MethodPost, "/eth/v2/beacon/pool/attester_slashings") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let headerVersion = request.headers.getString("Eth-Consensus-Version") consensusVersion = ConsensusFork.init(headerVersion) if consensusVersion.isNone(): return RestApiResponse.jsonError(Http400, FailedToObtainConsensusForkError) if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) template decodeAttesterSlashing(AttesterSlashingType: untyped) = let dres = decodeBody(AttesterSlashingType, contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidAttesterSlashingObjectError, $dres.error) let res = await node.router.routeAttesterSlashing(dres.get()) if res.isErr(): return RestApiResponse.jsonError(Http400, AttesterSlashingValidationError, $res.error) return RestApiResponse.jsonMsgResponse(AttesterSlashingValidationSuccess) case consensusVersion.get(): of ConsensusFork.Phase0 .. ConsensusFork.Deneb: decodeAttesterSlashing(phase0.AttesterSlashing) of ConsensusFork.Electra .. ConsensusFork.Fulu: decodeAttesterSlashing(electra.AttesterSlashing) # https://ethereum.github.io/beacon-APIs/#/Beacon/getPoolProposerSlashings router.api2(MethodGet, "/eth/v1/beacon/pool/proposer_slashings") do ( ) -> RestApiResponse: RestApiResponse.jsonResponse( toSeq(node.validatorChangePool.proposer_slashings)) # https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolProposerSlashings router.api(MethodPost, "/eth/v1/beacon/pool/proposer_slashings") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let slashing = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let dres = decodeBody(ProposerSlashing, contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidProposerSlashingObjectError, $dres.error) dres.get() let res = await node.router.routeProposerSlashing(slashing) if res.isErr(): return RestApiResponse.jsonError(Http400, ProposerSlashingValidationError, $res.error) RestApiResponse.jsonMsgResponse(ProposerSlashingValidationSuccess) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/getPoolBLSToExecutionChanges # https://github.com/ethereum/beacon-APIs/blob/86850001845df9163da5ae9605dbf15cd318d5d0/apis/beacon/pool/bls_to_execution_changes.yaml router.api2(MethodGet, "/eth/v1/beacon/pool/bls_to_execution_changes") do ( ) -> RestApiResponse: RestApiResponse.jsonResponse( toSeq(node.validatorChangePool.bls_to_execution_changes_gossip) & toSeq(node.validatorChangePool.bls_to_execution_changes_api)) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Beacon/submitPoolBLSToExecutionChange # https://github.com/ethereum/beacon-APIs/blob/86850001845df9163da5ae9605dbf15cd318d5d0/apis/beacon/pool/bls_to_execution_changes.yaml router.api2(MethodPost, "/eth/v1/beacon/pool/bls_to_execution_changes") do ( contentBody: Option[ContentBody]) -> RestApiResponse: if node.currentSlot().epoch() < node.dag.cfg.CAPELLA_FORK_EPOCH: return RestApiResponse.jsonError(Http400, InvalidBlsToExecutionChangeObjectError, "Attempt to add to BLS to execution change pool pre-Capella") let bls_to_execution_changes = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let dres = decodeBody(seq[SignedBLSToExecutionChange], contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError( Http400, InvalidBlsToExecutionChangeObjectError, $dres.error) dres.get() pending = mapIt(bls_to_execution_changes, node.router.routeBlsToExecutionChange(it)) await allFutures(pending) for future in pending: if future.failed() or future.cancelled(): return RestApiResponse.jsonError(Http400, BlsToExecutionChangeValidationError, $future.error().msg) let res = future.value() if res.isErr(): return RestApiResponse.jsonError(Http400, BlsToExecutionChangeValidationError, $res.error) RestApiResponse.jsonMsgResponse(BlsToExecutionChangeValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolSyncCommitteeSignatures router.api(MethodPost, "/eth/v1/beacon/pool/sync_committees") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let messages = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let dres = decodeBody(seq[SyncCommitteeMessage], contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidSyncCommitteeSignatureMessageError) dres.get() let results = await node.router.routeSyncCommitteeMessages(messages) let failures = block: var res: seq[RestIndexedErrorMessageItem] for index, item in results: if item.isErr(): res.add(RestIndexedErrorMessageItem(index: index, message: $item.error)) res if len(failures) > 0: RestApiResponse.jsonErrorList( Http400, SyncCommitteeMessageValidationError, failures) else: RestApiResponse.jsonMsgResponse( SyncCommitteeMessageValidationSuccess) # https://ethereum.github.io/beacon-APIs/#/Beacon/getPoolVoluntaryExits router.api2(MethodGet, "/eth/v1/beacon/pool/voluntary_exits") do ( ) -> RestApiResponse: return RestApiResponse.jsonResponse( toSeq(node.validatorChangePool.voluntary_exits)) # https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolVoluntaryExit router.api(MethodPost, "/eth/v1/beacon/pool/voluntary_exits") do ( contentBody: Option[ContentBody]) -> RestApiResponse: let exit = block: if contentBody.isNone(): return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) let dres = decodeBody(SignedVoluntaryExit, contentBody.get()) if dres.isErr(): return RestApiResponse.jsonError(Http400, InvalidVoluntaryExitObjectError, $dres.error) dres.get() let res = await node.router.routeSignedVoluntaryExit(exit) if res.isErr(): return RestApiResponse.jsonError( Http400, VoluntaryExitValidationError, $res.error) return RestApiResponse.jsonMsgResponse(VoluntaryExitValidationSuccess) # https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.2#/Beacon/getBlobSidecars # https://github.com/ethereum/beacon-APIs/blob/v2.4.2/apis/beacon/blob_sidecars/blob_sidecars.yaml router.api2(MethodGet, "/eth/v1/beacon/blob_sidecars/{block_id}") do ( block_id: BlockIdent, indices: seq[uint64]) -> RestApiResponse: let blockIdent = block_id.valueOr: return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, $error) bid = node.getBlockId(blockIdent).valueOr: return RestApiResponse.jsonError(Http404, BlockNotFoundError) contentType = block: let res = preferredContentType(jsonMediaType, sszMediaType) if res.isErr(): return RestApiResponse.jsonError(Http406, ContentNotAcceptableError) res.get() # https://github.com/ethereum/beacon-APIs/blob/v2.4.2/types/deneb/blob_sidecar.yaml#L2-L28 let data = newClone(default(List[BlobSidecar, Limit MAX_BLOBS_PER_BLOCK])) if indices.isErr: return RestApiResponse.jsonError(Http400, InvalidSidecarIndexValueError) let indexFilter = indices.get.toHashSet for blobIndex in 0'u64 ..< MAX_BLOBS_PER_BLOCK: if indexFilter.len > 0 and blobIndex notin indexFilter: continue var blobSidecar = new BlobSidecar if node.dag.db.getBlobSidecar(bid.root, blobIndex, blobSidecar[]): discard data[].add blobSidecar[] if contentType == sszMediaType: RestApiResponse.sszResponse( data[], headers = [("eth-consensus-version", node.dag.cfg.consensusForkAtEpoch(bid.slot.epoch).toString())]) elif contentType == jsonMediaType: RestApiResponse.jsonResponse(data) else: RestApiResponse.jsonError(Http500, InvalidAcceptError)