Skip to main content

host_chain_core/
registry.rs

1//! Chain registry — maps genesis hashes to chain identifiers.
2//!
3//! Provides a lookup table populated from compile-time known chains, and
4//! supports runtime insertion for chains whose genesis hashes are discovered
5//! dynamically (e.g. testnets that may reset).
6
7use crate::chain::{ChainId, ConnectionBackend, GenesisHash};
8use std::collections::HashMap;
9
10/// A single entry in the [`ChainRegistry`].
11///
12/// Contains the full chain configuration. All string fields use `String` (not
13/// `&'static str`) so that runtime-provided values for custom chains work
14/// without lifetime constraints.
15#[derive(Debug, Clone)]
16pub struct ChainRegistryEntry {
17    pub id: ChainId,
18    pub genesis_hash: GenesisHash,
19    /// Human-readable chain name for display in UIs.
20    pub display_name: String,
21    /// WebSocket RPC endpoint URL. Empty for non-RPC backends (ETH, BTC).
22    pub endpoint: String,
23    /// Which connection backend to use for this chain.
24    pub backend: ConnectionBackend,
25    /// Database key shared by all parachains on the same relay chain.
26    pub relay_db_key: String,
27    /// Database key unique to this parachain's state.
28    pub para_db_key: String,
29    /// Smoldot chain specs as `(relay_spec, para_spec)`. `None` for chains
30    /// that use RPC, Kyoto, or Helios backends.
31    pub chain_specs: Option<(String, String)>,
32}
33
34/// Registry mapping genesis hashes to [`ChainId`] values.
35///
36/// Use [`ChainRegistry::from_known_chains`] to build a registry pre-populated
37/// with all chains whose genesis hashes are known at compile time.  Additional
38/// chains (e.g. testnets) can be added at runtime with [`ChainRegistry::insert`].
39#[derive(Debug, Default)]
40pub struct ChainRegistry {
41    entries: Vec<ChainRegistryEntry>,
42    by_hash: HashMap<GenesisHash, ChainId>,
43}
44
45impl ChainRegistry {
46    /// Build a registry containing every chain that has a compile-time known
47    /// genesis hash (i.e. `ChainId::genesis_hash()` returns `Some`).
48    ///
49    /// Only chains with known genesis hashes get full entries here. Testnets
50    /// without compile-time genesis hashes are inserted at runtime after
51    /// connecting, via [`ChainRegistry::insert`] or [`ChainRegistry::insert_entry`].
52    pub fn from_known_chains() -> Self {
53        let mut registry = Self::default();
54        for &id in ChainId::all() {
55            if let Some(hash) = id.genesis_hash() {
56                registry.insert_entry(ChainRegistryEntry {
57                    id,
58                    genesis_hash: hash,
59                    display_name: id.display_name().to_string(),
60                    endpoint: id.endpoint().to_string(),
61                    backend: id.backend(),
62                    relay_db_key: id.relay_db_key().to_string(),
63                    para_db_key: id.para_db_key().to_string(),
64                    chain_specs: id
65                        .chain_specs()
66                        .map(|(r, p)| (r.to_string(), p.to_string())),
67                });
68            }
69        }
70        registry
71    }
72
73    /// Insert a fully populated [`ChainRegistryEntry`] into the registry.
74    ///
75    /// Deduplicates by both genesis hash and ChainId: if the same ChainId was
76    /// previously registered with a different hash (e.g. testnet reset), the
77    /// old entry is removed. If the same hash was registered under a different
78    /// ChainId, that entry is also removed.
79    pub fn insert_entry(&mut self, entry: ChainRegistryEntry) {
80        let id = entry.id;
81        let genesis_hash = entry.genesis_hash;
82        // Remove any existing entry for this ChainId (different hash after reset).
83        self.by_hash.retain(|_, v| *v != id);
84        // Remove any existing entry for this hash (different ChainId).
85        self.entries
86            .retain(|e| e.id != id && e.genesis_hash != genesis_hash);
87        self.entries.push(entry);
88        self.by_hash.insert(genesis_hash, id);
89    }
90
91    /// Insert a chain into the registry with minimal data.
92    ///
93    /// Convenience method for runtime-discovered chains (e.g. testnets) where
94    /// only the genesis hash is known at insertion time. Configuration fields
95    /// are populated from [`ChainId`] methods. Prefer [`ChainRegistry::insert_entry`]
96    /// when you have a fully constructed entry.
97    ///
98    /// Deduplicates by both genesis hash and ChainId: if the same ChainId was
99    /// previously registered with a different hash (e.g. testnet reset), the
100    /// old entry is removed. If the same hash was registered under a different
101    /// ChainId, that entry is also removed.
102    pub fn insert(&mut self, id: ChainId, genesis_hash: GenesisHash) {
103        self.insert_entry(ChainRegistryEntry {
104            id,
105            genesis_hash,
106            display_name: id.display_name().to_string(),
107            endpoint: id.endpoint().to_string(),
108            backend: id.backend(),
109            relay_db_key: id.relay_db_key().to_string(),
110            para_db_key: id.para_db_key().to_string(),
111            chain_specs: id
112                .chain_specs()
113                .map(|(r, p)| (r.to_string(), p.to_string())),
114        });
115    }
116
117    /// Look up a chain by its genesis hash.
118    ///
119    /// Returns `None` if the hash is not registered.
120    pub fn by_genesis_hash(&self, hash: &GenesisHash) -> Option<ChainId> {
121        self.by_hash.get(hash).copied()
122    }
123
124    /// Return all genesis hashes currently in the registry.
125    pub fn genesis_hashes(&self) -> Vec<GenesisHash> {
126        self.entries.iter().map(|e| e.genesis_hash).collect()
127    }
128
129    /// Return all entries in insertion order.
130    pub fn entries(&self) -> &[ChainRegistryEntry] {
131        &self.entries
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::chain::GENESIS_POLKADOT_ASSET_HUB;
139
140    #[test]
141    fn test_from_known_chains_includes_polkadot_asset_hub() {
142        let registry = ChainRegistry::from_known_chains();
143        let ids: Vec<ChainId> = registry.entries().iter().map(|e| e.id).collect();
144        assert!(
145            ids.contains(&ChainId::PolkadotAssetHub),
146            "registry must contain PolkadotAssetHub"
147        );
148    }
149
150    #[test]
151    fn test_by_genesis_hash_returns_correct_chain() {
152        let registry = ChainRegistry::from_known_chains();
153        let result = registry.by_genesis_hash(&GENESIS_POLKADOT_ASSET_HUB);
154        assert_eq!(result, Some(ChainId::PolkadotAssetHub));
155    }
156
157    #[test]
158    fn test_by_genesis_hash_returns_none_for_unknown() {
159        let registry = ChainRegistry::from_known_chains();
160        let unknown: GenesisHash = [0xde; 32];
161        assert_eq!(registry.by_genesis_hash(&unknown), None);
162    }
163
164    #[test]
165    fn test_insert_and_lookup() {
166        let mut registry = ChainRegistry::default();
167        let hash: GenesisHash = [0x01; 32];
168        registry.insert(ChainId::PaseoAssetHub, hash);
169
170        assert_eq!(
171            registry.by_genesis_hash(&hash),
172            Some(ChainId::PaseoAssetHub)
173        );
174        assert_eq!(registry.entries().len(), 1);
175
176        // Same hash, different ChainId → overwrites.
177        registry.insert(ChainId::PaseoPeople, hash);
178        assert_eq!(registry.entries().len(), 1);
179        assert_eq!(registry.by_genesis_hash(&hash), Some(ChainId::PaseoPeople));
180    }
181
182    #[test]
183    fn test_insert_same_chain_id_different_hash_replaces() {
184        let mut registry = ChainRegistry::default();
185        let hash_a: GenesisHash = [0xAA; 32];
186        let hash_b: GenesisHash = [0xBB; 32];
187
188        registry.insert(ChainId::PaseoAssetHub, hash_a);
189        assert_eq!(
190            registry.by_genesis_hash(&hash_a),
191            Some(ChainId::PaseoAssetHub)
192        );
193
194        // Testnet reset: same ChainId, new genesis hash.
195        registry.insert(ChainId::PaseoAssetHub, hash_b);
196        assert_eq!(registry.entries().len(), 1, "old entry must be removed");
197        assert_eq!(
198            registry.by_genesis_hash(&hash_b),
199            Some(ChainId::PaseoAssetHub)
200        );
201        assert_eq!(
202            registry.by_genesis_hash(&hash_a),
203            None,
204            "old hash must be gone"
205        );
206    }
207
208    #[test]
209    fn test_genesis_hashes_returns_all() {
210        let mut registry = ChainRegistry::default();
211        let hash_a: GenesisHash = [0xAA; 32];
212        let hash_b: GenesisHash = [0xBB; 32];
213        registry.insert(ChainId::PaseoAssetHub, hash_a);
214        registry.insert(ChainId::PaseoPeople, hash_b);
215
216        let hashes = registry.genesis_hashes();
217        assert_eq!(hashes.len(), 2);
218        assert!(hashes.contains(&hash_a));
219        assert!(hashes.contains(&hash_b));
220    }
221
222    #[test]
223    fn test_empty_registry() {
224        let registry = ChainRegistry::default();
225        assert!(registry.entries().is_empty());
226        assert!(registry.genesis_hashes().is_empty());
227        assert_eq!(registry.by_genesis_hash(&[0u8; 32]), None);
228    }
229
230    #[test]
231    fn test_entry_has_display_name_and_endpoint() {
232        let registry = ChainRegistry::from_known_chains();
233        let entry = registry
234            .entries()
235            .iter()
236            .find(|e| e.id == ChainId::PolkadotAssetHub)
237            .expect("PolkadotAssetHub must be present in from_known_chains()");
238
239        assert_eq!(entry.display_name, "Polkadot Asset Hub");
240        assert_eq!(entry.endpoint, "wss://polkadot-asset-hub-rpc.polkadot.io");
241    }
242
243    #[test]
244    fn test_entry_has_chain_specs_for_smoldot_chains() {
245        let registry = ChainRegistry::from_known_chains();
246        let entry = registry
247            .entries()
248            .iter()
249            .find(|e| e.id == ChainId::PolkadotAssetHub)
250            .expect("PolkadotAssetHub must be present in from_known_chains()");
251
252        // PolkadotAssetHub uses smoldot — chain_specs must be Some.
253        assert!(
254            entry.chain_specs.is_some(),
255            "PolkadotAssetHub entry must carry chain_specs for smoldot backend"
256        );
257        let (relay_spec, para_spec) = entry.chain_specs.as_ref().unwrap();
258        assert!(!relay_spec.is_empty(), "relay chain spec must not be empty");
259        assert!(!para_spec.is_empty(), "para chain spec must not be empty");
260    }
261
262    #[test]
263    fn test_entry_relay_db_key_matches_legacy() {
264        let registry = ChainRegistry::from_known_chains();
265        let entry = registry
266            .entries()
267            .iter()
268            .find(|e| e.id == ChainId::PolkadotAssetHub)
269            .expect("PolkadotAssetHub must be present in from_known_chains()");
270
271        // Entry's relay_db_key must match the value returned by ChainId::relay_db_key().
272        assert_eq!(
273            entry.relay_db_key,
274            ChainId::PolkadotAssetHub.relay_db_key(),
275            "ChainRegistryEntry.relay_db_key must match ChainId::relay_db_key()"
276        );
277        assert_eq!(entry.relay_db_key, "polkadot-relay");
278    }
279}