Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GET /highWatermark endpoint #908

Merged
merged 6 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
jframe marked this conversation as resolved.
Show resolved Hide resolved
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) {
jframe marked this conversation as resolved.
Show resolved Hide resolved
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