Skip to content

Commit

Permalink
GET /highWatermark endpoint (#908)
Browse files Browse the repository at this point in the history
- Add GET /highWatermark endpoint to eth2 API
- Handle null epoch OR null slot (case when both are null is already handled)
  • Loading branch information
siladu committed Sep 12, 2023
1 parent a383c32 commit b4a725d
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> slashingProtectionContext;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RoutingContext> {
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());
}
}
Original file line number Diff line number Diff line change
@@ -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 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':
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'
2 changes: 2 additions & 0 deletions core/src/main/resources/openapi-specs/eth2/web3signer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -274,6 +275,11 @@ public void updateValidatorEnabledStatus(final Bytes publicKey, final boolean en
});
}

@Override
public Optional<HighWatermark> getHighWatermark() {
return jdbi.inTransaction(READ_COMMITTED, metadataDao::findHighWatermark);
}

private boolean isEnabled(final Handle handle, final int validatorId) {
return validatorsDao.isEnabled(handle, validatorId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,4 +51,6 @@ boolean maySignBlock(
boolean isEnabledValidator(Bytes publicKey);

void updateValidatorEnabledStatus(Bytes publicKey, boolean enabled);

Optional<HighWatermark> getHighWatermark();
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public Optional<HighWatermark> 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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HighWatermark> 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<HighWatermark> 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();
Expand All @@ -110,15 +130,27 @@ public void insertsHighWatermark(final Handle handle) {

int updateCount = metadataDao.updateHighWatermark(handle, highWatermark);

assertThat(updateCount).isEqualTo(1);
final List<HighWatermark> 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
Expand All @@ -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<HighWatermark> highWatermarks =
handle
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit b4a725d

Please sign in to comment.