Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for slashing interchange format tests #8185

Merged
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add the name of the test which covers this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there is no test for this, will add it.

// 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