diff --git a/build.gradle b/build.gradle index 5124105b7f4..9b91c74e362 100644 --- a/build.gradle +++ b/build.gradle @@ -297,10 +297,13 @@ allprojects { def refTestVersion = 'v1.4.0' // Arbitrary change to refresh cache number: 1 def blsRefTestVersion = 'v0.1.2' +def slashingProtectionInterchangeRefTestVersion = 'v5.3.0' def refTestBaseUrl = 'https://github.com/ethereum/consensus-spec-tests/releases/download' def blsRefTestBaseUrl = 'https://github.com/ethereum/bls12-381-tests/releases/download' +def slashingProtectionInterchangeRefTestBaseUrl = 'https://github.com/eth-clients/slashing-protection-interchange-tests/archive/refs/tags' def refTestDownloadDir = "${buildDir}/refTests/${refTestVersion}" def blsRefTestDownloadDir = "${buildDir}/blsRefTests/${blsRefTestVersion}" +def slashingProtectionInterchangeRefTestDownloadDir = "${buildDir}/slashingProtectionInterchangeRefTests/${slashingProtectionInterchangeRefTestVersion}" def refTestExpandDir = "${project.rootDir}/eth-reference-tests/src/referenceTest/resources/consensus-spec-tests/" task downloadEthRefTests(type: Download) { @@ -321,7 +324,15 @@ task downloadBlsRefTests(type: Download) { overwrite false } -task downloadRefTests(dependsOn: [downloadEthRefTests, downloadBlsRefTests]) +task downloadSlashingProtectionInterchangeRefTests(type: Download) { + src([ + "${slashingProtectionInterchangeRefTestBaseUrl}/${slashingProtectionInterchangeRefTestVersion}.tar.gz" + ]) + dest "${slashingProtectionInterchangeRefTestDownloadDir}/slashing-protection-interchange-tests.tar.gz" + overwrite false +} + +task downloadRefTests(dependsOn: [downloadEthRefTests, downloadBlsRefTests, downloadSlashingProtectionInterchangeRefTests]) task cleanRefTestsGeneral(type: Delete) { delete "${refTestExpandDir}/tests/general" @@ -359,8 +370,25 @@ task expandRefTestsBls(type: Copy, dependsOn: [cleanRefTestsBls, downloadBlsRefT into "${refTestExpandDir}/tests/bls" } -task expandRefTests(dependsOn: [expandRefTestsGeneral, expandRefTestsMainnet, expandRefTestsMinimal, expandRefTestsBls]) -task cleanRefTests(dependsOn: [cleanRefTestsGeneral, cleanRefTestsMainnet, cleanRefTestsMinimal, cleanRefTestsBls]) +task cleanRefTestsSlashingProtectionInterchange(type: Delete) { + delete "${refTestExpandDir}/tests/slashing-protection-interchange" +} + +task expandRefTestsSlashingProtectionInterchange(type: Copy, dependsOn: [cleanRefTestsSlashingProtectionInterchange, downloadSlashingProtectionInterchangeRefTests]) { + from { + tarTree("${slashingProtectionInterchangeRefTestDownloadDir}/slashing-protection-interchange-tests.tar.gz").matching { + include "**/tests/generated/*.json" + // flatten + eachFile { FileCopyDetails fcp -> + fcp.path = fcp.name + } + } + } + into "${refTestExpandDir}/tests/slashing-protection-interchange" +} + +task expandRefTests(dependsOn: [expandRefTestsGeneral, expandRefTestsMainnet, expandRefTestsMinimal, expandRefTestsBls, expandRefTestsSlashingProtectionInterchange]) +task cleanRefTests(dependsOn: [cleanRefTestsGeneral, cleanRefTestsMainnet, cleanRefTestsMinimal, cleanRefTestsBls, cleanRefTestsSlashingProtectionInterchange]) task deploy() {} diff --git a/data/dataexchange/src/test/java/tech/pegasys/teku/data/SlashingProtectionImporterTest.java b/data/dataexchange/src/test/java/tech/pegasys/teku/data/SlashingProtectionImporterTest.java index 29f443e8e1e..0ac17428386 100644 --- a/data/dataexchange/src/test/java/tech/pegasys/teku/data/SlashingProtectionImporterTest.java +++ b/data/dataexchange/src/test/java/tech/pegasys/teku/data/SlashingProtectionImporterTest.java @@ -163,6 +163,31 @@ void shouldImportFileOverRepairedRecords(@TempDir Path tempDir) throws Exception repairedEpoch)); } + @Test + void shouldFailImportingIfValidatorExistingRecordHasDifferentGenesisValidatorsRoot( + @TempDir Path tempDir) throws URISyntaxException, IOException { + final SlashingProtectionImporter importer = new SlashingProtectionImporter(tempDir); + + final File slashProtection = getResourceFile("format2_minimal.json"); + + importer.initialise(slashProtection); + + Map errors = importer.updateLocalRecords(__ -> {}); + + assertThat(errors).isEmpty(); + + final File slashProtectionWithDifferentGvr = + getResourceFile("format2_minimal_different_genesis_validators_root.json"); + + importer.initialise(slashProtectionWithDifferentGvr); + + errors = importer.updateLocalRecords(__ -> {}); + + assertThat(errors) + .hasSize(1) + .containsEntry(publicKey, "Genesis validators root did not match what was expected."); + } + private ValidatorSigningRecord loadSigningRecord(final File repairedRuleFile) throws IOException { return ValidatorSigningRecord.fromBytes( Bytes.wrap(Files.readAllBytes(repairedRuleFile.toPath()))); @@ -182,9 +207,11 @@ private File usingResourceFile(final String resourceFileName, final Path tempDir throws URISyntaxException, IOException { final Path tempFile = tempDir.resolve(pubkey + ".yml").toAbsolutePath(); Files.copy( - new File(Resources.getResource(resourceFileName).toURI()).toPath(), - tempFile, - StandardCopyOption.REPLACE_EXISTING); + getResourceFile(resourceFileName).toPath(), tempFile, StandardCopyOption.REPLACE_EXISTING); return tempFile.toFile(); } + + private File getResourceFile(final String resourceFileName) throws URISyntaxException { + return new File(Resources.getResource(resourceFileName).toURI()); + } } diff --git a/data/dataexchange/src/test/resources/format2_minimal.json b/data/dataexchange/src/test/resources/format2_minimal.json index 9a4f1645fef..89c0bab40c1 100644 --- a/data/dataexchange/src/test/resources/format2_minimal.json +++ b/data/dataexchange/src/test/resources/format2_minimal.json @@ -7,7 +7,8 @@ { "pubkey": "0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed", "signed_blocks": [ - {"slot": "81952" + { + "slot": "81952" } ], "signed_attestations": [ diff --git a/data/dataexchange/src/test/resources/format2_minimal_different_genesis_validators_root.json b/data/dataexchange/src/test/resources/format2_minimal_different_genesis_validators_root.json new file mode 100644 index 00000000000..9a926c08fd6 --- /dev/null +++ b/data/dataexchange/src/test/resources/format2_minimal_different_genesis_validators_root.json @@ -0,0 +1,22 @@ +{ + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000123457" + }, + "data": [ + { + "pubkey": "0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed", + "signed_blocks": [ + { + "slot": "81952" + } + ], + "signed_attestations": [ + { + "source_epoch": "2290", + "target_epoch": "3007" + } + ] + } + ] +} \ No newline at end of file diff --git a/eth-reference-tests/build.gradle b/eth-reference-tests/build.gradle index 17954ada5fe..65b4fb739d1 100644 --- a/eth-reference-tests/build.gradle +++ b/eth-reference-tests/build.gradle @@ -15,9 +15,12 @@ dependencies { referenceTestImplementation project(':storage') referenceTestImplementation testFixtures(project(':storage')) referenceTestImplementation project(':infrastructure:async') + referenceTestImplementation project(':infrastructure:io') referenceTestImplementation testFixtures(project(':infrastructure:async')) referenceTestImplementation testFixtures(project(':infrastructure:metrics')) referenceTestImplementation project(':infrastructure:time') + referenceTestImplementation project(':data:dataexchange') + referenceTestImplementation project(':data:serializer') referenceTestImplementation 'org.hyperledger.besu:plugin-api' referenceTestImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/Eth2ReferenceTestCase.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/Eth2ReferenceTestCase.java index 2be8f4414ea..ca669b4a0d3 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/Eth2ReferenceTestCase.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/Eth2ReferenceTestCase.java @@ -31,6 +31,7 @@ import tech.pegasys.teku.reference.phase0.rewards.RewardsTestExecutorPhase0; import tech.pegasys.teku.reference.phase0.sanity.SanityTests; import tech.pegasys.teku.reference.phase0.shuffling.ShufflingTestExecutor; +import tech.pegasys.teku.reference.phase0.slashing_protection_interchange.SlashingProtectionInterchangeTestExecutor; import tech.pegasys.teku.reference.phase0.ssz_generic.SszGenericTests; import tech.pegasys.teku.reference.phase0.ssz_static.SszTestExecutor; @@ -48,6 +49,7 @@ public abstract class Eth2ReferenceTestCase { .putAll(SszGenericTests.SSZ_GENERIC_TEST_TYPES) .putAll(OperationsTestExecutor.OPERATIONS_TEST_TYPES) .putAll(SanityTests.SANITY_TEST_TYPES) + .put("slashing-protection-interchange", new SlashingProtectionInterchangeTestExecutor()) .put("light_client/single_merkle_proof", TestExecutor.IGNORE_TESTS) .put("light_client/sync", TestExecutor.IGNORE_TESTS) .put("light_client/update_ranking", TestExecutor.IGNORE_TESTS) diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/ManualReferenceTestRunner.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/ManualReferenceTestRunner.java index 8db4b6cbafb..abdbc308224 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/ManualReferenceTestRunner.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/ManualReferenceTestRunner.java @@ -42,7 +42,7 @@ public class ManualReferenceTestRunner extends Eth2ReferenceTestCase { * *

May be overridden by the ENV_TEST_TYPE environment variable. */ - private static final String TEST_TYPE = "fork_choice"; + private static final String TEST_TYPE = ""; /** * Filter test to run to those from the specified spec. One of general, minimal or mainnet diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/TestDataUtils.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/TestDataUtils.java index ddedd55c3af..7e2039767ae 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/TestDataUtils.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/TestDataUtils.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Function; @@ -29,11 +30,13 @@ import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; import tech.pegasys.teku.infrastructure.ssz.SszData; import tech.pegasys.teku.infrastructure.ssz.schema.SszSchema; +import tech.pegasys.teku.provider.JsonProvider; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; public class TestDataUtils { private static final YAMLFactory YAML_FACTORY; + private static final JsonProvider JSON_PROVIDER; static { final LoaderOptions loaderOptions = new LoaderOptions(); @@ -41,6 +44,7 @@ public class TestDataUtils { // https://github.com/FasterXML/jackson-dataformats-text/tree/2.15/yaml#maximum-input-yaml-document-size-3-mb loaderOptions.setCodePointLimit(1024 * 1024 * 100); YAML_FACTORY = YAMLFactory.builder().loaderOptions(loaderOptions).build(); + JSON_PROVIDER = new JsonProvider(); } public static T loadSsz( @@ -85,7 +89,7 @@ public static T loadYaml( throws IOException { final Path path = testDefinition.getTestDirectory().resolve(fileName); try (final InputStream in = Files.newInputStream(path)) { - return new ObjectMapper(YAML_FACTORY).readerFor(type).readValue(in); + return new ObjectMapper(YAML_FACTORY).readValue(in, type); } } @@ -100,4 +104,18 @@ public static T loadYaml( return type.deserialize(in); } } + + public static T loadJson( + final TestDefinition testDefinition, final String fileName, final Class type) + throws IOException { + final Path path = testDefinition.getTestDirectory().resolve(fileName); + try (final InputStream in = Files.newInputStream(path)) { + return JSON_PROVIDER.getObjectMapper().readValue(in, type); + } + } + + public static void writeJsonToFile(final T object, final Path file) throws IOException { + final String json = JSON_PROVIDER.getObjectMapper().writeValueAsString(object); + Files.writeString(file, json, StandardCharsets.UTF_8); + } } diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/slashing_protection_interchange/SlashingProtectionInterchangeTestExecutor.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/slashing_protection_interchange/SlashingProtectionInterchangeTestExecutor.java new file mode 100644 index 00000000000..e9cb0119f85 --- /dev/null +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/slashing_protection_interchange/SlashingProtectionInterchangeTestExecutor.java @@ -0,0 +1,161 @@ +/* + * 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.reference.phase0.slashing_protection_interchange; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.data.SlashingProtectionImporter; +import tech.pegasys.teku.data.slashinginterchange.SlashingProtectionInterchangeFormat; +import tech.pegasys.teku.ethtests.finder.TestDefinition; +import tech.pegasys.teku.infrastructure.io.SyncDataAccessor; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.reference.TestDataUtils; +import tech.pegasys.teku.reference.TestExecutor; +import tech.pegasys.teku.reference.phase0.slashing_protection_interchange.SlashingProtectionInterchangeTestExecutor.TestData.Step; +import tech.pegasys.teku.spec.signatures.LocalSlashingProtector; + +public class SlashingProtectionInterchangeTestExecutor implements TestExecutor { + + private static final Logger LOG = LogManager.getLogger(); + + @Override + public void runTest(final TestDefinition testDefinition) throws Throwable { + final TestData testData = + TestDataUtils.loadJson(testDefinition, testDefinition.getTestName(), TestData.class); + + // our implementation fails when importing one of the keys in an interchange, which is already + // in our slashprotection directory with a different genesis validators root. However, the test + // does not import any keys. This case is covered by + // SlashingProtectionImporterTest#shouldFailImportingIfValidatorExistingRecordHasDifferentGenesisValidatorsRoot() + if (testData.name.startsWith("wrong_genesis_validators_root")) { + LOG.info("Skipping {}", testData.name); + return; + } + + LOG.info("Running {}", testData.name); + + final Path slashingProtectionPath = Files.createTempDirectory("slashprotection"); + try { + runTest(testData, slashingProtectionPath); + } finally { + deleteDirectory(slashingProtectionPath); + } + } + + private void runTest(final TestData testData, final Path slashingProtectionPath) { + final SlashingProtectionImporter importer = + new SlashingProtectionImporter(slashingProtectionPath); + final LocalSlashingProtector slashingProtector = + new LocalSlashingProtector( + SyncDataAccessor.create(slashingProtectionPath), slashingProtectionPath); + testData.steps.forEach(step -> runStep(step, importer, slashingProtector)); + } + + private void runStep( + final Step step, + final SlashingProtectionImporter importer, + final LocalSlashingProtector slashingProtector) { + final Map importErrors = importInterchange(importer, step.interchange); + if (step.shouldSucceed) { + assertThat(importErrors).isEmpty(); + } else { + assertThat(importErrors).isNotEmpty(); + } + final Bytes32 genesisValidatorsRoot = step.interchange.metadata.genesisValidatorsRoot; + step.blocks.forEach( + block -> + assertThat( + slashingProtector.maySignBlock(block.pubkey, genesisValidatorsRoot, block.slot)) + .isCompletedWithValue(block.shouldSucceed)); + step.attestations.forEach( + attestation -> + assertThat( + slashingProtector.maySignAttestation( + attestation.pubkey, + genesisValidatorsRoot, + attestation.sourceEpoch, + attestation.targetEpoch)) + .isCompletedWithValue(attestation.shouldSucceed)); + } + + private Map importInterchange( + final SlashingProtectionImporter importer, + final SlashingProtectionInterchangeFormat interchange) { + try { + final Path importFile = Files.createTempFile("import", ".json"); + TestDataUtils.writeJsonToFile(interchange, importFile); + final Optional initialiseError = importer.initialise(importFile.toFile()); + assertThat(initialiseError).isEmpty(); + // cleanup + Files.delete(importFile); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return importer.updateLocalRecords(status -> LOG.info("Import status: " + status)); + } + + private void deleteDirectory(final Path dir) { + try (DirectoryStream files = Files.newDirectoryStream(dir)) { + for (Path file : files) { + if (Files.isRegularFile(file)) { + Files.delete(file); + } + } + Files.delete(dir); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public record TestData( + String name, + @JsonProperty("genesis_validators_root") Bytes32 genesisValidatorsRoot, + List steps) { + + public record Step( + @JsonProperty("should_succeed") boolean shouldSucceed, + // we don't fail importing when the interchange contains slashable data, so can safely + // ignore this field in the tests + @JsonProperty("contains_slashable_data") boolean containsSlashableData, + SlashingProtectionInterchangeFormat interchange, + List blocks, + List attestations) {} + + public record Block( + BLSPublicKey pubkey, + UInt64 slot, + @JsonProperty("signing_root") Bytes32 signingRoot, + @JsonProperty("should_succeed") boolean shouldSucceed) {} + + public record Attestation( + BLSPublicKey pubkey, + @JsonProperty("source_epoch") UInt64 sourceEpoch, + @JsonProperty("target_epoch") UInt64 targetEpoch, + @JsonProperty("signing_root") Bytes32 signingRoot, + @JsonProperty("should_succeed") boolean shouldSucceed) {} + } +} diff --git a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/BlsRefTestFinder.java b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/BlsRefTestFinder.java index 14ede1f211d..a67f28e2af7 100644 --- a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/BlsRefTestFinder.java +++ b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/BlsRefTestFinder.java @@ -26,27 +26,27 @@ public class BlsRefTestFinder implements TestFinder { @Override @MustBeClosed - public Stream findTests(final String fork, final String spec, final Path testRoot) - throws IOException { - if (!spec.equals("bls")) { + public Stream findTests( + final String fork, final String config, final Path testRoot) throws IOException { + if (!config.equals("bls")) { return Stream.empty(); } return Files.list(testRoot) .filter(path -> path.toFile().isDirectory()) - .flatMap(unchecked(path -> findBlsTests(spec, testRoot, path))); + .flatMap(unchecked(path -> findBlsTests(config, testRoot, path))); } @MustBeClosed private Stream findBlsTests( - final String spec, final Path testRoot, final Path testCategoryDir) throws IOException { - final String testType = "bls/" + testRoot.relativize(testCategoryDir).toString(); + final String config, final Path testRoot, final Path testCategoryDir) throws IOException { + final String testType = "bls/" + testRoot.relativize(testCategoryDir); return Files.list(testCategoryDir) .filter(file -> file.toFile().getName().endsWith(".yaml")) .map( testFile -> new TestDefinition( "", - spec, + config, testType, testFile.toFile().getName(), testRoot.relativize(testCategoryDir))); diff --git a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/ReferenceTestFinder.java b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/ReferenceTestFinder.java index 656471c79c4..d58be7186fa 100644 --- a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/ReferenceTestFinder.java +++ b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/ReferenceTestFinder.java @@ -47,7 +47,10 @@ public static Stream findReferenceTests() throws IOException { private static Stream findTestTypes(final Path specDirectory) throws IOException { final String spec = specDirectory.getFileName().toString(); if (spec.equals("bls")) { - return new BlsRefTestFinder().findTests(TestFork.PHASE0, spec, specDirectory); + return new BlsRefTestFinder().findTests("", spec, specDirectory); + } + if (spec.equals("slashing-protection-interchange")) { + return new SlashingProtectionInterchangeRefTestFinder().findTests("", spec, specDirectory); } return SUPPORTED_FORKS.stream() .flatMap( @@ -60,7 +63,6 @@ private static Stream findTestTypes(final Path specDirectory) th return Stream.of( new BlsTestFinder(), new KzgTestFinder(), - new BlsRefTestFinder(), new SszTestFinder("ssz_generic"), new SszTestFinder("ssz_static"), new ShufflingTestFinder(), diff --git a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/SlashingProtectionInterchangeRefTestFinder.java b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/SlashingProtectionInterchangeRefTestFinder.java new file mode 100644 index 00000000000..6470bb4ea3f --- /dev/null +++ b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/SlashingProtectionInterchangeRefTestFinder.java @@ -0,0 +1,42 @@ +/* + * 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.ethtests.finder; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class SlashingProtectionInterchangeRefTestFinder implements TestFinder { + + @Override + @MustBeClosed + public Stream findTests( + final String fork, final String config, final Path testRoot) throws IOException { + if (!config.equals("slashing-protection-interchange")) { + return Stream.empty(); + } + return Files.list(testRoot) + .filter(file -> file.toFile().getName().endsWith(".json")) + .map( + testFile -> + new TestDefinition( + fork, + config, + config, + testFile.toFile().getName(), + testRoot.relativize(testFile.getParent()))); + } +}