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 1608a0ce1..2c3629063 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 @@ -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; @@ -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 = @@ -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() @@ -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; } diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/HighWatermark.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/HighWatermark.java index aa81a8665..aa6fc5260 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/HighWatermark.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/dao/HighWatermark.java @@ -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 + '}'; + } } diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/AttestationImporter.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/AttestationImporter.java index 2aad12da5..5a34ce58f 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/AttestationImporter.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/AttestationImporter.java @@ -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; @@ -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; @@ -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; } @@ -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()); diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/BlockImporter.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/BlockImporter.java index 01b2f0466..f558b06f5 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/BlockImporter.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/BlockImporter.java @@ -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; @@ -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; @@ -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; } @@ -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()); diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/InterchangeV5Importer.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/InterchangeV5Importer.java index bd00ca29d..93d352cb0 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/InterchangeV5Importer.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/interchange/InterchangeV5Importer.java @@ -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); } @@ -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); } diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/AttestationValidator.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/AttestationValidator.java index 683e73766..5264885df 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/AttestationValidator.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/AttestationValidator.java @@ -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; @@ -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> watermarkSupplier; @@ -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; @@ -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)); @@ -129,6 +134,24 @@ public boolean hasTargetOlderThanWatermark() { return false; } + public boolean hasEpochAtOrBeyondHighWatermark() { + final Optional 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 surroundedAttestations = diff --git a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/BlockValidator.java b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/BlockValidator.java index 7df5c021f..e447e2a81 100644 --- a/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/BlockValidator.java +++ b/slashing-protection/src/main/java/tech/pegasys/web3signer/slashingprotection/validator/BlockValidator.java @@ -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; @@ -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> watermarkSupplier; @@ -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)); } @@ -79,6 +84,15 @@ public boolean isOlderThanWatermark() { return false; } + public boolean isAtOrBeyondHighWatermark() { + final Optional 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); diff --git a/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtectionTest.java b/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtectionTest.java index 19ac1ce13..f850f43d6 100644 --- a/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtectionTest.java +++ b/slashing-protection/src/test/java/tech/pegasys/web3signer/slashingprotection/DbSlashingProtectionTest.java @@ -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; @@ -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 = @@ -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 =