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