diff --git a/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/EphemeryDatabaseReset.java b/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/EphemeryDatabaseReset.java new file mode 100644 index 00000000000..92de802252b --- /dev/null +++ b/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/EphemeryDatabaseReset.java @@ -0,0 +1,74 @@ +/* + * 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.services.chainstorage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; +import tech.pegasys.teku.service.serviceutils.ServiceConfig; +import tech.pegasys.teku.storage.server.Database; +import tech.pegasys.teku.storage.server.VersionedDatabaseFactory; + +public class EphemeryDatabaseReset { + + /** This method is called only on Ephemery network when reset is due. */ + Database resetDatabaseAndCreate( + final ServiceConfig serviceConfig, final VersionedDatabaseFactory dbFactory) { + try { + final Path beaconDataDir = serviceConfig.getDataDirLayout().getBeaconDataDirectory(); + final Path dbDataDir = beaconDataDir.resolve("db"); + final Path networkFile = beaconDataDir.resolve("network.yml"); + final Path validatorDataDir = serviceConfig.getDataDirLayout().getValidatorDataDirectory(); + final Path slashProtectionDir; + if (validatorDataDir.endsWith("slashprotection")) { + slashProtectionDir = validatorDataDir; + } else { + slashProtectionDir = validatorDataDir.resolve("slashprotection"); + } + deleteDirectoryRecursively(dbDataDir); + deleteDirectoryRecursively(networkFile); + deleteDirectoryRecursively(slashProtectionDir); + return dbFactory.createDatabase(); + } catch (final Exception ex) { + throw new InvalidConfigurationException( + "The existing ephemery database was old, and was unable to reset it.", ex); + } + } + + void deleteDirectoryRecursively(final Path path) throws IOException { + if (Files.exists(path)) { + if (Files.isDirectory(path)) { + try (var stream = Files.walk(path)) { + stream + .sorted((o1, o2) -> o2.compareTo(o1)) + .forEach( + p -> { + try { + Files.delete(p); + } catch (IOException e) { + throw new RuntimeException("Failed to delete file/directory: " + p, e); + } + }); + } + } else { + try { + Files.delete(path); + } catch (IOException e) { + throw new RuntimeException("Failed to delete file: " + path, e); + } + } + } + } +} diff --git a/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java b/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java index 3fd27928183..878c90f0023 100644 --- a/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java +++ b/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java @@ -42,6 +42,7 @@ import tech.pegasys.teku.storage.server.RetryingStorageUpdateChannel; import tech.pegasys.teku.storage.server.StorageConfiguration; import tech.pegasys.teku.storage.server.VersionedDatabaseFactory; +import tech.pegasys.teku.storage.server.network.EphemeryException; import tech.pegasys.teku.storage.server.pruner.BlobSidecarPruner; import tech.pegasys.teku.storage.server.pruner.BlockPruner; import tech.pegasys.teku.storage.server.pruner.StatePruner; @@ -85,7 +86,14 @@ protected SafeFuture doStart() { serviceConfig.getMetricsSystem(), serviceConfig.getDataDirLayout().getBeaconDataDirectory(), config); - database = dbFactory.createDatabase(); + try { + database = dbFactory.createDatabase(); + } catch (EphemeryException e) { + final EphemeryDatabaseReset ephemeryDatabaseReset = new EphemeryDatabaseReset(); + LOG.warn( + "Ephemery network deposit contract id has updated, resetting the stored database and slashing protection data."); + database = ephemeryDatabaseReset.resetDatabaseAndCreate(serviceConfig, dbFactory); + } final SettableLabelledGauge pruningTimingsLabelledGauge = SettableLabelledGauge.create( diff --git a/services/chainstorage/src/test/java/tech/pegasys/teku/services/chainstorage/EphemeryDatabaseResetTest.java b/services/chainstorage/src/test/java/tech/pegasys/teku/services/chainstorage/EphemeryDatabaseResetTest.java new file mode 100644 index 00000000000..1121926ce0c --- /dev/null +++ b/services/chainstorage/src/test/java/tech/pegasys/teku/services/chainstorage/EphemeryDatabaseResetTest.java @@ -0,0 +1,135 @@ +/* + * 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.services.chainstorage; + +import static java.nio.file.Files.createTempDirectory; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; +import tech.pegasys.teku.service.serviceutils.ServiceConfig; +import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout; +import tech.pegasys.teku.storage.server.Database; +import tech.pegasys.teku.storage.server.VersionedDatabaseFactory; + +class EphemeryDatabaseResetTest { + + @Mock private ServiceConfig serviceConfig; + + @Mock private VersionedDatabaseFactory dbFactory; + + @Mock private DataDirLayout dataDirLayout; + private Path beaconDataDir; + private Path dbDataDir; + private Path resolvedSlashProtectionDir; + private Path networkFilePath; + @Mock private Database database; + + @Mock private EphemeryDatabaseReset ephemeryDatabaseReset; + + @BeforeEach + void setUp() throws IOException { + MockitoAnnotations.openMocks(this); + ephemeryDatabaseReset = spy(new EphemeryDatabaseReset()); + beaconDataDir = createTempDirectory("beacon"); + dbDataDir = beaconDataDir.resolve("db"); + Files.createDirectory(dbDataDir); + Path networkFile = beaconDataDir.resolve("network.yml"); + Files.createFile(networkFile); + networkFilePath = networkFile; + + final Path validatorDataDir = createTempDirectory("validator"); + resolvedSlashProtectionDir = validatorDataDir.resolve("slashprotection"); + + when(serviceConfig.getDataDirLayout()).thenReturn(dataDirLayout); + when(dataDirLayout.getBeaconDataDirectory()).thenReturn(beaconDataDir); + when(dataDirLayout.getValidatorDataDirectory()).thenReturn(validatorDataDir); + when(dataDirLayout.getValidatorDataDirectory().resolve("slashprotection")) + .thenReturn(resolvedSlashProtectionDir); + } + + @Test + void shouldResetSpecificDirectoriesAndCreateDatabase() throws IOException { + final Path kvStoreDir = beaconDataDir.resolve("kvstore"); + Files.createDirectory(kvStoreDir); + final Path dbVersion = beaconDataDir.resolve("db.version"); + Files.createFile(dbVersion); + + when(dbFactory.createDatabase()).thenReturn(database); + + final Database result = ephemeryDatabaseReset.resetDatabaseAndCreate(serviceConfig, dbFactory); + verify(ephemeryDatabaseReset).deleteDirectoryRecursively(dbDataDir); + verify(ephemeryDatabaseReset).deleteDirectoryRecursively(networkFilePath); + verify(ephemeryDatabaseReset).deleteDirectoryRecursively(resolvedSlashProtectionDir); + + verify(dbFactory).createDatabase(); + verifyNoMoreInteractions(dbFactory); + + assertTrue(Files.exists(kvStoreDir)); + assertTrue(Files.exists(dbVersion)); + assertEquals(database, result); + } + + @Test + void shouldThrowInvalidConfigurationExceptionWhenDirectoryDeletionFails() throws IOException { + doThrow(new IOException("Failed to delete directory")) + .when(ephemeryDatabaseReset) + .deleteDirectoryRecursively(dbDataDir); + final InvalidConfigurationException exception = + assertThrows( + InvalidConfigurationException.class, + () -> { + ephemeryDatabaseReset.resetDatabaseAndCreate(serviceConfig, dbFactory); + }); + assertEquals( + "The existing ephemery database was old, and was unable to reset it.", + exception.getMessage()); + verify(dbFactory, never()).createDatabase(); + verify(ephemeryDatabaseReset, never()).deleteDirectoryRecursively(resolvedSlashProtectionDir); + } + + @Test + void shouldThrowInvalidConfigurationExceptionWhenDatabaseCreationFails() throws IOException { + doNothing().when(ephemeryDatabaseReset).deleteDirectoryRecursively(dbDataDir); + doNothing().when(ephemeryDatabaseReset).deleteDirectoryRecursively(resolvedSlashProtectionDir); + when(dbFactory.createDatabase()).thenThrow(new RuntimeException("Database creation failed")); + final InvalidConfigurationException exception = + assertThrows( + InvalidConfigurationException.class, + () -> { + ephemeryDatabaseReset.resetDatabaseAndCreate(serviceConfig, dbFactory); + }); + assertEquals( + "The existing ephemery database was old, and was unable to reset it.", + exception.getMessage()); + verify(ephemeryDatabaseReset).deleteDirectoryRecursively(dbDataDir); + verify(ephemeryDatabaseReset).deleteDirectoryRecursively(resolvedSlashProtectionDir); + verify(dbFactory).createDatabase(); + } +} diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java b/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java index 4505e1c3cd5..a722e51b323 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/VersionedDatabaseFactory.java @@ -46,7 +46,6 @@ public class VersionedDatabaseFactory implements DatabaseFactory { @VisibleForTesting static final String STORAGE_MODE_PATH = "data-storage-mode.txt"; @VisibleForTesting static final String METADATA_FILENAME = "metadata.yml"; @VisibleForTesting static final String NETWORK_FILENAME = "network.yml"; - private final MetricsSystem metricsSystem; private final File dataDirectory; private final int maxKnownNodeCacheSize; diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java b/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java index ea35a55855e..c4aa809d782 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/network/DatabaseNetwork.java @@ -45,6 +45,9 @@ public class DatabaseNetwork { @VisibleForTesting final Long depositChainId; + private static final String EPHEMERY_DEPOSIT_CONTRACT_ADDRESS = + "0x4242424242424242424242424242424242424242"; + @JsonCreator DatabaseNetwork( @JsonProperty(value = "fork_version") final String forkVersion, @@ -84,6 +87,20 @@ public static DatabaseNetwork init( formatMessage( "deposit contract", depositContractString, databaseNetwork.depositContract)); } + if (databaseNetwork.depositChainId != null + && !depositContractString.equals(EPHEMERY_DEPOSIT_CONTRACT_ADDRESS) + && !databaseNetwork.depositChainId.equals(depositChainId)) { + throw DatabaseStorageException.unrecoverable( + formatMessage( + "deposit chain id", + String.valueOf(depositChainId), + String.valueOf(databaseNetwork.depositChainId))); + } + if (databaseNetwork.depositChainId != null + && depositContractString.equals(EPHEMERY_DEPOSIT_CONTRACT_ADDRESS) + && !databaseNetwork.depositChainId.equals(depositChainId)) { + throw new EphemeryException(); + } return databaseNetwork; } else { DatabaseNetwork databaseNetwork = @@ -93,6 +110,10 @@ public static DatabaseNetwork init( } } + public Long getDepositChainId() { + return depositChainId; + } + private static String formatMessage( final String fieldName, final String expected, final String actual) { return String.format( diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/network/EphemeryException.java b/storage/src/main/java/tech/pegasys/teku/storage/server/network/EphemeryException.java new file mode 100644 index 00000000000..394aad3a064 --- /dev/null +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/network/EphemeryException.java @@ -0,0 +1,16 @@ +/* + * 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.storage.server.network; + +public class EphemeryException extends RuntimeException {}