mirror of https://github.com/hyperledger/besu
[PAN-2312] Validate DAO block (#939)
Adds a PeerValidator that, when the Dao fork milestone is in use, checks that the Dao block is present on each peer when they connect and disconnects them if they are on the wrong chain. Also: * Make GetHeadersFromPeer task stricter in validating response matches. * Update BlockHeadersMessage to return a list of headers * Add more controls to DeterministicEthScheduler test util Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
8f7ed8c3ae
commit
1b0a749ca6
@ -0,0 +1,132 @@ |
|||||||
|
/* |
||||||
|
* 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.peervalidation; |
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthContext; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.task.AbstractPeerTask; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.task.GetHeadersFromPeerByNumberTask; |
||||||
|
import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHeaderValidator; |
||||||
|
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; |
||||||
|
import tech.pegasys.pantheon.metrics.LabelledMetric; |
||||||
|
import tech.pegasys.pantheon.metrics.OperationTimer; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.CompletableFuture; |
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager; |
||||||
|
import org.apache.logging.log4j.Logger; |
||||||
|
|
||||||
|
public class DaoForkPeerValidator implements PeerValidator { |
||||||
|
private static final Logger LOG = LogManager.getLogger(); |
||||||
|
private static long DEFAULT_CHAIN_HEIGHT_ESTIMATION_BUFFER = 10L; |
||||||
|
|
||||||
|
private final EthContext ethContext; |
||||||
|
private final ProtocolSchedule<?> protocolSchedule; |
||||||
|
private final LabelledMetric<OperationTimer> ethTasksTimer; |
||||||
|
|
||||||
|
private final long daoBlockNumber; |
||||||
|
// Wait for peer's chainhead to advance some distance beyond daoBlockNumber before validating
|
||||||
|
private final long chainHeightEstimationBuffer; |
||||||
|
|
||||||
|
public DaoForkPeerValidator( |
||||||
|
final EthContext ethContext, |
||||||
|
final ProtocolSchedule<?> protocolSchedule, |
||||||
|
final LabelledMetric<OperationTimer> ethTasksTimer, |
||||||
|
final long daoBlockNumber, |
||||||
|
final long chainHeightEstimationBuffer) { |
||||||
|
checkArgument(chainHeightEstimationBuffer >= 0); |
||||||
|
this.ethContext = ethContext; |
||||||
|
this.protocolSchedule = protocolSchedule; |
||||||
|
this.ethTasksTimer = ethTasksTimer; |
||||||
|
this.daoBlockNumber = daoBlockNumber; |
||||||
|
this.chainHeightEstimationBuffer = chainHeightEstimationBuffer; |
||||||
|
} |
||||||
|
|
||||||
|
public DaoForkPeerValidator( |
||||||
|
final EthContext ethContext, |
||||||
|
final ProtocolSchedule<?> protocolSchedule, |
||||||
|
final LabelledMetric<OperationTimer> ethTasksTimer, |
||||||
|
final long daoBlockNumber) { |
||||||
|
this( |
||||||
|
ethContext, |
||||||
|
protocolSchedule, |
||||||
|
ethTasksTimer, |
||||||
|
daoBlockNumber, |
||||||
|
DEFAULT_CHAIN_HEIGHT_ESTIMATION_BUFFER); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public CompletableFuture<Boolean> validatePeer(final EthPeer ethPeer) { |
||||||
|
AbstractPeerTask<List<BlockHeader>> getHeaderTask = |
||||||
|
GetHeadersFromPeerByNumberTask.forSingleNumber( |
||||||
|
protocolSchedule, ethContext, daoBlockNumber, ethTasksTimer) |
||||||
|
.setTimeout(Duration.ofSeconds(20)) |
||||||
|
.assignPeer(ethPeer); |
||||||
|
return getHeaderTask |
||||||
|
.run() |
||||||
|
.handle( |
||||||
|
(res, err) -> { |
||||||
|
if (err != null) { |
||||||
|
// Mark peer as invalid on error
|
||||||
|
LOG.debug( |
||||||
|
"Peer {} is invalid because DAO block ({}) is unavailable: {}", |
||||||
|
ethPeer, |
||||||
|
daoBlockNumber, |
||||||
|
err.toString()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
List<BlockHeader> headers = res.getResult(); |
||||||
|
if (headers.size() == 0) { |
||||||
|
// If no headers are returned, fail
|
||||||
|
LOG.debug( |
||||||
|
"Peer {} is invalid because DAO block ({}) is unavailable.", |
||||||
|
ethPeer, |
||||||
|
daoBlockNumber); |
||||||
|
return false; |
||||||
|
} |
||||||
|
BlockHeader header = headers.get(0); |
||||||
|
boolean validDaoBlock = MainnetBlockHeaderValidator.validateHeaderForDaoFork(header); |
||||||
|
if (!validDaoBlock) { |
||||||
|
LOG.debug( |
||||||
|
"Peer {} is invalid because DAO block ({}) is invalid.", |
||||||
|
ethPeer, |
||||||
|
daoBlockNumber); |
||||||
|
} |
||||||
|
return validDaoBlock; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean canBeValidated(final EthPeer ethPeer) { |
||||||
|
return ethPeer.chainState().getEstimatedHeight() |
||||||
|
>= (daoBlockNumber + chainHeightEstimationBuffer); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Duration nextValidationCheckTimeout(final EthPeer ethPeer) { |
||||||
|
if (!ethPeer.chainState().hasEstimatedHeight()) { |
||||||
|
return Duration.ofSeconds(30); |
||||||
|
} |
||||||
|
long distanceToDaoBlock = daoBlockNumber - ethPeer.chainState().getEstimatedHeight(); |
||||||
|
if (distanceToDaoBlock < 100_000L) { |
||||||
|
return Duration.ofMinutes(1); |
||||||
|
} |
||||||
|
// If the peer is trailing behind, give it some time to catch up before trying again.
|
||||||
|
return Duration.ofMinutes(10); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
/* |
||||||
|
* 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.peervalidation; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.concurrent.CompletableFuture; |
||||||
|
|
||||||
|
public interface PeerValidator { |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether the peer can currently be validated. |
||||||
|
* |
||||||
|
* @param ethPeer The peer that need validation. |
||||||
|
* @return {@code} True if peer can be validated now. |
||||||
|
*/ |
||||||
|
boolean canBeValidated(final EthPeer ethPeer); |
||||||
|
|
||||||
|
/** |
||||||
|
* If the peer cannot currently be validated, returns a timeout indicating how long to wait. |
||||||
|
* before trying to validate the peer again. |
||||||
|
* |
||||||
|
* @param ethPeer The peer to be validated. |
||||||
|
* @return A duration representing how long to wait before trying to validate this peer again. |
||||||
|
*/ |
||||||
|
Duration nextValidationCheckTimeout(final EthPeer ethPeer); |
||||||
|
|
||||||
|
/** |
||||||
|
* Validates the given peer. |
||||||
|
* |
||||||
|
* @param ethPeer The peer to be validated. |
||||||
|
* @return True if the peer is valid, false otherwise. |
||||||
|
*/ |
||||||
|
CompletableFuture<Boolean> validatePeer(final EthPeer ethPeer); |
||||||
|
|
||||||
|
/** |
||||||
|
* @param ethPeer The peer to be disconnected. |
||||||
|
* @return The reason for disconnecting. |
||||||
|
*/ |
||||||
|
default DisconnectReason getDisconnectReason(final EthPeer ethPeer) { |
||||||
|
return DisconnectReason.SUBPROTOCOL_TRIGGERED; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
/* |
||||||
|
* 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.peervalidation; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthContext; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager; |
||||||
|
import org.apache.logging.log4j.Logger; |
||||||
|
|
||||||
|
public class PeerValidatorRunner { |
||||||
|
private static final Logger LOG = LogManager.getLogger(); |
||||||
|
protected final EthContext ethContext; |
||||||
|
private final PeerValidator peerValidator; |
||||||
|
|
||||||
|
PeerValidatorRunner(final EthContext ethContext, final PeerValidator peerValidator) { |
||||||
|
this.ethContext = ethContext; |
||||||
|
this.peerValidator = peerValidator; |
||||||
|
|
||||||
|
ethContext.getEthPeers().subscribeConnect(this::checkPeer); |
||||||
|
} |
||||||
|
|
||||||
|
public static void runValidator(final EthContext ethContext, final PeerValidator peerValidator) { |
||||||
|
new PeerValidatorRunner(ethContext, peerValidator); |
||||||
|
} |
||||||
|
|
||||||
|
public void checkPeer(final EthPeer ethPeer) { |
||||||
|
if (peerValidator.canBeValidated(ethPeer)) { |
||||||
|
peerValidator |
||||||
|
.validatePeer(ethPeer) |
||||||
|
.whenComplete( |
||||||
|
(validated, err) -> { |
||||||
|
if (err != null || !validated) { |
||||||
|
// Disconnect invalid peer
|
||||||
|
disconnectPeer(ethPeer); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else if (!ethPeer.isDisconnected()) { |
||||||
|
scheduleNextCheck(ethPeer); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void disconnectPeer(final EthPeer ethPeer) { |
||||||
|
LOG.debug( |
||||||
|
"Disconnecting from peer {} marked invalid by {}", |
||||||
|
ethPeer, |
||||||
|
peerValidator.getClass().getSimpleName()); |
||||||
|
ethPeer.disconnect(peerValidator.getDisconnectReason(ethPeer)); |
||||||
|
} |
||||||
|
|
||||||
|
protected void scheduleNextCheck(final EthPeer ethPeer) { |
||||||
|
Duration timeout = peerValidator.nextValidationCheckTimeout(ethPeer); |
||||||
|
ethContext.getScheduler().scheduleFutureTask(() -> checkPeer(ethPeer), timeout); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,236 @@ |
|||||||
|
/* |
||||||
|
* 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.peervalidation; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.core.Block; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockDataGenerator; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.BlockDataGenerator.BlockOptions; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.DeterministicEthScheduler.TimeoutPolicy; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManager; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.RespondingEthPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.messages.BlockHeadersMessage; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.messages.EthPV62; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.messages.GetBlockHeadersMessage; |
||||||
|
import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHeaderValidator; |
||||||
|
import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; |
||||||
|
import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; |
||||||
|
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.CompletableFuture; |
||||||
|
import java.util.concurrent.atomic.AtomicBoolean; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
public class DaoForkPeerValidatorTest { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void validatePeer_responsivePeerOnRightSideOfFork() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
BlockDataGenerator gen = new BlockDataGenerator(1); |
||||||
|
long daoBlockNumber = 500; |
||||||
|
Block daoBlock = |
||||||
|
gen.block( |
||||||
|
BlockOptions.create() |
||||||
|
.setBlockNumber(daoBlockNumber) |
||||||
|
.setExtraData(MainnetBlockHeaderValidator.DAO_EXTRA_DATA)); |
||||||
|
|
||||||
|
PeerValidator validator = |
||||||
|
new DaoForkPeerValidator( |
||||||
|
ethProtocolManager.ethContext(), |
||||||
|
MainnetProtocolSchedule.create(), |
||||||
|
NoOpMetricsSystem.NO_OP_LABELLED_TIMER, |
||||||
|
daoBlockNumber, |
||||||
|
0); |
||||||
|
|
||||||
|
RespondingEthPeer peer = |
||||||
|
EthProtocolManagerTestUtil.createPeer(ethProtocolManager, daoBlockNumber); |
||||||
|
|
||||||
|
CompletableFuture<Boolean> result = validator.validatePeer(peer.getEthPeer()); |
||||||
|
|
||||||
|
assertThat(result).isNotDone(); |
||||||
|
|
||||||
|
// Send response for dao block
|
||||||
|
AtomicBoolean daoBlockRequested = respondToDaoBlockRequest(peer, daoBlock); |
||||||
|
|
||||||
|
assertThat(daoBlockRequested).isTrue(); |
||||||
|
assertThat(result).isDone(); |
||||||
|
assertThat(result).isCompletedWithValue(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void validatePeer_responsivePeerOnWrongSideOfFork() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
BlockDataGenerator gen = new BlockDataGenerator(1); |
||||||
|
long daoBlockNumber = 500; |
||||||
|
Block daoBlock = |
||||||
|
gen.block( |
||||||
|
BlockOptions.create().setBlockNumber(daoBlockNumber).setExtraData(BytesValue.EMPTY)); |
||||||
|
|
||||||
|
PeerValidator validator = |
||||||
|
new DaoForkPeerValidator( |
||||||
|
ethProtocolManager.ethContext(), |
||||||
|
MainnetProtocolSchedule.create(), |
||||||
|
NoOpMetricsSystem.NO_OP_LABELLED_TIMER, |
||||||
|
daoBlockNumber, |
||||||
|
0); |
||||||
|
|
||||||
|
RespondingEthPeer peer = |
||||||
|
EthProtocolManagerTestUtil.createPeer(ethProtocolManager, daoBlockNumber); |
||||||
|
|
||||||
|
CompletableFuture<Boolean> result = validator.validatePeer(peer.getEthPeer()); |
||||||
|
|
||||||
|
assertThat(result).isNotDone(); |
||||||
|
|
||||||
|
// Send response for dao block
|
||||||
|
AtomicBoolean daoBlockRequested = respondToDaoBlockRequest(peer, daoBlock); |
||||||
|
|
||||||
|
assertThat(daoBlockRequested).isTrue(); |
||||||
|
assertThat(result).isDone(); |
||||||
|
assertThat(result).isCompletedWithValue(false); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void validatePeer_unresponsivePeer() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(TimeoutPolicy.ALWAYS); |
||||||
|
long daoBlockNumber = 500; |
||||||
|
|
||||||
|
PeerValidator validator = |
||||||
|
new DaoForkPeerValidator( |
||||||
|
ethProtocolManager.ethContext(), |
||||||
|
MainnetProtocolSchedule.create(), |
||||||
|
NoOpMetricsSystem.NO_OP_LABELLED_TIMER, |
||||||
|
daoBlockNumber, |
||||||
|
0); |
||||||
|
|
||||||
|
RespondingEthPeer peer = |
||||||
|
EthProtocolManagerTestUtil.createPeer(ethProtocolManager, daoBlockNumber); |
||||||
|
|
||||||
|
CompletableFuture<Boolean> result = validator.validatePeer(peer.getEthPeer()); |
||||||
|
|
||||||
|
// Request should timeout immediately
|
||||||
|
assertThat(result).isDone(); |
||||||
|
assertThat(result).isCompletedWithValue(false); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void validatePeer_requestBlockFromPeerBeingTested() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
BlockDataGenerator gen = new BlockDataGenerator(1); |
||||||
|
long daoBlockNumber = 500; |
||||||
|
Block daoBlock = |
||||||
|
gen.block( |
||||||
|
BlockOptions.create() |
||||||
|
.setBlockNumber(daoBlockNumber) |
||||||
|
.setExtraData(MainnetBlockHeaderValidator.DAO_EXTRA_DATA)); |
||||||
|
|
||||||
|
PeerValidator validator = |
||||||
|
new DaoForkPeerValidator( |
||||||
|
ethProtocolManager.ethContext(), |
||||||
|
MainnetProtocolSchedule.create(), |
||||||
|
NoOpMetricsSystem.NO_OP_LABELLED_TIMER, |
||||||
|
daoBlockNumber, |
||||||
|
0); |
||||||
|
|
||||||
|
int peerCount = 1000; |
||||||
|
List<RespondingEthPeer> otherPeers = |
||||||
|
Stream.generate( |
||||||
|
() -> EthProtocolManagerTestUtil.createPeer(ethProtocolManager, daoBlockNumber)) |
||||||
|
.limit(peerCount) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
RespondingEthPeer targetPeer = |
||||||
|
EthProtocolManagerTestUtil.createPeer(ethProtocolManager, daoBlockNumber); |
||||||
|
|
||||||
|
CompletableFuture<Boolean> result = validator.validatePeer(targetPeer.getEthPeer()); |
||||||
|
|
||||||
|
assertThat(result).isNotDone(); |
||||||
|
|
||||||
|
// Other peers should not receive request for dao block
|
||||||
|
for (RespondingEthPeer otherPeer : otherPeers) { |
||||||
|
AtomicBoolean daoBlockRequestedForOtherPeer = respondToDaoBlockRequest(otherPeer, daoBlock); |
||||||
|
assertThat(daoBlockRequestedForOtherPeer).isFalse(); |
||||||
|
} |
||||||
|
|
||||||
|
// Target peer should receive request for dao block
|
||||||
|
final AtomicBoolean daoBlockRequested = respondToDaoBlockRequest(targetPeer, daoBlock); |
||||||
|
assertThat(daoBlockRequested).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void canBeValidated() { |
||||||
|
BlockDataGenerator gen = new BlockDataGenerator(1); |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(TimeoutPolicy.ALWAYS); |
||||||
|
long daoBlockNumber = 500; |
||||||
|
long buffer = 10; |
||||||
|
|
||||||
|
PeerValidator validator = |
||||||
|
new DaoForkPeerValidator( |
||||||
|
ethProtocolManager.ethContext(), |
||||||
|
MainnetProtocolSchedule.create(), |
||||||
|
NoOpMetricsSystem.NO_OP_LABELLED_TIMER, |
||||||
|
daoBlockNumber, |
||||||
|
buffer); |
||||||
|
|
||||||
|
EthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0).getEthPeer(); |
||||||
|
|
||||||
|
peer.chainState().update(gen.hash(), daoBlockNumber - 10); |
||||||
|
assertThat(validator.canBeValidated(peer)).isFalse(); |
||||||
|
|
||||||
|
peer.chainState().update(gen.hash(), daoBlockNumber); |
||||||
|
assertThat(validator.canBeValidated(peer)).isFalse(); |
||||||
|
|
||||||
|
peer.chainState().update(gen.hash(), daoBlockNumber + buffer - 1); |
||||||
|
assertThat(validator.canBeValidated(peer)).isFalse(); |
||||||
|
|
||||||
|
peer.chainState().update(gen.hash(), daoBlockNumber + buffer); |
||||||
|
assertThat(validator.canBeValidated(peer)).isTrue(); |
||||||
|
|
||||||
|
peer.chainState().update(gen.hash(), daoBlockNumber + buffer + 10); |
||||||
|
assertThat(validator.canBeValidated(peer)).isTrue(); |
||||||
|
} |
||||||
|
|
||||||
|
private AtomicBoolean respondToDaoBlockRequest( |
||||||
|
final RespondingEthPeer peer, final Block daoBlock) { |
||||||
|
AtomicBoolean daoBlockRequested = new AtomicBoolean(false); |
||||||
|
|
||||||
|
Responder responder = |
||||||
|
RespondingEthPeer.targetedResponder( |
||||||
|
(cap, msg) -> { |
||||||
|
if (msg.getCode() != EthPV62.GET_BLOCK_HEADERS) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
GetBlockHeadersMessage headersRequest = GetBlockHeadersMessage.readFrom(msg); |
||||||
|
boolean isDaoBlockRequest = |
||||||
|
headersRequest.blockNumber().isPresent() |
||||||
|
&& headersRequest.blockNumber().getAsLong() |
||||||
|
== daoBlock.getHeader().getNumber(); |
||||||
|
if (isDaoBlockRequest) { |
||||||
|
daoBlockRequested.set(true); |
||||||
|
} |
||||||
|
return isDaoBlockRequest; |
||||||
|
}, |
||||||
|
(cap, msg) -> BlockHeadersMessage.create(daoBlock.getHeader())); |
||||||
|
|
||||||
|
// Respond
|
||||||
|
peer.respond(responder); |
||||||
|
|
||||||
|
return daoBlockRequested; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
/* |
||||||
|
* 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.peervalidation; |
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.never; |
||||||
|
import static org.mockito.Mockito.spy; |
||||||
|
import static org.mockito.Mockito.times; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManager; |
||||||
|
import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; |
||||||
|
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.concurrent.CompletableFuture; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
public class PeerValidatorRunnerTest { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void checkPeer_schedulesFutureCheckWhenPeerNotReady() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
EthProtocolManagerTestUtil.disableEthSchedulerAutoRun(ethProtocolManager); |
||||||
|
EthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager).getEthPeer(); |
||||||
|
|
||||||
|
PeerValidator validator = mock(PeerValidator.class); |
||||||
|
when(validator.canBeValidated(eq(peer))).thenReturn(false); |
||||||
|
when(validator.nextValidationCheckTimeout(eq(peer))).thenReturn(Duration.ofSeconds(30)); |
||||||
|
|
||||||
|
PeerValidatorRunner runner = |
||||||
|
spy(new PeerValidatorRunner(ethProtocolManager.ethContext(), validator)); |
||||||
|
runner.checkPeer(peer); |
||||||
|
|
||||||
|
verify(runner, times(1)).checkPeer(eq(peer)); |
||||||
|
verify(validator, never()).validatePeer(eq(peer)); |
||||||
|
verify(runner, never()).disconnectPeer(eq(peer)); |
||||||
|
verify(runner, times(1)).scheduleNextCheck(eq(peer)); |
||||||
|
|
||||||
|
// Run pending futures to trigger the next check
|
||||||
|
EthProtocolManagerTestUtil.runPendingFutures(ethProtocolManager); |
||||||
|
verify(runner, times(2)).checkPeer(eq(peer)); |
||||||
|
verify(validator, never()).validatePeer(eq(peer)); |
||||||
|
verify(runner, never()).disconnectPeer(eq(peer)); |
||||||
|
verify(runner, times(2)).scheduleNextCheck(eq(peer)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void checkPeer_doesNotScheduleFutureCheckWhenPeerNotReadyAndDisconnected() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
EthProtocolManagerTestUtil.disableEthSchedulerAutoRun(ethProtocolManager); |
||||||
|
EthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager).getEthPeer(); |
||||||
|
peer.disconnect(DisconnectReason.SUBPROTOCOL_TRIGGERED); |
||||||
|
|
||||||
|
PeerValidator validator = mock(PeerValidator.class); |
||||||
|
when(validator.canBeValidated(eq(peer))).thenReturn(false); |
||||||
|
when(validator.nextValidationCheckTimeout(eq(peer))).thenReturn(Duration.ofSeconds(30)); |
||||||
|
|
||||||
|
PeerValidatorRunner runner = |
||||||
|
spy(new PeerValidatorRunner(ethProtocolManager.ethContext(), validator)); |
||||||
|
runner.checkPeer(peer); |
||||||
|
|
||||||
|
verify(runner, times(1)).checkPeer(eq(peer)); |
||||||
|
verify(validator, never()).validatePeer(eq(peer)); |
||||||
|
verify(runner, never()).disconnectPeer(eq(peer)); |
||||||
|
verify(runner, times(0)).scheduleNextCheck(eq(peer)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void checkPeer_handlesInvalidPeer() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
EthProtocolManagerTestUtil.disableEthSchedulerAutoRun(ethProtocolManager); |
||||||
|
EthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager).getEthPeer(); |
||||||
|
|
||||||
|
PeerValidator validator = mock(PeerValidator.class); |
||||||
|
when(validator.canBeValidated(eq(peer))).thenReturn(true); |
||||||
|
when(validator.validatePeer(eq(peer))).thenReturn(CompletableFuture.completedFuture(false)); |
||||||
|
when(validator.nextValidationCheckTimeout(eq(peer))).thenReturn(Duration.ofSeconds(30)); |
||||||
|
|
||||||
|
PeerValidatorRunner runner = |
||||||
|
spy(new PeerValidatorRunner(ethProtocolManager.ethContext(), validator)); |
||||||
|
runner.checkPeer(peer); |
||||||
|
|
||||||
|
verify(validator, times(1)).validatePeer(eq(peer)); |
||||||
|
verify(runner, times(1)).disconnectPeer(eq(peer)); |
||||||
|
verify(runner, never()).scheduleNextCheck(eq(peer)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void checkPeer_handlesValidPeer() { |
||||||
|
EthProtocolManager ethProtocolManager = EthProtocolManagerTestUtil.create(); |
||||||
|
EthProtocolManagerTestUtil.disableEthSchedulerAutoRun(ethProtocolManager); |
||||||
|
EthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager).getEthPeer(); |
||||||
|
|
||||||
|
PeerValidator validator = mock(PeerValidator.class); |
||||||
|
when(validator.canBeValidated(eq(peer))).thenReturn(true); |
||||||
|
when(validator.validatePeer(eq(peer))).thenReturn(CompletableFuture.completedFuture(true)); |
||||||
|
when(validator.nextValidationCheckTimeout(eq(peer))).thenReturn(Duration.ofSeconds(30)); |
||||||
|
|
||||||
|
PeerValidatorRunner runner = |
||||||
|
spy(new PeerValidatorRunner(ethProtocolManager.ethContext(), validator)); |
||||||
|
runner.checkPeer(peer); |
||||||
|
|
||||||
|
verify(validator, times(1)).validatePeer(eq(peer)); |
||||||
|
verify(runner, never()).disconnectPeer(eq(peer)); |
||||||
|
verify(runner, never()).scheduleNextCheck(eq(peer)); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue