Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Ibft Integration test framework #502

Merged
merged 9 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion consensus/ibft/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ dependencies {
implementation 'io.vertx:vertx-core'
implementation 'com.google.guava:guava'

integrationTestImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
integrationTestImplementation project(path: ':config:', configuration: 'testSupportArtifacts')

testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
testImplementation project(path: ':config:', configuration:'testSupportArtifacts')
testImplementation project(path: ':config:', configuration: 'testSupportArtifacts')
integrationTestImplementation 'junit:junit'
integrationTestImplementation 'org.assertj:assertj-core'
integrationTestImplementation 'org.mockito:mockito-core'

testImplementation 'junit:junit'
testImplementation 'org.awaitility:awaitility'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.consensus.ibft.support;

import static org.assertj.core.api.Assertions.assertThat;

import tech.pegasys.pantheon.consensus.ibft.ibftmessage.CommitMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.IbftV2;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.NewRoundMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.PrepareMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.ProposalMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.RoundChangeMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.Payload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData;
import tech.pegasys.pantheon.ethereum.p2p.api.MessageData;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class MessageReceptionHelpers {

public static void assertPeersReceivedNoMessages(final Collection<ValidatorPeer> nodes) {
nodes.forEach(n -> assertThat(n.getReceivedMessages()).isEmpty());
}

@SafeVarargs
public static void assertPeersReceivedExactly(
final Collection<ValidatorPeer> allPeers, final SignedData<? extends Payload>... msgs) {
allPeers.forEach(n -> assertThat(n.getReceivedMessages().size()).isEqualTo(msgs.length));

List<SignedData<? extends Payload>> msgList = Arrays.asList(msgs);

for (int i = 0; i < msgList.size(); i++) {
final int index = i;
final SignedData<? extends Payload> msg = msgList.get(index);
allPeers.forEach(
n -> {
final List<MessageData> rxMsgs = n.getReceivedMessages();
final MessageData rxMsgData = rxMsgs.get(index);
assertThat(msgMatchesExpected(rxMsgData, msg)).isTrue();
});
}
allPeers.forEach(p -> p.clearReceivedMessages());
}

public static boolean msgMatchesExpected(
final MessageData actual, final SignedData<? extends Payload> expected) {
final Payload expectedPayload = expected.getPayload();

switch (expectedPayload.getMessageType()) {
case IbftV2.PROPOSAL:
return ProposalMessage.fromMessage(actual).decode().equals(expected);
case IbftV2.PREPARE:
return PrepareMessage.fromMessage(actual).decode().equals(expected);
case IbftV2.COMMIT:
return CommitMessage.fromMessage(actual).decode().equals(expected);
case IbftV2.NEW_ROUND:
return NewRoundMessage.fromMessage(actual).decode().equals(expected);
case IbftV2.ROUND_CHANGE:
return RoundChangeMessage.fromMessage(actual).decode().equals(expected);
default:
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.consensus.ibft.support;

import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Util;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;

import com.google.common.collect.Iterables;

public class NetworkLayout {

private final NodeParams localNode;
private final TreeMap<Address, NodeParams> addressKeyMap;
private final List<NodeParams> remotePeers;

public NetworkLayout(
final NodeParams localNode, final TreeMap<Address, NodeParams> addressKeyMap) {
this.localNode = localNode;
this.addressKeyMap = addressKeyMap;
this.remotePeers = new ArrayList<>(addressKeyMap.values());
this.remotePeers.remove(localNode);
}

public static NetworkLayout createNetworkLayout(
final int validatorCount, final int firstLocalNodeBlockNum) {
final TreeMap<Address, NodeParams> addressKeyMap = createValidators(validatorCount);

final NodeParams localNode = Iterables.get(addressKeyMap.values(), firstLocalNodeBlockNum);

return new NetworkLayout(localNode, addressKeyMap);
}

private static TreeMap<Address, NodeParams> createValidators(final int validatorCount) {
// Map is required to be sorted by address
final TreeMap<Address, NodeParams> addressKeyMap = new TreeMap<>();

for (int i = 0; i < validatorCount; i++) {
final KeyPair newKeyPair = KeyPair.generate();
final Address nodeAddress = Util.publicKeyToAddress(newKeyPair.getPublicKey());
addressKeyMap.put(nodeAddress, new NodeParams(nodeAddress, newKeyPair));
}

return addressKeyMap;
}

public Set<Address> getValidatorAddresses() {
return addressKeyMap.keySet();
}

public NodeParams getLocalNode() {
return localNode;
}

public List<NodeParams> getRemotePeers() {
return remotePeers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.consensus.ibft.support;

import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.core.Address;

public class NodeParams {
private final Address address;
private final KeyPair nodeKeys;

public NodeParams(final Address address, final KeyPair nodeKeys) {
this.address = address;
this.nodeKeys = nodeKeys;
}

public Address getAddress() {
return address;
}

public KeyPair getNodeKeyPair() {
return nodeKeys;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.consensus.ibft.support;

import java.util.Collection;
import java.util.List;

public class RoundSpecificNodeRoles {

private final ValidatorPeer proposer;
private final Collection<ValidatorPeer> peers;
private final List<ValidatorPeer> nonProposingPeers;

public RoundSpecificNodeRoles(
final ValidatorPeer proposer,
final Collection<ValidatorPeer> peers,
final List<ValidatorPeer> nonProposingPeers) {
this.proposer = proposer;
this.peers = peers;
this.nonProposingPeers = nonProposingPeers;
}

public ValidatorPeer getProposer() {
return proposer;
}

public Collection<ValidatorPeer> getAllPeers() {
return peers;
}

public List<ValidatorPeer> getNonProposingPeers() {
return nonProposingPeers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.consensus.ibft.support;

import tech.pegasys.pantheon.consensus.ibft.network.IbftMulticaster;
import tech.pegasys.pantheon.ethereum.p2p.api.MessageData;

import java.util.Collection;
import java.util.List;

import com.google.common.collect.Lists;

public class StubIbftMulticaster implements IbftMulticaster {

private final List<ValidatorPeer> validatorNodes = Lists.newArrayList();

public StubIbftMulticaster() {}

public void addNetworkPeers(final Collection<ValidatorPeer> nodes) {
validatorNodes.addAll(nodes);
}

@Override
public void multicastToValidators(final MessageData message) {
validatorNodes.forEach(v -> v.handleReceivedMessage(message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.consensus.ibft.support;

import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.statemachine.IbftController;
import tech.pegasys.pantheon.consensus.ibft.statemachine.IbftFinalState;
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Block;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/*
Responsible for creating an environment in which integration testing can be conducted.

The test setup is an 'n' node network, one of which is the local node (i.e. the Unit Under Test).

There is some complexity with determining the which node is the proposer etc. THus necessitating
NetworkLayout and RoundSpecificNodeRoles concepts.
*/
public class TestContext {

private Map<Address, ValidatorPeer> remotePeers;
private final MutableBlockchain blockchain;
private final IbftController controller;
private final IbftFinalState finalState;

public TestContext(
final Map<Address, ValidatorPeer> remotePeers,
final MutableBlockchain blockchain,
final IbftController controller,
final IbftFinalState finalState) {
this.remotePeers = remotePeers;
this.blockchain = blockchain;
this.controller = controller;
this.finalState = finalState;
}

public Collection<ValidatorPeer> getRemotePeers() {
return remotePeers.values();
}

public MutableBlockchain getBlockchain() {
return blockchain;
}

public IbftController getController() {
return controller;
}

public MessageFactory getLocalNodeMessageFactory() {
return finalState.getMessageFactory();
}

public Block createBlockForProposal(final int round, final long timestamp) {
return finalState
.getBlockCreatorFactory()
.create(blockchain.getChainHeadHeader(), round)
.createBlock(timestamp);
}

public RoundSpecificNodeRoles getRoundSpecificRoles(final ConsensusRoundIdentifier roundId) {
// This will return NULL if the LOCAL node is the proposer for the specified round
final Address proposerAddress = finalState.getProposerForRound(roundId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the return value is optional, what are your thoughts about using Optional<Address> for the return type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would really rather not - otherwise, everytime you go to use "getProposer()" you then have to perform a "get()".
ATM: The test script writer is expected to know how their network is laid out, and thus should appreciate that the "remote nodes" do not have a proposer amongst them....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and there's always the NPE.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I kinda figured the NPE failing the test would result in someone checking out what's gone wrong ... ultimately is a badly formed test so should fail.

final ValidatorPeer proposer = remotePeers.getOrDefault(proposerAddress, null);

final List<ValidatorPeer> nonProposers = new ArrayList<>(remotePeers.values());
nonProposers.remove(proposer);

return new RoundSpecificNodeRoles(proposer, remotePeers.values(), nonProposers);
}

public NodeParams getLocalNodeParams() {
return new NodeParams(finalState.getLocalAddress(), finalState.getNodeKeys());
}

public long getCurrentChainHeight() {
return blockchain.getChainHeadBlockNumber();
}
}
Loading