From baa4209e58dd7afa25acad913b35a997d99cb97a Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 6 May 2026 14:39:50 +1000 Subject: [PATCH 1/6] feat: add GLOAS (Glamsterdam) signing support Adds four new signing types for the upcoming Glamsterdam (ePBS) fork: EXECUTION_PAYLOAD_BID, EXECUTION_PAYLOAD_ENVELOPE, PAYLOAD_ATTESTATION_MESSAGE, and PROPOSER_PREFERENCES. Bumps Teku to the develop snapshot as the GLOAS schemas (e.g. ExecutionPayloadBid added execution_requests_root, ExecutionPayloadEnvelope dropped slot/state_root, new ExecutionPayloadGloas) are still evolving post-26.4.0. Should pin to a stable Teku version once the next release is cut. Co-authored-by: Sally MacFarlane --- acceptance-tests/build.gradle | 1 + .../dsl/utils/Eth2RequestUtils.java | 128 ++++++++++ .../utils/Eth2SigningRequestBodyBuilder.java | 38 ++- .../signing/BlsSigningAcceptanceTest.java | 17 +- .../core/service/http/ArtifactType.java | 6 +- .../eth2/Eth2SignForIdentifierHandler.java | 38 +++ .../signing/eth2/Eth2SigningRequestBody.java | 10 +- .../schema/gloas/ExecutionPayloadBid.java | 158 ++++++++++++ .../gloas/ExecutionPayloadEnvelope.java | 84 +++++++ .../schema/gloas/ExecutionPayloadGloas.java | 140 +++++++++++ .../schema/gloas/PayloadAttestationData.java | 69 ++++++ .../schema/gloas/ProposerPreferences.java | 68 ++++++ gradle/versions.gradle | 2 +- openapi-specs/eth2/signing/paths/sign.yaml | 8 + openapi-specs/eth2/signing/schemas.yaml | 228 ++++++++++++++++++ 15 files changed, 989 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadBid.java create mode 100644 core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadEnvelope.java create mode 100644 core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadGloas.java create mode 100644 core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/PayloadAttestationData.java create mode 100644 core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ProposerPreferences.java diff --git a/acceptance-tests/build.gradle b/acceptance-tests/build.gradle index fc666dc29..2092bd4a4 100644 --- a/acceptance-tests/build.gradle +++ b/acceptance-tests/build.gradle @@ -47,6 +47,7 @@ dependencies { testImplementation 'tech.pegasys.teku.internal:serializer' testImplementation 'tech.pegasys.teku.internal:unsigned' testImplementation 'tech.pegasys.teku.internal:async' + testImplementation 'tech.pegasys.teku.internal:execution-types' testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.web3j:core' testImplementation 'org.web3j:crypto' diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java index 901242286..2a14b1357 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java @@ -47,7 +47,14 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.Checkpoint; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.Eth1Data; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.Fork; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.KZGCommitment; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.VoluntaryExit; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.electra.ExecutionRequests; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadBid; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadEnvelope; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadGloas; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.PayloadAttestationData; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ProposerPreferences; import tech.pegasys.web3signer.core.util.DepositSigningRootUtil; import java.util.Random; @@ -88,6 +95,12 @@ public class Eth2RequestUtils { private static final Eth2BlockSigningRequestUtil ALTAIR_BLOCK_UTIL = new Eth2BlockSigningRequestUtil(SpecMilestone.ALTAIR); + // Gloas Spec + private static final Spec GLOAS_SPEC = TestSpecFactory.createMinimalGloas(); + private static final DataStructureUtil GLOAS_DATA_STRUCTURE_UTIL = + new DataStructureUtil(GLOAS_SPEC); + private static final SigningRootUtil GLOAS_SIGNING_ROOT_UTIL = new SigningRootUtil(GLOAS_SPEC); + public static Eth2SigningRequestBody createCannedRequest(final ArtifactType artifactType) { return switch (artifactType) { case DEPOSIT -> createDepositRequest(); @@ -116,6 +129,14 @@ public static Eth2SigningRequestBody createCannedRequest(final ArtifactType arti createSyncCommitteeContributionAndProofRequest(); case VALIDATOR_REGISTRATION -> createValidatorRegistrationRequest(); + + case EXECUTION_PAYLOAD_BID -> createExecutionPayloadBidRequest(); + + case EXECUTION_PAYLOAD_ENVELOPE -> createExecutionPayloadEnvelopeRequest(); + + case PAYLOAD_ATTESTATION_MESSAGE -> createPayloadAttestationMessageRequest(); + + case PROPOSER_PREFERENCES -> createProposerPreferencesRequest(); }; } @@ -416,6 +437,113 @@ private static Eth2SigningRequestBody createValidatorRegistrationRequest() { .build(); } + public static ForkInfo gloasForkInfo() { + final tech.pegasys.teku.spec.datastructures.state.Fork internalFork = + GLOAS_SPEC.getForkSchedule().getFork(UInt64.ZERO); + final Fork fork = + new Fork( + internalFork.getPreviousVersion(), + internalFork.getCurrentVersion(), + internalFork.getEpoch()); + return new ForkInfo(fork, Bytes32.fromHexString(GENESIS_VALIDATORS_ROOT)); + } + + private static Eth2SigningRequestBody createExecutionPayloadBidRequest() { + final ForkInfo forkInfo = gloasForkInfo(); + final tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid randomBid = + GLOAS_DATA_STRUCTURE_UTIL.randomExecutionPayloadBid(); + final ExecutionPayloadBid bid = + new ExecutionPayloadBid( + randomBid.getParentBlockHash(), + randomBid.getParentBlockRoot(), + randomBid.getBlockHash(), + randomBid.getPrevRandao(), + new tech.pegasys.teku.infrastructure.bytes.Bytes20( + randomBid.getFeeRecipient().getWrappedBytes()), + randomBid.getGasLimit(), + randomBid.getBuilderIndex(), + randomBid.getSlot(), + randomBid.getValue(), + randomBid.getExecutionPayment(), + randomBid.getBlobKzgCommitments().stream() + .map(c -> new KZGCommitment(c.getKZGCommitment())) + .toList(), + randomBid.getExecutionRequestsRoot()); + final Bytes signingRoot = + GLOAS_SIGNING_ROOT_UTIL.signingRootForSignExecutionPayloadBid( + randomBid, forkInfo.asInternalForkInfo()); + return Eth2SigningRequestBodyBuilder.anEth2SigningRequestBody() + .withType(ArtifactType.EXECUTION_PAYLOAD_BID) + .withSigningRoot(signingRoot) + .withForkInfo(forkInfo) + .withExecutionPayloadBid(bid) + .build(); + } + + private static Eth2SigningRequestBody createExecutionPayloadEnvelopeRequest() { + final ForkInfo forkInfo = gloasForkInfo(); + final tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope + randomEnvelope = + GLOAS_DATA_STRUCTURE_UTIL.randomExecutionPayloadEnvelope(UInt64.valueOf(7)); + final ExecutionPayloadEnvelope envelope = + new ExecutionPayloadEnvelope( + new ExecutionPayloadGloas(randomEnvelope.getPayload()), + new ExecutionRequests(randomEnvelope.getExecutionRequests()), + randomEnvelope.getBuilderIndex(), + randomEnvelope.getBeaconBlockRoot()); + final Bytes signingRoot = + GLOAS_SIGNING_ROOT_UTIL.signingRootForSignExecutionPayloadEnvelope( + randomEnvelope, forkInfo.asInternalForkInfo()); + return Eth2SigningRequestBodyBuilder.anEth2SigningRequestBody() + .withType(ArtifactType.EXECUTION_PAYLOAD_ENVELOPE) + .withSigningRoot(signingRoot) + .withForkInfo(forkInfo) + .withExecutionPayloadEnvelope(envelope) + .build(); + } + + private static Eth2SigningRequestBody createPayloadAttestationMessageRequest() { + final ForkInfo forkInfo = gloasForkInfo(); + final tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData + randomData = GLOAS_DATA_STRUCTURE_UTIL.randomPayloadAttestationData(UInt64.valueOf(7)); + final PayloadAttestationData payloadAttestationData = + new PayloadAttestationData( + randomData.getBeaconBlockRoot(), + randomData.getSlot(), + randomData.isPayloadPresent(), + randomData.isBlobDataAvailable()); + final Bytes signingRoot = + GLOAS_SIGNING_ROOT_UTIL.signingRootForSignPayloadAttestationData( + randomData, forkInfo.asInternalForkInfo()); + return Eth2SigningRequestBodyBuilder.anEth2SigningRequestBody() + .withType(ArtifactType.PAYLOAD_ATTESTATION_MESSAGE) + .withSigningRoot(signingRoot) + .withForkInfo(forkInfo) + .withPayloadAttestationMessage(payloadAttestationData) + .build(); + } + + private static Eth2SigningRequestBody createProposerPreferencesRequest() { + final ForkInfo forkInfo = gloasForkInfo(); + final tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferences + randomPreferences = GLOAS_DATA_STRUCTURE_UTIL.randomProposerPreferences(); + final ProposerPreferences proposerPreferences = + new ProposerPreferences( + randomPreferences.getProposalSlot(), + randomPreferences.getValidatorIndex(), + randomPreferences.getFeeRecipient(), + randomPreferences.getGasLimit()); + final Bytes signingRoot = + GLOAS_SIGNING_ROOT_UTIL.signingRootForSignProposerPreferences( + randomPreferences, forkInfo.asInternalForkInfo()); + return Eth2SigningRequestBodyBuilder.anEth2SigningRequestBody() + .withType(ArtifactType.PROPOSER_PREFERENCES) + .withSigningRoot(signingRoot) + .withForkInfo(forkInfo) + .withProposerPreferences(proposerPreferences) + .build(); + } + private static tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.altair .ContributionAndProof getContributionAndProof() { diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2SigningRequestBodyBuilder.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2SigningRequestBodyBuilder.java index 8fbdd2ab9..627442ac3 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2SigningRequestBodyBuilder.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2SigningRequestBodyBuilder.java @@ -27,6 +27,10 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.BeaconBlock; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.VoluntaryExit; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.altair.ContributionAndProof; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadBid; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadEnvelope; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.PayloadAttestationData; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ProposerPreferences; import org.apache.tuweni.bytes.Bytes; @@ -46,6 +50,10 @@ public final class Eth2SigningRequestBodyBuilder { private SyncAggregatorSelectionData syncAggregatorSelectionData; private ContributionAndProof contributionAndProof; private ValidatorRegistration validatorRegistration; + private ExecutionPayloadBid executionPayloadBid; + private ExecutionPayloadEnvelope executionPayloadEnvelope; + private PayloadAttestationData payloadAttestationMessage; + private ProposerPreferences proposerPreferences; private Eth2SigningRequestBodyBuilder() {} @@ -133,6 +141,30 @@ public Eth2SigningRequestBodyBuilder withValidatorRegistration( return this; } + public Eth2SigningRequestBodyBuilder withExecutionPayloadBid( + ExecutionPayloadBid executionPayloadBid) { + this.executionPayloadBid = executionPayloadBid; + return this; + } + + public Eth2SigningRequestBodyBuilder withExecutionPayloadEnvelope( + ExecutionPayloadEnvelope executionPayloadEnvelope) { + this.executionPayloadEnvelope = executionPayloadEnvelope; + return this; + } + + public Eth2SigningRequestBodyBuilder withPayloadAttestationMessage( + PayloadAttestationData payloadAttestationMessage) { + this.payloadAttestationMessage = payloadAttestationMessage; + return this; + } + + public Eth2SigningRequestBodyBuilder withProposerPreferences( + ProposerPreferences proposerPreferences) { + this.proposerPreferences = proposerPreferences; + return this; + } + public Eth2SigningRequestBody build() { return new Eth2SigningRequestBody( type, @@ -149,6 +181,10 @@ public Eth2SigningRequestBody build() { syncCommitteeMessage, syncAggregatorSelectionData, contributionAndProof, - validatorRegistration); + validatorRegistration, + executionPayloadBid, + executionPayloadEnvelope, + payloadAttestationMessage, + proposerPreferences); } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/BlsSigningAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/BlsSigningAcceptanceTest.java index 5b7f5fc3d..d1594ac18 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/BlsSigningAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/BlsSigningAcceptanceTest.java @@ -278,7 +278,11 @@ public void failsIfSigningRootDoesNotMatchSigningData(final ArtifactType artifac request.syncCommitteeMessage(), request.syncAggregatorSelectionData(), request.contributionAndProof(), - request.validatorRegistration()); + request.validatorRegistration(), + request.executionPayloadBid(), + request.executionPayloadEnvelope(), + request.payloadAttestationMessage(), + request.proposerPreferences()); final Response response = signer.eth2Sign(KEY_PAIR.getPublicKey().toString(), requestWithMismatchedSigningRoot); @@ -316,7 +320,11 @@ public void ableToSignWithoutSigningRootField(final ContentType acceptableConten request.syncCommitteeMessage(), request.syncAggregatorSelectionData(), request.contributionAndProof(), - request.validatorRegistration()); + request.validatorRegistration(), + request.executionPayloadBid(), + request.executionPayloadEnvelope(), + request.payloadAttestationMessage(), + request.proposerPreferences()); final Response response = signer.eth2Sign( @@ -373,6 +381,11 @@ private void setupMinimalWeb3Signer(final ArtifactType artifactType) { SYNC_COMMITTEE_SELECTION_PROOF, SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF -> setupEth2Signer(Eth2Network.MINIMAL, SpecMilestone.ALTAIR); + case EXECUTION_PAYLOAD_BID, + EXECUTION_PAYLOAD_ENVELOPE, + PAYLOAD_ATTESTATION_MESSAGE, + PROPOSER_PREFERENCES -> + setupEth2Signer(Eth2Network.MINIMAL, SpecMilestone.GLOAS); default -> setupEth2Signer(Eth2Network.MINIMAL, SpecMilestone.PHASE0); } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/ArtifactType.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/ArtifactType.java index f361a8538..4c2f51b84 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/ArtifactType.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/ArtifactType.java @@ -26,5 +26,9 @@ public enum ArtifactType { SYNC_COMMITTEE_MESSAGE, SYNC_COMMITTEE_SELECTION_PROOF, SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF, - VALIDATOR_REGISTRATION + VALIDATOR_REGISTRATION, + EXECUTION_PAYLOAD_BID, + EXECUTION_PAYLOAD_ENVELOPE, + PAYLOAD_ATTESTATION_MESSAGE, + PROPOSER_PREFERENCES } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java index 13929d582..55b3552d2 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java @@ -319,6 +319,44 @@ private Bytes computeSigningRoot(final Eth2SigningRequestBody body) { return signingRootUtil.signingRootForValidatorRegistration( validatorRegistration.asInternalValidatorRegistration()); } + case EXECUTION_PAYLOAD_BID -> { + final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas + .ExecutionPayloadBid + bid = body.executionPayloadBid(); + checkArgument(bid != null, "executionPayloadBid is required"); + return signingRootUtil.signingRootForSignExecutionPayloadBid( + bid.asInternalExecutionPayloadBid(eth2Spec.atSlot(bid.getSlot())), + body.forkInfo().asInternalForkInfo()); + } + case EXECUTION_PAYLOAD_ENVELOPE -> { + final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas + .ExecutionPayloadEnvelope + envelope = body.executionPayloadEnvelope(); + checkArgument(envelope != null, "executionPayloadEnvelope is required"); + return signingRootUtil.signingRootForSignExecutionPayloadEnvelope( + envelope.asInternalExecutionPayloadEnvelope(eth2Spec.atSlot(envelope.getSlot())), + body.forkInfo().asInternalForkInfo()); + } + case PAYLOAD_ATTESTATION_MESSAGE -> { + final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas + .PayloadAttestationData + payloadAttestationData = body.payloadAttestationMessage(); + checkArgument(payloadAttestationData != null, "payloadAttestationMessage is required"); + return signingRootUtil.signingRootForSignPayloadAttestationData( + payloadAttestationData.asInternalPayloadAttestationData( + eth2Spec.atSlot(payloadAttestationData.getSlot())), + body.forkInfo().asInternalForkInfo()); + } + case PROPOSER_PREFERENCES -> { + final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas + .ProposerPreferences + proposerPreferences = body.proposerPreferences(); + checkArgument(proposerPreferences != null, "proposerPreferences is required"); + return signingRootUtil.signingRootForSignProposerPreferences( + proposerPreferences.asInternalProposerPreferences( + eth2Spec.atSlot(proposerPreferences.getProposalSlot())), + body.forkInfo().asInternalForkInfo()); + } default -> throw new IllegalStateException("Signing root unimplemented for type " + body.type()); } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SigningRequestBody.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SigningRequestBody.java index f106b1a4a..0fd5462c0 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SigningRequestBody.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SigningRequestBody.java @@ -17,6 +17,10 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.BeaconBlock; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.VoluntaryExit; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.altair.ContributionAndProof; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadBid; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadEnvelope; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.PayloadAttestationData; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ProposerPreferences; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; @@ -38,4 +42,8 @@ public record Eth2SigningRequestBody( @JsonProperty("sync_aggregator_selection_data") SyncAggregatorSelectionData syncAggregatorSelectionData, @JsonProperty("contribution_and_proof") ContributionAndProof contributionAndProof, - @JsonProperty("validator_registration") ValidatorRegistration validatorRegistration) {} + @JsonProperty("validator_registration") ValidatorRegistration validatorRegistration, + @JsonProperty("execution_payload_bid") ExecutionPayloadBid executionPayloadBid, + @JsonProperty("execution_payload_envelope") ExecutionPayloadEnvelope executionPayloadEnvelope, + @JsonProperty("payload_attestation_message") PayloadAttestationData payloadAttestationMessage, + @JsonProperty("proposer_preferences") ProposerPreferences proposerPreferences) {} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadBid.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadBid.java new file mode 100644 index 000000000..3bad1177b --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadBid.java @@ -0,0 +1,158 @@ +/* + * Copyright 2026 ConsenSys AG. + * + * 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.web3signer.core.service.http.handlers.signing.eth2.schema.gloas; + +import tech.pegasys.teku.infrastructure.bytes.Bytes20; +import tech.pegasys.teku.infrastructure.ssz.SszList; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBidSchema; +import tech.pegasys.teku.spec.datastructures.type.SszKZGCommitment; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.KZGCommitment; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.tuweni.bytes.Bytes32; + +public class ExecutionPayloadBid { + + private final Bytes32 parentBlockHash; + private final Bytes32 parentBlockRoot; + private final Bytes32 blockHash; + private final Bytes32 prevRandao; + private final Bytes20 feeRecipient; + private final UInt64 gasLimit; + private final UInt64 builderIndex; + private final UInt64 slot; + private final UInt64 value; + private final UInt64 executionPayment; + private final List blobKzgCommitments; + private final Bytes32 executionRequestsRoot; + + @JsonCreator + public ExecutionPayloadBid( + @JsonProperty(value = "parent_block_hash", required = true) final Bytes32 parentBlockHash, + @JsonProperty(value = "parent_block_root", required = true) final Bytes32 parentBlockRoot, + @JsonProperty(value = "block_hash", required = true) final Bytes32 blockHash, + @JsonProperty(value = "prev_randao", required = true) final Bytes32 prevRandao, + @JsonProperty(value = "fee_recipient", required = true) final Bytes20 feeRecipient, + @JsonProperty(value = "gas_limit", required = true) final UInt64 gasLimit, + @JsonProperty(value = "builder_index", required = true) final UInt64 builderIndex, + @JsonProperty(value = "slot", required = true) final UInt64 slot, + @JsonProperty(value = "value", required = true) final UInt64 value, + @JsonProperty(value = "execution_payment", required = true) final UInt64 executionPayment, + @JsonProperty(value = "blob_kzg_commitments", required = true) + final List blobKzgCommitments, + @JsonProperty(value = "execution_requests_root", required = true) + final Bytes32 executionRequestsRoot) { + this.parentBlockHash = parentBlockHash; + this.parentBlockRoot = parentBlockRoot; + this.blockHash = blockHash; + this.prevRandao = prevRandao; + this.feeRecipient = feeRecipient; + this.gasLimit = gasLimit; + this.builderIndex = builderIndex; + this.slot = slot; + this.value = value; + this.executionPayment = executionPayment; + this.blobKzgCommitments = blobKzgCommitments; + this.executionRequestsRoot = executionRequestsRoot; + } + + @JsonProperty("parent_block_hash") + public Bytes32 getParentBlockHash() { + return parentBlockHash; + } + + @JsonProperty("parent_block_root") + public Bytes32 getParentBlockRoot() { + return parentBlockRoot; + } + + @JsonProperty("block_hash") + public Bytes32 getBlockHash() { + return blockHash; + } + + @JsonProperty("prev_randao") + public Bytes32 getPrevRandao() { + return prevRandao; + } + + @JsonProperty("fee_recipient") + public Bytes20 getFeeRecipient() { + return feeRecipient; + } + + @JsonProperty("gas_limit") + public UInt64 getGasLimit() { + return gasLimit; + } + + @JsonProperty("builder_index") + public UInt64 getBuilderIndex() { + return builderIndex; + } + + @JsonProperty("slot") + public UInt64 getSlot() { + return slot; + } + + @JsonProperty("value") + public UInt64 getValue() { + return value; + } + + @JsonProperty("execution_payment") + public UInt64 getExecutionPayment() { + return executionPayment; + } + + @JsonProperty("blob_kzg_commitments") + public List getBlobKzgCommitments() { + return blobKzgCommitments; + } + + @JsonProperty("execution_requests_root") + public Bytes32 getExecutionRequestsRoot() { + return executionRequestsRoot; + } + + public tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid + asInternalExecutionPayloadBid(final SpecVersion specVersion) { + final ExecutionPayloadBidSchema schema = + SchemaDefinitionsGloas.required(specVersion.getSchemaDefinitions()) + .getExecutionPayloadBidSchema(); + final SszList sszBlobKzgCommitments = + blobKzgCommitments.stream() + .map(c -> new SszKZGCommitment(c.asInternalKZGCommitment())) + .collect(schema.getBlobKzgCommitmentsSchema().collector()); + return schema.create( + parentBlockHash, + parentBlockRoot, + blockHash, + prevRandao, + feeRecipient, + gasLimit, + builderIndex, + slot, + value, + executionPayment, + sszBlobKzgCommitments, + executionRequestsRoot); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadEnvelope.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadEnvelope.java new file mode 100644 index 000000000..d90ec2e05 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadEnvelope.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 ConsenSys AG. + * + * 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.web3signer.core.service.http.handlers.signing.eth2.schema.gloas; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsElectra; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.electra.ExecutionRequests; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.tuweni.bytes.Bytes32; + +public class ExecutionPayloadEnvelope { + + private final ExecutionPayloadGloas payload; + private final ExecutionRequests executionRequests; + private final UInt64 builderIndex; + private final Bytes32 beaconBlockRoot; + + @JsonCreator + public ExecutionPayloadEnvelope( + @JsonProperty(value = "payload", required = true) final ExecutionPayloadGloas payload, + @JsonProperty(value = "execution_requests", required = true) + final ExecutionRequests executionRequests, + @JsonProperty(value = "builder_index", required = true) final UInt64 builderIndex, + @JsonProperty(value = "beacon_block_root", required = true) final Bytes32 beaconBlockRoot) { + this.payload = payload; + this.executionRequests = executionRequests; + this.builderIndex = builderIndex; + this.beaconBlockRoot = beaconBlockRoot; + } + + @JsonProperty("payload") + public ExecutionPayloadGloas getPayload() { + return payload; + } + + @JsonProperty("execution_requests") + public ExecutionRequests getExecutionRequests() { + return executionRequests; + } + + @JsonProperty("builder_index") + public UInt64 getBuilderIndex() { + return builderIndex; + } + + @JsonProperty("beacon_block_root") + public Bytes32 getBeaconBlockRoot() { + return beaconBlockRoot; + } + + /** The slot lives on the inner payload in Glamsterdam (ePBS); delegated for handler use. */ + public UInt64 getSlot() { + return payload.slotNumber; + } + + public tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope + asInternalExecutionPayloadEnvelope(final SpecVersion specVersion) { + final SchemaDefinitionsGloas gloasSchemas = + SchemaDefinitionsGloas.required(specVersion.getSchemaDefinitions()); + return gloasSchemas + .getExecutionPayloadEnvelopeSchema() + .create( + payload.asInternalExecutionPayload(specVersion), + executionRequests.asInternalConsolidationRequest( + SchemaDefinitionsElectra.required(specVersion.getSchemaDefinitions()) + .getExecutionRequestsSchema()), + builderIndex, + beaconBlockRoot); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadGloas.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadGloas.java new file mode 100644 index 000000000..64c31046a --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ExecutionPayloadGloas.java @@ -0,0 +1,140 @@ +/* + * Copyright 2026 ConsenSys AG. + * + * 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.web3signer.core.service.http.handlers.signing.eth2.schema.gloas; + +import tech.pegasys.teku.infrastructure.bytes.Bytes20; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadBuilder; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadSchema; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.capella.Withdrawal; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.deneb.ExecutionPayloadDeneb; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; + +public class ExecutionPayloadGloas extends ExecutionPayloadDeneb { + + @JsonProperty("block_access_list") + public final Bytes blockAccessList; + + @JsonProperty("slot_number") + public final UInt64 slotNumber; + + @JsonCreator + public ExecutionPayloadGloas( + @JsonProperty("parent_hash") final Bytes32 parentHash, + @JsonProperty("fee_recipient") final Bytes20 feeRecipient, + @JsonProperty("state_root") final Bytes32 stateRoot, + @JsonProperty("receipts_root") final Bytes32 receiptsRoot, + @JsonProperty("logs_bloom") final Bytes logsBloom, + @JsonProperty("prev_randao") final Bytes32 prevRandao, + @JsonProperty("block_number") final UInt64 blockNumber, + @JsonProperty("gas_limit") final UInt64 gasLimit, + @JsonProperty("gas_used") final UInt64 gasUsed, + @JsonProperty("timestamp") final UInt64 timestamp, + @JsonProperty("extra_data") final Bytes extraData, + @JsonProperty("base_fee_per_gas") final UInt256 baseFeePerGas, + @JsonProperty("block_hash") final Bytes32 blockHash, + @JsonProperty("transactions") final List transactions, + @JsonProperty("withdrawals") final List withdrawals, + @JsonProperty("blob_gas_used") final UInt64 blobGasUsed, + @JsonProperty("excess_blob_gas") final UInt64 excessBlobGas, + @JsonProperty("block_access_list") final Bytes blockAccessList, + @JsonProperty("slot_number") final UInt64 slotNumber) { + super( + parentHash, + feeRecipient, + stateRoot, + receiptsRoot, + logsBloom, + prevRandao, + blockNumber, + gasLimit, + gasUsed, + timestamp, + extraData, + baseFeePerGas, + blockHash, + transactions, + withdrawals, + blobGasUsed, + excessBlobGas); + this.blockAccessList = blockAccessList; + this.slotNumber = slotNumber; + } + + public ExecutionPayloadGloas( + final tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload executionPayload) { + super(executionPayload); + final tech.pegasys.teku.spec.datastructures.execution.versions.gloas.ExecutionPayloadGloas + gloasPayload = + tech.pegasys.teku.spec.datastructures.execution.versions.gloas.ExecutionPayloadGloas + .required(executionPayload); + this.blockAccessList = gloasPayload.getBlockAccessList().getBytes(); + this.slotNumber = gloasPayload.getSlotNumber(); + } + + @Override + protected ExecutionPayloadBuilder applyToBuilder( + final ExecutionPayloadSchema executionPayloadSchema, + final ExecutionPayloadBuilder builder) { + return super.applyToBuilder(executionPayloadSchema, builder) + .blockAccessList(() -> blockAccessList) + .slotNumber(() -> slotNumber); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof ExecutionPayloadGloas that)) return false; + if (!super.equals(o)) return false; + return Objects.equals(blockAccessList, that.blockAccessList) + && Objects.equals(slotNumber, that.slotNumber); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), blockAccessList, slotNumber); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("parentHash", parentHash) + .add("feeRecipient", feeRecipient) + .add("stateRoot", stateRoot) + .add("receiptsRoot", receiptsRoot) + .add("logsBloom", logsBloom) + .add("prevRandao", prevRandao) + .add("blockNumber", blockNumber) + .add("gasLimit", gasLimit) + .add("gasUsed", gasUsed) + .add("timestamp", timestamp) + .add("extraData", extraData) + .add("baseFeePerGas", baseFeePerGas) + .add("blockHash", blockHash) + .add("transactions", transactions) + .add("withdrawals", withdrawals) + .add("blobGasUsed", blobGasUsed) + .add("excessBlobGas", excessBlobGas) + .add("blockAccessList", blockAccessList) + .add("slotNumber", slotNumber) + .toString(); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/PayloadAttestationData.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/PayloadAttestationData.java new file mode 100644 index 000000000..686789a8c --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/PayloadAttestationData.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 ConsenSys AG. + * + * 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.web3signer.core.service.http.handlers.signing.eth2.schema.gloas; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.tuweni.bytes.Bytes32; + +public class PayloadAttestationData { + + private final Bytes32 beaconBlockRoot; + private final UInt64 slot; + private final boolean payloadPresent; + private final boolean blobDataAvailable; + + @JsonCreator + public PayloadAttestationData( + @JsonProperty(value = "beacon_block_root", required = true) final Bytes32 beaconBlockRoot, + @JsonProperty(value = "slot", required = true) final UInt64 slot, + @JsonProperty(value = "payload_present", required = true) final boolean payloadPresent, + @JsonProperty(value = "blob_data_available", required = true) + final boolean blobDataAvailable) { + this.beaconBlockRoot = beaconBlockRoot; + this.slot = slot; + this.payloadPresent = payloadPresent; + this.blobDataAvailable = blobDataAvailable; + } + + @JsonProperty("beacon_block_root") + public Bytes32 getBeaconBlockRoot() { + return beaconBlockRoot; + } + + @JsonProperty("slot") + public UInt64 getSlot() { + return slot; + } + + @JsonProperty("payload_present") + public boolean isPayloadPresent() { + return payloadPresent; + } + + @JsonProperty("blob_data_available") + public boolean isBlobDataAvailable() { + return blobDataAvailable; + } + + public tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationData + asInternalPayloadAttestationData(final SpecVersion specVersion) { + return SchemaDefinitionsGloas.required(specVersion.getSchemaDefinitions()) + .getPayloadAttestationDataSchema() + .create(beaconBlockRoot, slot, payloadPresent, blobDataAvailable); + } +} diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ProposerPreferences.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ProposerPreferences.java new file mode 100644 index 000000000..5beb561af --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/schema/gloas/ProposerPreferences.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 ConsenSys AG. + * + * 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.web3signer.core.service.http.handlers.signing.eth2.schema.gloas; + +import tech.pegasys.teku.ethereum.execution.types.Eth1Address; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ProposerPreferences { + + private final UInt64 proposalSlot; + private final UInt64 validatorIndex; + private final Eth1Address feeRecipient; + private final UInt64 gasLimit; + + @JsonCreator + public ProposerPreferences( + @JsonProperty(value = "proposal_slot", required = true) final UInt64 proposalSlot, + @JsonProperty(value = "validator_index", required = true) final UInt64 validatorIndex, + @JsonProperty(value = "fee_recipient", required = true) final Eth1Address feeRecipient, + @JsonProperty(value = "gas_limit", required = true) final UInt64 gasLimit) { + this.proposalSlot = proposalSlot; + this.validatorIndex = validatorIndex; + this.feeRecipient = feeRecipient; + this.gasLimit = gasLimit; + } + + @JsonProperty("proposal_slot") + public UInt64 getProposalSlot() { + return proposalSlot; + } + + @JsonProperty("validator_index") + public UInt64 getValidatorIndex() { + return validatorIndex; + } + + @JsonProperty("fee_recipient") + public Eth1Address getFeeRecipient() { + return feeRecipient; + } + + @JsonProperty("gas_limit") + public UInt64 getGasLimit() { + return gasLimit; + } + + public tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferences + asInternalProposerPreferences(final SpecVersion specVersion) { + return SchemaDefinitionsGloas.required(specVersion.getSchemaDefinitions()) + .getProposerPreferencesSchema() + .create(proposalSlot, validatorIndex, feeRecipient, gasLimit); + } +} diff --git a/gradle/versions.gradle b/gradle/versions.gradle index ed2c6e64a..7668e0754 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -86,7 +86,7 @@ dependencyManagement { dependency 'org.xipki.iaik:sunpkcs11-wrapper:1.4.10' - dependencySet(group: 'tech.pegasys.teku.internal', version: '26.4.0') { + dependencySet(group: 'tech.pegasys.teku.internal', version: 'develop') { entry ('bls') { exclude group: 'org.bouncycastle', name: 'bcprov-jdk15on' } diff --git a/openapi-specs/eth2/signing/paths/sign.yaml b/openapi-specs/eth2/signing/paths/sign.yaml index 58c9f0d8a..d71385d2f 100644 --- a/openapi-specs/eth2/signing/paths/sign.yaml +++ b/openapi-specs/eth2/signing/paths/sign.yaml @@ -31,6 +31,10 @@ post: - $ref: '../schemas.yaml#/components/schemas/SyncCommitteeSelectionProofSigning' - $ref: '../schemas.yaml#/components/schemas/SyncCommitteeContributionAndProofSigning' - $ref: '../schemas.yaml#/components/schemas/ValidatorRegistrationSigning' + - $ref: '../schemas.yaml#/components/schemas/ExecutionPayloadBidSigning' + - $ref: '../schemas.yaml#/components/schemas/ExecutionPayloadEnvelopeSigning' + - $ref: '../schemas.yaml#/components/schemas/PayloadAttestationMessageSigning' + - $ref: '../schemas.yaml#/components/schemas/ProposerPreferencesSigning' discriminator: propertyName: type mapping: @@ -47,6 +51,10 @@ post: SYNC_COMMITTEE_SELECTION_PROOF: '../schemas.yaml#/components/schemas/SyncCommitteeSelectionProofSigning' SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF: '../schemas.yaml#/components/schemas/SyncCommitteeContributionAndProofSigning' VALIDATOR_REGISTRATION: '../schemas.yaml#/components/schemas/ValidatorRegistrationSigning' + EXECUTION_PAYLOAD_BID: '../schemas.yaml#/components/schemas/ExecutionPayloadBidSigning' + EXECUTION_PAYLOAD_ENVELOPE: '../schemas.yaml#/components/schemas/ExecutionPayloadEnvelopeSigning' + PAYLOAD_ATTESTATION_MESSAGE: '../schemas.yaml#/components/schemas/PayloadAttestationMessageSigning' + PROPOSER_PREFERENCES: '../schemas.yaml#/components/schemas/ProposerPreferencesSigning' examples: BLOCK_V2 (FULU): value: diff --git a/openapi-specs/eth2/signing/schemas.yaml b/openapi-specs/eth2/signing/schemas.yaml index 4c33907fc..c32269153 100644 --- a/openapi-specs/eth2/signing/schemas.yaml +++ b/openapi-specs/eth2/signing/schemas.yaml @@ -341,6 +341,66 @@ components: required: - type - validator_registration + ExecutionPayloadBidSigning: + allOf: + - $ref: '#/components/schemas/Signing' + - type: object + properties: + type: + type: "string" + description: Signing Request type + enum: + - 'EXECUTION_PAYLOAD_BID' + execution_payload_bid: + "$ref": "#/components/schemas/ExecutionPayloadBid" + required: + - type + - execution_payload_bid + ExecutionPayloadEnvelopeSigning: + allOf: + - $ref: '#/components/schemas/Signing' + - type: object + properties: + type: + type: "string" + description: Signing Request type + enum: + - 'EXECUTION_PAYLOAD_ENVELOPE' + execution_payload_envelope: + "$ref": "#/components/schemas/ExecutionPayloadEnvelope" + required: + - type + - execution_payload_envelope + PayloadAttestationMessageSigning: + allOf: + - $ref: '#/components/schemas/Signing' + - type: object + properties: + type: + type: "string" + description: Signing Request type + enum: + - 'PAYLOAD_ATTESTATION_MESSAGE' + payload_attestation_message: + "$ref": "#/components/schemas/PayloadAttestationData" + required: + - type + - payload_attestation_message + ProposerPreferencesSigning: + allOf: + - $ref: '#/components/schemas/Signing' + - type: object + properties: + type: + type: "string" + description: Signing Request type + enum: + - 'PROPOSER_PREFERENCES' + proposer_preferences: + "$ref": "#/components/schemas/ProposerPreferences" + required: + - type + - proposer_preferences RandaoReveal: type: "object" properties: @@ -632,6 +692,174 @@ components: format: uint64 pubkey: type: string + ExecutionPayloadBid: + type: object + description: >- + SSZ container signed by a builder using DOMAIN_BEACON_BUILDER (0x0B000000) + in Glamsterdam (ePBS) for proposing an execution payload bid. + properties: + parent_block_hash: + type: string + description: Bytes32 hexadecimal + parent_block_root: + type: string + description: Bytes32 hexadecimal + block_hash: + type: string + description: Bytes32 hexadecimal + prev_randao: + type: string + description: Bytes32 hexadecimal + fee_recipient: + type: string + description: Bytes20 hexadecimal + gas_limit: + type: string + format: uint64 + builder_index: + type: string + format: uint64 + slot: + type: string + format: uint64 + value: + type: string + format: uint64 + execution_payment: + type: string + format: uint64 + blob_kzg_commitments: + type: array + items: + type: string + description: Bytes48 hexadecimal KZG commitment + execution_requests_root: + type: string + description: Bytes32 hash-tree-root of the execution requests + ExecutionPayloadEnvelope: + type: object + description: >- + SSZ container signed by a builder using DOMAIN_BEACON_BUILDER (0x0B000000) + in Glamsterdam (ePBS) when revealing the actual execution payload. + properties: + payload: + description: 'Execution payload (Glamsterdam shape, see ExecutionPayloadGloas)' + $ref: '#/components/schemas/ExecutionPayloadGloas' + execution_requests: + description: 'Execution requests (deposits, withdrawals, consolidations)' + type: object + builder_index: + type: string + format: uint64 + beacon_block_root: + type: string + description: Bytes32 hexadecimal + ExecutionPayloadGloas: + type: object + description: >- + Glamsterdam execution payload. Extends the Deneb execution payload with + block_access_list and slot_number fields (slot moved from envelope into + the payload in ePBS). + properties: + parent_hash: + type: string + description: Bytes32 hexadecimal + fee_recipient: + type: string + description: Bytes20 hexadecimal + state_root: + type: string + description: Bytes32 hexadecimal + receipts_root: + type: string + description: Bytes32 hexadecimal + logs_bloom: + type: string + description: Bytes256 hexadecimal + prev_randao: + type: string + description: Bytes32 hexadecimal + block_number: + type: string + format: uint64 + gas_limit: + type: string + format: uint64 + gas_used: + type: string + format: uint64 + timestamp: + type: string + format: uint64 + extra_data: + type: string + description: variable-length bytes + base_fee_per_gas: + type: string + description: UInt256 decimal + block_hash: + type: string + description: Bytes32 hexadecimal + transactions: + type: array + items: + type: string + description: variable-length bytes (RLP-encoded transaction) + withdrawals: + type: array + items: + type: object + blob_gas_used: + type: string + format: uint64 + excess_blob_gas: + type: string + format: uint64 + block_access_list: + type: string + description: variable-length bytes (block access list) + slot_number: + type: string + format: uint64 + PayloadAttestationData: + type: object + description: >- + The data component of a PayloadAttestationMessage, signed by a PTC + validator using DOMAIN_PTC_ATTESTER (0x0C000000). + properties: + beacon_block_root: + type: string + description: Bytes32 hexadecimal + example: '0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2' + slot: + type: string + format: uint64 + example: '1' + payload_present: + type: boolean + description: Whether the execution payload for this slot is present + blob_data_available: + type: boolean + description: Whether blob data is available for this slot + ProposerPreferences: + type: object + description: >- + Container signed by a proposer using DOMAIN_PROPOSER_PREFERENCES + (0x0D000000) to advertise their preferences (fee_recipient, gas_limit) + for an upcoming proposal slot. + properties: + proposal_slot: + type: string + format: uint64 + validator_index: + type: string + format: uint64 + fee_recipient: + type: string + description: Bytes20 hexadecimal + gas_limit: + type: string + format: uint64 BeaconBlockSigning: allOf: - $ref: '#/components/schemas/Signing' From c2a2e1ed13d462bbdebc2391ed579471b7aeb747 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 6 May 2026 15:22:09 +1000 Subject: [PATCH 2/6] openapi: inline Withdrawal field types in ExecutionPayloadGloas --- openapi-specs/eth2/signing/schemas.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openapi-specs/eth2/signing/schemas.yaml b/openapi-specs/eth2/signing/schemas.yaml index c32269153..3bc5be5cc 100644 --- a/openapi-specs/eth2/signing/schemas.yaml +++ b/openapi-specs/eth2/signing/schemas.yaml @@ -807,8 +807,22 @@ components: description: variable-length bytes (RLP-encoded transaction) withdrawals: type: array + description: List of validator withdrawals (Capella+). items: type: object + properties: + index: + type: string + format: uint64 + validator_index: + type: string + format: uint64 + address: + type: string + description: Bytes20 hexadecimal + amount: + type: string + format: uint64 blob_gas_used: type: string format: uint64 From c6db7a7b0ab57faf93200b7cb3567cc67fd918aa Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 6 May 2026 15:24:18 +1000 Subject: [PATCH 3/6] openapi: bump eth2 spec version to 2.1.0 for Glamsterdam additions --- openapi-specs/eth2/web3signer.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi-specs/eth2/web3signer.yaml b/openapi-specs/eth2/web3signer.yaml index 74a60ba17..f5ed2dd95 100644 --- a/openapi-specs/eth2/web3signer.yaml +++ b/openapi-specs/eth2/web3signer.yaml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: 'Web3Signer ETH2 Api' description: 'Sign Eth2 Artifacts' - version: '2.0.0' + version: '2.1.0' license: name: 'Apache 2.0' url: 'http://www.apache.org/licenses/LICENSE-2.0.html' From 68fdaade38e42621fac6b14e82a5870f89ff5665 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 6 May 2026 15:36:19 +1000 Subject: [PATCH 4/6] Use imports instead of fully-qualified names for non-conflicting types --- .../dsl/utils/Eth2RequestUtils.java | 4 ++-- .../eth2/Eth2SignForIdentifierHandler.java | 20 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java index 2a14b1357..1596e6576 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/Eth2RequestUtils.java @@ -16,6 +16,7 @@ import static tech.pegasys.web3signer.core.util.DepositSigningRootUtil.computeDomain; import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.bytes.Bytes20; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; @@ -458,8 +459,7 @@ private static Eth2SigningRequestBody createExecutionPayloadBidRequest() { randomBid.getParentBlockRoot(), randomBid.getBlockHash(), randomBid.getPrevRandao(), - new tech.pegasys.teku.infrastructure.bytes.Bytes20( - randomBid.getFeeRecipient().getWrappedBytes()), + new Bytes20(randomBid.getFeeRecipient().getWrappedBytes()), randomBid.getGasLimit(), randomBid.getBuilderIndex(), randomBid.getSlot(), diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java index 55b3552d2..d898c9efc 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/signing/eth2/Eth2SignForIdentifierHandler.java @@ -30,6 +30,10 @@ import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.AttestationData; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.altair.ContributionAndProof; import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.altair.SyncCommitteeContribution; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadBid; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ExecutionPayloadEnvelope; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.PayloadAttestationData; +import tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas.ProposerPreferences; import tech.pegasys.web3signer.core.service.http.metrics.HttpApiMetrics; import tech.pegasys.web3signer.core.util.DepositSigningRootUtil; import tech.pegasys.web3signer.slashingprotection.SlashingProtection; @@ -320,27 +324,21 @@ private Bytes computeSigningRoot(final Eth2SigningRequestBody body) { validatorRegistration.asInternalValidatorRegistration()); } case EXECUTION_PAYLOAD_BID -> { - final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas - .ExecutionPayloadBid - bid = body.executionPayloadBid(); + final ExecutionPayloadBid bid = body.executionPayloadBid(); checkArgument(bid != null, "executionPayloadBid is required"); return signingRootUtil.signingRootForSignExecutionPayloadBid( bid.asInternalExecutionPayloadBid(eth2Spec.atSlot(bid.getSlot())), body.forkInfo().asInternalForkInfo()); } case EXECUTION_PAYLOAD_ENVELOPE -> { - final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas - .ExecutionPayloadEnvelope - envelope = body.executionPayloadEnvelope(); + final ExecutionPayloadEnvelope envelope = body.executionPayloadEnvelope(); checkArgument(envelope != null, "executionPayloadEnvelope is required"); return signingRootUtil.signingRootForSignExecutionPayloadEnvelope( envelope.asInternalExecutionPayloadEnvelope(eth2Spec.atSlot(envelope.getSlot())), body.forkInfo().asInternalForkInfo()); } case PAYLOAD_ATTESTATION_MESSAGE -> { - final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas - .PayloadAttestationData - payloadAttestationData = body.payloadAttestationMessage(); + final PayloadAttestationData payloadAttestationData = body.payloadAttestationMessage(); checkArgument(payloadAttestationData != null, "payloadAttestationMessage is required"); return signingRootUtil.signingRootForSignPayloadAttestationData( payloadAttestationData.asInternalPayloadAttestationData( @@ -348,9 +346,7 @@ private Bytes computeSigningRoot(final Eth2SigningRequestBody body) { body.forkInfo().asInternalForkInfo()); } case PROPOSER_PREFERENCES -> { - final tech.pegasys.web3signer.core.service.http.handlers.signing.eth2.schema.gloas - .ProposerPreferences - proposerPreferences = body.proposerPreferences(); + final ProposerPreferences proposerPreferences = body.proposerPreferences(); checkArgument(proposerPreferences != null, "proposerPreferences is required"); return signingRootUtil.signingRootForSignProposerPreferences( proposerPreferences.asInternalProposerPreferences( From 2b426c400e7e513fd7dcc5d94755904ebe2153bc Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 6 May 2026 15:37:09 +1000 Subject: [PATCH 5/6] docs: add CHANGELOG entry for GLOAS signing support --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3f546c5..71f015ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - Docker images are unchanged — they have shipped Java 25 since 25.12.0. - Contributors no longer need to install JDK 25 manually. The build now uses a Gradle toolchain (`JavaLanguageVersion.of(25)`) with the foojay resolver, so Gradle will auto-detect a locally installed JDK 25 and download Temurin 25 if none is found. The Gradle daemon itself can run on any JDK supported by Gradle 9 (17+). +### Features Added +- Initial signing support for the upcoming Glamsterdam (GLOAS / ePBS) fork. Adds four new sign types — `EXECUTION_PAYLOAD_BID`, `EXECUTION_PAYLOAD_ENVELOPE`, `PAYLOAD_ATTESTATION_MESSAGE`, `PROPOSER_PREFERENCES` — together with the corresponding OpenAPI schemas (eth2 spec bumped to `2.1.0`). Field shapes track Teku `develop` and are subject to change until the next Teku release pins the schemas. + --- ## 26.4.2 ### Features Added From ec8d5fc21ffe6c1395ab44cd9e93927f47bd4f2c Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 6 May 2026 15:37:59 +1000 Subject: [PATCH 6/6] docs: shorten CHANGELOG entry, link PR --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71f015ce7..9789a5c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ - Contributors no longer need to install JDK 25 manually. The build now uses a Gradle toolchain (`JavaLanguageVersion.of(25)`) with the foojay resolver, so Gradle will auto-detect a locally installed JDK 25 and download Temurin 25 if none is found. The Gradle daemon itself can run on any JDK supported by Gradle 9 (17+). ### Features Added -- Initial signing support for the upcoming Glamsterdam (GLOAS / ePBS) fork. Adds four new sign types — `EXECUTION_PAYLOAD_BID`, `EXECUTION_PAYLOAD_ENVELOPE`, `PAYLOAD_ATTESTATION_MESSAGE`, `PROPOSER_PREFERENCES` — together with the corresponding OpenAPI schemas (eth2 spec bumped to `2.1.0`). Field shapes track Teku `develop` and are subject to change until the next Teku release pins the schemas. +- Initial signing support for the upcoming Glamsterdam (GLOAS / ePBS) fork. Subject to change until the next Teku release pins the schemas. PR [#1192][PR_1192]. + +[PR_1192]: https://github.com/Consensys/web3signer/pull/1192 --- ## 26.4.2