mirror of https://github.com/hyperledger/besu
[NC-1909] IBFT message gossiping (#501)
parent
637af126ca
commit
bd9ffac033
@ -0,0 +1,58 @@ |
||||
/* |
||||
* 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 java.util.Collections.emptyList; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.api.MessageData; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; |
||||
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.net.SocketAddress; |
||||
import java.util.Set; |
||||
|
||||
public class StubbedPeerConnection implements PeerConnection { |
||||
|
||||
@Override |
||||
public void send(final Capability capability, final MessageData message) |
||||
throws PeerNotConnected {} |
||||
|
||||
@Override |
||||
public Set<Capability> getAgreedCapabilities() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public PeerInfo getPeer() { |
||||
return new PeerInfo(0, "IbftIntTestPeer", emptyList(), 0, BytesValue.EMPTY); |
||||
} |
||||
|
||||
@Override |
||||
public void terminateConnection(final DisconnectReason reason, final boolean peerInitiated) {} |
||||
|
||||
@Override |
||||
public void disconnect(final DisconnectReason reason) {} |
||||
|
||||
@Override |
||||
public SocketAddress getLocalAddress() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public SocketAddress getRemoteAddress() { |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,108 @@ |
||||
/* |
||||
* 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.ibft; |
||||
|
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.CommitMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.IbftV2; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.NewRoundMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.PrepareMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.ProposalMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.RoundChangeMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData; |
||||
import tech.pegasys.pantheon.consensus.ibft.network.IbftMulticaster; |
||||
import tech.pegasys.pantheon.crypto.SECP256K1.Signature; |
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.Message; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.MessageData; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import com.google.common.collect.Lists; |
||||
|
||||
/** Class responsible for rebroadcasting IBFT messages to known validators */ |
||||
public class IbftGossip { |
||||
private final IbftMulticaster peers; |
||||
|
||||
// Size of the seenMessages cache, should end up utilising 65bytes * this number + some meta data
|
||||
private final int maxSeenMessages; |
||||
|
||||
// Set that starts evicting members when it hits capacity
|
||||
private final Set<Signature> seenMessages = |
||||
Collections.newSetFromMap( |
||||
new LinkedHashMap<Signature, Boolean>() { |
||||
@Override |
||||
protected boolean removeEldestEntry(final Map.Entry<Signature, Boolean> eldest) { |
||||
return size() > maxSeenMessages; |
||||
} |
||||
}); |
||||
|
||||
IbftGossip(final IbftMulticaster peers, final int maxSeenMessages) { |
||||
this.maxSeenMessages = maxSeenMessages; |
||||
this.peers = peers; |
||||
} |
||||
|
||||
/** |
||||
* Constructor that attaches gossip logic to a set of peers |
||||
* |
||||
* @param peers The always up to date set of connected peers that understand IBFT |
||||
*/ |
||||
public IbftGossip(final IbftMulticaster peers) { |
||||
this(peers, 10_000); |
||||
} |
||||
|
||||
/** |
||||
* Retransmit a given IBFT message to other known validators nodes |
||||
* |
||||
* @param message The raw message to be gossiped |
||||
* @return Whether the message was rebroadcast or has been ignored as a repeat |
||||
*/ |
||||
public boolean gossipMessage(final Message message) { |
||||
final MessageData messageData = message.getData(); |
||||
final SignedData<?> signedData; |
||||
switch (messageData.getCode()) { |
||||
case IbftV2.PROPOSAL: |
||||
signedData = ProposalMessageData.fromMessageData(messageData).decode(); |
||||
break; |
||||
case IbftV2.PREPARE: |
||||
signedData = PrepareMessageData.fromMessageData(messageData).decode(); |
||||
break; |
||||
case IbftV2.COMMIT: |
||||
signedData = CommitMessageData.fromMessageData(messageData).decode(); |
||||
break; |
||||
case IbftV2.ROUND_CHANGE: |
||||
signedData = RoundChangeMessageData.fromMessageData(messageData).decode(); |
||||
break; |
||||
case IbftV2.NEW_ROUND: |
||||
signedData = NewRoundMessageData.fromMessageData(messageData).decode(); |
||||
break; |
||||
default: |
||||
throw new IllegalArgumentException( |
||||
"Received message does not conform to any recognised IBFT message structure."); |
||||
} |
||||
final Signature signature = signedData.getSignature(); |
||||
if (seenMessages.contains(signature)) { |
||||
return false; |
||||
} else { |
||||
final List<Address> excludeAddressesList = |
||||
Lists.newArrayList( |
||||
message.getConnection().getPeer().getAddress(), signedData.getSender()); |
||||
peers.multicastToValidatorsExcept(messageData, excludeAddressesList); |
||||
seenMessages.add(signature); |
||||
return true; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,194 @@ |
||||
/* |
||||
* 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; |
||||
|
||||
import static com.google.common.collect.Lists.newArrayList; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.times; |
||||
import static org.mockito.Mockito.verify; |
||||
|
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.CommitMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.NewRoundMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.PrepareMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.ProposalMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.RoundChangeMessageData; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.Payload; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.ProposalPayload; |
||||
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData; |
||||
import tech.pegasys.pantheon.consensus.ibft.network.IbftNetworkPeers; |
||||
import tech.pegasys.pantheon.consensus.ibft.network.MockPeerFactory; |
||||
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; |
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.core.AddressHelpers; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.Message; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.MessageData; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.DefaultMessage; |
||||
|
||||
import java.util.function.Function; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.class) |
||||
public class IbftGossipTest { |
||||
private IbftGossip ibftGossip; |
||||
@Mock private IbftNetworkPeers ibftNetworkPeers; |
||||
private PeerConnection peerConnection; |
||||
private static final Address senderAddress = AddressHelpers.ofValue(9); |
||||
|
||||
@Before |
||||
public void setup() { |
||||
ibftGossip = new IbftGossip(ibftNetworkPeers, 10); |
||||
peerConnection = MockPeerFactory.create(senderAddress); |
||||
} |
||||
|
||||
private <P extends Payload> void assertRebroadcastToAllExceptSignerAndSender( |
||||
final Function<KeyPair, SignedData<P>> createPayload, |
||||
final Function<SignedData<P>, MessageData> createMessageData) { |
||||
final KeyPair keypair = KeyPair.generate(); |
||||
final SignedData<P> payload = createPayload.apply(keypair); |
||||
final MessageData messageData = createMessageData.apply(payload); |
||||
final Message message = new DefaultMessage(peerConnection, messageData); |
||||
|
||||
final boolean gossipResult = ibftGossip.gossipMessage(message); |
||||
assertThat(gossipResult).isTrue(); |
||||
verify(ibftNetworkPeers) |
||||
.multicastToValidatorsExcept(messageData, newArrayList(senderAddress, payload.getSender())); |
||||
} |
||||
|
||||
private <P extends Payload> void assertRebroadcastOnlyOnce( |
||||
final Function<KeyPair, SignedData<P>> createPayload, |
||||
final Function<SignedData<P>, MessageData> createMessageData) { |
||||
final KeyPair keypair = KeyPair.generate(); |
||||
final SignedData<P> payload = createPayload.apply(keypair); |
||||
final MessageData messageData = createMessageData.apply(payload); |
||||
final Message message = new DefaultMessage(peerConnection, messageData); |
||||
|
||||
final boolean gossip1Result = ibftGossip.gossipMessage(message); |
||||
final boolean gossip2Result = ibftGossip.gossipMessage(message); |
||||
assertThat(gossip1Result).isTrue(); |
||||
assertThat(gossip2Result).isFalse(); |
||||
verify(ibftNetworkPeers, times(1)) |
||||
.multicastToValidatorsExcept(messageData, newArrayList(senderAddress, payload.getSender())); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsProposalToAllExceptSignerAndSender() { |
||||
assertRebroadcastToAllExceptSignerAndSender( |
||||
TestHelpers::createSignedProposalPayload, ProposalMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsProposalOnlyOnce() { |
||||
assertRebroadcastOnlyOnce( |
||||
TestHelpers::createSignedProposalPayload, ProposalMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsPrepareToAllExceptSignerAndSender() { |
||||
assertRebroadcastToAllExceptSignerAndSender( |
||||
TestHelpers::createSignedPreparePayload, PrepareMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsPrepareOnlyOnce() { |
||||
assertRebroadcastOnlyOnce(TestHelpers::createSignedPreparePayload, PrepareMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsCommitToAllExceptSignerAndSender() { |
||||
assertRebroadcastToAllExceptSignerAndSender( |
||||
TestHelpers::createSignedCommitPayload, CommitMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsCommitOnlyOnce() { |
||||
assertRebroadcastOnlyOnce(TestHelpers::createSignedCommitPayload, CommitMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsRoundChangeToAllExceptSignerAndSender() { |
||||
assertRebroadcastToAllExceptSignerAndSender( |
||||
TestHelpers::createSignedRoundChangePayload, RoundChangeMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsRoundChangeOnlyOnce() { |
||||
assertRebroadcastOnlyOnce( |
||||
TestHelpers::createSignedRoundChangePayload, RoundChangeMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsNewRoundToAllExceptSignerAndSender() { |
||||
assertRebroadcastToAllExceptSignerAndSender( |
||||
TestHelpers::createSignedNewRoundPayload, NewRoundMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void assertRebroadcastsNewRoundOnlyOnce() { |
||||
assertRebroadcastOnlyOnce( |
||||
TestHelpers::createSignedNewRoundPayload, NewRoundMessageData::create); |
||||
} |
||||
|
||||
@Test |
||||
public void evictMessageRecordAtCapacity() { |
||||
final KeyPair keypair = KeyPair.generate(); |
||||
final SignedData<ProposalPayload> payload = |
||||
TestHelpers.createSignedProposalPayloadWithRound(keypair, 0); |
||||
final MessageData messageData = ProposalMessageData.create(payload); |
||||
final Message message = new DefaultMessage(peerConnection, messageData); |
||||
final boolean gossip1Result = ibftGossip.gossipMessage(message); |
||||
final boolean gossip2Result = ibftGossip.gossipMessage(message); |
||||
assertThat(gossip1Result).isTrue(); |
||||
assertThat(gossip2Result).isFalse(); |
||||
verify(ibftNetworkPeers, times(1)) |
||||
.multicastToValidatorsExcept(messageData, newArrayList(senderAddress, payload.getSender())); |
||||
|
||||
for (int i = 1; i <= 9; i++) { |
||||
final SignedData<ProposalPayload> nextPayload = |
||||
TestHelpers.createSignedProposalPayloadWithRound(keypair, i); |
||||
final MessageData nextMessageData = ProposalMessageData.create(nextPayload); |
||||
final Message nextMessage = new DefaultMessage(peerConnection, nextMessageData); |
||||
final boolean nextGossipResult = ibftGossip.gossipMessage(nextMessage); |
||||
assertThat(nextGossipResult).isTrue(); |
||||
} |
||||
|
||||
final boolean gossip3Result = ibftGossip.gossipMessage(message); |
||||
assertThat(gossip3Result).isFalse(); |
||||
verify(ibftNetworkPeers, times(1)) |
||||
.multicastToValidatorsExcept(messageData, newArrayList(senderAddress, payload.getSender())); |
||||
|
||||
{ |
||||
final SignedData<ProposalPayload> nextPayload = |
||||
TestHelpers.createSignedProposalPayloadWithRound(keypair, 10); |
||||
final MessageData nextMessageData = ProposalMessageData.create(nextPayload); |
||||
final Message nextMessage = new DefaultMessage(peerConnection, nextMessageData); |
||||
final boolean nextGossipResult = ibftGossip.gossipMessage(nextMessage); |
||||
assertThat(nextGossipResult).isTrue(); |
||||
} |
||||
|
||||
final boolean gossip4Result = ibftGossip.gossipMessage(message); |
||||
assertThat(gossip4Result).isTrue(); |
||||
verify(ibftNetworkPeers, times(2)) |
||||
.multicastToValidatorsExcept(messageData, newArrayList(senderAddress, payload.getSender())); |
||||
|
||||
final boolean gossip5Result = ibftGossip.gossipMessage(message); |
||||
assertThat(gossip5Result).isFalse(); |
||||
verify(ibftNetworkPeers, times(2)) |
||||
.multicastToValidatorsExcept(messageData, newArrayList(senderAddress, payload.getSender())); |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
/* |
||||
* 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.ibft.network; |
||||
|
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.core.AddressHelpers; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; |
||||
|
||||
public class MockPeerFactory { |
||||
public static PeerConnection create() { |
||||
return create(AddressHelpers.ofValue(9)); |
||||
} |
||||
|
||||
public static PeerConnection create(final Address address) { |
||||
final PeerConnection peerConnection = mock(PeerConnection.class); |
||||
final PeerInfo peerInfo = createPeerInfo(address); |
||||
when(peerConnection.getPeer()).thenReturn(peerInfo); |
||||
return peerConnection; |
||||
} |
||||
|
||||
public static PeerInfo createPeerInfo(final Address address) { |
||||
final PeerInfo peerInfo = mock(PeerInfo.class); |
||||
when(peerInfo.getAddress()).thenReturn(address); |
||||
return peerInfo; |
||||
} |
||||
} |
Loading…
Reference in new issue