Skip to content

Commit

Permalink
Improve block equivocation check for block publishing V2 API (#8043)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbenr authored Mar 6, 2024
1 parent 5989775 commit 6e65915
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock;
import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel;
import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult;
import tech.pegasys.teku.statetransition.validation.BlockGossipValidator.EquivocationCheckResult;

public class BlockBroadcastValidatorImpl implements BlockBroadcastValidator {
private final BlockGossipValidator blockGossipValidator;
Expand Down Expand Up @@ -92,7 +93,8 @@ private void buildValidationPipeline(final SignedBeaconBlock block) {
.validate(block, true)
.thenApply(
gossipValidationResult -> {
if (gossipValidationResult.isAccept()) {
if (gossipValidationResult.isAccept()
|| gossipValidationResult.isIgnoreAlreadySeen()) {
return SUCCESS;
}
return GOSSIP_FAILURE;
Expand Down Expand Up @@ -136,11 +138,13 @@ private void buildValidationPipeline(final SignedBeaconBlock block) {
}

// perform final equivocation validation
if (blockGossipValidator.blockIsFirstBlockWithValidSignatureForSlot(block)) {
return SUCCESS;
if (blockGossipValidator
.performBlockEquivocationCheck(block)
.equals(EquivocationCheckResult.EQUIVOCATING_BLOCK_FOR_SLOT_PROPOSER)) {
return FINAL_EQUIVOCATION_FAILURE;
}

return FINAL_EQUIVOCATION_FAILURE;
return SUCCESS;
})
.propagateTo(broadcastValidationResult);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@

import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture;
import static tech.pegasys.teku.spec.config.Constants.VALID_BLOCK_SET_SIZE;
import static tech.pegasys.teku.statetransition.validation.BlockGossipValidator.EquivocationCheckResult.BLOCK_ALREADY_SEEN_FOR_SLOT_PROPOSER;
import static tech.pegasys.teku.statetransition.validation.BlockGossipValidator.EquivocationCheckResult.EQUIVOCATING_BLOCK_FOR_SLOT_PROPOSER;
import static tech.pegasys.teku.statetransition.validation.BlockGossipValidator.EquivocationCheckResult.FIRST_BLOCK_FOR_SLOT_PROPOSER;
import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore;
import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject;
import static tech.pegasys.teku.statetransition.validation.ValidationResultCode.ValidationResultSubCode.IGNORE_ALREADY_SEEN;
import static tech.pegasys.teku.statetransition.validation.ValidationResultCode.ValidationResultSubCode.IGNORE_EQUIVOCATION_DETECTED;

import com.google.common.base.Objects;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.collections.LimitedSet;
import tech.pegasys.teku.infrastructure.collections.LimitedMap;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.constants.Domain;
Expand All @@ -43,8 +47,8 @@ public class BlockGossipValidator {
private final GossipValidationHelper gossipValidationHelper;
private final ReceivedBlockEventsChannel receivedBlockEventsChannelPublisher;

private final Set<SlotAndProposer> receivedValidBlockInfoSet =
LimitedSet.createSynchronized(VALID_BLOCK_SET_SIZE);
private final Map<SlotAndProposer, Bytes32> receivedValidBlockInfoSet =
LimitedMap.createNonSynchronized(VALID_BLOCK_SET_SIZE);

public BlockGossipValidator(
final Spec spec,
Expand Down Expand Up @@ -76,14 +80,18 @@ public SafeFuture<InternalValidationResult> validate(
private SafeFuture<InternalValidationResult> internalValidate(
final SignedBeaconBlock block, final boolean isLocallyProduced) {

if (gossipValidationHelper.isSlotFinalized(block.getSlot())
|| !blockIsFirstBlockWithValidSignatureForSlot(block)) {
LOG.trace(
"BlockValidator: Block is either too old or is not the first block with valid signature for "
+ "its slot. It will be dropped");
if (gossipValidationHelper.isSlotFinalized(block.getSlot())) {
LOG.trace("BlockValidator: Block is too old. It will be dropped");
return completedFuture(InternalValidationResult.IGNORE);
}

final InternalValidationResult intermediateValidationResult =
equivocationCheckResultToInternalValidationResult(performBlockEquivocationCheck(block));

if (!intermediateValidationResult.isAccept()) {
return completedFuture(intermediateValidationResult);
}

if (gossipValidationHelper.isSlotFromFuture(block.getSlot())) {
LOG.trace("BlockValidator: Block is from the future. It will be saved for future processing");
return completedFuture(InternalValidationResult.SAVE_FOR_FUTURE);
Expand Down Expand Up @@ -163,21 +171,57 @@ private SafeFuture<InternalValidationResult> internalValidate(
// BroadcastValidationLevel.CONSENSUS_EQUIVOCATION is used (one at the beginning of
// the blocks import,
// another after consensus validation)
final boolean equivocationCheckSuccessful =
final EquivocationCheckResult secondEquivocationCheckResult =
isLocallyProduced
? blockIsFirstBlockWithValidSignatureForSlot(block)
: receivedValidBlockInfoSet.add(new SlotAndProposer(block));
if (!equivocationCheckSuccessful) {
return ignore(
"Block is not the first with valid signature for its slot. It will be dropped.");
}
? performBlockEquivocationCheck(false, block)
: performBlockEquivocationCheck(true, block);

return InternalValidationResult.ACCEPT;
return equivocationCheckResultToInternalValidationResult(
secondEquivocationCheckResult);
});
}

boolean blockIsFirstBlockWithValidSignatureForSlot(final SignedBeaconBlock block) {
return !receivedValidBlockInfoSet.contains(new SlotAndProposer(block));
private InternalValidationResult equivocationCheckResultToInternalValidationResult(
final EquivocationCheckResult equivocationCheckResult) {
return switch (equivocationCheckResult) {
case FIRST_BLOCK_FOR_SLOT_PROPOSER -> InternalValidationResult.ACCEPT;
case EQUIVOCATING_BLOCK_FOR_SLOT_PROPOSER -> ignore(
IGNORE_EQUIVOCATION_DETECTED, "Equivocating block detected. It will be dropped.");
case BLOCK_ALREADY_SEEN_FOR_SLOT_PROPOSER -> ignore(
IGNORE_ALREADY_SEEN,
"Block is not the first with valid signature for its slot. It will be dropped.");
};
}

private synchronized EquivocationCheckResult performBlockEquivocationCheck(
final boolean add, final SignedBeaconBlock block) {
final SlotAndProposer slotAndProposer = new SlotAndProposer(block);

final Optional<Bytes32> maybePreviouslySeenBlockRoot =
Optional.ofNullable(receivedValidBlockInfoSet.get(slotAndProposer));

if (maybePreviouslySeenBlockRoot.isEmpty()) {
if (add) {
receivedValidBlockInfoSet.put(slotAndProposer, block.getRoot());
}
return FIRST_BLOCK_FOR_SLOT_PROPOSER;
}

if (maybePreviouslySeenBlockRoot.get().equals(block.getRoot())) {
return BLOCK_ALREADY_SEEN_FOR_SLOT_PROPOSER;
}

return EQUIVOCATING_BLOCK_FOR_SLOT_PROPOSER;
}

EquivocationCheckResult performBlockEquivocationCheck(final SignedBeaconBlock block) {
return performBlockEquivocationCheck(false, block);
}

public enum EquivocationCheckResult {
FIRST_BLOCK_FOR_SLOT_PROPOSER,
BLOCK_ALREADY_SEEN_FOR_SLOT_PROPOSER,
EQUIVOCATING_BLOCK_FOR_SLOT_PROPOSER
}

private boolean blockSignatureIsValidWithRespectToProposerIndex(
Expand All @@ -194,30 +238,9 @@ private boolean blockSignatureIsValidWithRespectToProposerIndex(
signingRoot, block.getProposerIndex(), block.getSignature(), postState);
}

private static class SlotAndProposer {
private final UInt64 slot;
private final UInt64 proposerIndex;

private record SlotAndProposer(UInt64 slot, UInt64 proposerIndex) {
public SlotAndProposer(final SignedBeaconBlock block) {
this.slot = block.getSlot();
this.proposerIndex = block.getMessage().getProposerIndex();
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SlotAndProposer)) {
return false;
}
SlotAndProposer that = (SlotAndProposer) o;
return Objects.equal(slot, that.slot) && Objects.equal(proposerIndex, that.proposerIndex);
}

@Override
public int hashCode() {
return Objects.hashCode(slot, proposerIndex);
this(block.getSlot(), block.getMessage().getProposerIndex());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.google.errorprone.annotations.FormatMethod;
import java.util.Objects;
import java.util.Optional;
import tech.pegasys.teku.statetransition.validation.ValidationResultCode.ValidationResultSubCode;

public class InternalValidationResult {

Expand All @@ -30,20 +31,33 @@ public class InternalValidationResult {

private final ValidationResultCode validationResultCode;
private final Optional<String> description;
private final Optional<ValidationResultSubCode> validationResultSubCode;

private InternalValidationResult(
final ValidationResultCode validationResultCode, final Optional<String> description) {
final ValidationResultCode validationResultCode,
final Optional<ValidationResultSubCode> validationResultSubCode,
final Optional<String> description) {
this.validationResultCode = validationResultCode;
this.validationResultSubCode = validationResultSubCode;
this.description = description;
}

static InternalValidationResult create(final ValidationResultCode validationResultCode) {
return new InternalValidationResult(validationResultCode, Optional.empty());
return new InternalValidationResult(validationResultCode, Optional.empty(), Optional.empty());
}

public static InternalValidationResult create(
final ValidationResultCode validationResultCode, final String description) {
return new InternalValidationResult(validationResultCode, Optional.of(description));
return new InternalValidationResult(
validationResultCode, Optional.empty(), Optional.of(description));
}

public static InternalValidationResult create(
final ValidationResultCode validationResultCode,
final ValidationResultSubCode subCode,
final String description) {
return new InternalValidationResult(
validationResultCode, Optional.of(subCode), Optional.of(description));
}

@FormatMethod
Expand All @@ -58,10 +72,25 @@ public static InternalValidationResult ignore(
return create(ValidationResultCode.IGNORE, String.format(descriptionTemplate, args));
}

@FormatMethod
public static InternalValidationResult ignore(
final ValidationResultSubCode validationResultSubCode,
final String descriptionTemplate,
final Object... args) {
return create(
ValidationResultCode.IGNORE,
validationResultSubCode,
String.format(descriptionTemplate, args));
}

public ValidationResultCode code() {
return validationResultCode;
}

public Optional<ValidationResultSubCode> getValidationResultSubCode() {
return validationResultSubCode;
}

public Optional<String> getDescription() {
return description;
}
Expand Down Expand Up @@ -91,6 +120,13 @@ public boolean isIgnore() {
return this.validationResultCode.equals(ValidationResultCode.IGNORE);
}

public boolean isIgnoreAlreadySeen() {
return isIgnore()
&& this.validationResultSubCode
.map(subCode -> subCode.equals(ValidationResultSubCode.IGNORE_ALREADY_SEEN))
.orElse(false);
}

public boolean isReject() {
return this.validationResultCode.equals(ValidationResultCode.REJECT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ public enum ValidationResultCode {
ACCEPT,
SAVE_FOR_FUTURE,
IGNORE,
REJECT
REJECT;

public enum ValidationResultSubCode {
IGNORE_EQUIVOCATION_DETECTED,
IGNORE_ALREADY_SEEN
}
}
Loading

0 comments on commit 6e65915

Please sign in to comment.