From 309a03700ba88b3c3b505baf532b968fcdd05396 Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Mon, 11 Sep 2023 15:49:20 +1000 Subject: [PATCH 1/4] Handle null epoch OR null slot (both null already handled) --- .../slashingprotection/dao/MetadataDao.java | 2 +- .../dao/MetadataDaoTest.java | 57 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDao.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDao.java index a5a5864fa..b52d6f26e 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDao.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDao.java @@ -43,7 +43,7 @@ public Optional findHighWatermark(Handle handle) { "SELECT high_watermark_epoch as epoch, high_watermark_slot as slot FROM metadata WHERE id = ?") .bind(0, METADATA_ROW_ID) .mapToBean(HighWatermark.class) - .filter(h -> h.getEpoch() != null && h.getSlot() != null) + .filter(h -> h.getEpoch() != null || h.getSlot() != null) .findFirst(); } diff --git a/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDaoTest.java b/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDaoTest.java index 15e268f69..2a7e730c5 100644 --- a/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDaoTest.java +++ b/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/dao/MetadataDaoTest.java @@ -92,6 +92,26 @@ public void findsExistingHighWatermark(final Handle handle) { .contains(new HighWatermark(UInt64.valueOf(2), UInt64.valueOf(1))); } + @Test + public void findsExistingHighWatermarkWithOnlyEpoch(final Handle handle) { + insertGvr(handle, Bytes32.leftPad(Bytes.of(3))); + updateHighWatermark(handle, 1, null); + + final Optional existingHighWatermark = metadataDao.findHighWatermark(handle); + + assertThat(existingHighWatermark).contains(new HighWatermark(null, UInt64.valueOf(1))); + } + + @Test + public void findsExistingHighWatermarkWithOnlySlot(final Handle handle) { + insertGvr(handle, Bytes32.leftPad(Bytes.of(3))); + updateHighWatermark(handle, null, 2); + + final Optional existingHighWatermark = metadataDao.findHighWatermark(handle); + + assertThat(existingHighWatermark).contains(new HighWatermark(UInt64.valueOf(2), null)); + } + @Test public void returnsEmptyForNonExistingHighWatermark(final Handle handle) { assertThat(metadataDao.findHighWatermark(handle)).isEmpty(); @@ -110,15 +130,27 @@ public void insertsHighWatermark(final Handle handle) { int updateCount = metadataDao.updateHighWatermark(handle, highWatermark); - assertThat(updateCount).isEqualTo(1); - final List highWatermarks = - handle - .createQuery( - "SELECT high_watermark_epoch as epoch, high_watermark_slot as slot FROM metadata") - .mapToBean(HighWatermark.class) - .list(); - assertThat(highWatermarks.size()).isEqualTo(1); - assertThat(highWatermarks.get(0)).isEqualTo(highWatermark); + assertHighWatermarkUpdatedSuccessfully(handle, updateCount, highWatermark); + } + + @Test + public void insertsOnlyEpochHighWatermark(final Handle handle) { + insertGvr(handle, Bytes32.leftPad(Bytes.of(3))); + HighWatermark highWatermark = new HighWatermark(null, UInt64.valueOf(1)); + + int updateCount = metadataDao.updateHighWatermark(handle, highWatermark); + + assertHighWatermarkUpdatedSuccessfully(handle, updateCount, highWatermark); + } + + @Test + public void insertsOnlySlotHighWatermark(final Handle handle) { + insertGvr(handle, Bytes32.leftPad(Bytes.of(3))); + HighWatermark highWatermark = new HighWatermark(UInt64.valueOf(1), null); + + int updateCount = metadataDao.updateHighWatermark(handle, highWatermark); + + assertHighWatermarkUpdatedSuccessfully(handle, updateCount, highWatermark); } @Test @@ -129,6 +161,11 @@ public void updatesHighWatermark(final Handle handle) { int updateCount = metadataDao.updateHighWatermark(handle, highWatermark); + assertHighWatermarkUpdatedSuccessfully(handle, updateCount, highWatermark); + } + + private void assertHighWatermarkUpdatedSuccessfully( + Handle handle, int updateCount, HighWatermark highWatermark) { assertThat(updateCount).isEqualTo(1); final List highWatermarks = handle @@ -220,7 +257,7 @@ private void insertLowWatermarks(Handle handle) { MAX_LOW_WATERMARK_SOURCE_EPOCH); } - private void updateHighWatermark(final Handle handle, final int epoch, final int slot) { + private void updateHighWatermark(final Handle handle, final Integer epoch, final Integer slot) { handle .createUpdate("UPDATE metadata set high_watermark_epoch=:epoch, high_watermark_slot=:slot") .bind("epoch", epoch) From 7ba40c018f3b5d8b082508c45b5f4f7e6ad15455 Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Mon, 11 Sep 2023 16:43:33 +1000 Subject: [PATCH 2/4] Add GET /highWatermark endpoint to eth2 API --- .../pegasys/web3signer/core/Eth2Runner.java | 9 ++++ .../http/handlers/HighWatermarkHandler.java | 48 +++++++++++++++++++ .../eth2/signing/paths/high_watermark.yaml | 24 ++++++++++ .../openapi-specs/eth2/web3signer.yaml | 2 + .../DbSlashingProtection.java | 6 +++ .../SlashingProtection.java | 4 ++ 6 files changed, 93 insertions(+) create mode 100644 core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/HighWatermarkHandler.java create mode 100644 core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml diff --git a/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java b/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java index 04cbd0713..b38c0a7ff 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth2Runner.java @@ -25,6 +25,7 @@ import tech.pegasys.web3signer.core.config.BaseConfig; import tech.pegasys.web3signer.core.metrics.SlashingProtectionMetrics; import tech.pegasys.web3signer.core.service.http.SigningObjectMapperFactory; +import tech.pegasys.web3signer.core.service.http.handlers.HighWatermarkHandler; import tech.pegasys.web3signer.core.service.http.handlers.LogErrorHandler; import tech.pegasys.web3signer.core.service.http.handlers.keymanager.delete.DeleteKeystoresHandler; import tech.pegasys.web3signer.core.service.http.handlers.keymanager.imports.ImportKeystoresHandler; @@ -90,6 +91,7 @@ public class Eth2Runner extends Runner { public static final String KEYSTORES_PATH = "/eth/v1/keystores"; public static final String PUBLIC_KEYS_PATH = "/api/v1/eth2/publicKeys"; public static final String SIGN_PATH = "/api/v1/eth2/sign/:identifier"; + public static final String HIGH_WATERMARK_PATH = "/api/v1/eth2/highWatermark"; private static final Logger LOG = LogManager.getLogger(); private final Optional slashingProtectionContext; @@ -171,6 +173,13 @@ private void registerEth2Routes( addReloadHandler(router, List.of(blsSignerProvider), errorHandler); + slashingProtectionContext.ifPresent( + protectionContext -> + router + .route(HttpMethod.GET, HIGH_WATERMARK_PATH) + .handler(new HighWatermarkHandler(protectionContext.getSlashingProtection())) + .failureHandler(errorHandler)); + if (isKeyManagerApiEnabled) { router .route(HttpMethod.GET, KEYSTORES_PATH) diff --git a/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/HighWatermarkHandler.java b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/HighWatermarkHandler.java new file mode 100644 index 000000000..29540dd74 --- /dev/null +++ b/core/src/main/java/tech/pegasys/web3signer/core/service/http/handlers/HighWatermarkHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 ConsenSys AG. + * + * 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.web3signer.core.service.http.handlers; + +import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; +import static tech.pegasys.web3signer.core.service.http.handlers.ContentTypes.JSON_UTF_8; + +import tech.pegasys.web3signer.slashingprotection.SlashingProtection; + +import java.util.Map; + +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +public class HighWatermarkHandler implements Handler { + private final SlashingProtection slashingProtection; + + public HighWatermarkHandler(final SlashingProtection slashingProtection) { + this.slashingProtection = slashingProtection; + } + + @Override + public void handle(final RoutingContext context) { + JsonObject response = + slashingProtection + .getHighWatermark() + .map( + hw -> + new JsonObject( + Map.of( + "epoch", String.valueOf(hw.getEpoch()), + "slot", String.valueOf(hw.getSlot())))) + .orElse(new JsonObject()); + + context.response().putHeader(CONTENT_TYPE, JSON_UTF_8).end(response.encode()); + } +} diff --git a/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml b/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml new file mode 100644 index 000000000..11062da05 --- /dev/null +++ b/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml @@ -0,0 +1,24 @@ +get: + tags: + - 'High Watermark' + summary: 'The High Watermark epoch and slot applicable to all validators' + description: 'Returns the uint64 epoch and slot of the high watermark. Signing of attestations or blocks are prevented at or beyond this high watermark' + operationId: 'HIGH_WATERMARK' + responses: + '200': + description: 'high watermark' + content: + application/json: + schema: + type: "object" + properties: + epoch: + type: "string" + format: "uint64" + slot: + type: "string" + format: "uint64" + '400': + description: 'Bad request format' + '500': + description: 'Internal Web3Signer server error' \ No newline at end of file diff --git a/core/src/main/resources/openapi-specs/eth2/web3signer.yaml b/core/src/main/resources/openapi-specs/eth2/web3signer.yaml index 02921d42f..2fed18419 100644 --- a/core/src/main/resources/openapi-specs/eth2/web3signer.yaml +++ b/core/src/main/resources/openapi-specs/eth2/web3signer.yaml @@ -16,6 +16,8 @@ paths: $ref: './signing/paths/sign.yaml' /api/v1/eth2/publicKeys: $ref: './signing/paths/public_keys.yaml' + /api/v1/eth2/highWatermark: + $ref: './signing/paths/high_watermark.yaml' /reload: $ref: './signing/paths/reload.yaml' /upcheck: diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtection.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtection.java index 2c3629063..4247a911e 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtection.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtection.java @@ -16,6 +16,7 @@ import static tech.pegasys.web3signer.slashingprotection.DbLocker.lockForValidator; import tech.pegasys.web3signer.slashingprotection.DbLocker.LockType; +import tech.pegasys.web3signer.slashingprotection.dao.HighWatermark; import tech.pegasys.web3signer.slashingprotection.dao.LowWatermarkDao; import tech.pegasys.web3signer.slashingprotection.dao.MetadataDao; import tech.pegasys.web3signer.slashingprotection.dao.SignedAttestationsDao; @@ -274,6 +275,11 @@ public void updateValidatorEnabledStatus(final Bytes publicKey, final boolean en }); } + @Override + public Optional getHighWatermark() { + return jdbi.inTransaction(READ_COMMITTED, metadataDao::findHighWatermark); + } + private boolean isEnabled(final Handle handle, final int validatorId) { return validatorsDao.isEnabled(handle, validatorId); } diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/SlashingProtection.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/SlashingProtection.java index 12219eba8..d6e370a76 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/SlashingProtection.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/SlashingProtection.java @@ -12,11 +12,13 @@ */ package tech.pegasys.web3signer.slashingprotection; +import tech.pegasys.web3signer.slashingprotection.dao.HighWatermark; import tech.pegasys.web3signer.slashingprotection.interchange.IncrementalExporter; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import java.util.Optional; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -49,4 +51,6 @@ boolean maySignBlock( boolean isEnabledValidator(Bytes publicKey); void updateValidatorEnabledStatus(Bytes publicKey, boolean enabled); + + Optional getHighWatermark(); } From 772f30c1700ec600c4724867ee496fb9f9e08e49 Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Tue, 12 Sep 2023 15:21:42 +1000 Subject: [PATCH 3/4] Add empty response case to open api description --- .../openapi-specs/eth2/signing/paths/high_watermark.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml b/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml index 11062da05..9956e6057 100644 --- a/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml +++ b/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml @@ -2,7 +2,7 @@ get: tags: - 'High Watermark' summary: 'The High Watermark epoch and slot applicable to all validators' - description: 'Returns the uint64 epoch and slot of the high watermark. Signing of attestations or blocks are prevented at or beyond this high watermark' + description: 'Returns the uint64 epoch and slot of the high watermark. Signing of attestations or blocks are prevented at or beyond this high watermark. If no high watermark is set, an empty JSON object will be returned.' operationId: 'HIGH_WATERMARK' responses: '200': From 7abdcf1e23fd6c24972a6a0e5e2d27e1eec3828c Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Tue, 12 Sep 2023 15:53:09 +1000 Subject: [PATCH 4/4] Clarify open api description --- .../openapi-specs/eth2/signing/paths/high_watermark.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml b/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml index 9956e6057..dba0d1311 100644 --- a/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml +++ b/core/src/main/resources/openapi-specs/eth2/signing/paths/high_watermark.yaml @@ -2,7 +2,7 @@ get: tags: - 'High Watermark' summary: 'The High Watermark epoch and slot applicable to all validators' - description: 'Returns the uint64 epoch and slot of the high watermark. Signing of attestations or blocks are prevented at or beyond this high watermark. If no high watermark is set, an empty JSON object will be returned.' + description: 'Returns the uint64 epoch and slot of the high watermark. Signing of attestations or blocks are only allowed when they are lower than this high watermark. If no high watermark is set, an empty JSON object will be returned.' operationId: 'HIGH_WATERMARK' responses: '200':