mirror of https://github.com/hyperledger/besu
Handle timeouts when requesting checkpoint headers correctly. (#743)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
2d62a2c520
commit
da651844d8
@ -0,0 +1,28 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 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.manager; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.DeterministicEthScheduler.TimeoutPolicy; |
||||||
|
|
||||||
|
public class EthContextTestUtil { |
||||||
|
|
||||||
|
private static final String PROTOCOL_NAME = "ETH"; |
||||||
|
|
||||||
|
public static EthContext createTestEthContext(final TimeoutPolicy timeoutPolicy) { |
||||||
|
return new EthContext( |
||||||
|
PROTOCOL_NAME, |
||||||
|
new EthPeers(PROTOCOL_NAME), |
||||||
|
new EthMessages(), |
||||||
|
new DeterministicEthScheduler(timeoutPolicy)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,201 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 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; |
||||||
|
|
||||||
|
import static java.util.Arrays.asList; |
||||||
|
import static java.util.Collections.emptyList; |
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean; |
||||||
|
import static org.mockito.ArgumentMatchers.anyInt; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.reset; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
import static tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem.NO_OP_LABELLED_TIMER; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.ProtocolContext; |
||||||
|
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.Hash; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.ChainState; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthContext; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthContextTestUtil; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthMessage; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.RequestManager; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.messages.BlockHeadersMessage; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncState; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncTarget; |
||||||
|
import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; |
||||||
|
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; |
||||||
|
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; |
||||||
|
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; |
||||||
|
import tech.pegasys.pantheon.metrics.LabelledMetric; |
||||||
|
import tech.pegasys.pantheon.metrics.OperationTimer; |
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean; |
||||||
|
|
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
public class CheckpointHeaderManagerTest { |
||||||
|
|
||||||
|
private static final BlockHeader GENESIS = block(0); |
||||||
|
private static final int SEGMENT_SIZE = 5; |
||||||
|
private static final int HEADER_REQUEST_SIZE = 3; |
||||||
|
|
||||||
|
private static final ProtocolSchedule<Void> PROTOCOL_SCHEDULE = MainnetProtocolSchedule.create(); |
||||||
|
|
||||||
|
private final MutableBlockchain blockchain = mock(MutableBlockchain.class); |
||||||
|
private final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); |
||||||
|
private final ProtocolContext<Void> protocolContext = |
||||||
|
new ProtocolContext<>(blockchain, worldStateArchive, null); |
||||||
|
|
||||||
|
private final AtomicBoolean timeout = new AtomicBoolean(false); |
||||||
|
private final EthContext ethContext = EthContextTestUtil.createTestEthContext(timeout::get); |
||||||
|
private final SyncState syncState = new SyncState(blockchain, ethContext.getEthPeers()); |
||||||
|
private final LabelledMetric<OperationTimer> ethTasksTimer = NO_OP_LABELLED_TIMER; |
||||||
|
private final EthPeer syncTargetPeer = mock(EthPeer.class); |
||||||
|
private final RequestManager requestManager = new RequestManager(syncTargetPeer); |
||||||
|
private SyncTarget syncTarget; |
||||||
|
|
||||||
|
private final CheckpointHeaderManager<Void> checkpointHeaderManager = |
||||||
|
new CheckpointHeaderManager<>( |
||||||
|
SynchronizerConfiguration.builder() |
||||||
|
.downloaderChainSegmentSize(SEGMENT_SIZE) |
||||||
|
.downloaderHeadersRequestSize(HEADER_REQUEST_SIZE) |
||||||
|
.downloaderCheckpointTimeoutsPermitted(2) |
||||||
|
.build(), |
||||||
|
protocolContext, |
||||||
|
ethContext, |
||||||
|
syncState, |
||||||
|
PROTOCOL_SCHEDULE, |
||||||
|
ethTasksTimer); |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() { |
||||||
|
when(syncTargetPeer.chainState()).thenReturn(new ChainState()); |
||||||
|
syncTarget = syncState.setSyncTarget(syncTargetPeer, GENESIS); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldHandleErrorsWhenRequestingHeaders() throws Exception { |
||||||
|
when(anyHeadersRequested()).thenThrow(new PeerNotConnected("Nope")); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(emptyList()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldHandleTimeouts() throws Exception { |
||||||
|
timeout.set(true); |
||||||
|
when(anyHeadersRequested()).thenReturn(createResponseStream(), createResponseStream()); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(emptyList()); |
||||||
|
assertThat(checkpointHeaderManager.checkpointsHaveTimedOut()).isFalse(); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(emptyList()); |
||||||
|
assertThat(checkpointHeaderManager.checkpointsHaveTimedOut()).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldResetTimeoutWhenHeadersReceived() throws Exception { |
||||||
|
// Timeout
|
||||||
|
timeout.set(true); |
||||||
|
when(anyHeadersRequested()).thenReturn(createResponseStream()); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(emptyList()); |
||||||
|
assertThat(checkpointHeaderManager.checkpointsHaveTimedOut()).isFalse(); |
||||||
|
|
||||||
|
// Receive response
|
||||||
|
reset(syncTargetPeer); |
||||||
|
respondToHeaderRequests(GENESIS, block(5)); |
||||||
|
timeout.set(false); |
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(GENESIS, block(5))); |
||||||
|
assertThat(checkpointHeaderManager.checkpointsHaveTimedOut()).isFalse(); |
||||||
|
|
||||||
|
// Timeout again but shouldn't have reached threshold
|
||||||
|
reset(syncTargetPeer); |
||||||
|
timeout.set(true); |
||||||
|
when(anyHeadersRequested()).thenReturn(createResponseStream()); |
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(GENESIS, block(5))); |
||||||
|
assertThat(checkpointHeaderManager.checkpointsHaveTimedOut()).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldUseReturnedHeadersAsCheckpointHeaders() throws Exception { |
||||||
|
respondToHeaderRequests(GENESIS, block(5), block(10)); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(GENESIS, block(5), block(10))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldPullAdditionalCheckpointsWhenRequired() throws Exception { |
||||||
|
respondToHeaderRequests(GENESIS, block(5)); |
||||||
|
respondToHeaderRequests(block(5), block(10), block(15), block(20)); |
||||||
|
|
||||||
|
// Pull initial headers
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(GENESIS, block(5))); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(GENESIS, block(5), block(10), block(15), block(20))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void shouldRemoveImportedCheckpointHeaders() throws Exception { |
||||||
|
respondToHeaderRequests(GENESIS, block(5), block(10)); |
||||||
|
respondToHeaderRequests(block(10)); |
||||||
|
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(GENESIS, block(5), block(10))); |
||||||
|
|
||||||
|
when(blockchain.contains(GENESIS.getHash())).thenReturn(true); |
||||||
|
when(blockchain.contains(block(5).getHash())).thenReturn(true); |
||||||
|
when(blockchain.contains(block(10).getHash())).thenReturn(false); |
||||||
|
checkpointHeaderManager.clearImportedCheckpointHeaders(); |
||||||
|
|
||||||
|
// The first checkpoint header should always be in the blockchain (just as geneis was present)
|
||||||
|
assertThat(checkpointHeaderManager.pullCheckpointHeaders(syncTarget)) |
||||||
|
.isCompletedWithValue(asList(block(5), block(10))); |
||||||
|
} |
||||||
|
|
||||||
|
private void respondToHeaderRequests(final BlockHeader... headers) throws Exception { |
||||||
|
final ResponseStream responseStream = createResponseStream(); |
||||||
|
when(syncTargetPeer.getHeadersByHash( |
||||||
|
headers[0].getHash(), HEADER_REQUEST_SIZE + 1, SEGMENT_SIZE - 1, false)) |
||||||
|
.thenReturn(responseStream); |
||||||
|
requestManager.dispatchResponse( |
||||||
|
new EthMessage(syncTargetPeer, BlockHeadersMessage.create(asList(headers)))); |
||||||
|
} |
||||||
|
|
||||||
|
private static BlockHeader block(final int blockNumber) { |
||||||
|
return new BlockHeaderTestFixture().number(blockNumber).buildHeader(); |
||||||
|
} |
||||||
|
|
||||||
|
private ResponseStream createResponseStream() throws PeerNotConnected { |
||||||
|
return requestManager.dispatchRequest(() -> {}); |
||||||
|
} |
||||||
|
|
||||||
|
private ResponseStream anyHeadersRequested() throws PeerNotConnected { |
||||||
|
return syncTargetPeer.getHeadersByHash(any(Hash.class), anyInt(), anyInt(), anyBoolean()); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue