Skip to content

Commit

Permalink
Ibft Integration test framework (PegaSysEng#502)
Browse files Browse the repository at this point in the history
To test the IBFT implementation at an integration level, an amount of "setup" is required.

This commit creates a framework of "n" 'external' nodes which are able to inject messages to the IbftController, while also exposing the messages received from the local node.
Thus integration tests are able to be written in terms of input events (including messages), expected (network) responses and changes in the blockchain state.
  • Loading branch information
rain-on authored and Errorific committed Jan 8, 2019
1 parent a23c3e7 commit 6056d80
Show file tree
Hide file tree
Showing 16 changed files with 974 additions and 20 deletions.
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);
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

0 comments on commit 6056d80

Please sign in to comment.