-
Notifications
You must be signed in to change notification settings - Fork 130
Ibft Integration test framework #502
Changes from 5 commits
ad6ed07
8358827
f19ff16
6131a74
90adcdf
58147aa
f68d96e
36c65db
895f0bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 getNodeKeys() { | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the return value is optional, what are your thoughts about using There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...and there's always the NPE. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
getNodeKeyPair
would be more accurate here. Sounds like the node has multiple key pairs otherwise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.