diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/clique/CliqueMiningAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/clique/CliqueMiningAcceptanceTest.java index 41896191b3..b9969e7309 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/clique/CliqueMiningAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/clique/CliqueMiningAcceptanceTest.java @@ -72,4 +72,21 @@ public class CliqueMiningAcceptanceTest extends AcceptanceTestBase { minerNode1.verify(net.awaitPeerCount(0)); minerNode1.verify(clique.noNewBlockCreated(minerNode1)); } + + @Test + public void shouldStillMineWhenANodeFailsAndHasSufficientValidators() throws IOException { + final PantheonNode minerNode1 = pantheon.createCliqueNode("miner1"); + final PantheonNode minerNode2 = pantheon.createCliqueNode("miner2"); + final PantheonNode minerNode3 = pantheon.createCliqueNode("miner3"); + cluster.start(minerNode1, minerNode2, minerNode3); + + cluster.verifyOnActiveNodes(blockchain.reachesHeight(minerNode1, 1, 85)); + + cluster.stopNode(minerNode3); + cluster.verifyOnActiveNodes(net.awaitPeerCount(1)); + + cluster.verifyOnActiveNodes(blockchain.reachesHeight(minerNode1, 2)); + cluster.verifyOnActiveNodes(clique.blockIsCreatedByProposer(minerNode1)); + cluster.verifyOnActiveNodes(clique.blockIsCreatedByProposer(minerNode2)); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/BlockUtils.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/BlockUtils.java new file mode 100644 index 0000000000..b25c009d10 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/BlockUtils.java @@ -0,0 +1,53 @@ +/* + * 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.tests.acceptance.dsl; + +import static tech.pegasys.pantheon.ethereum.core.Hash.fromHexString; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderFunctions; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.LogsBloomFilter; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import org.web3j.protocol.core.methods.response.EthBlock.Block; + +public class BlockUtils { + + public static BlockHeader createBlockHeader( + final Block block, final BlockHeaderFunctions blockHeaderFunctions) { + final Hash mixHash = + block.getMixHash() == null + ? Hash.fromHexStringLenient("0x0") + : fromHexString(block.getMixHash()); + return new BlockHeader( + fromHexString(block.getParentHash()), + fromHexString(block.getSha3Uncles()), + Address.fromHexString(block.getMiner()), + fromHexString(block.getStateRoot()), + fromHexString(block.getTransactionsRoot()), + fromHexString(block.getReceiptsRoot()), + LogsBloomFilter.fromHexString(block.getLogsBloom()), + UInt256.fromHexString(block.getDifficultyRaw()), + block.getNumber().longValue(), + block.getGasLimit().longValue(), + block.getGasUsed().longValue(), + block.getTimestamp().longValue(), + BytesValue.fromHexString(block.getExtraData()), + mixHash, + block.getNonce().longValue(), + blockHeaderFunctions); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java new file mode 100644 index 0000000000..e424fc2084 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/clique/ExpectedBlockHasProposer.java @@ -0,0 +1,49 @@ +/* + * 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.tests.acceptance.dsl.condition.clique; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static tech.pegasys.pantheon.tests.acceptance.dsl.BlockUtils.createBlockHeader; +import static tech.pegasys.pantheon.tests.acceptance.dsl.WaitUtils.waitFor; + +import tech.pegasys.pantheon.consensus.clique.CliqueBlockHeaderFunctions; +import tech.pegasys.pantheon.consensus.clique.CliqueExtraData; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.eth.EthTransactions; + +import org.web3j.protocol.core.methods.response.EthBlock.Block; + +public class ExpectedBlockHasProposer implements Condition { + private final EthTransactions eth; + private Address proposer; + + public ExpectedBlockHasProposer(final EthTransactions eth, final Address proposer) { + this.eth = eth; + this.proposer = proposer; + } + + @Override + public void verify(final Node node) { + waitFor(() -> assertThat(proposerAddress(node)).isEqualTo(proposer)); + } + + private Address proposerAddress(final Node node) { + final Block block = node.execute(eth.block()); + final BlockHeader blockHeader = createBlockHeader(block, new CliqueBlockHeaderFunctions()); + final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(blockHeader); + return cliqueExtraData.getProposerAddress(); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Clique.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Clique.java index d5bf312ddb..380e7bd001 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Clique.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Clique.java @@ -28,6 +28,7 @@ import tech.pegasys.pantheon.tests.acceptance.dsl.condition.clique.ExpectProposa import tech.pegasys.pantheon.tests.acceptance.dsl.condition.clique.ExpectValidators; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.clique.ExpectValidatorsAtBlock; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.clique.ExpectValidatorsAtBlockHash; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.clique.ExpectedBlockHasProposer; import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.clique.CliqueTransactions; @@ -104,6 +105,10 @@ public class Clique { return Arrays.stream(validators).map(PantheonNode::getAddress).sorted().toArray(Address[]::new); } + public Condition blockIsCreatedByProposer(final PantheonNode proposer) { + return new ExpectedBlockHasProposer(eth, proposer.getAddress()); + } + public static class ProposalsConfig { private final Map proposals = new HashMap<>(); private final CliqueTransactions clique;