initial version of mock builder relay (#5034)
This commit is contained in:
parent
58e3de3bf2
commit
38a6706509
1
Makefile
1
Makefile
|
@ -58,6 +58,7 @@ TOOLS_CORE := \
|
|||
deposit_contract \
|
||||
resttest \
|
||||
logtrace \
|
||||
mev_mock \
|
||||
ncli \
|
||||
ncli_db \
|
||||
ncli_split_keystore \
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
# 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()
|
Loading…
Reference in New Issue