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

Implement the /eth/v1/validator/prepare_beacon_proposer end-point #3901

Merged
merged 1 commit into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion beacon_chain/beacon_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import
blockchain_dag, block_quarantine, consensus_manager, exit_pool,
attestation_pool, sync_committee_msg_pool],
./spec/datatypes/[base, altair],
./spec/eth2_apis/dynamic_fee_recipients,
./sync/[optimistic_sync_light_client, sync_manager, request_manager],
./validators/[
action_tracker, message_router, validator_monitor, validator_pool],
Expand All @@ -34,7 +35,7 @@ export
eth2_network, eth1_monitor, optimistic_sync_light_client,
request_manager, sync_manager, eth2_processor, blockchain_dag,
block_quarantine, base, exit_pool, message_router, validator_monitor,
consensus_manager
consensus_manager, dynamic_fee_recipients

type
EventBus* = object
Expand Down Expand Up @@ -86,6 +87,7 @@ type
stateTtlCache*: StateTtlCache
nextExchangeTransitionConfTime*: Moment
router*: ref MessageRouter
dynamicFeeRecipientsStore*: DynamicFeeRecipientsStore

const
MaxEmptySlotCount* = uint64(10*60) div SECONDS_PER_SLOT
Expand Down
4 changes: 3 additions & 1 deletion beacon_chain/nimbus_beacon_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,8 @@ proc init*(T: type BeaconNode,
beaconClock: beaconClock,
validatorMonitor: validatorMonitor,
stateTtlCache: stateTtlCache,
nextExchangeTransitionConfTime: nextExchangeTransitionConfTime)
nextExchangeTransitionConfTime: nextExchangeTransitionConfTime,
dynamicFeeRecipientsStore: DynamicFeeRecipientsStore.init())

node.initLightClient(
rng, cfg, dag.forkDigests, getBeaconTime, dag.genesis_validators_root)
Expand Down Expand Up @@ -1237,6 +1238,7 @@ proc onSlotEnd(node: BeaconNode, slot: Slot) {.async.} =
node.syncCommitteeMsgPool[].pruneData(slot)
if slot.is_epoch:
node.trackNextSyncCommitteeTopics(slot)
node.dynamicFeeRecipientsStore.pruneOldMappings(slot.epoch)

# Update upcoming actions - we do this every slot in case a reorg happens
let head = node.dag.head
Expand Down
2 changes: 2 additions & 0 deletions beacon_chain/rpc/rest_constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ const
"Unable to decode sync committee subscription request(s)"
InvalidContributionAndProofMessageError* =
"Unable to decode contribute and proof message(s)"
InvalidPrepareBeaconProposerError* =
"Unable to decode prepare beacon proposer request"
SyncCommitteeMessageValidationError* =
"Some errors happened while validating sync committee message(s)"
SyncCommitteeMessageValidationSuccess* =
Expand Down
30 changes: 29 additions & 1 deletion beacon_chain/rpc/rest_validator_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400,
InvalidContributionAndProofMessageError)
InvalidContributionAndProofMessageError)
dres.get()

let pending =
Expand Down Expand Up @@ -759,6 +759,29 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
ContributionAndProofValidationSuccess
)

# https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/prepareBeaconProposer
router.api(MethodPost,
"/eth/v1/validator/prepare_beacon_proposer") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let
proposerData =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
let dres = decodeBody(PrepareBeaconProposerBody, contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400,
InvalidPrepareBeaconProposerError)
dres.get()
currentEpoch = node.beaconClock.now.slotOrZero.epoch

node.dynamicFeeRecipientsStore.addMapping(
proposerData.validator_index,
proposerData.fee_recipient,
currentEpoch)

return RestApiResponse.response("", Http200, "text/plain")

# Legacy URLS - Nimbus <= 1.5.5 used to expose the REST API with an additional
# `/api` path component
router.redirect(
Expand Down Expand Up @@ -821,3 +844,8 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
"/api/eth/v1/validator/contribution_and_proofs",
"/eth/v1/validator/contribution_and_proofs"
)
router.redirect(
MethodPost,
"/api/eth/v1/validator/prepare_beacon_proposer",
"/eth/v1/validator/prepare_beacon_proposer"
)
54 changes: 54 additions & 0 deletions beacon_chain/spec/eth2_apis/dynamic_fee_recipients.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import
std/tables,
stew/results,
web3/ethtypes,
../datatypes/base

type
Entry = object
recipient: Eth1Address
addedAt: Epoch

DynamicFeeRecipientsStore* = object
mappings: Table[ValidatorIndex, Entry]

func init*(T: type DynamicFeeRecipientsStore): T =
T(mappings: initTable[ValidatorIndex, Entry]())

func addMapping*(store: var DynamicFeeRecipientsStore,
validator: ValidatorIndex,
feeRecipient: Eth1Address,
currentEpoch: Epoch) =
store.mappings[validator] = Entry(recipient: feeRecipient,
addedAt: currentEpoch)

func getDynamicFeeRecipient*(store: var DynamicFeeRecipientsStore,
validator: ValidatorIndex,
currentEpoch: Epoch): Opt[Eth1Address] =
store.mappings.withValue(validator, entry) do:
# https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/prepareBeaconProposer
#
# The information supplied for each validator index will persist
# through the epoch in which the call is submitted and for a further
# two epochs after that, or until the beacon node restarts.
#
# It is expected that validator clients will send this information
# periodically, for example each epoch, to ensure beacon nodes have
# correct and timely fee recipient information.
return if (currentEpoch - entry.addedAt) > 2:
err()
else:
ok entry.recipient
do:
return err()

func pruneOldMappings*(store: var DynamicFeeRecipientsStore,
currentEpoch: Epoch) =
var toPrune: seq[ValidatorIndex]

for idx, entry in store.mappings:
if (currentEpoch - entry.addedAt) > 2:
toPrune.add idx

for idx in toPrune:
store.mappings.del idx
2 changes: 2 additions & 0 deletions beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type
ImportDistributedKeystoresBody |
ImportRemoteKeystoresBody |
KeystoresAndSlashingProtection |
PrepareBeaconProposerBody |
ProposerSlashing |
SetFeeRecipientRequest |
SignedBlindedBeaconBlock |
Expand Down Expand Up @@ -123,6 +124,7 @@ type
KeymanagerGenericError |
KeystoresAndSlashingProtection |
ListFeeRecipientResponse |
PrepareBeaconProposerBody |
ProduceBlockResponseV2 |
RestDutyError |
RestGenericError |
Expand Down
4 changes: 4 additions & 0 deletions beacon_chain/spec/eth2_apis/rest_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ type
epoch*: Epoch
active*: bool

PrepareBeaconProposerBody* = object
validator_index*: ValidatorIndex
Copy link
Member

Choose a reason for hiding this comment

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

RestValidatorIndex I believe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RestValidatorIndex was something that raised in prominence when we tried to introduce a wider ValidatorIndex reform (which wasn't merged). In the current code, it's used only for a single field and all other REST data types use the spec ValidatorIndex type.

Copy link
Member

@arnetheduck arnetheduck Aug 9, 2022

Choose a reason for hiding this comment

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

The wider VI is unrelated - RestValidatorIndex is used to differentiate between errors: a validator index that is larger than 2**32 but smaller than 2**41 (or something like that) is a valid but not yet assigned validator index - the ValidatorIndex parser is unable to distinguish these two cases, resulting in a poor error message, or in some requests, invalid responses (because some requests give an empty response for a not-yet-assigned validator index)

fee_recipient*: Eth1Address

RestPublishedSignedBeaconBlock* = distinct ForkedSignedBeaconBlock

RestPublishedBeaconBlock* = distinct ForkedBeaconBlock
Expand Down
5 changes: 5 additions & 0 deletions beacon_chain/spec/eth2_apis/rest_validator_calls.nim
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ proc publishContributionAndProofs*(body: seq[RestSignedContributionAndProof]): R
rest, endpoint: "/eth/v1/validator/contribution_and_proofs",
meth: MethodPost.}
## https://ethereum.github.io/beacon-APIs/#/Validator/publishContributionAndProofs

proc prepareBeaconProposer*(body: PrepareBeaconProposerBody): RestPlainResponse {.
rest, endpoint: "/eth/v1/validator/prepare_beacon_proposer",
meth: MethodPost.}
## https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/prepareBeaconProposer
14 changes: 11 additions & 3 deletions beacon_chain/validators/validator_duties.nim
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ proc getBlockProposalEth1Data*(node: BeaconNode,
state, finalizedEpochRef.eth1_data,
finalizedEpochRef.eth1_deposit_index)

# TODO: This copies the entire BeaconState on each call
proc forkchoice_updated(state: bellatrix.BeaconState,
head_block_hash: Eth2Digest,
finalized_block_hash: Eth2Digest,
Expand Down Expand Up @@ -340,7 +341,10 @@ proc get_execution_payload(
await execution_engine.getPayload(payload_id.get))

proc getExecutionPayload(
node: BeaconNode, proposalState: auto, pubkey: ValidatorPubKey):
node: BeaconNode, proposalState: auto,
epoch: Epoch,
validator_index: ValidatorIndex,
pubkey: ValidatorPubKey):
Future[ExecutionPayload] {.async.} =
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/bellatrix/validator.md#executionpayload

Expand Down Expand Up @@ -374,8 +378,11 @@ proc getExecutionPayload(
terminalBlockHash
latestFinalized =
node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck)
feeRecipient = node.config.getSuggestedFeeRecipient(pubkey).valueOr:
node.config.defaultFeeRecipient
dynamicFeeRecipient = node.dynamicFeeRecipientsStore.getDynamicFeeRecipient(
Copy link
Member

Choose a reason for hiding this comment

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

fwiw, I think this can be written as gDFR() or gSFR() or Opt.some(default)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The valueOr construct effectively works as a right-associative operator while or is left-associative. For this particular purpose, right-associativity produces better code which is equivalent to a hand-written tree of if statements.

validator_index, epoch)
feeRecipient = dynamicFeeRecipient.valueOr:
node.config.getSuggestedFeeRecipient(pubkey).valueOr:
node.config.defaultFeeRecipient
payload_id = (await forkchoice_updated(
proposalState.bellatrixData.data, latestHead, latestFinalized,
feeRecipient,
Expand Down Expand Up @@ -461,6 +468,7 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode,
let pubkey = node.dag.validatorKey(validator_index)
(await getExecutionPayload(
node, proposalState,
slot.epoch, validator_index,
# TODO https://github.com/nim-lang/Nim/issues/19802
if pubkey.isSome: pubkey.get.toPubKey else: default(ValidatorPubKey))),
noRollback, # Temporary state - no need for rollback
Expand Down