forked from Consensys/teku
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a5dfb3b
commit 2e2740d
Showing
9 changed files
with
491 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
validator/api/src/main/java/tech/pegasys/teku/validator/api/GraffitiManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.validator.api; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.Optional; | ||
import java.util.function.Supplier; | ||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import tech.pegasys.teku.bls.BLSPublicKey; | ||
import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout; | ||
|
||
public class GraffitiManager { | ||
static final String GRAFFITI_MANAGEMENT_DIR = "graffiti-management"; | ||
|
||
private static final Logger LOG = LogManager.getLogger(); | ||
private final Optional<Path> graffitiPath; | ||
|
||
public GraffitiManager(final DataDirLayout dataDirLayout) { | ||
this.graffitiPath = createManagementDirectory(dataDirLayout); | ||
} | ||
|
||
public Optional<String> setGraffiti(final BLSPublicKey publicKey, final String graffiti) { | ||
return updateGraffiti(publicKey, () -> Bytes32Parser.toBytes32(graffiti).toArray()); | ||
} | ||
|
||
public Optional<String> deleteGraffiti(final BLSPublicKey publicKey) { | ||
return updateGraffiti(publicKey, () -> new byte[0]); | ||
} | ||
|
||
private Optional<Path> createManagementDirectory(final DataDirLayout dataDirLayout) { | ||
final Path graffitiDirectory = | ||
dataDirLayout.getValidatorDataDirectory().resolve(GRAFFITI_MANAGEMENT_DIR); | ||
if (!graffitiDirectory.toFile().exists() && !graffitiDirectory.toFile().mkdirs()) { | ||
LOG.error( | ||
"Unable to create {} directory. Updating graffiti through the validator API is disabled.", | ||
GRAFFITI_MANAGEMENT_DIR); | ||
return Optional.empty(); | ||
} | ||
return Optional.of(graffitiDirectory); | ||
} | ||
|
||
private Optional<String> updateGraffiti( | ||
final BLSPublicKey publicKey, final Supplier<byte[]> graffiti) { | ||
if (graffitiPath.isEmpty()) { | ||
return Optional.of("graffiti-management directory does not exist to handle update."); | ||
} | ||
|
||
try { | ||
final Path file = graffitiPath.get().resolve(resolveFileName(publicKey)); | ||
Files.write(file, graffiti.get()); | ||
} catch (IOException | IllegalArgumentException e) { | ||
return Optional.of(e.toString()); | ||
} | ||
return Optional.empty(); | ||
} | ||
|
||
static String resolveFileName(final BLSPublicKey publicKey) { | ||
return publicKey.toSSZBytes().toUnprefixedHexString() + ".txt"; | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
validator/api/src/main/java/tech/pegasys/teku/validator/api/UpdatableGraffitiProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
* 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.validator.api; | ||
|
||
import static tech.pegasys.teku.validator.api.GraffitiManager.GRAFFITI_MANAGEMENT_DIR; | ||
|
||
import java.nio.file.Path; | ||
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.service.serviceutils.layout.DataDirLayout; | ||
|
||
public class UpdatableGraffitiProvider implements GraffitiProvider { | ||
private static final Logger LOG = LogManager.getLogger(); | ||
|
||
private final Path graffitiPath; | ||
private final GraffitiProvider defaultProvider; | ||
|
||
public UpdatableGraffitiProvider( | ||
final DataDirLayout dataDirLayout, | ||
final BLSPublicKey publicKey, | ||
final GraffitiProvider defaultProvider) { | ||
this.graffitiPath = | ||
dataDirLayout | ||
.getValidatorDataDirectory() | ||
.resolve(GRAFFITI_MANAGEMENT_DIR) | ||
.resolve(GraffitiManager.resolveFileName(publicKey)); | ||
this.defaultProvider = defaultProvider; | ||
} | ||
|
||
@Override | ||
public Optional<Bytes32> get() { | ||
return getGraffitiFromStorage().or(defaultProvider::get).filter(this::graffitiNotEmpty); | ||
} | ||
|
||
private Optional<Bytes32> getGraffitiFromStorage() { | ||
if (!graffitiPath.toFile().exists()) { | ||
return Optional.empty(); | ||
} | ||
|
||
try { | ||
return Optional.of(GraffitiParser.loadFromFile(graffitiPath)); | ||
} catch (GraffitiLoaderException | IllegalArgumentException e) { | ||
LOG.warn("Unable to read graffiti from storage", e); | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
private boolean graffitiNotEmpty(final Bytes32 graffiti) { | ||
final Bytes32 emptyBytesParsed = Bytes32Parser.toBytes32(new byte[0]); | ||
return !graffiti.equals(emptyBytesParsed); | ||
} | ||
} |
170 changes: 170 additions & 0 deletions
170
validator/api/src/test/java/tech/pegasys/teku/validator/api/GraffitiManagerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
/* | ||
* 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.validator.api; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.fail; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import org.apache.tuweni.bytes.Bytes32; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.condition.DisabledOnOs; | ||
import org.junit.jupiter.api.condition.OS; | ||
import org.junit.jupiter.api.io.TempDir; | ||
import tech.pegasys.techu.service.serviceutils.layout.SimpleDataDirLayout; | ||
import tech.pegasys.teku.bls.BLSPublicKey; | ||
import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout; | ||
import tech.pegasys.teku.spec.TestSpecFactory; | ||
import tech.pegasys.teku.spec.util.DataStructureUtil; | ||
|
||
class GraffitiManagerTest { | ||
private final DataStructureUtil dataStructureUtil = | ||
new DataStructureUtil(TestSpecFactory.createDefault()); | ||
private final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey(); | ||
private final String graffiti = "Test graffiti"; | ||
private GraffitiManager manager; | ||
private DataDirLayout dataDirLayout; | ||
|
||
@Test | ||
@DisabledOnOs(OS.WINDOWS) // Can't set permissions on Windows | ||
void setGraffiti_shouldThrowExceptionWhenNoDirectory(@TempDir final Path tempDir) { | ||
assertThat(tempDir.toFile().setWritable(false)).isTrue(); | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
|
||
assertThat(getGraffitiManagementDir().toFile().exists()).isFalse(); | ||
assertThat(manager.setGraffiti(dataStructureUtil.randomPublicKey(), graffiti)) | ||
.hasValue("graffiti-management directory does not exist to handle update."); | ||
} | ||
|
||
@Test | ||
void setGraffiti_shouldSetGraffitiWhenFileNotExist(@TempDir final Path tempDir) { | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
assertThat(getGraffitiManagementDir().toFile().exists()).isTrue(); | ||
|
||
assertThat(manager.setGraffiti(publicKey, graffiti)).isEmpty(); | ||
checkGraffitiFile(publicKey, graffiti); | ||
} | ||
|
||
@Test | ||
void setGraffiti_shouldSetGraffitiWhenFileExist(@TempDir final Path tempDir) throws IOException { | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
|
||
assertThat(getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile().createNewFile()) | ||
.isTrue(); | ||
|
||
assertThat(manager.setGraffiti(publicKey, graffiti)).isEmpty(); | ||
checkGraffitiFile(publicKey, graffiti); | ||
} | ||
|
||
@Test | ||
@DisabledOnOs(OS.WINDOWS) // Can't set permissions on Windows | ||
void setGraffiti_shouldThrowExceptionWhenUnableToWriteFile(@TempDir final Path tempDir) | ||
throws IOException { | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
|
||
final File file = getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile(); | ||
assertThat(file.createNewFile()).isTrue(); | ||
assertThat(file.setWritable(false)).isTrue(); | ||
|
||
assertThat(manager.setGraffiti(publicKey, graffiti)) | ||
.hasValue("java.nio.file.AccessDeniedException: " + file); | ||
} | ||
|
||
@Test | ||
void setGraffiti_shouldThrowExceptionWhenGraffitiTooBig(@TempDir final Path tempDir) { | ||
final String invalidGraffiti = "This graffiti is a bit too long!!"; | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
assertThat(getGraffitiManagementDir().toFile().exists()).isTrue(); | ||
|
||
assertThat(manager.setGraffiti(publicKey, invalidGraffiti)) | ||
.hasValue( | ||
"java.lang.IllegalArgumentException: " | ||
+ "'This graffiti is a bit too long!!' converts to 33 bytes. Input must be 32 bytes or less."); | ||
} | ||
|
||
@Test | ||
@DisabledOnOs(OS.WINDOWS) // Can't set permissions on Windows | ||
void deleteGraffiti_shouldThrowExceptionWhenNoDirectory(@TempDir final Path tempDir) { | ||
assertThat(tempDir.toFile().setWritable(false)).isTrue(); | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
|
||
assertThat(getGraffitiManagementDir().toFile().exists()).isFalse(); | ||
assertThat(manager.deleteGraffiti(dataStructureUtil.randomPublicKey())) | ||
.hasValue("graffiti-management directory does not exist to handle update."); | ||
} | ||
|
||
@Test | ||
void deleteGraffiti_shouldSetGraffitiWhenFileNotExist(@TempDir final Path tempDir) { | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
assertThat(getGraffitiManagementDir().toFile().exists()).isTrue(); | ||
|
||
assertThat(manager.deleteGraffiti(publicKey)).isEmpty(); | ||
checkGraffitiFile(publicKey, ""); | ||
} | ||
|
||
@Test | ||
void deleteGraffiti_shouldSetGraffitiWhenFileExist(@TempDir final Path tempDir) | ||
throws IOException { | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
|
||
assertThat(getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile().createNewFile()) | ||
.isTrue(); | ||
|
||
assertThat(manager.deleteGraffiti(publicKey)).isEmpty(); | ||
checkGraffitiFile(publicKey, ""); | ||
} | ||
|
||
@Test | ||
void deleteGraffiti_shouldThrowExceptionWhenUnableToWriteFile(@TempDir final Path tempDir) | ||
throws IOException { | ||
dataDirLayout = new SimpleDataDirLayout(tempDir); | ||
manager = new GraffitiManager(dataDirLayout); | ||
|
||
final File file = getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile(); | ||
assertThat(file.createNewFile()).isTrue(); | ||
assertThat(file.setWritable(false)).isTrue(); | ||
|
||
assertThat(manager.deleteGraffiti(publicKey)) | ||
.hasValue("java.nio.file.AccessDeniedException: " + file); | ||
} | ||
|
||
private void checkGraffitiFile(final BLSPublicKey publicKey, final String graffiti) { | ||
final Path filePath = getGraffitiManagementDir().resolve(getFileName(publicKey)); | ||
try { | ||
final Bytes32 expectedBytes = Bytes32Parser.toBytes32(graffiti); | ||
final Bytes32 parsedBytes = GraffitiParser.loadFromFile(filePath); | ||
assertThat(parsedBytes).isEqualTo(expectedBytes); | ||
} catch (GraffitiLoaderException e) { | ||
fail(e.getMessage()); | ||
} | ||
} | ||
|
||
private Path getGraffitiManagementDir() { | ||
return dataDirLayout.getValidatorDataDirectory().resolve("graffiti-management"); | ||
} | ||
|
||
private String getFileName(final BLSPublicKey publicKey) { | ||
return publicKey.toSSZBytes().toUnprefixedHexString() + ".txt"; | ||
} | ||
} |
Oops, something went wrong.