Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Commit

Permalink
[PAN-2342] Update discovery logic to trust bootnodes only when out of…
Browse files Browse the repository at this point in the history
… sync (#1039)

* [PAN-2342] Created SyncStatusNodePermissioningProvider and NodePermissioningController

* Fix block height comparison logic

* Unit test for SyncStatusNodePermissioningProvider

* Add comment about permissioning while not in sync

* PR comments

* Fix missing final

* Fixing unit test

* Unsubscribing from Synchronizer SyncStatus updates after reaching sync

* Fix race condition

* Simplifying synchronization between callbacks
  • Loading branch information
lucassaldanha authored Mar 6, 2019
1 parent 0499e1a commit 7fbbcc9
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist;
import tech.pegasys.pantheon.ethereum.permissioning.NodeWhitelistController;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodeWhitelistUpdatedEvent;
import tech.pegasys.pantheon.util.Subscribers;
import tech.pegasys.pantheon.util.bytes.BytesValue;
Expand Down Expand Up @@ -135,6 +136,9 @@ public class PeerDiscoveryController {

private RecursivePeerRefreshState recursivePeerRefreshState;

private final Optional<NodePermissioningController> nodePermissioningController =
Optional.empty();

public PeerDiscoveryController(
final KeyPair keypair,
final DiscoveryPeer localPeer,
Expand Down Expand Up @@ -189,7 +193,15 @@ public void start() {
bootstrapNodes.stream()
.filter(this::whitelistIfPresentIsNodePermitted)
.collect(Collectors.toList());
recursivePeerRefreshState.start(initialDiscoveryPeers, localPeer.getId());

if (nodePermissioningController.isPresent()) {
nodePermissioningController
.get()
.startPeerDiscoveryCallback(
() -> recursivePeerRefreshState.start(initialDiscoveryPeers, localPeer.getId()));
} else {
recursivePeerRefreshState.start(initialDiscoveryPeers, localPeer.getId());
}

final long timerId =
timerUtil.setPeriodic(
Expand Down
1 change: 1 addition & 0 deletions ethereum/permissioning/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jar {

dependencies {
implementation project(':util')
implementation project(':ethereum:core')

implementation 'com.google.guava:guava'
implementation 'net.consensys.cava:cava-toml'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.permissioning.node;

import tech.pegasys.pantheon.ethereum.permissioning.node.provider.SyncStatusNodePermissioningProvider;
import tech.pegasys.pantheon.util.enode.EnodeURL;

import java.util.Optional;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class NodePermissioningController {

private static final Logger LOG = LogManager.getLogger();

private final Optional<SyncStatusNodePermissioningProvider> syncStatusNodePermissioningProvider;

public NodePermissioningController(
final SyncStatusNodePermissioningProvider syncStatusNodePermissioningProvider) {
this.syncStatusNodePermissioningProvider = Optional.of(syncStatusNodePermissioningProvider);
}

public NodePermissioningController() {
this.syncStatusNodePermissioningProvider = Optional.empty();
}

public boolean isPermitted(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
LOG.trace("Checking node permission: {} -> {}", sourceEnode, destinationEnode);

return syncStatusNodePermissioningProvider
.map((provider) -> provider.isPermitted(sourceEnode, destinationEnode))
.orElse(true);
}

public void startPeerDiscoveryCallback(final Runnable peerDiscoveryCallback) {
syncStatusNodePermissioningProvider.ifPresent(
(p) -> p.setHasReachedSyncCallback(peerDiscoveryCallback));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.permissioning.node;

import tech.pegasys.pantheon.util.enode.EnodeURL;

@FunctionalInterface
public interface NodePermissioningProvider {

boolean isPermitted(final EnodeURL sourceEnode, final EnodeURL destinationEnode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.permissioning.node.provider;

import static com.google.common.base.Preconditions.checkNotNull;

import tech.pegasys.pantheon.ethereum.core.SyncStatus;
import tech.pegasys.pantheon.ethereum.core.Synchronizer;
import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningProvider;
import tech.pegasys.pantheon.util.enode.EnodeURL;

import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.OptionalLong;

import com.google.common.annotations.VisibleForTesting;

public class SyncStatusNodePermissioningProvider implements NodePermissioningProvider {

private final Synchronizer synchronizer;
private final Collection<EnodeURL> bootnodes = new HashSet<>();
private OptionalLong syncStatusObserverId;
private boolean hasReachedSync = false;
private Optional<Runnable> hasReachedSyncCallback = Optional.empty();

public SyncStatusNodePermissioningProvider(
final Synchronizer synchronizer, final Collection<EnodeURL> bootnodes) {
checkNotNull(synchronizer);
this.synchronizer = synchronizer;
long id = this.synchronizer.observeSyncStatus(this::handleSyncStatusUpdate);
this.syncStatusObserverId = OptionalLong.of(id);
this.bootnodes.addAll(bootnodes);
}

private void handleSyncStatusUpdate(final SyncStatus syncStatus) {
if (syncStatus != null) {
long blocksBehind = syncStatus.getHighestBlock() - syncStatus.getCurrentBlock();
if (blocksBehind <= 0) {
synchronized (this) {
if (!hasReachedSync) {
runCallback();
syncStatusObserverId.ifPresent(
id -> {
synchronizer.removeObserver(id);
syncStatusObserverId = OptionalLong.empty();
});
hasReachedSync = true;
}
}
}
}
}

public synchronized void setHasReachedSyncCallback(final Runnable runnable) {
if (hasReachedSync) {
runCallback();
} else {
this.hasReachedSyncCallback = Optional.of(runnable);
}
}

private synchronized void runCallback() {
hasReachedSyncCallback.ifPresent(Runnable::run);
hasReachedSyncCallback = Optional.empty();
}

/**
* Before reaching a sync'd state, the node will only be allowed to talk to its bootnodes
* (outgoing connections). After reaching a sync'd state, it is expected that other providers will
* check the permissions (most likely the smart contract based provider). That's why we always
* return true after reaching a sync'd state.
*
* @param sourceEnode the enode source of the packet or connection
* @param destinationEnode the enode target of the packet or connection
* @return true, if the communication from sourceEnode to destinationEnode is permitted, false
* otherwise
*/
@Override
public boolean isPermitted(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
if (hasReachedSync) {
return true;
} else {
return bootnodes.contains(destinationEnode);
}
}

@VisibleForTesting
boolean hasReachedSync() {
return hasReachedSync;
}
}
Original file line number Diff line number Diff line change
@@ -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.ethereum.permissioning.node;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;

import tech.pegasys.pantheon.ethereum.permissioning.node.provider.SyncStatusNodePermissioningProvider;
import tech.pegasys.pantheon.util.enode.EnodeURL;

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 NodePermissioningControllerTest {

private static final EnodeURL enode1 =
new EnodeURL(
"enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f09@192.168.0.2:1234");
private static final EnodeURL enode2 =
new EnodeURL(
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.3:5678");

@Mock private SyncStatusNodePermissioningProvider syncStatusNodePermissioningProvider;

private NodePermissioningController controller;

@Before
public void before() {
this.controller = new NodePermissioningController(syncStatusNodePermissioningProvider);
}

@Test
public void isPermittedShouldDelegateToSyncStatusProvider() {
controller.isPermitted(enode1, enode2);

verify(syncStatusNodePermissioningProvider).isPermitted(eq(enode1), eq(enode2));
}

@Test
public void peerDiscoveryCallbackShouldBeDelegatedToSyncStatusNodePermissioningProvider() {
controller.startPeerDiscoveryCallback(() -> {});

verify(syncStatusNodePermissioningProvider).setHasReachedSyncCallback(any(Runnable.class));
}
}
Loading

0 comments on commit 7fbbcc9

Please sign in to comment.