mirror of https://github.com/hyperledger/besu
[PAN-2614] Add simple PeerPermissions interface (#1446)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
01780b8332
commit
2a64902bd2
@ -0,0 +1,52 @@ |
||||
/* |
||||
* 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.ethereum.p2p.network; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.api.DisconnectCallback; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; |
||||
|
||||
import java.util.Set; |
||||
|
||||
import com.google.common.collect.ImmutableSet; |
||||
|
||||
public class PeerReputationManager implements DisconnectCallback { |
||||
private static final Set<DisconnectReason> locallyTriggeredDisconnectReasons = |
||||
ImmutableSet.of( |
||||
DisconnectReason.BREACH_OF_PROTOCOL, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION); |
||||
|
||||
private static final Set<DisconnectReason> remotelyTriggeredDisconnectReasons = |
||||
ImmutableSet.of(DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION); |
||||
|
||||
private final PeerPermissionsBlacklist blacklist; |
||||
|
||||
public PeerReputationManager(final PeerPermissionsBlacklist blacklist) { |
||||
this.blacklist = blacklist; |
||||
} |
||||
|
||||
@Override |
||||
public void onDisconnect( |
||||
final PeerConnection connection, |
||||
final DisconnectReason reason, |
||||
final boolean initiatedByPeer) { |
||||
if (shouldBlock(reason, initiatedByPeer)) { |
||||
blacklist.add(connection.getPeer()); |
||||
} |
||||
} |
||||
|
||||
private boolean shouldBlock(final DisconnectReason reason, final boolean initiatedByPeer) { |
||||
return (!initiatedByPeer && locallyTriggeredDisconnectReasons.contains(reason)) |
||||
|| (initiatedByPeer && remotelyTriggeredDisconnectReasons.contains(reason)); |
||||
} |
||||
} |
@ -1,117 +0,0 @@ |
||||
/* |
||||
* 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.ethereum.p2p.peers; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.api.DisconnectCallback; |
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; |
||||
import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; |
||||
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import com.google.common.collect.ImmutableSet; |
||||
|
||||
/** |
||||
* A list of nodes that the running client will not communicate with. This can be because of network |
||||
* issues, protocol issues, or by being explicitly set on the command line. |
||||
* |
||||
* <p>Peers are stored and identified strictly by their nodeId, the convenience methods taking |
||||
* {@link Peer}s and {@link PeerConnection}s redirect to the methods that take {@link BytesValue} |
||||
* object that represent the node ID of the banned nodes. |
||||
* |
||||
* <p>The storage list is not infinite. A default cap of 500 is applied and nodes are removed on a |
||||
* first added first removed basis. Adding a new copy of the same node will not affect the priority |
||||
* for removal. The exception to this is a list of banned nodes passed in by reference to the |
||||
* constructor. This list neither adds nor removes from that list passed in. |
||||
*/ |
||||
public class PeerBlacklist implements DisconnectCallback { |
||||
private static final int DEFAULT_BLACKLIST_CAP = 500; |
||||
|
||||
private static final Set<DisconnectReason> locallyTriggeredBlacklistReasons = |
||||
ImmutableSet.of( |
||||
DisconnectReason.BREACH_OF_PROTOCOL, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION); |
||||
|
||||
private static final Set<DisconnectReason> remotelyTriggeredBlacklistReasons = |
||||
ImmutableSet.of(DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION); |
||||
|
||||
private final int blacklistCap; |
||||
private final Set<BytesValue> blacklistedNodeIds = |
||||
Collections.synchronizedSet( |
||||
Collections.newSetFromMap( |
||||
new LinkedHashMap<BytesValue, Boolean>(20, 0.75f, true) { |
||||
@Override |
||||
protected boolean removeEldestEntry(final Map.Entry<BytesValue, Boolean> eldest) { |
||||
return size() > blacklistCap; |
||||
} |
||||
})); |
||||
|
||||
/** These nodes are always banned for the life of this list. They are not subject to rollover. */ |
||||
private final Set<BytesValue> bannedNodeIds; |
||||
|
||||
public PeerBlacklist(final int blacklistCap, final Set<BytesValue> bannedNodeIds) { |
||||
this.blacklistCap = blacklistCap; |
||||
this.bannedNodeIds = bannedNodeIds; |
||||
} |
||||
|
||||
public PeerBlacklist(final int blacklistCap) { |
||||
this(blacklistCap, Collections.emptySet()); |
||||
} |
||||
|
||||
public PeerBlacklist(final Set<BytesValue> bannedNodeIds) { |
||||
this(DEFAULT_BLACKLIST_CAP, bannedNodeIds); |
||||
} |
||||
|
||||
public PeerBlacklist() { |
||||
this(DEFAULT_BLACKLIST_CAP, Collections.emptySet()); |
||||
} |
||||
|
||||
public boolean contains(final BytesValue nodeId) { |
||||
return blacklistedNodeIds.contains(nodeId) || bannedNodeIds.contains(nodeId); |
||||
} |
||||
|
||||
public boolean contains(final PeerConnection peer) { |
||||
return contains(peer.getPeerInfo().getNodeId()); |
||||
} |
||||
|
||||
public boolean contains(final Peer peer) { |
||||
return contains(peer.getId()); |
||||
} |
||||
|
||||
public void add(final Peer peer) { |
||||
add(peer.getId()); |
||||
} |
||||
|
||||
public void add(final BytesValue peerId) { |
||||
blacklistedNodeIds.add(peerId); |
||||
} |
||||
|
||||
@Override |
||||
public void onDisconnect( |
||||
final PeerConnection connection, |
||||
final DisconnectReason reason, |
||||
final boolean initiatedByPeer) { |
||||
if (shouldBlacklistForDisconnect(reason, initiatedByPeer)) { |
||||
add(connection.getPeerInfo().getNodeId()); |
||||
} |
||||
} |
||||
|
||||
private boolean shouldBlacklistForDisconnect( |
||||
final DisconnectMessage.DisconnectReason reason, final boolean initiatedByPeer) { |
||||
return (!initiatedByPeer && locallyTriggeredBlacklistReasons.contains(reason)) |
||||
|| (initiatedByPeer && remotelyTriggeredBlacklistReasons.contains(reason)); |
||||
} |
||||
} |
@ -0,0 +1,111 @@ |
||||
/* |
||||
* 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.ethereum.p2p.permissions; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; |
||||
import tech.pegasys.pantheon.util.Subscribers; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
import java.util.stream.Stream; |
||||
|
||||
import com.google.common.collect.ImmutableList; |
||||
|
||||
public abstract class PeerPermissions { |
||||
private final Subscribers<PermissionsUpdateCallback> updateSubscribers = new Subscribers<>(); |
||||
|
||||
public static final PeerPermissions NOOP = new NoopPeerPermissions(); |
||||
|
||||
public static PeerPermissions noop() { |
||||
return NOOP; |
||||
} |
||||
|
||||
public static PeerPermissions combine(final PeerPermissions... permissions) { |
||||
return combine(Arrays.asList(permissions)); |
||||
} |
||||
|
||||
public static PeerPermissions combine(final List<PeerPermissions> permissions) { |
||||
return CombinedPeerPermissions.create(permissions); |
||||
} |
||||
|
||||
/** |
||||
* @param peer The {@link Peer} object representing the remote node |
||||
* @return True if we are allowed to communicate with this peer. |
||||
*/ |
||||
public abstract boolean isPermitted(final Peer peer); |
||||
|
||||
public void subscribeUpdate(final PermissionsUpdateCallback callback) { |
||||
updateSubscribers.subscribe(callback); |
||||
} |
||||
|
||||
protected void dispatchUpdate( |
||||
final boolean permissionsRestricted, final Optional<List<Peer>> affectedPeers) { |
||||
updateSubscribers.forEach(s -> s.onUpdate(permissionsRestricted, affectedPeers)); |
||||
} |
||||
|
||||
private static class NoopPeerPermissions extends PeerPermissions { |
||||
@Override |
||||
public boolean isPermitted(final Peer peer) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
private static class CombinedPeerPermissions extends PeerPermissions { |
||||
private final ImmutableList<PeerPermissions> permissions; |
||||
|
||||
private CombinedPeerPermissions(final ImmutableList<PeerPermissions> permissions) { |
||||
this.permissions = permissions; |
||||
} |
||||
|
||||
public static PeerPermissions create(final List<PeerPermissions> permissions) { |
||||
final ImmutableList<PeerPermissions> filteredPermissions = |
||||
permissions.stream() |
||||
.flatMap( |
||||
p -> { |
||||
if (p instanceof CombinedPeerPermissions) { |
||||
return ((CombinedPeerPermissions) p).permissions.stream(); |
||||
} else { |
||||
return Stream.of(p); |
||||
} |
||||
}) |
||||
.filter(p -> !(p instanceof NoopPeerPermissions)) |
||||
.collect(ImmutableList.toImmutableList()); |
||||
|
||||
if (filteredPermissions.size() == 0) { |
||||
return PeerPermissions.NOOP; |
||||
} else if (filteredPermissions.size() == 1) { |
||||
return filteredPermissions.get(0); |
||||
} else { |
||||
return new CombinedPeerPermissions(filteredPermissions); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void subscribeUpdate(final PermissionsUpdateCallback callback) { |
||||
for (final PeerPermissions permission : permissions) { |
||||
permission.subscribeUpdate(callback); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean isPermitted(final Peer peer) { |
||||
for (PeerPermissions permission : permissions) { |
||||
if (!permission.isPermitted(peer)) { |
||||
return false; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
/* |
||||
* 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.ethereum.p2p.permissions; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; |
||||
import tech.pegasys.pantheon.util.LimitedSet; |
||||
import tech.pegasys.pantheon.util.LimitedSet.Mode; |
||||
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Optional; |
||||
import java.util.OptionalInt; |
||||
import java.util.Set; |
||||
|
||||
import io.vertx.core.impl.ConcurrentHashSet; |
||||
|
||||
public class PeerPermissionsBlacklist extends PeerPermissions { |
||||
private static int DEFAULT_INITIAL_CAPACITY = 20; |
||||
|
||||
private final Set<BytesValue> blacklist; |
||||
|
||||
private PeerPermissionsBlacklist(final int initialCapacity, final OptionalInt maxSize) { |
||||
if (maxSize.isPresent()) { |
||||
blacklist = |
||||
LimitedSet.create(initialCapacity, maxSize.getAsInt(), Mode.DROP_LEAST_RECENTLY_ACCESSED); |
||||
} else { |
||||
blacklist = new ConcurrentHashSet<>(initialCapacity); |
||||
} |
||||
} |
||||
|
||||
private PeerPermissionsBlacklist(final OptionalInt maxSize) { |
||||
this(DEFAULT_INITIAL_CAPACITY, maxSize); |
||||
} |
||||
|
||||
public static PeerPermissionsBlacklist create() { |
||||
return new PeerPermissionsBlacklist(OptionalInt.empty()); |
||||
} |
||||
|
||||
public static PeerPermissionsBlacklist create(final int maxSize) { |
||||
return new PeerPermissionsBlacklist(OptionalInt.of(maxSize)); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isPermitted(final Peer peer) { |
||||
return !blacklist.contains(peer.getId()); |
||||
} |
||||
|
||||
public void add(final Peer peer) { |
||||
if (blacklist.add(peer.getId())) { |
||||
dispatchUpdate(true, Optional.of(Collections.singletonList(peer))); |
||||
} |
||||
} |
||||
|
||||
public void remove(final Peer peer) { |
||||
if (blacklist.remove(peer.getId())) { |
||||
dispatchUpdate(false, Optional.of(Collections.singletonList(peer))); |
||||
} |
||||
} |
||||
|
||||
public void add(final BytesValue peerId) { |
||||
if (blacklist.add(peerId)) { |
||||
dispatchUpdate(true, Optional.empty()); |
||||
} |
||||
} |
||||
|
||||
public void remove(final BytesValue peerId) { |
||||
if (blacklist.remove(peerId)) { |
||||
dispatchUpdate(false, Optional.empty()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
/* |
||||
* 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.ethereum.p2p.permissions; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
public interface PermissionsUpdateCallback { |
||||
|
||||
/** |
||||
* @param permissionsRestricted True if permissions were narrowed in any way, meaning that |
||||
* previously permitted peers may no longer be permitted. False indicates that permissions |
||||
* were made less restrictive, meaning peers that were previously restricted may now be |
||||
* permitted. |
||||
* @param affectedPeers If non-empty, contains the entire set of peers affected by this |
||||
* permissions update. If permissions were restricted, this is the list of peers that are no |
||||
* longer permitted. If permissions were broadened, this is the list of peers that are now |
||||
* permitted. |
||||
*/ |
||||
void onUpdate(final boolean permissionsRestricted, final Optional<List<Peer>> affectedPeers); |
||||
} |
@ -0,0 +1,107 @@ |
||||
/* |
||||
* 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.ethereum.p2p.network; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; |
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; |
||||
import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist; |
||||
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 tech.pegasys.pantheon.util.enode.EnodeURL; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class PeerReputationManagerTest { |
||||
private final PeerReputationManager peerReputationManager; |
||||
private final PeerPermissionsBlacklist blacklist; |
||||
|
||||
public PeerReputationManagerTest() { |
||||
blacklist = PeerPermissionsBlacklist.create(); |
||||
peerReputationManager = new PeerReputationManager(blacklist); |
||||
} |
||||
|
||||
@Test |
||||
public void doesNotBlacklistPeerForNormalDisconnect() { |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
|
||||
peerReputationManager.onDisconnect(peer, DisconnectReason.TOO_MANY_PEERS, false); |
||||
|
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void blacklistPeerForBadBehavior() { |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
peerReputationManager.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void doesNotBlacklistPeerForOurBadBehavior() { |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
peerReputationManager.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, true); |
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void blacklistIncompatiblePeer() { |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
peerReputationManager.onDisconnect( |
||||
peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, false); |
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void blacklistIncompatiblePeerWhoIssuesDisconnect() { |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isTrue(); |
||||
peerReputationManager.onDisconnect( |
||||
peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, true); |
||||
assertThat(blacklist.isPermitted(peer.getPeer())).isFalse(); |
||||
} |
||||
|
||||
private PeerConnection generatePeerConnection() { |
||||
final BytesValue nodeId = Peer.randomId(); |
||||
final PeerConnection conn = mock(PeerConnection.class); |
||||
final PeerInfo peerInfo = mock(PeerInfo.class); |
||||
final Peer peer = |
||||
DefaultPeer.fromEnodeURL( |
||||
EnodeURL.builder() |
||||
.nodeId(Peer.randomId()) |
||||
.ipAddress("10.9.8.7") |
||||
.discoveryPort(65535) |
||||
.listeningPort(65534) |
||||
.build()); |
||||
|
||||
when(peerInfo.getNodeId()).thenReturn(nodeId); |
||||
when(conn.getPeerInfo()).thenReturn(peerInfo); |
||||
when(conn.getPeer()).thenReturn(peer); |
||||
|
||||
return conn; |
||||
} |
||||
} |
@ -1,219 +0,0 @@ |
||||
/* |
||||
* 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.ethereum.p2p.peers; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; |
||||
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 tech.pegasys.pantheon.util.enode.EnodeURL; |
||||
|
||||
import java.util.Collections; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class PeerBlacklistTest { |
||||
private int nodeIdValue = 1; |
||||
|
||||
@Test |
||||
public void directlyAddingPeerWorks() { |
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final Peer peer = generatePeer(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.add(peer); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void directlyAddingPeerByPeerIdWorks() { |
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final Peer peer = generatePeer(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.add(peer.getId()); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void banningPeerByPeerIdWorks() { |
||||
final Peer peer = generatePeer(); |
||||
final PeerBlacklist blacklist = new PeerBlacklist(Collections.singleton(peer.getId())); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
|
||||
blacklist.add(peer.getId()); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void bannedNodesDoNotRollover() { |
||||
final Peer bannedPeer = generatePeer(); |
||||
final Peer peer1 = generatePeer(); |
||||
final Peer peer2 = generatePeer(); |
||||
final Peer peer3 = generatePeer(); |
||||
final PeerBlacklist blacklist = new PeerBlacklist(2, Collections.singleton(bannedPeer.getId())); |
||||
|
||||
assertThat(blacklist.contains(bannedPeer)).isTrue(); |
||||
assertThat(blacklist.contains(peer1)).isFalse(); |
||||
assertThat(blacklist.contains(peer2)).isFalse(); |
||||
assertThat(blacklist.contains(peer3)).isFalse(); |
||||
|
||||
// fill to the limit
|
||||
blacklist.add(peer1.getId()); |
||||
blacklist.add(peer2.getId()); |
||||
assertThat(blacklist.contains(bannedPeer)).isTrue(); |
||||
assertThat(blacklist.contains(peer1)).isTrue(); |
||||
assertThat(blacklist.contains(peer2)).isTrue(); |
||||
assertThat(blacklist.contains(peer3)).isFalse(); |
||||
|
||||
// trigger rollover
|
||||
blacklist.add(peer3.getId()); |
||||
assertThat(blacklist.contains(bannedPeer)).isTrue(); |
||||
assertThat(blacklist.contains(peer1)).isFalse(); |
||||
assertThat(blacklist.contains(peer2)).isTrue(); |
||||
assertThat(blacklist.contains(peer3)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void doesNotBlacklistPeerForNormalDisconnect() { |
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.onDisconnect(peer, DisconnectReason.TOO_MANY_PEERS, false); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void blacklistPeerForBadBehavior() { |
||||
|
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void doesNotBlacklistPeerForOurBadBehavior() { |
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, true); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void blacklistIncompatiblePeer() { |
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.onDisconnect(peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, false); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void blacklistIncompatiblePeerWhoIssuesDisconnect() { |
||||
final PeerBlacklist blacklist = new PeerBlacklist(); |
||||
final PeerConnection peer = generatePeerConnection(); |
||||
|
||||
assertThat(blacklist.contains(peer)).isFalse(); |
||||
|
||||
blacklist.onDisconnect(peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, true); |
||||
|
||||
assertThat(blacklist.contains(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void capsSizeOfList() { |
||||
|
||||
final PeerBlacklist blacklist = new PeerBlacklist(2); |
||||
final PeerConnection peer1 = generatePeerConnection(); |
||||
final PeerConnection peer2 = generatePeerConnection(); |
||||
final PeerConnection peer3 = generatePeerConnection(); |
||||
|
||||
// Add first peer
|
||||
blacklist.onDisconnect(peer1, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
assertThat(blacklist.contains(peer1)).isTrue(); |
||||
assertThat(blacklist.contains(peer2)).isFalse(); |
||||
assertThat(blacklist.contains(peer3)).isFalse(); |
||||
|
||||
// Add second peer
|
||||
blacklist.onDisconnect(peer2, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
assertThat(blacklist.contains(peer1)).isTrue(); |
||||
assertThat(blacklist.contains(peer2)).isTrue(); |
||||
assertThat(blacklist.contains(peer3)).isFalse(); |
||||
|
||||
// Adding third peer should kick out least recently accessed peer
|
||||
blacklist.onDisconnect(peer3, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
assertThat(blacklist.contains(peer1)).isFalse(); |
||||
assertThat(blacklist.contains(peer2)).isTrue(); |
||||
assertThat(blacklist.contains(peer3)).isTrue(); |
||||
|
||||
// Adding peer1 back in should kick out peer2
|
||||
blacklist.onDisconnect(peer1, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
assertThat(blacklist.contains(peer1)).isTrue(); |
||||
assertThat(blacklist.contains(peer2)).isFalse(); |
||||
assertThat(blacklist.contains(peer3)).isTrue(); |
||||
|
||||
// Adding peer2 back in should kick out peer3
|
||||
blacklist.onDisconnect(peer2, DisconnectReason.BREACH_OF_PROTOCOL, false); |
||||
assertThat(blacklist.contains(peer1)).isTrue(); |
||||
assertThat(blacklist.contains(peer2)).isTrue(); |
||||
assertThat(blacklist.contains(peer3)).isFalse(); |
||||
} |
||||
|
||||
private PeerConnection generatePeerConnection() { |
||||
final BytesValue nodeId = BytesValue.of(nodeIdValue++); |
||||
final PeerConnection peer = mock(PeerConnection.class); |
||||
final PeerInfo peerInfo = mock(PeerInfo.class); |
||||
|
||||
when(peerInfo.getNodeId()).thenReturn(nodeId); |
||||
when(peer.getPeerInfo()).thenReturn(peerInfo); |
||||
|
||||
return peer; |
||||
} |
||||
|
||||
private Peer generatePeer() { |
||||
final byte[] id = new byte[64]; |
||||
id[0] = (byte) nodeIdValue++; |
||||
return DefaultPeer.fromEnodeURL( |
||||
EnodeURL.builder() |
||||
.nodeId(id) |
||||
.ipAddress("10.9.8.7") |
||||
.discoveryPort(65535) |
||||
.listeningPort(65534) |
||||
.build()); |
||||
} |
||||
} |
@ -0,0 +1,218 @@ |
||||
/* |
||||
* 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.ethereum.p2p.permissions; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; |
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; |
||||
import tech.pegasys.pantheon.util.enode.EnodeURL; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class PeerPermissionsBlacklistTest { |
||||
|
||||
@Test |
||||
public void add_peer() { |
||||
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
Peer peer = createPeer(); |
||||
|
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
blacklist.subscribeUpdate( |
||||
(restricted, affectedPeers) -> { |
||||
callbackCount.incrementAndGet(); |
||||
assertThat(restricted).isTrue(); |
||||
assertThat(affectedPeers).contains(Collections.singletonList(peer)); |
||||
}); |
||||
|
||||
assertThat(callbackCount).hasValue(0); |
||||
|
||||
blacklist.add(peer); |
||||
assertThat(callbackCount).hasValue(1); |
||||
} |
||||
|
||||
@Test |
||||
public void remove_peer() { |
||||
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
Peer peer = createPeer(); |
||||
blacklist.add(peer); |
||||
|
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
blacklist.subscribeUpdate( |
||||
(restricted, affectedPeers) -> { |
||||
callbackCount.incrementAndGet(); |
||||
assertThat(restricted).isFalse(); |
||||
assertThat(affectedPeers).contains(Collections.singletonList(peer)); |
||||
}); |
||||
|
||||
assertThat(callbackCount).hasValue(0); |
||||
|
||||
blacklist.remove(peer); |
||||
assertThat(callbackCount).hasValue(1); |
||||
} |
||||
|
||||
@Test |
||||
public void add_id() { |
||||
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
Peer peer = createPeer(); |
||||
|
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
blacklist.subscribeUpdate( |
||||
(restricted, affectedPeers) -> { |
||||
callbackCount.incrementAndGet(); |
||||
assertThat(restricted).isTrue(); |
||||
assertThat(affectedPeers).isEmpty(); |
||||
}); |
||||
|
||||
assertThat(callbackCount).hasValue(0); |
||||
|
||||
blacklist.add(peer.getId()); |
||||
assertThat(callbackCount).hasValue(1); |
||||
} |
||||
|
||||
@Test |
||||
public void remove_id() { |
||||
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
Peer peer = createPeer(); |
||||
blacklist.add(peer); |
||||
|
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
blacklist.subscribeUpdate( |
||||
(restricted, affectedPeers) -> { |
||||
callbackCount.incrementAndGet(); |
||||
assertThat(restricted).isFalse(); |
||||
assertThat(affectedPeers).isEmpty(); |
||||
}); |
||||
|
||||
assertThat(callbackCount).hasValue(0); |
||||
|
||||
blacklist.remove(peer.getId()); |
||||
assertThat(callbackCount).hasValue(1); |
||||
} |
||||
|
||||
@Test |
||||
public void trackedPeerIsNotPermitted() { |
||||
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
|
||||
Peer peer = createPeer(); |
||||
assertThat(blacklist.isPermitted(peer)).isTrue(); |
||||
|
||||
blacklist.add(peer); |
||||
assertThat(blacklist.isPermitted(peer)).isFalse(); |
||||
|
||||
blacklist.remove(peer); |
||||
assertThat(blacklist.isPermitted(peer)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void subscribeUpdate() { |
||||
PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
final AtomicInteger restrictedCallbackCount = new AtomicInteger(0); |
||||
Peer peer = createPeer(); |
||||
|
||||
blacklist.subscribeUpdate( |
||||
(permissionsRestricted, affectedPeers) -> { |
||||
callbackCount.incrementAndGet(); |
||||
if (permissionsRestricted) { |
||||
restrictedCallbackCount.incrementAndGet(); |
||||
} |
||||
}); |
||||
|
||||
assertThat(blacklist.isPermitted(peer)).isTrue(); |
||||
assertThat(callbackCount).hasValue(0); |
||||
assertThat(restrictedCallbackCount).hasValue(0); |
||||
|
||||
blacklist.add(peer); |
||||
assertThat(callbackCount).hasValue(1); |
||||
assertThat(restrictedCallbackCount).hasValue(1); |
||||
|
||||
blacklist.add(peer); |
||||
assertThat(callbackCount).hasValue(1); |
||||
assertThat(restrictedCallbackCount).hasValue(1); |
||||
|
||||
blacklist.remove(peer); |
||||
assertThat(callbackCount).hasValue(2); |
||||
assertThat(restrictedCallbackCount).hasValue(1); |
||||
|
||||
blacklist.remove(peer); |
||||
assertThat(callbackCount).hasValue(2); |
||||
assertThat(restrictedCallbackCount).hasValue(1); |
||||
|
||||
blacklist.add(peer); |
||||
assertThat(callbackCount).hasValue(3); |
||||
assertThat(restrictedCallbackCount).hasValue(2); |
||||
} |
||||
|
||||
@Test |
||||
public void createWithLimitedCapacity() { |
||||
final PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(2); |
||||
Peer peerA = createPeer(); |
||||
Peer peerB = createPeer(); |
||||
Peer peerC = createPeer(); |
||||
|
||||
// All peers are initially permitted
|
||||
assertThat(blacklist.isPermitted(peerA)).isTrue(); |
||||
assertThat(blacklist.isPermitted(peerB)).isTrue(); |
||||
assertThat(blacklist.isPermitted(peerC)).isTrue(); |
||||
|
||||
// Add peerA
|
||||
blacklist.add(peerA); |
||||
assertThat(blacklist.isPermitted(peerA)).isFalse(); |
||||
assertThat(blacklist.isPermitted(peerB)).isTrue(); |
||||
assertThat(blacklist.isPermitted(peerC)).isTrue(); |
||||
|
||||
// Add peerB
|
||||
blacklist.add(peerB); |
||||
assertThat(blacklist.isPermitted(peerA)).isFalse(); |
||||
assertThat(blacklist.isPermitted(peerB)).isFalse(); |
||||
assertThat(blacklist.isPermitted(peerC)).isTrue(); |
||||
|
||||
// Add peerC
|
||||
// Limit is exceeded and peerA should drop off of the list and be allowed
|
||||
blacklist.add(peerC); |
||||
assertThat(blacklist.isPermitted(peerA)).isTrue(); |
||||
assertThat(blacklist.isPermitted(peerB)).isFalse(); |
||||
assertThat(blacklist.isPermitted(peerC)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void createWithUnlimitedCapacity() { |
||||
final PeerPermissionsBlacklist blacklist = PeerPermissionsBlacklist.create(); |
||||
final int peerCount = 200; |
||||
final List<Peer> peers = |
||||
Stream.generate(this::createPeer).limit(peerCount).collect(Collectors.toList()); |
||||
|
||||
peers.forEach(p -> assertThat(blacklist.isPermitted(p)).isTrue()); |
||||
peers.forEach(blacklist::add); |
||||
peers.forEach(p -> assertThat(blacklist.isPermitted(p)).isFalse()); |
||||
|
||||
peers.forEach(blacklist::remove); |
||||
peers.forEach(p -> assertThat(blacklist.isPermitted(p)).isTrue()); |
||||
} |
||||
|
||||
private Peer createPeer() { |
||||
return DefaultPeer.fromEnodeURL( |
||||
EnodeURL.builder() |
||||
.nodeId(Peer.randomId()) |
||||
.ipAddress("127.0.0.1") |
||||
.discoveryAndListeningPorts(EnodeURL.DEFAULT_LISTENING_PORT) |
||||
.build()); |
||||
} |
||||
} |
@ -0,0 +1,131 @@ |
||||
/* |
||||
* 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.ethereum.p2p.permissions; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; |
||||
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; |
||||
import tech.pegasys.pantheon.util.enode.EnodeURL; |
||||
|
||||
import java.util.Optional; |
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class PeerPermissionsTest { |
||||
@Test |
||||
public void subscribeUpdate() { |
||||
TestPeerPermissions peerPermissions = new TestPeerPermissions(false); |
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
|
||||
peerPermissions.subscribeUpdate( |
||||
(permissionsRestricted, affectedPeers) -> callbackCount.incrementAndGet()); |
||||
|
||||
peerPermissions.allowPeers(true); |
||||
assertThat(callbackCount).hasValue(1); |
||||
|
||||
peerPermissions.allowPeers(false); |
||||
assertThat(callbackCount).hasValue(2); |
||||
} |
||||
|
||||
@Test |
||||
public void subscribeUpdate_forCombinedPermissions() { |
||||
TestPeerPermissions peerPermissionsA = new TestPeerPermissions(false); |
||||
TestPeerPermissions peerPermissionsB = new TestPeerPermissions(false); |
||||
PeerPermissions combined = PeerPermissions.combine(peerPermissionsA, peerPermissionsB); |
||||
|
||||
final AtomicInteger callbackCount = new AtomicInteger(0); |
||||
final AtomicInteger restrictedCallbackCount = new AtomicInteger(0); |
||||
|
||||
combined.subscribeUpdate( |
||||
(permissionsRestricted, affectedPeers) -> { |
||||
callbackCount.incrementAndGet(); |
||||
if (permissionsRestricted) { |
||||
restrictedCallbackCount.incrementAndGet(); |
||||
} |
||||
}); |
||||
|
||||
peerPermissionsA.allowPeers(true); |
||||
assertThat(callbackCount).hasValue(1); |
||||
assertThat(restrictedCallbackCount).hasValue(0); |
||||
|
||||
peerPermissionsB.allowPeers(true); |
||||
assertThat(callbackCount).hasValue(2); |
||||
assertThat(restrictedCallbackCount).hasValue(0); |
||||
|
||||
peerPermissionsA.allowPeers(false); |
||||
assertThat(callbackCount).hasValue(3); |
||||
assertThat(restrictedCallbackCount).hasValue(1); |
||||
|
||||
peerPermissionsB.allowPeers(false); |
||||
assertThat(callbackCount).hasValue(4); |
||||
assertThat(restrictedCallbackCount).hasValue(2); |
||||
} |
||||
|
||||
@Test |
||||
public void isPermitted_forCombinedPermissions() { |
||||
final PeerPermissions allowPeers = new TestPeerPermissions(true); |
||||
final PeerPermissions disallowPeers = new TestPeerPermissions(false); |
||||
final PeerPermissions noop = PeerPermissions.NOOP; |
||||
final PeerPermissions combinedPermissive = PeerPermissions.combine(noop, allowPeers); |
||||
final PeerPermissions combinedRestrictive = PeerPermissions.combine(disallowPeers, allowPeers); |
||||
|
||||
Peer peer = |
||||
DefaultPeer.fromEnodeURL( |
||||
EnodeURL.builder() |
||||
.listeningPort(30303) |
||||
.discoveryPort(30303) |
||||
.nodeId(Peer.randomId()) |
||||
.ipAddress("127.0.0.1") |
||||
.build()); |
||||
|
||||
assertThat(PeerPermissions.combine(allowPeers, disallowPeers).isPermitted(peer)).isFalse(); |
||||
assertThat(PeerPermissions.combine(disallowPeers, disallowPeers).isPermitted(peer)).isFalse(); |
||||
assertThat(PeerPermissions.combine(disallowPeers, disallowPeers).isPermitted(peer)).isFalse(); |
||||
assertThat(PeerPermissions.combine(allowPeers, disallowPeers).isPermitted(peer)).isFalse(); |
||||
assertThat(PeerPermissions.combine(allowPeers, allowPeers).isPermitted(peer)).isTrue(); |
||||
|
||||
assertThat(PeerPermissions.combine(combinedPermissive, allowPeers).isPermitted(peer)).isTrue(); |
||||
assertThat(PeerPermissions.combine(combinedPermissive, disallowPeers).isPermitted(peer)) |
||||
.isFalse(); |
||||
assertThat(PeerPermissions.combine(combinedRestrictive, allowPeers).isPermitted(peer)) |
||||
.isFalse(); |
||||
assertThat(PeerPermissions.combine(combinedRestrictive, disallowPeers).isPermitted(peer)) |
||||
.isFalse(); |
||||
assertThat(PeerPermissions.combine(combinedRestrictive).isPermitted(peer)).isFalse(); |
||||
assertThat(PeerPermissions.combine(combinedPermissive).isPermitted(peer)).isTrue(); |
||||
|
||||
assertThat(PeerPermissions.combine(noop).isPermitted(peer)).isTrue(); |
||||
assertThat(PeerPermissions.combine().isPermitted(peer)).isTrue(); |
||||
} |
||||
|
||||
private static class TestPeerPermissions extends PeerPermissions { |
||||
|
||||
private boolean allowPeers; |
||||
|
||||
public TestPeerPermissions(final boolean allowPeers) { |
||||
this.allowPeers = allowPeers; |
||||
} |
||||
|
||||
public void allowPeers(final boolean doAllowPeers) { |
||||
this.allowPeers = doAllowPeers; |
||||
dispatchUpdate(!doAllowPeers, Optional.empty()); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isPermitted(final Peer peer) { |
||||
return allowPeers; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,48 @@ |
||||
/* |
||||
* 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.util; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** Helper that creates a thread-safe set with a maximum capacity. */ |
||||
public final class LimitedSet { |
||||
public enum Mode { |
||||
DROP_LEAST_RECENTLY_ACCESSED, |
||||
DROP_OLDEST_ELEMENT |
||||
} |
||||
|
||||
private LimitedSet() {} |
||||
|
||||
/** |
||||
* @param initialCapacity The initial size to allocate for the set. |
||||
* @param maxSize The maximum number of elements to keep in the set. |
||||
* @param mode A mode that determines which element is evicted when the set exceeds its max size. |
||||
* @param <T> The type of object held in the set. |
||||
* @return A thread-safe set that will evict elements when the max size is exceeded. |
||||
*/ |
||||
public static final <T> Set<T> create( |
||||
final int initialCapacity, final int maxSize, final Mode mode) { |
||||
final boolean useAccessOrder = mode.equals(Mode.DROP_LEAST_RECENTLY_ACCESSED); |
||||
return Collections.synchronizedSet( |
||||
Collections.newSetFromMap( |
||||
new LinkedHashMap<T, Boolean>(initialCapacity, 0.75f, useAccessOrder) { |
||||
@Override |
||||
protected boolean removeEldestEntry(final Map.Entry<T, Boolean> eldest) { |
||||
return size() > maxSize; |
||||
} |
||||
})); |
||||
} |
||||
} |
@ -0,0 +1,60 @@ |
||||
/* |
||||
* 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.util; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import tech.pegasys.pantheon.util.LimitedSet.Mode; |
||||
|
||||
import java.util.Set; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class LimitedSetTest { |
||||
|
||||
@Test |
||||
public void create_evictOldest() { |
||||
final Set<Integer> set = LimitedSet.create(1, 2, Mode.DROP_OLDEST_ELEMENT); |
||||
set.add(1); |
||||
assertThat(set.size()).isEqualTo(1); |
||||
set.add(2); |
||||
assertThat(set.size()).isEqualTo(2); |
||||
|
||||
// Access element 1 then add a new element that will put us over the limit
|
||||
set.add(1); |
||||
|
||||
set.add(3); |
||||
assertThat(set.size()).isEqualTo(2); |
||||
// Element 1 should have been evicted
|
||||
assertThat(set.contains(3)).isTrue(); |
||||
assertThat(set.contains(2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void create_evictLeastRecentlyAccessed() { |
||||
final Set<Integer> set = LimitedSet.create(1, 2, Mode.DROP_LEAST_RECENTLY_ACCESSED); |
||||
set.add(1); |
||||
assertThat(set.size()).isEqualTo(1); |
||||
set.add(2); |
||||
assertThat(set.size()).isEqualTo(2); |
||||
|
||||
// Access element 1 then add a new element that will put us over the limit
|
||||
set.add(1); |
||||
|
||||
set.add(3); |
||||
assertThat(set.size()).isEqualTo(2); |
||||
// Element 2 should have been evicted
|
||||
assertThat(set.contains(3)).isTrue(); |
||||
assertThat(set.contains(1)).isTrue(); |
||||
} |
||||
} |
Loading…
Reference in new issue