From f482f88902f840b4b952a07e7c516ce77e9b450a Mon Sep 17 00:00:00 2001 From: Stefan Bratanov Date: Wed, 10 Apr 2024 17:24:12 +0100 Subject: [PATCH] Add support for slashing interchange format tests --- build.gradle | 34 ++++++- eth-reference-tests/build.gradle | 3 + .../teku/reference/Eth2ReferenceTestCase.java | 2 + .../pegasys/teku/reference/TestDataUtils.java | 16 ++++ ...hingProtectionInterchangeTestExecutor.java | 93 +++++++++++++++++++ .../ethtests/finder/BlsRefTestFinder.java | 2 +- .../ethtests/finder/ReferenceTestFinder.java | 6 +- ...ashingProtectionInterchangeTestFinder.java | 42 +++++++++ 8 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/slashing_protection_interchange/SlashingProtectionInterchangeTestExecutor.java create mode 100644 eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/SlashingProtectionInterchangeTestFinder.java diff --git a/build.gradle b/build.gradle index 5124105b7f4..cc70c059324 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 slashingProtectionRefTestVersion = '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 slashingProtectionRefTestBaseUrl = 'https://github.com/eth-clients/slashing-protection-interchange-tests/archive/refs/tags' def refTestDownloadDir = "${buildDir}/refTests/${refTestVersion}" def blsRefTestDownloadDir = "${buildDir}/blsRefTests/${blsRefTestVersion}" +def slashingProtectionRefTestDownloadDir = "${buildDir}/slashingProtectionRefTests/${slashingProtectionRefTestVersion}" 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 downloadSlashingProtectionRefTests(type: Download) { + src([ + "${slashingProtectionRefTestBaseUrl}/${slashingProtectionRefTestVersion}.tar.gz" + ]) + dest "${slashingProtectionRefTestDownloadDir}/slashing-protection-interchange-tests.tar.gz" + overwrite false +} + +task downloadRefTests(dependsOn: [downloadEthRefTests, downloadBlsRefTests, downloadSlashingProtectionRefTests]) 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 cleanRefTestsSlashingProtection(type: Delete) { + delete "${refTestExpandDir}/tests/slashing-protection-interchange" +} + +task expandRefTestsSlashingProtection(type: Copy, dependsOn: [cleanRefTestsSlashingProtection, downloadSlashingProtectionRefTests]) { + into "${refTestExpandDir}/tests/slashing-protection-interchange" + from { + tarTree("${slashingProtectionRefTestDownloadDir}/slashing-protection-interchange-tests.tar.gz").matching { + include "**/tests/generated/*.json" + // flatten the directory structure + eachFile { FileCopyDetails fcp -> + fcp.path = fcp.name + } + } + } +} + +task expandRefTests(dependsOn: [expandRefTestsGeneral, expandRefTestsMainnet, expandRefTestsMinimal, expandRefTestsBls, expandRefTestsSlashingProtection]) +task cleanRefTests(dependsOn: [cleanRefTestsGeneral, cleanRefTestsMainnet, cleanRefTestsMinimal, cleanRefTestsBls, cleanRefTestsSlashingProtection]) task deploy() {} 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/TestDataUtils.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/TestDataUtils.java index ddedd55c3af..bd696597ee1 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 @@ -13,6 +13,8 @@ package tech.pegasys.teku.reference; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLParser; @@ -29,11 +31,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 +45,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( @@ -100,4 +105,15 @@ public static T loadYaml( return type.deserialize(in); } } + + public static JsonNode loadJson(final TestDefinition testDefinition, final String fileName) + throws IOException { + final Path path = testDefinition.getTestDirectory().resolve(fileName); + return JSON_PROVIDER.getObjectMapper().readTree(Files.newInputStream(path)); + } + + public static T jsonToObject(final String json, final Class type) + throws JsonProcessingException { + return JSON_PROVIDER.jsonToObject(json, type); + } } 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..af2c875c878 --- /dev/null +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/slashing_protection_interchange/SlashingProtectionInterchangeTestExecutor.java @@ -0,0 +1,93 @@ +/* + * 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.databind.JsonNode; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +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.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.spec.signatures.LocalSlashingProtector; + +public class SlashingProtectionInterchangeTestExecutor implements TestExecutor { + + private static final Logger LOG = LogManager.getLogger(); + + //TODO: implement the logic + @Override + public void runTest(final TestDefinition testDefinition) throws Throwable { + final JsonNode testNode = TestDataUtils.loadJson(testDefinition, testDefinition.getTestName()); + + final String testName = testNode.get("name").asText(); + final Bytes32 genesisValidatorsRoot = + Bytes32.fromHexString(testNode.get("genesis_validators_root").asText()); + + LOG.info("Running {}", testName); + + final Path tempDir = Files.createTempDirectory(testName); + + final Path slashProtectionPath = tempDir.resolve("slashprotection"); + + final Path slashProtectionImportFile = tempDir.resolve("import.yml"); + + Files.writeString( + slashProtectionImportFile, + testNode.get("steps").get(0).get("interchange").toString(), + StandardCharsets.UTF_8); + + final BLSPublicKey pubkey = + BLSPublicKey.fromHexString( + "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"); + + Files.createDirectories(slashProtectionPath); + SlashingProtectionImporter importer = new SlashingProtectionImporter(slashProtectionPath); + importer.initialise(slashProtectionImportFile.toFile()); + final Map errors = importer.updateLocalRecords((__) -> {}); + + assertThat(errors).isEmpty(); + + LocalSlashingProtector localSlashingProtector = + new LocalSlashingProtector( + SyncDataAccessor.create(slashProtectionPath), slashProtectionPath); + assertThat( + localSlashingProtector.maySignBlock(pubkey, genesisValidatorsRoot, UInt64.valueOf(10))) + .isCompletedWithValue(false); + assertThat( + localSlashingProtector.maySignBlock(pubkey, genesisValidatorsRoot, UInt64.valueOf(13))) + .isCompletedWithValue(false); + assertThat( + localSlashingProtector.maySignBlock(pubkey, genesisValidatorsRoot, UInt64.valueOf(14))) + .isCompletedWithValue(true); + assertThat( + localSlashingProtector.maySignAttestation( + pubkey, genesisValidatorsRoot, UInt64.valueOf(0), UInt64.valueOf(2))) + .isCompletedWithValue(false); + assertThat( + localSlashingProtector.maySignAttestation( + pubkey, genesisValidatorsRoot, UInt64.valueOf(1), UInt64.valueOf(3))) + .isCompletedWithValue(false); + } +} 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..414fde73f21 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 @@ -39,7 +39,7 @@ public Stream findTests(final String fork, final String spec, fi @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 testType = "bls/" + testRoot.relativize(testCategoryDir); return Files.list(testCategoryDir) .filter(file -> file.toFile().getName().endsWith(".yaml")) .map( 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..66b6905431c 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 SlashingProtectionInterchangeTestFinder().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/SlashingProtectionInterchangeTestFinder.java b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/SlashingProtectionInterchangeTestFinder.java new file mode 100644 index 00000000000..5d5e8e8345b --- /dev/null +++ b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/SlashingProtectionInterchangeTestFinder.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 SlashingProtectionInterchangeTestFinder implements TestFinder { + + @Override + @MustBeClosed + public Stream findTests(final String fork, final String spec, final Path testRoot) + throws IOException { + if (!spec.equals("slashing-protection-interchange")) { + return Stream.empty(); + } + return Files.list(testRoot) + .filter(file -> file.toFile().getName().endsWith(".json")) + .map( + testFile -> + new TestDefinition( + fork, + spec, + spec, + testFile.toFile().getName(), + testRoot.relativize(testFile.getParent()))); + } +}