diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDistanceCalculator.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDistanceCalculator.java new file mode 100644 index 0000000000..a95f519cf4 --- /dev/null +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDistanceCalculator.java @@ -0,0 +1,50 @@ +/* + * 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.discovery.internal; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; + +public class PeerDistanceCalculator { + + /** + * Calculates the XOR distance between two values. + * + * @param v1 the first value + * @param v2 the second value + * @return the distance + */ + static int distance(final BytesValue v1, final BytesValue v2) { + assert (v1.size() == v2.size()); + final byte[] v1b = v1.extractArray(); + final byte[] v2b = v2.extractArray(); + if (Arrays.equals(v1b, v2b)) { + return 0; + } + int distance = v1b.length * 8; + for (int i = 0; i < v1b.length; i++) { + final byte xor = (byte) (0xff & (v1b[i] ^ v2b[i])); + if (xor == 0) { + distance -= 8; + } else { + int p = 7; + while (((xor >> p--) & 0x01) == 0) { + distance--; + } + break; + } + } + return distance; + } +} diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java index 3442b86afe..d1b3dfbc84 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java @@ -15,6 +15,7 @@ package tech.pegasys.pantheon.ethereum.p2p.discovery.internal; import static java.util.Collections.unmodifiableList; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.toList; +import static tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDistanceCalculator.distance; import tech.pegasys.pantheon.crypto.Hash; import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; @@ -203,38 +204,6 @@ public class PeerTable { return distance == null ? distance(keccak256, peer.keccak256()) : distance; } - /** - * Calculates the XOR distance between two values. - * - * @param v1 the first value - * @param v2 the second value - * @return the distance - */ - static int distance(final BytesValue v1, final BytesValue v2) { - assert (v1.size() == v2.size()); - final byte[] v1b = v1.extractArray(); - final byte[] v2b = v2.extractArray(); - - if (Arrays.equals(v1b, v2b)) { - return 0; - } - - int distance = v1b.length * 8; - for (int i = 0; i < v1b.length; i++) { - final byte xor = (byte) (0xff & (v1b[i] ^ v2b[i])); - if (xor == 0) { - distance -= 8; - } else { - int p = 7; - while (((xor >> p--) & 0x01) == 0) { - distance--; - } - break; - } - } - return distance; - } - /** A class that encapsulates the result of a peer addition to the table. */ public static class AddResult { /** The outcome of the operation. */ diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshState.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshState.java new file mode 100644 index 0000000000..4a0015ba27 --- /dev/null +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshState.java @@ -0,0 +1,223 @@ +/* + * 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.discovery.internal; + +import static java.util.stream.Collectors.toList; +import static tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDistanceCalculator.distance; + +import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; +import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.google.common.annotations.VisibleForTesting; + +class RecursivePeerRefreshState { + private final int CONCURRENT_REQUEST_LIMIT = 3; + private final BytesValue target; + private final PeerBlacklist peerBlacklist; + private final BondingAgent bondingAgent; + private final NeighborFinder neighborFinder; + private final List anteList; + private final List outstandingRequestList; + private final List contactedInCurrentExecution; + + RecursivePeerRefreshState( + final BytesValue target, + final PeerBlacklist peerBlacklist, + final BondingAgent bondingAgent, + final NeighborFinder neighborFinder) { + this.target = target; + this.peerBlacklist = peerBlacklist; + this.bondingAgent = bondingAgent; + this.neighborFinder = neighborFinder; + this.anteList = new ArrayList<>(); + this.outstandingRequestList = new ArrayList<>(); + this.contactedInCurrentExecution = new ArrayList<>(); + } + + void kickstartBootstrapPeers(final List bootstrapPeers) { + for (Peer bootstrapPeer : bootstrapPeers) { + final BytesValue peerId = bootstrapPeer.getId(); + outstandingRequestList.add(new OutstandingRequest(bootstrapPeer)); + contactedInCurrentExecution.add(peerId); + bondingAgent.performBonding(bootstrapPeer, true); + neighborFinder.issueFindNodeRequest(bootstrapPeer, target); + } + } + + /** + * This method is intended to be called periodically by the {@link PeerDiscoveryController}, which + * will maintain a timer for purposes of effecting expiration of requests outstanding. Requests + * once encountered are deemed eligible for eviction if they have not been dispatched before the + * next invocation of the method. + */ + public void executeTimeoutEvaluation() { + for (int i = 0; i < outstandingRequestList.size(); i++) { + if (outstandingRequestList.get(i).getEvaluation()) { + final List queryCandidates = determineFindNodeCandidates(anteList.size()); + for (DiscoveryPeer candidate : queryCandidates) { + if (!contactedInCurrentExecution.contains(candidate.getId()) + && !outstandingRequestList.contains(new OutstandingRequest(candidate))) { + outstandingRequestList.remove(i); + executeFindNodeRequest(candidate); + } + } + } + outstandingRequestList.get(i).setEvaluation(); + } + } + + private void executeFindNodeRequest(final DiscoveryPeer peer) { + final BytesValue peerId = peer.getId(); + outstandingRequestList.add(new OutstandingRequest(peer)); + contactedInCurrentExecution.add(peerId); + neighborFinder.issueFindNodeRequest(peer, target); + } + + /** + * The lookup initiator starts by picking CONCURRENT_REQUEST_LIMIT closest nodes to the target it + * knows of. The initiator then issues concurrent FindNode packets to those nodes. + */ + private void initiatePeerRefreshCycle(final List peers) { + for (DiscoveryPeer peer : peers) { + if (!contactedInCurrentExecution.contains(peer.getId())) { + executeFindNodeRequest(peer); + } + } + } + + void onNeighboursPacketReceived(final NeighborsPacketData neighboursPacket, final Peer peer) { + if (outstandingRequestList.contains(new OutstandingRequest(peer))) { + final List receivedPeerList = neighboursPacket.getNodes(); + for (DiscoveryPeer receivedPeer : receivedPeerList) { + if (!peerBlacklist.contains(receivedPeer)) { + bondingAgent.performBonding(receivedPeer, false); + anteList.add(new PeerDistance(receivedPeer, distance(target, receivedPeer.getId()))); + } + } + outstandingRequestList.remove(new OutstandingRequest(peer)); + queryNearestNodes(); + } + } + + private List determineFindNodeCandidates(final int threshold) { + anteList.sort( + (peer1, peer2) -> { + if (peer1.getDistance() > peer2.getDistance()) return 1; + if (peer1.getDistance() < peer2.getDistance()) return -1; + return 0; + }); + return anteList.subList(0, threshold).stream().map(PeerDistance::getPeer).collect(toList()); + } + + private void queryNearestNodes() { + if (outstandingRequestList.isEmpty()) { + final List queryCandidates = + determineFindNodeCandidates(CONCURRENT_REQUEST_LIMIT); + initiatePeerRefreshCycle(queryCandidates); + } + } + + @VisibleForTesting + List getOutstandingRequestList() { + return outstandingRequestList; + } + + static class PeerDistance { + DiscoveryPeer peer; + Integer distance; + + PeerDistance(final DiscoveryPeer peer, final Integer distance) { + this.peer = peer; + this.distance = distance; + } + + DiscoveryPeer getPeer() { + return peer; + } + + Integer getDistance() { + return distance; + } + + @Override + public String toString() { + return peer + ": " + distance; + } + } + + static class OutstandingRequest { + boolean evaluation; + Peer peer; + + OutstandingRequest(final Peer peer) { + this.evaluation = false; + this.peer = peer; + } + + boolean getEvaluation() { + return evaluation; + } + + Peer getPeer() { + return peer; + } + + void setEvaluation() { + this.evaluation = true; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OutstandingRequest that = (OutstandingRequest) o; + return Objects.equals(peer.getId(), that.peer.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(peer.getId()); + } + + @Override + public String toString() { + return peer.toString(); + } + } + + public interface NeighborFinder { + /** + * Sends a FIND_NEIGHBORS message to a {@link DiscoveryPeer}, in search of a target value. + * + * @param peer the peer to interrogate + * @param target the target node ID to find + */ + void issueFindNodeRequest(final Peer peer, final BytesValue target); + } + + public interface BondingAgent { + /** + * Initiates a bonding PING-PONG cycle with a peer. + * + * @param peer The targeted peer. + * @param bootstrap Whether this is a bootstrap interaction. + */ + void performBonding(final Peer peer, final boolean bootstrap); + } +} diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java index ed0396026a..5ace8b4245 100644 --- a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java @@ -13,6 +13,7 @@ package tech.pegasys.pantheon.ethereum.p2p.discovery.internal; import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.ethereum.p2p.discovery.internal.PeerDistanceCalculator.distance; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -26,55 +27,55 @@ public class PeerDiscoveryControllerDistanceCalculatorTest { public void distanceZero() { final byte[] id = new byte[64]; new Random().nextBytes(id); - assertThat(PeerTable.distance(BytesValue.wrap(id), BytesValue.wrap(id))).isEqualTo(0); + assertThat(distance(BytesValue.wrap(id), BytesValue.wrap(id))).isEqualTo(0); } @Test public void distance1() { final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); final BytesValue id2 = BytesValue.fromHexString("0x8f19400001"); - assertThat(PeerTable.distance(id1, id2)).isEqualTo(1); + assertThat(distance(id1, id2)).isEqualTo(1); } @Test public void distance2() { final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); final BytesValue id2 = BytesValue.fromHexString("0x8f19400002"); - assertThat(PeerTable.distance(id1, id2)).isEqualTo(2); + assertThat(distance(id1, id2)).isEqualTo(2); } @Test public void distance3() { final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); final BytesValue id2 = BytesValue.fromHexString("0x8f19400004"); - assertThat(PeerTable.distance(id1, id2)).isEqualTo(3); + assertThat(distance(id1, id2)).isEqualTo(3); } @Test public void distance9() { final BytesValue id1 = BytesValue.fromHexString("0x8f19400100"); final BytesValue id2 = BytesValue.fromHexString("0x8f19400000"); - assertThat(PeerTable.distance(id1, id2)).isEqualTo(9); + assertThat(distance(id1, id2)).isEqualTo(9); } @Test public void distance40() { final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); final BytesValue id2 = BytesValue.fromHexString("0x0f19400000"); - assertThat(PeerTable.distance(id1, id2)).isEqualTo(40); + assertThat(distance(id1, id2)).isEqualTo(40); } @Test(expected = AssertionError.class) public void distance40_differentLengths() { final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); final BytesValue id2 = BytesValue.fromHexString("0x0f1940000099"); - assertThat(PeerTable.distance(id1, id2)).isEqualTo(40); + assertThat(distance(id1, id2)).isEqualTo(40); } @Test public void distanceZero_emptyArrays() { final BytesValue id1 = BytesValue.EMPTY; final BytesValue id2 = BytesValue.EMPTY; - assertThat(PeerTable.distance(id1, id2)).isEqualTo(0); + assertThat(distance(id1, id2)).isEqualTo(0); } } diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshStateTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshStateTest.java new file mode 100644 index 0000000000..5a314c49fa --- /dev/null +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/RecursivePeerRefreshStateTest.java @@ -0,0 +1,331 @@ +/* + * 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.discovery.internal; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import tech.pegasys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import tech.pegasys.pantheon.ethereum.p2p.peers.Endpoint; +import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; + +public class RecursivePeerRefreshStateTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private RecursivePeerRefreshState recursivePeerRefreshState; + + private final BytesValue target = + BytesValue.fromHexString( + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + + private final RecursivePeerRefreshState.BondingAgent bondingAgent = + mock(RecursivePeerRefreshState.BondingAgent.class); + private final RecursivePeerRefreshState.NeighborFinder neighborFinder = + mock(RecursivePeerRefreshState.NeighborFinder.class); + + private final List aggregatePeerList = new ArrayList<>(); + + private NeighborsPacketData neighborsPacketData_000; + private NeighborsPacketData neighborsPacketData_010; + private NeighborsPacketData neighborsPacketData_011; + private NeighborsPacketData neighborsPacketData_012; + private NeighborsPacketData neighborsPacketData_013; + + private TestPeer peer_000; + private TestPeer peer_010; + private TestPeer peer_020; + private TestPeer peer_021; + private TestPeer peer_022; + private TestPeer peer_023; + private TestPeer peer_011; + private TestPeer peer_120; + private TestPeer peer_121; + private TestPeer peer_122; + private TestPeer peer_123; + private TestPeer peer_012; + private TestPeer peer_220; + private TestPeer peer_221; + private TestPeer peer_222; + private TestPeer peer_223; + private TestPeer peer_013; + private TestPeer peer_320; + private TestPeer peer_321; + private TestPeer peer_322; + private TestPeer peer_323; + + @Before + public void setup() throws Exception { + JsonNode peers = + MAPPER.readTree(RecursivePeerRefreshStateTest.class.getResource("/peers.json")); + recursivePeerRefreshState = + new RecursivePeerRefreshState(target, new PeerBlacklist(), bondingAgent, neighborFinder); + + peer_000 = (TestPeer) generatePeer(peers); + + peer_010 = (TestPeer) peer_000.getPeerTable().get(0); + + peer_020 = (TestPeer) peer_010.getPeerTable().get(0); + peer_021 = (TestPeer) peer_010.getPeerTable().get(1); + peer_022 = (TestPeer) peer_010.getPeerTable().get(2); + peer_023 = (TestPeer) peer_010.getPeerTable().get(3); + + peer_011 = (TestPeer) peer_000.getPeerTable().get(1); + + peer_120 = (TestPeer) peer_011.getPeerTable().get(0); + peer_121 = (TestPeer) peer_011.getPeerTable().get(1); + peer_122 = (TestPeer) peer_011.getPeerTable().get(2); + peer_123 = (TestPeer) peer_011.getPeerTable().get(3); + + peer_012 = (TestPeer) peer_000.getPeerTable().get(2); + + peer_220 = (TestPeer) peer_012.getPeerTable().get(0); + peer_221 = (TestPeer) peer_012.getPeerTable().get(1); + peer_222 = (TestPeer) peer_012.getPeerTable().get(2); + peer_223 = (TestPeer) peer_012.getPeerTable().get(3); + + peer_013 = (TestPeer) peer_000.getPeerTable().get(3); + + peer_320 = (TestPeer) peer_013.getPeerTable().get(0); + peer_321 = (TestPeer) peer_013.getPeerTable().get(1); + peer_322 = (TestPeer) peer_013.getPeerTable().get(2); + peer_323 = (TestPeer) peer_013.getPeerTable().get(3); + + neighborsPacketData_000 = NeighborsPacketData.create(peer_000.getPeerTable()); + neighborsPacketData_010 = NeighborsPacketData.create(peer_010.getPeerTable()); + neighborsPacketData_011 = NeighborsPacketData.create(peer_011.getPeerTable()); + neighborsPacketData_012 = NeighborsPacketData.create(peer_012.getPeerTable()); + neighborsPacketData_013 = NeighborsPacketData.create(peer_013.getPeerTable()); + + addPeersToAggregateListByOrdinalRank(); + } + + private void addPeersToAggregateListByOrdinalRank() { + aggregatePeerList.add(peer_323); // 1 + aggregatePeerList.add(peer_011); // 2 + aggregatePeerList.add(peer_012); // 3 + aggregatePeerList.add(peer_013); // 4 + aggregatePeerList.add(peer_020); // 5 + aggregatePeerList.add(peer_021); // 6 + aggregatePeerList.add(peer_022); // 7 + aggregatePeerList.add(peer_023); // 8 + aggregatePeerList.add(peer_120); // 9 + aggregatePeerList.add(peer_121); // 10 + aggregatePeerList.add(peer_122); // 11 + aggregatePeerList.add(peer_123); // 12 + aggregatePeerList.add(peer_220); // 13 + aggregatePeerList.add(peer_221); // 14 + aggregatePeerList.add(peer_222); // 15 + aggregatePeerList.add(peer_223); // 16 + aggregatePeerList.add(peer_320); // 17 + aggregatePeerList.add(peer_321); // 18 + aggregatePeerList.add(peer_322); // 19 + aggregatePeerList.add(peer_010); // 20 + aggregatePeerList.add(peer_000); // 21 + } + + @Test + public void shouldEstablishRelativeDistanceValues() { + for (int i = 0; i < aggregatePeerList.size() - 1; i++) { + int nodeOrdinalRank = aggregatePeerList.get(i).getOrdinalRank(); + int neighborOrdinalRank = aggregatePeerList.get(i + 1).getOrdinalRank(); + assertThat(nodeOrdinalRank).isLessThan(neighborOrdinalRank); + } + } + + @Test + public void shouldConfirmPeersMatchCorrespondingPackets() { + assertThat(matchPeerToCorrespondingPacketData(peer_000, neighborsPacketData_000)).isTrue(); + assertThat(matchPeerToCorrespondingPacketData(peer_010, neighborsPacketData_010)).isTrue(); + assertThat(matchPeerToCorrespondingPacketData(peer_011, neighborsPacketData_011)).isTrue(); + assertThat(matchPeerToCorrespondingPacketData(peer_012, neighborsPacketData_012)).isTrue(); + assertThat(matchPeerToCorrespondingPacketData(peer_013, neighborsPacketData_013)).isTrue(); + } + + private boolean matchPeerToCorrespondingPacketData( + final TestPeer peer, final NeighborsPacketData neighborsPacketData) { + for (TestPeer neighbour : + neighborsPacketData.getNodes().stream().map(p -> (TestPeer) p).collect(toList())) { + if (neighbour.getParent() != peer.getIdentifier()) { + return false; + } + if (neighbour.getTier() != peer.getTier() + 1) { + return false; + } + } + return true; + } + + @Test + public void shouldIssueRequestToPeerWithLesserDistanceGreaterHops() { + recursivePeerRefreshState.kickstartBootstrapPeers(Collections.singletonList(peer_000)); + + verify(bondingAgent).performBonding(peer_000, true); + verify(neighborFinder).issueFindNodeRequest(peer_000, target); + + recursivePeerRefreshState.onNeighboursPacketReceived(neighborsPacketData_000, peer_000); + assertThat(recursivePeerRefreshState.getOutstandingRequestList().size()).isLessThanOrEqualTo(3); + + verify(bondingAgent).performBonding(peer_010, false); + verify(bondingAgent).performBonding(peer_011, false); + verify(bondingAgent).performBonding(peer_012, false); + verify(bondingAgent).performBonding(peer_013, false); + + verify(neighborFinder, never()).issueFindNodeRequest(peer_010, target); + verify(neighborFinder).issueFindNodeRequest(peer_011, target); + verify(neighborFinder).issueFindNodeRequest(peer_012, target); + verify(neighborFinder).issueFindNodeRequest(peer_013, target); + + recursivePeerRefreshState.onNeighboursPacketReceived(neighborsPacketData_011, peer_011); + assertThat(recursivePeerRefreshState.getOutstandingRequestList().size()).isLessThanOrEqualTo(3); + + verify(bondingAgent).performBonding(peer_120, false); + verify(bondingAgent).performBonding(peer_121, false); + verify(bondingAgent).performBonding(peer_122, false); + verify(bondingAgent).performBonding(peer_123, false); + + recursivePeerRefreshState.onNeighboursPacketReceived(neighborsPacketData_012, peer_012); + assertThat(recursivePeerRefreshState.getOutstandingRequestList().size()).isLessThanOrEqualTo(3); + + verify(bondingAgent).performBonding(peer_220, false); + verify(bondingAgent).performBonding(peer_221, false); + verify(bondingAgent).performBonding(peer_222, false); + verify(bondingAgent).performBonding(peer_223, false); + + recursivePeerRefreshState.onNeighboursPacketReceived(neighborsPacketData_013, peer_013); + assertThat(recursivePeerRefreshState.getOutstandingRequestList().size()).isLessThanOrEqualTo(3); + + verify(bondingAgent).performBonding(peer_320, false); + verify(bondingAgent).performBonding(peer_321, false); + verify(bondingAgent).performBonding(peer_322, false); + verify(bondingAgent).performBonding(peer_323, false); + + verify(neighborFinder, never()).issueFindNodeRequest(peer_320, target); + verify(neighborFinder, never()).issueFindNodeRequest(peer_321, target); + verify(neighborFinder, never()).issueFindNodeRequest(peer_322, target); + verify(neighborFinder).issueFindNodeRequest(peer_323, target); + } + + @Test + public void shouldIssueRequestToPeerWithGreaterDistanceOnExpirationOfLowerDistancePeerRequest() { + recursivePeerRefreshState.kickstartBootstrapPeers(Collections.singletonList(peer_000)); + recursivePeerRefreshState.executeTimeoutEvaluation(); + + verify(bondingAgent).performBonding(peer_000, true); + verify(neighborFinder).issueFindNodeRequest(peer_000, target); + + recursivePeerRefreshState.onNeighboursPacketReceived(neighborsPacketData_000, peer_000); + assertThat(recursivePeerRefreshState.getOutstandingRequestList().size()).isLessThanOrEqualTo(3); + + recursivePeerRefreshState.executeTimeoutEvaluation(); + + verify(neighborFinder, never()).issueFindNodeRequest(peer_010, target); + verify(neighborFinder).issueFindNodeRequest(peer_011, target); + verify(neighborFinder).issueFindNodeRequest(peer_012, target); + verify(neighborFinder).issueFindNodeRequest(peer_013, target); + + recursivePeerRefreshState.executeTimeoutEvaluation(); + assertThat(recursivePeerRefreshState.getOutstandingRequestList().size()).isLessThanOrEqualTo(3); + + verify(neighborFinder).issueFindNodeRequest(peer_010, target); + } + + private DiscoveryPeer generatePeer(final JsonNode peer) { + int parent = peer.get("parent").asInt(); + int tier = peer.get("tier").asInt(); + int identifier = peer.get("identifier").asInt(); + int ordinalRank = peer.get("ordinalRank").asInt(); + BytesValue id = BytesValue.fromHexString(peer.get("id").asText()); + List peerTable = new ArrayList<>(); + if (peer.get("peerTable") != null) { + JsonNode peers = peer.get("peerTable"); + for (JsonNode element : peers) { + peerTable.add(generatePeer(element)); + } + } else { + peerTable = Collections.emptyList(); + } + return new TestPeer(parent, tier, identifier, ordinalRank, id, peerTable); + } + + static class TestPeer extends DiscoveryPeer { + int parent; + int tier; + int identifier; + int ordinalRank; + List peerTable; + + TestPeer( + final int parent, + final int tier, + final int identifier, + final int ordinalRank, + final BytesValue id, + final List peerTable) { + super(id, "0.0.0.0", 1, 1); + this.parent = parent; + this.tier = tier; + this.identifier = identifier; + this.ordinalRank = ordinalRank; + this.peerTable = peerTable; + } + + int getParent() { + return parent; + } + + int getTier() { + return tier; + } + + int getIdentifier() { + return identifier; + } + + int getOrdinalRank() { + return ordinalRank; + } + + List getPeerTable() { + return peerTable; + } + + @Override + public Bytes32 keccak256() { + return null; + } + + @Override + public Endpoint getEndpoint() { + return null; + } + + @Override + public String toString() { + return parent + "." + tier + "." + identifier; + } + } +} diff --git a/ethereum/p2p/src/test/resources/peers.json b/ethereum/p2p/src/test/resources/peers.json new file mode 100644 index 0000000000..2767aed866 --- /dev/null +++ b/ethereum/p2p/src/test/resources/peers.json @@ -0,0 +1,173 @@ +{ + "parent": 0, + "tier": 0, + "identifier": 0, + "ordinalRank": 21, + "id": "0x11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "peerTable": [ + { + "parent": 0, + "tier": 1, + "identifier": 0, + "ordinalRank": 20, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000", + "peerTable": [ + { + "parent": 0, + "tier": 2, + "identifier": 0, + "ordinalRank": 5, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000", + "peerTable": null + }, + { + "parent": 0, + "tier": 2, + "identifier": 1, + "ordinalRank": 6, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000", + "peerTable": null + }, + { + "parent": 0, + "tier": 2, + "identifier": 2, + "ordinalRank": 7, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000", + "peerTable": null + }, + { + "parent": 0, + "tier": 2, + "identifier": 3, + "ordinalRank": 8, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000", + "peerTable": null + } + ] + }, + { + "parent": 0, + "tier": 1, + "identifier": 1, + "ordinalRank": 2, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010", + "peerTable": [ + { + "parent": 1, + "tier": 2, + "identifier": 0, + "ordinalRank": 9, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000", + "peerTable": null + }, + { + "parent": 1, + "tier": 2, + "identifier": 1, + "ordinalRank": 10, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000", + "peerTable": null + }, + { + "parent": 1, + "tier": 2, + "identifier": 2, + "ordinalRank": 11, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000", + "peerTable": null + }, + { + "parent": 1, + "tier": 2, + "identifier": 3, + "ordinalRank": 12, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000", + "peerTable": null + } + ] + }, + { + "parent": 0, + "tier": 1, + "identifier": 2, + "ordinalRank": 3, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100", + "peerTable": [ + { + "parent": 2, + "tier": 2, + "identifier": 0, + "ordinalRank": 13, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000", + "peerTable": null + }, + { + "parent": 2, + "tier": 2, + "identifier": 1, + "ordinalRank": 14, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000", + "peerTable": null + }, + { + "parent": 2, + "tier": 2, + "identifier": 2, + "ordinalRank": 15, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000", + "peerTable": null + }, + { + "parent": 2, + "tier": 2, + "identifier": 3, + "ordinalRank": 16, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000", + "peerTable": null + } + ] + }, + { + "parent": 0, + "tier": 1, + "identifier": 3, + "ordinalRank": 4, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000", + "peerTable": [ + { + "parent": 3, + "tier": 2, + "identifier": 0, + "ordinalRank": 17, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000", + "peerTable": null + }, + { + "parent": 3, + "tier": 2, + "identifier": 1, + "ordinalRank": 18, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000", + "peerTable": null + }, + { + "parent": 3, + "tier": 2, + "identifier": 2, + "ordinalRank": 19, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000", + "peerTable": null + }, + { + "parent": 3, + "tier": 2, + "identifier": 3, + "ordinalRank": 1, + "id": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "peerTable": null + } + ] + } + ] +} \ No newline at end of file