Skip to content

Commit 2fdbe67

Browse files
Mateusz Czeladkaclaude
andcommitted
fix: add Ouroboros Genesis support for Cardano Node 10.5.1+
Implement support for Cardano Node 10.5.1 which uses Ouroboros Genesis consensus protocol. This replaces bootstrap peers with big ledger peer snapshots for better decentralization. Changes: - Add PeerSnapshotService to load peers from Genesis peer snapshot files - Create model classes for BigLedgerPool, PeerSnapshotConfig, and Relay - Update TopologyConfigServiceImpl to use peer snapshots with bootstrap fallback - Add support for domain, IPv4, IPv6, and address relay formats - Update mainnet, preprod, and preview topology.json to use peer-snapshot.json - Add comprehensive unit tests (16 test cases) using mainnet peer snapshot - Keep devkit with bootstrap peers for backward compatibility The implementation maintains dynamic peer discovery priority: 1. Try dynamically discovered peers from yaci-indexer (when enabled) 2. Fallback to static peers from peer-snapshot.json (Genesis mode) 3. Fallback to bootstrap peers (legacy mode) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent afca350 commit 2fdbe67

File tree

12 files changed

+518
-33
lines changed

12 files changed

+518
-33
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.cardanofoundation.rosetta.api.network.model;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
11+
import com.fasterxml.jackson.annotation.JsonProperty;
12+
13+
@Data
14+
@Builder
15+
@AllArgsConstructor
16+
@NoArgsConstructor
17+
@JsonIgnoreProperties(ignoreUnknown = true)
18+
public class BigLedgerPool {
19+
@JsonProperty("accumulatedStake")
20+
private Double accumulatedStake;
21+
22+
@JsonProperty("relativeStake")
23+
private Double relativeStake;
24+
25+
@JsonProperty("relays")
26+
private List<Relay> relays;
27+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.cardanofoundation.rosetta.api.network.model;
2+
3+
import java.util.List;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
11+
import com.fasterxml.jackson.annotation.JsonProperty;
12+
13+
@Data
14+
@Builder
15+
@AllArgsConstructor
16+
@NoArgsConstructor
17+
@JsonIgnoreProperties(ignoreUnknown = true)
18+
public class PeerSnapshotConfig {
19+
@JsonProperty("bigLedgerPools")
20+
private List<BigLedgerPool> bigLedgerPools;
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.cardanofoundation.rosetta.api.network.model;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
9+
import com.fasterxml.jackson.annotation.JsonProperty;
10+
11+
@Data
12+
@Builder
13+
@AllArgsConstructor
14+
@NoArgsConstructor
15+
@JsonIgnoreProperties(ignoreUnknown = true)
16+
public class Relay {
17+
@JsonProperty("domain")
18+
private String domain;
19+
20+
@JsonProperty("port")
21+
private Integer port;
22+
23+
@JsonProperty("address")
24+
private String address;
25+
}

api/src/main/java/org/cardanofoundation/rosetta/api/network/model/TopologyConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
public class TopologyConfig {
2020
@JsonProperty("Producers")
2121
private List<Producer> producers;
22-
22+
2323
@JsonProperty("publicRoots")
2424
private List<PublicRoot> publicRoots;
25-
25+
2626
@JsonProperty("bootstrapPeers")
2727
private List<BootstrapPeer> bootstrapPeers;
28+
29+
@JsonProperty("peerSnapshotFile")
30+
private String peerSnapshotFile;
2831
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.cardanofoundation.rosetta.api.network.service;
2+
3+
import jakarta.validation.constraints.NotNull;
4+
import java.util.List;
5+
import org.openapitools.client.model.Peer;
6+
7+
/**
8+
* Service for loading peers from Cardano Node Genesis mode peer snapshot files.
9+
* This is used with Cardano Node 10.5.1+ which uses Ouroboros Genesis consensus.
10+
*/
11+
public interface PeerSnapshotService {
12+
13+
/**
14+
* Load peers from a peer snapshot file.
15+
* The file path is resolved relative to the provided base directory.
16+
*
17+
* @param peerSnapshotFile the peer snapshot file name (e.g., "peer-snapshot.json")
18+
* @param baseDirectory the base directory to resolve the file path from
19+
* @return list of peers extracted from the snapshot, or empty list if file doesn't exist or parsing fails
20+
*/
21+
List<Peer> loadPeersFromSnapshot(@NotNull String peerSnapshotFile, @NotNull String baseDirectory);
22+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.cardanofoundation.rosetta.api.network.service;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import jakarta.validation.constraints.NotNull;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.cardanofoundation.rosetta.api.network.model.PeerSnapshotConfig;
7+
import org.cardanofoundation.rosetta.api.network.model.Relay;
8+
import org.cardanofoundation.rosetta.common.util.FileUtils;
9+
import org.openapitools.client.model.Peer;
10+
import org.springframework.stereotype.Service;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.nio.file.Path;
15+
import java.nio.file.Paths;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
/**
21+
* Implementation of PeerSnapshotService for loading peers from Genesis mode peer snapshot files.
22+
* Cardano Node 10.5.1+ uses Ouroboros Genesis which requires peer snapshots instead of bootstrap peers.
23+
*/
24+
@Service
25+
@Slf4j
26+
public class PeerSnapshotServiceImpl implements PeerSnapshotService {
27+
28+
@Override
29+
public List<Peer> loadPeersFromSnapshot(@NotNull String peerSnapshotFile, @NotNull String baseDirectory) {
30+
try {
31+
Path snapshotPath = resolveSnapshotPath(peerSnapshotFile, baseDirectory);
32+
String snapshotFilePath = snapshotPath.toString();
33+
34+
log.debug("[loadPeersFromSnapshot] Loading peer snapshot from: {}", snapshotFilePath);
35+
36+
if (!new File(snapshotFilePath).exists()) {
37+
log.warn("[loadPeersFromSnapshot] Peer snapshot file not found: {}", snapshotFilePath);
38+
return new ArrayList<>();
39+
}
40+
41+
PeerSnapshotConfig peerSnapshot = parsePeerSnapshot(snapshotFilePath);
42+
43+
if (peerSnapshot == null || peerSnapshot.getBigLedgerPools() == null) {
44+
log.warn("[loadPeersFromSnapshot] Peer snapshot is empty or invalid");
45+
return new ArrayList<>();
46+
}
47+
48+
List<Peer> peers = extractPeersFromSnapshot(peerSnapshot);
49+
50+
log.info("[loadPeersFromSnapshot] Extracted {} relays from {} big ledger pools",
51+
peers.size(), peerSnapshot.getBigLedgerPools().size());
52+
53+
return peers;
54+
55+
} catch (IOException e) {
56+
log.error("[loadPeersFromSnapshot] Failed to load peer snapshot from: {}", peerSnapshotFile, e);
57+
return new ArrayList<>();
58+
}
59+
}
60+
61+
private Path resolveSnapshotPath(String peerSnapshotFile, String baseDirectory) {
62+
Path basePath = Paths.get(baseDirectory);
63+
return basePath.resolve(peerSnapshotFile);
64+
}
65+
66+
private PeerSnapshotConfig parsePeerSnapshot(String snapshotFilePath) throws IOException {
67+
ObjectMapper mapper = new ObjectMapper();
68+
String content = FileUtils.fileReader(snapshotFilePath);
69+
return mapper.readValue(content, PeerSnapshotConfig.class);
70+
}
71+
72+
private List<Peer> extractPeersFromSnapshot(PeerSnapshotConfig peerSnapshot) {
73+
return peerSnapshot.getBigLedgerPools().stream()
74+
.flatMap(pool -> pool.getRelays().stream())
75+
.map(this::mapRelayToPeer)
76+
.toList();
77+
}
78+
79+
@NotNull
80+
private Peer mapRelayToPeer(@NotNull Relay relay) {
81+
String address;
82+
String type;
83+
84+
if (relay.getDomain() != null) {
85+
address = relay.getDomain();
86+
type = "domain";
87+
} else {
88+
address = relay.getAddress();
89+
type = isIpv6(address) ? "IPv6" : "IPv4";
90+
}
91+
92+
address = "%s:%d".formatted(address, relay.getPort());
93+
94+
return new Peer(address, Map.of("type", type));
95+
}
96+
97+
private boolean isIpv6(String address) {
98+
return address.contains(":");
99+
}
100+
}

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import javax.annotation.PostConstruct;
1717
import java.io.IOException;
18+
import java.nio.file.Paths;
1819
import java.util.ArrayList;
1920
import java.util.List;
2021
import java.util.Map;
@@ -25,6 +26,7 @@
2526
public class TopologyConfigServiceImpl implements TopologyConfigService {
2627

2728
private final YaciHttpGateway yaciHttpGateway;
29+
private final PeerSnapshotService peerSnapshotService;
2830

2931
@Value("${cardano.rosetta.TOPOLOGY_FILEPATH}")
3032
private String topologyFilepath;
@@ -55,18 +57,31 @@ public List<Peer> getStaticPeers() {
5557
}
5658

5759
public List<Peer> getPeerFromConfig(TopologyConfig topologyConfig) {
58-
log.info("[getPeersFromConfig] Looking for bootstrap peers from topologyFile");
60+
log.info("[getPeersFromConfig] Looking for peers from topology configuration");
5961

6062
if (topologyConfig == null) {
6163
log.warn("Topology config is null");
6264
return new ArrayList<>();
6365
}
6466

65-
// Only read bootstrap peers now
67+
// For Genesis mode (Cardano Node 10.5.1+), try peer snapshot first
68+
String peerSnapshotFile = topologyConfig.getPeerSnapshotFile();
69+
if (peerSnapshotFile != null && !peerSnapshotFile.isEmpty()) {
70+
log.info("[getPeersFromConfig] Peer snapshot file configured: {}", peerSnapshotFile);
71+
String baseDirectory = Paths.get(topologyFilepath).getParent().toString();
72+
List<Peer> peersFromSnapshot = peerSnapshotService.loadPeersFromSnapshot(peerSnapshotFile, baseDirectory);
73+
if (!peersFromSnapshot.isEmpty()) {
74+
log.info("[getPeersFromConfig] Loaded {} peers from peer snapshot", peersFromSnapshot.size());
75+
return peersFromSnapshot;
76+
}
77+
log.warn("[getPeersFromConfig] Failed to load peers from snapshot, falling back to bootstrap peers");
78+
}
79+
80+
// Fallback to bootstrap peers for backward compatibility
6681
List<BootstrapPeer> bootstrapPeers = topologyConfig.getBootstrapPeers();
67-
82+
6883
if (bootstrapPeers == null || bootstrapPeers.isEmpty()) {
69-
log.warn("No bootstrap peers found in topology config");
84+
log.warn("No bootstrap peers or peer snapshot found in topology config");
7085
return new ArrayList<>();
7186
}
7287

0 commit comments

Comments
 (0)