200 lines
7.8 KiB
Nim
200 lines
7.8 KiB
Nim
# beacon_chain
|
|
# Copyright (c) 2023 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.
|
|
|
|
import
|
|
confutils,
|
|
../beacon_chain/spec/datatypes/capella,
|
|
../beacon_chain/rpc/rest_utils,
|
|
../beacon_chain/spec/eth2_apis/rest_beacon_client
|
|
|
|
const HttpOk = 200
|
|
|
|
type
|
|
ParentHeaderInfo = object
|
|
block_number: uint64
|
|
timestamp: uint64
|
|
|
|
MevMockConf* = object
|
|
# Deliberately no default. Assuming such has caused too many CI issues
|
|
port {. desc: "REST HTTP server port" .}: int
|
|
|
|
proc getPrevRandao(restClient: RestClientRef):
|
|
Future[Opt[Eth2Digest]] {.async.} =
|
|
let resp: RestResponse[rest_types.GetStateRandaoResponse] =
|
|
await restClient.getStateRandao(StateIdent.init(StateIdentType.Head))
|
|
|
|
return if resp.status == HttpOk:
|
|
Opt.some resp.data.data.randao
|
|
else:
|
|
Opt.none Eth2Digest
|
|
|
|
proc getParentBlock(restClient: RestClientRef):
|
|
Future[Opt[ParentHeaderInfo]] {.async.} =
|
|
let
|
|
respMaybe: Option[ref ForkedSignedBeaconBlock] =
|
|
# defaultRuntimeConfig only kicks in for SSZ and this can use JSON
|
|
await restClient.getBlockV2(
|
|
BlockIdent.init(BlockIdentType.Head), defaultRuntimeConfig)
|
|
resp =
|
|
if respMaybe.isSome and not respMaybe.get.isNil:
|
|
respMaybe.get[]
|
|
else:
|
|
return Opt.none ParentHeaderInfo
|
|
|
|
withBlck(resp):
|
|
when consensusFork >= ConsensusFork.Capella:
|
|
return Opt.some ParentHeaderInfo(
|
|
block_number: blck.message.body.execution_payload.block_number,
|
|
timestamp: blck.message.body.execution_payload.timestamp)
|
|
else:
|
|
discard
|
|
|
|
proc getWithdrawals(restClient: RestClientRef):
|
|
Future[Opt[seq[Withdrawal]]] {.async.} =
|
|
let resp: RestResponse[rest_types.GetNextWithdrawalsResponse] =
|
|
await restClient.getNextWithdrawals(StateIdent.init(StateIdentType.Head))
|
|
|
|
return if resp.status == HttpOk:
|
|
Opt.some resp.data.data
|
|
else:
|
|
Opt.none seq[Withdrawal]
|
|
|
|
proc getInfo(parent_hash: Eth2Digest):
|
|
Future[Opt[capella.ExecutionPayload]] {.async.} =
|
|
const DEFAULT_GAS_LIMIT: uint64 = 30000000
|
|
|
|
# TODO parallelize with await allFutures() to at least mitigate head race
|
|
var restClient: RestClientRef
|
|
let
|
|
prev_randao = (await getPrevRandao(restClient)).valueOr:
|
|
return Opt.none capella.ExecutionPayload
|
|
parent_block_info = (await getParentBlock(restClient)).valueOr:
|
|
return Opt.none capella.ExecutionPayload
|
|
withdrawals = (await getWithdrawals(restClient)).valueOr:
|
|
return Opt.none capella.ExecutionPayload
|
|
|
|
var execution_payload = capella.ExecutionPayload(
|
|
parent_hash: parent_hash,
|
|
fee_recipient: default(ExecutionAddress), # only a CL suggestion
|
|
logs_bloom: default(BloomLogs),
|
|
timestamp: parentBlockInfo.timestamp,
|
|
prev_randao: prev_randao,
|
|
block_number: parent_block_info.block_number,
|
|
gas_limit: DEFAULT_GAS_LIMIT,
|
|
gas_used: 0,
|
|
extra_data: default(List[byte, 32]),
|
|
transactions: default(List[Transaction, 1048576]),
|
|
withdrawals: List[capella.Withdrawal, 16].init(withdrawals)
|
|
)
|
|
|
|
return Opt.some execution_payload
|
|
|
|
func getExecutionPayloadHeader(execution_payload: capella.ExecutionPayload):
|
|
capella.ExecutionPayloadHeader =
|
|
capella.ExecutionPayloadHeader(
|
|
parent_hash: execution_payload.parent_hash,
|
|
fee_recipient: execution_payload.fee_recipient,
|
|
state_root: execution_payload.state_root,
|
|
receipts_root: execution_payload.receipts_root,
|
|
logs_bloom: execution_payload.logs_bloom,
|
|
prev_randao: execution_payload.prev_randao,
|
|
block_number: execution_payload.block_number,
|
|
gas_limit: execution_payload.gas_limit,
|
|
gas_used: execution_payload.gas_used,
|
|
timestamp: execution_payload.timestamp,
|
|
base_fee_per_gas: execution_payload.base_fee_per_gas,
|
|
block_hash: execution_payload.block_hash,
|
|
extra_data: execution_payload.extra_data,
|
|
transactions_root: hash_tree_root(execution_payload.transactions),
|
|
withdrawals_root: hash_tree_root(execution_payload.withdrawals))
|
|
|
|
func getSignedUnblindedBeaconBlock(
|
|
signedBlindedBlck: capella_mev.SignedBlindedBeaconBlock,
|
|
execution_payload: capella.ExecutionPayload): capella.SignedBeaconBlock =
|
|
template blindedBlck: untyped = signedBlindedBlck.message
|
|
var blck = capella.SignedBeaconBlock(
|
|
message: capella.BeaconBlock(
|
|
slot: blindedBlck.slot,
|
|
parent_root: blindedBlck.parent_root,
|
|
state_root: blindedBlck.state_root,
|
|
body: capella.BeaconBlockBody(
|
|
randao_reveal: blindedBlck.body.randao_reveal,
|
|
eth1_data: blindedBlck.body.eth1_data,
|
|
graffiti: blindedBlck.body.graffiti,
|
|
proposer_slashings: blindedBlck.body.proposer_slashings,
|
|
attester_slashings: blindedBlck.body.attester_slashings,
|
|
deposits: blindedBlck.body.deposits,
|
|
voluntary_exits: blindedBlck.body.voluntary_exits,
|
|
sync_aggregate: blindedBlck.body.sync_aggregate,
|
|
execution_payload: execution_payload,
|
|
bls_to_execution_changes:
|
|
blindedBlck.body.bls_to_execution_changes)),
|
|
signature: signedBlindedBlck.signature)
|
|
blck.root = hash_tree_root(blck.message)
|
|
blck
|
|
|
|
proc setupEngineAPI*(router: var RestRouter, payloadCache:
|
|
TableRef[Eth2Digest, capella.ExecutionPayload]) =
|
|
router.api(MethodPost, "/eth/v1/builder/validators") do (
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
|
|
if contentBody.isNone:
|
|
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
|
|
|
|
# No-op, deliberately. For this purpse, the only thing this does
|
|
return RestApiResponse.jsonResponse("")
|
|
|
|
router.api(MethodGet, "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}") do (
|
|
slot: Slot, parent_hash: Eth2Digest, pubkey: ValidatorPubKey) -> RestApiResponse:
|
|
if parent_hash.isErr:
|
|
return RestApiResponse.jsonError(Http400, "No parent head hash provided")
|
|
let execution_payload = (await getInfo(parent_hash.get)).valueOr:
|
|
return RestApiResponse.jsonError(Http400, "Error getting parent head information")
|
|
payloadCache[hash_tree_root(execution_payload)] = execution_payload
|
|
return RestApiResponse.jsonResponse(
|
|
getExecutionPayloadHeader(execution_payload))
|
|
|
|
router.api(MethodPost, "/eth/v1/builder/blinded_blocks") do (
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
if contentBody.isNone:
|
|
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
|
|
|
|
let
|
|
body = contentBody.get()
|
|
restBlock = decodeBody(
|
|
capella_mev.SignedBlindedBeaconBlock, body).valueOr:
|
|
return RestApiResponse.jsonError(Http400, InvalidBlockObjectError,
|
|
$error)
|
|
execution_header_root = hash_tree_root(
|
|
restBlock.message.body.execution_payload_header)
|
|
|
|
return if execution_header_root in payloadCache:
|
|
RestApiResponse.jsonResponse(getSignedUnblindedBeaconBlock(
|
|
restBlock, payloadCache[execution_header_root]))
|
|
else:
|
|
return RestApiResponse.jsonError(Http400, "Unknown execution payload")
|
|
|
|
router.api(MethodGet, "/eth/v1/builder/status") do () -> RestApiResponse:
|
|
return RestApiResponse.response("", Http200, "text/plain")
|
|
|
|
when isMainModule:
|
|
let conf = MevMockConf.load()
|
|
var router = RestRouter.init(proc(pattern: string, value: string): int = 0)
|
|
var payloadCache: TableRef[Eth2Digest, capella.ExecutionPayload]
|
|
setupEngineAPI(router, payloadCache)
|
|
|
|
let server = RestServerRef.new(
|
|
router, initTAddress("127.0.0.1", conf.port)).get()
|
|
|
|
server.start()
|
|
|
|
when compiles(waitFor waitSignal(SIGINT)):
|
|
waitFor waitSignal(SIGINT)
|
|
waitFor server.stop()
|
|
else:
|
|
runForever()
|