Skip to content
Open
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 @@ -17,15 +17,17 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes32;
import tech.pegasys.teku.bls.impl.BlsException;
import tech.pegasys.teku.ethereum.events.SlotEventsChannel;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.exceptions.ExceptionUtil;
import tech.pegasys.teku.infrastructure.logging.EventLogger;
import tech.pegasys.teku.infrastructure.ssz.InvalidValueSchemaException;
import tech.pegasys.teku.infrastructure.ssz.sos.SszDeserializeException;
import tech.pegasys.teku.infrastructure.subscribers.Subscribers;
import tech.pegasys.teku.infrastructure.time.TimeProvider;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
Expand All @@ -35,6 +37,8 @@
import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadSummary;
import tech.pegasys.teku.spec.datastructures.validator.BroadcastValidationLevel;
import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.BlockProcessingException;
import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.StateTransitionException;
import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult;
import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult.FailureReason;
import tech.pegasys.teku.statetransition.blobs.BlockEventsListener;
Expand All @@ -55,6 +59,28 @@ public class BlockManager extends Service
ReceivedExecutionPayloadEventsChannel {
private static final Logger LOG = LogManager.getLogger();

/**
* An internal error is, by default, not a proof that a block is invalid: it is most likely caused
* by a local failure (resource exhaustion, a transient infrastructure issue or a bug). Marking
* the block as invalid in those cases makes us reject the canonical chain, along with all its
* descendants, until the entry is evicted from the invalid blocks cache.
*
* <p>So only errors which can exclusively be attributed to the content of the block itself are
* considered as a proof of invalidity.
*/
private static final List<Class<? extends Throwable>> INVALID_BLOCK_INTERNAL_ERRORS =
List.of(
// the state transition rejected the block
StateTransitionException.class,
BlockProcessingException.class,
// the block contains malformed SSZ data or makes the post state violate its schema
SszDeserializeException.class,
InvalidValueSchemaException.class,
// the block contains a malformed BLS public key or signature
BlsException.class,
// a value in the block caused an overflow or underflow while processing it
ArithmeticException.class);

private final RecentChainData recentChainData;
private final BlockImporter blockImporter;
private final BlockEventsListener blockEventsListener;
Expand Down Expand Up @@ -380,7 +406,7 @@ private SafeFuture<BlockImportResult> handleBlockImport(
logFailedBlockImport(block, result.getFailureReason());
if (result
.getFailureCause()
.map(this::internalErrorToBeConsiderAsInvalidBlock)
.map(BlockManager::internalErrorToBeConsiderAsInvalidBlock)
.orElse(false)) {
dropInvalidBlock(block, result);
}
Expand Down Expand Up @@ -447,12 +473,10 @@ private List<SignedBeaconBlock> removeBlocksPendingParentExecutionPayloadDependi
return pendingBlockPool.removeBlocksWaitingForParentExecutionPayload(parentRoot);
}

private boolean internalErrorToBeConsiderAsInvalidBlock(final Throwable internalError) {
if (internalError instanceof RejectedExecutionException
|| ExceptionUtil.hasCause(internalError, RejectedExecutionException.class)) {
return false;
}
return true;
private static boolean internalErrorToBeConsiderAsInvalidBlock(final Throwable internalError) {
// hasCause also checks the exception itself

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the original internalError instanceof RejectedExecutionException || ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's opposite originally

private boolean internalErrorToBeConsiderAsInvalidBlock(final Throwable internalError) {
  if (internalError instanceof RejectedExecutionException
      || ExceptionUtil.hasCause(internalError, RejectedExecutionException.class)) {
    return false;
  }

i guess because we don't want to stuck forever on EL malfunctioning

return INVALID_BLOCK_INTERNAL_ERRORS.stream()
.anyMatch(errorType -> ExceptionUtil.hasCause(internalError, errorType));
}

private void logFailedBlockImport(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Answers;
import tech.pegasys.teku.bls.BLSSignatureVerifier;
import tech.pegasys.teku.bls.impl.BlsException;
import tech.pegasys.teku.infrastructure.async.AsyncRunner;
import tech.pegasys.teku.infrastructure.async.ExceptionThrowingFutureSupplier;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
Expand All @@ -70,9 +75,12 @@
import tech.pegasys.teku.infrastructure.logging.EventLogger;
import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge;
import tech.pegasys.teku.infrastructure.metrics.StubMetricsSystem;
import tech.pegasys.teku.infrastructure.ssz.InvalidValueSchemaException;
import tech.pegasys.teku.infrastructure.ssz.sos.SszDeserializeException;
import tech.pegasys.teku.infrastructure.time.StubTimeProvider;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.kzg.NoOpKZG;
import tech.pegasys.teku.service.serviceutils.ServiceCapacityExceededException;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.TestSpecFactory;
import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar;
Expand All @@ -87,6 +95,8 @@
import tech.pegasys.teku.spec.generator.ChainBuilder.BlockOptions;
import tech.pegasys.teku.spec.logic.common.statetransition.availability.AvailabilityChecker;
import tech.pegasys.teku.spec.logic.common.statetransition.availability.DataAndValidationResult;
import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.BlockProcessingException;
import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.StateTransitionException;
import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult;
import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult.FailureReason;
import tech.pegasys.teku.spec.util.DataStructureUtil;
Expand Down Expand Up @@ -479,26 +489,46 @@ public void onGossipedBlock_unattachedFutureBlock() {
assertThat(pendingBlocks.contains(nextNextBlock)).isTrue();
}

@Test
public void onGossipedBlock_onKnownInternalErrorsShouldNotMarkAsInvalid() {
final RecentChainData localRecentChainData = mock(RecentChainData.class);
blockManager = setupBlockManagerWithMockRecentChainData(localRecentChainData, false);
static Stream<Arguments> internalErrorsNotProvingBlockIsInvalid() {
return Stream.of(
Arguments.of(new RejectedExecutionException("full")),
Arguments.of(new RuntimeException("wrapped", new RejectedExecutionException("full"))),
Arguments.of(new OutOfMemoryError("Java heap space")),
Arguments.of(new ServiceCapacityExceededException("queue is full")),
Arguments.of(new IllegalStateException("unexpected")),
Arguments.of(new RuntimeException("unknown")));
}

final UInt64 nextSlot = GENESIS_SLOT.plus(UInt64.ONE);
final SignedBeaconBlock nextBlock =
localChain.chainBuilder().generateBlockAtSlot(nextSlot).getBlock();
incrementSlot();
static Stream<Arguments> internalErrorsProvingBlockIsInvalid() {
return Stream.of(
Arguments.of(new StateTransitionException("state transition failed")),
Arguments.of(new BlockProcessingException("block processing failed")),
Arguments.of(new SszDeserializeException("malformed ssz")),
Arguments.of(new InvalidValueSchemaException("value doesn't match the schema")),
Arguments.of(new BlsException("invalid public key")),
Arguments.of(new ArithmeticException("uint64 overflow")),
Arguments.of(new RuntimeException("wrapped", new BlockProcessingException("invalid"))));
}

doAnswer(invocation -> SafeFuture.failedFuture(new RejectedExecutionException("full")))
.when(asyncRunner)
.runAsync((ExceptionThrowingFutureSupplier<?>) any());
@ParameterizedTest(name = "{0}")
@MethodSource("internalErrorsNotProvingBlockIsInvalid")
public void onGossipedBlock_onInternalErrorShouldNotMarkAsInvalid(final Throwable internalError) {
final SignedBeaconBlock nextBlock = setupBlockFailingImportWith(internalError);

assertThatBlockImport(nextBlock).isCompletedWithValueMatching(result -> !result.isSuccessful());
assertThat(invalidBlockRoots).isEmpty();
}

@Test
public void onGossipedBlock_onInternalErrorsShouldMarkAsInvalid() {
@ParameterizedTest(name = "{0}")
@MethodSource("internalErrorsProvingBlockIsInvalid")
public void onGossipedBlock_onInternalErrorShouldMarkAsInvalid(final Throwable internalError) {
final SignedBeaconBlock nextBlock = setupBlockFailingImportWith(internalError);

assertThatBlockImport(nextBlock).isCompletedWithValueMatching(result -> !result.isSuccessful());
assertThat(invalidBlockRoots).containsOnlyKeys(nextBlock.getRoot());
}

private SignedBeaconBlock setupBlockFailingImportWith(final Throwable internalError) {
final RecentChainData localRecentChainData = mock(RecentChainData.class);
blockManager = setupBlockManagerWithMockRecentChainData(localRecentChainData, false);

Expand All @@ -507,12 +537,11 @@ public void onGossipedBlock_onInternalErrorsShouldMarkAsInvalid() {
localChain.chainBuilder().generateBlockAtSlot(nextSlot).getBlock();
incrementSlot();

doAnswer(invocation -> SafeFuture.failedFuture(new RuntimeException("unknown")))
doAnswer(invocation -> SafeFuture.failedFuture(internalError))
.when(asyncRunner)
.runAsync((ExceptionThrowingFutureSupplier<?>) any());

assertThatBlockImport(nextBlock).isCompletedWithValueMatching(result -> !result.isSuccessful());
assertThat(invalidBlockRoots).containsOnlyKeys(nextBlock.getRoot());
return nextBlock;
}

@Test
Expand Down
Loading