Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REST: getBlockRewards() and getSyncCommitteeRewards() implementation #6556

Merged
merged 11 commits into from
Sep 18, 2024
1 change: 1 addition & 0 deletions beacon_chain/nimbus_beacon_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions beacon_chain/rpc/rest_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions beacon_chain/rpc/rest_constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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* =
Expand Down Expand Up @@ -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"
226 changes: 226 additions & 0 deletions beacon_chain/rpc/rest_rewards_api.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# 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"

proc installRewardsApiHandlers*(router: var RestRouter, node: BeaconNode) =
let
tersec marked this conversation as resolved.
Show resolved Hide resolved
genesisBlockRewardsResponse =
RestApiResponse.prepareJsonResponseFinalized(
(
proposer_index: "0", total: "0", attestations: "0",
sync_aggregate: "0", proposer_slashings: "0", attester_slashings: "0"
),
Opt.some(false),
true,
)

# 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 (bident.kind == BlockQueryKind.Named) and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone specifies the genesis block in some other way than literally naming it genesis, the same logic should apply.

Copy link
Contributor Author

@cheatfate cheatfate Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b216cba and 23e6d3b

(bident.value == BlockIdentType.Genesis):
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 restore(v: var ForkedHashedBeaconState) =
tersec marked this conversation as resolved.
Show resolved Hide resolved
assign(node.dag.clearanceState, node.dag.headState)

let
rewards =
withBlck(bdata):
state_transition_block(
node.dag.cfg, tmpState[], forkyBlck,
cache, node.dag.updateFlags, restore).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 (bident.kind == BlockQueryKind.Named) and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone specifies the genesis block in some other way than literally naming it genesis, the same logic should apply.

Copy link
Contributor Author

@cheatfate cheatfate Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b216cba and 23e6d3b

(bident.value == BlockIdentType.Genesis):
let genesisBlockId =
node.dag.getBlockId(forkyBlck.root).valueOr:
return RestApiResponse.jsonError(Http404, BlockNotFoundError)
BlockSlotId.init(genesisBlockId, GENESIS_SLOT)
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: seq[ValidatorPubKey]
tersec marked this conversation as resolved.
Show resolved Hide resolved
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.add(forkyState.data.validators.item(vindex).pubkey)
of ValidatorQueryKind.Key:
res.add(item.key)
toHashSet(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

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 = pubkeyIndices.getOrDefault(pubkey)
tersec marked this conversation as resolved.
Show resolved Hide resolved
reward =
block:
let res = uint64(get_participant_reward(total_active_balance))
tersec marked this conversation as resolved.
Show resolved Hide resolved
if res > uint64(high(int64)):
return RestApiResponse.jsonError(
Http500, RewardOverflowError)
if sync_aggregate.sync_committee_bits[i]:
cast[int64](res)
else:
-cast[int64](res)

if (len(idents) == 0) or (pubkey in keys):
resp.add(RestSyncCommitteeReward(
validator_index: RestValidatorIndex(vindex),
reward: RestReward(reward)))

resp

RestApiResponse.jsonResponseFinalized(
response,
node.getBlockOptimistic(bdata),
node.dag.isFinalized(bid)
)
65 changes: 50 additions & 15 deletions beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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].} =
Expand Down Expand Up @@ -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()
6 changes: 6 additions & 0 deletions beacon_chain/spec/eth2_apis/rest_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading