diff --git a/.circleci/config.yml b/.circleci/config.yml index cccfb3f2dd6..eb5c38b23b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,6 +68,16 @@ executors: architecture: "amd64" platform: "linux/amd64" + machine_large_executor_amd64: + machine: + image: ubuntu-2204:2024.01.1 # https://circleci.com/developer/machine/image/ubuntu-2204 + docker_layer_caching: true + resource_class: large + working_directory: ~/project + environment: + architecture: "amd64" + platform: "linux/amd64" + machine_executor_arm64: machine: image: ubuntu-2204:2024.01.1 # https://circleci.com/developer/machine/image/ubuntu-2204 @@ -354,7 +364,7 @@ jobs: acceptanceTests: parallelism: 5 - executor: machine_executor_amd64 + executor: machine_large_executor_amd64 steps: - install_java_21 - prepare diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a19f0475f7..1097e165782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added support for [Ephemery Testnet](https://github.com/ephemery.dev) `--network=ephemery` - Renamed metrics `validator_attestation_publication_delay`,`validator_block_publication_delay` and `beacon_block_import_delay_counter` to include the suffix `_total` added by the current version of prometheus. - Updated bootnodes for Holesky network +- Added new `--p2p-flood-publish-enabled` parameter to control whenever flood publishing behaviour is enabled (applies to all subnets). Previous teku versions always had this behaviour enabled. Default is `true`. ### Bug Fixes - removed a warning from logs about non blinded blocks being requested (#8562) diff --git a/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/CapellaUpgradeAcceptanceTest.java b/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/CapellaUpgradeAcceptanceTest.java index c38d394a984..0c0f9c2d00a 100644 --- a/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/CapellaUpgradeAcceptanceTest.java +++ b/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/CapellaUpgradeAcceptanceTest.java @@ -34,7 +34,7 @@ public class CapellaUpgradeAcceptanceTest extends AcceptanceTestBase { @Test void shouldUpgradeToCapella() throws Exception { final UInt64 currentTime = timeProvider.getTimeInSeconds(); - final int genesisTime = currentTime.plus(30).intValue(); // magic node startup time + final int genesisTime = currentTime.plus(60).intValue(); // magic node startup time final int shanghaiTime = genesisTime + 4 * 2; // 4 slots, 2 seconds each final Map genesisOverrides = Map.of("shanghaiTime", String.valueOf(shanghaiTime)); @@ -51,6 +51,15 @@ void shouldUpgradeToCapella() throws Exception { genesisOverrides); primaryEL.start(); + TekuBeaconNode primaryNode = + createTekuBeaconNode( + beaconNodeConfigWithForks(genesisTime, primaryEL) + .withStartupTargetPeerCount(0) + .build()); + + primaryNode.start(); + primaryNode.waitForMilestone(SpecMilestone.CAPELLA); + BesuNode secondaryEL = createBesuNode( BesuDockerVersion.STABLE, @@ -64,15 +73,6 @@ void shouldUpgradeToCapella() throws Exception { secondaryEL.start(); secondaryEL.addPeer(primaryEL); - TekuBeaconNode primaryNode = - createTekuBeaconNode( - beaconNodeConfigWithForks(genesisTime, primaryEL) - .withStartupTargetPeerCount(0) - .build()); - - primaryNode.start(); - primaryNode.waitForMilestone(SpecMilestone.CAPELLA); - final int primaryNodeGenesisTime = primaryNode.getGenesisTime().intValue(); TekuBeaconNode lateJoiningNode = diff --git a/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/DenebUpgradeAcceptanceTest.java b/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/DenebUpgradeAcceptanceTest.java index dba6033a322..a9c0e1f80e8 100644 --- a/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/DenebUpgradeAcceptanceTest.java +++ b/acceptance-tests/src/acceptance-test/java/tech/pegasys/teku/test/acceptance/DenebUpgradeAcceptanceTest.java @@ -34,7 +34,7 @@ public class DenebUpgradeAcceptanceTest extends AcceptanceTestBase { @Test void shouldUpgradeToDeneb() throws Exception { final UInt64 currentTime = timeProvider.getTimeInSeconds(); - final int genesisTime = currentTime.plus(30).intValue(); // magic node startup time + final int genesisTime = currentTime.plus(60).intValue(); // magic node startup time final int epochDuration = 4 * 2; // 4 slots, 2 seconds each for swift final int shanghaiTime = genesisTime + epochDuration; final Map genesisOverrides = @@ -56,6 +56,15 @@ void shouldUpgradeToDeneb() throws Exception { genesisOverrides); primaryEL.start(); + TekuBeaconNode primaryNode = + createTekuBeaconNode( + beaconNodeWithTrustedSetup(genesisTime, primaryEL) + .withStartupTargetPeerCount(0) + .build()); + + primaryNode.start(); + primaryNode.waitForMilestone(SpecMilestone.DENEB); + BesuNode secondaryEL = createBesuNode( BesuDockerVersion.STABLE, @@ -69,15 +78,6 @@ void shouldUpgradeToDeneb() throws Exception { secondaryEL.start(); secondaryEL.addPeer(primaryEL); - TekuBeaconNode primaryNode = - createTekuBeaconNode( - beaconNodeWithTrustedSetup(genesisTime, primaryEL) - .withStartupTargetPeerCount(0) - .build()); - - primaryNode.start(); - primaryNode.waitForMilestone(SpecMilestone.DENEB); - final int primaryNodeGenesisTime = primaryNode.getGenesisTime().intValue(); TekuBeaconNode lateJoiningNode = diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/SpecMilestone.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/SpecMilestone.java index fc8585d9987..a624747d776 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/SpecMilestone.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/SpecMilestone.java @@ -46,6 +46,14 @@ public boolean isGreaterThanOrEqualTo(final SpecMilestone other) { return compareTo(other) >= 0; } + public boolean isGreaterThan(final SpecMilestone other) { + return compareTo(other) > 0; + } + + public boolean isLessThanOrEqualTo(final SpecMilestone other) { + return compareTo(other) <= 0; + } + /** Returns the milestone prior to this milestone */ public SpecMilestone getPreviousMilestone() { if (equals(PHASE0)) { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/Domain.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/Domain.java index 9666cb38f7e..28c40afc6e5 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/Domain.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/constants/Domain.java @@ -33,7 +33,4 @@ public class Domain { // Capella public static final Bytes4 DOMAIN_BLS_TO_EXECUTION_CHANGE = Bytes4.fromHexString("0x0A000000"); - - // Electra - public static final Bytes4 DOMAIN_CONSOLIDATION = Bytes4.fromHexString("0x0B000000"); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/AbstractSchemaProvider.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/AbstractSchemaProvider.java new file mode 100644 index 00000000000..c1addf4d779 --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/AbstractSchemaProvider.java @@ -0,0 +1,90 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +abstract class AbstractSchemaProvider implements SchemaProvider { + private final NavigableMap milestoneToEffectiveMilestone = + new TreeMap<>(); + private final SchemaId schemaId; + + protected AbstractSchemaProvider(final SchemaId schemaId) { + this.schemaId = schemaId; + } + + protected void addMilestoneMapping( + final SpecMilestone milestone, final SpecMilestone untilMilestone) { + checkArgument( + untilMilestone.isGreaterThan(milestone), + "%s must be earlier than %s", + milestone, + untilMilestone); + + checkOverlappingVersionMappings(milestone, untilMilestone); + + SpecMilestone currentMilestone = untilMilestone; + while (currentMilestone.isGreaterThan(milestone)) { + milestoneToEffectiveMilestone.put(currentMilestone, milestone); + currentMilestone = currentMilestone.getPreviousMilestone(); + } + } + + private void checkOverlappingVersionMappings( + final SpecMilestone milestone, final SpecMilestone untilMilestone) { + final Map.Entry floorEntry = + milestoneToEffectiveMilestone.floorEntry(untilMilestone); + if (floorEntry != null && floorEntry.getValue().isGreaterThanOrEqualTo(milestone)) { + throw new IllegalArgumentException( + String.format( + "Milestone %s is already mapped to %s", + floorEntry.getKey(), getEffectiveMilestone(floorEntry.getValue()))); + } + final Map.Entry ceilingEntry = + milestoneToEffectiveMilestone.ceilingEntry(milestone); + if (ceilingEntry != null && ceilingEntry.getKey().isLessThanOrEqualTo(untilMilestone)) { + throw new IllegalArgumentException( + String.format( + "Milestone %s is already mapped to %s", + ceilingEntry.getKey(), getEffectiveMilestone(ceilingEntry.getValue()))); + } + } + + @Override + public SpecMilestone getEffectiveMilestone(final SpecMilestone milestone) { + return milestoneToEffectiveMilestone.getOrDefault(milestone, milestone); + } + + @Override + public T getSchema(final SchemaRegistry registry) { + final SpecMilestone milestone = registry.getMilestone(); + final SpecMilestone effectiveMilestone = getEffectiveMilestone(milestone); + return createSchema(registry, effectiveMilestone, registry.getSpecConfig()); + } + + @Override + public SchemaId getSchemaId() { + return schemaId; + } + + protected abstract T createSchema( + SchemaRegistry registry, SpecMilestone effectiveMilestone, SpecConfig specConfig); +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaCache.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaCache.java new file mode 100644 index 00000000000..5bd6dabee3b --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaCache.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +interface SchemaCache { + static SchemaCache createDefault() { + return new SchemaCache() { + private final Map, Object>> cache = + new EnumMap<>(SpecMilestone.class); + + @SuppressWarnings("unchecked") + @Override + public T get(final SpecMilestone milestone, final SchemaId schemaId) { + final Map milestoneSchemaIds = cache.get(milestone); + if (milestoneSchemaIds == null) { + return null; + } + return (T) milestoneSchemaIds.get(schemaId); + } + + @Override + public void put( + final SpecMilestone milestone, final SchemaId schemaId, final T schema) { + cache.computeIfAbsent(milestone, __ -> new HashMap<>()).put(schemaId, schema); + } + }; + } + + T get(SpecMilestone milestone, SchemaId schemaId); + + void put(SpecMilestone milestone, SchemaId schemaId, T schema); +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaProvider.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaProvider.java new file mode 100644 index 00000000000..7c6efcafd5a --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static tech.pegasys.teku.spec.SpecMilestone.BELLATRIX; +import static tech.pegasys.teku.spec.SpecMilestone.CAPELLA; +import static tech.pegasys.teku.spec.SpecMilestone.DENEB; +import static tech.pegasys.teku.spec.SpecMilestone.ELECTRA; + +import java.util.EnumSet; +import java.util.Set; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +interface SchemaProvider { + Set ALL_MILESTONES = EnumSet.allOf(SpecMilestone.class); + Set FROM_BELLATRIX = from(BELLATRIX); + Set FROM_CAPELLA = from(CAPELLA); + Set FROM_DENEB = from(DENEB); + Set FROM_ELECTRA = from(ELECTRA); + + static Set from(final SpecMilestone milestone) { + return EnumSet.copyOf(SpecMilestone.getAllMilestonesFrom(milestone)); + } + + static Set fromTo( + final SpecMilestone fromMilestone, final SpecMilestone toMilestone) { + return EnumSet.copyOf( + SpecMilestone.getAllMilestonesFrom(fromMilestone).stream() + .filter(toMilestone::isLessThanOrEqualTo) + .toList()); + } + + T getSchema(SchemaRegistry registry); + + Set getSupportedMilestones(); + + SpecMilestone getEffectiveMilestone(SpecMilestone version); + + SchemaId getSchemaId(); +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistry.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistry.java new file mode 100644 index 00000000000..d595c60bfab --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistry.java @@ -0,0 +1,138 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.annotations.VisibleForTesting; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +public class SchemaRegistry { + // this is used for dependency loop detection during priming + private static final Set> INFLIGHT_PROVIDERS = new HashSet<>(); + + private final Map, SchemaProvider> providers = new HashMap<>(); + private final SpecMilestone milestone; + private final SchemaCache cache; + private final SpecConfig specConfig; + private boolean primed; + + SchemaRegistry( + final SpecMilestone milestone, final SpecConfig specConfig, final SchemaCache cache) { + this.milestone = milestone; + this.specConfig = specConfig; + this.cache = cache; + this.primed = false; + } + + /** + * This is supposed to be called only by {@link SchemaRegistryBuilder#build(SpecMilestone, + * SpecConfig)} which is synchronized + */ + void registerProvider(final SchemaProvider provider) { + if (primed) { + throw new IllegalStateException("Cannot add a provider to a primed registry"); + } + if (providers.put(provider.getSchemaId(), provider) != null) { + throw new IllegalStateException( + "Cannot add provider " + + provider.getClass().getSimpleName() + + " referencing " + + provider.getSchemaId() + + " which has been already added via another provider"); + } + } + + @VisibleForTesting + boolean isProviderRegistered(final SchemaProvider provider) { + return provider.equals(providers.get(provider.getSchemaId())); + } + + @SuppressWarnings("unchecked") + public T get(final SchemaId schemaId) { + SchemaProvider provider = (SchemaProvider) providers.get(schemaId); + if (provider == null) { + throw new IllegalArgumentException( + "No provider registered for schema " + + schemaId + + " or it does not support milestone " + + milestone); + } + T schema = cache.get(milestone, schemaId); + if (schema != null) { + return schema; + } + + // let's check if the schema is stored associated to the effective milestone + final SpecMilestone effectiveMilestone = provider.getEffectiveMilestone(milestone); + if (effectiveMilestone != milestone) { + schema = cache.get(effectiveMilestone, schemaId); + if (schema != null) { + // let's cache the schema for current milestone as well + cache.put(milestone, schemaId, schema); + return schema; + } + } + + // The schema was not found. + // we reach this point only during priming when we actually ask providers to generate schemas + checkState(!primed, "Registry is primed but schema not found for %s", schemaId); + + // save the provider as "inflight" + if (!INFLIGHT_PROVIDERS.add(provider)) { + throw new IllegalStateException("loop detected creating schema for " + schemaId); + } + + // actual schema creation (may trigger recursive registry lookups) + schema = provider.getSchema(this); + + // release the provider + INFLIGHT_PROVIDERS.remove(provider); + + // cache the schema + cache.put(effectiveMilestone, schemaId, schema); + if (effectiveMilestone != milestone) { + cache.put(milestone, schemaId, schema); + } + return schema; + } + + public SpecMilestone getMilestone() { + return milestone; + } + + public SpecConfig getSpecConfig() { + return specConfig; + } + + /** + * This is supposed to be called only by {@link SchemaRegistryBuilder#build(SpecMilestone, + * SpecConfig)} which is synchronized + */ + void primeRegistry() { + if (primed) { + throw new IllegalStateException("Registry already primed"); + } + for (final SchemaId schemaClass : providers.keySet()) { + get(schemaClass); + } + primed = true; + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java new file mode 100644 index 00000000000..e11bb1f6678 --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java @@ -0,0 +1,68 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import com.google.common.annotations.VisibleForTesting; +import java.util.HashSet; +import java.util.Set; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +public class SchemaRegistryBuilder { + private final Set> providers = new HashSet<>(); + private final Set> schemaIds = new HashSet<>(); + private final SchemaCache cache; + + public static SchemaRegistryBuilder create() { + return new SchemaRegistryBuilder(); + } + + public SchemaRegistryBuilder() { + this.cache = SchemaCache.createDefault(); + } + + @VisibleForTesting + SchemaRegistryBuilder(final SchemaCache cache) { + this.cache = cache; + } + + SchemaRegistryBuilder addProvider(final SchemaProvider provider) { + if (!providers.add(provider)) { + throw new IllegalArgumentException( + "The provider " + provider.getClass().getSimpleName() + " has been already added"); + } + if (!schemaIds.add(provider.getSchemaId())) { + throw new IllegalStateException( + "A previously added provider was already providing the schema for " + + provider.getSchemaId()); + } + return this; + } + + public synchronized SchemaRegistry build( + final SpecMilestone milestone, final SpecConfig specConfig) { + final SchemaRegistry registry = new SchemaRegistry(milestone, specConfig, cache); + + for (final SchemaProvider provider : providers) { + if (provider.getSupportedMilestones().contains(milestone)) { + registry.registerProvider(provider); + } + } + + registry.primeRegistry(); + + return registry; + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java new file mode 100644 index 00000000000..68a82c8d4a9 --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java @@ -0,0 +1,98 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.CaseFormat; +import com.google.common.base.Converter; +import com.google.common.base.MoreObjects; +import java.util.Locale; +import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; +import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszBitvectorSchema; +import tech.pegasys.teku.spec.SpecMilestone; + +public class SchemaTypes { + // PHASE0 + public static final SchemaId> ATTNETS_ENR_FIELD_SCHEMA = + create("ATTNETS_ENR_FIELD_SCHEMA"); + + // Altair + + // Bellatrix + + // Capella + + // Deneb + + private SchemaTypes() { + // Prevent instantiation + } + + @VisibleForTesting + static SchemaId create(final String name) { + return new SchemaId<>(name); + } + + public static class SchemaId { + private static final Converter UPPER_UNDERSCORE_TO_UPPER_CAMEL = + CaseFormat.UPPER_UNDERSCORE.converterTo(CaseFormat.UPPER_CAMEL); + + public static String upperSnakeCaseToUpperCamel(final String camelCase) { + return UPPER_UNDERSCORE_TO_UPPER_CAMEL.convert(camelCase); + } + + private static String capitalizeMilestone(final SpecMilestone milestone) { + return milestone.name().charAt(0) + milestone.name().substring(1).toLowerCase(Locale.ROOT); + } + + private final String name; + + private SchemaId(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getContainerName(final SpecMilestone milestone) { + return getContainerName() + capitalizeMilestone(milestone); + } + + public String getContainerName() { + return upperSnakeCaseToUpperCamel(name.replace("_SCHEMA", "")); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o instanceof SchemaId other) { + return name.equals(other.name); + } + return false; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("name", name).toString(); + } + } +} diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/BaseSchemaProviderTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/BaseSchemaProviderTest.java new file mode 100644 index 00000000000..67b6a3d5f5d --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/BaseSchemaProviderTest.java @@ -0,0 +1,92 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.spec.SpecMilestone.ALTAIR; +import static tech.pegasys.teku.spec.SpecMilestone.BELLATRIX; +import static tech.pegasys.teku.spec.SpecMilestone.CAPELLA; +import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; + +import java.util.EnumSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +class BaseSchemaProviderTest { + @SuppressWarnings("unchecked") + private static final SchemaId STRING_SCHEMA_ID = mock(SchemaId.class); + + private final TestSchemaProvider provider = new TestSchemaProvider(); + private final SchemaRegistry mockRegistry = mock(SchemaRegistry.class); + + @Test + void shouldGetEffectiveMilestone() { + provider.addMilestoneMapping(PHASE0, ALTAIR); + assertEquals(PHASE0, provider.getEffectiveMilestone(PHASE0)); + assertEquals(PHASE0, provider.getEffectiveMilestone(ALTAIR)); + assertEquals(BELLATRIX, provider.getEffectiveMilestone(BELLATRIX)); + } + + @Test + void shouldGetSchema() { + when(mockRegistry.getMilestone()).thenReturn(PHASE0); + String result = provider.getSchema(mockRegistry); + assertEquals("TestSchema", result); + } + + @Test + void shouldGetNonOverlappingVersionMappings() { + provider.addMilestoneMapping(PHASE0, ALTAIR); + provider.addMilestoneMapping(BELLATRIX, CAPELLA); + + assertEquals(PHASE0, provider.getEffectiveMilestone(PHASE0)); + assertEquals(PHASE0, provider.getEffectiveMilestone(ALTAIR)); + assertEquals(BELLATRIX, provider.getEffectiveMilestone(BELLATRIX)); + assertEquals(BELLATRIX, provider.getEffectiveMilestone(CAPELLA)); + } + + @Test + void testOverlappingVersionMappingsThrowsException() { + provider.addMilestoneMapping(PHASE0, ALTAIR); + + assertThatThrownBy(() -> provider.addMilestoneMapping(ALTAIR, BELLATRIX)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Milestone ALTAIR is already mapped to PHASE0"); + } + + private static class TestSchemaProvider extends AbstractSchemaProvider { + TestSchemaProvider() { + super(STRING_SCHEMA_ID); + } + + @Override + protected String createSchema( + final SchemaRegistry registry, + final SpecMilestone effectiveMilestone, + final SpecConfig specConfig) { + return "TestSchema"; + } + + @Override + public Set getSupportedMilestones() { + return EnumSet.allOf(SpecMilestone.class); + } + } +} diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaCacheTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaCacheTest.java new file mode 100644 index 00000000000..920c4b3954a --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaCacheTest.java @@ -0,0 +1,105 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +public class SchemaCacheTest { + + private SchemaCache schemaCache; + + @BeforeEach + void setUp() { + schemaCache = SchemaCache.createDefault(); + } + + @Test + void shouldPutAndGetSchema() { + final SchemaId schemaId = SchemaTypes.create("test"); + final SpecMilestone milestone = SpecMilestone.PHASE0; + final String schema = "Test Schema"; + + schemaCache.put(milestone, schemaId, schema); + final String retrievedSchema = schemaCache.get(milestone, schemaId); + + assertEquals(schema, retrievedSchema); + } + + @Test + void shouldReturnNullForNonExistentSchema() { + final SchemaId schemaId = SchemaTypes.create("nonexistent"); + final SpecMilestone milestone = SpecMilestone.PHASE0; + + final String retrievedSchema = schemaCache.get(milestone, schemaId); + + assertNull(retrievedSchema); + } + + @Test + void shouldPutAndGetMultipleSchemasForSameMilestone() { + final SchemaId schemaId1 = SchemaTypes.create("test1"); + final SchemaId schemaId2 = SchemaTypes.create("test2"); + final SpecMilestone milestone = SpecMilestone.PHASE0; + final String schema1 = "Test Schema 1"; + final Integer schema2 = 42; + + schemaCache.put(milestone, schemaId1, schema1); + schemaCache.put(milestone, schemaId2, schema2); + + final String retrievedSchema1 = schemaCache.get(milestone, schemaId1); + final Integer retrievedSchema2 = schemaCache.get(milestone, schemaId2); + + assertEquals(schema1, retrievedSchema1); + assertEquals(schema2, retrievedSchema2); + } + + @Test + void shouldPutAndGetSchemasForDifferentMilestones() { + final SchemaId schemaId = SchemaTypes.create("test"); + final SpecMilestone milestone1 = SpecMilestone.PHASE0; + final SpecMilestone milestone2 = SpecMilestone.ALTAIR; + final String schema1 = "Test Schema 1"; + final String schema2 = "Test Schema 2"; + + schemaCache.put(milestone1, schemaId, schema1); + schemaCache.put(milestone2, schemaId, schema2); + + final String retrievedSchema1 = schemaCache.get(milestone1, schemaId); + final String retrievedSchema2 = schemaCache.get(milestone2, schemaId); + + assertEquals(schema1, retrievedSchema1); + assertEquals(schema2, retrievedSchema2); + } + + @Test + void shouldOverwriteExistingSchema() { + final SchemaId schemaId = SchemaTypes.create("test"); + final SpecMilestone milestone = SpecMilestone.PHASE0; + final String schema1 = "Test Schema 1"; + final String schema2 = "Test Schema 2"; + + schemaCache.put(milestone, schemaId, schema1); + schemaCache.put(milestone, schemaId, schema2); + + final String retrievedSchema = schemaCache.get(milestone, schemaId); + + assertEquals(schema2, retrievedSchema); + } +} diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilderTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilderTest.java new file mode 100644 index 00000000000..71a5b04db38 --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilderTest.java @@ -0,0 +1,100 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.spec.SpecMilestone.ALTAIR; +import static tech.pegasys.teku.spec.SpecMilestone.PHASE0; + +import java.util.EnumSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +public class SchemaRegistryBuilderTest { + private final SpecConfig specConfig = mock(SpecConfig.class); + private final SchemaCache cache = spy(SchemaCache.createDefault()); + private final SchemaRegistryBuilder builder = new SchemaRegistryBuilder(cache); + private final SchemaId stringId = SchemaTypes.create("stringType"); + private final String stringSchema = "stringSchema"; + + @SuppressWarnings("unchecked") + private final SchemaProvider mockProvider = mock(SchemaProvider.class); + + private final EnumSet supportedMilestones = EnumSet.of(PHASE0, ALTAIR); + + @BeforeEach + void setUp() { + when(mockProvider.getSchemaId()).thenReturn(stringId); + when(mockProvider.getSchema(any())).thenReturn(stringSchema); + when(mockProvider.getSupportedMilestones()).thenReturn(supportedMilestones); + when(mockProvider.getEffectiveMilestone(any())).thenReturn(PHASE0); + } + + @Test + void shouldAddProviderForSupportedMilestone() { + + builder.addProvider(mockProvider); + + for (final SpecMilestone milestone : SpecMilestone.values()) { + final SchemaRegistry registry = builder.build(milestone, specConfig); + if (supportedMilestones.contains(milestone)) { + assertThat(registry.isProviderRegistered(mockProvider)).isTrue(); + } else { + assertThat(registry.isProviderRegistered(mockProvider)).isFalse(); + } + } + + verify(mockProvider, times(SpecMilestone.values().length)).getSupportedMilestones(); + } + + @Test + void shouldPrimeRegistry() { + builder.addProvider(mockProvider); + builder.build(ALTAIR, specConfig); + + // we should have it in cache immediately + verify(cache).put(ALTAIR, stringId, stringSchema); + } + + @Test + void shouldThrowWhenAddingTheSameProviderTwice() { + builder.addProvider(mockProvider); + assertThatThrownBy(() -> builder.addProvider(mockProvider)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("has been already added"); + } + + @Test + void shouldThrowWhenAddingTwoProvidersReferencingTheSameSchemaId() { + @SuppressWarnings("unchecked") + final SchemaProvider mockProvider2 = mock(SchemaProvider.class); + when(mockProvider2.getSchemaId()).thenReturn(stringId); + + builder.addProvider(mockProvider); + + assertThatThrownBy(() -> builder.addProvider(mockProvider2)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("A previously added provider was already providing the"); + } +} diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryTest.java new file mode 100644 index 00000000000..d83a6b30f01 --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryTest.java @@ -0,0 +1,269 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +public class SchemaRegistryTest { + + private final SpecConfig specConfig = mock(SpecConfig.class); + private final SchemaCache schemaCache = spy(SchemaCache.createDefault()); + + @SuppressWarnings("unchecked") + private final SchemaProvider schemaProvider = mock(SchemaProvider.class); + + @SuppressWarnings("unchecked") + private final SchemaId schemaId = mock(SchemaId.class); + + private final SchemaRegistry schemaRegistry = + new SchemaRegistry(SpecMilestone.ALTAIR, specConfig, schemaCache); + + @Test + void shouldGetSchemaFromCache() { + final String cachedSchema = "schema"; + when(schemaProvider.getSchemaId()).thenReturn(schemaId); + when(schemaCache.get(SpecMilestone.ALTAIR, schemaId)).thenReturn(cachedSchema); + + schemaRegistry.registerProvider(schemaProvider); + final String result = schemaRegistry.get(schemaId); + + assertEquals(cachedSchema, result); + verify(schemaCache).get(SpecMilestone.ALTAIR, schemaId); + verify(schemaProvider, never()).getSchema(any()); + } + + @Test + void shouldGetSchemaFromProvider() { + final String newSchema = "schema"; + when(schemaProvider.getSchemaId()).thenReturn(schemaId); + when(schemaProvider.getEffectiveMilestone(SpecMilestone.ALTAIR)) + .thenReturn(SpecMilestone.ALTAIR); + when(schemaProvider.getSchema(schemaRegistry)).thenReturn(newSchema); + + schemaRegistry.registerProvider(schemaProvider); + final String result = schemaRegistry.get(schemaId); + + assertEquals(newSchema, result); + verify(schemaCache).get(SpecMilestone.ALTAIR, schemaId); + verify(schemaProvider).getSchema(schemaRegistry); + verify(schemaCache).put(SpecMilestone.ALTAIR, schemaId, newSchema); + } + + @Test + void shouldCacheMilestoneAndEffectiveMilestoneFromProvider() { + final String newSchema = "schema"; + when(schemaProvider.getSchemaId()).thenReturn(schemaId); + when(schemaProvider.getEffectiveMilestone(SpecMilestone.ALTAIR)) + .thenReturn(SpecMilestone.PHASE0); + when(schemaProvider.getSchema(schemaRegistry)).thenReturn(newSchema); + + schemaRegistry.registerProvider(schemaProvider); + final String result = schemaRegistry.get(schemaId); + + assertEquals(newSchema, result); + verify(schemaCache).get(SpecMilestone.PHASE0, schemaId); + verify(schemaCache).get(SpecMilestone.ALTAIR, schemaId); + verify(schemaProvider).getSchema(schemaRegistry); + verify(schemaCache).put(SpecMilestone.PHASE0, schemaId, newSchema); + verify(schemaCache).put(SpecMilestone.ALTAIR, schemaId, newSchema); + } + + @Test + void shouldGetFromCachedOfEffectiveMilestone() { + final String newSchema = "schema"; + when(schemaProvider.getSchemaId()).thenReturn(schemaId); + when(schemaCache.get(SpecMilestone.PHASE0, schemaId)).thenReturn(newSchema); + when(schemaProvider.getEffectiveMilestone(SpecMilestone.ALTAIR)) + .thenReturn(SpecMilestone.PHASE0); + when(schemaProvider.getSchema(schemaRegistry)).thenReturn(newSchema); + + schemaRegistry.registerProvider(schemaProvider); + final String result = schemaRegistry.get(schemaId); + + assertEquals(newSchema, result); + verify(schemaCache).put(SpecMilestone.ALTAIR, schemaId, newSchema); + verify(schemaProvider).getEffectiveMilestone(SpecMilestone.ALTAIR); + + verify(schemaProvider, never()).getSchema(schemaRegistry); + } + + @Test + void shouldThrowExceptionWhenGettingSchemaForUnregisteredProvider() { + assertThrows(IllegalArgumentException.class, () -> schemaRegistry.get(schemaId)); + } + + @Test + @SuppressWarnings("unchecked") + void shouldThrowIfDependencyWhenDependencyLoop() { + final SchemaProvider provider1 = mock(SchemaProvider.class); + final SchemaProvider provider2 = mock(SchemaProvider.class); + final SchemaProvider provider3 = mock(SchemaProvider.class); + final SchemaId id1 = mock(SchemaId.class); + final SchemaId id2 = mock(SchemaId.class); + final SchemaId id3 = mock(SchemaId.class); + + when(provider1.getSchemaId()).thenReturn(id1); + when(provider2.getSchemaId()).thenReturn(id2); + when(provider3.getSchemaId()).thenReturn(id3); + + // create a dependency loop + when(provider1.getSchema(schemaRegistry)) + .thenAnswer( + invocation -> { + invocation.getArgument(0, SchemaRegistry.class).get(id2); + return "test"; + }); + + when(provider2.getSchema(schemaRegistry)) + .thenAnswer( + invocation -> { + invocation.getArgument(0, SchemaRegistry.class).get(id3); + return 42; + }); + + when(provider3.getSchema(schemaRegistry)) + .thenAnswer( + invocation -> { + invocation.getArgument(0, SchemaRegistry.class).get(id1); + return 42; + }); + + schemaRegistry.registerProvider(provider1); + schemaRegistry.registerProvider(provider2); + schemaRegistry.registerProvider(provider3); + + assertThatThrownBy(schemaRegistry::primeRegistry) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("loop detected creating schema"); + } + + @Test + @SuppressWarnings("unchecked") + void shouldThrowIfDependencyWhenMutualDependencyLoop() { + final SchemaProvider provider1 = mock(SchemaProvider.class); + final SchemaProvider provider2 = mock(SchemaProvider.class); + final SchemaId id1 = mock(SchemaId.class); + final SchemaId id2 = mock(SchemaId.class); + + when(provider1.getSchemaId()).thenReturn(id1); + when(provider2.getSchemaId()).thenReturn(id2); + + // create a mutual dependency + when(provider2.getSchema(schemaRegistry)) + .thenAnswer( + invocation -> { + invocation.getArgument(0, SchemaRegistry.class).get(id1); + return 42; + }); + + when(provider1.getSchema(schemaRegistry)) + .thenAnswer( + invocation -> { + invocation.getArgument(0, SchemaRegistry.class).get(id2); + return "test"; + }); + + schemaRegistry.registerProvider(provider1); + schemaRegistry.registerProvider(provider2); + + assertThatThrownBy(schemaRegistry::primeRegistry) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("loop detected creating schema"); + } + + @Test + @SuppressWarnings("unchecked") + void shouldResolveNonLoopedDependencies() { + final SchemaProvider provider1 = mock(SchemaProvider.class); + final SchemaProvider provider2 = mock(SchemaProvider.class); + + final SchemaId id1 = mock(SchemaId.class); + final SchemaId id2 = mock(SchemaId.class); + + when(provider1.getEffectiveMilestone(SpecMilestone.ALTAIR)).thenReturn(SpecMilestone.ALTAIR); + when(provider2.getEffectiveMilestone(SpecMilestone.ALTAIR)).thenReturn(SpecMilestone.ALTAIR); + when(provider1.getSchemaId()).thenReturn(id1); + when(provider2.getSchemaId()).thenReturn(id2); + + // create a mutual dependency + when(provider1.getSchema(schemaRegistry)).thenReturn("test"); + when(provider2.getSchema(schemaRegistry)) + .thenAnswer( + invocation -> { + invocation.getArgument(0, SchemaRegistry.class).get(id1); + return 42; + }); + + schemaRegistry.registerProvider(provider1); + schemaRegistry.registerProvider(provider2); + + schemaRegistry.primeRegistry(); + + verify(schemaCache).put(SpecMilestone.ALTAIR, id1, "test"); + verify(schemaCache).put(SpecMilestone.ALTAIR, id2, 42); + } + + @Test + @SuppressWarnings("unchecked") + void shouldPrimeRegistry() { + final SchemaProvider provider1 = mock(SchemaProvider.class); + final SchemaProvider provider2 = mock(SchemaProvider.class); + final SchemaId id1 = mock(SchemaId.class); + final SchemaId id2 = mock(SchemaId.class); + + when(provider1.getEffectiveMilestone(SpecMilestone.ALTAIR)).thenReturn(SpecMilestone.ALTAIR); + when(provider2.getEffectiveMilestone(SpecMilestone.ALTAIR)).thenReturn(SpecMilestone.ALTAIR); + when(provider1.getSchemaId()).thenReturn(id1); + when(provider2.getSchemaId()).thenReturn(id2); + + schemaRegistry.registerProvider(provider1); + schemaRegistry.registerProvider(provider2); + + schemaRegistry.primeRegistry(); + + verify(provider1).getSchema(schemaRegistry); + verify(provider2).getSchema(schemaRegistry); + } + + @Test + void shouldThrowIfPrimeTwice() { + schemaRegistry.primeRegistry(); + assertThatThrownBy(schemaRegistry::primeRegistry) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Registry already primed"); + } + + @Test + @SuppressWarnings("unchecked") + void shouldThrowIfRegisteringTheSameSchemaIdTwice() { + final SchemaProvider provider1 = mock(SchemaProvider.class); + schemaRegistry.registerProvider(provider1); + assertThatThrownBy(() -> schemaRegistry.registerProvider(provider1)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("has been already added via another provider"); + } +} diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypesTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypesTest.java new file mode 100644 index 00000000000..a3bf01a8889 --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypesTest.java @@ -0,0 +1,59 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * 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.schemas.registry; + +import static java.lang.reflect.Modifier.isFinal; +import static java.lang.reflect.Modifier.isStatic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SchemaId; + +public class SchemaTypesTest { + + @Test + public void shouldProvideContainerNameViaSchemaId() { + final SchemaId schemaId = SchemaTypes.create("MY_TEST_SCHEMA"); + assertEquals(schemaId.getContainerName(), "MyTest"); + assertEquals(schemaId.getContainerName(SpecMilestone.DENEB), "MyTestDeneb"); + } + + @Test + public void validateStaticFieldNamesAndSchemaIdNames() throws IllegalAccessException { + // Get all declared fields in the SchemaTypes class + final Field[] fields = SchemaTypes.class.getDeclaredFields(); + + for (final Field field : fields) { + // Ensure the field is static and final + if (isStatic(field.getModifiers()) && isFinal(field.getModifiers())) { + + // Get the field name + final String fieldName = field.getName(); + + assertThat(fieldName).matches("^[A-Z][A-Z_]*_SCHEMA$"); + + // Get the value of the field + if (field.get(null) instanceof SchemaId schemaId) { + assertEquals( + fieldName, + schemaId.getName(), + "Field name does not match the create argument for field: " + fieldName); + } + } + } + } +} diff --git a/gradle/trivyignore.txt b/gradle/trivyignore.txt index 8c2f2da5552..0bef3c1bb88 100644 --- a/gradle/trivyignore.txt +++ b/gradle/trivyignore.txt @@ -2,4 +2,4 @@ # The following comment is an example of how CVE entries should be used in this file: # CVE-2022-0123 -CVE-2023-39017 +CVE-2024-7254 \ No newline at end of file diff --git a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/collections/impl/SszBitlistImpl.java b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/collections/impl/SszBitlistImpl.java index 754c19125dd..135f6450067 100644 --- a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/collections/impl/SszBitlistImpl.java +++ b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/collections/impl/SszBitlistImpl.java @@ -170,4 +170,8 @@ public boolean isWritableSupported() { public String toString() { return "SszBitlist{size=" + this.size() + ", " + value.toString() + "}"; } + + public static SszBitlist fromBytes(final SszBitlistSchema schema, final Bytes value) { + return new SszBitlistImpl(schema, BitlistImpl.fromSszBytes(value, schema.getMaxLength())); + } } diff --git a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/SszBitlistSchema.java b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/SszBitlistSchema.java index 2c4f53750e3..71e0a70b4fb 100644 --- a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/SszBitlistSchema.java +++ b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/SszBitlistSchema.java @@ -14,6 +14,7 @@ package tech.pegasys.teku.infrastructure.ssz.schema.collections; import java.util.BitSet; +import org.apache.tuweni.bytes.Bytes; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBit; import tech.pegasys.teku.infrastructure.ssz.schema.collections.impl.SszBitlistSchemaImpl; @@ -41,4 +42,22 @@ default SszBitlistT empty() { * @return SszBitlist instance */ SszBitlistT wrapBitSet(int size, BitSet bitSet); + + /** + * Creates a SszBitlist from bytes. + * + * @param bytes The bytes to create the SszBitlist from + * @return A new SszBitlist instance + */ + SszBitlistT fromBytes(Bytes bytes); + + /** + * Creates a SszBitlist from a hexadecimal string. + * + * @param hexString The hexadecimal string to create the SszBitlist from + * @return A new SszBitlist instance + */ + default SszBitlistT fromHexString(final String hexString) { + return fromBytes(Bytes.fromHexString(hexString)); + } } diff --git a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/impl/SszBitlistSchemaImpl.java b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/impl/SszBitlistSchemaImpl.java index dfe62445344..67f7783854b 100644 --- a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/impl/SszBitlistSchemaImpl.java +++ b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/schema/collections/impl/SszBitlistSchemaImpl.java @@ -154,4 +154,13 @@ public int flushWithBoundaryBit(final SszWriter writer, final int boundaryBitOff } } } + + @Override + public SszBitlist fromBytes(final Bytes bytes) { + checkArgument(bytes != null, "Input bytes cannot be null"); + try (final SszReader reader = SszReader.fromBytes(bytes)) { + final TreeNode node = sszDeserializeTree(reader); + return createFromBackingNode(node); + } + } } diff --git a/infrastructure/ssz/src/test/java/tech/pegasys/teku/infrastructure/ssz/collections/SszBitlistTest.java b/infrastructure/ssz/src/test/java/tech/pegasys/teku/infrastructure/ssz/collections/SszBitlistTest.java index c560b494121..207de309b75 100644 --- a/infrastructure/ssz/src/test/java/tech/pegasys/teku/infrastructure/ssz/collections/SszBitlistTest.java +++ b/infrastructure/ssz/src/test/java/tech/pegasys/teku/infrastructure/ssz/collections/SszBitlistTest.java @@ -95,6 +95,14 @@ public Stream emptyBitlistArgs() { return sszData().filter(SszCollection::isEmpty).map(Arguments::of); } + public Stream fromBytesTestCases() { + return sszData().map(bitlist -> Arguments.of(bitlist, bitlist.sszSerialize())); + } + + public Stream fromHexStringTestCases() { + return sszData().map(bitlist -> Arguments.of(bitlist, bitlist.sszSerialize().toHexString())); + } + @ParameterizedTest @MethodSource("bitlistArgs") void testSszRoundtrip(final SszBitlist bitlist1) { @@ -394,4 +402,65 @@ void testBitEmptyListSsz(final SszBitlist bitlist) { SszBitlist emptyList1 = bitlist.getSchema().sszDeserialize(Bytes.of(1)); assertThat(emptyList1).isEmpty(); } + + @ParameterizedTest + @MethodSource("fromBytesTestCases") + void testFromBytes(final SszBitlist bitlist, final Bytes serialized) { + SszBitlist deserialized = bitlist.getSchema().fromBytes(serialized); + + assertThat(deserialized).isEqualTo(bitlist); + assertThat(deserialized.size()).isEqualTo(bitlist.size()); + assertThatIntCollection(deserialized.getAllSetBits()).isEqualTo(bitlist.getAllSetBits()); + assertThat(deserialized.hashTreeRoot()).isEqualTo(bitlist.hashTreeRoot()); + } + + @ParameterizedTest + @MethodSource("fromHexStringTestCases") + void testFromHexString(final SszBitlist bitlist, final String hexString) { + SszBitlist deserialized = bitlist.getSchema().fromHexString(hexString); + + assertThat(deserialized).isEqualTo(bitlist); + assertThat(deserialized.size()).isEqualTo(bitlist.size()); + assertThatIntCollection(deserialized.getAllSetBits()).isEqualTo(bitlist.getAllSetBits()); + assertThat(deserialized.hashTreeRoot()).isEqualTo(bitlist.hashTreeRoot()); + } + + private static final SszBitlistSchema FROM_HEX_STRING_TEST_SCHEMA = + SszBitlistSchema.create(100); + + @Test + public void fromHexString_shouldHandleMinimalValidHexString() { + String minimalHex = "0x01"; + SszBitlist validResult = FROM_HEX_STRING_TEST_SCHEMA.fromHexString(minimalHex); + assertThat(validResult).isNotNull(); + assertThat(validResult.sszSerialize()).isEqualTo(Bytes.fromHexString(minimalHex)); + assertThat(validResult.size()).isZero(); + } + + @Test + public void fromHexString_shouldHandleComplexValidHexString() { + String complexHex = "0x01020304"; + SszBitlist complexResult = FROM_HEX_STRING_TEST_SCHEMA.fromHexString(complexHex); + assertThat(complexResult).isNotNull(); + assertThat(complexResult.sszSerialize()).isEqualTo(Bytes.fromHexString(complexHex)); + } + + @Test + public void fromHexString_shouldThrowForEmptyString() { + assertThatThrownBy(() -> FROM_HEX_STRING_TEST_SCHEMA.fromHexString("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void fromHexString_shouldThrowForOnlyPrefix() { + assertThatThrownBy(() -> FROM_HEX_STRING_TEST_SCHEMA.fromHexString("0x")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void fromHexString_shouldThrowForInvalidHexString() { + String invalidHex = "i am a string, not a valid hex string"; + assertThatThrownBy(() -> FROM_HEX_STRING_TEST_SCHEMA.fromHexString(invalidHex)) + .isInstanceOf(IllegalArgumentException.class); + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java index 04dd18f9faa..21b7a06e9f7 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/P2PConfig.java @@ -23,6 +23,7 @@ import tech.pegasys.teku.networking.eth2.gossip.config.GossipConfigurator; import tech.pegasys.teku.networking.eth2.gossip.encoding.GossipEncoding; import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; +import tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig; import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.config.NetworkingSpecConfig; @@ -174,6 +175,7 @@ public static class Builder { private boolean batchVerifyStrictThreadLimitEnabled = DEFAULT_BATCH_VERIFY_STRICT_THREAD_LIMIT_ENABLED; private boolean allTopicsFilterEnabled = DEFAULT_PEER_ALL_TOPIC_FILTER_ENABLED; + private Boolean isFloodPublishEnabled = GossipConfig.DEFAULT_FLOOD_PUBLISH_ENABLED; private Builder() {} @@ -196,6 +198,7 @@ public P2PConfig build() { builder.seenTTL( Duration.ofSeconds( (long) specConfig.getSecondsPerSlot() * specConfig.getSlotsPerEpoch() * 2)); + builder.floodPublishEnabled(isFloodPublishEnabled); }); final NetworkConfig networkConfig = this.networkConfig.build(); @@ -284,6 +287,12 @@ public Builder peerRequestLimit(final Integer peerRequestLimit) { return this; } + public Builder isFloodPublishEnabled(final Boolean floodPublishEnabled) { + checkNotNull(floodPublishEnabled); + this.isFloodPublishEnabled = floodPublishEnabled; + return this; + } + public Builder batchVerifyMaxThreads(final int batchVerifyMaxThreads) { if (batchVerifyMaxThreads < 0) { throw new InvalidConfigurationException( diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/gossip/config/GossipConfig.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/gossip/config/GossipConfig.java index 633b99c6174..4f7711beb1d 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/gossip/config/GossipConfig.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/gossip/config/GossipConfig.java @@ -36,6 +36,7 @@ public class GossipConfig { // After EIP-7045, attestations are valid for up to 2 full epochs, so TTL is 65 // slots 1115 * HEARTBEAT = 1115 * 0.7 / 12 = 65.125 static final Duration DEFAULT_SEEN_TTL = DEFAULT_HEARTBEAT_INTERVAL.multipliedBy(1115); + public static final Boolean DEFAULT_FLOOD_PUBLISH_ENABLED = Boolean.TRUE; private final int d; private final int dLow; @@ -46,6 +47,7 @@ public class GossipConfig { private final int history; private final Duration heartbeatInterval; private final Duration seenTTL; + private final Boolean floodPublishEnabled; private final GossipScoringConfig scoringConfig; private GossipConfig( @@ -58,6 +60,7 @@ private GossipConfig( final int history, final Duration heartbeatInterval, final Duration seenTTL, + final Boolean floodPublishEnabled, final GossipScoringConfig scoringConfig) { this.d = d; this.dLow = dLow; @@ -68,6 +71,7 @@ private GossipConfig( this.history = history; this.heartbeatInterval = heartbeatInterval; this.seenTTL = seenTTL; + this.floodPublishEnabled = floodPublishEnabled; this.scoringConfig = scoringConfig; } @@ -115,6 +119,10 @@ public Duration getSeenTTL() { return seenTTL; } + public boolean isFloodPublishEnabled() { + return floodPublishEnabled; + } + public GossipScoringConfig getScoringConfig() { return scoringConfig; } @@ -131,6 +139,7 @@ public static class Builder { private Integer history = DEFAULT_HISTORY; private Duration heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL; private Duration seenTTL = DEFAULT_SEEN_TTL; + private Boolean floodPublishEnabled = DEFAULT_FLOOD_PUBLISH_ENABLED; private Builder() {} @@ -145,6 +154,7 @@ public GossipConfig build() { history, heartbeatInterval, seenTTL, + floodPublishEnabled, scoringConfigBuilder.build()); } @@ -217,6 +227,11 @@ public Builder seenTTL(final Duration seenTTL) { return this; } + public Builder floodPublishEnabled(final Boolean floodPublishEnabled) { + this.floodPublishEnabled = floodPublishEnabled; + return this; + } + public Builder directPeerManager(final DirectPeerManager directPeerManager) { checkNotNull(directPeerManager); this.scoringConfigBuilder.directPeerManager(directPeerManager); diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/config/LibP2PParamsFactory.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/config/LibP2PParamsFactory.java index 9ad76de4a14..42f9aeb32d6 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/config/LibP2PParamsFactory.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/config/LibP2PParamsFactory.java @@ -47,10 +47,10 @@ private static void addGossipParamsMiscValues( final GossipConfig gossipConfig, final GossipParamsBuilder builder) { builder .fanoutTTL(gossipConfig.getFanoutTTL()) + .floodPublish(gossipConfig.isFloodPublishEnabled()) .gossipSize(gossipConfig.getAdvertise()) .gossipHistoryLength(gossipConfig.getHistory()) .heartbeatInterval(gossipConfig.getHeartbeatInterval()) - .floodPublish(true) .seenTTL(gossipConfig.getSeenTTL()); } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java b/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java index 16cac554b7a..4505e1c3cd5 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java @@ -166,7 +166,10 @@ public StateStorageMode getStateStorageMode() { private Database createV4Database() { try { DatabaseNetwork.init( - getNetworkFile(), spec.getGenesisSpecConfig().getGenesisForkVersion(), eth1Address); + getNetworkFile(), + spec.getGenesisSpecConfig().getGenesisForkVersion(), + eth1Address, + spec.getGenesisSpecConfig().getDepositChainId()); return RocksDbDatabaseFactory.createV4( metricsSystem, KvStoreConfiguration.v4Settings(dbDirectory.toPath()), @@ -190,7 +193,10 @@ private Database createV5Database() { final V5DatabaseMetadata metaData = V5DatabaseMetadata.init(getMetadataFile(), V5DatabaseMetadata.v5Defaults()); DatabaseNetwork.init( - getNetworkFile(), spec.getGenesisSpecConfig().getGenesisForkVersion(), eth1Address); + getNetworkFile(), + spec.getGenesisSpecConfig().getGenesisForkVersion(), + eth1Address, + spec.getGenesisSpecConfig().getDepositChainId()); return RocksDbDatabaseFactory.createV4( metricsSystem, metaData.getHotDbConfiguration().withDatabaseDir(dbDirectory.toPath()), @@ -233,7 +239,10 @@ private Database createLevelDbV1Database() { final V5DatabaseMetadata metaData = V5DatabaseMetadata.init(getMetadataFile(), V5DatabaseMetadata.v5Defaults()); DatabaseNetwork.init( - getNetworkFile(), spec.getGenesisSpecConfig().getGenesisForkVersion(), eth1Address); + getNetworkFile(), + spec.getGenesisSpecConfig().getGenesisForkVersion(), + eth1Address, + spec.getGenesisSpecConfig().getDepositChainId()); return LevelDbDatabaseFactory.createLevelDb( metricsSystem, metaData.getHotDbConfiguration().withDatabaseDir(dbDirectory.toPath()), @@ -284,7 +293,10 @@ private KvStoreConfiguration initV6Configuration() throws IOException { V6DatabaseMetadata.init(getMetadataFile(), V6DatabaseMetadata.singleDBDefault()); DatabaseNetwork.init( - getNetworkFile(), spec.getGenesisSpecConfig().getGenesisForkVersion(), eth1Address); + getNetworkFile(), + spec.getGenesisSpecConfig().getGenesisForkVersion(), + eth1Address, + spec.getGenesisSpecConfig().getDepositChainId()); return metaData.getSingleDbConfiguration().getConfiguration(); } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java b/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java index edd4a5cc99a..ea35a55855e 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -29,26 +30,41 @@ import tech.pegasys.teku.infrastructure.bytes.Bytes4; import tech.pegasys.teku.storage.server.DatabaseStorageException; +@JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class DatabaseNetwork { - @JsonProperty("fork_version") + @JsonProperty(value = "fork_version", required = true) @VisibleForTesting final String forkVersion; - @JsonProperty("deposit_contract") + @JsonProperty(value = "deposit_contract", required = true) @VisibleForTesting final String depositContract; + @JsonProperty("deposit_chain_id") + @VisibleForTesting + final Long depositChainId; + @JsonCreator DatabaseNetwork( - @JsonProperty("fork_version") final String forkVersion, - @JsonProperty("deposit_contract") final String depositContract) { + @JsonProperty(value = "fork_version") final String forkVersion, + @JsonProperty(value = "deposit_contract") final String depositContract, + @JsonProperty("deposit_chain_id") final Long depositChainId) { this.forkVersion = forkVersion; this.depositContract = depositContract; + this.depositChainId = depositChainId; + } + + @VisibleForTesting + DatabaseNetwork(final String forkVersion, final String depositContract) { + this(forkVersion, depositContract, null); } public static DatabaseNetwork init( - final File source, final Bytes4 forkVersion, final Eth1Address depositContract) + final File source, + final Bytes4 forkVersion, + final Eth1Address depositContract, + final Long depositChainId) throws IOException { final String forkVersionString = forkVersion.toHexString().toLowerCase(Locale.ROOT); final String depositContractString = depositContract.toHexString().toLowerCase(Locale.ROOT); @@ -71,7 +87,7 @@ public static DatabaseNetwork init( return databaseNetwork; } else { DatabaseNetwork databaseNetwork = - new DatabaseNetwork(forkVersionString, depositContractString); + new DatabaseNetwork(forkVersionString, depositContractString, depositChainId); objectMapper.writerFor(DatabaseNetwork.class).writeValue(source, databaseNetwork); return databaseNetwork; } @@ -95,7 +111,8 @@ public boolean equals(final Object o) { } final DatabaseNetwork that = (DatabaseNetwork) o; return Objects.equals(forkVersion, that.forkVersion) - && Objects.equals(depositContract, that.depositContract); + && Objects.equals(depositContract, that.depositContract) + && Objects.equals(depositChainId, that.depositChainId); } @Override diff --git a/storage/src/test/java/tech/pegasys/teku/storage/server/network/DatabaseNetworkTest.java b/storage/src/test/java/tech/pegasys/teku/storage/server/network/DatabaseNetworkTest.java index d2ffefe2636..e52f529df75 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/server/network/DatabaseNetworkTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/server/network/DatabaseNetworkTest.java @@ -13,13 +13,21 @@ package tech.pegasys.teku.storage.server.network; +import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.WRITE_DOC_START_MARKER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.Locale; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import tech.pegasys.teku.ethereum.execution.types.Eth1Address; @@ -30,56 +38,136 @@ public class DatabaseNetworkTest { DataStructureUtil dataStructureUtil = new DataStructureUtil(TestSpecFactory.createDefault()); + private ObjectMapper objectMapper; + private static final String NETWORK_FILENAME = "network.yml"; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(new YAMLFactory().disable(WRITE_DOC_START_MARKER)); + } @Test public void shouldCreateNetworkFile(@TempDir final File tempDir) throws IOException { - final File networkFile = new File(tempDir, "network.yml"); + final File networkFile = new File(tempDir, NETWORK_FILENAME); assertThat(networkFile).doesNotExist(); final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); - assertThat(DatabaseNetwork.init(networkFile, fork, eth1Address)) + final Long depositChainId = dataStructureUtil.randomLong(); + assertThat(DatabaseNetwork.init(networkFile, fork, eth1Address, depositChainId)) .isEqualTo( new DatabaseNetwork( fork.toHexString().toLowerCase(Locale.ROOT), - eth1Address.toHexString().toLowerCase(Locale.ROOT))); + eth1Address.toHexString().toLowerCase(Locale.ROOT), + depositChainId)); assertThat(networkFile).exists(); } @Test public void shouldThrowIfForkDiffers(@TempDir final File tempDir) throws IOException { - final File networkFile = new File(tempDir, "network.yml"); + final File networkFile = new File(tempDir, NETWORK_FILENAME); assertThat(networkFile).doesNotExist(); final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); + final Long depositChainId = dataStructureUtil.randomLong(); DatabaseNetwork.init( - networkFile, dataStructureUtil.randomFork().getCurrentVersion(), eth1Address); + networkFile, + dataStructureUtil.randomFork().getCurrentVersion(), + eth1Address, + depositChainId); - assertThatThrownBy(() -> DatabaseNetwork.init(networkFile, fork, eth1Address)) + assertThatThrownBy(() -> DatabaseNetwork.init(networkFile, fork, eth1Address, depositChainId)) .isInstanceOf(DatabaseStorageException.class) .hasMessageStartingWith("Supplied fork version"); } @Test public void shouldThrowIfDepositContractDiffers(@TempDir final File tempDir) throws IOException { - final File networkFile = new File(tempDir, "network.yml"); + final File networkFile = new File(tempDir, NETWORK_FILENAME); assertThat(networkFile).doesNotExist(); final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); - DatabaseNetwork.init(networkFile, fork, dataStructureUtil.randomEth1Address()); + final Long depositChainId = dataStructureUtil.randomLong(); - assertThatThrownBy(() -> DatabaseNetwork.init(networkFile, fork, eth1Address)) + DatabaseNetwork.init(networkFile, fork, dataStructureUtil.randomEth1Address(), depositChainId); + + assertThatThrownBy(() -> DatabaseNetwork.init(networkFile, fork, eth1Address, depositChainId)) .isInstanceOf(DatabaseStorageException.class) .hasMessageStartingWith("Supplied deposit contract"); } @Test public void shouldNotThrowIfForkAndContractMatch(@TempDir final File tempDir) throws IOException { - final File networkFile = new File(tempDir, "network.yml"); + final File networkFile = new File(tempDir, NETWORK_FILENAME); assertThat(networkFile).doesNotExist(); final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); - DatabaseNetwork.init(networkFile, fork, eth1Address); + final Long depositChainId = dataStructureUtil.randomLong(); + + DatabaseNetwork.init(networkFile, fork, eth1Address, depositChainId); + + assertDoesNotThrow(() -> DatabaseNetwork.init(networkFile, fork, eth1Address, depositChainId)); + } + + @Test + void shouldWriteAndReadDatabaseNetworkWithDepositChainId(@TempDir final File tempDir) + throws IOException { + final File networkFile = new File(tempDir, NETWORK_FILENAME); + + final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); + final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); + final Long depositChainId = dataStructureUtil.randomLong(); + final DatabaseNetwork databaseNetwork = + new DatabaseNetwork(fork.toHexString(), eth1Address.toHexString(), depositChainId); + + objectMapper.writerFor(DatabaseNetwork.class).writeValue(networkFile, databaseNetwork); + final DatabaseNetwork readDatabaseNetwork = + objectMapper.readerFor(DatabaseNetwork.class).readValue(networkFile); + + assertEquals(fork.toHexString(), readDatabaseNetwork.forkVersion); + assertEquals(eth1Address.toHexString(), readDatabaseNetwork.depositContract); + assertEquals(depositChainId, readDatabaseNetwork.depositChainId); + } + + @Test + void shouldWriteAndReadDatabaseNetworkWithoutDepositChainId(@TempDir final File tempDir) + throws IOException { + final File networkFile = new File(tempDir, NETWORK_FILENAME); + + final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); + final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); + + final DatabaseNetwork databaseNetwork = + new DatabaseNetwork(fork.toHexString(), eth1Address.toHexString()); + + objectMapper.writerFor(DatabaseNetwork.class).writeValue(networkFile, databaseNetwork); + String networkContent = Files.readString(networkFile.toPath()); + + final DatabaseNetwork readDatabaseNetwork = + objectMapper.readerFor(DatabaseNetwork.class).readValue(networkFile); + + assertFalse(networkContent.contains("deposit_chain_id")); + assertEquals(fork.toHexString(), readDatabaseNetwork.forkVersion); + assertEquals(eth1Address.toHexString(), readDatabaseNetwork.depositContract); + assertNull(readDatabaseNetwork.depositChainId); + } + + @Test + void shouldNotIncludeDepositChainIdWhenNull(@TempDir final File tempDir) throws IOException { + final File networkFile = new File(tempDir, NETWORK_FILENAME); + + final Bytes4 fork = dataStructureUtil.randomFork().getCurrentVersion(); + final Eth1Address eth1Address = dataStructureUtil.randomEth1Address(); + + final DatabaseNetwork databaseNetwork = + new DatabaseNetwork(fork.toHexString(), eth1Address.toHexString(), null); + + objectMapper.writerFor(DatabaseNetwork.class).writeValue(networkFile, databaseNetwork); + String networkContent = Files.readString(networkFile.toPath()); + + final DatabaseNetwork readDatabaseNetwork = + objectMapper.readerFor(DatabaseNetwork.class).readValue(networkFile); - assertDoesNotThrow(() -> DatabaseNetwork.init(networkFile, fork, eth1Address)); + assertFalse(networkContent.contains("deposit_chain_id")); + assertNull(readDatabaseNetwork.depositChainId); } } diff --git a/teku/src/main/java/tech/pegasys/teku/cli/options/P2POptions.java b/teku/src/main/java/tech/pegasys/teku/cli/options/P2POptions.java index ede26d99337..d08cfd6ecc5 100644 --- a/teku/src/main/java/tech/pegasys/teku/cli/options/P2POptions.java +++ b/teku/src/main/java/tech/pegasys/teku/cli/options/P2POptions.java @@ -32,6 +32,7 @@ import tech.pegasys.teku.config.TekuConfiguration; import tech.pegasys.teku.networking.eth2.P2PConfig; import tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig; +import tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig; import tech.pegasys.teku.networking.p2p.libp2p.MultiaddrPeerAddress; import tech.pegasys.teku.networking.p2p.network.config.NetworkConfig; @@ -366,6 +367,17 @@ The network interface(s) on which the node listens for P2P communication. fallbackValue = "true") private boolean yamuxEnabled = NetworkConfig.DEFAULT_YAMUX_ENABLED; + // More about flood publishing + // https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#flood-publishing + @Option( + names = {"--p2p-flood-publish-enabled"}, + paramLabel = "", + showDefaultValue = Visibility.ALWAYS, + description = "Enables gossip 'floodPublish' feature", + arity = "0..1", + fallbackValue = "true") + private boolean floodPublishEnabled = GossipConfig.DEFAULT_FLOOD_PUBLISH_ENABLED; + private OptionalInt getP2pLowerBound() { if (p2pUpperBound.isPresent() && p2pLowerBound.isPresent()) { return p2pLowerBound.getAsInt() < p2pUpperBound.getAsInt() ? p2pLowerBound : p2pUpperBound; @@ -395,7 +407,8 @@ public void configure(final TekuConfiguration.Builder builder) { .isGossipScoringEnabled(gossipScoringEnabled) .peerRateLimit(peerRateLimit) .allTopicsFilterEnabled(allTopicsFilterEnabled) - .peerRequestLimit(peerRequestLimit); + .peerRequestLimit(peerRequestLimit) + .isFloodPublishEnabled(floodPublishEnabled); batchVerifyQueueCapacity.ifPresent(b::batchVerifyQueueCapacity); }) .discovery( diff --git a/teku/src/test/java/tech/pegasys/teku/cli/options/P2POptionsTest.java b/teku/src/test/java/tech/pegasys/teku/cli/options/P2POptionsTest.java index 0acf1d5096f..b32a6e29217 100644 --- a/teku/src/test/java/tech/pegasys/teku/cli/options/P2POptionsTest.java +++ b/teku/src/test/java/tech/pegasys/teku/cli/options/P2POptionsTest.java @@ -18,6 +18,7 @@ import static tech.pegasys.teku.infrastructure.async.AsyncRunnerFactory.DEFAULT_MAX_QUEUE_SIZE_ALL_SUBNETS; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig.DEFAULT_P2P_PEERS_LOWER_BOUND_ALL_SUBNETS; import static tech.pegasys.teku.networking.p2p.discovery.DiscoveryConfig.DEFAULT_P2P_PEERS_UPPER_BOUND_ALL_SUBNETS; +import static tech.pegasys.teku.networking.p2p.gossip.config.GossipConfig.DEFAULT_FLOOD_PUBLISH_ENABLED; import static tech.pegasys.teku.networking.p2p.network.config.NetworkConfig.DEFAULT_P2P_PORT; import static tech.pegasys.teku.networking.p2p.network.config.NetworkConfig.DEFAULT_P2P_PORT_IPV6; import static tech.pegasys.teku.validator.api.ValidatorConfig.DEFAULT_EXECUTOR_MAX_QUEUE_SIZE_ALL_SUBNETS; @@ -338,6 +339,35 @@ public void allSubnetsShouldNotOverrideQueuesIfExplicitlySet() { assertThat(tekuConfiguration.p2p().getBatchVerifyQueueCapacity()).isEqualTo(15_220); } + @Test + public void floodPublishEnabled_isSetCorrectly() { + final TekuConfiguration config = + getTekuConfigurationFromArguments("--p2p-flood-publish-enabled"); + assertThat(config.network().getGossipConfig().isFloodPublishEnabled()) + .isEqualTo(DEFAULT_FLOOD_PUBLISH_ENABLED); + } + + @Test + public void floodPublishEnabled_shouldNotRequireAValue() { + final TekuConfiguration config = + getTekuConfigurationFromArguments("--p2p-flood-publish-enabled"); + assertThat(config.network().getGossipConfig().isFloodPublishEnabled()).isTrue(); + } + + @Test + public void floodPublishEnabled_true() { + final TekuConfiguration config = + getTekuConfigurationFromArguments("--p2p-flood-publish-enabled=true"); + assertThat(config.network().getGossipConfig().isFloodPublishEnabled()).isTrue(); + } + + @Test + public void floodPublishEnabled_false() { + final TekuConfiguration config = + getTekuConfigurationFromArguments("--p2p-flood-publish-enabled=false"); + assertThat(config.network().getGossipConfig().isFloodPublishEnabled()).isFalse(); + } + @Test public void defaultPortsAreSetCorrectly() { final TekuConfiguration tekuConfiguration = getTekuConfigurationFromArguments();