Skip to content

Commit

Permalink
Prevent signing at or beyond high watermark (#907)
Browse files Browse the repository at this point in the history
  • Loading branch information
siladu authored Sep 11, 2023
1 parent 79d8053 commit f98bae7
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class DbSlashingProtection implements SlashingProtection {
private final SignedAttestationsDao signedAttestationsDao;
private final InterchangeManager interchangeManager;
private final LowWatermarkDao lowWatermarkDao;
private final MetadataDao metadataDao;
private final GenesisValidatorRootValidator gvrValidator;
private final RegisteredValidators registeredValidators;

Expand All @@ -68,6 +69,7 @@ public DbSlashingProtection(
this.signedBlocksDao = signedBlocksDao;
this.signedAttestationsDao = signedAttestationsDao;
this.lowWatermarkDao = lowWatermarkDao;
this.metadataDao = metadataDao;
this.registeredValidators = registeredValidators;
this.gvrValidator = new GenesisValidatorRootValidator(jdbi, metadataDao);
this.interchangeManager =
Expand Down Expand Up @@ -175,13 +177,15 @@ public boolean maySignAttestation(
targetEpoch,
validatorId,
signedAttestationsDao,
lowWatermarkDao);
lowWatermarkDao,
metadataDao);

if (attestationValidator.sourceGreaterThanTargetEpoch()) {
return false;
}

if (attestationValidator.hasSourceOlderThanWatermark()
if (attestationValidator.hasEpochAtOrBeyondHighWatermark()
|| attestationValidator.hasSourceOlderThanWatermark()
|| attestationValidator.hasTargetOlderThanWatermark()
|| attestationValidator.directlyConflictsWithExistingEntry()
|| attestationValidator.isSurroundedByExistingAttestation()
Expand Down Expand Up @@ -220,9 +224,16 @@ public boolean maySignBlock(

final BlockValidator blockValidator =
new BlockValidator(
h, signingRoot, blockSlot, validatorId, signedBlocksDao, lowWatermarkDao);
h,
signingRoot,
blockSlot,
validatorId,
signedBlocksDao,
lowWatermarkDao,
metadataDao);

if (blockValidator.isOlderThanWatermark()
if (blockValidator.isAtOrBeyondHighWatermark()
|| blockValidator.isOlderThanWatermark()
|| blockValidator.directlyConflictsWithExistingEntry()) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(slot, epoch);
}

@Override
public String toString() {
return "HighWatermark{" + "slot=" + slot + ", epoch=" + epoch + '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package tech.pegasys.web3signer.slashingprotection.interchange;

import tech.pegasys.web3signer.slashingprotection.dao.LowWatermarkDao;
import tech.pegasys.web3signer.slashingprotection.dao.MetadataDao;
import tech.pegasys.web3signer.slashingprotection.dao.SignedAttestationsDao;
import tech.pegasys.web3signer.slashingprotection.dao.SigningWatermark;
import tech.pegasys.web3signer.slashingprotection.dao.Validator;
Expand All @@ -36,6 +37,7 @@ public class AttestationImporter {
private final OptionalMinValueTracker minSourceTracker = new OptionalMinValueTracker();
private final OptionalMinValueTracker minTargetTracker = new OptionalMinValueTracker();
private final LowWatermarkDao lowWatermarkDao;
private final MetadataDao metadataDao;
private final SignedAttestationsDao signedAttestationsDao;
private final Validator validator;
private final Handle handle;
Expand All @@ -46,11 +48,13 @@ public AttestationImporter(
final Handle handle,
final ObjectMapper mapper,
final LowWatermarkDao lowWatermarkDao,
final MetadataDao metadataDao,
final SignedAttestationsDao signedAttestationsDao) {
this.validator = validator;
this.handle = handle;
this.mapper = mapper;
this.lowWatermarkDao = lowWatermarkDao;
this.metadataDao = metadataDao;
this.signedAttestationsDao = signedAttestationsDao;
}

Expand All @@ -68,7 +72,8 @@ public void importFrom(final ArrayNode signedAttestationNode) throws JsonProcess
jsonAttestation.getTargetEpoch(),
validator.getId(),
signedAttestationsDao,
lowWatermarkDao);
lowWatermarkDao,
metadataDao);

final String attestationIdentifierString =
String.format("Attestation with index %d for validator %s", i, validator.getPublicKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package tech.pegasys.web3signer.slashingprotection.interchange;

import tech.pegasys.web3signer.slashingprotection.dao.LowWatermarkDao;
import tech.pegasys.web3signer.slashingprotection.dao.MetadataDao;
import tech.pegasys.web3signer.slashingprotection.dao.SignedBlocksDao;
import tech.pegasys.web3signer.slashingprotection.dao.SigningWatermark;
import tech.pegasys.web3signer.slashingprotection.dao.Validator;
Expand All @@ -35,6 +36,7 @@ public class BlockImporter {

private final OptionalMinValueTracker minSlotTracker = new OptionalMinValueTracker();
private final LowWatermarkDao lowWatermarkDao;
private final MetadataDao metadataDao;
private final SignedBlocksDao signedBlocksDao;
private final Validator validator;
private final Handle handle;
Expand All @@ -45,11 +47,13 @@ public BlockImporter(
final Handle handle,
final ObjectMapper mapper,
final LowWatermarkDao lowWatermarkDao,
final MetadataDao metadataDao,
final SignedBlocksDao signedBlocksDao) {
this.validator = validator;
this.handle = handle;
this.mapper = mapper;
this.lowWatermarkDao = lowWatermarkDao;
this.metadataDao = metadataDao;
this.signedBlocksDao = signedBlocksDao;
}

Expand All @@ -64,7 +68,8 @@ public void importFrom(final ArrayNode signedBlocksNode) throws JsonProcessingEx
jsonBlock.getSlot(),
validator.getId(),
signedBlocksDao,
lowWatermarkDao);
lowWatermarkDao,
metadataDao);

final String blockIdentifierString =
String.format("Block with index %d for validator %s", i, validator.getPublicKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ private void importBlocks(
throws JsonProcessingException {

final BlockImporter blockImporter =
new BlockImporter(validator, handle, JSON_MAPPER, lowWatermarkDao, signedBlocksDao);
new BlockImporter(
validator, handle, JSON_MAPPER, lowWatermarkDao, metadataDao, signedBlocksDao);
blockImporter.importFrom(signedBlocksNode);
}

Expand All @@ -164,7 +165,7 @@ private void importAttestations(

final AttestationImporter attestationImporter =
new AttestationImporter(
validator, handle, JSON_MAPPER, lowWatermarkDao, signedAttestationsDao);
validator, handle, JSON_MAPPER, lowWatermarkDao, metadataDao, signedAttestationsDao);

attestationImporter.importFrom(signedAttestationNode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
*/
package tech.pegasys.web3signer.slashingprotection.validator;

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.SignedAttestation;
import tech.pegasys.web3signer.slashingprotection.dao.SignedAttestationsDao;
import tech.pegasys.web3signer.slashingprotection.dao.SigningWatermark;
Expand Down Expand Up @@ -40,6 +42,7 @@ public class AttestationValidator {
private final int validatorId;
private final SignedAttestationsDao signedAttestationsDao;
private final LowWatermarkDao lowWatermarkDao;
private final MetadataDao metadataDao;

private final Supplier<Optional<SigningWatermark>> watermarkSupplier;

Expand All @@ -51,7 +54,8 @@ public AttestationValidator(
final UInt64 targetEpoch,
final int validatorId,
final SignedAttestationsDao signedAttestationsDao,
final LowWatermarkDao lowWatermarkDao) {
final LowWatermarkDao lowWatermarkDao,
final MetadataDao metadataDao) {
this.handle = handle;
this.publicKey = publicKey;
this.signingRoot = signingRoot;
Expand All @@ -60,6 +64,7 @@ public AttestationValidator(
this.validatorId = validatorId;
this.signedAttestationsDao = signedAttestationsDao;
this.lowWatermarkDao = lowWatermarkDao;
this.metadataDao = metadataDao;

watermarkSupplier =
Suppliers.memoize(() -> lowWatermarkDao.findLowWatermarkForValidator(handle, validatorId));
Expand Down Expand Up @@ -129,6 +134,24 @@ public boolean hasTargetOlderThanWatermark() {
return false;
}

public boolean hasEpochAtOrBeyondHighWatermark() {
final Optional<HighWatermark> highWatermark = metadataDao.findHighWatermark(handle);
if (highWatermark
.map(
h ->
sourceEpoch.compareTo(h.getEpoch()) >= 0
|| targetEpoch.compareTo(h.getEpoch()) >= 0)
.orElse(false)) {
LOG.warn(
"Attestation source {} or target {} is at or beyond high watermark {}",
sourceEpoch,
targetEpoch,
highWatermark.get());
return true;
}
return false;
}

public boolean surroundsExistingAttestation() {
// check that no previous vote is surrounded by attestation
final List<SignedAttestation> surroundedAttestations =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
*/
package tech.pegasys.web3signer.slashingprotection.validator;

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.SignedBlock;
import tech.pegasys.web3signer.slashingprotection.dao.SignedBlocksDao;
import tech.pegasys.web3signer.slashingprotection.dao.SigningWatermark;
Expand All @@ -37,6 +39,7 @@ public class BlockValidator {
private final int validatorId;
private final SignedBlocksDao signedBlocksDao;
private final LowWatermarkDao lowWatermarkDao;
private final MetadataDao metadataDao;

private final Supplier<Optional<SigningWatermark>> watermarkSupplier;

Expand All @@ -46,13 +49,15 @@ public BlockValidator(
final UInt64 blockSlot,
final int validatorId,
final SignedBlocksDao signedBlocksDao,
final LowWatermarkDao lowWatermarkDao) {
final LowWatermarkDao lowWatermarkDao,
final MetadataDao metadataDao) {
this.handle = handle;
this.signingRoot = signingRoot;
this.blockSlot = blockSlot;
this.validatorId = validatorId;
this.signedBlocksDao = signedBlocksDao;
this.lowWatermarkDao = lowWatermarkDao;
this.metadataDao = metadataDao;
watermarkSupplier =
Suppliers.memoize(() -> lowWatermarkDao.findLowWatermarkForValidator(handle, validatorId));
}
Expand All @@ -79,6 +84,15 @@ public boolean isOlderThanWatermark() {
return false;
}

public boolean isAtOrBeyondHighWatermark() {
final Optional<HighWatermark> highWatermark = metadataDao.findHighWatermark(handle);
if (highWatermark.map(h -> blockSlot.compareTo(h.getSlot()) >= 0).orElse(false)) {
LOG.warn("Block slot {} is at or beyond high watermark {}", blockSlot, highWatermark.get());
return true;
}
return false;
}

public void persist() {
final SignedBlock signedBlock = new SignedBlock(validatorId, blockSlot, signingRoot);
signedBlocksDao.insertBlockProposal(handle, signedBlock);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

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.SignedAttestation;
Expand Down Expand Up @@ -149,6 +150,32 @@ public void blockCannotSignWhenSamePublicKeyAndSlotButDifferentSigningRootAboveW
.insertBlockProposal(any(), refEq(new SignedBlock(VALIDATOR_ID, SLOT, SIGNING_ROOT)));
}

@Test
public void blockCannotSignWhenSlotIsAtOrBeyondHighWatermark() {
final SignedBlock signedBlock = new SignedBlock(VALIDATOR_ID, SLOT, SIGNING_ROOT);
when(metadataDao.findHighWatermark(any()))
// current equals high-watermark
.thenReturn(Optional.of(new HighWatermark(SLOT, null)))
// current beyond high-watermark
.thenReturn(Optional.of(new HighWatermark(SLOT.subtract(1L), null)));

assertThat(dbSlashingProtection.maySignBlock(PUBLIC_KEY1, SIGNING_ROOT, SLOT, GVR)).isFalse();
assertThat(dbSlashingProtection.maySignBlock(PUBLIC_KEY1, SIGNING_ROOT, SLOT, GVR)).isFalse();

verify(signedBlocksDao, never()).insertBlockProposal(any(), refEq(signedBlock));
}

@Test
public void blockCanSignWhenSlotIsBelowHighWatermark() {
final SignedBlock signedBlock = new SignedBlock(VALIDATOR_ID, SLOT, SIGNING_ROOT);
when(metadataDao.findHighWatermark(any()))
.thenReturn(Optional.of(new HighWatermark(SLOT.add(1L), null)));

assertThat(dbSlashingProtection.maySignBlock(PUBLIC_KEY1, SIGNING_ROOT, SLOT, GVR)).isTrue();

verify(signedBlocksDao).insertBlockProposal(any(), refEq(signedBlock));
}

@Test
public void blockCannotSignWhenNoRegisteredValidator(final Jdbi jdbi) {
final DbSlashingProtection dbSlashingProtection =
Expand Down Expand Up @@ -238,6 +265,80 @@ public void cannotSignAttestationWhichWasPreviouslySignedButBelowTargetWatermark
verify(lowWatermarkDao).findLowWatermarkForValidator(any(), eq(VALIDATOR_ID));
}

@Test
public void cannotSignAttestationWhenSourceIsAtOrBeyondHighWatermark() {
final SignedAttestation attestation =
new SignedAttestation(VALIDATOR_ID, SOURCE_EPOCH, SOURCE_EPOCH, SIGNING_ROOT);
when(metadataDao.findHighWatermark(any()))
// current equals at high-watermark
.thenReturn(Optional.of(new HighWatermark(null, SOURCE_EPOCH)))
// current beyond high-watermark
.thenReturn(Optional.of(new HighWatermark(null, SOURCE_EPOCH.subtract(1L))));

assertThat(
dbSlashingProtection.maySignAttestation(
PUBLIC_KEY1, SIGNING_ROOT, SOURCE_EPOCH, SOURCE_EPOCH, GVR))
.isFalse();
assertThat(
dbSlashingProtection.maySignAttestation(
PUBLIC_KEY1, SIGNING_ROOT, SOURCE_EPOCH, SOURCE_EPOCH, GVR))
.isFalse();

verify(signedAttestationsDao, never()).insertAttestation(any(), refEq(attestation));
}

@Test
public void cannotSignAttestationWhenTargetIsAtOrBeyondHighWatermark() {
final SignedAttestation attestation =
new SignedAttestation(VALIDATOR_ID, SOURCE_EPOCH, TARGET_EPOCH, SIGNING_ROOT);
when(metadataDao.findHighWatermark(any()))
// current equals at high-watermark
.thenReturn(Optional.of(new HighWatermark(null, TARGET_EPOCH)))
// current beyond high-watermark
.thenReturn(Optional.of(new HighWatermark(null, TARGET_EPOCH.subtract(1L))));

assertThat(
dbSlashingProtection.maySignAttestation(
PUBLIC_KEY1, SIGNING_ROOT, SOURCE_EPOCH, TARGET_EPOCH, GVR))
.isFalse();
assertThat(
dbSlashingProtection.maySignAttestation(
PUBLIC_KEY1, SIGNING_ROOT, SOURCE_EPOCH, TARGET_EPOCH, GVR))
.isFalse();

verify(signedAttestationsDao, never()).insertAttestation(any(), refEq(attestation));
}

@Test
public void attestationCanSignWhenSourceIsBelowHighWatermark() {
final SignedAttestation attestation =
new SignedAttestation(VALIDATOR_ID, SOURCE_EPOCH, SOURCE_EPOCH, SIGNING_ROOT);
when(metadataDao.findHighWatermark(any()))
.thenReturn(Optional.of(new HighWatermark(null, SOURCE_EPOCH.add(1L))));

assertThat(
dbSlashingProtection.maySignAttestation(
PUBLIC_KEY1, SIGNING_ROOT, SOURCE_EPOCH, SOURCE_EPOCH, GVR))
.isTrue();

verify(signedAttestationsDao).insertAttestation(any(), refEq(attestation));
}

@Test
public void attestationCanSignWhenTargetIsBelowHighWatermark() {
final SignedAttestation attestation =
new SignedAttestation(VALIDATOR_ID, SOURCE_EPOCH, TARGET_EPOCH, SIGNING_ROOT);
when(metadataDao.findHighWatermark(any()))
.thenReturn(Optional.of(new HighWatermark(null, TARGET_EPOCH.add(1L))));

assertThat(
dbSlashingProtection.maySignAttestation(
PUBLIC_KEY1, SIGNING_ROOT, SOURCE_EPOCH, TARGET_EPOCH, GVR))
.isTrue();

verify(signedAttestationsDao).insertAttestation(any(), refEq(attestation));
}

@Test
public void attestationCannotSignWhenPreviousIsSurroundingAttestation() {
final SignedAttestation attestation =
Expand Down

0 comments on commit f98bae7

Please sign in to comment.