diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStep.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStep.java index b3bd6819f4..aebd574a13 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStep.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStep.java @@ -39,16 +39,24 @@ public CheckpointHeaderValidationStep( @Override public Stream apply(final CheckpointRangeHeaders checkpointRangeHeaders) { - final BlockHeader expectedParent = checkpointRangeHeaders.getCheckpointRange().getStart(); + final BlockHeader rangeStart = checkpointRangeHeaders.getCheckpointRange().getStart(); final BlockHeader firstHeaderToImport = checkpointRangeHeaders.getFirstHeaderToImport(); - if (isValid(expectedParent, firstHeaderToImport)) { + if (isValid(rangeStart, firstHeaderToImport)) { return checkpointRangeHeaders.getHeadersToImport().stream(); } else { + final BlockHeader rangeEnd = checkpointRangeHeaders.getCheckpointRange().getEnd(); + final String errorMessage = + String.format( + "Invalid checkpoint headers. Headers downloaded between #%d (%s) and #%d (%s) do not connect at #%d (%s)", + rangeStart.getNumber(), + rangeStart.getHash(), + rangeEnd.getNumber(), + rangeEnd.getHash(), + firstHeaderToImport.getNumber(), + firstHeaderToImport.getHash()); throw new InvalidBlockException( - "Provided first header does not connect to last header.", - expectedParent.getNumber(), - expectedParent.getHash()); + errorMessage, firstHeaderToImport.getNumber(), firstHeaderToImport.getHash()); } } diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloader.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloader.java index ff01ccc43b..022f442619 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloader.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloader.java @@ -21,7 +21,7 @@ import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncState; import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncTarget; import tech.pegasys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; -import tech.pegasys.pantheon.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; +import tech.pegasys.pantheon.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import tech.pegasys.pantheon.metrics.Counter; import tech.pegasys.pantheon.metrics.LabelledMetric; import tech.pegasys.pantheon.metrics.MetricsSystem; @@ -115,6 +115,13 @@ private CompletionStage repeatUnlessDownloadComplete( private CompletionStage handleFailedDownload(final Throwable error) { pipelineErrorCounter.inc(); + if (ExceptionUtils.rootCause(error) instanceof InvalidBlockException) { + LOG.warn( + "Invalid block detected. Disconnecting from sync target. {}", + ExceptionUtils.rootCause(error).getMessage()); + syncState.disconnectSyncTarget(DisconnectReason.BREACH_OF_PROTOCOL); + } + if (!cancelled.get() && syncTargetManager.shouldContinueDownloading() && !(ExceptionUtils.rootCause(error) instanceof CancellationException)) { @@ -122,15 +129,6 @@ private CompletionStage handleFailedDownload(final Throwable error) { // Allowing the normal looping logic to retry after a brief delay. return scheduler.scheduleFutureTask(() -> completedFuture(null), PAUSE_AFTER_ERROR_DURATION); } - if (ExceptionUtils.rootCause(error) instanceof InvalidBlockException) { - syncState - .syncTarget() - .ifPresent( - syncTarget -> - syncTarget - .peer() - .disconnect(DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL)); - } logDownloadFailure("Chain download failed.", error); // Propagate the error out, terminating this chain download. @@ -141,9 +139,10 @@ private void logDownloadFailure(final String message, final Throwable error) { final Throwable rootCause = ExceptionUtils.rootCause(error); if (rootCause instanceof CancellationException || rootCause instanceof InterruptedException) { LOG.trace(message, error); - } else if (rootCause instanceof EthTaskException - || rootCause instanceof InvalidBlockException) { + } else if (rootCause instanceof EthTaskException) { LOG.debug(message, error); + } else if (rootCause instanceof InvalidBlockException) { + LOG.warn(message, error); } else { LOG.error(message, error); } diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/state/SyncState.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/state/SyncState.java index 7f93d354b5..257a83803c 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/state/SyncState.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/state/SyncState.java @@ -19,6 +19,7 @@ import tech.pegasys.pantheon.ethereum.core.Synchronizer.SyncStatusListener; import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; import tech.pegasys.pantheon.ethereum.eth.manager.EthPeers; +import tech.pegasys.pantheon.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import tech.pegasys.pantheon.util.Subscribers; import java.util.Optional; @@ -106,6 +107,10 @@ private boolean isInSyncWithBestPeer(final long syncTolerance) { .orElse(true); } + public void disconnectSyncTarget(final DisconnectReason reason) { + syncTarget.ifPresent(syncTarget -> syncTarget.peer().disconnect(reason)); + } + public void clearSyncTarget() { replaceSyncTarget(Optional.empty()); } diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java index 85b43a9fe0..2f3c9dab53 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java @@ -204,7 +204,7 @@ private CompletableFuture> processHeaders( headersResult.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); future.completeExceptionally( new InvalidBlockException( - "Invalid header", header.getNumber(), header.getHash())); + "Header failed validation.", child.getNumber(), child.getHash())); return future; } headers[headerIndex] = header; diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java index b55e764cf0..85d670e079 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java @@ -17,6 +17,6 @@ public class InvalidBlockException extends RuntimeException { public InvalidBlockException(final String message, final long blockNumber, final Hash blockHash) { - super(message + ": " + blockNumber + ", " + blockHash); + super(message + ": Invalid block at #" + blockNumber + " (" + blockHash + ")"); } } diff --git a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/MockPeerConnection.java b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/MockPeerConnection.java index d07d47a01a..3db9dd820a 100644 --- a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/MockPeerConnection.java +++ b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/MockPeerConnection.java @@ -24,6 +24,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Optional; import java.util.Set; public class MockPeerConnection implements PeerConnection { @@ -35,6 +36,7 @@ public class MockPeerConnection implements PeerConnection { private final BytesValue nodeId; private final Peer peer; private final PeerInfo peerInfo; + private Optional disconnectReason = Optional.empty(); public MockPeerConnection(final Set caps, final PeerSendHandler onSend) { this.caps = caps; @@ -84,9 +86,18 @@ public void terminateConnection(final DisconnectReason reason, final boolean pee @Override public void disconnect(final DisconnectReason reason) { + if (disconnected) { + // Already disconnected + return; + } + disconnectReason = Optional.of(reason); disconnected = true; } + public Optional getDisconnectReason() { + return disconnectReason; + } + @Override public InetSocketAddress getLocalAddress() { throw new UnsupportedOperationException(); diff --git a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java index e98313e6fa..c86cc6b57c 100644 --- a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java +++ b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java @@ -16,6 +16,7 @@ import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryBlockchain; import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryWorldStateArchive; +import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.chain.Blockchain; import tech.pegasys.pantheon.ethereum.chain.GenesisState; @@ -32,6 +33,7 @@ import tech.pegasys.pantheon.ethereum.util.RawBlockIterator; import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; import tech.pegasys.pantheon.testutil.BlockTestUtil; +import tech.pegasys.pantheon.testutil.BlockTestUtil.ChainResources; import java.io.IOException; import java.net.URISyntaxException; @@ -91,15 +93,28 @@ public int blockCount() { } public static BlockchainSetupUtil forTesting() { - final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.create(); + return createEthashChain(BlockTestUtil.getTestChainResources()); + } + + public static BlockchainSetupUtil forOutdatedFork() { + return createEthashChain(BlockTestUtil.getOutdatedForkResources()); + } + + public static BlockchainSetupUtil forUpgradedFork() { + return createEthashChain(BlockTestUtil.getUpgradedForkResources()); + } + + private static BlockchainSetupUtil createEthashChain(final ChainResources chainResources) { final TemporaryFolder temp = new TemporaryFolder(); try { temp.create(); - final String genesisJson = - Resources.toString(BlockTestUtil.getTestGenesisUrl(), Charsets.UTF_8); + final String genesisJson = Resources.toString(chainResources.getGenesisURL(), Charsets.UTF_8); - final GenesisState genesisState = GenesisState.fromJson(genesisJson, protocolSchedule); + final GenesisConfigFile genesisConfigFile = GenesisConfigFile.fromConfig(genesisJson); + final ProtocolSchedule protocolSchedule = + MainnetProtocolSchedule.fromConfig(genesisConfigFile.getConfigOptions()); + final GenesisState genesisState = GenesisState.fromJson(genesisJson, protocolSchedule); final MutableBlockchain blockchain = createInMemoryBlockchain(genesisState.getBlock()); final WorldStateArchive worldArchive = createInMemoryWorldStateArchive(); @@ -107,7 +122,7 @@ public static BlockchainSetupUtil forTesting() { final ProtocolContext protocolContext = new ProtocolContext<>(blockchain, worldArchive, null); - final Path blocksPath = Path.of(BlockTestUtil.getTestBlockchainUrl().toURI()); + final Path blocksPath = Path.of(chainResources.getBlocksURL().toURI()); final List blocks = new ArrayList<>(); final BlockHeaderFunctions blockHeaderFunctions = ScheduleBasedBlockHeaderFunctions.create(protocolSchedule); diff --git a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStepTest.java b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStepTest.java index 53d69b226e..034dd65fae 100644 --- a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStepTest.java +++ b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/CheckpointHeaderValidationStepTest.java @@ -50,11 +50,12 @@ public class CheckpointHeaderValidationStepTest { private CheckpointHeaderValidationStep validationStep; private final BlockHeader checkpointStart = gen.header(10); + private final BlockHeader checkpointEnd = gen.header(13); private final BlockHeader firstHeader = gen.header(11); private final CheckpointRangeHeaders rangeHeaders = new CheckpointRangeHeaders( - new CheckpointRange(syncTarget, checkpointStart, gen.header(13)), - asList(firstHeader, gen.header(12), gen.header(13))); + new CheckpointRange(syncTarget, checkpointStart, checkpointEnd), + asList(firstHeader, gen.header(12), checkpointEnd)); @Before public void setUp() { @@ -88,6 +89,20 @@ public void shouldThrowExceptionWhenValidationFails() { firstHeader, checkpointStart, protocolContext, DETACHED_ONLY)) .thenReturn(false); assertThatThrownBy(() -> validationStep.apply(rangeHeaders)) - .isInstanceOf(InvalidBlockException.class); + .isInstanceOf(InvalidBlockException.class) + .hasMessageContaining( + "Invalid checkpoint headers. Headers downloaded between #" + + checkpointStart.getNumber() + + " (" + + checkpointStart.getHash() + + ") and #" + + checkpointEnd.getNumber() + + " (" + + checkpointEnd.getHash() + + ") do not connect at #" + + firstHeader.getNumber() + + " (" + + firstHeader.getHash() + + ")"); } } diff --git a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloaderTest.java b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloaderTest.java index 9219369b6a..8811552835 100644 --- a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloaderTest.java +++ b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/PipelineChainDownloaderTest.java @@ -32,7 +32,7 @@ import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncState; import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncTarget; import tech.pegasys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; -import tech.pegasys.pantheon.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; +import tech.pegasys.pantheon.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.services.pipeline.Pipeline; @@ -46,7 +46,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) @@ -255,20 +254,42 @@ public void shouldAbortPipelineIfCancelledAfterDownloadStarts() { } @Test - public void shouldDisconnectPeerIfInvalidBlockException() { + public void shouldDisconnectSyncTargetOnInvalidBlockException_finishedDownloading_cancelled() { + testInvalidBlockHandling(true, true); + } + + @Test + public void + shouldDisconnectSyncTargetOnInvalidBlockException_notFinishedDownloading_notCancelled() { + testInvalidBlockHandling(false, false); + } + + @Test + public void shouldDisconnectSyncTargetOnInvalidBlockException_finishedDownloading_notCancelled() { + testInvalidBlockHandling(true, false); + } + + @Test + public void shouldDisconnectSyncTargetOnInvalidBlockException_notFinishedDownloading_cancelled() { + testInvalidBlockHandling(false, true); + } + + public void testInvalidBlockHandling( + final boolean isFinishedDownloading, final boolean isCancelled) { final CompletableFuture selectTargetFuture = new CompletableFuture<>(); - when(syncTargetManager.shouldContinueDownloading()).thenReturn(false); + when(syncTargetManager.shouldContinueDownloading()).thenReturn(isFinishedDownloading); when(syncTargetManager.findSyncTarget(Optional.empty())) .thenReturn(selectTargetFuture) .thenReturn(new CompletableFuture<>()); - final EthPeer ethPeer = Mockito.mock(EthPeer.class); - final BlockHeader commonAncestor = Mockito.mock(BlockHeader.class); - final SyncTarget target = new SyncTarget(ethPeer, commonAncestor); - when(syncState.syncTarget()).thenReturn(Optional.of(target)); + chainDownloader.start(); verify(syncTargetManager).findSyncTarget(Optional.empty()); + if (isCancelled) { + chainDownloader.cancel(); + } selectTargetFuture.completeExceptionally(new InvalidBlockException("", 1, null)); - verify(ethPeer).disconnect(DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL); + + verify(syncState, times(1)).disconnectSyncTarget(DisconnectReason.BREACH_OF_PROTOCOL); } private CompletableFuture expectPipelineStarted(final SyncTarget syncTarget) { diff --git a/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java new file mode 100644 index 0000000000..9697ce3dd1 --- /dev/null +++ b/ethereum/eth/src/test/java/tech/pegasys/pantheon/ethereum/eth/sync/fullsync/FullSyncChainDownloaderForkTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2018 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.pantheon.ethereum.eth.sync.fullsync; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.chain.Blockchain; +import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; +import tech.pegasys.pantheon.ethereum.eth.manager.EthContext; +import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManager; +import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import tech.pegasys.pantheon.ethereum.eth.manager.EthScheduler; +import tech.pegasys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import tech.pegasys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import tech.pegasys.pantheon.ethereum.eth.manager.ethtaskutils.BlockchainSetupUtil; +import tech.pegasys.pantheon.ethereum.eth.sync.ChainDownloader; +import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncState; +import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; +import tech.pegasys.pantheon.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.metrics.MetricsSystem; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; +import tech.pegasys.pantheon.util.uint.UInt256; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FullSyncChainDownloaderForkTest { + + protected ProtocolSchedule protocolSchedule; + protected EthProtocolManager ethProtocolManager; + protected EthContext ethContext; + protected ProtocolContext protocolContext; + private SyncState syncState; + + private BlockchainSetupUtil localBlockchainSetup; + protected MutableBlockchain localBlockchain; + private BlockchainSetupUtil otherBlockchainSetup; + protected Blockchain otherBlockchain; + private final MetricsSystem metricsSystem = new NoOpMetricsSystem(); + + @Before + public void setupTest() { + localBlockchainSetup = BlockchainSetupUtil.forUpgradedFork(); + localBlockchain = localBlockchainSetup.getBlockchain(); + otherBlockchainSetup = BlockchainSetupUtil.forOutdatedFork(); + otherBlockchain = otherBlockchainSetup.getBlockchain(); + + protocolSchedule = localBlockchainSetup.getProtocolSchedule(); + protocolContext = localBlockchainSetup.getProtocolContext(); + ethProtocolManager = + EthProtocolManagerTestUtil.create( + localBlockchain, + localBlockchainSetup.getWorldArchive(), + new EthScheduler(1, 1, 1, new NoOpMetricsSystem())); + ethContext = ethProtocolManager.ethContext(); + syncState = new SyncState(protocolContext.getBlockchain(), ethContext.getEthPeers()); + } + + @After + public void tearDown() { + ethProtocolManager.stop(); + } + + private ChainDownloader downloader(final SynchronizerConfiguration syncConfig) { + return FullSyncChainDownloader.create( + syncConfig, protocolSchedule, protocolContext, ethContext, syncState, metricsSystem); + } + + private ChainDownloader downloader() { + final SynchronizerConfiguration syncConfig = syncConfigBuilder().build(); + return downloader(syncConfig); + } + + private SynchronizerConfiguration.Builder syncConfigBuilder() { + return SynchronizerConfiguration.builder(); + } + + @Test + public void disconnectsFromPeerOnBadFork() { + otherBlockchainSetup.importAllBlocks(); + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + final RespondingEthPeer peer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100), 100); + + final ChainDownloader downloader = downloader(); + downloader.start(); + + // Process until the sync target is selected + peer.respondWhileOtherThreadsWork(responder, () -> syncState.syncTarget().isEmpty()); + + // Check that we picked our peer + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peer.getEthPeer()); + + // Process until the sync target is cleared + peer.respondWhileOtherThreadsWork(responder, () -> syncState.syncTarget().isPresent()); + + // We should have disconnected from our peer on the invalid chain + assertThat(peer.getEthPeer().isDisconnected()).isTrue(); + assertThat(peer.getPeerConnection().getDisconnectReason()) + .contains(DisconnectReason.BREACH_OF_PROTOCOL); + assertThat(syncState.syncTarget()).isEmpty(); + } +} diff --git a/services/pipeline/src/main/java/tech/pegasys/pantheon/services/pipeline/AsyncOperationProcessor.java b/services/pipeline/src/main/java/tech/pegasys/pantheon/services/pipeline/AsyncOperationProcessor.java index 29bac46888..bf2c6eaac6 100644 --- a/services/pipeline/src/main/java/tech/pegasys/pantheon/services/pipeline/AsyncOperationProcessor.java +++ b/services/pipeline/src/main/java/tech/pegasys/pantheon/services/pipeline/AsyncOperationProcessor.java @@ -81,7 +81,7 @@ private void outputNextCompletedTask(final WritePipe outputPipe) { } catch (final InterruptedException e) { LOG.trace("Interrupted while waiting for processing to complete", e); } catch (final ExecutionException e) { - throw new RuntimeException("Async operation failed", e); + throw new RuntimeException("Async operation failed. " + e.getMessage(), e); } catch (final TimeoutException e) { // Ignore and go back around the loop. } diff --git a/testutil/src/main/java/tech/pegasys/pantheon/testutil/BlockTestUtil.java b/testutil/src/main/java/tech/pegasys/pantheon/testutil/BlockTestUtil.java index 028326b0b1..c24f4bd40b 100644 --- a/testutil/src/main/java/tech/pegasys/pantheon/testutil/BlockTestUtil.java +++ b/testutil/src/main/java/tech/pegasys/pantheon/testutil/BlockTestUtil.java @@ -29,25 +29,67 @@ public final class BlockTestUtil { - private static final Supplier blockchainURLSupplier = - Suppliers.memoize(BlockTestUtil::supplyTestBlockchainURL); - private static final Supplier genesisURLSupplier = - Suppliers.memoize(BlockTestUtil::supplyTestGenesisURL); + private static final Supplier testChainSupplier = + Suppliers.memoize(BlockTestUtil::supplyTestChainResources); + private static final Supplier forkOutdatedSupplier = + Suppliers.memoize(BlockTestUtil::supplyOutdatedForkResources); + private static final Supplier forkUpgradedSupplier = + Suppliers.memoize(BlockTestUtil::supplyUpgradedForkResources); - private static URL supplyTestBlockchainURL() { - return ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("testBlockchain.blocks")); + public static URL getTestBlockchainUrl() { + return getTestChainResources().getBlocksURL(); } - private static URL supplyTestGenesisURL() { - return ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("testGenesis.json")); + public static URL getTestGenesisUrl() { + return getTestChainResources().getGenesisURL(); } - public static URL getTestBlockchainUrl() { - return blockchainURLSupplier.get(); + public static ChainResources getTestChainResources() { + return testChainSupplier.get(); } - public static URL getTestGenesisUrl() { - return genesisURLSupplier.get(); + public static ChainResources getOutdatedForkResources() { + return forkOutdatedSupplier.get(); + } + + public static ChainResources getUpgradedForkResources() { + return forkUpgradedSupplier.get(); + } + + private static ChainResources supplyTestChainResources() { + final URL genesisURL = + ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("testGenesis.json")); + final URL blocksURL = + ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("testBlockchain.blocks")); + return new ChainResources(genesisURL, blocksURL); + } + + private static ChainResources supplyOutdatedForkResources() { + final URL genesisURL = + ensureFileUrl( + BlockTestUtil.class + .getClassLoader() + .getResource("fork-chain-data/genesis-outdated.json")); + final URL blocksURL = + ensureFileUrl( + BlockTestUtil.class + .getClassLoader() + .getResource("fork-chain-data/fork-outdated.blocks")); + return new ChainResources(genesisURL, blocksURL); + } + + private static ChainResources supplyUpgradedForkResources() { + final URL genesisURL = + ensureFileUrl( + BlockTestUtil.class + .getClassLoader() + .getResource("fork-chain-data/genesis-upgraded.json")); + final URL blocksURL = + ensureFileUrl( + BlockTestUtil.class + .getClassLoader() + .getResource("fork-chain-data/fork-upgraded.blocks")); + return new ChainResources(genesisURL, blocksURL); } /** Take a resource URL and if needed copy it to a temp file and return that URL. */ @@ -84,4 +126,22 @@ public static void write1000Blocks(final Path target) { throw new IllegalStateException(ex); } } + + public static class ChainResources { + private final URL genesisURL; + private final URL blocksURL; + + public ChainResources(final URL genesisURL, final URL blocksURL) { + this.genesisURL = genesisURL; + this.blocksURL = blocksURL; + } + + public URL getGenesisURL() { + return genesisURL; + } + + public URL getBlocksURL() { + return blocksURL; + } + } } diff --git a/testutil/src/main/resources/fork-chain-data/common.blocks b/testutil/src/main/resources/fork-chain-data/common.blocks new file mode 100644 index 0000000000..d1ba3e2549 Binary files /dev/null and b/testutil/src/main/resources/fork-chain-data/common.blocks differ diff --git a/testutil/src/main/resources/fork-chain-data/fork-outdated.blocks b/testutil/src/main/resources/fork-chain-data/fork-outdated.blocks new file mode 100644 index 0000000000..50b1b5d26d Binary files /dev/null and b/testutil/src/main/resources/fork-chain-data/fork-outdated.blocks differ diff --git a/testutil/src/main/resources/fork-chain-data/fork-upgraded.blocks b/testutil/src/main/resources/fork-chain-data/fork-upgraded.blocks new file mode 100644 index 0000000000..ebc3642ff1 Binary files /dev/null and b/testutil/src/main/resources/fork-chain-data/fork-upgraded.blocks differ diff --git a/testutil/src/main/resources/fork-chain-data/genesis-outdated.json b/testutil/src/main/resources/fork-chain-data/genesis-outdated.json new file mode 100644 index 0000000000..4648bb5f36 --- /dev/null +++ b/testutil/src/main/resources/fork-chain-data/genesis-outdated.json @@ -0,0 +1,47 @@ +{ + "config": { + "chainId": 2018, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 5, + "contractSizeLimit": 2147483647, + "ethash": { + "fixeddifficulty": 100 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "1000000000000000000000000000000000000000": { + "comment": "Add smart contract that will produce different output for constantinople vs constantinopleFix forks", + "code": "0x6020356000355560603560403555" + }, + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/testutil/src/main/resources/fork-chain-data/genesis-upgraded.json b/testutil/src/main/resources/fork-chain-data/genesis-upgraded.json new file mode 100644 index 0000000000..14dd549a70 --- /dev/null +++ b/testutil/src/main/resources/fork-chain-data/genesis-upgraded.json @@ -0,0 +1,48 @@ +{ + "config": { + "chainId": 2018, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 5, + "constantinopleFixBlock": 7, + "contractSizeLimit": 2147483647, + "ethash": { + "fixeddifficulty": 100 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "1000000000000000000000000000000000000000": { + "comment": "Add smart contract that will produce different output for constantinople vs constantinopleFix forks", + "code": "0x6020356000355560603560403555" + }, + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/testutil/src/main/resources/fork-chain-data/src/common-blocks.json b/testutil/src/main/resources/fork-chain-data/src/common-blocks.json new file mode 100644 index 0000000000..a0dcb40d46 --- /dev/null +++ b/testutil/src/main/resources/fork-chain-data/src/common-blocks.json @@ -0,0 +1,36 @@ +{ + "blocks": [ + { + "number": 1, + "transactions": [] + }, + { + "number": 2, + "transactions": [] + }, + { + "number": 3, + "transactions": [] + }, + { + "number": 4, + "transactions": [] + }, + { + "number": 5, + "transactions": [] + }, + { + "number": 6, + "transactions": [ + { + "fromPrivateKey": "0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF2", + "gasPrice": "0xEF", + "to": "0x1000000000000000000000000000000000000000", + "data": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" + } + ] + } + ] +} \ No newline at end of file diff --git a/testutil/src/main/resources/fork-chain-data/src/fork-blocks.json b/testutil/src/main/resources/fork-chain-data/src/fork-blocks.json new file mode 100644 index 0000000000..ba4d7ac037 --- /dev/null +++ b/testutil/src/main/resources/fork-chain-data/src/fork-blocks.json @@ -0,0 +1,28 @@ +{ + "blocks": [ + { + "number": 7, + "transactions": [ + { + "fromPrivateKey": "0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF2", + "gasPrice": "0xEF", + "to": "0x1000000000000000000000000000000000000000", + "data": "0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002" + } + ] + }, + { + "number": 8, + "transactions": [] + }, + { + "number": 9, + "transactions": [] + }, + { + "number": "0x0a", + "transactions": [] + } + ] +} \ No newline at end of file