From 3977f1529aa9e7d1f5bbf98df0c60ee357d805fc Mon Sep 17 00:00:00 2001 From: tersec Date: Tue, 14 Feb 2023 11:49:48 +0100 Subject: [PATCH] fill in remaining capellaImplementationMissing holes for builder API (#4606) --- beacon_chain/rpc/rest_beacon_api.nim | 16 +++- beacon_chain/spec/datatypes/base.nim | 8 +- .../eth2_apis/eth2_rest_serialization.nim | 13 ++- beacon_chain/spec/state_transition.nim | 28 ++++++- .../validators/message_router_mev.nim | 80 +++++++++++++++++++ 5 files changed, 135 insertions(+), 10 deletions(-) diff --git a/beacon_chain/rpc/rest_beacon_api.nim b/beacon_chain/rpc/rest_beacon_api.nim index c70d15c9f..cbb3dfe6c 100644 --- a/beacon_chain/rpc/rest_beacon_api.nim +++ b/beacon_chain/rpc/rest_beacon_api.nim @@ -852,7 +852,21 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = of ConsensusFork.EIP4844: return RestApiResponse.jsonError(Http500, $eip4844ImplementationMissing) of ConsensusFork.Capella: - return RestApiResponse.jsonError(Http500, $capellaImplementationMissing) + let res = + block: + let restBlock = decodeBodyJsonOrSsz( + capella_mev.SignedBlindedBeaconBlock, body).valueOr: + return RestApiResponse.jsonError(Http400, InvalidBlockObjectError, + $error) + await node.unblindAndRouteBlockMEV(restBlock) + + if res.isErr(): + return RestApiResponse.jsonError( + Http503, BeaconNodeInSyncError, $res.error()) + if res.get().isNone(): + return RestApiResponse.jsonError(Http202, BlockValidationError) + + return RestApiResponse.jsonMsgResponse(BlockValidationSuccess) of ConsensusFork.Bellatrix: let res = block: diff --git a/beacon_chain/spec/datatypes/base.nim b/beacon_chain/spec/datatypes/base.nim index eb6387d0a..1c8786df8 100644 --- a/beacon_chain/spec/datatypes/base.nim +++ b/beacon_chain/spec/datatypes/base.nim @@ -1010,11 +1010,9 @@ func checkForkConsistency*(cfg: RuntimeConfig) = assertForkEpochOrder(cfg.CAPELLA_FORK_EPOCH, cfg.EIP4844_FORK_EPOCH) # This is a readily/uniquely searchable token of where a false assertion is -# due to Capella implementation missing. checkForkConsistency() checks that -# Nimbus does not actually run any non-FAR_FUTURE_EPOCH Capella network, so -# such cases won't be hit. -const capellaImplementationMissing* = false - +# due to a Deneb implementation missing. checkForkConsistency() checks that +# Nimbus does not run any non-FAR_FUTURE_EPOCH Deneb network, so such cases +# won't be hit. const eip4844ImplementationMissing* = false #template debugRaiseAssert*(x: string) = raiseAssert x diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 161464f22..8cb94b190 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -1050,8 +1050,17 @@ proc readValue*[BlockType: ForkedBlindedBeaconBlock]( value = ForkedBlindedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: res) of ConsensusFork.Capella: - reader.raiseUnexpectedValue($capellaImplementationMissing) - + let res = + try: + RestJson.decode(string(data.get()), + BlindedBeaconBlock, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + reader.raiseUnexpectedValue("Incorrect capella block format, [" & + exc.formatMsg("BlindedBlock") & "]") + value = ForkedBlindedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: res) of ConsensusFork.EIP4844: reader.raiseUnexpectedValue($eip4844ImplementationMissing) diff --git a/beacon_chain/spec/state_transition.nim b/beacon_chain/spec/state_transition.nim index 84af084bc..7d7a22454 100644 --- a/beacon_chain/spec/state_transition.nim +++ b/beacon_chain/spec/state_transition.nim @@ -539,7 +539,10 @@ proc makeBeaconBlock*[T: bellatrix.ExecutionPayload | capella.ExecutionPayload | # Override for MEV if transactions_root.isSome and execution_payload_root.isSome: withState(state): - when stateFork >= ConsensusFork.Bellatrix: + when stateFork < ConsensusFork.Bellatrix: + # Vacuously + discard + elif stateFork == ConsensusFork.Bellatrix: forkyState.data.latest_execution_payload_header.transactions_root = transactions_root.get @@ -547,7 +550,6 @@ proc makeBeaconBlock*[T: bellatrix.ExecutionPayload | capella.ExecutionPayload | # Effectively hash_tree_root(ExecutionPayload) with the beacon block # body, with the execution payload replaced by the execution payload # header. htr(payload) == htr(payload header), so substitute. - discard $capellaImplementationMissing # need different htr to match capella changes forkyState.data.latest_block_header.body_root = hash_tree_root( [hash_tree_root(randao_reveal), hash_tree_root(eth1_data), @@ -559,6 +561,28 @@ proc makeBeaconBlock*[T: bellatrix.ExecutionPayload | capella.ExecutionPayload | hash_tree_root(validator_changes.voluntary_exits), hash_tree_root(sync_aggregate), execution_payload_root.get]) + elif stateFork == ConsensusFork.Capella: + forkyState.data.latest_execution_payload_header.transactions_root = + transactions_root.get + + # https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.2/specs/capella/beacon-chain.md#beaconblockbody + # Effectively hash_tree_root(ExecutionPayload) with the beacon block + # body, with the execution payload replaced by the execution payload + # header. htr(payload) == htr(payload header), so substitute. + forkyState.data.latest_block_header.body_root = hash_tree_root( + [hash_tree_root(randao_reveal), + hash_tree_root(eth1_data), + hash_tree_root(graffiti), + hash_tree_root(validator_changes.proposer_slashings), + hash_tree_root(validator_changes.attester_slashings), + hash_tree_root(List[Attestation, Limit MAX_ATTESTATIONS](attestations)), + hash_tree_root(List[Deposit, Limit MAX_DEPOSITS](deposits)), + hash_tree_root(validator_changes.voluntary_exits), + hash_tree_root(sync_aggregate), + execution_payload_root.get, + hash_tree_root(validator_changes.bls_to_execution_changes)]) + elif stateFork > ConsensusFork.Capella: + discard eip4844ImplementationMissing state.`kind Data`.root = hash_tree_root(state.`kind Data`.data) blck.`kind Data`.state_root = state.`kind Data`.root diff --git a/beacon_chain/validators/message_router_mev.nim b/beacon_chain/validators/message_router_mev.nim index b39b9792b..c05ef28f0 100644 --- a/beacon_chain/validators/message_router_mev.nim +++ b/beacon_chain/validators/message_router_mev.nim @@ -41,6 +41,9 @@ macro copyFields*( result.add newAssignment( newDotExpr(dst, ident(name)), newDotExpr(src, ident(name))) +# TODO when https://github.com/nim-lang/Nim/issues/21346 and/or +# https://github.com/nim-lang/Nim/issues/21347 fixed, combine and make generic +# these two very similar versions of unblindAndRouteBlockMEV proc unblindAndRouteBlockMEV*( node: BeaconNode, blindedBlock: bellatrix_mev.SignedBlindedBeaconBlock): Future[Result[Opt[BlockRef], string]] {.async.} = @@ -114,3 +117,80 @@ proc unblindAndRouteBlockMEV*( # local build process as a fallback, even in the event of some failure # with the external buildernetwork. return err("unblindAndRouteBlockMEV error") + +# TODO currently cannot be combined into one generic function +# Only difference is `var signedBlock = capella.SignedBeaconBlock` instead of +# `var signedBlock = bellatrix.SignedBeaconBlock` +proc unblindAndRouteBlockMEV*( + node: BeaconNode, blindedBlock: capella_mev.SignedBlindedBeaconBlock): + Future[Result[Opt[BlockRef], string]] {.async.} = + # By time submitBlindedBlock is called, must already have done slashing + # protection check + if node.payloadBuilderRestClient.isNil: + return err "unblindAndRouteBlockMEV: nil REST client" + + let unblindedPayload = + try: + awaitWithTimeout( + node.payloadBuilderRestClient.submitBlindedBlock(blindedBlock), + BUILDER_BLOCK_SUBMISSION_DELAY_TOLERANCE): + return err("Submitting blinded block timed out") + # From here on, including error paths, disallow local EL production by + # returning Opt.some, regardless of whether on head or newBlock. + except RestDecodingError as exc: + return err("REST decoding error submitting blinded block: " & exc.msg) + except CatchableError as exc: + return err("exception in submitBlindedBlock: " & exc.msg) + + const httpOk = 200 + if unblindedPayload.status == httpOk: + if hash_tree_root( + blindedBlock.message.body.execution_payload_header) != + hash_tree_root(unblindedPayload.data.data): + debug "unblindAndRouteBlockMEV: unblinded payload doesn't match blinded payload", + blindedPayload = + blindedBlock.message.body.execution_payload_header + else: + # Signature provided is consistent with unblinded execution payload, + # so construct full beacon block + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal + var signedBlock = capella.SignedBeaconBlock( + signature: blindedBlock.signature) + copyFields( + signedBlock.message, blindedBlock.message, + getFieldNames(typeof(signedBlock.message))) + copyFields( + signedBlock.message.body, blindedBlock.message.body, + getFieldNames(typeof(signedBlock.message.body))) + signedBlock.message.body.execution_payload = unblindedPayload.data.data + + signedBlock.root = hash_tree_root(signedBlock.message) + + doAssert signedBlock.root == hash_tree_root(blindedBlock.message) + + debug "unblindAndRouteBlockMEV: proposing unblinded block", + blck = shortLog(signedBlock) + + let newBlockRef = + (await node.router.routeSignedBeaconBlock(signedBlock)).valueOr: + # submitBlindedBlock has run, so don't allow fallback to run + return err("routeSignedBeaconBlock error") # Errors logged in router + + if newBlockRef.isSome: + beacon_block_builder_proposed.inc() + notice "Block proposed (MEV)", + blockRoot = shortLog(signedBlock.root), blck = shortLog(signedBlock), + signature = shortLog(signedBlock.signature) + + return ok newBlockRef + else: + debug "unblindAndRouteBlockMEV: submitBlindedBlock failed", + blindedBlock, payloadStatus = unblindedPayload.status + + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#proposer-slashing + # This means if a validator publishes a signature for a + # `BlindedBeaconBlock` (via a dissemination of a + # `SignedBlindedBeaconBlock`) then the validator **MUST** not use the + # local build process as a fallback, even in the event of some failure + # with the external buildernetwork. + return err("unblindAndRouteBlockMEV error")