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"
246 changes: 246 additions & 0 deletions beacon_chain/rpc/rest_rewards_api.nim
Original file line number Diff line number Diff line change
@@ -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
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,
)
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)
)
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
Loading