Skip to content

Commit

Permalink
Add support for slashing interchange format tests (Consensys#8185)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanBratanov authored Apr 15, 2024
1 parent e9ef718 commit 6a84100
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 18 deletions.
34 changes: 31 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"
Expand Down Expand Up @@ -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() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BLSPublicKey, String> 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())));
Expand All @@ -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());
}
}
3 changes: 2 additions & 1 deletion data/dataexchange/src/test/resources/format2_minimal.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
{
"pubkey": "0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed",
"signed_blocks": [
{"slot": "81952"
{
"slot": "81952"
}
],
"signed_attestations": [
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
3 changes: 3 additions & 0 deletions eth-reference-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class ManualReferenceTestRunner extends Eth2ReferenceTestCase {
*
* <p>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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,18 +30,21 @@
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();
// Set the code point limit to 100MB - context:
// 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 extends SszData> T loadSsz(
Expand Down Expand Up @@ -85,7 +89,7 @@ public static <T> 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);
}
}

Expand All @@ -100,4 +104,18 @@ public static <T> T loadYaml(
return type.deserialize(in);
}
}

public static <T> T loadJson(
final TestDefinition testDefinition, final String fileName, final Class<T> 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 <T> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<BLSPublicKey, String> 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<BLSPublicKey, String> importInterchange(
final SlashingProtectionImporter importer,
final SlashingProtectionInterchangeFormat interchange) {
try {
final Path importFile = Files.createTempFile("import", ".json");
TestDataUtils.writeJsonToFile(interchange, importFile);
final Optional<String> 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<Path> 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<Step> 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<Block> blocks,
List<Attestation> 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) {}
}
}
Loading

0 comments on commit 6a84100

Please sign in to comment.