Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ethereum/spec/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {
api project(':infrastructure:kzg')

implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'org.yaml:snakeyaml'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
implementation 'io.consensys.tuweni:tuweni-bytes'
implementation 'io.consensys.tuweni:tuweni-ssz'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,17 @@ public static SpecConfigAndParent<? extends SpecConfig> loadConfig(
}

public static SpecConfigAndParent<? extends SpecConfig> loadRemoteConfig(
final Map<String, String> config) {
final Map<String, Object> config) {
final SpecConfigReader reader = new SpecConfigReader();
if (config.containsKey(SpecConfigReader.PRESET_KEY)) {
try {
applyPreset("remote", reader, true, config.get(SpecConfigReader.PRESET_KEY));
applyPreset("remote", reader, true, (String) config.get(SpecConfigReader.PRESET_KEY));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
if (config.containsKey(SpecConfigReader.CONFIG_NAME_KEY)) {
final String configNameKey = config.get(SpecConfigReader.CONFIG_NAME_KEY);
final String configNameKey = (String) config.get(SpecConfigReader.CONFIG_NAME_KEY);
try {
processConfig(configNameKey, reader, true);
} catch (IllegalArgumentException exception) {
Expand All @@ -87,9 +87,9 @@ public static SpecConfigAndParent<? extends SpecConfig> loadRemoteConfig(
static void processConfig(
final String source, final SpecConfigReader reader, final boolean ignoreUnknownConfigItems) {
try (final InputStream configFile = loadConfigurationFile(source)) {
final Map<String, String> configValues = reader.readValues(configFile);
final Map<String, Object> configValues = reader.readValues(configFile);
final Optional<String> maybePreset =
Optional.ofNullable(configValues.get(SpecConfigReader.PRESET_KEY));
Optional.ofNullable((String) configValues.get(SpecConfigReader.PRESET_KEY));

// Legacy config files won't have a preset field
if (maybePreset.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@

import static tech.pegasys.teku.spec.config.SpecConfigFormatter.camelToSnakeCase;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
Expand Down Expand Up @@ -131,14 +129,14 @@ public SpecConfigAndParent<? extends SpecConfig> build(
*/
public void readAndApply(final InputStream source, final boolean ignoreUnknownConfigItems)
throws IOException {
final Map<String, String> rawValues = readValues(source);
final Map<String, Object> rawValues = readValues(source);
loadFromMap(rawValues, ignoreUnknownConfigItems);
}

public void loadFromMap(
final Map<String, String> rawValues, final boolean ignoreUnknownConfigItems) {
final Map<String, String> unprocessedConfig = new HashMap<>(rawValues);
final Map<String, String> apiSpecConfig = new HashMap<>(rawValues);
final Map<String, Object> rawValues, final boolean ignoreUnknownConfigItems) {
final Map<String, Object> unprocessedConfig = new HashMap<>(rawValues);
final Map<String, Object> apiSpecConfig = new HashMap<>(rawValues);
// Remove any keys that we're ignoring
KEYS_TO_IGNORE.forEach(
key -> {
Expand Down Expand Up @@ -240,16 +238,10 @@ public void loadFromMap(
}
}

@SuppressWarnings("unchecked")
public Map<String, String> readValues(final InputStream source) throws IOException {
final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
public Map<String, Object> readValues(final InputStream source) throws IOException {
final YamlConfigReader reader = new YamlConfigReader();
try {
return (Map<String, String>)
mapper
.readerFor(
mapper.getTypeFactory().constructMapType(Map.class, String.class, String.class))
.readValues(source)
.next();
return reader.readValues(source);
} catch (NoSuchElementException e) {
throw new IllegalArgumentException("Supplied spec config is empty");
} catch (RuntimeJsonMappingException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright Consensys Software Inc., 2025
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.spec.config;

import java.io.InputStream;
import java.util.Map;
import java.util.NoSuchElementException;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;

public class YamlConfigReader {
private final Yaml yaml;

public YamlConfigReader() {
final Resolver resolver = new CustomResolver();
yaml =
new Yaml(
new Constructor(new LoaderOptions()),
new Representer(new DumperOptions()),
new DumperOptions(),
resolver);
}

public Map<String, Object> readValues(final InputStream source) {
final Map<String, Object> values = yaml.load(source);
Comment thread Dismissed
if (values == null) {
throw new NoSuchElementException();
}
return values;
}

/*
* For the purposes of reading config, we just want strings, we don't want numeric values.
* This will ensure that we get hex values through as unparsed, and ensure we're getting
* things such as CRC checks through without them being altered.
* The implicit resolver below is basically saying just match numerics as string.
*/
public static class CustomResolver extends Resolver {
@Override
protected void addImplicitResolvers() {
addImplicitResolver(Tag.STR, INT, "+-0123456789.");
Comment thread
rolfyone marked this conversation as resolved.
super.addImplicitResolvers();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
import static tech.pegasys.teku.spec.config.SpecConfigAssertions.assertAllBellatrixFieldsSet;
import static tech.pegasys.teku.spec.config.SpecConfigAssertions.assertAllFieldsSet;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.Resources;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
Expand Down Expand Up @@ -55,7 +53,7 @@ public void shouldLoadAllKnownNetworks(final Eth2Network network) throws Excepti
* sufficient.
*/
@ParameterizedTest(name = "{0}")
@ValueSource(strings = {"holesky", "mainnet"})
@ValueSource(strings = {"hoodi", "mainnet"})
public void shouldMaintainConfigNameBackwardsCompatibility(final String name) {
final SpecConfig config = SpecConfigLoader.loadConfig(name).specConfig();
assertThat(config.getRawConfig().get("CONFIG_NAME")).isEqualTo(name);
Expand Down Expand Up @@ -161,10 +159,8 @@ private InputStream loadInvalidFile(final String file) {
return getClass().getResourceAsStream("invalid/" + file);
}

private Map<String, String> readJsonConfig(final InputStream source) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
return mapper
.readerFor(mapper.getTypeFactory().constructMapType(Map.class, String.class, String.class))
.readValue(source);
private Map<String, Object> readJsonConfig(final InputStream source) {
YamlConfigReader reader = new YamlConfigReader();
return reader.readValues(source);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@

package tech.pegasys.teku.spec.config;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static tech.pegasys.teku.spec.config.SpecConfigAssertions.assertAllAltairFieldsSet;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;

public class SpecConfigReaderTest {
Expand Down Expand Up @@ -141,7 +146,7 @@ public void read_invalidLong_wrongType() {
assertThatThrownBy(() -> readConfig(stream))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(
"Cannot read spec config: Cannot deserialize value of type `java.lang.String` from Array"));
"Failed to parse value for constant VALIDATOR_REGISTRY_LIMIT"));
}

@Test
Expand Down Expand Up @@ -187,6 +192,19 @@ public void read_invalidUInt64_tooLarge() {
"Failed to parse value for constant MIN_GENESIS_TIME: '18446744073709552001'"));
}

@Test
public void read_localConfigFile_notLoadingDefaults(@TempDir final Path tempDir)
throws IOException {
Files.writeString(
tempDir.resolve("test.yaml"), "PRESET_BASE: 'mainnet'\nCONFIG_NAME: 'mainnet'", UTF_8);
Map<String, Object> data =
reader.readValues(Files.newInputStream(tempDir.resolve("test.yaml")));
reader.loadFromMap(data, true);
assertThatThrownBy(reader::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("SECONDS_PER_ETH1_BLOCK");
}

@Test
public void read_invalidBytes4_tooLarge() {
processFileAsInputStream(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Consensys Software Inc., 2025
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.spec.config;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;

@SuppressWarnings("unchecked")
public class YamlConfigReaderTest {
private final YamlConfigReader reader = new YamlConfigReader();

@Test
void shouldReadSimpleObjectToStringMap() {
final String testData =
"""
version: 250
info: the info
""";
final Map<String, Object> objData = reader.readValues(IOUtils.toInputStream(testData, UTF_8));
assertThat(objData.get("version")).isInstanceOf(String.class).isEqualTo("250");
assertThat(objData.get("info")).isInstanceOf(String.class).isEqualTo("the info");
}

@Test
void shouldReadObjectWithListOfObjects() {
final String testData =
"""
data:
- a: 1
aa: 11
- b: two
bb: three
""";

final Map<String, Object> objData = reader.readValues(IOUtils.toInputStream(testData, UTF_8));
assertThat(objData.get("data")).isInstanceOf(List.class);
final List<Map<String, Object>> list = (List<Map<String, Object>>) objData.get("data");
for (Object o : list) {
assertThat(o).isInstanceOf(Map.class);
assertThat(((Map) o).size()).isEqualTo(2);
}
assertThat(list)
.containsExactly(Map.of("a", "1", "aa", "11"), Map.of("b", "two", "bb", "three"));
}

@Test
void shouldReadEth1Address() {
final String testData =
"""
DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242
""";
final Map<String, Object> objData = reader.readValues(IOUtils.toInputStream(testData, UTF_8));
assertThat(objData.get("DEPOSIT_CONTRACT_ADDRESS"))
.isInstanceOf(String.class)
.isEqualTo("0x4242424242424242424242424242424242424242");
}
}
2 changes: 2 additions & 0 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencyManagement {

dependency 'com.google.guava:guava:33.1.0-jre'

dependency 'org.yaml:snakeyaml:2.4'

dependency 'org.jsoup:jsoup:1.20.1'

dependency 'com.launchdarkly:okhttp-eventsource:4.1.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package tech.pegasys.teku.cli.subcommand;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -47,7 +48,7 @@ static Spec getSpec(final OkHttpValidatorMinimalTypeDefClient apiClient) {
try {
return apiClient
.getSpec()
.map(SpecConfigLoader::loadRemoteConfig)
.map(config -> SpecConfigLoader.loadRemoteConfig(new HashMap<>(config)))
.map(SpecFactory::create)
.orElseThrow();
} catch (final Throwable ex) {
Expand Down Expand Up @@ -78,13 +79,13 @@ private static Spec getSpecWithFailovers(
throw new InvalidConfigurationException(errMsg);
}

private static <T> T retry(final Callable<T> f) {
private static <T> T retry(final Callable<T> callable) {
try {
return f.call();
return callable.call();
} catch (Throwable ex) {
logError(ex);
sleep();
return retry(f);
return retry(callable);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.common.io.Resources;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -70,7 +71,8 @@ void shouldDefaultNetworkConfigThatMovedFromConstants() throws IOException {
final ObjectMapper objectMapper = new ObjectMapper();
TypeReference<Map<String, String>> typeReference = new TypeReference<>() {};
Map<String, String> data = objectMapper.readValue(jsonConfig, typeReference);
final SpecConfig specConfig = SpecConfigLoader.loadRemoteConfig(data).specConfig();
final SpecConfig specConfig =
SpecConfigLoader.loadRemoteConfig(new HashMap<>(data)).specConfig();

// Check values not assigned, using default values
assertThat(specConfig.getMaxPayloadSize()).isEqualTo(10485760);
Expand Down