diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 4b3968cd87..fcd02c9632 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -722,6 +722,18 @@ AllTests-mainnet + Invalid Authorization Token [Beacon Node] [Preset: mainnet] OK + Missing Authorization header [Beacon Node] [Preset: mainnet] OK ``` +## MEV calls serialization/deserialization and behavior test suite +```diff ++ /eth/v1/builder/blinded_blocks [json/json] test OK ++ /eth/v1/builder/blinded_blocks [json/ssz] test OK ++ /eth/v1/builder/blinded_blocks [ssz/json] test OK ++ /eth/v1/builder/blinded_blocks [ssz/ssz] test OK ++ /eth/v1/builder/header [json] test OK ++ /eth/v1/builder/header [ssz] test OK ++ /eth/v1/builder/status test OK ++ /eth/v1/builder/validators [json] test OK ++ /eth/v1/builder/validators [ssz] test OK +``` ## Message signatures ```diff + Aggregate and proof signatures OK diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index 1cac1725d5..d527efd57e 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -271,4 +271,3 @@ const "Unable to load state for parent block, database corrupt?" RewardOverflowError* = "Reward value overflow" - InvalidContentTypeError* = "Invalid content type" diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 3cd01bd8e5..69b065ec0b 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -26,6 +26,7 @@ export jsonSerializationResults, rest_keymanager_types from web3/primitives import Hash32, Quantity +from json import getStr, newJString export primitives.Hash32, primitives.Quantity func decodeMediaType*( @@ -82,8 +83,6 @@ RestJson.useDefaultSerializationFor( GetForkChoiceResponse, GetForkScheduleResponse, GetGenesisResponse, - GetHeaderResponseDeneb, - GetHeaderResponseElectra, GetKeystoresResponse, GetNextWithdrawalsResponse, GetPoolAttesterSlashingsResponse, @@ -168,8 +167,6 @@ RestJson.useDefaultSerializationFor( SignedContributionAndProof, SignedValidatorRegistrationV1, SignedVoluntaryExit, - SubmitBlindedBlockResponseDeneb, - SubmitBlindedBlockResponseElectra, SyncAggregate, SyncAggregatorSelectionData, SyncCommittee, @@ -341,6 +338,8 @@ const UnableDecodeVersionError = "Unable to decode version" UnableDecodeError = "Unable to decode data" UnexpectedDecodeError = "Unexpected decoding error" + InvalidContentTypeError* = "Invalid content type" + UnexpectedForkVersionError* = "Unexpected fork version received" type EncodeTypes* = @@ -356,9 +355,6 @@ type SetGasLimitRequest | bellatrix_mev.SignedBlindedBeaconBlock | capella_mev.SignedBlindedBeaconBlock | - deneb_mev.SignedBlindedBeaconBlock | - electra_mev.SignedBlindedBeaconBlock | - fulu_mev.SignedBlindedBeaconBlock | phase0.AttesterSlashing | SignedValidatorRegistrationV1 | SignedVoluntaryExit | @@ -374,7 +370,10 @@ type DenebSignedBlockContents | ElectraSignedBlockContents | FuluSignedBlockContents | - ForkedMaybeBlindedBeaconBlock + ForkedMaybeBlindedBeaconBlock | + deneb_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock | + fulu_mev.SignedBlindedBeaconBlock EncodeArrays* = seq[phase0.Attestation] | @@ -392,6 +391,14 @@ type seq[RestBeaconCommitteeSelection] | seq[RestSyncCommitteeSelection] + MevDecodeTypes* = + GetHeaderResponseDeneb | + GetHeaderResponseElectra | + GetHeaderResponseFulu | + SubmitBlindedBlockResponseDeneb | + SubmitBlindedBlockResponseElectra | + SubmitBlindedBlockResponseFulu + DecodeTypes* = DataEnclosedObject | DataMetaEnclosedObject | @@ -3266,11 +3273,67 @@ proc decodeBodyJsonOrSsz*( return err( RestErrorMessage.init(Http400, UnableDecodeError, [exc.formatMsg("")])) - ok(data.toSeq) + ok(data.asSeq) else: err(RestErrorMessage.init(Http415, InvalidContentTypeError, [$body.contentType])) +proc decodeBytesJsonOrSsz*( + T: typedesc[MevDecodeTypes], + data: openArray[byte], + contentType: Opt[ContentTypeData], + version: string +): Result[T, RestErrorMessage] = + var res {.noinit.}: T + + let + typeFork = kind(typeof(res.data)) + consensusFork = ConsensusFork.decodeString(version).valueOr: + return err(RestErrorMessage.init(Http400, UnableDecodeVersionError, + [version, $error])) + if typeFork != consensusFork: + return err( + RestErrorMessage.init(Http400, UnexpectedForkVersionError, + ["eth-consensus-version", consensusFork.toString(), + typeFork.toString()])) + + if contentType == ApplicationJsonMediaType: + res = + try: + RestJson.decode( + data, + T, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg("") + return err( + RestErrorMessage.init(Http400, UnableDecodeError, + [exc.formatMsg("")])) + let jsonFork = ConsensusFork.decodeString(res.version.getStr()).valueOr: + return err(RestErrorMessage.init(Http400, UnableDecodeVersionError, + [res.version.getStr(), $error])) + if typeFork != jsonFork: + return err( + RestErrorMessage.init(Http400, UnexpectedForkVersionError, + ["json-version", res.version.getStr(), + typeFork.toString()])) + ok(res) + elif contentType == OctetStreamMediaType: + ok(T( + version: newJString(typeFork.toString()), + data: + try: + SSZ.decode(data, typeof(res.data)) + except SerializationError as exc: + return err( + RestErrorMessage.init(Http400, UnableDecodeError, + [exc.formatMsg("")])))) + else: + err(RestErrorMessage.init(Http415, InvalidContentTypeError, + [$contentType])) + proc decodeBody*[T](t: typedesc[T], body: ContentBody): Result[T, cstring] = if body.contentType != ApplicationJsonMediaType: @@ -3324,6 +3387,31 @@ proc decodeBodyJsonOrSsz*[T](t: typedesc[T], err(RestErrorMessage.init(Http415, InvalidContentTypeError, [$body.contentType])) +proc encodeBytes*(value: seq[SignedValidatorRegistrationV1], + contentType: string): RestResult[seq[byte]] = + case contentType + of "application/json": + try: + var + stream = memoryOutput() + writer = JsonWriter[RestJson].init(stream) + writer.writeArray(value) + ok(stream.getOutput(seq[byte])) + except IOError: + return err("Input/output error") + except SerializationError: + return err("Serialization error") + of "application/octet-stream": + try: + ok(SSZ.encode( + init( + List[SignedValidatorRegistrationV1, Limit VALIDATOR_REGISTRY_LIMIT], + value))) + except SerializationError: + return err("Serialization error") + else: + err("Content-Type not supported") + proc encodeBytes*[T: EncodeTypes](value: T, contentType: string): RestResult[seq[byte]] = case contentType @@ -3363,29 +3451,26 @@ proc encodeBytes*[T: EncodeArrays](value: T, err("Content-Type not supported") proc encodeBytes*[T: EncodeOctetTypes]( - value: T, - contentType: string - ): RestResult[seq[byte]] = + value: T, + contentType: string +): RestResult[seq[byte]] = case contentType of "application/json": - let data = - try: - var stream = memoryOutput() - var writer = JsonWriter[RestJson].init(stream) - writer.writeValue(value) - stream.getOutput(seq[byte]) - except IOError: - return err("Input/output error") - except SerializationError: - return err("Serialization error") - ok(data) + try: + var + stream = memoryOutput() + writer = JsonWriter[RestJson].init(stream) + writer.writeValue(value) + ok(stream.getOutput(seq[byte])) + except IOError: + err("Input/output error") + except SerializationError: + err("Serialization error") of "application/octet-stream": - let data = - try: - SSZ.encode(value) - except CatchableError: - return err("Serialization error") - ok(data) + try: + ok(SSZ.encode(value)) + except CatchableError: + err("Serialization error") else: err("Content-Type not supported") diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index 06182eb2b1..074a8bb2f9 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2018-2024 Status Research & Development GmbH +# Copyright (c) 2018-2025 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). @@ -518,9 +518,6 @@ type GetEpochCommitteesResponse* = DataEnclosedObject[seq[RestBeaconStatesCommittees]] GetForkScheduleResponse* = DataEnclosedObject[seq[Fork]] GetGenesisResponse* = DataEnclosedObject[RestGenesis] - GetHeaderResponseDeneb* = DataVersionEnclosedObject[deneb_mev.SignedBuilderBid] - GetHeaderResponseElectra* = DataVersionEnclosedObject[electra_mev.SignedBuilderBid] - GetHeaderResponseFulu* = DataVersionEnclosedObject[fulu_mev.SignedBuilderBid] GetNetworkIdentityResponse* = DataEnclosedObject[RestNetworkIdentity] GetPeerCountResponse* = DataMetaEnclosedObject[RestPeerCount] GetPeerResponse* = DataMetaEnclosedObject[RestNodePeer] @@ -546,14 +543,18 @@ type GetEpochSyncCommitteesResponse* = DataEnclosedObject[RestEpochSyncCommittee] ProduceAttestationDataResponse* = DataEnclosedObject[AttestationData] ProduceSyncCommitteeContributionResponse* = DataEnclosedObject[SyncCommitteeContribution] - SubmitBlindedBlockResponseDeneb* = DataEnclosedObject[deneb_mev.ExecutionPayloadAndBlobsBundle] - SubmitBlindedBlockResponseElectra* = DataEnclosedObject[electra_mev.ExecutionPayloadAndBlobsBundle] - SubmitBlindedBlockResponseFulu* = DataEnclosedObject[fulu_mev.ExecutionPayloadAndBlobsBundle] GetValidatorsActivityResponse* = DataEnclosedObject[seq[RestActivityItem]] GetValidatorsLivenessResponse* = DataEnclosedObject[seq[RestLivenessItem]] SubmitBeaconCommitteeSelectionsResponse* = DataEnclosedObject[seq[RestBeaconCommitteeSelection]] SubmitSyncCommitteeSelectionsResponse* = DataEnclosedObject[seq[RestSyncCommitteeSelection]] + GetHeaderResponseDeneb* = DataVersionEnclosedObject[deneb_mev.SignedBuilderBid] + GetHeaderResponseElectra* = DataVersionEnclosedObject[electra_mev.SignedBuilderBid] + GetHeaderResponseFulu* = DataVersionEnclosedObject[fulu_mev.SignedBuilderBid] + SubmitBlindedBlockResponseDeneb* = DataVersionEnclosedObject[deneb_mev.ExecutionPayloadAndBlobsBundle] + SubmitBlindedBlockResponseElectra* = DataVersionEnclosedObject[electra_mev.ExecutionPayloadAndBlobsBundle] + SubmitBlindedBlockResponseFulu* = DataVersionEnclosedObject[fulu_mev.ExecutionPayloadAndBlobsBundle] + RestNodeValidity* {.pure.} = enum valid = "VALID", invalid = "INVALID", diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index 01f01a0f88..d19caa59db 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -441,7 +441,9 @@ template kind*( deneb.SigVerifiedSignedBeaconBlock | deneb.MsgTrustedSignedBeaconBlock | deneb.TrustedSignedBeaconBlock | - deneb_mev.SignedBlindedBeaconBlock]): ConsensusFork = + deneb_mev.SignedBlindedBeaconBlock | + deneb_mev.SignedBuilderBid | + deneb_mev.ExecutionPayloadAndBlobsBundle]): ConsensusFork = ConsensusFork.Deneb template kind*( @@ -464,7 +466,9 @@ template kind*( electra.SingleAttestation | electra.AggregateAndProof | electra.SignedAggregateAndProof | - electra_mev.SignedBlindedBeaconBlock]): ConsensusFork = + electra_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBuilderBid | + electra_mev.ExecutionPayloadAndBlobsBundle]): ConsensusFork = ConsensusFork.Electra template kind*( @@ -483,7 +487,9 @@ template kind*( fulu.SigVerifiedSignedBeaconBlock | fulu.MsgTrustedSignedBeaconBlock | fulu.TrustedSignedBeaconBlock | - fulu_mev.SignedBlindedBeaconBlock]): ConsensusFork = + fulu_mev.SignedBlindedBeaconBlock | + fulu_mev.SignedBuilderBid | + fulu_mev.ExecutionPayloadAndBlobsBundle]): ConsensusFork = ConsensusFork.Fulu template BeaconState*(kind: static ConsensusFork): auto = diff --git a/beacon_chain/spec/mev/rest_deneb_mev_calls.nim b/beacon_chain/spec/mev/rest_deneb_mev_calls.nim index 327144d220..8074bef942 100644 --- a/beacon_chain/spec/mev/rest_deneb_mev_calls.nim +++ b/beacon_chain/spec/mev/rest_deneb_mev_calls.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2023-2024 Status Research & Development GmbH +# Copyright (c) 2023-2025 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). @@ -13,6 +13,11 @@ import export chronos, client, rest_types, eth2_rest_serialization +proc getStatus*(): RestPlainResponse {. + rest, endpoint: "/eth/v1/builder/status", + meth: MethodGet.} + ## https://ethereum.github.io/builder-specs/#/Builder/status + proc registerValidator*(body: seq[SignedValidatorRegistrationV1] ): RestPlainResponse {. rest, endpoint: "/eth/v1/builder/validators", @@ -20,19 +25,33 @@ proc registerValidator*(body: seq[SignedValidatorRegistrationV1] ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/validators.yaml ## https://github.com/ethereum/beacon-APIs/blob/v2.3.0/apis/validator/register_validator.yaml -proc getHeaderDeneb*(slot: Slot, - parent_hash: Eth2Digest, - pubkey: ValidatorPubKey - ): RestPlainResponse {. - rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", - meth: MethodGet, connection: {Dedicated, Close}.} +proc getHeaderDenebPlain*( + slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey +): RestPlainResponse {. + rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", + meth: MethodGet, connection: {Dedicated, Close}.} ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/header.yaml +proc getHeaderDeneb*( + client: RestClientRef, + slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey +): Future[RestPlainResponse] {. + async: (raises: [CancelledError, RestEncodingError, RestDnsResolveError, + RestCommunicationError], raw: true).} = + client.getHeaderDenebPlain( + slot, parent_hash, pubkey, + restAcceptType = "application/octet-stream,application/json;q=0.5", + ) + proc submitBlindedBlockPlain*( body: deneb_mev.SignedBlindedBeaconBlock ): RestPlainResponse {. - rest, endpoint: "/eth/v1/builder/blinded_blocks", - meth: MethodPost, connection: {Dedicated, Close}.} + rest, endpoint: "/eth/v1/builder/blinded_blocks", + meth: MethodPost, connection: {Dedicated, Close}.} ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/blinded_blocks.yaml proc submitBlindedBlock*( @@ -40,9 +59,10 @@ proc submitBlindedBlock*( body: deneb_mev.SignedBlindedBeaconBlock ): Future[RestPlainResponse] {. async: (raises: [CancelledError, RestEncodingError, RestDnsResolveError, - RestCommunicationError]).} = + RestCommunicationError], raw: true).} = ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/blinded_blocks.yaml - await client.submitBlindedBlockPlain( + client.submitBlindedBlockPlain( body, + restAcceptType = "application/octet-stream,application/json;q=0.5", extraHeaders = @[("eth-consensus-version", toString(ConsensusFork.Deneb))] ) diff --git a/beacon_chain/spec/mev/rest_electra_mev_calls.nim b/beacon_chain/spec/mev/rest_electra_mev_calls.nim index 2b92d8a55d..6984c5571c 100644 --- a/beacon_chain/spec/mev/rest_electra_mev_calls.nim +++ b/beacon_chain/spec/mev/rest_electra_mev_calls.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2024 Status Research & Development GmbH +# Copyright (c) 2024-2025 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). @@ -13,29 +13,44 @@ import export chronos, client, rest_types, eth2_rest_serialization -proc getHeaderElectra*(slot: Slot, - parent_hash: Eth2Digest, - pubkey: ValidatorPubKey - ): RestPlainResponse {. - rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", - meth: MethodGet, connection: {Dedicated, Close}.} +proc getHeaderElectraPlain*( + slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey +): RestPlainResponse {. + rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", + meth: MethodGet, connection: {Dedicated, Close}.} ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/header.yaml +proc getHeaderElectra*( + client: RestClientRef, + slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey +): Future[RestPlainResponse] {. + async: (raises: [CancelledError, RestEncodingError, RestDnsResolveError, + RestCommunicationError], raw: true).} = + client.getHeaderElectraPlain( + slot, parent_hash, pubkey, + restAcceptType = "application/octet-stream,application/json;q=0.5", + ) + proc submitBlindedBlockPlain*( body: electra_mev.SignedBlindedBeaconBlock ): RestPlainResponse {. - rest, endpoint: "/eth/v1/builder/blinded_blocks", - meth: MethodPost, connection: {Dedicated, Close}.} + rest, endpoint: "/eth/v1/builder/blinded_blocks", + meth: MethodPost, connection: {Dedicated, Close}.} ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/blinded_blocks.yaml proc submitBlindedBlock*( - client: RestClientRef, - body: electra_mev.SignedBlindedBeaconBlock + client: RestClientRef, + body: electra_mev.SignedBlindedBeaconBlock ): Future[RestPlainResponse] {. async: (raises: [CancelledError, RestEncodingError, RestDnsResolveError, - RestCommunicationError]).} = + RestCommunicationError], raw: true).} = ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/blinded_blocks.yaml - await client.submitBlindedBlockPlain( + client.submitBlindedBlockPlain( body, + restAcceptType = "application/octet-stream,application/json;q=0.5", extraHeaders = @[("eth-consensus-version", toString(ConsensusFork.Electra))] ) diff --git a/beacon_chain/spec/mev/rest_fulu_mev_calls.nim b/beacon_chain/spec/mev/rest_fulu_mev_calls.nim index 8be08ae3c5..61bd649bcd 100644 --- a/beacon_chain/spec/mev/rest_fulu_mev_calls.nim +++ b/beacon_chain/spec/mev/rest_fulu_mev_calls.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2024 Status Research & Development GmbH +# Copyright (c) 2024-2025 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). @@ -13,19 +13,33 @@ import export chronos, client, rest_types, eth2_rest_serialization -proc getHeaderFulu*(slot: Slot, - parent_hash: Eth2Digest, - pubkey: ValidatorPubKey - ): RestPlainResponse {. - rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", - meth: MethodGet, connection: {Dedicated, Close}.} +proc getHeaderFuluPlain*( + slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey +): RestPlainResponse {. + rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", + meth: MethodGet, connection: {Dedicated, Close}.} ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/header.yaml +proc getHeaderFulu*( + client: RestClientRef, + slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey +): Future[RestPlainResponse] {. + async: (raises: [CancelledError, RestEncodingError, RestDnsResolveError, + RestCommunicationError], raw: true).} = + client.getHeaderFuluPlain( + slot, parent_hash, pubkey, + restAcceptType = "application/octet-stream,application/json;q=0.5", + ) + proc submitBlindedBlockPlain*( body: fulu_mev.SignedBlindedBeaconBlock ): RestPlainResponse {. - rest, endpoint: "/eth/v1/builder/blinded_blocks", - meth: MethodPost, connection: {Dedicated, Close}.} + rest, endpoint: "/eth/v1/builder/blinded_blocks", + meth: MethodPost, connection: {Dedicated, Close}.} ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/blinded_blocks.yaml proc submitBlindedBlock*( @@ -33,9 +47,10 @@ proc submitBlindedBlock*( body: fulu_mev.SignedBlindedBeaconBlock ): Future[RestPlainResponse] {. async: (raises: [CancelledError, RestEncodingError, RestDnsResolveError, - RestCommunicationError]).} = + RestCommunicationError], raw: true).} = ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/blinded_blocks.yaml - await client.submitBlindedBlockPlain( + client.submitBlindedBlockPlain( body, + restAcceptType = "application/octet-stream,application/json;q=0.5", extraHeaders = @[("eth-consensus-version", toString(ConsensusFork.Fulu))] ) diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index 075f756348..726c1b42ee 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -391,7 +391,7 @@ proc get_builder_signature*( proc verify_builder_signature*( fork: Fork, msg: deneb_mev.BuilderBid | electra_mev.BuilderBid | - fulu_mev.BuilderBid, + fulu_mev.BuilderBid | ValidatorRegistrationV1, pubkey: ValidatorPubKey | CookedPubKey, signature: SomeSig): bool = let signing_root = compute_builder_signing_root(fork, msg) blsVerify(pubkey, signing_root.data, signature) diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index b6bd2dfd60..a5aff2d5aa 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -656,8 +656,9 @@ proc getBlindedExecutionPayload[ BUILDER_PROPOSAL_DELAY_TOLERANCE): return err "Timeout obtaining Deneb blinded header from builder" - res = decodeBytes( - GetHeaderResponseDeneb, response.data, response.contentType) + res = decodeBytesJsonOrSsz( + GetHeaderResponseDeneb, response.data, response.contentType, + response.headers.getString("eth-consensus-version")) blindedHeader = res.valueOr: return err( @@ -672,8 +673,9 @@ proc getBlindedExecutionPayload[ BUILDER_PROPOSAL_DELAY_TOLERANCE): return err "Timeout obtaining Electra blinded header from builder" - res = decodeBytes( - GetHeaderResponseElectra, response.data, response.contentType) + res = decodeBytesJsonOrSsz( + GetHeaderResponseElectra, response.data, response.contentType, + response.headers.getString("eth-consensus-version")) blindedHeader = res.valueOr: return err( @@ -690,8 +692,9 @@ proc getBlindedExecutionPayload[ BUILDER_PROPOSAL_DELAY_TOLERANCE): return err "Timeout obtaining Fulu blinded header from builder" - res = decodeBytes( - GetHeaderResponseFulu, response.data, response.contentType) + res = decodeBytesJsonOrSsz( + GetHeaderResponseFulu, response.data, response.contentType, + response.headers.getString("eth-consensus-version")) blindedHeader = res.valueOr: return err( diff --git a/beacon_chain/validators/message_router_mev.nim b/beacon_chain/validators/message_router_mev.nim index d8633e46c3..b6bf3951cc 100644 --- a/beacon_chain/validators/message_router_mev.nim +++ b/beacon_chain/validators/message_router_mev.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2022-2024 Status Research & Development GmbH +# Copyright (c) 2022-2025 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). @@ -87,14 +87,17 @@ proc unblindAndRouteBlockMEV*( $response.status & ": " & $shortLog(blindedBlock)) when blindedBlock is deneb_mev.SignedBlindedBeaconBlock: - let res = decodeBytes( - SubmitBlindedBlockResponseDeneb, response.data, response.contentType) + let res = decodeBytesJsonOrSsz( + SubmitBlindedBlockResponseDeneb, response.data, response.contentType, + response.headers.getString("eth-consensus-version")) elif blindedBlock is electra_mev.SignedBlindedBeaconBlock: - let res = decodeBytes( - SubmitBlindedBlockResponseElectra, response.data, response.contentType) + let res = decodeBytesJsonOrSsz( + SubmitBlindedBlockResponseElectra, response.data, response.contentType, + response.headers.getString("eth-consensus-version")) elif blindedBlock is fulu_mev.SignedBlindedBeaconBlock: - let res = decodeBytes( - SubmitBlindedBlockResponseFulu, response.data, response.contentType) + let res = decodeBytesJsonOrSsz( + SubmitBlindedBlockResponseFulu, response.data, response.contentType, + response.headers.getString("eth-consensus-version")) else: static: doAssert false diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 4cc34150de..7001d0f4e4 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -62,7 +62,8 @@ import # Unit test ./slashing_protection/test_slashing_protection_db, ./test_validator_client, ./test_beacon_validators, - ./test_beacon_chain_file + ./test_beacon_chain_file, + ./test_mev_calls when not defined(windows): import ./test_keymanager_api diff --git a/tests/test_mev_calls.nim b/tests/test_mev_calls.nim new file mode 100644 index 0000000000..a278ed1894 --- /dev/null +++ b/tests/test_mev_calls.nim @@ -0,0 +1,627 @@ +# beacon_chain +# Copyright (c) 2025 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: [].} +{.used.} + +import + stew/[bitseqs, endians2, objects, byteutils], + blscurve, bearssl/rand, + results, chronos, presto, unittest2, + chronos/unittest2/asynctests, + ../beacon_chain/spec/[presets, crypto, signatures, eth2_ssz_serialization, + helpers, forks], + ../beacon_chain/spec/mev/[deneb_mev, electra_mev, fulu_mev, + rest_deneb_mev_calls, rest_electra_mev_calls, + rest_fulu_mev_calls], + ../beacon_chain/rpc/rest_utils + +from std/times import Time, toUnix, fromUnix, getTime + +const + DenebSlot = Slot(32000) + ElectraSlot = Slot(64000) + FuluSlot = Slot(96000) + emptyFork = Fork() + emptyRoot = Eth2Digest() + +type + MevBlocks = deneb_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock | + fulu_mev.SignedBlindedBeaconBlock + + TestNodeRef* = ref object + validators: seq[ValidatorPubKey] + + TestKind* {.pure.} = enum + Json, Ssz + +proc keyGen(rng: var HmacDrbgContext): BlsResult[ValidatorPrivKey] = + var + pubkey: blscurve.PublicKey + seckey: blscurve.SecretKey + let bytes = rng.generate(array[32, byte]) + if not keyGen(bytes, pubkey, seckey): + return err "key generation failed" + ok(ValidatorPrivKey(seckey)) + +func specifiedFeeRecipient(x: int): Eth1Address = + copyMem(addr result, unsafeAddr x, sizeof x) + +proc prepareRegistration( + fork: Fork, + key: ValidatorPrivKey, + gas_limit: uint64 = 0'u64, + timestamp: Time, + feeRecipient: Eth1Address +): SignedValidatorRegistrationV1 = + var msg = + SignedValidatorRegistrationV1( + message: ValidatorRegistrationV1( + fee_recipient: ExecutionAddress(data: distinctBase(feeRecipient)), + gas_limit: gas_limit, + timestamp: uint64(timestamp.toUnix()), + pubkey: key.toPubKey().toPubKey() + )) + msg.signature = get_builder_signature(fork, msg.message, key).toValidatorSig() + msg + +proc generateRegistrations( + rng: var HmacDrbgContext, + count: int +): seq[SignedValidatorRegistrationV1] = + var res: seq[SignedValidatorRegistrationV1] + for index in 0 ..< count: + let + privateKey = keyGen(rng).valueOr: + raiseAssert "Unable to generate private key" + feeRecipient = specifiedFeeRecipient(index) + res.add(prepareRegistration( + emptyFork, privateKey, 30_000_000'u64, getTime(), feeRecipient)) + res + +proc prepare( + T: typedesc[MevBlocks], + slot: Slot, + parent_hash: Eth2Digest, + proposer_index: uint64, + privateKey: ValidatorPrivKey +): T = + var tmp: T + let + blindedBlock = typeof(tmp.message)( + slot: slot, + proposer_index: proposer_index, + body: typeof(tmp.message.body)( + execution_payload_header: + typeof(tmp.message.body.execution_payload_header)( + parent_hash: parent_hash + ))) + block_root = hash_tree_root(blindedBlock) + T(message: blindedBlock, + signature: get_block_signature(emptyFork, emptyRoot, slot, block_root, + privateKey).toValidatorSig()) + +proc jsonResponseSignedBuilderBid( + t: typedesc[RestApiResponse], + bid: deneb_mev.SignedBuilderBid | electra_mev.SignedBuilderBid | + fulu_mev.SignedBuilderBid +): RestApiResponse = + let + consensusFork = typeof(bid).kind() + headers = [("eth-consensus-version", consensusFork.toString())] + res = + try: + var stream = memoryOutput() + var writer = JsonWriter[RestJson].init(stream) + writer.beginRecord() + writer.writeField("version", consensusFork.toString()) + writer.writeField("data", bid) + writer.endRecord() + stream.getOutput(seq[byte]) + except IOError: + default(seq[byte]) + RestApiResponse.response(res, Http200, "application/json", headers = headers) + +proc jsonResponseExecutionPayloadAndBlobsBundle( + t: typedesc[RestApiResponse], + payload: deneb_mev.ExecutionPayloadAndBlobsBundle | + electra_mev.ExecutionPayloadAndBlobsBundle | + fulu_mev.ExecutionPayloadAndBlobsBundle +): RestApiResponse = + let + consensusFork = typeof(payload).kind() + headers = [("eth-consensus-version", consensusFork.toString())] + res = + try: + var stream = memoryOutput() + var writer = JsonWriter[RestJson].init(stream) + writer.beginRecord() + writer.writeField("version", consensusFork.toString()) + writer.writeField("data", payload) + writer.endRecord() + stream.getOutput(seq[byte]) + except IOError: + default(seq[byte]) + RestApiResponse.response(res, Http200, "application/json", headers = headers) + +proc sszResponseSignedBuilderBid*( + t: typedesc[RestApiResponse], + bid: deneb_mev.SignedBuilderBid | electra_mev.SignedBuilderBid | + fulu_mev.SignedBuilderBid, +): RestApiResponse = + mixin kind + let + consensusFork = typeof(bid).kind() + headers = [("eth-consensus-version", consensusFork.toString())] + res = + try: + var stream = memoryOutput() + var writer = SszWriter.init(stream) + writer.writeValue(bid) + stream.getOutput(seq[byte]) + except IOError: + default(seq[byte]) + RestApiResponse.response(res, Http200, "application/octet-stream", + headers = headers) + +proc sszResponseExecutionPayloadAndBlobsBundle*( + t: typedesc[RestApiResponse], + payload: deneb_mev.ExecutionPayloadAndBlobsBundle | + electra_mev.ExecutionPayloadAndBlobsBundle | + fulu_mev.ExecutionPayloadAndBlobsBundle, +): RestApiResponse = + mixin kind + let + consensusFork = typeof(payload).kind() + headers = [("eth-consensus-version", consensusFork.toString())] + res = + try: + var stream = memoryOutput() + var writer = SszWriter.init(stream) + writer.writeValue(payload) + stream.getOutput(seq[byte]) + except IOError: + default(seq[byte]) + RestApiResponse.response(res, Http200, "application/octet-stream", + headers = headers) + +proc setupEngineAPI*(router: var RestRouter, node: TestNodeRef) = + router.api2(MethodPost, "/eth/v1/builder/validators") do ( + contentBody: Option[ContentBody]) -> RestApiResponse: + + if contentBody.isNone: + return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) + + let registrations = + decodeBodyJsonOrSsz(seq[SignedValidatorRegistrationV1], + contentBody.get()).valueOr: + return RestApiResponse.jsonError(error) + + for item in registrations: + if not(verify_builder_signature(emptyFork, item.message, + item.message.pubkey, item.signature)): + return RestApiResponse.jsonError(Http400, + "Signature verification failed") + RestApiResponse.jsonResponse(Http200) + + router.api2(MethodGet, + "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}") do ( + slot: Slot, parent_hash: Eth2Digest, + pubkey: ValidatorPubKey) -> RestApiResponse: + let + qslot = slot.valueOr: + return RestApiResponse.jsonError(Http400, "Invalid slot", $error) + qhash = parent_hash.valueOr: + return RestApiResponse.jsonError(Http400, "Invalid parent_hash", $error) + qpubkey {.used.} = pubkey.valueOr: + return RestApiResponse.jsonError(Http400, "Invalid pubkey", $error) + contentType = preferredContentType(jsonMediaType, + sszMediaType).valueOr: + return RestApiResponse.jsonError(Http406, "Content is not acceptable") + + template respondSszOrJson(contentType, bid: auto): RestApiResponse = + if contentType == sszMediaType: + RestApiResponse.sszResponseSignedBuilderBid(bid) + elif contentType == jsonMediaType: + RestApiResponse.jsonResponseSignedBuilderBid(bid) + else: + RestApiResponse.jsonError(Http415, "Invalid Accept") + + if qslot == DenebSlot: + let bid = deneb_mev.SignedBuilderBid( + message: deneb_mev.BuilderBid( + header: deneb.ExecutionPayloadHeader(parent_hash: qhash)) + ) + respondSszOrJson(contentType, bid) + elif qslot == ElectraSlot: + let bid = electra_mev.SignedBuilderBid( + message: electra_mev.BuilderBid( + header: electra.ExecutionPayloadHeader(parent_hash: qhash)) + ) + respondSszOrJson(contentType, bid) + elif qslot == FuluSlot: + let bid = fulu_mev.SignedBuilderBid( + message: fulu_mev.BuilderBid( + header: fulu.ExecutionPayloadHeader(parent_hash: qhash)) + ) + respondSszOrJson(contentType, bid) + else: + RestApiResponse.jsonError(Http500, "Unsupported slot number") + + router.api2(MethodPost, "/eth/v1/builder/blinded_blocks") do ( + contentBody: Option[ContentBody]) -> RestApiResponse: + + if contentBody.isNone: + return RestApiResponse.jsonError(Http400, EmptyRequestBodyError) + + let + rawVersion = request.headers.getString("eth-consensus-version") + consensusFork = ConsensusFork.decodeString(rawVersion).valueOr: + return RestApiResponse.jsonError(Http400, "Invalid consensus version") + contentType = preferredContentType(jsonMediaType, + sszMediaType).valueOr: + return RestApiResponse.jsonError(Http406, "Content type not acceptable") + + if consensusFork < ConsensusFork.Deneb: + return RestApiResponse.jsonError(Http400, "Unsupported fork version") + + template respondSszOrJson(contentType, payload: auto): RestApiResponse = + if contentType == sszMediaType: + RestApiResponse.sszResponseExecutionPayloadAndBlobsBundle(payload) + elif contentType == jsonMediaType: + RestApiResponse.jsonResponseExecutionPayloadAndBlobsBundle(payload) + else: + RestApiResponse.jsonError(Http415, "Invalid Accept") + + if consensusFork == ConsensusFork.Deneb: + let + blck = + decodeBodyJsonOrSsz(deneb_mev.SignedBlindedBeaconBlock, + contentBody.get()).valueOr: + return RestApiResponse.jsonError(error) + proposerKey = + if int(blck.message.proposer_index) < len(node.validators): + node.validators[int(blck.message.proposer_index)] + else: + ValidatorPubKey() + slot = blck.message.slot + blockRoot = hash_tree_root(blck.message) + + if not(verify_block_signature(emptyFork, emptyRoot, slot, blockRoot, + proposerKey, blck.signature)): + return RestApiResponse.jsonError(Http400, "Invalid signature") + + let + payload = deneb_mev.ExecutionPayloadAndBlobsBundle( + execution_payload: deneb.ExecutionPayload( + parent_hash: blck.message.body.execution_payload_header.parent_hash + ), + blobs_bundle: BlobsBundle() + ) + respondSszOrJson(contentType, payload) + elif consensusFork == ConsensusFork.Electra: + let + blck = + decodeBodyJsonOrSsz(electra_mev.SignedBlindedBeaconBlock, + contentBody.get()).valueOr: + return RestApiResponse.jsonError(error) + payload = electra_mev.ExecutionPayloadAndBlobsBundle( + execution_payload: electra.ExecutionPayload( + parent_hash: blck.message.body.execution_payload_header.parent_hash + ), + blobs_bundle: BlobsBundle() + ) + respondSszOrJson(contentType, payload) + elif consensusFork == ConsensusFork.Fulu: + let + blck = + decodeBodyJsonOrSsz(fulu_mev.SignedBlindedBeaconBlock, + contentBody.get()).valueOr: + return RestApiResponse.jsonError(error) + payload = fulu_mev.ExecutionPayloadAndBlobsBundle( + execution_payload: fulu.ExecutionPayload( + parent_hash: blck.message.body.execution_payload_header.parent_hash + ), + blobs_bundle: BlobsBundle() + ) + respondSszOrJson(contentType, payload) + else: + raiseAssert "Unsupported fork version" + + router.api2(MethodGet, "/eth/v1/builder/status") do () -> RestApiResponse: + RestApiResponse.response(Http200) + +proc testSuite() = + + suite "MEV calls serialization/deserialization and behavior test suite": + let + rng = HmacDrbgContext.new() + node = TestNodeRef() + var router = RestRouter.init(proc(pattern: string, value: string): int = 0) + setupEngineAPI(router, node) + + let + bindAddress = try: + initTAddress("127.0.0.1", Port(0)) + except TransportAddressError as exc: + raiseAssert "Unexpected error, reason " & $exc.msg + + server = RestServerRef.new(router, bindAddress).valueOr: + raiseAssert "Unable to establish REST server, reason " & $error + address = server.localAddress() + + server.start() + + setup: + let + httpFlags: set[HttpClientFlag] = {} + prestoFlags = {RestClientFlag.CommaSeparatedArray} + socketFlags = {SocketFlags.TcpNoDelay} + remoteUri = "http://" & $address & "/" + client = RestClientRef.new( + remoteUri, prestoFlags, httpFlags, socketFlags = socketFlags).valueOr: + raiseAssert "Unable to resolve distributed signer address " & $address + + teardown: + waitFor client.closeWait() + + template getHeaderTest(responseKind: TestKind): untyped = + let + bytes = rng[].generate(array[32, byte]) + parent_hash = Eth2Digest(data: bytes) + privateKey = keyGen(rng[]).valueOr: + raiseAssert "Unable to generate private key" + publicKey = privateKey.toPubKey().toPubKey() + (restAcceptType1, responseMediaType1) = + if responseKind == TestKind.Ssz: + ("application/octet-stream", OctetStreamMediaType) + else: + ("application/json", ApplicationJsonMediaType) + (restAcceptType2, responseMediaType2) = + if responseKind == TestKind.Ssz: + ("application/json;q=0.9,application/octet-stream", + OctetStreamMediaType) + else: + ("application/octet-stream;q=0.9,application/json", + ApplicationJsonMediaType) + (restAcceptType3, responseMediaType3) = + if responseKind == TestKind.Ssz: + ("application/json;q=0.5,application/octet-stream;q=1.0", + OctetStreamMediaType) + else: + ("application/octet-stream;q=0.5,application/json;q=1.0", + ApplicationJsonMediaType) + + let + response1 = + await client.getHeaderDenebPlain(DenebSlot, parent_hash, + publicKey, restAcceptType = restAcceptType1) + response2 = + await client.getHeaderElectraPlain(ElectraSlot, parent_hash, + publicKey, restAcceptType = restAcceptType2) + response3 = + await client.getHeaderFuluPlain(FuluSlot, parent_hash, + publicKey, restAcceptType = restAcceptType3) + + check: + response1.status == 200 + response2.status == 200 + response3.status == 200 + response1.contentType.isSome() + response2.contentType.isSome() + response3.contentType.isSome() + response1.contentType.get().mediaType == responseMediaType1 + response2.contentType.get().mediaType == responseMediaType2 + response3.contentType.get().mediaType == responseMediaType3 + + let + version1 = response1.headers.getString("eth-consensus-version") + version2 = response2.headers.getString("eth-consensus-version") + version3 = response3.headers.getString("eth-consensus-version") + + check: + version1 == ConsensusFork.Deneb.toString() + version2 == ConsensusFork.Electra.toString() + version3 == ConsensusFork.Fulu.toString() + + let + bid1res = + decodeBytesJsonOrSsz(GetHeaderResponseDeneb, response1.data, + response1.contentType, version1) + bid2res = + decodeBytesJsonOrSsz(GetHeaderResponseElectra, response2.data, + response2.contentType, version2) + bid3res = + decodeBytesJsonOrSsz(GetHeaderResponseFulu, response3.data, + response3.contentType, version3) + + check: + bid1res.isOk() + bid2res.isOk() + bid3res.isOk() + bid1res.get().data.message.header.parent_hash == parent_hash + bid2res.get().data.message.header.parent_hash == parent_hash + bid3res.get().data.message.header.parent_hash == parent_hash + + template submitBlindedBlockTest( + requestKind: TestKind, + responseKind: TestKind + ): untyped = + let + parent_hash1 = Eth2Digest(data: rng[].generate(array[32, byte])) + parent_hash2 = Eth2Digest(data: rng[].generate(array[32, byte])) + parent_hash3 = Eth2Digest(data: rng[].generate(array[32, byte])) + privateKey1 = keyGen(rng[]).valueOr: + raiseAssert "Unable to generate private key" + privateKey2 = keyGen(rng[]).valueOr: + raiseAssert "Unable to generate private key" + privateKey3 = keyGen(rng[]).valueOr: + raiseAssert "Unable to generate private key" + publicKey1 = privateKey1.toPubKey().toPubKey() + publicKey2 = privateKey1.toPubKey().toPubKey() + publicKey3 = privateKey1.toPubKey().toPubKey() + + node.validators.reset() + node.validators.add(publicKey1) + node.validators.add(publicKey2) + node.validators.add(publicKey3) + + let + blck1 = + prepare(deneb_mev.SignedBlindedBeaconBlock, DenebSlot, parent_hash1, + 0'u64, privateKey1) + blck2 = + prepare(electra_mev.SignedBlindedBeaconBlock, ElectraSlot, parent_hash2, + 1'u64, privateKey2) + blck3 = + prepare(fulu_mev.SignedBlindedBeaconBlock, FuluSlot, parent_hash3, + 2'u64, privateKey3) + + restContentType1 = + if requestKind == TestKind.Ssz: + "application/octet-stream" + else: + "application/json" + restContentType2 = + if requestKind == TestKind.Ssz: + "application/octet-stream" + else: + "application/json" + restContentType3 = + if requestKind == TestKind.Ssz: + "application/octet-stream" + else: + "application/json" + (restAcceptType1, responseMediaType1) = + if responseKind == TestKind.Ssz: + ("application/octet-stream", OctetStreamMediaType) + else: + ("application/json", ApplicationJsonMediaType) + (restAcceptType2, responseMediaType2) = + if responseKind == TestKind.Ssz: + ("application/octet-stream,application/json;q=0.9", + OctetStreamMediaType) + else: + ("application/json,application/octet-stream;q=0.9", + ApplicationJsonMediaType) + (restAcceptType3, responseMediaType3) = + if responseKind == TestKind.Ssz: + ("application/json;q=0.5,application/octet-stream;q=1.0", + OctetStreamMediaType) + else: + ("application/octet-stream;q=0.5,application/json;q=1.0", + ApplicationJsonMediaType) + + response1 = + await client.submitBlindedBlockPlain( + blck1, + restContentType = restContentType1, + restAcceptType = restAcceptType1, + extraHeaders = @[("eth-consensus-version", + toString(ConsensusFork.Deneb))]) + response2 = + await client.submitBlindedBlockPlain( + blck2, + restContentType = restContentType2, + restAcceptType = restAcceptType2, + extraHeaders = @[("eth-consensus-version", + toString(ConsensusFork.Electra))]) + response3 = + await client.submitBlindedBlockPlain( + blck3, + restContentType = restContentType3, + restAcceptType = restAcceptType3, + extraHeaders = @[("eth-consensus-version", + toString(ConsensusFork.Fulu))]) + check: + response1.status == 200 + response2.status == 200 + response3.status == 200 + + let + version1 = response1.headers.getString("eth-consensus-version") + version2 = response2.headers.getString("eth-consensus-version") + version3 = response3.headers.getString("eth-consensus-version") + + check: + response1.contentType.isSome() + response2.contentType.isSome() + response3.contentType.isSome() + response1.contentType.get().mediaType == responseMediaType1 + response2.contentType.get().mediaType == responseMediaType2 + response3.contentType.get().mediaType == responseMediaType3 + version1 == ConsensusFork.Deneb.toString() + version2 == ConsensusFork.Electra.toString() + version3 == ConsensusFork.Fulu.toString() + + let + payload1res = + decodeBytesJsonOrSsz(SubmitBlindedBlockResponseDeneb, + response1.data, response1.contentType, version1) + payload2res = + decodeBytesJsonOrSsz(SubmitBlindedBlockResponseElectra, + response2.data, response2.contentType, version2) + payload3res = + decodeBytesJsonOrSsz(SubmitBlindedBlockResponseFulu, + response3.data, response3.contentType, version3) + + check: + payload1res.isOk() + payload2res.isOk() + payload3res.isOk() + payload1res.get().data.execution_payload.parent_hash == parent_hash1 + payload2res.get().data.execution_payload.parent_hash == parent_hash2 + payload3res.get().data.execution_payload.parent_hash == parent_hash3 + + asyncTest "/eth/v1/builder/status test": + let response = await client.getStatus() + check response.status == 200 + + asyncTest "/eth/v1/builder/validators [json] test": + let + response1 = + await client.registerValidator( + generateRegistrations(rng[], 5)) + response2 = + await client.registerValidator( + generateRegistrations(rng[], 5), + restContentType = "application/json") + check: + response1.status == 200 + response2.status == 200 + + asyncTest "/eth/v1/builder/validators [ssz] test": + let + response = + await client.registerValidator( + generateRegistrations(rng[], 5), + restContentType = "application/octet-stream") + check response.status == 200 + + asyncTest "/eth/v1/builder/header [json] test": + getHeaderTest(TestKind.Json) + + asyncTest "/eth/v1/builder/header [ssz] test": + getHeaderTest(TestKind.Ssz) + + asyncTest "/eth/v1/builder/blinded_blocks [json/json] test": + submitBlindedBlockTest(TestKind.Json, TestKind.Json) + + asyncTest "/eth/v1/builder/blinded_blocks [json/ssz] test": + submitBlindedBlockTest(TestKind.Json, TestKind.Ssz) + + asyncTest "/eth/v1/builder/blinded_blocks [ssz/ssz] test": + submitBlindedBlockTest(TestKind.Ssz, TestKind.Ssz) + + asyncTest "/eth/v1/builder/blinded_blocks [ssz/json] test": + submitBlindedBlockTest(TestKind.Ssz, TestKind.Json) + + suiteTeardown: + waitFor server.stop() + +testSuite()