mirror of https://github.com/hyperledger/besu
[PAN-2342] Update discovery logic to trust bootnodes only when out of 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 Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
55563e04b2
commit
2fac2397b3
@ -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)); |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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)); |
||||
} |
||||
} |
@ -0,0 +1,157 @@ |
||||
/* |
||||
* 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 org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.ArgumentMatchers.eq; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyZeroInteractions; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.core.SyncStatus; |
||||
import tech.pegasys.pantheon.ethereum.core.Synchronizer; |
||||
import tech.pegasys.pantheon.ethereum.core.Synchronizer.SyncStatusListener; |
||||
import tech.pegasys.pantheon.util.enode.EnodeURL; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.ArgumentCaptor; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.class) |
||||
public class SyncStatusNodePermissioningProviderTest { |
||||
|
||||
private static final EnodeURL bootnode = |
||||
new EnodeURL( |
||||
"enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@192.168.0.1:9999"); |
||||
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 Synchronizer synchronizer; |
||||
private Collection<EnodeURL> bootnodes = new ArrayList<>(); |
||||
private SyncStatusNodePermissioningProvider provider; |
||||
private SyncStatusListener syncStatusListener; |
||||
private long syncStatusObserverId = 1L; |
||||
|
||||
@Before |
||||
public void before() { |
||||
final ArgumentCaptor<SyncStatusListener> captor = |
||||
ArgumentCaptor.forClass(SyncStatusListener.class); |
||||
when(synchronizer.observeSyncStatus(captor.capture())).thenReturn(syncStatusObserverId); |
||||
bootnodes.add(bootnode); |
||||
|
||||
this.provider = new SyncStatusNodePermissioningProvider(synchronizer, bootnodes); |
||||
this.syncStatusListener = captor.getValue(); |
||||
|
||||
verify(synchronizer).observeSyncStatus(any()); |
||||
} |
||||
|
||||
@Test |
||||
public void whenIsNotInSyncHasReachedSyncShouldReturnFalse() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2)); |
||||
|
||||
assertThat(provider.hasReachedSync()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void whenInSyncHasReachedSyncShouldReturnTrue() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 1)); |
||||
|
||||
assertThat(provider.hasReachedSync()).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void whenInSyncChangesFromTrueToFalseHasReachedSyncShouldReturnTrue() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2)); |
||||
assertThat(provider.hasReachedSync()).isFalse(); |
||||
|
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 2, 1)); |
||||
assertThat(provider.hasReachedSync()).isTrue(); |
||||
|
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 2, 3)); |
||||
assertThat(provider.hasReachedSync()).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void whenNotInSyncShouldNotExecuteCallback() { |
||||
final Runnable callbackFunction = mock(Runnable.class); |
||||
provider.setHasReachedSyncCallback(callbackFunction); |
||||
|
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2)); |
||||
|
||||
verifyZeroInteractions(callbackFunction); |
||||
} |
||||
|
||||
@Test |
||||
public void whenInSyncShouldExecuteCallback() { |
||||
final Runnable callbackFunction = mock(Runnable.class); |
||||
provider.setHasReachedSyncCallback(callbackFunction); |
||||
|
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 1)); |
||||
|
||||
verify(callbackFunction).run(); |
||||
// after executing callback, it should unsubscribe from the SyncStatus updates
|
||||
verify(synchronizer).removeObserver(eq(syncStatusObserverId)); |
||||
} |
||||
|
||||
@Test |
||||
public void whenHasNotSyncedNonBootnodeShouldNotBePermitted() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2)); |
||||
assertThat(provider.hasReachedSync()).isFalse(); |
||||
|
||||
boolean isPermitted = provider.isPermitted(enode1, enode2); |
||||
|
||||
assertThat(isPermitted).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void whenHasNotSyncedBootnodeIncomingConnectionShouldNotBePermitted() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2)); |
||||
assertThat(provider.hasReachedSync()).isFalse(); |
||||
|
||||
boolean isPermitted = provider.isPermitted(bootnode, enode1); |
||||
|
||||
assertThat(isPermitted).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void whenHasNotSyncedBootnodeOutgoingConnectionShouldBePermitted() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 2)); |
||||
assertThat(provider.hasReachedSync()).isFalse(); |
||||
|
||||
boolean isPermitted = provider.isPermitted(enode1, bootnode); |
||||
|
||||
assertThat(isPermitted).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void whenHasSyncedIsPermittedShouldReturnTrue() { |
||||
syncStatusListener.onSyncStatus(new SyncStatus(0, 1, 1)); |
||||
assertThat(provider.hasReachedSync()).isTrue(); |
||||
|
||||
boolean isPermitted = provider.isPermitted(enode1, enode2); |
||||
|
||||
assertThat(isPermitted).isTrue(); |
||||
} |
||||
} |
Loading…
Reference in new issue