mirror of https://github.com/hyperledger/besu
Clique: Prevent out of turn blocks interrupt in-turn mining (#364)
parent
bbc76a25b6
commit
08cc58236e
@ -0,0 +1,44 @@ |
||||
/* |
||||
* 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.consensus.clique; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.ProtocolContext; |
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||
|
||||
public class CliqueMiningTracker { |
||||
|
||||
private final Address localAddress; |
||||
private final ProtocolContext<CliqueContext> protocolContext; |
||||
|
||||
public CliqueMiningTracker( |
||||
final Address localAddress, final ProtocolContext<CliqueContext> protocolContext) { |
||||
this.localAddress = localAddress; |
||||
this.protocolContext = protocolContext; |
||||
} |
||||
|
||||
public boolean isProposerAfter(final BlockHeader header) { |
||||
final Address nextProposer = |
||||
CliqueHelpers.getProposerForBlockAfter( |
||||
header, protocolContext.getConsensusState().getVoteTallyCache()); |
||||
return localAddress.equals(nextProposer); |
||||
} |
||||
|
||||
public boolean canMakeBlockNextRound(final BlockHeader header) { |
||||
return CliqueHelpers.addressIsAllowedToProduceNextBlock(localAddress, protocolContext, header); |
||||
} |
||||
|
||||
public boolean blockCreatedLocally(final BlockHeader header) { |
||||
return CliqueHelpers.getProposerOfBlock(header).equals(localAddress); |
||||
} |
||||
} |
@ -0,0 +1,240 @@ |
||||
/* |
||||
* 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.consensus.clique.blockcreation; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.never; |
||||
import static org.mockito.Mockito.reset; |
||||
import static org.mockito.Mockito.times; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.when; |
||||
import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryBlockchain; |
||||
|
||||
import tech.pegasys.pantheon.consensus.clique.CliqueContext; |
||||
import tech.pegasys.pantheon.consensus.clique.CliqueMiningTracker; |
||||
import tech.pegasys.pantheon.consensus.clique.TestHelpers; |
||||
import tech.pegasys.pantheon.consensus.clique.VoteTallyCache; |
||||
import tech.pegasys.pantheon.consensus.common.VoteTally; |
||||
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; |
||||
import tech.pegasys.pantheon.ethereum.ProtocolContext; |
||||
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; |
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.core.Block; |
||||
import tech.pegasys.pantheon.ethereum.core.BlockBody; |
||||
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.core.Util; |
||||
import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncState; |
||||
|
||||
import java.util.List; |
||||
|
||||
import org.assertj.core.util.Lists; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.ArgumentCaptor; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.class) |
||||
public class CliqueMiningCoordinatorTest { |
||||
|
||||
private final KeyPair proposerKeys = KeyPair.generate(); |
||||
private final KeyPair validatorKeys = KeyPair.generate(); |
||||
private final Address proposerAddress = Util.publicKeyToAddress(proposerKeys.getPublicKey()); |
||||
private final Address validatorAddress = Util.publicKeyToAddress(validatorKeys.getPublicKey()); |
||||
|
||||
private final List<Address> validators = Lists.newArrayList(validatorAddress, proposerAddress); |
||||
|
||||
private final BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture(); |
||||
|
||||
private CliqueMiningTracker miningTracker; |
||||
|
||||
@Mock private MutableBlockchain blockChain; |
||||
@Mock private ProtocolContext<CliqueContext> protocolContext; |
||||
@Mock private CliqueMinerExecutor minerExecutor; |
||||
@Mock private CliqueBlockMiner blockMiner; |
||||
@Mock private SyncState syncState; |
||||
@Mock private VoteTallyCache voteTallyCache; |
||||
|
||||
@Before |
||||
public void setup() { |
||||
|
||||
headerTestFixture.number(1); |
||||
Block genesisBlock = createEmptyBlock(0, Hash.ZERO, proposerKeys); // not normally signed but ok
|
||||
blockChain = createInMemoryBlockchain(genesisBlock); |
||||
|
||||
final VoteTally voteTally = mock(VoteTally.class); |
||||
when(voteTally.getValidators()).thenReturn(validators); |
||||
when(voteTallyCache.getVoteTallyAfterBlock(any())).thenReturn(voteTally); |
||||
final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, null, null); |
||||
|
||||
when(protocolContext.getConsensusState()).thenReturn(cliqueContext); |
||||
when(protocolContext.getBlockchain()).thenReturn(blockChain); |
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(blockMiner); |
||||
when(syncState.isInSync()).thenReturn(true); |
||||
|
||||
miningTracker = new CliqueMiningTracker(proposerAddress, protocolContext); |
||||
} |
||||
|
||||
@Test |
||||
public void outOfTurnBlockImportedDoesNotInterruptInTurnMiningOperation() { |
||||
// As the head of the blockChain is 0 (which effectively doesn't have a signer, all validators
|
||||
// are able to propose.
|
||||
|
||||
when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); |
||||
|
||||
// Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_1
|
||||
// should be created by proposer.
|
||||
final CliqueMiningCoordinator coordinator = |
||||
new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); |
||||
|
||||
coordinator.enable(); |
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any()); |
||||
|
||||
reset(minerExecutor); |
||||
|
||||
final Block importedBlock = createEmptyBlock(1, blockChain.getChainHeadHash(), validatorKeys); |
||||
|
||||
blockChain.appendBlock(importedBlock, Lists.emptyList()); |
||||
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
verify(minerExecutor, never()).startAsyncMining(any(), any()); |
||||
} |
||||
|
||||
@Test |
||||
public void outOfTurnBlockImportedAtHigherLevelInterruptsMiningOperation() { |
||||
// As the head of the blockChain is 1 (which effectively doesn't have a signer, all validators
|
||||
// are able to propose.
|
||||
when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); |
||||
|
||||
// Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_1
|
||||
// should be created by proposer.
|
||||
final CliqueMiningCoordinator coordinator = |
||||
new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); |
||||
|
||||
coordinator.enable(); |
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any()); |
||||
|
||||
reset(minerExecutor); |
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(blockMiner); |
||||
|
||||
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys); |
||||
|
||||
blockChain.appendBlock(importedBlock, Lists.emptyList()); |
||||
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class); |
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture()); |
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); |
||||
} |
||||
|
||||
@Test |
||||
public void outOfTurnBlockImportedInterruptsOutOfTurnMiningOperation() { |
||||
blockChain.appendBlock( |
||||
createEmptyBlock(1, blockChain.getChainHeadHash(), validatorKeys), Lists.emptyList()); |
||||
|
||||
when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); |
||||
|
||||
// Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_2
|
||||
// should be created by 'validator', thus Proposer is out-of-turn.
|
||||
final CliqueMiningCoordinator coordinator = |
||||
new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); |
||||
|
||||
coordinator.enable(); |
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any()); |
||||
|
||||
reset(minerExecutor); |
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(blockMiner); |
||||
|
||||
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys); |
||||
|
||||
blockChain.appendBlock(importedBlock, Lists.emptyList()); |
||||
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class); |
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture()); |
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); |
||||
} |
||||
|
||||
@Test |
||||
public void outOfTurnBlockImportedInterruptsNonRunningMiner() { |
||||
blockChain.appendBlock( |
||||
createEmptyBlock(1, blockChain.getChainHeadHash(), proposerKeys), Lists.emptyList()); |
||||
|
||||
when(blockMiner.getParentHeader()).thenReturn(blockChain.getChainHeadHeader()); |
||||
|
||||
// Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_2
|
||||
// should be created by 'validator', thus Proposer is out-of-turn.
|
||||
final CliqueMiningCoordinator coordinator = |
||||
new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); |
||||
|
||||
coordinator.enable(); |
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any()); |
||||
|
||||
reset(minerExecutor); |
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(blockMiner); |
||||
|
||||
final Block importedBlock = createEmptyBlock(2, blockChain.getChainHeadHash(), validatorKeys); |
||||
|
||||
blockChain.appendBlock(importedBlock, Lists.emptyList()); |
||||
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class); |
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture()); |
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); |
||||
} |
||||
|
||||
@Test |
||||
public void locallyGeneratedBlockInvalidatesMiningEvenIfInTurn() { |
||||
// Note also - validators is an hard-ordered LIST, thus in-turn will follow said list - block_1
|
||||
// should be created by Proposer, and thus will be in-turn.
|
||||
final CliqueMiningCoordinator coordinator = |
||||
new CliqueMiningCoordinator(blockChain, minerExecutor, syncState, miningTracker); |
||||
|
||||
coordinator.enable(); |
||||
|
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), any()); |
||||
|
||||
reset(minerExecutor); |
||||
when(minerExecutor.startAsyncMining(any(), any())).thenReturn(blockMiner); |
||||
|
||||
final Block importedBlock = createEmptyBlock(1, blockChain.getChainHeadHash(), proposerKeys); |
||||
blockChain.appendBlock(importedBlock, Lists.emptyList()); |
||||
|
||||
// The minerExecutor should not be invoked as the mining operation was conducted by an in-turn
|
||||
// validator, and the created block came from an out-turn validator.
|
||||
ArgumentCaptor<BlockHeader> varArgs = ArgumentCaptor.forClass(BlockHeader.class); |
||||
verify(minerExecutor, times(1)).startAsyncMining(any(), varArgs.capture()); |
||||
assertThat(varArgs.getValue()).isEqualTo(blockChain.getChainHeadHeader()); |
||||
} |
||||
|
||||
private Block createEmptyBlock( |
||||
final long blockNumber, final Hash parentHash, final KeyPair signer) { |
||||
headerTestFixture.number(blockNumber).parentHash(parentHash); |
||||
final BlockHeader header = |
||||
TestHelpers.createCliqueSignedBlockHeader(headerTestFixture, signer, validators); |
||||
return new Block(header, new BlockBody(Lists.emptyList(), Lists.emptyList())); |
||||
} |
||||
} |
Loading…
Reference in new issue