Skip to content

Commit 2e2740d

Browse files
committed
Create graffiti management
1 parent a5dfb3b commit 2e2740d

File tree

9 files changed

+491
-6
lines changed

9 files changed

+491
-6
lines changed

Diff for: validator/api/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dependencies {
55
implementation project(':infrastructure:events')
66
implementation project(':infrastructure:exceptions')
77
implementation project(':infrastructure:http')
8+
implementation project(':infrastructure:serviceutils')
89
implementation project(':ethereum:execution-types')
910
implementation project(':ethereum:json-types')
1011
implementation project(':ethereum:spec')
@@ -13,6 +14,7 @@ dependencies {
1314

1415
testImplementation testFixtures(project(':infrastructure:bls'))
1516
testImplementation testFixtures(project(':infrastructure:logging'))
17+
testImplementation testFixtures(project(':infrastructure:serviceutils'))
1618
testImplementation testFixtures(project(':ethereum:spec'))
1719
}
1820

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Consensys Software Inc., 2024
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package tech.pegasys.teku.validator.api;
15+
16+
import java.io.IOException;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.util.Optional;
20+
import java.util.function.Supplier;
21+
import org.apache.logging.log4j.LogManager;
22+
import org.apache.logging.log4j.Logger;
23+
import tech.pegasys.teku.bls.BLSPublicKey;
24+
import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout;
25+
26+
public class GraffitiManager {
27+
static final String GRAFFITI_MANAGEMENT_DIR = "graffiti-management";
28+
29+
private static final Logger LOG = LogManager.getLogger();
30+
private final Optional<Path> graffitiPath;
31+
32+
public GraffitiManager(final DataDirLayout dataDirLayout) {
33+
this.graffitiPath = createManagementDirectory(dataDirLayout);
34+
}
35+
36+
public Optional<String> setGraffiti(final BLSPublicKey publicKey, final String graffiti) {
37+
return updateGraffiti(publicKey, () -> Bytes32Parser.toBytes32(graffiti).toArray());
38+
}
39+
40+
public Optional<String> deleteGraffiti(final BLSPublicKey publicKey) {
41+
return updateGraffiti(publicKey, () -> new byte[0]);
42+
}
43+
44+
private Optional<Path> createManagementDirectory(final DataDirLayout dataDirLayout) {
45+
final Path graffitiDirectory =
46+
dataDirLayout.getValidatorDataDirectory().resolve(GRAFFITI_MANAGEMENT_DIR);
47+
if (!graffitiDirectory.toFile().exists() && !graffitiDirectory.toFile().mkdirs()) {
48+
LOG.error(
49+
"Unable to create {} directory. Updating graffiti through the validator API is disabled.",
50+
GRAFFITI_MANAGEMENT_DIR);
51+
return Optional.empty();
52+
}
53+
return Optional.of(graffitiDirectory);
54+
}
55+
56+
private Optional<String> updateGraffiti(
57+
final BLSPublicKey publicKey, final Supplier<byte[]> graffiti) {
58+
if (graffitiPath.isEmpty()) {
59+
return Optional.of("graffiti-management directory does not exist to handle update.");
60+
}
61+
62+
try {
63+
final Path file = graffitiPath.get().resolve(resolveFileName(publicKey));
64+
Files.write(file, graffiti.get());
65+
} catch (IOException | IllegalArgumentException e) {
66+
return Optional.of(e.toString());
67+
}
68+
return Optional.empty();
69+
}
70+
71+
static String resolveFileName(final BLSPublicKey publicKey) {
72+
return publicKey.toSSZBytes().toUnprefixedHexString() + ".txt";
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Consensys Software Inc., 2024
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package tech.pegasys.teku.validator.api;
15+
16+
import static tech.pegasys.teku.validator.api.GraffitiManager.GRAFFITI_MANAGEMENT_DIR;
17+
18+
import java.nio.file.Path;
19+
import java.util.Optional;
20+
import org.apache.logging.log4j.LogManager;
21+
import org.apache.logging.log4j.Logger;
22+
import org.apache.tuweni.bytes.Bytes32;
23+
import tech.pegasys.teku.bls.BLSPublicKey;
24+
import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout;
25+
26+
public class UpdatableGraffitiProvider implements GraffitiProvider {
27+
private static final Logger LOG = LogManager.getLogger();
28+
29+
private final Path graffitiPath;
30+
private final GraffitiProvider defaultProvider;
31+
32+
public UpdatableGraffitiProvider(
33+
final DataDirLayout dataDirLayout,
34+
final BLSPublicKey publicKey,
35+
final GraffitiProvider defaultProvider) {
36+
this.graffitiPath =
37+
dataDirLayout
38+
.getValidatorDataDirectory()
39+
.resolve(GRAFFITI_MANAGEMENT_DIR)
40+
.resolve(GraffitiManager.resolveFileName(publicKey));
41+
this.defaultProvider = defaultProvider;
42+
}
43+
44+
@Override
45+
public Optional<Bytes32> get() {
46+
return getGraffitiFromStorage().or(defaultProvider::get).filter(this::graffitiNotEmpty);
47+
}
48+
49+
private Optional<Bytes32> getGraffitiFromStorage() {
50+
if (!graffitiPath.toFile().exists()) {
51+
return Optional.empty();
52+
}
53+
54+
try {
55+
return Optional.of(GraffitiParser.loadFromFile(graffitiPath));
56+
} catch (GraffitiLoaderException | IllegalArgumentException e) {
57+
LOG.warn("Unable to read graffiti from storage", e);
58+
return Optional.empty();
59+
}
60+
}
61+
62+
private boolean graffitiNotEmpty(final Bytes32 graffiti) {
63+
final Bytes32 emptyBytesParsed = Bytes32Parser.toBytes32(new byte[0]);
64+
return !graffiti.equals(emptyBytesParsed);
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright Consensys Software Inc., 2024
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package tech.pegasys.teku.validator.api;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.fail;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.nio.file.Path;
22+
import org.apache.tuweni.bytes.Bytes32;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.condition.DisabledOnOs;
25+
import org.junit.jupiter.api.condition.OS;
26+
import org.junit.jupiter.api.io.TempDir;
27+
import tech.pegasys.techu.service.serviceutils.layout.SimpleDataDirLayout;
28+
import tech.pegasys.teku.bls.BLSPublicKey;
29+
import tech.pegasys.teku.service.serviceutils.layout.DataDirLayout;
30+
import tech.pegasys.teku.spec.TestSpecFactory;
31+
import tech.pegasys.teku.spec.util.DataStructureUtil;
32+
33+
class GraffitiManagerTest {
34+
private final DataStructureUtil dataStructureUtil =
35+
new DataStructureUtil(TestSpecFactory.createDefault());
36+
private final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey();
37+
private final String graffiti = "Test graffiti";
38+
private GraffitiManager manager;
39+
private DataDirLayout dataDirLayout;
40+
41+
@Test
42+
@DisabledOnOs(OS.WINDOWS) // Can't set permissions on Windows
43+
void setGraffiti_shouldThrowExceptionWhenNoDirectory(@TempDir final Path tempDir) {
44+
assertThat(tempDir.toFile().setWritable(false)).isTrue();
45+
dataDirLayout = new SimpleDataDirLayout(tempDir);
46+
manager = new GraffitiManager(dataDirLayout);
47+
48+
assertThat(getGraffitiManagementDir().toFile().exists()).isFalse();
49+
assertThat(manager.setGraffiti(dataStructureUtil.randomPublicKey(), graffiti))
50+
.hasValue("graffiti-management directory does not exist to handle update.");
51+
}
52+
53+
@Test
54+
void setGraffiti_shouldSetGraffitiWhenFileNotExist(@TempDir final Path tempDir) {
55+
dataDirLayout = new SimpleDataDirLayout(tempDir);
56+
manager = new GraffitiManager(dataDirLayout);
57+
assertThat(getGraffitiManagementDir().toFile().exists()).isTrue();
58+
59+
assertThat(manager.setGraffiti(publicKey, graffiti)).isEmpty();
60+
checkGraffitiFile(publicKey, graffiti);
61+
}
62+
63+
@Test
64+
void setGraffiti_shouldSetGraffitiWhenFileExist(@TempDir final Path tempDir) throws IOException {
65+
dataDirLayout = new SimpleDataDirLayout(tempDir);
66+
manager = new GraffitiManager(dataDirLayout);
67+
68+
assertThat(getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile().createNewFile())
69+
.isTrue();
70+
71+
assertThat(manager.setGraffiti(publicKey, graffiti)).isEmpty();
72+
checkGraffitiFile(publicKey, graffiti);
73+
}
74+
75+
@Test
76+
@DisabledOnOs(OS.WINDOWS) // Can't set permissions on Windows
77+
void setGraffiti_shouldThrowExceptionWhenUnableToWriteFile(@TempDir final Path tempDir)
78+
throws IOException {
79+
dataDirLayout = new SimpleDataDirLayout(tempDir);
80+
manager = new GraffitiManager(dataDirLayout);
81+
82+
final File file = getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile();
83+
assertThat(file.createNewFile()).isTrue();
84+
assertThat(file.setWritable(false)).isTrue();
85+
86+
assertThat(manager.setGraffiti(publicKey, graffiti))
87+
.hasValue("java.nio.file.AccessDeniedException: " + file);
88+
}
89+
90+
@Test
91+
void setGraffiti_shouldThrowExceptionWhenGraffitiTooBig(@TempDir final Path tempDir) {
92+
final String invalidGraffiti = "This graffiti is a bit too long!!";
93+
dataDirLayout = new SimpleDataDirLayout(tempDir);
94+
manager = new GraffitiManager(dataDirLayout);
95+
assertThat(getGraffitiManagementDir().toFile().exists()).isTrue();
96+
97+
assertThat(manager.setGraffiti(publicKey, invalidGraffiti))
98+
.hasValue(
99+
"java.lang.IllegalArgumentException: "
100+
+ "'This graffiti is a bit too long!!' converts to 33 bytes. Input must be 32 bytes or less.");
101+
}
102+
103+
@Test
104+
@DisabledOnOs(OS.WINDOWS) // Can't set permissions on Windows
105+
void deleteGraffiti_shouldThrowExceptionWhenNoDirectory(@TempDir final Path tempDir) {
106+
assertThat(tempDir.toFile().setWritable(false)).isTrue();
107+
dataDirLayout = new SimpleDataDirLayout(tempDir);
108+
manager = new GraffitiManager(dataDirLayout);
109+
110+
assertThat(getGraffitiManagementDir().toFile().exists()).isFalse();
111+
assertThat(manager.deleteGraffiti(dataStructureUtil.randomPublicKey()))
112+
.hasValue("graffiti-management directory does not exist to handle update.");
113+
}
114+
115+
@Test
116+
void deleteGraffiti_shouldSetGraffitiWhenFileNotExist(@TempDir final Path tempDir) {
117+
dataDirLayout = new SimpleDataDirLayout(tempDir);
118+
manager = new GraffitiManager(dataDirLayout);
119+
assertThat(getGraffitiManagementDir().toFile().exists()).isTrue();
120+
121+
assertThat(manager.deleteGraffiti(publicKey)).isEmpty();
122+
checkGraffitiFile(publicKey, "");
123+
}
124+
125+
@Test
126+
void deleteGraffiti_shouldSetGraffitiWhenFileExist(@TempDir final Path tempDir)
127+
throws IOException {
128+
dataDirLayout = new SimpleDataDirLayout(tempDir);
129+
manager = new GraffitiManager(dataDirLayout);
130+
131+
assertThat(getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile().createNewFile())
132+
.isTrue();
133+
134+
assertThat(manager.deleteGraffiti(publicKey)).isEmpty();
135+
checkGraffitiFile(publicKey, "");
136+
}
137+
138+
@Test
139+
void deleteGraffiti_shouldThrowExceptionWhenUnableToWriteFile(@TempDir final Path tempDir)
140+
throws IOException {
141+
dataDirLayout = new SimpleDataDirLayout(tempDir);
142+
manager = new GraffitiManager(dataDirLayout);
143+
144+
final File file = getGraffitiManagementDir().resolve(getFileName(publicKey)).toFile();
145+
assertThat(file.createNewFile()).isTrue();
146+
assertThat(file.setWritable(false)).isTrue();
147+
148+
assertThat(manager.deleteGraffiti(publicKey))
149+
.hasValue("java.nio.file.AccessDeniedException: " + file);
150+
}
151+
152+
private void checkGraffitiFile(final BLSPublicKey publicKey, final String graffiti) {
153+
final Path filePath = getGraffitiManagementDir().resolve(getFileName(publicKey));
154+
try {
155+
final Bytes32 expectedBytes = Bytes32Parser.toBytes32(graffiti);
156+
final Bytes32 parsedBytes = GraffitiParser.loadFromFile(filePath);
157+
assertThat(parsedBytes).isEqualTo(expectedBytes);
158+
} catch (GraffitiLoaderException e) {
159+
fail(e.getMessage());
160+
}
161+
}
162+
163+
private Path getGraffitiManagementDir() {
164+
return dataDirLayout.getValidatorDataDirectory().resolve("graffiti-management");
165+
}
166+
167+
private String getFileName(final BLSPublicKey publicKey) {
168+
return publicKey.toSSZBytes().toUnprefixedHexString() + ".txt";
169+
}
170+
}

0 commit comments

Comments
 (0)