From ddfccd519741927a11be972b44eb0c8f79406d47 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Thu, 13 Aug 2026 10:25:19 +1000 Subject: [PATCH 1/6] remove BlindedExecutionPayloadEnvelope/SignedBlindedExecutionPayloadEnvelope from PostExecutionPayloadEnvelope, and add `Eth-Blob-Data-Included` header to distiguish between ExecutionPayloadEnvelopeContents and ExecutionPayloadEnvelope when parsing request body Signed-off-by: Gabriel Fukushima --- .../coordinator/ExecutionPayloadFactory.java | 10 - .../ExecutionPayloadFactoryGloas.java | 14 - .../coordinator/ValidatorApiHandler.java | 15 - .../publisher/ExecutionPayloadPublisher.java | 14 - .../ExecutionPayloadPublisherGloas.java | 34 --- .../ExecutionPayloadPublisherGloasTest.java | 36 --- ...v1_beacon_execution_payload_envelopes.json | 178 +++++++----- .../_eth_v4_validator_blocks_{slot}.json | 270 ++++++++++-------- .../BlindedExecutionPayloadEnvelope.json | 31 -- .../schema/ExecutionPayloadHeaderGloas.json | 121 -------- ...SignedBlindedExecutionPayloadEnvelope.json | 16 -- .../beaconrestapi/BeaconRestApiTypes.java | 6 +- .../beacon/PostExecutionPayloadEnvelope.java | 60 ++-- .../handlers/v4/validator/GetNewBlockV4.java | 10 +- .../PostExecutionPayloadEnvelopeTest.java | 25 +- .../teku/api/ValidatorDataProvider.java | 8 - .../infrastructure/http/RestApiConstants.java | 3 +- .../teku/validator/api/BuilderApiChannel.java | 5 - .../validator/api/ValidatorApiChannel.java | 10 - .../MetricRecordingValidatorApiChannel.java | 11 - ...lishSignedExecutionPayloadRequestTest.java | 4 +- .../remote/FailoverValidatorApiHandler.java | 26 -- .../remote/RemoteValidatorApiHandler.java | 11 - .../sentry/SentryValidatorApiChannel.java | 10 - .../typedef/OkHttpValidatorTypeDefClient.java | 10 - .../PublishSignedExecutionPayloadRequest.java | 45 ++- 26 files changed, 326 insertions(+), 657 deletions(-) delete mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BlindedExecutionPayloadEnvelope.json delete mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/ExecutionPayloadHeaderGloas.json delete mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SignedBlindedExecutionPayloadEnvelope.json diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactory.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactory.java index b5681bd6d23..21331107854 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactory.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactory.java @@ -19,7 +19,6 @@ import tech.pegasys.teku.spec.datastructures.blobs.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockAndState; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -33,12 +32,6 @@ public SafeFuture createUnsignedExecutionPayload( return SafeFuture.completedFuture(null); } - @Override - public SafeFuture unblindSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload) { - return SafeFuture.completedFuture(null); - } - @Override public SafeFuture> createDataColumnSidecars( final SignedExecutionPayloadEnvelope signedExecutionPayload) { @@ -55,9 +48,6 @@ public SafeFuture> createDataColumnSidecars( SafeFuture createUnsignedExecutionPayload( UInt64 builderIndex, BeaconBlockAndState blockAndState); - SafeFuture unblindSignedExecutionPayload( - SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload); - SafeFuture> createDataColumnSidecars( SignedExecutionPayloadEnvelope signedExecutionPayload); diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java index 20ec9ae9f5e..beebd5ea9b3 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java @@ -25,7 +25,6 @@ import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.Blob; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockAndState; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.spec.datastructures.execution.BlobAndCellProofs; @@ -69,19 +68,6 @@ public SafeFuture createUnsignedExecutionPayload( proposalSlot, builderIndex, blockAndState, executionPayloadProposalDataFuture); } - @Override - public SafeFuture unblindSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload) { - final UInt64 slot = signedBlindedExecutionPayload.getSlot(); - final SchemaDefinitionsGloas schemaDefinitions = - SchemaDefinitionsGloas.required(spec.atSlot(slot).getSchemaDefinitions()); - return getCachedGetPayloadResponseFuture(slot) - .thenApply( - getPayloadResponse -> - signedBlindedExecutionPayload.unblind( - schemaDefinitions, getPayloadResponse.getExecutionPayload())); - } - @Override public SafeFuture> createDataColumnSidecars( final SignedExecutionPayloadEnvelope signedExecutionPayload) { diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java index f958adf93cd..a17052990e0 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ValidatorApiHandler.java @@ -86,7 +86,6 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -1060,20 +1059,6 @@ public SafeFuture publishSignedExecutionPay }); } - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return executionPayloadPublisher - .publishSignedExecutionPayload(signedBlindedExecutionPayload, broadcastValidationLevel) - .exceptionally( - ex -> { - final String reason = getRootCauseMessage(ex); - return PublishSignedExecutionPayloadResult.rejected( - signedBlindedExecutionPayload.getBeaconBlockRoot(), reason); - }); - } - private Optional fromInternalValidationResult( final InternalValidationResult internalValidationResult, final int resultIndex) { if (!internalValidationResult.isReject()) { diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisher.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisher.java index f359aceceb3..cb9f72e6a4c 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisher.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisher.java @@ -15,7 +15,6 @@ import java.util.Optional; import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel; @@ -43,15 +42,6 @@ public SafeFuture publishSignedExecutionPay PublishSignedExecutionPayloadResult.success( signedExecutionPayloadEnvelopeContents.getBeaconBlockRoot())); } - - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return SafeFuture.completedFuture( - PublishSignedExecutionPayloadResult.success( - signedBlindedExecutionPayload.getBeaconBlockRoot())); - } }; default SafeFuture publishSignedExecutionPayload( @@ -66,8 +56,4 @@ SafeFuture publishSignedExecutionPayload( SafeFuture publishSignedExecutionPayload( SignedExecutionPayloadEnvelopeContents signedExecutionPayloadEnvelopeContents, Optional broadcastValidationLevel); - - SafeFuture publishSignedExecutionPayload( - SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - Optional broadcastValidationLevel); } diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java index 6000e6ca4f1..543805d50bd 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java @@ -22,7 +22,6 @@ import tech.pegasys.teku.networking.eth2.gossip.DataColumnSidecarGossipChannel; import tech.pegasys.teku.networking.eth2.gossip.ExecutionPayloadGossipChannel; import tech.pegasys.teku.spec.datastructures.blobs.DataColumnSidecar; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel; @@ -72,39 +71,6 @@ public SafeFuture publishSignedExecutionPay broadcastValidationLevel); } - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return SafeFuture.of( - () -> - executionPayloadFactory.unblindSignedExecutionPayload( - signedBlindedExecutionPayload)) - .thenApply(Optional::of) - .exceptionally( - error -> { - LOG.warn( - "Failed to unblind execution payload envelope for beacon block root {}", - signedBlindedExecutionPayload.getBeaconBlockRoot(), - error); - return Optional.empty(); - }) - .thenCompose( - maybeSignedExecutionPayload -> - maybeSignedExecutionPayload - .map( - signedExecutionPayload -> - publishSignedExecutionPayload( - signedExecutionPayload, broadcastValidationLevel)) - .orElseGet( - () -> - SafeFuture.completedFuture( - PublishSignedExecutionPayloadResult.rejected( - signedBlindedExecutionPayload.getBeaconBlockRoot(), - "No cached execution payload envelope found for blinded" - + " envelope")))); - } - private SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelope signedExecutionPayload, final SafeFuture> dataColumnSidecarsFuture, diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java index 91a5243571d..9edf356d6fc 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java @@ -31,7 +31,6 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.blobs.DataColumnSidecar; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.logic.common.statetransition.results.ExecutionPayloadImportResult; import tech.pegasys.teku.spec.util.DataStructureUtil; @@ -65,8 +64,6 @@ class ExecutionPayloadPublisherGloasTest { final SignedExecutionPayloadEnvelope signedExecutionPayload = dataStructureUtil.randomSignedExecutionPayloadEnvelope(42); - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload = - signedExecutionPayload.blind(spec); final List dataColumnSidecars = List.of(dataStructureUtil.randomDataColumnSidecar()); @@ -100,39 +97,6 @@ public void publishSignedExecutionPayload_shouldValidateAndPublish() { .publishDataColumnSidecars(dataColumnSidecars, RemoteOrigin.LOCAL_PROPOSAL); } - @Test - public void publishSignedBlindedExecutionPayload_shouldReconstructFromCacheAndPublish() { - when(executionPayloadFactory.unblindSignedExecutionPayload(signedBlindedExecutionPayload)) - .thenReturn(SafeFuture.completedFuture(signedExecutionPayload)); - - SafeFutureAssert.assertThatSafeFuture( - executionPayloadPublisher.publishSignedExecutionPayload( - signedBlindedExecutionPayload, Optional.empty())) - .isCompletedWithValue( - PublishSignedExecutionPayloadResult.success( - signedBlindedExecutionPayload.getBeaconBlockRoot())); - - verify(executionPayloadGossipChannel).publishExecutionPayload(signedExecutionPayload); - verify(dataColumnSidecarGossipChannel) - .publishDataColumnSidecars(dataColumnSidecars, RemoteOrigin.LOCAL_PROPOSAL); - } - - @Test - public void publishSignedBlindedExecutionPayload_shouldRejectWhenNotCached() { - when(executionPayloadFactory.unblindSignedExecutionPayload(signedBlindedExecutionPayload)) - .thenReturn(SafeFuture.failedFuture(new IllegalStateException("not cached"))); - - SafeFutureAssert.assertThatSafeFuture( - executionPayloadPublisher.publishSignedExecutionPayload( - signedBlindedExecutionPayload, Optional.empty())) - .isCompletedWithValue( - PublishSignedExecutionPayloadResult.rejected( - signedBlindedExecutionPayload.getBeaconBlockRoot(), - "No cached execution payload envelope found for blinded envelope")); - - verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); - } - @Test public void publishSignedExecutionPayload_shouldReturnRejectedResultIfBroadcastValidationFails() { when(executionPayloadManager.validateAndImportExecutionPayloadForBroadcast( diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_execution_payload_envelopes.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_execution_payload_envelopes.json index c85cb0b052f..c160731be93 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_execution_payload_envelopes.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_beacon_execution_payload_envelopes.json @@ -1,93 +1,127 @@ { - "post" : { - "tags" : [ "Beacon", "Validator Required Api" ], - "operationId" : "publishExecutionPayloadEnvelope", - "summary" : "Publish signed execution payload envelope", - "description" : "Instructs the beacon node to broadcast a signed execution payload envelope to the network. The request body is selected by the Eth-Execution-Payload-Blinded header: false submits SignedExecutionPayloadEnvelopeContents and true submits SignedBlindedExecutionPayloadEnvelope.", - "parameters" : [ { - "name" : "broadcast_validation", - "in" : "query", - "schema" : { - "type" : "string", - "description" : "Level of validation that must be applied to a block before it is broadcast. Possible values:\n- **`gossip`** (default): lightweight gossip checks only\n- **`consensus`**: full consensus checks, including validation of all signatures and blocks fields _except_ for the execution payload transactions.\n- **`consensus_and_equivocation`**: the same as `consensus`, with an extra equivocation check immediately before the block is broadcast. If the block is found to be an\n equivocation it fails validation.\nIf the block fails the requested level of a validation a 400 status MUST be returned immediately and the block MUST NOT be broadcast to the network.\nIf validation succeeds, the block must still be fully verified before it is incorporated into the state and a 20x status is returned to the caller.", - "example" : "consensus_and_equivocation", - "format" : "string", - "enum" : [ "gossip", "consensus", "consensus_and_equivocation" ] - } - }, { - "name" : "Eth-Consensus-Version", - "required" : true, - "in" : "header", - "schema" : { - "type" : "string", - "enum" : [ "phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu", "gloas", "heze" ], - "description" : "The active consensus version to which the execution payload envelope being submitted belongs." - } - }, { - "name" : "Eth-Execution-Payload-Blinded", - "required" : true, - "in" : "header", - "schema" : { - "type" : "boolean", - "description" : "When false, the body is SignedExecutionPayloadEnvelopeContents. When true, the body is SignedBlindedExecutionPayloadEnvelope." + "post": { + "tags": [ + "Beacon", + "Validator Required Api" + ], + "operationId": "publishExecutionPayloadEnvelope", + "summary": "Publish signed execution payload envelope", + "description": "Instructs the beacon node to broadcast a signed execution payload envelope to the network. The request body is selected by the Eth-Blob-Data-Included header: true submits SignedExecutionPayloadEnvelopeContents (envelope plus blobs and KZG proofs, stateless flow) and false submits SignedExecutionPayloadEnvelope (stateful flow, the beacon node attaches the blobs and KZG proofs cached during block production).", + "parameters": [ + { + "name": "broadcast_validation", + "in": "query", + "schema": { + "type": "string", + "description": "Level of validation that must be applied to a block before it is broadcast. Possible values:\n- **`gossip`** (default): lightweight gossip checks only\n- **`consensus`**: full consensus checks, including validation of all signatures and blocks fields _except_ for the execution payload transactions.\n- **`consensus_and_equivocation`**: the same as `consensus`, with an extra equivocation check immediately before the block is broadcast. If the block is found to be an\n equivocation it fails validation.\nIf the block fails the requested level of a validation a 400 status MUST be returned immediately and the block MUST NOT be broadcast to the network.\nIf validation succeeds, the block must still be fully verified before it is incorporated into the state and a 20x status is returned to the caller.", + "example": "consensus_and_equivocation", + "format": "string", + "enum": [ + "gossip", + "consensus", + "consensus_and_equivocation" + ] + } + }, + { + "name": "Eth-Blob-Data-Included", + "required": true, + "in": "header", + "schema": { + "type": "boolean", + "description": "When true, the body is SignedExecutionPayloadEnvelopeContents. When false, the body is SignedExecutionPayloadEnvelope." + } + }, + { + "name": "Eth-Consensus-Version", + "required": true, + "in": "header", + "schema": { + "type": "string", + "enum": [ + "phase0", + "altair", + "bellatrix", + "capella", + "deneb", + "electra", + "fulu", + "gloas", + "heze" + ], + "description": "The active consensus version to which the execution payload envelope being submitted belongs." + } } - } ], - "requestBody" : { - "content" : { - "application/octet-stream" : { - "schema" : { - "type" : "string", - "format" : "binary" + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" } }, - "application/json" : { - "schema" : { - "title" : "SignedExecutionPayloadEnvelopeRequest", - "type" : "object", - "oneOf" : [ { - "$ref" : "#/components/schemas/SignedExecutionPayloadEnvelopeContents" - }, { - "$ref" : "#/components/schemas/SignedBlindedExecutionPayloadEnvelope" - } ] + "application/json": { + "schema": { + "title": "SignedExecutionPayloadEnvelopeRequest", + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/SignedExecutionPayloadEnvelopeContents" + }, + { + "$ref": "#/components/schemas/SignedExecutionPayloadEnvelope" + } + ] } } } }, - "responses" : { - "415" : { - "description" : "Unsupported media type", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "responses": { + "415": { + "description": "Unsupported media type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } }, - "200" : { - "description" : "The envelope was validated successfully and has been broadcast. It has also been integrated into the beacon node's database.", - "content" : { } + "200": { + "description": "The envelope was validated successfully and has been broadcast. It has also been integrated into the beacon node's database.", + "content": {} + }, + "202": { + "description": "The envelope could not be integrated into the beacon node's database as it failed validation, but was successfully broadcast.", + "content": {} }, - "202" : { - "description" : "The envelope could not be integrated into the beacon node's database as it failed validation, but was successfully broadcast.", - "content" : { } + "400": { + "description": "The signed envelope object is invalid, broadcast validation failed, or an envelope without blob data was submitted but the beacon node has no cached blobs and KZG proofs to attach.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" + } + } + } }, - "400" : { - "description" : "The signed envelope object is invalid, broadcast validation failed, or a blinded envelope could not be reconstructed from cache.", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } }, - "500" : { - "description" : "Internal server error", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "503": { + "description": "Beacon node is currently syncing and not serving requests.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json index b8aceb7fe48..fe82d8a881d 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json @@ -1,163 +1,183 @@ { - "get" : { - "tags" : [ "Validator", "Validator Required Api" ], - "operationId" : "produceBlockV4", - "summary" : "Produce a new block, without signature.", - "description" : "Requests a beacon node to produce a valid block, which can then be signed by a validator.\n\nPost-Gloas, proposers submit execution payload bids rather than full execution payloads,\nso there is no longer a concept of blinded or unblinded blocks. Builders release the\npayload later. This endpoint is specific to the post-Gloas forks and is not backwards compatible\nwith previous forks.\n\nWhen self-building (local execution payload), the response will include the full block contents\nincluding the beacon block, execution payload envelope, blobs, and KZG proofs.\nWhen using an external builder bid, only the `BeaconBlock` is returned as the beacon node\ndoes not have access to the builder's execution payload.\n\nThe `Eth-Execution-Payload-Included` header and `execution_payload_included` response field\nindicate which response type was returned.\n", - "parameters" : [ { - "name" : "slot", - "required" : true, - "in" : "path", - "schema" : { - "type" : "string", - "description" : "The slot for which the block should be proposed.", - "example" : "1", - "format" : "uint64" - } - }, { - "name" : "randao_reveal", - "required" : true, - "in" : "query", - "schema" : { - "type" : "string", - "description" : "`BLSSignature Hex` BLS12-381 signature for the current epoch.", - "example" : "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", - "format" : "byte" - } - }, { - "name" : "graffiti", - "in" : "query", - "schema" : { - "type" : "string", - "description" : "`Bytes32 Hex` Graffiti.", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - } - }, { - "name" : "builder_boost_factor", - "in" : "query", - "schema" : { - "type" : "string", - "description" : "Percentage multiplier to apply to the builder's payload value when choosing between a\nbuilder payload header and payload from the paired execution node. This parameter is only\nrelevant if the beacon node is connected to a builder, deems it safe to produce a builder\npayload, and receives valid responses from both the builder endpoint _and_ the paired\nexecution node. When these preconditions are met, the server MUST act as follows:\n\n* if `exec_node_payload_value >= builder_boost_factor * (builder_payload_value // 100)`,\n then return a full (unblinded) block containing the execution node payload.\n* otherwise, return a blinded block containing the builder payload header.\n\nServers must support the following values of the boost factor which encode common\npreferences:\n\n* `builder_boost_factor=0`: prefer the execution node payload unless an error makes it\n unviable.\n* `builder_boost_factor=100`: default profit maximization mode; choose whichever\n payload pays more.\n* `builder_boost_factor=2**64 - 1`: prefer the builder payload unless an error or\n beacon node health check makes it unviable.\n\nServers should use saturating arithmetic or another technique to ensure that large values of\nthe `builder_boost_factor` do not trigger overflows or errors. If this parameter is\nprovided and the beacon node is not configured with a builder then the beacon node MUST\nrespond with a full block, which the caller can choose to reject if it wishes. If this\nparameter is **not** provided then it should be treated as having the default value of 100.\nIf the value is provided but out of range for a 64-bit unsigned integer, then an error\nresponse with status code 400 MUST be returned.", - "example" : "1", - "format" : "uint64" - } - }, { - "name" : "skip_randao_verification", - "allowEmptyValue" : true, - "in" : "query", - "schema" : { - "type" : "string", - "description" : "Skip verification of the `randao_reveal` value. Ignored in the Teku implementation.", - "minLength" : 0, - "maxLength" : 0 - } - }, { - "name" : "include_payload", - "allowEmptyValue" : true, - "in" : "query", - "schema" : { - "type" : "boolean", - "description" : "Controls whether the execution payload envelope and blobs are included in the response\nwhen self-building (using local execution payload).\n\nWhen `true` (default), the response includes the full block contents: beacon block,\nexecution payload envelope, blobs, and KZG proofs. This enables stateless operation\nwhere the validator client can use multiple beacon nodes (multi-BN setups, distributed validators, failover).\n\nWhen `false`, only the beacon block is returned and the beacon node caches the execution\npayload envelope and blobs internally. The validator client must then fetch them separately\nvia `GET /eth/v1/validator/execution_payload_envelope/{slot}`. This saves\nbandwidth but requires the validator client to publish via the same beacon node that\nproduced the block (stateful operation).\n\nThis parameter only affects self-building scenarios. When using an external builder's bid,\nonly the beacon block is returned regardless of this parameter (the beacon node does not\nhave access to the builder's execution payload)." + "get": { + "tags": [ + "Validator", + "Validator Required Api" + ], + "operationId": "produceBlockV4", + "summary": "Produce a new block, without signature.", + "description": "Requests a beacon node to produce a valid block, which can then be signed by a validator.\n\nPost-Gloas, proposers submit execution payload bids rather than full execution payloads,\nso there is no longer a concept of blinded or unblinded blocks. Builders release the\npayload later. This endpoint is specific to the post-Gloas forks and is not backwards compatible\nwith previous forks.\n\nWhen self-building (local execution payload), the response includes the full block contents\n(beacon block, execution payload envelope, blobs, and KZG proofs) if `include_payload` is\nset to `true`, otherwise only the `BeaconBlock` is returned.\nWhen using an external builder bid, only the `BeaconBlock` is returned as the beacon node\ndoes not have access to the builder's execution payload.\n\nThe `Eth-Execution-Payload-Included` header and `execution_payload_included` response field\nindicate which response type was returned.\n", + "parameters": [ + { + "name": "slot", + "required": true, + "in": "path", + "schema": { + "type": "string", + "description": "The slot for which the block should be proposed.", + "example": "1", + "format": "uint64" + } + }, + { + "name": "randao_reveal", + "required": true, + "in": "query", + "schema": { + "type": "string", + "description": "`BLSSignature Hex` BLS12-381 signature for the current epoch.", + "example": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "format": "byte" + } + }, + { + "name": "include_payload", + "required": true, + "in": "query", + "schema": { + "type": "boolean", + "description": "Controls whether the execution payload envelope and blobs are included in the response\nwhen self-building (using local execution payload).\n\nWhen `true`, the response includes the full block contents: beacon block,\nexecution payload envelope, blobs, and KZG proofs. This enables stateless operation\nwhere the validator client can use multiple beacon nodes (multi-BN setups, distributed validators, failover).\n\nWhen `false`, only the beacon block is returned and the beacon node caches the execution\npayload envelope and blobs internally. The validator client must then fetch them separately\nvia `GET /eth/v1/validator/execution_payload_envelope/{slot}`. This saves\nbandwidth but requires the validator client to publish via the same beacon node that\nproduced the block (stateful operation).\n\nThis parameter only affects self-building scenarios. When using an external builder's bid,\nonly the beacon block is returned regardless of this parameter (the beacon node does not\nhave access to the builder's execution payload)." + } + }, + { + "name": "graffiti", + "in": "query", + "schema": { + "type": "string", + "description": "`Bytes32 Hex` Graffiti.", + "example": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format": "byte" + } + }, + { + "name": "builder_boost_factor", + "in": "query", + "schema": { + "type": "string", + "description": "Percentage multiplier to apply to the builder's payload value when choosing between a\nbuilder payload header and payload from the paired execution node. This parameter is only\nrelevant if the beacon node is connected to a builder, deems it safe to produce a builder\npayload, and receives valid responses from both the builder endpoint _and_ the paired\nexecution node. When these preconditions are met, the server MUST act as follows:\n\n* if `exec_node_payload_value >= builder_boost_factor * (builder_payload_value // 100)`,\n then return a full (unblinded) block containing the execution node payload.\n* otherwise, return a blinded block containing the builder payload header.\n\nServers must support the following values of the boost factor which encode common\npreferences:\n\n* `builder_boost_factor=0`: prefer the execution node payload unless an error makes it\n unviable.\n* `builder_boost_factor=100`: default profit maximization mode; choose whichever\n payload pays more.\n* `builder_boost_factor=2**64 - 1`: prefer the builder payload unless an error or\n beacon node health check makes it unviable.\n\nServers should use saturating arithmetic or another technique to ensure that large values of\nthe `builder_boost_factor` do not trigger overflows or errors. If this parameter is\nprovided and the beacon node is not configured with a builder then the beacon node MUST\nrespond with a full block, which the caller can choose to reject if it wishes. If this\nparameter is **not** provided then it should be treated as having the default value of 100.\nIf the value is provided but out of range for a 64-bit unsigned integer, then an error\nresponse with status code 400 MUST be returned.", + "example": "1", + "format": "uint64" + } + }, + { + "name": "skip_randao_verification", + "allowEmptyValue": true, + "in": "query", + "schema": { + "type": "string", + "description": "Skip verification of the `randao_reveal` value. Ignored in the Teku implementation.", + "minLength": 0, + "maxLength": 0 + } } - } ], - "responses" : { - "200" : { - "description" : "Request successful", - "headers" : { - "Eth-Consensus-Version" : { - "description" : "Required in response so client can deserialize returned json or ssz data more effectively.", - "required" : true, - "schema" : { - "type" : "string", - "enum" : [ "phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu", "gloas", "heze" ], - "example" : "phase0" + ], + "responses": { + "200": { + "description": "Request successful", + "headers": { + "Eth-Consensus-Version": { + "description": "Required in response so client can deserialize returned json or ssz data more effectively.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "phase0", + "altair", + "bellatrix", + "capella", + "deneb", + "electra", + "fulu", + "gloas", + "heze" + ], + "example": "phase0" } }, - "Eth-Consensus-Block-Value" : { - "description" : "Consensus rewards for this block in Wei paid to the proposer. The rewards value is the sum of values of the proposer rewards from attestations, sync committees and slashings included in the proposal. Required in response so client can determine relative value of consensus blocks.", - "required" : true, - "schema" : { - "type" : "string", - "example" : "1" + "Eth-Consensus-Block-Value": { + "description": "Consensus rewards for this block in Wei paid to the proposer. The rewards value is the sum of values of the proposer rewards from attestations, sync committees and slashings included in the proposal. Required in response so client can determine relative value of consensus blocks.", + "required": true, + "schema": { + "type": "string", + "example": "1" } }, - "Eth-Execution-Payload-Included" : { - "description" : "Indicates whether the execution payload envelope is included in the response.\nWhen `true`, the `data` field contains the full\nexecution payload envelope, blobs, and KZG proofs. When `false`, the `data`\nfield contains only a `BeaconBlock`.", - "required" : false, - "schema" : { - "type" : "boolean" + "Eth-Execution-Payload-Included": { + "description": "Indicates whether the execution payload envelope is included in the response.\nWhen `true`, the `data` field contains the full\nexecution payload envelope, blobs, and KZG proofs. When `false`, the `data`\nfield contains only a `BeaconBlock`.", + "required": false, + "schema": { + "type": "boolean" } } }, - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/ProduceBlockV4Response" + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProduceBlockV4Response" } }, - "application/octet-stream" : { - "schema" : { - "type" : "string", - "format" : "binary" + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" } } } }, - "503" : { - "description" : "Service unavailable", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } }, - "204" : { - "description" : "Data is unavailable because the chain has not yet reached genesis", - "content" : { } + "204": { + "description": "Data is unavailable because the chain has not yet reached genesis", + "content": {} }, - "406" : { - "description" : "Not acceptable", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "406": { + "description": "Not acceptable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } }, - "501" : { - "description" : "Not implemented", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "501": { + "description": "Not implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } }, - "400" : { - "description" : "The request could not be processed, check the response for more information.", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "400": { + "description": "The request could not be processed, check the response for more information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } }, - "500" : { - "description" : "Internal server error", - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/HttpErrorResponse" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" } } } } } } -} \ No newline at end of file +} diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BlindedExecutionPayloadEnvelope.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BlindedExecutionPayloadEnvelope.json deleted file mode 100644 index 6e3ea0782b4..00000000000 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/BlindedExecutionPayloadEnvelope.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "title" : "BlindedExecutionPayloadEnvelope", - "type" : "object", - "required" : [ "payload_header", "execution_requests", "builder_index", "beacon_block_root", "parent_beacon_block_root" ], - "properties" : { - "payload_header" : { - "$ref" : "#/components/schemas/ExecutionPayloadHeaderGloas" - }, - "execution_requests" : { - "$ref" : "#/components/schemas/ExecutionRequestsGloas" - }, - "builder_index" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "beacon_block_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "parent_beacon_block_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - } - } -} diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/ExecutionPayloadHeaderGloas.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/ExecutionPayloadHeaderGloas.json deleted file mode 100644 index f8417b50592..00000000000 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/ExecutionPayloadHeaderGloas.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "title" : "ExecutionPayloadHeaderGloas", - "type" : "object", - "required" : [ "parent_hash", "fee_recipient", "state_root", "receipts_root", "logs_bloom", "prev_randao", "block_number", "gas_limit", "gas_used", "timestamp", "extra_data", "base_fee_per_gas", "block_hash", "transactions_root", "withdrawals_root", "blob_gas_used", "excess_blob_gas", "block_access_list_root", "slot_number" ], - "properties" : { - "parent_hash" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "fee_recipient" : { - "type" : "string", - "pattern" : "^0x[a-fA-F0-9]{2,}$", - "description" : "SSZ hexadecimal", - "format" : "bytes" - }, - "state_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "receipts_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "logs_bloom" : { - "type" : "string", - "pattern" : "^0x[a-fA-F0-9]{2,}$", - "description" : "SSZ hexadecimal", - "format" : "bytes" - }, - "prev_randao" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "block_number" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "gas_limit" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "gas_used" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "timestamp" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "extra_data" : { - "type" : "string", - "pattern" : "^0x[a-fA-F0-9]{2,}$", - "description" : "SSZ encoded byte list", - "format" : "bytes" - }, - "base_fee_per_gas" : { - "type" : "string", - "description" : "unsigned 256 bit integer", - "example" : "1", - "format" : "uint256" - }, - "block_hash" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "transactions_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "withdrawals_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "blob_gas_used" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "excess_blob_gas" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - }, - "block_access_list_root" : { - "type" : "string", - "description" : "Bytes32 hexadecimal", - "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - "format" : "byte" - }, - "slot_number" : { - "type" : "string", - "description" : "unsigned 64 bit integer", - "example" : "1", - "format" : "uint64" - } - } -} diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SignedBlindedExecutionPayloadEnvelope.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SignedBlindedExecutionPayloadEnvelope.json deleted file mode 100644 index 7483bf0a786..00000000000 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/SignedBlindedExecutionPayloadEnvelope.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "title" : "SignedBlindedExecutionPayloadEnvelope", - "type" : "object", - "required" : [ "message", "signature" ], - "properties" : { - "message" : { - "$ref" : "#/components/schemas/BlindedExecutionPayloadEnvelope" - }, - "signature" : { - "type" : "string", - "pattern" : "^0x[a-fA-F0-9]{2,}$", - "description" : "SSZ hexadecimal", - "format" : "bytes" - } - } -} diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java index c467546f0e8..7ab6e02bab7 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/BeaconRestApiTypes.java @@ -26,8 +26,8 @@ import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EPOCH; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EPOCH_QUERY_DESCRIPTION; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.GRAFFITI; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_BLOB_DATA_INCLUDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_EXECUTION_PAYLOAD_BLINDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.INCLUDE_PAYLOAD; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.INCLUDE_PAYLOAD_PARAM_DESCRIPTION; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.INDEX; @@ -283,8 +283,8 @@ public class BeaconRestApiTypes { public static final ParameterMetadata ETH_CONSENSUS_VERSION_TYPE = new ParameterMetadata<>(HEADER_CONSENSUS_VERSION, MILESTONE_TYPE); - public static final ParameterMetadata ETH_EXECUTION_PAYLOAD_BLINDED_TYPE = - new ParameterMetadata<>(HEADER_EXECUTION_PAYLOAD_BLINDED, BOOLEAN_TYPE); + public static final ParameterMetadata ETH_BLOB_DATA_INCLUDED_TYPE = + new ParameterMetadata<>(HEADER_BLOB_DATA_INCLUDED, BOOLEAN_TYPE); public static DeserializableTypeDefinition electraAttestationTypeDef( final SchemaDefinitionCache schemaDefinitionCache) { diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelope.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelope.java index 5c6df0b1173..dacad6387fd 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelope.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelope.java @@ -13,16 +13,16 @@ package tech.pegasys.teku.beaconrestapi.handlers.v1.beacon; +import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_BLOB_DATA_INCLUDED_TYPE; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE; -import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_EXECUTION_PAYLOAD_BLINDED_TYPE; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_BROADCAST_VALIDATION; import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelector; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_UNSUPPORTED_MEDIA_TYPE; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_BLOB_DATA_INCLUDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_EXECUTION_PAYLOAD_BLINDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR_REQUIRED; import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.HTTP_ERROR_RESPONSE_TYPE; @@ -45,7 +45,7 @@ import tech.pegasys.teku.infrastructure.restapi.openapi.request.HeaderBasedOctetStreamRequestContentTypeDefinition; import tech.pegasys.teku.infrastructure.ssz.SszData; import tech.pegasys.teku.spec.SpecMilestone; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel; import tech.pegasys.teku.spec.schemas.SchemaDefinitionCache; @@ -71,10 +71,10 @@ private static EndpointMetadata createEndpointMetadata(final SchemaDefinitionCac schemaDefinitionsGloas .getSignedExecutionPayloadEnvelopeContentsSchema() .getJsonTypeDefinition(); - final DeserializableTypeDefinition - signedBlindedExecutionPayloadEnvelopeType = + final DeserializableTypeDefinition + signedExecutionPayloadEnvelopeType = schemaDefinitionsGloas - .getSignedBlindedExecutionPayloadEnvelopeSchema() + .getSignedExecutionPayloadEnvelopeSchema() .getJsonTypeDefinition(); final SerializableOneOfTypeDefinition requestType = new SerializableOneOfTypeDefinitionBuilder() @@ -83,8 +83,8 @@ private static EndpointMetadata createEndpointMetadata(final SchemaDefinitionCac value -> value instanceof SignedExecutionPayloadEnvelopeContents, signedExecutionPayloadEnvelopeContentsType) .withType( - value -> value instanceof SignedBlindedExecutionPayloadEnvelope, - signedBlindedExecutionPayloadEnvelopeType) + value -> value instanceof SignedExecutionPayloadEnvelope, + signedExecutionPayloadEnvelopeType) .build(); return EndpointMetadata.post(ROUTE) @@ -93,22 +93,24 @@ private static EndpointMetadata createEndpointMetadata(final SchemaDefinitionCac .description( """ Instructs the beacon node to broadcast a signed execution payload envelope to the network. \ - The request body is selected by the Eth-Execution-Payload-Blinded header: false submits \ - SignedExecutionPayloadEnvelopeContents and true submits SignedBlindedExecutionPayloadEnvelope.""") + The request body is selected by the Eth-Blob-Data-Included header: true submits \ + SignedExecutionPayloadEnvelopeContents (envelope plus blobs and KZG proofs, stateless flow) \ + and false submits SignedExecutionPayloadEnvelope (stateful flow, the beacon node attaches \ + the blobs and KZG proofs cached during block production).""") .tags(TAG_BEACON, TAG_VALIDATOR_REQUIRED) .queryParam(PARAMETER_BROADCAST_VALIDATION) .headerRequired( ETH_CONSENSUS_VERSION_TYPE.withDescription( "The active consensus version to which the execution payload envelope being submitted belongs.")) .headerRequired( - ETH_EXECUTION_PAYLOAD_BLINDED_TYPE.withDescription( - "When false, the body is SignedExecutionPayloadEnvelopeContents. When true, the body is SignedBlindedExecutionPayloadEnvelope.")) + ETH_BLOB_DATA_INCLUDED_TYPE.withDescription( + "When true, the body is SignedExecutionPayloadEnvelopeContents. When false, the body is SignedExecutionPayloadEnvelope.")) .requestBodyType( requestType, context -> - isExecutionPayloadBlinded(context.getHeaders()) - ? signedBlindedExecutionPayloadEnvelopeType - : signedExecutionPayloadEnvelopeContentsType, + isBlobDataIncluded(context.getHeaders()) + ? signedExecutionPayloadEnvelopeContentsType + : signedExecutionPayloadEnvelopeType, HeaderBasedOctetStreamRequestContentTypeDefinition.parseBytes( (bytes, headers) -> deserializeRequestBody(bytes, headers, schemaCache))) .response( @@ -120,11 +122,13 @@ private static EndpointMetadata createEndpointMetadata(final SchemaDefinitionCac .response( SC_BAD_REQUEST, """ - The signed envelope object is invalid, broadcast validation failed, or a blinded \ - envelope could not be reconstructed from cache.""", + The signed envelope object is invalid, broadcast validation failed, or an envelope \ + without blob data was submitted but the beacon node has no cached blobs and KZG proofs \ + to attach.""", HTTP_ERROR_RESPONSE_TYPE) .response(SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type", HTTP_ERROR_RESPONSE_TYPE) .withInternalErrorResponse() + .withServiceUnavailableResponse() .build(); } @@ -156,10 +160,9 @@ private SafeFuture publishExecutionPayloadE return validatorDataProvider.publishSignedExecutionPayload( signedExecutionPayloadEnvelopeContents, broadcastValidationLevel); } - if (requestBody - instanceof SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayloadEnvelope) { + if (requestBody instanceof SignedExecutionPayloadEnvelope signedExecutionPayloadEnvelope) { return validatorDataProvider.publishSignedExecutionPayload( - signedBlindedExecutionPayloadEnvelope, broadcastValidationLevel); + signedExecutionPayloadEnvelope, broadcastValidationLevel); } throw new BadRequestException("Invalid signed execution payload envelope request body"); } @@ -173,27 +176,24 @@ private static SszData deserializeRequestBody( schemaCache, Optional.ofNullable(headers.get(HEADER_CONSENSUS_VERSION)), (__, schemaDefinitions) -> SchemaDefinitionsGloas.required(schemaDefinitions)); - if (isExecutionPayloadBlinded(headers)) { + if (isBlobDataIncluded(headers)) { return schemaDefinitionsGloas - .getSignedBlindedExecutionPayloadEnvelopeSchema() + .getSignedExecutionPayloadEnvelopeContentsSchema() .sszDeserialize(bytes); } - return schemaDefinitionsGloas - .getSignedExecutionPayloadEnvelopeContentsSchema() - .sszDeserialize(bytes); + return schemaDefinitionsGloas.getSignedExecutionPayloadEnvelopeSchema().sszDeserialize(bytes); } - private static boolean isExecutionPayloadBlinded(final Map headers) { - final String headerValue = headers.get(HEADER_EXECUTION_PAYLOAD_BLINDED); + private static boolean isBlobDataIncluded(final Map headers) { + final String headerValue = headers.get(HEADER_BLOB_DATA_INCLUDED); if (headerValue == null) { throw new BadRequestException( - String.format( - "Missing required header value for (%s)", HEADER_EXECUTION_PAYLOAD_BLINDED)); + String.format("Missing required header value for (%s)", HEADER_BLOB_DATA_INCLUDED)); } if (!headerValue.equalsIgnoreCase("true") && !headerValue.equalsIgnoreCase("false")) { throw new BadRequestException( String.format( - "Invalid value for (%s) header: %s", HEADER_EXECUTION_PAYLOAD_BLINDED, headerValue)); + "Invalid value for (%s) header: %s", HEADER_BLOB_DATA_INCLUDED, headerValue)); } return Boolean.parseBoolean(headerValue); } diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java index ab29c636332..0d51ff4af60 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java @@ -90,8 +90,9 @@ private static EndpointMetadata getEndpointMetaData( payload later. This endpoint is specific to the post-Gloas forks and is not backwards compatible with previous forks. - When self-building (local execution payload), the response will include the full block contents - including the beacon block, execution payload envelope, blobs, and KZG proofs. + When self-building (local execution payload), the response includes the full block contents + (beacon block, execution payload envelope, blobs, and KZG proofs) if `include_payload` is + set to `true`, otherwise only the `BeaconBlock` is returned. When using an external builder bid, only the `BeaconBlock` is returned as the beacon node does not have access to the builder's execution payload. @@ -103,7 +104,7 @@ private static EndpointMetadata getEndpointMetaData( .queryParamRequired(RANDAO_PARAMETER) .queryParam(GRAFFITI_PARAMETER) .queryParamAllowsEmpty(SKIP_RANDAO_VERIFICATION_PARAMETER) - .queryParamAllowsEmpty(INCLUDE_PAYLOAD_PARAMETER) + .queryParamRequired(INCLUDE_PAYLOAD_PARAMETER) .queryParam(BUILDER_BOOST_FACTOR_PARAMETER) .response( SC_OK, @@ -123,8 +124,7 @@ public void handleRequest(final RestApiRequest request) throws JsonProcessingExc request.getPathParameter(SLOT_PARAMETER.withDescription(SLOT_PATH_DESCRIPTION)); final BLSSignature randao = request.getQueryParameter(RANDAO_PARAMETER); final Optional graffiti = request.getOptionalQueryParameter(GRAFFITI_PARAMETER); - final Optional includePayload = - request.getOptionalQueryParameter(INCLUDE_PAYLOAD_PARAMETER); + final boolean includePayload = request.getQueryParameter(INCLUDE_PAYLOAD_PARAMETER); final Optional requestedBuilderBoostFactor = request.getOptionalQueryParameter(BUILDER_BOOST_FACTOR_PARAMETER); diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelopeTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelopeTest.java index c43cee85ec0..83497dd11f4 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelopeTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/beacon/PostExecutionPayloadEnvelopeTest.java @@ -20,8 +20,8 @@ import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_BLOB_DATA_INCLUDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_EXECUTION_PAYLOAD_BLINDED; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getRequestBodyFromMetadata; import java.util.List; @@ -33,7 +33,6 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.TestSpecFactory; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.validator.api.PublishSignedExecutionPayloadResult; @@ -53,9 +52,9 @@ void shouldReturnOkIfSuccess() throws Exception { final PublishSignedExecutionPayloadResult successResult = PublishSignedExecutionPayloadResult.success(envelope.getBeaconBlockRoot()); - request.setRequestBody(envelope.blind(spec)); + request.setRequestBody(envelope); when(validatorDataProvider.publishSignedExecutionPayload( - any(SignedBlindedExecutionPayloadEnvelope.class), any())) + any(SignedExecutionPayloadEnvelope.class), any())) .thenReturn(SafeFuture.completedFuture(successResult)); handler.handleRequest(request); @@ -72,9 +71,9 @@ void shouldReturnAcceptedIfPublishedButRejected() throws Exception { PublishSignedExecutionPayloadResult.notImported( envelope.getBeaconBlockRoot(), "Invalid payload"); - request.setRequestBody(envelope.blind(spec)); + request.setRequestBody(envelope); when(validatorDataProvider.publishSignedExecutionPayload( - any(SignedBlindedExecutionPayloadEnvelope.class), any())) + any(SignedExecutionPayloadEnvelope.class), any())) .thenReturn(SafeFuture.completedFuture(failResult)); handler.handleRequest(request); @@ -90,9 +89,9 @@ void shouldReturnBadRequestIfRejectedAndNotPublished() throws Exception { final PublishSignedExecutionPayloadResult failResult = PublishSignedExecutionPayloadResult.rejected(envelope.getBeaconBlockRoot(), "oopsy"); - request.setRequestBody(envelope.blind(spec)); + request.setRequestBody(envelope); when(validatorDataProvider.publishSignedExecutionPayload( - any(SignedBlindedExecutionPayloadEnvelope.class), any())) + any(SignedExecutionPayloadEnvelope.class), any())) .thenReturn(SafeFuture.completedFuture(failResult)); handler.handleRequest(request); @@ -107,7 +106,7 @@ void shouldReturnBadRequestIfInvalidBroadcastValidation() throws Exception { final SignedExecutionPayloadEnvelope envelope = dataStructureUtil.randomSignedExecutionPayloadEnvelope(1); - request.setRequestBody(envelope.blind(spec)); + request.setRequestBody(envelope); request.setOptionalQueryParameter("broadcast_validation", "invalid_value"); handler.handleRequest(request); @@ -119,7 +118,7 @@ void shouldReturnBadRequestIfInvalidBroadcastValidation() throws Exception { } @Test - void shouldReturnOkIfSuccessWithUnblindedContents() throws Exception { + void shouldReturnOkIfSuccessWithContents() throws Exception { final SignedExecutionPayloadEnvelope envelope = dataStructureUtil.randomSignedExecutionPayloadEnvelope(1); final SignedExecutionPayloadEnvelopeContents contents = contentsFor(envelope); @@ -138,7 +137,7 @@ void shouldReturnOkIfSuccessWithUnblindedContents() throws Exception { } @Test - void shouldReturnAcceptedIfPublishedButRejectedWithUnblindedContents() throws Exception { + void shouldReturnAcceptedIfPublishedButRejectedWithContents() throws Exception { final SignedExecutionPayloadEnvelope envelope = dataStructureUtil.randomSignedExecutionPayloadEnvelope(1); final SignedExecutionPayloadEnvelopeContents contents = contentsFor(envelope); @@ -158,12 +157,12 @@ void shouldReturnAcceptedIfPublishedButRejectedWithUnblindedContents() throws Ex } @Test - void shouldRejectRequestWhenBlindedHeaderMissing() { + void shouldRejectRequestWhenBlobDataIncludedHeaderMissing() { assertThatThrownBy( () -> getRequestBodyFromMetadata(handler, Map.of(HEADER_CONSENSUS_VERSION, "gloas"), "")) .isInstanceOf(BadRequestException.class) - .hasMessageContaining(HEADER_EXECUTION_PAYLOAD_BLINDED); + .hasMessageContaining(HEADER_BLOB_DATA_INCLUDED); } private SignedExecutionPayloadEnvelopeContents contentsFor( diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java b/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java index 247bf5e2ae3..243aad0cf48 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java @@ -35,7 +35,6 @@ import tech.pegasys.teku.spec.datastructures.builder.SignedValidatorRegistration; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -270,13 +269,6 @@ public SafeFuture publishSignedExecutionPay envelopeContents, broadcastValidationLevel); } - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope blindedEnvelope, - final Optional broadcastValidationLevel) { - return validatorApiChannel.publishSignedExecutionPayload( - blindedEnvelope, broadcastValidationLevel); - } - public SafeFuture registerValidators( final SszList validatorRegistrations) { return validatorApiChannel diff --git a/infrastructure/http/src/main/java/tech/pegasys/teku/infrastructure/http/RestApiConstants.java b/infrastructure/http/src/main/java/tech/pegasys/teku/infrastructure/http/RestApiConstants.java index 8a9b7c4a392..27a758e3a71 100644 --- a/infrastructure/http/src/main/java/tech/pegasys/teku/infrastructure/http/RestApiConstants.java +++ b/infrastructure/http/src/main/java/tech/pegasys/teku/infrastructure/http/RestApiConstants.java @@ -194,7 +194,7 @@ public class RestApiConstants { Controls whether the execution payload envelope and blobs are included in the response when self-building (using local execution payload). - When `true` (default), the response includes the full block contents: beacon block, + When `true`, the response includes the full block contents: beacon block, execution payload envelope, blobs, and KZG proofs. This enables stateless operation where the validator client can use multiple beacon nodes (multi-BN setups, distributed validators, failover). @@ -243,6 +243,7 @@ then return a full (unblinded) block containing the execution node payload. public static final String HEADER_CONSENSUS_VERSION = "Eth-Consensus-Version"; public static final String HEADER_EXECUTION_PAYLOAD_BLINDED = "Eth-Execution-Payload-Blinded"; + public static final String HEADER_BLOB_DATA_INCLUDED = "Eth-Blob-Data-Included"; public static final String HEADER_EXECUTION_PAYLOAD_VALUE = "Eth-Execution-Payload-Value"; public static final String HEADER_CONSENSUS_BLOCK_VALUE = "Eth-Consensus-Block-Value"; public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; diff --git a/validator/api/src/main/java/tech/pegasys/teku/validator/api/BuilderApiChannel.java b/validator/api/src/main/java/tech/pegasys/teku/validator/api/BuilderApiChannel.java index 26af678f17a..73d870bcf41 100644 --- a/validator/api/src/main/java/tech/pegasys/teku/validator/api/BuilderApiChannel.java +++ b/validator/api/src/main/java/tech/pegasys/teku/validator/api/BuilderApiChannel.java @@ -18,7 +18,6 @@ import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -52,8 +51,4 @@ SafeFuture publishSignedExecutionPayload( SafeFuture publishSignedExecutionPayload( SignedExecutionPayloadEnvelopeContents signedExecutionPayloadEnvelopeContents, Optional broadcastValidationLevel); - - SafeFuture publishSignedExecutionPayload( - SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - Optional broadcastValidationLevel); } diff --git a/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java b/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java index 10925ef20a3..ee5a28c8f8a 100644 --- a/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java +++ b/validator/api/src/main/java/tech/pegasys/teku/validator/api/ValidatorApiChannel.java @@ -42,7 +42,6 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -269,15 +268,6 @@ public SafeFuture publishSignedExecutionPay PublishSignedExecutionPayloadResult.success( signedExecutionPayloadEnvelopeContents.getBeaconBlockRoot())); } - - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return SafeFuture.completedFuture( - PublishSignedExecutionPayloadResult.success( - signedBlindedExecutionPayload.getBeaconBlockRoot())); - } }; int UNKNOWN_VALIDATOR_ID = -1; diff --git a/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java b/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java index 1b9b4b8332b..b0110fe0091 100644 --- a/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java +++ b/validator/beaconnode/src/main/java/tech/pegasys/teku/validator/beaconnode/metrics/MetricRecordingValidatorApiChannel.java @@ -51,7 +51,6 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -366,16 +365,6 @@ public SafeFuture publishSignedExecutionPay BeaconNodeRequestLabels.PUBLISH_EXECUTION_PAYLOAD_METHOD); } - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return countDataRequest( - delegate.publishSignedExecutionPayload( - signedBlindedExecutionPayload, broadcastValidationLevel), - BeaconNodeRequestLabels.PUBLISH_EXECUTION_PAYLOAD_METHOD); - } - private SafeFuture countDataRequest( final SafeFuture request, final String requestName) { return request diff --git a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequestTest.java b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequestTest.java index c78f4f62d73..85ce44e40c4 100644 --- a/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequestTest.java +++ b/validator/remote/src/integration-test/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequestTest.java @@ -16,8 +16,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_BLOB_DATA_INCLUDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_EXECUTION_PAYLOAD_BLINDED; import java.util.Locale; import java.util.Optional; @@ -62,7 +62,7 @@ public void shouldIncludeConsensusHeaderAndHandle200Ok() throws InterruptedExcep assertThat(recordedRequest.getMethod()).isEqualTo("POST"); assertThat(recordedRequest.getHeader(HEADER_CONSENSUS_VERSION)) .isEqualTo(specMilestone.name().toLowerCase(Locale.ROOT)); - assertThat(recordedRequest.getHeader(HEADER_EXECUTION_PAYLOAD_BLINDED)).isEqualTo("true"); + assertThat(recordedRequest.getHeader(HEADER_BLOB_DATA_INCLUDED)).isEqualTo("false"); } @TestTemplate diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java index f98b3cd961f..ba02cd6237b 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/FailoverValidatorApiHandler.java @@ -56,7 +56,6 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -477,31 +476,6 @@ public SafeFuture publishSignedExecutionPay BeaconNodeRequestLabels.PUBLISH_EXECUTION_PAYLOAD_METHOD); } - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - final BlockRootAndBuilderIndex blockRootAndBuilderIndex = - new BlockRootAndBuilderIndex( - signedBlindedExecutionPayload.getBeaconBlockRoot(), - signedBlindedExecutionPayload.getMessage().getBuilderIndex()); - if (executionPayloadEnvelopeCreatorCache.containsKey(blockRootAndBuilderIndex)) { - final ValidatorApiChannel executionPayloadCreatorApiChannel = - executionPayloadEnvelopeCreatorCache.remove(blockRootAndBuilderIndex); - LOG.info( - "Blinded execution payload for block root {} and builder index {} will only be sent to the beacon node which created it.", - blockRootAndBuilderIndex.blockRoot().toHexString(), - blockRootAndBuilderIndex.builderIndex()); - return executionPayloadCreatorApiChannel.publishSignedExecutionPayload( - signedBlindedExecutionPayload, broadcastValidationLevel); - } - return relayRequest( - apiChannel -> - apiChannel.publishSignedExecutionPayload( - signedBlindedExecutionPayload, broadcastValidationLevel), - BeaconNodeRequestLabels.PUBLISH_EXECUTION_PAYLOAD_METHOD); - } - private SafeFuture relayRequest( final ValidatorApiChannelRequest request, final String method) { return relayRequest(request, method, true); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java index c5f08f6ac58..6ede34471a5 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java @@ -58,7 +58,6 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -410,16 +409,6 @@ public SafeFuture publishSignedExecutionPay signedExecutionPayloadEnvelopeContents, broadcastValidationLevel)); } - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return sendRequest( - () -> - typeDefClient.publishSignedExecutionPayload( - signedBlindedExecutionPayload, broadcastValidationLevel)); - } - private SafeFuture sendRequest(final ExceptionThrowingRunnable requestExecutor) { return sendRequest( () -> { diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java index d93bcc3c30d..dc4c13aad83 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/sentry/SentryValidatorApiChannel.java @@ -41,7 +41,6 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; @@ -324,13 +323,4 @@ public SafeFuture publishSignedExecutionPay .publishSignedExecutionPayload( signedExecutionPayloadEnvelopeContents, broadcastValidationLevel); } - - @Override - public SafeFuture publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - return blockHandlerChannel - .orElse(dutiesProviderChannel) - .publishSignedExecutionPayload(signedBlindedExecutionPayload, broadcastValidationLevel); - } } diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java index 9c005d429ca..66432e394cb 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/OkHttpValidatorTypeDefClient.java @@ -39,7 +39,6 @@ import tech.pegasys.teku.spec.datastructures.blocks.SignedBlockContainer; import tech.pegasys.teku.spec.datastructures.builder.SignedValidatorRegistration; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.spec.datastructures.metadata.BlockContainerAndMetaData; @@ -318,13 +317,4 @@ public PublishSignedExecutionPayloadResult publishSignedExecutionPayload( return publishSignedExecutionPayloadRequest.submit( signedExecutionPayloadEnvelopeContents, broadcastValidationLevel); } - - public PublishSignedExecutionPayloadResult publishSignedExecutionPayload( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, - final Optional broadcastValidationLevel) { - final PublishSignedExecutionPayloadRequest publishSignedExecutionPayloadRequest = - new PublishSignedExecutionPayloadRequest(spec, getBaseEndpoint(), getOkHttpClient()); - return publishSignedExecutionPayloadRequest.submit( - signedBlindedExecutionPayload, broadcastValidationLevel); - } } diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequest.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequest.java index dcac5340a23..a16dce45071 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequest.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/typedef/handlers/PublishSignedExecutionPayloadRequest.java @@ -16,8 +16,8 @@ import static java.util.Collections.emptyMap; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_BLOB_DATA_INCLUDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION; -import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_EXECUTION_PAYLOAD_BLINDED; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.PARAM_BROADCAST_VALIDATION; import static tech.pegasys.teku.validator.remote.apiclient.ValidatorApiMethod.SEND_SIGNED_EXECUTION_PAYLOAD_ENVELOPE; @@ -31,7 +31,6 @@ import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedBlindedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelopeContents; import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel; @@ -53,25 +52,22 @@ public PublishSignedExecutionPayloadRequest( this.spec = spec; } + /** + * Stateful flow: the envelope is sent without blob data, the beacon node attaches the blobs and + * KZG proofs it cached during block production. + */ public PublishSignedExecutionPayloadResult submit( final SignedExecutionPayloadEnvelope signedExecutionPayload, final Optional broadcastValidationLevel) { - // Blind before sending: the BN unblinds from its getPayload cache; avoids a blob round-trip. - return submit(signedExecutionPayload.blind(spec), broadcastValidationLevel); - } - - public PublishSignedExecutionPayloadResult submit( - final SignedExecutionPayloadEnvelopeContents signedExecutionPayloadEnvelopeContents, - final Optional broadcastValidationLevel) { final Map queryParams = getQueryParams(broadcastValidationLevel); - final DeserializableTypeDefinition typeDefinition = - spec.atSlot(signedExecutionPayloadEnvelopeContents.getSlot()) + final DeserializableTypeDefinition typeDefinition = + spec.atSlot(signedExecutionPayload.getSlot()) .getSchemaDefinitions() .toVersionGloas() .orElseThrow() - .getSignedExecutionPayloadEnvelopeContentsSchema() + .getSignedExecutionPayloadEnvelopeSchema() .getJsonTypeDefinition(); return createResult( @@ -79,25 +75,26 @@ public PublishSignedExecutionPayloadResult submit( SEND_SIGNED_EXECUTION_PAYLOAD_ENVELOPE, emptyMap(), queryParams, - getHeaders(signedExecutionPayloadEnvelopeContents.getSlot(), false), - signedExecutionPayloadEnvelopeContents, + getHeaders(signedExecutionPayload.getSlot(), false), + signedExecutionPayload, typeDefinition, responseHandler), - signedExecutionPayloadEnvelopeContents.getBeaconBlockRoot()); + signedExecutionPayload.getBeaconBlockRoot()); } + /** Stateless flow: the envelope is sent along with its blobs and KZG proofs. */ public PublishSignedExecutionPayloadResult submit( - final SignedBlindedExecutionPayloadEnvelope signedBlindedExecutionPayload, + final SignedExecutionPayloadEnvelopeContents signedExecutionPayloadEnvelopeContents, final Optional broadcastValidationLevel) { final Map queryParams = getQueryParams(broadcastValidationLevel); - final DeserializableTypeDefinition typeDefinition = - spec.atSlot(signedBlindedExecutionPayload.getSlot()) + final DeserializableTypeDefinition typeDefinition = + spec.atSlot(signedExecutionPayloadEnvelopeContents.getSlot()) .getSchemaDefinitions() .toVersionGloas() .orElseThrow() - .getSignedBlindedExecutionPayloadEnvelopeSchema() + .getSignedExecutionPayloadEnvelopeContentsSchema() .getJsonTypeDefinition(); return createResult( @@ -105,11 +102,11 @@ public PublishSignedExecutionPayloadResult submit( SEND_SIGNED_EXECUTION_PAYLOAD_ENVELOPE, emptyMap(), queryParams, - getHeaders(signedBlindedExecutionPayload.getSlot(), true), - signedBlindedExecutionPayload, + getHeaders(signedExecutionPayloadEnvelopeContents.getSlot(), true), + signedExecutionPayloadEnvelopeContents, typeDefinition, responseHandler), - signedBlindedExecutionPayload.getBeaconBlockRoot()); + signedExecutionPayloadEnvelopeContents.getBeaconBlockRoot()); } private PublishSignedExecutionPayloadResult createResult( @@ -124,11 +121,11 @@ private PublishSignedExecutionPayloadResult createResult( () -> PublishSignedExecutionPayloadResult.notImported(beaconBlockRoot, "UNKNOWN")); } - private Map getHeaders(final UInt64 slot, final boolean blinded) { + private Map getHeaders(final UInt64 slot, final boolean blobDataIncluded) { final SpecMilestone milestone = spec.atSlot(slot).getMilestone(); return Map.of( HEADER_CONSENSUS_VERSION, milestone.name().toLowerCase(Locale.ROOT), - HEADER_EXECUTION_PAYLOAD_BLINDED, String.valueOf(blinded)); + HEADER_BLOB_DATA_INCLUDED, String.valueOf(blobDataIncluded)); } private Map getQueryParams( From 5fc22e40ea31d1452e27708d77910d4a3806ee79 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Thu, 13 Aug 2026 16:29:31 +1000 Subject: [PATCH 2/6] fix issue found by cursor Signed-off-by: Gabriel Fukushima --- .../ExecutionPayloadFactoryGloas.java | 11 ++++- .../ExecutionPayloadPublisherGloas.java | 38 +++++++++------- .../ExecutionPayloadFactoryGloasTest.java | 45 +++++++++++++++++-- .../ExecutionPayloadPublisherGloasTest.java | 14 ++++++ 4 files changed, 86 insertions(+), 22 deletions(-) diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java index beebd5ea9b3..b02eb305ec3 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java @@ -13,6 +13,8 @@ package tech.pegasys.teku.validator.coordinator; +import static com.google.common.base.Preconditions.checkState; + import java.util.List; import java.util.stream.IntStream; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -72,9 +74,16 @@ public SafeFuture createUnsignedExecutionPayload( public SafeFuture> createDataColumnSidecars( final SignedExecutionPayloadEnvelope signedExecutionPayload) { final UInt64 slot = signedExecutionPayload.getMessage().getSlot(); - return getCachedGetPayloadResponseFuture(slot) + return SafeFuture.of(() -> getCachedGetPayloadResponseFuture(slot)) .thenApply( getPayloadResponse -> { + checkState( + getPayloadResponse + .getExecutionPayload() + .hashTreeRoot() + .equals(signedExecutionPayload.getMessage().getPayload().hashTreeRoot()), + "Cached execution payload does not match signed execution payload envelope for slot %s", + slot); final BlobsBundle blobsBundle = getPayloadResponse.getBlobsBundle().orElseThrow(); return createDataColumnSidecars( signedExecutionPayload, blobsBundle.getBlobs(), blobsBundle.getProofs()); diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java index 543805d50bd..636535d2ba0 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java @@ -55,25 +55,33 @@ public ExecutionPayloadPublisherGloas( public SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelope signedExecutionPayload, final Optional broadcastValidationLevel) { - return publishSignedExecutionPayload( - signedExecutionPayload, - executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload), - broadcastValidationLevel); + return SafeFuture.>of( + () -> executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenCompose( + dataColumnSidecars -> + publishSignedExecutionPayload( + signedExecutionPayload, dataColumnSidecars, broadcastValidationLevel)); } @Override public SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelopeContents signedExecutionPayloadEnvelopeContents, final Optional broadcastValidationLevel) { - return publishSignedExecutionPayload( - signedExecutionPayloadEnvelopeContents.getSignedExecutionPayloadEnvelope(), - executionPayloadFactory.createDataColumnSidecars(signedExecutionPayloadEnvelopeContents), - broadcastValidationLevel); + return SafeFuture.>of( + () -> + executionPayloadFactory.createDataColumnSidecars( + signedExecutionPayloadEnvelopeContents)) + .thenCompose( + dataColumnSidecars -> + publishSignedExecutionPayload( + signedExecutionPayloadEnvelopeContents.getSignedExecutionPayloadEnvelope(), + dataColumnSidecars, + broadcastValidationLevel)); } private SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelope signedExecutionPayload, - final SafeFuture> dataColumnSidecarsFuture, + final List dataColumnSidecars, final Optional broadcastValidationLevel) { final Bytes32 beaconBlockRoot = signedExecutionPayload.getBeaconBlockRoot(); return executionPayloadManager @@ -94,7 +102,7 @@ private SafeFuture publishSignedExecutionPa .orElse(""))); } publishExecutionPayloadAndDataColumnSidecars( - signedExecutionPayload, dataColumnSidecarsFuture); + signedExecutionPayload, dataColumnSidecars); return validateAndImportResult .importResult() .orElseThrow(() -> new IllegalStateException("ACCEPT without import future")) @@ -109,13 +117,9 @@ private SafeFuture publishSignedExecutionPa private void publishExecutionPayloadAndDataColumnSidecars( final SignedExecutionPayloadEnvelope signedExecutionPayload, - final SafeFuture> dataColumnSidecarsFuture) { + final List dataColumnSidecars) { executionPayloadGossipChannel.publishExecutionPayload(signedExecutionPayload).finishError(LOG); - dataColumnSidecarsFuture - .thenAccept( - dataColumnSidecars -> - dataColumnSidecarGossipChannel.publishDataColumnSidecars( - dataColumnSidecars, RemoteOrigin.LOCAL_PROPOSAL)) - .finishError(LOG); + dataColumnSidecarGossipChannel.publishDataColumnSidecars( + dataColumnSidecars, RemoteOrigin.LOCAL_PROPOSAL); } } diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java index f7fa7d1f485..78990697f26 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java @@ -16,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.assertThatSafeFuture; import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.safeJoin; import java.util.Collections; @@ -81,9 +82,12 @@ public void createsDataColumnSidecars() { blobsBundle = dataStructureUtil.randomBlobsBundle(3); + final ExecutionPayloadEnvelope executionPayload = + dataStructureUtil.randomExecutionPayloadEnvelope(slot); + getPayloadResponse = new GetPayloadResponse( - dataStructureUtil.randomExecutionPayload(slot), + executionPayload.getPayload(), dataStructureUtil.randomUInt256(), blobsBundle, false, @@ -91,9 +95,6 @@ public void createsDataColumnSidecars() { setupCachingOfThePayloadResult(slot, getPayloadResponse); - final ExecutionPayloadEnvelope executionPayload = - dataStructureUtil.randomExecutionPayloadEnvelope(slot); - final SignedExecutionPayloadEnvelope signedExecutionPayload = schemaDefinitions .getSignedExecutionPayloadEnvelopeSchema() @@ -109,6 +110,42 @@ public void createsDataColumnSidecars() { .isEqualTo(executionPayload.getBeaconBlockRoot())); } + @Test + public void failsToCreateDataColumnSidecarsWhenPayloadResultIsNotCached() { + final UInt64 slot = UInt64.ONE; + final SignedExecutionPayloadEnvelope signedExecutionPayload = + dataStructureUtil.randomSignedExecutionPayloadEnvelope(slot.longValue()); + + when(executionLayerBlockProductionManager.getCachedPayloadResult(slot)) + .thenReturn(Optional.empty()); + + assertThatSafeFuture(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .isCompletedExceptionallyWith(IllegalStateException.class) + .hasMessage("ExecutionPayloadResult hasn't been cached for slot " + slot); + } + + @Test + public void failsToCreateDataColumnSidecarsWhenCachedPayloadDoesNotMatchEnvelope() { + final UInt64 slot = UInt64.ONE; + final SignedExecutionPayloadEnvelope signedExecutionPayload = + dataStructureUtil.randomSignedExecutionPayloadEnvelope(slot.longValue()); + final GetPayloadResponse mismatchedGetPayloadResponse = + new GetPayloadResponse( + dataStructureUtil.randomExecutionPayload(slot), + dataStructureUtil.randomUInt256(), + dataStructureUtil.randomBlobsBundle(1), + false, + dataStructureUtil.randomExecutionRequests(slot)); + + setupCachingOfThePayloadResult(slot, mismatchedGetPayloadResponse); + + assertThatSafeFuture(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .isCompletedExceptionallyWith(IllegalStateException.class) + .hasMessage( + "Cached execution payload does not match signed execution payload envelope for slot " + + slot); + } + private void assertExecutionPayloadCreated( final UInt64 slot, final int blobsCount, diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java index 9edf356d6fc..9e021ba75c2 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java @@ -97,6 +97,20 @@ public void publishSignedExecutionPayload_shouldValidateAndPublish() { .publishDataColumnSidecars(dataColumnSidecars, RemoteOrigin.LOCAL_PROPOSAL); } + @Test + public void publishSignedExecutionPayload_shouldFailBeforePublishingWhenSidecarsAreUnavailable() { + final IllegalStateException error = new IllegalStateException("payload cache unavailable"); + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn(SafeFuture.failedFuture(error)); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedExceptionallyWith(error); + + verifyNoInteractions( + executionPayloadManager, executionPayloadGossipChannel, dataColumnSidecarGossipChannel); + } + @Test public void publishSignedExecutionPayload_shouldReturnRejectedResultIfBroadcastValidationFails() { when(executionPayloadManager.validateAndImportExecutionPayloadForBroadcast( From fd745d5f250a50b991c5d1afd6fe0e31367bc7c8 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Fri, 14 Aug 2026 14:45:28 +1000 Subject: [PATCH 3/6] throw and log when datacolumn sidecar isn't in the cache or doesn't match with the signed execution payload envelope Signed-off-by: Gabriel Fukushima --- .../DataColumnSidecarCreationException.java | 49 ++++++++++ .../ExecutionPayloadFactoryGloas.java | 25 +++-- .../ExecutionPayloadPublisherGloas.java | 92 +++++++++++++++---- .../ExecutionPayloadFactoryGloasTest.java | 10 +- .../ExecutionPayloadPublisherGloasTest.java | 42 ++++++++- 5 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java new file mode 100644 index 00000000000..5cdac3b24e3 --- /dev/null +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc., 2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.validator.coordinator; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +/** + * Thrown when the data column sidecars for a submitted execution payload envelope cannot be built. + * Both cases are caused by the submission itself rather than by an internal failure, so the message + * is safe to return to the caller as the reason for a rejected publication. + */ +public class DataColumnSidecarCreationException extends IllegalStateException { + + private DataColumnSidecarCreationException(final String message) { + super(message); + } + + /** + * The envelope was submitted without blob data ({@code Eth-Blob-Data-Included: false}) but this + * beacon node has nothing cached to attach, typically because block production happened + * elsewhere. + */ + public static DataColumnSidecarCreationException noCachedBlobData(final UInt64 slot) { + return new DataColumnSidecarCreationException( + "No cached blobs and KZG proofs to attach to the execution payload envelope for slot " + + slot + + ". Block production likely happened on a different beacon node, resubmit with" + + " Eth-Blob-Data-Included: true"); + } + + /** The submitted envelope does not carry the execution payload this beacon node built. */ + public static DataColumnSidecarCreationException cachedPayloadMismatch(final UInt64 slot) { + return new DataColumnSidecarCreationException( + "Signed execution payload envelope for slot " + + slot + + " does not match the execution payload built by this beacon node"); + } +} diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java index b02eb305ec3..1a872111ec6 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloas.java @@ -13,8 +13,6 @@ package tech.pegasys.teku.validator.coordinator; -import static com.google.common.base.Preconditions.checkState; - import java.util.List; import java.util.stream.IntStream; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -74,17 +72,24 @@ public SafeFuture createUnsignedExecutionPayload( public SafeFuture> createDataColumnSidecars( final SignedExecutionPayloadEnvelope signedExecutionPayload) { final UInt64 slot = signedExecutionPayload.getMessage().getSlot(); - return SafeFuture.of(() -> getCachedGetPayloadResponseFuture(slot)) + return SafeFuture.of( + () -> + executionLayerBlockProductionManager + .getCachedPayloadResult(slot) + .orElseThrow(() -> DataColumnSidecarCreationException.noCachedBlobData(slot)) + .getPayloadResponseFutureFromLocalFlowRequired()) .thenApply( getPayloadResponse -> { - checkState( + if (!getPayloadResponse + .getExecutionPayload() + .hashTreeRoot() + .equals(signedExecutionPayload.getMessage().getPayload().hashTreeRoot())) { + throw DataColumnSidecarCreationException.cachedPayloadMismatch(slot); + } + final BlobsBundle blobsBundle = getPayloadResponse - .getExecutionPayload() - .hashTreeRoot() - .equals(signedExecutionPayload.getMessage().getPayload().hashTreeRoot()), - "Cached execution payload does not match signed execution payload envelope for slot %s", - slot); - final BlobsBundle blobsBundle = getPayloadResponse.getBlobsBundle().orElseThrow(); + .getBlobsBundle() + .orElseThrow(() -> DataColumnSidecarCreationException.noCachedBlobData(slot)); return createDataColumnSidecars( signedExecutionPayload, blobsBundle.getBlobs(), blobsBundle.getProofs()); }); diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java index 636535d2ba0..3132eb392dc 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java @@ -19,6 +19,7 @@ import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.exceptions.ExceptionUtil; import tech.pegasys.teku.networking.eth2.gossip.DataColumnSidecarGossipChannel; import tech.pegasys.teku.networking.eth2.gossip.ExecutionPayloadGossipChannel; import tech.pegasys.teku.spec.datastructures.blobs.DataColumnSidecar; @@ -29,6 +30,7 @@ import tech.pegasys.teku.statetransition.execution.ExecutionPayloadManager; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; import tech.pegasys.teku.validator.api.PublishSignedExecutionPayloadResult; +import tech.pegasys.teku.validator.coordinator.DataColumnSidecarCreationException; import tech.pegasys.teku.validator.coordinator.ExecutionPayloadFactory; public class ExecutionPayloadPublisherGloas implements ExecutionPayloadPublisher { @@ -55,40 +57,44 @@ public ExecutionPayloadPublisherGloas( public SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelope signedExecutionPayload, final Optional broadcastValidationLevel) { - return SafeFuture.>of( - () -> executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) - .thenCompose( - dataColumnSidecars -> - publishSignedExecutionPayload( - signedExecutionPayload, dataColumnSidecars, broadcastValidationLevel)); + return publishSignedExecutionPayload( + signedExecutionPayload, + SafeFuture.of( + () -> executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)), + broadcastValidationLevel); } @Override public SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelopeContents signedExecutionPayloadEnvelopeContents, final Optional broadcastValidationLevel) { - return SafeFuture.>of( + return publishSignedExecutionPayload( + signedExecutionPayloadEnvelopeContents.getSignedExecutionPayloadEnvelope(), + SafeFuture.of( () -> executionPayloadFactory.createDataColumnSidecars( - signedExecutionPayloadEnvelopeContents)) - .thenCompose( - dataColumnSidecars -> - publishSignedExecutionPayload( - signedExecutionPayloadEnvelopeContents.getSignedExecutionPayloadEnvelope(), - dataColumnSidecars, - broadcastValidationLevel)); + signedExecutionPayloadEnvelopeContents)), + broadcastValidationLevel); } + /** + * The data column sidecars are built concurrently with the broadcast validation, but both must + * have completed before anything is published: a failure to build the sidecars (for example when + * the blobs are not cached for a stateful submission) must prevent the execution payload from + * being broadcast. Gossiping an envelope whose columns cannot be made available is pointless, as + * no honest node adds such a payload to its store. + */ private SafeFuture publishSignedExecutionPayload( final SignedExecutionPayloadEnvelope signedExecutionPayload, - final List dataColumnSidecars, + final SafeFuture> dataColumnSidecarsFuture, final Optional broadcastValidationLevel) { final Bytes32 beaconBlockRoot = signedExecutionPayload.getBeaconBlockRoot(); return executionPayloadManager .validateAndImportExecutionPayloadForBroadcast( signedExecutionPayload, broadcastValidationLevel) - .thenCompose( - validateAndImportResult -> { + .thenComposeCombined( + recoverDataColumnSidecarCreationFailure(dataColumnSidecarsFuture, beaconBlockRoot), + (validateAndImportResult, dataColumnSidecarsResult) -> { final InternalValidationResult validationResult = validateAndImportResult.validationResult(); if (!validationResult.isAccept()) { @@ -101,8 +107,16 @@ private SafeFuture publishSignedExecutionPa .map(description -> ": " + description) .orElse(""))); } + if (dataColumnSidecarsResult.rejectionReason().isPresent()) { + // the payload is valid but we cannot make its columns available, so the caller is + // told it was not published while the import is left to complete on its own + validateAndImportResult.importResult().ifPresent(future -> future.finishError(LOG)); + return SafeFuture.completedFuture( + PublishSignedExecutionPayloadResult.rejected( + beaconBlockRoot, dataColumnSidecarsResult.rejectionReason().get())); + } publishExecutionPayloadAndDataColumnSidecars( - signedExecutionPayload, dataColumnSidecars); + signedExecutionPayload, dataColumnSidecarsResult.dataColumnSidecars()); return validateAndImportResult .importResult() .orElseThrow(() -> new IllegalStateException("ACCEPT without import future")) @@ -115,6 +129,48 @@ private SafeFuture publishSignedExecutionPa }); } + /** + * A {@link DataColumnSidecarCreationException} is caused by the submission itself rather than by + * an internal failure, so it is turned into a rejection reason the caller can act on instead of + * being propagated as an error. Any other failure is left to propagate. + */ + private static SafeFuture recoverDataColumnSidecarCreationFailure( + final SafeFuture> dataColumnSidecarsFuture, + final Bytes32 beaconBlockRoot) { + return dataColumnSidecarsFuture + .thenApply(DataColumnSidecarsResult::created) + .exceptionallyCompose( + error -> + ExceptionUtil.getCause(error, DataColumnSidecarCreationException.class) + .map( + creationException -> { + LOG.warn( + "Unable to build the data column sidecars for the execution payload" + + " envelope with beacon block root {}: {}", + beaconBlockRoot, + creationException.getMessage()); + return SafeFuture.completedFuture( + DataColumnSidecarsResult.rejected(creationException.getMessage())); + }) + .orElseGet(() -> SafeFuture.failedFuture(error))); + } + + /** + * Either the data column sidecars to publish, or the reason why the execution payload envelope + * must be rejected because they could not be built. + */ + private record DataColumnSidecarsResult( + List dataColumnSidecars, Optional rejectionReason) { + + static DataColumnSidecarsResult created(final List dataColumnSidecars) { + return new DataColumnSidecarsResult(dataColumnSidecars, Optional.empty()); + } + + static DataColumnSidecarsResult rejected(final String rejectionReason) { + return new DataColumnSidecarsResult(List.of(), Optional.of(rejectionReason)); + } + } + private void publishExecutionPayloadAndDataColumnSidecars( final SignedExecutionPayloadEnvelope signedExecutionPayload, final List dataColumnSidecars) { diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java index 78990697f26..84813c1227c 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/ExecutionPayloadFactoryGloasTest.java @@ -120,8 +120,8 @@ public void failsToCreateDataColumnSidecarsWhenPayloadResultIsNotCached() { .thenReturn(Optional.empty()); assertThatSafeFuture(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) - .isCompletedExceptionallyWith(IllegalStateException.class) - .hasMessage("ExecutionPayloadResult hasn't been cached for slot " + slot); + .isCompletedExceptionallyWith(DataColumnSidecarCreationException.class) + .hasMessage(DataColumnSidecarCreationException.noCachedBlobData(slot).getMessage()); } @Test @@ -140,10 +140,8 @@ public void failsToCreateDataColumnSidecarsWhenCachedPayloadDoesNotMatchEnvelope setupCachingOfThePayloadResult(slot, mismatchedGetPayloadResponse); assertThatSafeFuture(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) - .isCompletedExceptionallyWith(IllegalStateException.class) - .hasMessage( - "Cached execution payload does not match signed execution payload envelope for slot " - + slot); + .isCompletedExceptionallyWith(DataColumnSidecarCreationException.class) + .hasMessage(DataColumnSidecarCreationException.cachedPayloadMismatch(slot).getMessage()); } private void assertExecutionPayloadCreated( diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java index 9e021ba75c2..25e3a50b728 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java @@ -38,6 +38,7 @@ import tech.pegasys.teku.statetransition.execution.ExecutionPayloadManager; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; import tech.pegasys.teku.validator.api.PublishSignedExecutionPayloadResult; +import tech.pegasys.teku.validator.coordinator.DataColumnSidecarCreationException; import tech.pegasys.teku.validator.coordinator.ExecutionPayloadFactory; class ExecutionPayloadPublisherGloasTest { @@ -98,8 +99,42 @@ public void publishSignedExecutionPayload_shouldValidateAndPublish() { } @Test - public void publishSignedExecutionPayload_shouldFailBeforePublishingWhenSidecarsAreUnavailable() { - final IllegalStateException error = new IllegalStateException("payload cache unavailable"); + public void publishSignedExecutionPayload_shouldRejectWithoutPublishingWhenBlobDataIsNotCached() { + final DataColumnSidecarCreationException error = + DataColumnSidecarCreationException.noCachedBlobData(signedExecutionPayload.getSlot()); + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn(SafeFuture.failedFuture(error)); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedWithValue( + PublishSignedExecutionPayloadResult.rejected( + signedExecutionPayload.getBeaconBlockRoot(), error.getMessage())); + + // broadcast validation runs concurrently with the sidecar creation, but nothing is published + verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); + } + + @Test + public void + publishSignedExecutionPayload_shouldRejectWithoutPublishingWhenEnvelopeDoesNotMatchCachedPayload() { + final DataColumnSidecarCreationException error = + DataColumnSidecarCreationException.cachedPayloadMismatch(signedExecutionPayload.getSlot()); + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn(SafeFuture.failedFuture(error)); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedWithValue( + PublishSignedExecutionPayloadResult.rejected( + signedExecutionPayload.getBeaconBlockRoot(), error.getMessage())); + + verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); + } + + @Test + public void publishSignedExecutionPayload_shouldFailBeforePublishingOnUnexpectedSidecarError() { + final IllegalStateException error = new IllegalStateException("boom"); when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) .thenReturn(SafeFuture.failedFuture(error)); @@ -107,8 +142,7 @@ public void publishSignedExecutionPayload_shouldFailBeforePublishingWhenSidecars executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) .isCompletedExceptionallyWith(error); - verifyNoInteractions( - executionPayloadManager, executionPayloadGossipChannel, dataColumnSidecarGossipChannel); + verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); } @Test From 08af0ebeacf3471cd6f45bd21eb111b7489a30ed Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Mon, 17 Aug 2026 11:39:04 +1000 Subject: [PATCH 4/6] check if execution payload has no blobs before rejecting for lack of sidecars Signed-off-by: Gabriel Fukushima --- .../DataColumnSidecarCreationException.java | 21 ++++++- .../ExecutionPayloadPublisherGloas.java | 61 +++++++++++++++---- .../ExecutionPayloadPublisherGloasTest.java | 53 +++++++++++++++- .../beaconchain/BeaconChainController.java | 3 +- 4 files changed, 122 insertions(+), 16 deletions(-) diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java index 5cdac3b24e3..89657a57982 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java @@ -22,8 +22,12 @@ */ public class DataColumnSidecarCreationException extends IllegalStateException { - private DataColumnSidecarCreationException(final String message) { + private final boolean blobDataNotCached; + + private DataColumnSidecarCreationException( + final String message, final boolean blobDataNotCached) { super(message); + this.blobDataNotCached = blobDataNotCached; } /** @@ -36,7 +40,8 @@ public static DataColumnSidecarCreationException noCachedBlobData(final UInt64 s "No cached blobs and KZG proofs to attach to the execution payload envelope for slot " + slot + ". Block production likely happened on a different beacon node, resubmit with" - + " Eth-Blob-Data-Included: true"); + + " Eth-Blob-Data-Included: true", + true); } /** The submitted envelope does not carry the execution payload this beacon node built. */ @@ -44,6 +49,16 @@ public static DataColumnSidecarCreationException cachedPayloadMismatch(final UIn return new DataColumnSidecarCreationException( "Signed execution payload envelope for slot " + slot - + " does not match the execution payload built by this beacon node"); + + " does not match the execution payload built by this beacon node", + false); + } + + /** + * Whether the failure was caused by the absence of cached blob data rather than by the submitted + * envelope itself. Only in that case can the envelope still be published, and only when the block + * commits to no blobs at all so that no sidecars are needed. + */ + public boolean isBlobDataNotCached() { + return blobDataNotCached; } } diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java index 3132eb392dc..18abefe2b39 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java @@ -29,6 +29,7 @@ import tech.pegasys.teku.statetransition.blobs.RemoteOrigin; import tech.pegasys.teku.statetransition.execution.ExecutionPayloadManager; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; +import tech.pegasys.teku.storage.client.RecentChainData; import tech.pegasys.teku.validator.api.PublishSignedExecutionPayloadResult; import tech.pegasys.teku.validator.coordinator.DataColumnSidecarCreationException; import tech.pegasys.teku.validator.coordinator.ExecutionPayloadFactory; @@ -41,16 +42,19 @@ public class ExecutionPayloadPublisherGloas implements ExecutionPayloadPublisher private final ExecutionPayloadGossipChannel executionPayloadGossipChannel; private final DataColumnSidecarGossipChannel dataColumnSidecarGossipChannel; private final ExecutionPayloadManager executionPayloadManager; + private final RecentChainData recentChainData; public ExecutionPayloadPublisherGloas( final ExecutionPayloadFactory executionPayloadFactory, final ExecutionPayloadGossipChannel executionPayloadGossipChannel, final DataColumnSidecarGossipChannel dataColumnSidecarGossipChannel, - final ExecutionPayloadManager executionPayloadManager) { + final ExecutionPayloadManager executionPayloadManager, + final RecentChainData recentChainData) { this.executionPayloadFactory = executionPayloadFactory; this.executionPayloadGossipChannel = executionPayloadGossipChannel; this.dataColumnSidecarGossipChannel = dataColumnSidecarGossipChannel; this.executionPayloadManager = executionPayloadManager; + this.recentChainData = recentChainData; } @Override @@ -134,7 +138,7 @@ private SafeFuture publishSignedExecutionPa * an internal failure, so it is turned into a rejection reason the caller can act on instead of * being propagated as an error. Any other failure is left to propagate. */ - private static SafeFuture recoverDataColumnSidecarCreationFailure( + private SafeFuture recoverDataColumnSidecarCreationFailure( final SafeFuture> dataColumnSidecarsFuture, final Bytes32 beaconBlockRoot) { return dataColumnSidecarsFuture @@ -143,18 +147,53 @@ private static SafeFuture recoverDataColumnSidecarCrea error -> ExceptionUtil.getCause(error, DataColumnSidecarCreationException.class) .map( - creationException -> { - LOG.warn( - "Unable to build the data column sidecars for the execution payload" - + " envelope with beacon block root {}: {}", - beaconBlockRoot, - creationException.getMessage()); - return SafeFuture.completedFuture( - DataColumnSidecarsResult.rejected(creationException.getMessage())); - }) + creationException -> + onDataColumnSidecarCreationFailure(creationException, beaconBlockRoot)) .orElseGet(() -> SafeFuture.failedFuture(error))); } + /** + * Missing blob data only prevents publication when the block actually commits to blobs. A block + * with no blob KZG commitments needs no sidecars, so the envelope is published with an empty list + * rather than rejected. When the block cannot be found we assume blobs are needed and reject. + */ + private SafeFuture onDataColumnSidecarCreationFailure( + final DataColumnSidecarCreationException creationException, final Bytes32 beaconBlockRoot) { + if (!creationException.isBlobDataNotCached()) { + return SafeFuture.completedFuture( + rejectDataColumnSidecars(creationException, beaconBlockRoot)); + } + return recentChainData + .retrieveBlockByRoot(beaconBlockRoot) + .thenApply( + maybeBlock -> { + final boolean commitsToBlobs = + maybeBlock + .flatMap(block -> block.getBody().getOptionalSignedExecutionPayloadBid()) + .map(bid -> !bid.getMessage().getBlobKzgCommitments().isEmpty()) + .orElse(true); + if (commitsToBlobs) { + return rejectDataColumnSidecars(creationException, beaconBlockRoot); + } + LOG.debug( + "No cached blob data for the execution payload envelope with beacon block root" + + " {}, but the block commits to no blobs so no data column sidecars are" + + " needed", + beaconBlockRoot); + return DataColumnSidecarsResult.created(List.of()); + }); + } + + private static DataColumnSidecarsResult rejectDataColumnSidecars( + final DataColumnSidecarCreationException creationException, final Bytes32 beaconBlockRoot) { + LOG.warn( + "Unable to build the data column sidecars for the execution payload envelope with beacon" + + " block root {}: {}", + beaconBlockRoot, + creationException.getMessage()); + return DataColumnSidecarsResult.rejected(creationException.getMessage()); + } + /** * Either the data column sidecars to publish, or the reason why the execution payload envelope * must be rejected because they could not be built. diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java index 25e3a50b728..0709192c84b 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java @@ -37,6 +37,7 @@ import tech.pegasys.teku.statetransition.blobs.RemoteOrigin; import tech.pegasys.teku.statetransition.execution.ExecutionPayloadManager; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; +import tech.pegasys.teku.storage.client.RecentChainData; import tech.pegasys.teku.validator.api.PublishSignedExecutionPayloadResult; import tech.pegasys.teku.validator.coordinator.DataColumnSidecarCreationException; import tech.pegasys.teku.validator.coordinator.ExecutionPayloadFactory; @@ -55,13 +56,15 @@ class ExecutionPayloadPublisherGloasTest { mock(DataColumnSidecarGossipChannel.class); private final ExecutionPayloadManager executionPayloadManager = mock(ExecutionPayloadManager.class); + private final RecentChainData recentChainData = mock(RecentChainData.class); private final ExecutionPayloadPublisherGloas executionPayloadPublisher = new ExecutionPayloadPublisherGloas( executionPayloadFactory, executionPayloadGossipChannel, dataColumnSidecarGossipChannel, - executionPayloadManager); + executionPayloadManager, + recentChainData); final SignedExecutionPayloadEnvelope signedExecutionPayload = dataStructureUtil.randomSignedExecutionPayloadEnvelope(42); @@ -70,6 +73,12 @@ class ExecutionPayloadPublisherGloasTest { @BeforeEach public void setUp() { + // by default the block commits to blobs, so sidecars are required to publish + when(recentChainData.retrieveBlockByRoot(signedExecutionPayload.getBeaconBlockRoot())) + .thenReturn( + SafeFuture.completedFuture( + Optional.of( + dataStructureUtil.randomSignedBeaconBlockWithCommitments(1).getMessage()))); when(executionPayloadManager.validateAndImportExecutionPayloadForBroadcast( eq(signedExecutionPayload), any())) .thenReturn( @@ -132,6 +141,48 @@ public void publishSignedExecutionPayload_shouldRejectWithoutPublishingWhenBlobD verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); } + @Test + public void publishSignedExecutionPayload_shouldPublishWithoutSidecarsWhenBlockHasNoBlobs() { + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn( + SafeFuture.failedFuture( + DataColumnSidecarCreationException.noCachedBlobData( + signedExecutionPayload.getSlot()))); + when(recentChainData.retrieveBlockByRoot(signedExecutionPayload.getBeaconBlockRoot())) + .thenReturn( + SafeFuture.completedFuture( + Optional.of( + dataStructureUtil.randomSignedBeaconBlockWithEmptyCommitments().getMessage()))); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedWithValue( + PublishSignedExecutionPayloadResult.success( + signedExecutionPayload.getBeaconBlockRoot())); + + verify(executionPayloadGossipChannel).publishExecutionPayload(signedExecutionPayload); + verify(dataColumnSidecarGossipChannel) + .publishDataColumnSidecars(List.of(), RemoteOrigin.LOCAL_PROPOSAL); + } + + @Test + public void publishSignedExecutionPayload_shouldRejectWhenBlobDataIsNotCachedAndBlockIsUnknown() { + final DataColumnSidecarCreationException error = + DataColumnSidecarCreationException.noCachedBlobData(signedExecutionPayload.getSlot()); + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn(SafeFuture.failedFuture(error)); + when(recentChainData.retrieveBlockByRoot(signedExecutionPayload.getBeaconBlockRoot())) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedWithValue( + PublishSignedExecutionPayloadResult.rejected( + signedExecutionPayload.getBeaconBlockRoot(), error.getMessage())); + + verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); + } + @Test public void publishSignedExecutionPayload_shouldFailBeforePublishingOnUnexpectedSidecarError() { final IllegalStateException error = new IllegalStateException("boom"); diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index e590835a3bc..87aca967ad3 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -1885,7 +1885,8 @@ public void initValidatorApiHandler() { executionPayloadFactory, executionPayloadGossipChannel, dataColumnSidecarGossipChannel, - executionPayloadManager); + executionPayloadManager, + recentChainData); } else { executionPayloadFactory = ExecutionPayloadFactory.NOOP; executionPayloadPublisher = ExecutionPayloadPublisher.NOOP; From 6d97ea07c613c03ba9634cb6dfc3f49b407cffe9 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Mon, 17 Aug 2026 13:15:52 +1000 Subject: [PATCH 5/6] fix old optional check and tests Signed-off-by: Gabriel Fukushima --- .../_eth_v4_validator_blocks_{slot}.json | 2 +- .../handlers/v4/validator/GetNewBlockV4.java | 8 +---- .../v4/validator/GetNewBlockV4Test.java | 31 +++++++++++++------ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json index ace46b99fd0..dec03b8cb0f 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v4_validator_blocks_{slot}.json @@ -3,7 +3,7 @@ "tags" : [ "Validator", "Validator Required Api" ], "operationId" : "produceBlockV4", "summary" : "Produce a new block, without signature.", - "description" : "Requests a beacon node to produce a valid block, which can then be signed by a validator.\n\nPost-Gloas, proposers submit execution payload bids rather than full execution payloads,\nso there is no longer a concept of blinded or unblinded blocks. Builders release the\npayload later. This endpoint is specific to the post-Gloas forks and is not backwards compatible\nwith previous forks.\n\nWhen self-building (local execution payload), the response will include the full block contents\nincluding the beacon block, execution payload envelope, blobs, and KZG proofs.\nWhen using an external builder bid, only the `BeaconBlock` is returned as the beacon node\ndoes not have access to the builder's execution payload.\n\nThe `Eth-Execution-Payload-Included` header and `execution_payload_included` response field\nindicate which response type was returned.\n", + "description" : "Requests a beacon node to produce a valid block, which can then be signed by a validator.\n\nPost-Gloas, proposers submit execution payload bids rather than full execution payloads,\nso there is no longer a concept of blinded or unblinded blocks. Builders release the\npayload later. This endpoint is specific to the post-Gloas forks and is not backwards compatible\nwith previous forks.\n\nWhen self-building (local execution payload), the response includes the full block contents\n(beacon block, execution payload envelope, blobs, and KZG proofs) if `include_payload` is\nset to `true`, otherwise only the `BeaconBlock` is returned.\nWhen using an external builder bid, only the `BeaconBlock` is returned as the beacon node\ndoes not have access to the builder's execution payload.\n\nThe `Eth-Execution-Payload-Included` header and `execution_payload_included` response field\nindicate which response type was returned.\n", "parameters" : [ { "name" : "slot", "required" : true, diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java index 31cc76004e4..07563f52189 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4.java @@ -142,11 +142,6 @@ public void handleRequest(final RestApiRequest request) throws JsonProcessingExc return; } - if (includePayload.isEmpty()) { - request.respondError(SC_BAD_REQUEST, "include_payload is required"); - return; - } - final long requestTimeMs = System.currentTimeMillis(); LOG.debug( "produceBlockV4 requested: slot={}, include_payload={}, builder_boost_factor={}, timestampMs={}", @@ -169,8 +164,7 @@ public void handleRequest(final RestApiRequest request) throws JsonProcessingExc instanceof BlockContentsGloas; // include_payload=true and self-built → include full contents // include_payload=false or builder bid → return beacon block only - final boolean executionPayloadIncluded = - selfBuilt && includePayload.get(); + final boolean executionPayloadIncluded = selfBuilt && includePayload; final BlockContainerAndMetaData responseMetaData = executionPayloadIncluded ? blockContainerAndMetaData diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4Test.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4Test.java index 3cb7d794cbe..49c7289a020 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4Test.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v4/validator/GetNewBlockV4Test.java @@ -34,7 +34,9 @@ import static tech.pegasys.teku.spec.SpecMilestone.GLOAS; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import java.util.Optional; +import java.util.stream.StreamSupport; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest; @@ -42,6 +44,8 @@ import tech.pegasys.teku.bls.BLSTestUtil; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.http.HttpStatusCodes; +import tech.pegasys.teku.infrastructure.json.JsonTestUtil; +import tech.pegasys.teku.infrastructure.restapi.OpenApiTestUtil; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.TestSpecContext; @@ -67,6 +71,7 @@ public void setup(final TestSpecInvocationContextProvider.SpecContext specContex @TestTemplate void shouldReturnBadRequestForPreGloasFork() throws Exception { assumeThat(specMilestone).isLessThan(GLOAS); + request.setQueryParameter(INCLUDE_PAYLOAD, "true"); handler.handleRequest(request); @@ -76,7 +81,7 @@ void shouldReturnBadRequestForPreGloasFork() throws Exception { @TestTemplate void shouldIncludeEnvelopeWhenSelfBuiltAndIncludePayloadTrue() throws Exception { assumeThat(specMilestone).isGreaterThanOrEqualTo(GLOAS); - request.setOptionalQueryParameter(INCLUDE_PAYLOAD, "true"); + request.setQueryParameter(INCLUDE_PAYLOAD, "true"); final BlockContainerAndMetaData blockContainerAndMetaData = dataStructureUtil.randomBlockContentsGloasAndMetaData(ONE); @@ -99,7 +104,7 @@ void shouldIncludeEnvelopeWhenSelfBuiltAndIncludePayloadTrue() throws Exception @TestTemplate void shouldReturnBeaconBlockOnlyWhenBuilderBidAndIncludePayloadTrue() throws Exception { assumeThat(specMilestone).isGreaterThanOrEqualTo(GLOAS); - request.setOptionalQueryParameter(INCLUDE_PAYLOAD, "true"); + request.setQueryParameter(INCLUDE_PAYLOAD, "true"); // Plain BeaconBlock = external builder bid (no envelope available) final BlockContainerAndMetaData blockContainerAndMetaData = dataStructureUtil.randomBlockContainerAndMetaData(ONE); @@ -117,7 +122,7 @@ void shouldReturnBeaconBlockOnlyWhenBuilderBidAndIncludePayloadTrue() throws Exc @TestTemplate void shouldReturnBeaconBlockOnlyWhenIncludePayloadIsFalse() throws Exception { assumeThat(specMilestone).isGreaterThanOrEqualTo(GLOAS); - request.setOptionalQueryParameter(INCLUDE_PAYLOAD, "false"); + request.setQueryParameter(INCLUDE_PAYLOAD, "false"); // Even with a self-built block, include_payload=false strips the envelope final BlockContainerAndMetaData blockContainerAndMetaData = dataStructureUtil.randomBlockContentsGloasAndMetaData(ONE); @@ -133,18 +138,24 @@ void shouldReturnBeaconBlockOnlyWhenIncludePayloadIsFalse() throws Exception { } @TestTemplate - void shouldReturnBadRequestWhenIncludePayloadIsMissing() throws Exception { - assumeThat(specMilestone).isGreaterThanOrEqualTo(GLOAS); - - handler.handleRequest(request); - - assertThat(request.getResponseCode()).isEqualTo(SC_BAD_REQUEST); + void shouldDeclareIncludePayloadAsRequired() throws Exception { + // a missing include_payload is rejected by the rest api framework rather than by the handler, + // so all the handler has to guarantee is that the parameter is declared as required + final JsonNode metadata = + JsonTestUtil.parseAsJsonNode(OpenApiTestUtil.serializeEndpointMetadata(handler)); + final JsonNode includePayloadParameter = + StreamSupport.stream(metadata.get("get").get("parameters").spliterator(), false) + .filter(parameter -> INCLUDE_PAYLOAD.equals(parameter.get("name").asText())) + .findFirst() + .orElseThrow(); + + assertThat(includePayloadParameter.get("required").asBoolean()).isTrue(); } @TestTemplate void shouldThrowExceptionWhenEmptyBlock() throws Exception { assumeThat(specMilestone).isGreaterThanOrEqualTo(GLOAS); - request.setOptionalQueryParameter(INCLUDE_PAYLOAD, "true"); + request.setQueryParameter(INCLUDE_PAYLOAD, "true"); doReturn(SafeFuture.completedFuture(Optional.empty())) .when(validatorDataProvider) .produceBlock(ONE, signature, Optional.empty(), Optional.empty()); From 2b25ef678ac9c510fbfa1c3ac88b7bd4fe5ca060 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Tue, 18 Aug 2026 10:24:46 +1000 Subject: [PATCH 6/6] remove early return that would mislead in a second case where we would reject a payload mismatch Signed-off-by: Gabriel Fukushima --- .../DataColumnSidecarCreationException.java | 26 +++------ .../ExecutionPayloadPublisherGloas.java | 24 ++++---- .../ExecutionPayloadPublisherGloasTest.java | 57 +++++++++++++++++++ 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java index 89657a57982..70567f1fba5 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/DataColumnSidecarCreationException.java @@ -22,12 +22,8 @@ */ public class DataColumnSidecarCreationException extends IllegalStateException { - private final boolean blobDataNotCached; - - private DataColumnSidecarCreationException( - final String message, final boolean blobDataNotCached) { + private DataColumnSidecarCreationException(final String message) { super(message); - this.blobDataNotCached = blobDataNotCached; } /** @@ -40,25 +36,17 @@ public static DataColumnSidecarCreationException noCachedBlobData(final UInt64 s "No cached blobs and KZG proofs to attach to the execution payload envelope for slot " + slot + ". Block production likely happened on a different beacon node, resubmit with" - + " Eth-Blob-Data-Included: true", - true); + + " Eth-Blob-Data-Included: true"); } - /** The submitted envelope does not carry the execution payload this beacon node built. */ + /** + * The submitted envelope does not carry the execution payload this beacon node built, so the + * blobs cached for the slot belong to a different payload and cannot be attached to it. + */ public static DataColumnSidecarCreationException cachedPayloadMismatch(final UInt64 slot) { return new DataColumnSidecarCreationException( "Signed execution payload envelope for slot " + slot - + " does not match the execution payload built by this beacon node", - false); - } - - /** - * Whether the failure was caused by the absence of cached blob data rather than by the submitted - * envelope itself. Only in that case can the envelope still be published, and only when the block - * commits to no blobs at all so that no sidecars are needed. - */ - public boolean isBlobDataNotCached() { - return blobDataNotCached; + + " does not match the execution payload built by this beacon node"); } } diff --git a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java index 18abefe2b39..8a73b7ed2a2 100644 --- a/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java +++ b/beacon/validator/src/main/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloas.java @@ -153,16 +153,17 @@ private SafeFuture recoverDataColumnSidecarCreationFai } /** - * Missing blob data only prevents publication when the block actually commits to blobs. A block - * with no blob KZG commitments needs no sidecars, so the envelope is published with an empty list - * rather than rejected. When the block cannot be found we assume blobs are needed and reject. + * Every {@link DataColumnSidecarCreationException} means the cached blob data cannot be used for + * this envelope, either because it is absent or because it belongs to a different payload. That + * only prevents publication when the block actually commits to blobs: a block with no blob KZG + * commitments needs no sidecars, so the envelope is published with an empty list rather than + * rejected. When the block cannot be found we assume blobs are needed and reject. + * + *

An envelope that is invalid in its own right is still rejected, because the broadcast + * validation result is checked before these sidecars are used. */ private SafeFuture onDataColumnSidecarCreationFailure( final DataColumnSidecarCreationException creationException, final Bytes32 beaconBlockRoot) { - if (!creationException.isBlobDataNotCached()) { - return SafeFuture.completedFuture( - rejectDataColumnSidecars(creationException, beaconBlockRoot)); - } return recentChainData .retrieveBlockByRoot(beaconBlockRoot) .thenApply( @@ -176,10 +177,11 @@ private SafeFuture onDataColumnSidecarCreationFailure( return rejectDataColumnSidecars(creationException, beaconBlockRoot); } LOG.debug( - "No cached blob data for the execution payload envelope with beacon block root" - + " {}, but the block commits to no blobs so no data column sidecars are" - + " needed", - beaconBlockRoot); + "Could not use the cached blob data for the execution payload envelope with" + + " beacon block root {} ({}), but the block commits to no blobs so no data" + + " column sidecars are needed", + beaconBlockRoot, + creationException.getMessage()); return DataColumnSidecarsResult.created(List.of()); }); } diff --git a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java index 0709192c84b..c80aae20240 100644 --- a/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java +++ b/beacon/validator/src/test/java/tech/pegasys/teku/validator/coordinator/publisher/ExecutionPayloadPublisherGloasTest.java @@ -165,6 +165,63 @@ public void publishSignedExecutionPayload_shouldPublishWithoutSidecarsWhenBlockH .publishDataColumnSidecars(List.of(), RemoteOrigin.LOCAL_PROPOSAL); } + @Test + public void + publishSignedExecutionPayload_shouldPublishWithoutSidecarsWhenCachedPayloadDoesNotMatchAndBlockHasNoBlobs() { + // a mismatched cache only matters when blobs have to be attached, and a block committing to no + // blobs needs none: this is the failover case where the block was produced on another node + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn( + SafeFuture.failedFuture( + DataColumnSidecarCreationException.cachedPayloadMismatch( + signedExecutionPayload.getSlot()))); + when(recentChainData.retrieveBlockByRoot(signedExecutionPayload.getBeaconBlockRoot())) + .thenReturn( + SafeFuture.completedFuture( + Optional.of( + dataStructureUtil.randomSignedBeaconBlockWithEmptyCommitments().getMessage()))); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedWithValue( + PublishSignedExecutionPayloadResult.success( + signedExecutionPayload.getBeaconBlockRoot())); + + verify(executionPayloadGossipChannel).publishExecutionPayload(signedExecutionPayload); + verify(dataColumnSidecarGossipChannel) + .publishDataColumnSidecars(List.of(), RemoteOrigin.LOCAL_PROPOSAL); + } + + @Test + public void publishSignedExecutionPayload_shouldNotPublishInvalidEnvelopeWhenBlockHasNoBlobs() { + // publishing on a sidecar failure is only safe because broadcast validation is checked first, + // so an envelope that is invalid in its own right must never reach the network + when(executionPayloadManager.validateAndImportExecutionPayloadForBroadcast( + eq(signedExecutionPayload), any())) + .thenReturn( + SafeFuture.completedFuture( + new ExecutionPayloadManager.ValidateAndImportResult( + InternalValidationResult.reject("oopsy"), Optional.empty()))); + when(executionPayloadFactory.createDataColumnSidecars(signedExecutionPayload)) + .thenReturn( + SafeFuture.failedFuture( + DataColumnSidecarCreationException.cachedPayloadMismatch( + signedExecutionPayload.getSlot()))); + when(recentChainData.retrieveBlockByRoot(signedExecutionPayload.getBeaconBlockRoot())) + .thenReturn( + SafeFuture.completedFuture( + Optional.of( + dataStructureUtil.randomSignedBeaconBlockWithEmptyCommitments().getMessage()))); + + SafeFutureAssert.assertThatSafeFuture( + executionPayloadPublisher.publishSignedExecutionPayload(signedExecutionPayload)) + .isCompletedWithValue( + PublishSignedExecutionPayloadResult.rejected( + signedExecutionPayload.getBeaconBlockRoot(), "Failed broadcast validation: oopsy")); + + verifyNoInteractions(executionPayloadGossipChannel, dataColumnSidecarGossipChannel); + } + @Test public void publishSignedExecutionPayload_shouldRejectWhenBlobDataIsNotCachedAndBlockIsUnknown() { final DataColumnSidecarCreationException error =