Skip to content

Commit 594e45c

Browse files
matiwinnetouMateusz Czeladkaclaude
authored
feat: limit and randomize peer discovery to 25 peers (#640)
Implements peer limiting and randomization to improve network peer distribution: - Limits peer responses to 25 peers (configurable via MAX_PEERS constant) - Randomizes peer selection on each load/discovery to avoid always returning the same peers - Updates PeerSnapshotServiceImpl to shuffle and limit peers from snapshot files - Updates PeerDiscoveryManager to shuffle and limit discovered peers from cardano-node - Maintains deterministic in-memory cache between refreshes - Updates tests to reflect new peer limiting and randomization behavior This prevents excessive peer lists and naturally distributes connections across the network without needing to track peer performance metrics. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Mateusz Czeladka <mateusz.czeladka@cardanofoundation.org> Co-authored-by: Claude <noreply@anthropic.com>
1 parent daf17c4 commit 594e45c

File tree

4 files changed

+106
-27
lines changed

4 files changed

+106
-27
lines changed

api/src/main/java/org/cardanofoundation/rosetta/api/network/service/PeerSnapshotServiceImpl.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.nio.file.Path;
1515
import java.nio.file.Paths;
1616
import java.util.ArrayList;
17+
import java.util.Collections;
1718
import java.util.List;
1819
import java.util.Map;
1920

@@ -25,6 +26,12 @@
2526
@Slf4j
2627
public class PeerSnapshotServiceImpl implements PeerSnapshotService {
2728

29+
/**
30+
* Maximum number of peers to return. This limit prevents returning excessive peer lists
31+
* and ensures randomization of peer selection rather than always returning the same peers.
32+
*/
33+
private static final int MAX_PEERS = 25;
34+
2835
@Override
2936
public List<Peer> loadPeersFromSnapshot(@NotNull String peerSnapshotFile, @NotNull String baseDirectory) {
3037
try {
@@ -70,10 +77,19 @@ private PeerSnapshotConfig parsePeerSnapshot(String snapshotFilePath) throws IOE
7077
}
7178

7279
private List<Peer> extractPeersFromSnapshot(PeerSnapshotConfig peerSnapshot) {
73-
return peerSnapshot.getBigLedgerPools().stream()
80+
List<Peer> allPeers = peerSnapshot.getBigLedgerPools().stream()
7481
.flatMap(pool -> pool.getRelays().stream())
7582
.map(this::mapRelayToPeer)
7683
.toList();
84+
85+
// Shuffle to randomize peer selection and limit to MAX_PEERS
86+
// This ensures we don't always return the same peers
87+
List<Peer> shuffledPeers = new ArrayList<>(allPeers);
88+
Collections.shuffle(shuffledPeers);
89+
90+
return shuffledPeers.stream()
91+
.limit(MAX_PEERS)
92+
.toList();
7793
}
7894

7995
@NotNull

api/src/test/java/org/cardanofoundation/rosetta/api/network/service/PeerSnapshotServiceImplTest.java

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ void shouldLoadPeersFromMainnetSnapshot() {
3636
// then
3737
assertNotNull(peers);
3838
assertThat(peers).isNotEmpty();
39-
assertThat(peers.size()).isGreaterThan(100); // Mainnet has many big ledger pools
39+
assertThat(peers.size()).isLessThanOrEqualTo(25); // Limited to MAX_PEERS (25)
40+
assertThat(peers.size()).isEqualTo(25); // Should have exactly 25 peers if snapshot has enough
4041
}
4142

4243
@Test
@@ -56,11 +57,18 @@ void shouldExtractDomainRelaysCorrectly() {
5657
})
5758
.toList();
5859

60+
// Due to randomization, we can't guarantee specific domains are present
61+
// Just verify that domain peers exist and have correct format
5962
assertThat(domainPeers).isNotEmpty();
6063

61-
// Verify at least one known domain relay
62-
assertThat(domainPeers)
63-
.anyMatch(peer -> peer.getPeerId().contains("cardano.figment.io"));
64+
domainPeers.forEach(peer -> {
65+
// Domain peers should have format address:port
66+
assertThat(peer.getPeerId()).contains(":");
67+
68+
// Metadata should indicate domain type
69+
Map<String, Object> metadata = (Map<String, Object>) peer.getMetadata();
70+
assertThat(metadata.get("type")).isEqualTo("domain");
71+
});
6472
}
6573

6674
@Test
@@ -182,13 +190,19 @@ void shouldExtractMultipleRelaysFromSinglePool() {
182190
List<Peer> peers = peerSnapshotService.loadPeersFromSnapshot(peerSnapshotFile, baseDirectory);
183191

184192
// then
185-
// Nordic pool has multiple relays (Relay1-6.NordicPool.org)
186-
List<Peer> nordicPoolRelays = peers.stream()
187-
.filter(peer -> peer.getPeerId().contains("NordicPool.org"))
188-
.toList();
193+
// Due to randomization, we can't guarantee specific pools are in the selection
194+
// Verify we have peers limited to MAX_PEERS
195+
assertThat(peers).hasSizeLessThanOrEqualTo(25);
196+
assertThat(peers).isNotEmpty();
189197

190-
assertThat(nordicPoolRelays).isNotEmpty();
191-
assertThat(nordicPoolRelays.size()).isGreaterThan(1);
198+
// Verify all peers have valid format (address:port)
199+
peers.forEach(peer -> {
200+
assertThat(peer.getPeerId()).contains(":");
201+
202+
// All peers should have metadata with type
203+
Map<String, Object> metadata = (Map<String, Object>) peer.getMetadata();
204+
assertThat(metadata).containsKey("type");
205+
});
192206
}
193207

194208
@Test
@@ -216,15 +230,20 @@ void shouldVerifyKnownMainnetRelays() {
216230
List<Peer> peers = peerSnapshotService.loadPeersFromSnapshot(peerSnapshotFile, baseDirectory);
217231

218232
// then
219-
List<String> peerIds = peers.stream()
220-
.map(Peer::getPeerId)
221-
.toList();
233+
// Due to randomization, we can't guarantee specific relays are present
234+
// Instead verify that all peers have valid format and metadata
235+
assertThat(peers).isNotEmpty();
236+
assertThat(peers.size()).isEqualTo(25);
222237

223-
// Verify some known mainnet relays exist
224-
assertThat(peerIds)
225-
.anyMatch(id -> id.contains("cardano.figment.io"))
226-
.anyMatch(id -> id.contains("NordicPool.org"))
227-
.anyMatch(id -> id.contains("cardanosuisse.com"));
238+
peers.forEach(peer -> {
239+
// All peers should have a peer ID with port
240+
assertThat(peer.getPeerId()).contains(":");
241+
242+
// All peers should have metadata with type
243+
Map<String, Object> metadata = (Map<String, Object>) peer.getMetadata();
244+
assertThat(metadata).containsKey("type");
245+
assertThat(metadata.get("type")).isIn("domain", "IPv4", "IPv6");
246+
});
228247
}
229248

230249
@Test
@@ -237,9 +256,9 @@ void shouldExtractAllRelaysFromAllPools() {
237256
List<Peer> peers = peerSnapshotService.loadPeersFromSnapshot(peerSnapshotFile, baseDirectory);
238257

239258
// then
240-
// Mainnet snapshot has many pools with multiple relays each
241-
// Total should be significantly higher than the number of pools
242-
assertThat(peers.size()).isGreaterThan(200);
259+
// Peers are now limited to MAX_PEERS (25) and randomized
260+
assertThat(peers.size()).isLessThanOrEqualTo(25);
261+
assertThat(peers.size()).isEqualTo(25); // Should have exactly 25 peers
243262
}
244263

245264
@Test
@@ -290,5 +309,31 @@ void shouldVerifyCommonPorts() {
290309
assertThat(ports)
291310
.contains("3001", "6000");
292311
}
312+
313+
@Test
314+
void shouldRandomizePeerSelection() {
315+
// given
316+
String peerSnapshotFile = "peer-snapshot.json";
317+
String baseDirectory = "../config/node/mainnet";
318+
319+
// when - load peers multiple times
320+
List<Peer> firstLoad = peerSnapshotService.loadPeersFromSnapshot(peerSnapshotFile, baseDirectory);
321+
List<Peer> secondLoad = peerSnapshotService.loadPeersFromSnapshot(peerSnapshotFile, baseDirectory);
322+
323+
// then - both should have 25 peers
324+
assertThat(firstLoad.size()).isEqualTo(25);
325+
assertThat(secondLoad.size()).isEqualTo(25);
326+
327+
// And they should be different due to randomization (very high probability)
328+
// We check if at least 5 peers are different in order
329+
long differentPeers = 0;
330+
for (int i = 0; i < Math.min(firstLoad.size(), secondLoad.size()); i++) {
331+
if (!firstLoad.get(i).getPeerId().equals(secondLoad.get(i).getPeerId())) {
332+
differentPeers++;
333+
}
334+
}
335+
336+
assertThat(differentPeers).isGreaterThan(5);
337+
}
293338
}
294339
}

yaci-indexer/src/main/java/org/cardanofoundation/rosetta/yaciindexer/service/PeerDiscoveryManager.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
import reactor.core.publisher.Mono;
1010

1111
import java.time.Duration;
12+
import java.util.ArrayList;
13+
import java.util.Collections;
1214
import java.util.List;
1315
import java.util.concurrent.CopyOnWriteArrayList;
1416

1517
@Service
1618
@Slf4j
1719
public class PeerDiscoveryManager {
1820

19-
public static final int PEERS_REQUEST_AMOUNT = 50;
21+
/**
22+
* Maximum number of peers to request from the Cardano node and cache.
23+
* Limited to avoid excessive peer lists and allow randomization.
24+
*/
25+
public static final int PEERS_REQUEST_AMOUNT = 25;
2026
public static final int PEERS_REQUEST_TIMEOUT_SECS = 60;
2127

2228
@Value("${store.cardano.host:preprod-node.world.dev.cardano.org}")
@@ -32,10 +38,20 @@ public class PeerDiscoveryManager {
3238
private final CopyOnWriteArrayList<PeerAddress> cachedPeers = new CopyOnWriteArrayList<>();
3339

3440
public void updateCachedPeers(List<PeerAddress> peers) {
41+
// Shuffle to randomize peer selection and limit to PEERS_REQUEST_AMOUNT
42+
// This ensures we don't always return the same peers
43+
List<PeerAddress> shuffledPeers = new ArrayList<>(peers);
44+
Collections.shuffle(shuffledPeers);
45+
46+
List<PeerAddress> limitedPeers = shuffledPeers.stream()
47+
.limit(PEERS_REQUEST_AMOUNT)
48+
.toList();
49+
3550
this.cachedPeers.clear();
36-
this.cachedPeers.addAll(peers);
51+
this.cachedPeers.addAll(limitedPeers);
3752

38-
log.debug("Updated cached peers: {} peers available", this.cachedPeers.size());
53+
log.debug("Updated cached peers: {} peers available (from {} discovered)",
54+
this.cachedPeers.size(), peers.size());
3955
}
4056

4157
public List<PeerAddress> discoverPeers() {

yaci-indexer/src/test/java/org/cardanofoundation/rosetta/yaciindexer/service/PeerDiscoveryManagerTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ void shouldUpdateCachedPeers() {
6262
// Then
6363
List<PeerAddress> cachedPeers = peerDiscoveryManager.getCachedPeers();
6464
assertThat(cachedPeers).hasSize(3);
65-
assertThat(cachedPeers).containsExactlyElementsOf(newPeers);
65+
// Peers are shuffled, so we can't check for exact order
66+
assertThat(cachedPeers).containsExactlyInAnyOrderElementsOf(newPeers);
6667
}
6768

6869
@Test
@@ -85,7 +86,8 @@ void shouldClearAndReplaceExistingPeers() {
8586
// Then
8687
List<PeerAddress> cachedPeers = peerDiscoveryManager.getCachedPeers();
8788
assertThat(cachedPeers).hasSize(2);
88-
assertThat(cachedPeers).containsExactlyElementsOf(newPeers);
89+
// Peers are shuffled, so we can't check for exact order
90+
assertThat(cachedPeers).containsExactlyInAnyOrderElementsOf(newPeers);
8991
assertThat(cachedPeers).doesNotContainAnyElementsOf(initialPeers);
9092
}
9193

0 commit comments

Comments
 (0)