Skip to content

Commit

Permalink
REST: getBlockRewards() and getSyncCommitteeRewards() implementation (#…
Browse files Browse the repository at this point in the history
…6556)

* Initial commit.

* Use temporary state instead of clearance.

* Attempt to fix `finalized`.

* Fix `genesis` response.

* Pre-calculate genesis block rewards response.

* Add implementation for sync committee rewards.

* Add total active balance calculation.

* Add genesis special case.

* Fix negative reward values.

* Address review comments.

* Fix isGenesis implementation and add REST test rules for both calls.
  • Loading branch information
cheatfate committed Sep 18, 2024
1 parent 1cc3c59 commit e3fcd8b
Show file tree
Hide file tree
Showing 7 changed files with 686 additions and 17 deletions.
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
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

0 comments on commit e3fcd8b

Please sign in to comment.