diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index eee77aaf34..a35377a42c 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -1774,6 +1774,7 @@ proc installRestHandlers(restServer: RestServerRef, node: BeaconNode) = restServer.router.installNimbusApiHandlers(node) restServer.router.installNodeApiHandlers(node) restServer.router.installValidatorApiHandlers(node) + restServer.router.installRewardsApiHandlers(node) if node.dag.lcDataStore.serve: restServer.router.installLightClientApiHandlers(node) diff --git a/beacon_chain/rpc/rest_api.nim b/beacon_chain/rpc/rest_api.nim index bfc6b58243..1a2a91ff64 100644 --- a/beacon_chain/rpc/rest_api.nim +++ b/beacon_chain/rpc/rest_api.nim @@ -19,10 +19,10 @@ import rest_utils, rest_beacon_api, rest_builder_api, rest_config_api, rest_debug_api, rest_event_api, rest_key_management_api, rest_light_client_api, - rest_nimbus_api, rest_node_api, rest_validator_api] + rest_nimbus_api, rest_node_api, rest_validator_api, rest_rewards_api] export rest_utils, rest_beacon_api, rest_builder_api, rest_config_api, rest_debug_api, rest_event_api, rest_key_management_api, rest_light_client_api, - rest_nimbus_api, rest_node_api, rest_validator_api + rest_nimbus_api, rest_node_api, rest_validator_api, rest_rewards_api diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index 2a462b4b41..ca1f8a510b 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -24,6 +24,12 @@ const "Beacon node is currently syncing and not serving request on that endpoint" BlockNotFoundError* = "Block header/data has not been found" + BlockParentUnknownError* = + "Block parent unknown" + BlockOlderThanParentError* = + "Block older than parent block" + BlockInvalidError* = + "Invalid block" EmptyRequestBodyError* = "Empty request body" InvalidRequestBodyError* = @@ -259,3 +265,7 @@ const "Path not found" FileReadError* = "Error reading file" + ParentBlockMissingStateError* = + "Unable to load state for parent block, database corrupt?" + RewardOverflowError* = + "Reward value overflow" diff --git a/beacon_chain/rpc/rest_rewards_api.nim b/beacon_chain/rpc/rest_rewards_api.nim new file mode 100644 index 0000000000..bd4fa8d7ab --- /dev/null +++ b/beacon_chain/rpc/rest_rewards_api.nim @@ -0,0 +1,246 @@ +# 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/[forks, state_transition] + +export rest_utils + +logScope: topics = "rest_rewardsapi" + +func isGenesis(node: BeaconNode, + blockId: BlockIdent, + genesisBsid: BlockSlotId): bool = + case blockId.kind + of BlockQueryKind.Named: + case blockId.value + of BlockIdentType.Genesis: + true + of BlockIdentType.Head: + node.dag.head.bid.slot == GENESIS_SLOT + of BlockIdentType.Finalized: + node.dag.finalizedHead.slot == GENESIS_SLOT + of BlockQueryKind.Slot: + blockId.slot == GENESIS_SLOT + of BlockQueryKind.Root: + blockId.root == genesisBsid.bid.root + +proc installRewardsApiHandlers*(router: var RestRouter, node: BeaconNode) = + let + genesisBlockRewardsResponse = + RestApiResponse.prepareJsonResponseFinalized( + ( + proposer_index: "0", total: "0", attestations: "0", + sync_aggregate: "0", proposer_slashings: "0", attester_slashings: "0" + ), + Opt.some(false), + true, + ) + genesisBsid = node.dag.getBlockIdAtSlot(GENESIS_SLOT).get() + + # https://ethereum.github.io/beacon-APIs/#/Rewards/getBlockRewards + router.api2(MethodGet, "/eth/v1/beacon/rewards/blocks/{block_id}") do ( + block_id: BlockIdent) -> RestApiResponse: + let + bident = block_id.valueOr: + return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, + $error) + + if node.isGenesis(bident, genesisBsid): + return RestApiResponse.response( + genesisBlockRewardsResponse, Http200, "application/json") + + let + bdata = node.getForkedBlock(bident).valueOr: + return RestApiResponse.jsonError(Http404, BlockNotFoundError) + + bid = BlockId(slot: bdata.slot, root: bdata.root) + + targetBlock = + withBlck(bdata): + let parentBid = + node.dag.getBlockId(forkyBlck.message.parent_root).valueOr: + return RestApiResponse.jsonError(Http404, BlockParentUnknownError) + if parentBid.slot >= forkyBlck.message.slot: + return RestApiResponse.jsonError(Http404, BlockOlderThanParentError) + BlockSlotId.init(parentBid, forkyBlck.message.slot) + + var + cache = StateCache() + tmpState = assignClone(node.dag.headState) + + if not updateState( + node.dag, tmpState[], targetBlock, false, cache): + return RestApiResponse.jsonError(Http404, ParentBlockMissingStateError) + + func rollbackProc(state: var ForkedHashedBeaconState) {. + gcsafe, noSideEffect, raises: [].} = + discard + + let + rewards = + withBlck(bdata): + state_transition_block( + node.dag.cfg, tmpState[], forkyBlck, + cache, node.dag.updateFlags, rollbackProc).valueOr: + return RestApiResponse.jsonError(Http400, BlockInvalidError) + total = rewards.attestations + rewards.sync_aggregate + + rewards.proposer_slashings + rewards.attester_slashings + proposerIndex = + withBlck(bdata): + forkyBlck.message.proposer_index + + RestApiResponse.jsonResponseFinalized( + ( + proposer_index: Base10.toString(uint64(proposerIndex)), + total: Base10.toString(uint64(total)), + attestations: Base10.toString(uint64(rewards.attestations)), + sync_aggregate: Base10.toString(uint64(rewards.sync_aggregate)), + proposer_slashings: Base10.toString(uint64(rewards.proposer_slashings)), + attester_slashings: Base10.toString(uint64(rewards.attester_slashings)) + ), + node.getBlockOptimistic(bdata), + node.dag.isFinalized(bid) + ) + + # https://ethereum.github.io/beacon-APIs/#/Rewards/getSyncCommitteeRewards + router.api2( + MethodPost, "/eth/v1/beacon/rewards/sync_committee/{block_id}") do ( + block_id: BlockIdent, + contentBody: Option[ContentBody]) -> RestApiResponse: + let + idents = + block: + if contentBody.isNone(): + return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) + let res = decodeBody(seq[ValidatorIdent], contentBody.get()).valueOr: + return RestApiResponse.jsonError( + Http400, InvalidRequestBodyError, $error) + res + + bident = block_id.valueOr: + return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError, + $error) + bdata = node.getForkedBlock(bident).valueOr: + return RestApiResponse.jsonError(Http404, BlockNotFoundError) + + bid = BlockId(slot: bdata.slot, root: bdata.root) + + sync_aggregate = + withBlck(bdata): + when consensusFork > ConsensusFork.Phase0: + forkyBlck.message.body.sync_aggregate + else: + default(TrustedSyncAggregate) + + targetBlock = + withBlck(bdata): + if node.isGenesis(bident, genesisBsid): + genesisBsid + else: + let parentBid = + node.dag.getBlockId(forkyBlck.message.parent_root).valueOr: + return RestApiResponse.jsonError( + Http404, BlockParentUnknownError) + if parentBid.slot >= forkyBlck.message.slot: + return RestApiResponse.jsonError( + Http404, BlockOlderThanParentError) + BlockSlotId.init(parentBid, forkyBlck.message.slot) + + var + cache = StateCache() + tmpState = assignClone(node.dag.headState) + + if not updateState( + node.dag, tmpState[], targetBlock, false, cache): + return RestApiResponse.jsonError(Http404, ParentBlockMissingStateError) + + let response = + withState(tmpState[]): + let total_active_balance = + get_total_active_balance(forkyState.data, cache) + var resp: seq[RestSyncCommitteeReward] + when consensusFork > ConsensusFork.Phase0: + let + keys = + block: + var res: HashSet[ValidatorPubKey] + for item in idents: + case item.kind + of ValidatorQueryKind.Index: + let vindex = item.index.toValidatorIndex().valueOr: + case error + of ValidatorIndexError.TooHighValue: + return RestApiResponse.jsonError( + Http400, TooHighValidatorIndexValueError) + of ValidatorIndexError.UnsupportedValue: + return RestApiResponse.jsonError( + Http500, UnsupportedValidatorIndexValueError) + if uint64(vindex) >= lenu64(forkyState.data.validators): + return RestApiResponse.jsonError( + Http400, ValidatorNotFoundError) + res.incl(forkyState.data.validators.item(vindex).pubkey) + of ValidatorQueryKind.Key: + res.incl(item.key) + res + + committeeKeys = + toHashSet(forkyState.data.current_sync_committee.pubkeys.data) + + pubkeyIndices = + block: + var res: Table[ValidatorPubKey, ValidatorIndex] + for vindex in forkyState.data.validators.vindices: + let pubkey = forkyState.data.validators.item(vindex).pubkey + if pubkey in committeeKeys: + res[pubkey] = vindex + res + reward = + block: + let res = uint64(get_participant_reward(total_active_balance)) + if res > uint64(high(int64)): + return RestApiResponse.jsonError( + Http500, RewardOverflowError) + res + + for i in 0 ..< min( + len(forkyState.data.current_sync_committee.pubkeys), + len(sync_aggregate.sync_committee_bits)): + let + pubkey = forkyState.data.current_sync_committee.pubkeys.data[i] + vindex = + try: + pubkeyIndices[pubkey] + except KeyError: + raiseAssert "Unknown sync committee pubkey encountered!" + vreward = + if sync_aggregate.sync_committee_bits[i]: + cast[int64](reward) + else: + -cast[int64](reward) + + if (len(idents) == 0) or (pubkey in keys): + resp.add(RestSyncCommitteeReward( + validator_index: RestValidatorIndex(vindex), + reward: RestReward(vreward))) + + resp + + RestApiResponse.jsonResponseFinalized( + response, + node.getBlockOptimistic(bdata), + node.dag.isFinalized(bid) + ) diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index b0e49bc468..180864c4cd 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -675,24 +675,28 @@ proc jsonResponseWOpt*(t: typedesc[RestApiResponse], data: auto, default RestApiResponse.response(res, Http200, "application/json") +proc prepareJsonResponseFinalized*( + t: typedesc[RestApiResponse], data: auto, exec: Opt[bool], + finalized: bool +): seq[byte] = + try: + var + stream = memoryOutput() + writer = JsonWriter[RestJson].init(stream) + writer.beginRecord() + if exec.isSome(): + writer.writeField("execution_optimistic", exec.get()) + writer.writeField("finalized", finalized) + writer.writeField("data", data) + writer.endRecord() + stream.getOutput(seq[byte]) + except IOError: + default(seq[byte]) + proc jsonResponseFinalized*(t: typedesc[RestApiResponse], data: auto, exec: Opt[bool], finalized: bool): RestApiResponse = - let res = - block: - var default: seq[byte] - try: - var stream = memoryOutput() - var writer = JsonWriter[RestJson].init(stream) - writer.beginRecord() - if exec.isSome(): - writer.writeField("execution_optimistic", exec.get()) - writer.writeField("finalized", finalized) - writer.writeField("data", data) - writer.endRecord() - stream.getOutput(seq[byte]) - except IOError: - default + let res = RestApiResponse.prepareJsonResponseFinalized(data, exec, finalized) RestApiResponse.response(res, Http200, "application/json") proc jsonResponseWVersion*(t: typedesc[RestApiResponse], data: auto, @@ -975,6 +979,29 @@ proc readValue*(reader: var JsonReader[RestJson], value: var uint64) {. else: reader.raiseUnexpectedValue($res.error() & ": " & svalue) +## RestReward +proc writeValue*( + w: var JsonWriter[RestJson], value: RestReward) {.raises: [IOError].} = + writeValue(w, $int64(value)) + +proc readValue*(reader: var JsonReader[RestJson], value: var RestReward) {. + raises: [IOError, SerializationError].} = + let svalue = reader.readValue(string) + if svalue.startsWith("-"): + let res = + Base10.decode(uint64, svalue.toOpenArray(1, len(svalue) - 1)).valueOr: + reader.raiseUnexpectedValue($error & ": " & svalue) + if res > uint64(high(int64)): + reader.raiseUnexpectedValue("Integer value overflow " & svalue) + value = RestReward(-int64(res)) + else: + let res = + Base10.decode(uint64, svalue).valueOr: + reader.raiseUnexpectedValue($error & ": " & svalue) + if res > uint64(high(int64)): + reader.raiseUnexpectedValue("Integer value overflow " & svalue) + value = RestReward(int64(res)) + ## uint8 proc writeValue*( w: var JsonWriter[RestJson], value: uint8) {.raises: [IOError].} = @@ -4394,3 +4421,11 @@ proc writeValue*(writer: var JsonWriter[RestJson], if len(res) > 0: writer.writeField("statuses", res) writer.endRecord() + +## RestSyncCommitteeReward +proc writeValue*(writer: var JsonWriter[RestJson], + value: RestSyncCommitteeReward) {.raises: [IOError].} = + writer.beginRecord() + writer.writeField("validator_index", value.validator_index) + writer.writeField("reward", value.reward) + writer.endRecord() diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index 0509cff006..9b9cd31ec7 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -527,6 +527,12 @@ type subcommittee_index*: uint64 selection_proof*: ValidatorSig + RestReward* = distinct int64 + + RestSyncCommitteeReward* = object + validator_index*: RestValidatorIndex + reward*: RestReward + # Types based on the OAPI yaml file - used in responses to requests GetBeaconHeadResponse* = DataEnclosedObject[Slot] GetAggregatedAttestationResponse* = DataEnclosedObject[phase0.Attestation] diff --git a/ncli/resttest-rules.json b/ncli/resttest-rules.json index 071a8c38bf..aec181d077 100644 --- a/ncli/resttest-rules.json +++ b/ncli/resttest-rules.json @@ -4968,5 +4968,376 @@ "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], "body": [{"operator": "jstructcmpnsav", "value": {"code": 500, "message": "Only `gossip` broadcast_validation option supported"}}] } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/head", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": {"proposer_index": "", "total": "", "attestations": "", "sync_aggregate": "", "proposer_slashings": "", "attester_slashings": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/genesis", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": {"proposer_index": "", "total": "", "attestations": "", "sync_aggregate": "", "proposer_slashings": "", "attester_slashings": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/finalized", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": {"proposer_index": "", "total": "", "attestations": "", "sync_aggregate": "", "proposer_slashings": "", "attester_slashings": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/heat", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/geneziz", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/finalised", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/0", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": {"proposer_index": "", "total": "", "attestations": "", "sync_aggregate": "", "proposer_slashings": "", "attester_slashings": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/0x0000000000000000000000000000000000000000000000000000000000000000", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "404"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmpns", "value": {"code": 404, "message": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/18446744073709551615", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "404"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmpns", "value": {"code": 404, "message": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/18446744073709551616", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "400"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmpns", "value": {"code": 400, "message": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/0x", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/0x0", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/0x00", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/0xII", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "block_rewards_blockid"], + "request": { + "url": "/eth/v1/beacon/rewards/blocks/foobar", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/head", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"validator_index": "", "reward": ""}]}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/genesis", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"validator_index": "", "reward": ""}]}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/finalized", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"validator_index": "", "reward": ""}]}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/heat", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/geneziz", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/finalised", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/0", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "200"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"validator_index": "", "reward": ""}]}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/0x0000000000000000000000000000000000000000000000000000000000000000", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "404"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmpns", "value": {"code": 404, "message": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/18446744073709551615", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "404"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmpns", "value": {"code": 404, "message": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/18446744073709551616", + "headers": {"Accept": "application/json"} + }, + "response": { + "status": {"operator": "equals", "value": "400"}, + "headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}], + "body": [{"operator": "jstructcmpns", "value": {"code": 400, "message": ""}}] + } + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/0x", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/0x0", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/0x00", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/0xII", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} + }, + { + "topics": ["beacon", "rewards", "sync_committee_rewards_blockid"], + "request": { + "method": "POST", + "body": { + "content-type": "application/json", + "data": "[]" + }, + "url": "/eth/v1/beacon/rewards/sync_committee/foobar", + "headers": {"Accept": "application/json"} + }, + "response": {"status": {"operator": "equals", "value": "400"}} } ]