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/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); + } + } + } + } +}