diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/config/GetSpecTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/config/GetSpecTest.java index b3a566d0804..31c80da750f 100644 --- a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/config/GetSpecTest.java +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/v1/config/GetSpecTest.java @@ -18,12 +18,12 @@ import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; -import static tech.pegasys.teku.infrastructure.json.JsonTestUtil.parseStringMap; import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.Resources; -import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.pegasys.teku.api.ConfigProvider; @@ -64,12 +64,17 @@ void shouldGetCorrectMainnetConfig() throws Exception { setHandler(new GetSpec(configProvider)); handler.handleRequest(request); - final Map result = (Map) request.getResponseBody(); - final Map expected = - parseStringMap( + final String json = request.getResponseBodyAsJson(handler); + + assertThat(json).contains("BLOB_SCHEDULE"); + assertThat(json).isNotEmpty(); + final ObjectMapper mapper = new ObjectMapper(); + final JsonNode resultNode = mapper.readTree(json).get("data"); + final JsonNode referenceNode = + mapper.readTree( Resources.toString( Resources.getResource(GetSpecTest.class, "mainnetConfig.json"), UTF_8)); - assertThat(result).containsExactlyInAnyOrderEntriesOf(expected); + assertThat(resultNode).isEqualTo(referenceNode); } } diff --git a/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json b/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json index c64fb39aebe..014b00261d9 100644 --- a/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json +++ b/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json @@ -159,5 +159,9 @@ "SAMPLES_PER_SLOT":"8", "CUSTODY_REQUIREMENT":"4", "VALIDATOR_CUSTODY_REQUIREMENT":"8", -"BALANCE_PER_ADDITIONAL_CUSTODY_GROUP":"32000000000" +"BALANCE_PER_ADDITIONAL_CUSTODY_GROUP":"32000000000", +"BLOB_SCHEDULE":[ + {"EPOCH": "269568", "MAX_BLOBS_PER_BLOCK": "6"}, + {"EPOCH": "364032", "MAX_BLOBS_PER_BLOCK": "9"} + ] } \ No newline at end of file diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/SpecConfigData.java b/data/provider/src/main/java/tech/pegasys/teku/api/SpecConfigData.java index ca5efc1a38e..f9fe8b6f140 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/SpecConfigData.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/SpecConfigData.java @@ -14,6 +14,7 @@ package tech.pegasys.teku.api; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.logging.log4j.LogManager; @@ -33,13 +34,17 @@ public SpecConfigData(final SpecConfig specConfig) { this.specConfig = specConfig; } + @SuppressWarnings("unchecked") public Map getConfigMap() { final Map configAttributes = new HashMap<>(); specConfig .getRawConfig() .forEach( (name, value) -> { - if (value != null) { + if (value instanceof List) { + LOG.debug("Config field {} is a list", name); + configAttributes.put(name, value); + } else if (value != null) { configAttributes.put(name, ConfigProvider.formatValue(value)); } else { LOG.warn("Config field {} was set to null in runtime configuration", name); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/BlobSchedule.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/BlobSchedule.java new file mode 100644 index 00000000000..0ae7aa18955 --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/BlobSchedule.java @@ -0,0 +1,18 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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.spec.config; + +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +public record BlobSchedule(UInt64 epoch, int maxBlobsPerBlock) {} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/DelegatingSpecConfigFulu.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/DelegatingSpecConfigFulu.java index f00052b96f7..dbb8964bbd6 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/DelegatingSpecConfigFulu.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/DelegatingSpecConfigFulu.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.config; +import java.util.List; import java.util.Objects; import java.util.Optional; import tech.pegasys.teku.infrastructure.bytes.Bytes4; @@ -52,6 +53,11 @@ public UInt64 getFieldElementsPerExtBlob() { return delegate.getFieldElementsPerExtBlob(); } + @Override + public List getBlobSchedule() { + return delegate.getBlobSchedule(); + } + @Override public UInt64 getKzgCommitmentsInclusionProofDepth() { return delegate.getKzgCommitmentsInclusionProofDepth(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFulu.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFulu.java index e4e271cb8c8..f34c1a995ae 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFulu.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFulu.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.config; +import java.util.List; import java.util.Optional; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -36,6 +37,8 @@ static SpecConfigFulu required(final SpecConfig specConfig) { UInt64 getFieldElementsPerExtBlob(); + List getBlobSchedule(); + /** DataColumnSidecar's */ UInt64 getKzgCommitmentsInclusionProofDepth(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFuluImpl.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFuluImpl.java index e434136628e..cb29a1243f0 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFuluImpl.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigFuluImpl.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.config; +import java.util.List; import java.util.Objects; import java.util.Optional; import tech.pegasys.teku.infrastructure.bytes.Bytes4; @@ -37,6 +38,7 @@ public class SpecConfigFuluImpl extends DelegatingSpecConfigElectra implements S private final int maxRequestDataColumnSidecars; private final int maxBlobsPerBlockFulu; private final UInt64 balancePerAdditionalCustodyGroup; + private final List blobSchedule; public SpecConfigFuluImpl( final SpecConfigElectra specConfig, @@ -54,7 +56,8 @@ public SpecConfigFuluImpl( final int minEpochsForDataColumnSidecarsRequests, final int maxRequestDataColumnSidecars, final int maxBlobsPerBlockFulu, - final UInt64 balancePerAdditionalCustodyGroup) { + final UInt64 balancePerAdditionalCustodyGroup, + final List blobSchedule) { super(specConfig); this.fuluForkVersion = fuluForkVersion; this.fuluForkEpoch = fuluForkEpoch; @@ -71,6 +74,7 @@ public SpecConfigFuluImpl( this.maxRequestDataColumnSidecars = maxRequestDataColumnSidecars; this.maxBlobsPerBlockFulu = maxBlobsPerBlockFulu; this.balancePerAdditionalCustodyGroup = balancePerAdditionalCustodyGroup; + this.blobSchedule = blobSchedule; } @Override @@ -93,6 +97,11 @@ public UInt64 getFieldElementsPerExtBlob() { return fieldElementsPerExtBlob; } + @Override + public List getBlobSchedule() { + return blobSchedule; + } + @Override public UInt64 getKzgCommitmentsInclusionProofDepth() { return kzgCommitmentsInclusionProofDepth; diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigReader.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigReader.java index b38d4568c15..86344250735 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigReader.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigReader.java @@ -26,8 +26,10 @@ import java.lang.reflect.Modifier; import java.math.BigInteger; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; @@ -105,9 +107,34 @@ public class SpecConfigReader { .put(Bytes.class, fromString(Bytes::fromHexString)) .put(Bytes4.class, fromString(Bytes4::fromHexString)) .put(Bytes32.class, fromString(Bytes32::fromHexStringStrict)) + .put(List.class, this::blobScheduleFromList) .put(Eth1Address.class, fromString(Eth1Address::fromHexString)) .build(); + @SuppressWarnings("unchecked") + private Object blobScheduleFromList(final Object o) { + final List blobSchedule = new ArrayList<>(); + final List schedule = (List) o; + for (Object entry : schedule) { + if (entry instanceof Map) { + final Map data = (Map) entry; + if (!data.containsKey("EPOCH") + || !data.containsKey("MAX_BLOBS_PER_BLOCK") + || data.size() != 2) { + throw new IllegalArgumentException("Map does not look like a blob schedule"); + } + blobSchedule.add( + new BlobSchedule( + UInt64.valueOf(data.get("EPOCH")), + Integer.parseInt(data.get("MAX_BLOBS_PER_BLOCK")))); + + } else { + throw new IllegalArgumentException("Could not parse entry blob schedule"); + } + } + return blobSchedule; + } + final SpecConfigBuilder configBuilder = SpecConfig.builder(); public SpecConfigAndParent build() { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/DenebBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/DenebBuilder.java index 2cac41aa0b8..f0bd783ab44 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/DenebBuilder.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/DenebBuilder.java @@ -20,8 +20,11 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.config.BlobSchedule; import tech.pegasys.teku.spec.config.SpecConfig; import tech.pegasys.teku.spec.config.SpecConfigAndParent; import tech.pegasys.teku.spec.config.SpecConfigCapella; @@ -29,7 +32,7 @@ import tech.pegasys.teku.spec.config.SpecConfigDenebImpl; public class DenebBuilder implements ForkConfigBuilder { - + private static final Logger LOG = LogManager.getLogger(); private Bytes4 denebForkVersion; private UInt64 denebForkEpoch; @@ -67,6 +70,14 @@ public SpecConfigAndParent build( specConfigAndParent); } + public Optional getBlobSchedule() { + if (denebForkEpoch == null || maxBlobsPerBlock == null) { + LOG.debug("denebForkEpoch = {}, maxBlobsPerBlock = {}", denebForkEpoch, maxBlobsPerBlock); + return Optional.empty(); + } + return Optional.of(new BlobSchedule(denebForkEpoch, maxBlobsPerBlock)); + } + public DenebBuilder denebForkEpoch(final UInt64 denebForkEpoch) { checkNotNull(denebForkEpoch); this.denebForkEpoch = denebForkEpoch; diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/ElectraBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/ElectraBuilder.java index c6c1623c938..4eda71af890 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/ElectraBuilder.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/ElectraBuilder.java @@ -18,9 +18,13 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.function.BiConsumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.config.BlobSchedule; import tech.pegasys.teku.spec.config.SpecConfig; import tech.pegasys.teku.spec.config.SpecConfigAndParent; import tech.pegasys.teku.spec.config.SpecConfigDeneb; @@ -28,7 +32,7 @@ import tech.pegasys.teku.spec.config.SpecConfigElectraImpl; public class ElectraBuilder implements ForkConfigBuilder { - + private static final Logger LOG = LogManager.getLogger(); private Bytes4 electraForkVersion; private UInt64 electraForkEpoch; @@ -252,6 +256,17 @@ public Map getValidationMap() { return constants; } + public Optional getBlobSchedule() { + if (maxBlobsPerBlockElectra == null || electraForkEpoch == null) { + LOG.debug( + "electraForkEpoch = {}, maxBlobsPerBlockElectra = {}", + electraForkEpoch, + maxBlobsPerBlockElectra); + return Optional.empty(); + } + return Optional.of(new BlobSchedule(electraForkEpoch, maxBlobsPerBlockElectra)); + } + @Override public void addOverridableItemsToRawConfig(final BiConsumer rawConfig) { rawConfig.accept("ELECTRA_FORK_EPOCH", electraForkEpoch); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/FuluBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/FuluBuilder.java index 490876d9208..94ffb8008b3 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/FuluBuilder.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/FuluBuilder.java @@ -16,11 +16,15 @@ import static com.google.common.base.Preconditions.checkNotNull; import static tech.pegasys.teku.spec.config.SpecConfig.FAR_FUTURE_EPOCH; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.BiConsumer; import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.config.BlobSchedule; import tech.pegasys.teku.spec.config.SpecConfig; import tech.pegasys.teku.spec.config.SpecConfigAndParent; import tech.pegasys.teku.spec.config.SpecConfigElectra; @@ -45,6 +49,7 @@ public class FuluBuilder implements ForkConfigBuilder blobSchedule = new ArrayList<>(); FuluBuilder() {} @@ -68,7 +73,8 @@ public SpecConfigAndParent build( minEpochsForDataColumnSidecarsRequests, maxRequestDataColumnSidecars, maxBlobsPerBlockFulu, - balancePerAdditionalCustodyGroup), + balancePerAdditionalCustodyGroup, + blobSchedule), specConfigAndParent); } @@ -103,6 +109,11 @@ public FuluBuilder kzgCommitmentsInclusionProofDepth( return this; } + public FuluBuilder blobSchedule(final List blobSchedule) { + this.blobSchedule = blobSchedule; + return this; + } + public FuluBuilder numberOfColumns(final Integer numberOfColumns) { checkNotNull(numberOfColumns); this.numberOfColumns = numberOfColumns; @@ -180,6 +191,22 @@ public void validate() { validateConstants(); } + public void validateBlobSchedule( + final Optional denebSchedule, final Optional electraSchedule) { + denebSchedule.ifPresent( + schedule -> { + if (!blobSchedule.contains(schedule)) { + blobSchedule.add(schedule); + } + }); + electraSchedule.ifPresent( + schedule -> { + if (!blobSchedule.contains(schedule)) { + blobSchedule.add(schedule); + } + }); + } + @Override public Map getValidationMap() { final Map constants = new HashMap<>(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/SpecConfigBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/SpecConfigBuilder.java index 7a9f6bb6935..96626804ace 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/SpecConfigBuilder.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/SpecConfigBuilder.java @@ -129,15 +129,18 @@ public class SpecConfigBuilder { private Integer reorgHeadWeightThreshold = 20; private Integer reorgParentWeightThreshold = 160; + private final DenebBuilder denebBuilder = new DenebBuilder(); + private final ElectraBuilder electraBuilder = new ElectraBuilder(); + private final FuluBuilder fuluBuilder = new FuluBuilder(); private UInt64 maxPerEpochActivationExitChurnLimit = UInt64.valueOf(256000000000L); private final BuilderChain builderChain = BuilderChain.create(new AltairBuilder()) .appendBuilder(new BellatrixBuilder()) .appendBuilder(new CapellaBuilder()) - .appendBuilder(new DenebBuilder()) - .appendBuilder(new ElectraBuilder()) - .appendBuilder(new FuluBuilder()); + .appendBuilder(denebBuilder) + .appendBuilder(electraBuilder) + .appendBuilder(fuluBuilder); public SpecConfigAndParent build() { builderChain.addOverridableItemsToRawConfig( @@ -321,7 +324,8 @@ private void validate() { "The specified network configuration had missing or invalid values for constants %s", String.join(", ", fieldsFailingValidation))); } - + fuluBuilder.validateBlobSchedule( + denebBuilder.getBlobSchedule(), electraBuilder.getBlobSchedule()); builderChain.validate(); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/deneb/block/BlockProcessorDeneb.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/deneb/block/BlockProcessorDeneb.java index 436b120ed6c..622de079ee9 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/deneb/block/BlockProcessorDeneb.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/deneb/block/BlockProcessorDeneb.java @@ -70,13 +70,17 @@ public BlockProcessorDeneb( SchemaDefinitionsCapella.required(schemaDefinitions)); } + public int getMaxBlobsPerBlock(final BeaconState state) { + return SpecConfigDeneb.required(specConfig).getMaxBlobsPerBlock(); + } + @Override public void validateExecutionPayload( final BeaconState genericState, final BeaconBlockBody beaconBlockBody, final Optional payloadExecutor) throws BlockProcessingException { - final int maxBlobsPerBlock = SpecConfigDeneb.required(specConfig).getMaxBlobsPerBlock(); + final int maxBlobsPerBlock = getMaxBlobsPerBlock(genericState); final SszList blobKzgCommitments = extractBlobKzgCommitments(beaconBlockBody); if (blobKzgCommitments.size() > maxBlobsPerBlock) { throw new BlockProcessingException( diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/SpecLogicFulu.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/SpecLogicFulu.java index 337ecec24fa..6669ca4d885 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/SpecLogicFulu.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/SpecLogicFulu.java @@ -37,7 +37,6 @@ import tech.pegasys.teku.spec.logic.versions.capella.operations.validation.OperationValidatorCapella; import tech.pegasys.teku.spec.logic.versions.deneb.helpers.MiscHelpersDeneb; import tech.pegasys.teku.spec.logic.versions.deneb.util.ForkChoiceUtilDeneb; -import tech.pegasys.teku.spec.logic.versions.electra.block.BlockProcessorElectra; import tech.pegasys.teku.spec.logic.versions.electra.helpers.BeaconStateAccessorsElectra; import tech.pegasys.teku.spec.logic.versions.electra.helpers.BeaconStateMutatorsElectra; import tech.pegasys.teku.spec.logic.versions.electra.helpers.PredicatesElectra; @@ -45,6 +44,7 @@ import tech.pegasys.teku.spec.logic.versions.electra.operations.validation.VoluntaryExitValidatorElectra; import tech.pegasys.teku.spec.logic.versions.electra.statetransition.epoch.EpochProcessorElectra; import tech.pegasys.teku.spec.logic.versions.electra.util.AttestationUtilElectra; +import tech.pegasys.teku.spec.logic.versions.fulu.block.BlockProcessorFulu; import tech.pegasys.teku.spec.logic.versions.fulu.forktransition.FuluStateUpgrade; import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsFulu; @@ -65,7 +65,7 @@ private SpecLogicFulu( final OperationValidator operationValidator, final ValidatorStatusFactoryAltair validatorStatusFactory, final EpochProcessorElectra epochProcessor, - final BlockProcessorElectra blockProcessor, + final BlockProcessorFulu blockProcessor, final ForkChoiceUtil forkChoiceUtil, final BlockProposalUtil blockProposalUtil, final BlindBlockUtil blindBlockUtil, @@ -155,8 +155,8 @@ public static SpecLogicFulu create( new LightClientUtil(beaconStateAccessors, syncCommitteeUtil, schemaDefinitions); final ExecutionRequestsDataCodec executionRequestsDataCodec = new ExecutionRequestsDataCodec(schemaDefinitions.getExecutionRequestsSchema()); - final BlockProcessorElectra blockProcessor = - new BlockProcessorElectra( + final BlockProcessorFulu blockProcessor = + new BlockProcessorFulu( config, predicates, miscHelpers, diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/block/BlockProcessorFulu.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/block/BlockProcessorFulu.java new file mode 100644 index 00000000000..797397919fc --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/block/BlockProcessorFulu.java @@ -0,0 +1,70 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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.spec.logic.versions.fulu.block; + +import tech.pegasys.teku.spec.config.SpecConfigElectra; +import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionRequestsDataCodec; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.helpers.Predicates; +import tech.pegasys.teku.spec.logic.common.operations.OperationSignatureVerifier; +import tech.pegasys.teku.spec.logic.common.operations.validation.OperationValidator; +import tech.pegasys.teku.spec.logic.common.util.AttestationUtil; +import tech.pegasys.teku.spec.logic.common.util.BeaconStateUtil; +import tech.pegasys.teku.spec.logic.common.util.SyncCommitteeUtil; +import tech.pegasys.teku.spec.logic.common.util.ValidatorsUtil; +import tech.pegasys.teku.spec.logic.versions.electra.block.BlockProcessorElectra; +import tech.pegasys.teku.spec.logic.versions.electra.helpers.BeaconStateAccessorsElectra; +import tech.pegasys.teku.spec.logic.versions.electra.helpers.BeaconStateMutatorsElectra; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsElectra; + +public class BlockProcessorFulu extends BlockProcessorElectra { + final MiscHelpersFulu miscHelpersFulu; + + public BlockProcessorFulu( + final SpecConfigElectra specConfig, + final Predicates predicates, + final MiscHelpersFulu miscHelpers, + final SyncCommitteeUtil syncCommitteeUtil, + final BeaconStateAccessorsElectra beaconStateAccessors, + final BeaconStateMutatorsElectra beaconStateMutators, + final OperationSignatureVerifier operationSignatureVerifier, + final BeaconStateUtil beaconStateUtil, + final AttestationUtil attestationUtil, + final ValidatorsUtil validatorsUtil, + final OperationValidator operationValidator, + final SchemaDefinitionsElectra schemaDefinitions, + final ExecutionRequestsDataCodec executionRequestsDataCodec) { + super( + specConfig, + predicates, + miscHelpers, + syncCommitteeUtil, + beaconStateAccessors, + beaconStateMutators, + operationSignatureVerifier, + beaconStateUtil, + attestationUtil, + validatorsUtil, + operationValidator, + schemaDefinitions, + executionRequestsDataCodec); + miscHelpersFulu = miscHelpers; + } + + @Override + public int getMaxBlobsPerBlock(final BeaconState state) { + return miscHelpersFulu.getMaxBlobsPerBlock(miscHelpers.computeEpochAtSlot(state.getSlot())); + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/MiscHelpersFulu.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/MiscHelpersFulu.java index 275df2d61e7..c88af06ec3e 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/MiscHelpersFulu.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/MiscHelpersFulu.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.logic.versions.fulu.helpers; +import static com.google.common.base.Preconditions.checkArgument; import static tech.pegasys.teku.spec.logic.common.helpers.MathHelpers.bytesToUInt64; import static tech.pegasys.teku.spec.logic.common.helpers.MathHelpers.uint256ToBytes; @@ -22,6 +23,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.Set; @@ -40,6 +42,7 @@ import tech.pegasys.teku.kzg.KZGCellAndProof; import tech.pegasys.teku.kzg.KZGCellID; import tech.pegasys.teku.kzg.KZGCellWithColumnId; +import tech.pegasys.teku.spec.config.BlobSchedule; import tech.pegasys.teku.spec.config.SpecConfigElectra; import tech.pegasys.teku.spec.config.SpecConfigFulu; import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.Blob; @@ -90,6 +93,8 @@ public static MiscHelpersFulu required(final MiscHelpers miscHelpers) { @SuppressWarnings("unused") private final SchemaDefinitionsFulu schemaDefinitions; + private final List blobSchedule; + public MiscHelpersFulu( final SpecConfigFulu specConfigFulu, final Predicates predicates, @@ -101,6 +106,10 @@ public MiscHelpersFulu( this.predicates = predicates; this.specConfigFulu = specConfigFulu; this.schemaDefinitions = SchemaDefinitionsFulu.required(schemaDefinitions); + this.blobSchedule = + specConfigFulu.getBlobSchedule().stream() + .sorted(Comparator.comparing(BlobSchedule::epoch)) + .toList(); } @Override @@ -108,6 +117,19 @@ public Optional toVersionFulu() { return Optional.of(this); } + // get_max_blobs_per_block + public int getMaxBlobsPerBlock(final UInt64 epoch) { + checkArgument(!blobSchedule.isEmpty(), "Blob schedules not correctly defined."); + + final Optional maybeSchedule = + blobSchedule.stream() + .filter(blobSchedule -> blobSchedule.epoch().isLessThanOrEqualTo(epoch)) + .max(Comparator.comparing(BlobSchedule::epoch)); + return maybeSchedule.isPresent() + ? maybeSchedule.get().maxBlobsPerBlock() + : blobSchedule.getFirst().maxBlobsPerBlock(); + } + private UInt256 incrementByModule(final UInt256 n) { if (n.equals(UInt256.MAX_VALUE)) { return UInt256.ZERO; diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/chiado.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/chiado.yaml index 7aa1bf1ae28..fdfbaf6530f 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/chiado.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/chiado.yaml @@ -149,3 +149,14 @@ BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 2 MAX_BLOBS_PER_BLOCK_ELECTRA: 2 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 516608 + MAX_BLOBS_PER_BLOCK: 2 + # Electra + - EPOCH: 948224 + MAX_BLOBS_PER_BLOCK: 2 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/ephemery.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/ephemery.yaml index d2e31d1949c..6ef1b010dba 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/ephemery.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/ephemery.yaml @@ -134,4 +134,12 @@ BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 # `uint64(9)` MAX_BLOBS_PER_BLOCK_ELECTRA: 9 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA -MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 \ No newline at end of file +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 0 + MAX_BLOBS_PER_BLOCK: 6 + # Electra + - EPOCH: 10 + MAX_BLOBS_PER_BLOCK: 9 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/gnosis.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/gnosis.yaml index e64bddec408..1939c02f1f9 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/gnosis.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/gnosis.yaml @@ -147,4 +147,15 @@ BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 2 # `uint64(2)` MAX_BLOBS_PER_BLOCK_ELECTRA: 2 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA -MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 \ No newline at end of file +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 889856 + MAX_BLOBS_PER_BLOCK: 2 + # Electra + - EPOCH: 1337856 + MAX_BLOBS_PER_BLOCK: 2 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/holesky.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/holesky.yaml index 348061075bc..efc1d80129e 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/holesky.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/holesky.yaml @@ -151,4 +151,12 @@ SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 VALIDATOR_CUSTODY_REQUIREMENT: 8 MAX_BLOBS_PER_BLOCK_FULU: 12 -MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 \ No newline at end of file +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 29696 + MAX_BLOBS_PER_BLOCK: 6 + # Electra + - EPOCH: 115968 + MAX_BLOBS_PER_BLOCK: 9 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/hoodi.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/hoodi.yaml index acb12a45ccd..a55f8dc5e07 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/hoodi.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/hoodi.yaml @@ -140,4 +140,12 @@ BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 # `uint64(9)` MAX_BLOBS_PER_BLOCK_ELECTRA: 9 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA -MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 \ No newline at end of file +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 0 + MAX_BLOBS_PER_BLOCK: 6 + # Electra + - EPOCH: 2048 + MAX_BLOBS_PER_BLOCK: 9 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml index 885d89dea4f..8c940cb6470 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml @@ -168,4 +168,15 @@ CUSTODY_REQUIREMENT: 4 VALIDATOR_CUSTODY_REQUIREMENT: 8 MAX_BLOBS_PER_BLOCK_FULU: 12 MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 -BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 \ No newline at end of file +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 269568 + MAX_BLOBS_PER_BLOCK: 6 + # Electra + - EPOCH: 364032 + MAX_BLOBS_PER_BLOCK: 9 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml index 6b4a6e71729..54c2757c258 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml @@ -168,4 +168,12 @@ CUSTODY_REQUIREMENT: 4 VALIDATOR_CUSTODY_REQUIREMENT: 8 MAX_BLOBS_PER_BLOCK_FULU: 12 MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 -BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 \ No newline at end of file +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 18446744073709551615 + MAX_BLOBS_PER_BLOCK: 6 + # Electra + - EPOCH: 18446744073709551615 + MAX_BLOBS_PER_BLOCK: 9 \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/sepolia.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/sepolia.yaml index f80cbd20d6d..98bc65a35c7 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/sepolia.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/sepolia.yaml @@ -135,4 +135,12 @@ BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 # `uint64(9)` MAX_BLOBS_PER_BLOCK_ELECTRA: 9 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA -MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 \ No newline at end of file +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + +BLOB_SCHEDULE: + # Deneb + - EPOCH: 132608 + MAX_BLOBS_PER_BLOCK: 6 + # Electra + - EPOCH: 222464 + MAX_BLOBS_PER_BLOCK: 9 \ No newline at end of file diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigFuluTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigFuluTest.java index 9f631799ee8..d54abfc3c1e 100644 --- a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigFuluTest.java +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigFuluTest.java @@ -15,10 +15,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; import org.junit.jupiter.api.Test; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; import tech.pegasys.teku.spec.util.DataStructureUtil; public class SpecConfigFuluTest { @@ -106,6 +109,24 @@ public void equals_electraConfigDiffer() { assertThat(configA.hashCode()).isNotEqualTo(configB.hashCode()); } + @Test + public void mainnetBlobSchedule() { + final Spec mainnetSpec = TestSpecFactory.createMainnetFulu(); + final MiscHelpersFulu miscHelpersFulu = + mainnetSpec.forMilestone(SpecMilestone.FULU).miscHelpers().toVersionFulu().orElseThrow(); + // test defaulting to minimum + assertThat(miscHelpersFulu.getMaxBlobsPerBlock(UInt64.valueOf(0))).isEqualTo(6); + // test deneb max blobs boundary + assertThat(miscHelpersFulu.getMaxBlobsPerBlock(UInt64.valueOf(269568))).isEqualTo(6); + assertThat(miscHelpersFulu.getMaxBlobsPerBlock(UInt64.valueOf(269569))).isEqualTo(6); + // last epoch of deneb + assertThat(miscHelpersFulu.getMaxBlobsPerBlock(UInt64.valueOf(364031))).isEqualTo(6); + // electra boundary + assertThat(miscHelpersFulu.getMaxBlobsPerBlock(UInt64.valueOf(364032))).isEqualTo(9); + // inside electra + assertThat(miscHelpersFulu.getMaxBlobsPerBlock(UInt64.valueOf(364033))).isEqualTo(9); + } + @Test public void mainnetShouldHave12MaxBlobs() { final SpecConfigFulu specConfigFulu = @@ -133,6 +154,9 @@ private SpecConfigFulu createRandomFuluConfig( dataStructureUtil.randomPositiveInt(8192), dataStructureUtil.randomPositiveInt(8192), dataStructureUtil.randomPositiveInt(8192), - dataStructureUtil.randomUInt64(32000000000L)) {}; + dataStructureUtil.randomUInt64(32000000000L), + List.of( + new BlobSchedule( + dataStructureUtil.randomEpoch(), dataStructureUtil.randomPositiveInt(64)))) {}; } } diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigReaderTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigReaderTest.java index f6b2f0363f5..5500ceaeef4 100644 --- a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigReaderTest.java +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigReaderTest.java @@ -16,12 +16,15 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static tech.pegasys.teku.spec.config.SpecConfigAssertions.assertAllAltairFieldsSet; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -192,12 +195,33 @@ public void read_invalidUInt64_tooLarge() { "Failed to parse value for constant MIN_GENESIS_TIME: '18446744073709552001'")); } + @Test + public void read_listOfBlobSchedules() { + final Map data = new HashMap<>(); + data.put( + "BLOB_SCHEDULE", + List.of( + Map.of("EPOCH", "1", "MAX_BLOBS_PER_BLOCK", "2"), + Map.of("EPOCH", "3", "MAX_BLOBS_PER_BLOCK", "4"))); + + assertDoesNotThrow(() -> reader.loadFromMap(data, true)); + } + + @Test + public void read_invalidListThrowsException() { + final Map data = new HashMap<>(); + data.put("BLOB_SCHEDULE", List.of("A", "b")); + assertThatThrownBy(() -> reader.loadFromMap(data, true)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("BLOB_SCHEDULE"); + } + @Test public void read_localConfigFile_notLoadingDefaults(@TempDir final Path tempDir) throws IOException { Files.writeString( tempDir.resolve("test.yaml"), "PRESET_BASE: 'mainnet'\nCONFIG_NAME: 'mainnet'", UTF_8); - Map data = + final Map data = reader.readValues(Files.newInputStream(tempDir.resolve("test.yaml"))); reader.loadFromMap(data, true); assertThatThrownBy(reader::build)