host-chain-core 0.3.3

WASM-compatible DotNS resolution, IPFS fetching, and CAR parsing (async, reqwest + ruzstd)
Documentation
//! Chain registry — maps genesis hashes to chain identifiers.
//!
//! Provides a lookup table populated from compile-time known chains, and
//! supports runtime insertion for chains whose genesis hashes are discovered
//! dynamically (e.g. testnets that may reset).

use crate::chain::{ChainId, ConnectionBackend, GenesisHash};
use std::collections::HashMap;

/// A single entry in the [`ChainRegistry`].
///
/// Contains the full chain configuration. All string fields use `String` (not
/// `&'static str`) so that runtime-provided values for custom chains work
/// without lifetime constraints.
#[derive(Debug, Clone)]
pub struct ChainRegistryEntry {
    pub id: ChainId,
    pub genesis_hash: GenesisHash,
    /// Human-readable chain name for display in UIs.
    pub display_name: String,
    /// WebSocket RPC endpoint URL. Empty for non-RPC backends (ETH, BTC).
    pub endpoint: String,
    /// Which connection backend to use for this chain.
    pub backend: ConnectionBackend,
    /// Database key shared by all parachains on the same relay chain.
    pub relay_db_key: String,
    /// Database key unique to this parachain's state.
    pub para_db_key: String,
    /// Smoldot chain specs as `(relay_spec, para_spec)`. `None` for chains
    /// that use RPC, Kyoto, or Helios backends.
    pub chain_specs: Option<(String, String)>,
}

/// Registry mapping genesis hashes to [`ChainId`] values.
///
/// Use [`ChainRegistry::from_known_chains`] to build a registry pre-populated
/// with all chains whose genesis hashes are known at compile time.  Additional
/// chains (e.g. testnets) can be added at runtime with [`ChainRegistry::insert`].
#[derive(Debug, Default)]
pub struct ChainRegistry {
    entries: Vec<ChainRegistryEntry>,
    by_hash: HashMap<GenesisHash, ChainId>,
}

impl ChainRegistry {
    /// Build a registry containing every chain that has a compile-time known
    /// genesis hash (i.e. `ChainId::genesis_hash()` returns `Some`).
    ///
    /// Only chains with known genesis hashes get full entries here. Testnets
    /// without compile-time genesis hashes are inserted at runtime after
    /// connecting, via [`ChainRegistry::insert`] or [`ChainRegistry::insert_entry`].
    pub fn from_known_chains() -> Self {
        let mut registry = Self::default();
        for &id in ChainId::all() {
            if let Some(hash) = id.genesis_hash() {
                registry.insert_entry(ChainRegistryEntry {
                    id,
                    genesis_hash: hash,
                    display_name: id.display_name().to_string(),
                    endpoint: id.endpoint().to_string(),
                    backend: id.backend(),
                    relay_db_key: id.relay_db_key().to_string(),
                    para_db_key: id.para_db_key().to_string(),
                    chain_specs: id
                        .chain_specs()
                        .map(|(r, p)| (r.to_string(), p.to_string())),
                });
            }
        }
        registry
    }

    /// Insert a fully populated [`ChainRegistryEntry`] into the registry.
    ///
    /// Deduplicates by both genesis hash and ChainId: if the same ChainId was
    /// previously registered with a different hash (e.g. testnet reset), the
    /// old entry is removed. If the same hash was registered under a different
    /// ChainId, that entry is also removed.
    pub fn insert_entry(&mut self, entry: ChainRegistryEntry) {
        let id = entry.id;
        let genesis_hash = entry.genesis_hash;
        // Remove any existing entry for this ChainId (different hash after reset).
        self.by_hash.retain(|_, v| *v != id);
        // Remove any existing entry for this hash (different ChainId).
        self.entries
            .retain(|e| e.id != id && e.genesis_hash != genesis_hash);
        self.entries.push(entry);
        self.by_hash.insert(genesis_hash, id);
    }

    /// Insert a chain into the registry with minimal data.
    ///
    /// Convenience method for runtime-discovered chains (e.g. testnets) where
    /// only the genesis hash is known at insertion time. Configuration fields
    /// are populated from [`ChainId`] methods. Prefer [`ChainRegistry::insert_entry`]
    /// when you have a fully constructed entry.
    ///
    /// Deduplicates by both genesis hash and ChainId: if the same ChainId was
    /// previously registered with a different hash (e.g. testnet reset), the
    /// old entry is removed. If the same hash was registered under a different
    /// ChainId, that entry is also removed.
    pub fn insert(&mut self, id: ChainId, genesis_hash: GenesisHash) {
        self.insert_entry(ChainRegistryEntry {
            id,
            genesis_hash,
            display_name: id.display_name().to_string(),
            endpoint: id.endpoint().to_string(),
            backend: id.backend(),
            relay_db_key: id.relay_db_key().to_string(),
            para_db_key: id.para_db_key().to_string(),
            chain_specs: id
                .chain_specs()
                .map(|(r, p)| (r.to_string(), p.to_string())),
        });
    }

    /// Look up a chain by its genesis hash.
    ///
    /// Returns `None` if the hash is not registered.
    pub fn by_genesis_hash(&self, hash: &GenesisHash) -> Option<ChainId> {
        self.by_hash.get(hash).copied()
    }

    /// Return all genesis hashes currently in the registry.
    pub fn genesis_hashes(&self) -> Vec<GenesisHash> {
        self.entries.iter().map(|e| e.genesis_hash).collect()
    }

    /// Return all entries in insertion order.
    pub fn entries(&self) -> &[ChainRegistryEntry] {
        &self.entries
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chain::GENESIS_POLKADOT_ASSET_HUB;

    #[test]
    fn test_from_known_chains_includes_polkadot_asset_hub() {
        let registry = ChainRegistry::from_known_chains();
        let ids: Vec<ChainId> = registry.entries().iter().map(|e| e.id).collect();
        assert!(
            ids.contains(&ChainId::PolkadotAssetHub),
            "registry must contain PolkadotAssetHub"
        );
    }

    #[test]
    fn test_by_genesis_hash_returns_correct_chain() {
        let registry = ChainRegistry::from_known_chains();
        let result = registry.by_genesis_hash(&GENESIS_POLKADOT_ASSET_HUB);
        assert_eq!(result, Some(ChainId::PolkadotAssetHub));
    }

    #[test]
    fn test_by_genesis_hash_returns_none_for_unknown() {
        let registry = ChainRegistry::from_known_chains();
        let unknown: GenesisHash = [0xde; 32];
        assert_eq!(registry.by_genesis_hash(&unknown), None);
    }

    #[test]
    fn test_insert_and_lookup() {
        let mut registry = ChainRegistry::default();
        let hash: GenesisHash = [0x01; 32];
        registry.insert(ChainId::PaseoAssetHub, hash);

        assert_eq!(
            registry.by_genesis_hash(&hash),
            Some(ChainId::PaseoAssetHub)
        );
        assert_eq!(registry.entries().len(), 1);

        // Same hash, different ChainId → overwrites.
        registry.insert(ChainId::PaseoPeople, hash);
        assert_eq!(registry.entries().len(), 1);
        assert_eq!(registry.by_genesis_hash(&hash), Some(ChainId::PaseoPeople));
    }

    #[test]
    fn test_insert_same_chain_id_different_hash_replaces() {
        let mut registry = ChainRegistry::default();
        let hash_a: GenesisHash = [0xAA; 32];
        let hash_b: GenesisHash = [0xBB; 32];

        registry.insert(ChainId::PaseoAssetHub, hash_a);
        assert_eq!(
            registry.by_genesis_hash(&hash_a),
            Some(ChainId::PaseoAssetHub)
        );

        // Testnet reset: same ChainId, new genesis hash.
        registry.insert(ChainId::PaseoAssetHub, hash_b);
        assert_eq!(registry.entries().len(), 1, "old entry must be removed");
        assert_eq!(
            registry.by_genesis_hash(&hash_b),
            Some(ChainId::PaseoAssetHub)
        );
        assert_eq!(
            registry.by_genesis_hash(&hash_a),
            None,
            "old hash must be gone"
        );
    }

    #[test]
    fn test_genesis_hashes_returns_all() {
        let mut registry = ChainRegistry::default();
        let hash_a: GenesisHash = [0xAA; 32];
        let hash_b: GenesisHash = [0xBB; 32];
        registry.insert(ChainId::PaseoAssetHub, hash_a);
        registry.insert(ChainId::PaseoPeople, hash_b);

        let hashes = registry.genesis_hashes();
        assert_eq!(hashes.len(), 2);
        assert!(hashes.contains(&hash_a));
        assert!(hashes.contains(&hash_b));
    }

    #[test]
    fn test_empty_registry() {
        let registry = ChainRegistry::default();
        assert!(registry.entries().is_empty());
        assert!(registry.genesis_hashes().is_empty());
        assert_eq!(registry.by_genesis_hash(&[0u8; 32]), None);
    }

    #[test]
    fn test_entry_has_display_name_and_endpoint() {
        let registry = ChainRegistry::from_known_chains();
        let entry = registry
            .entries()
            .iter()
            .find(|e| e.id == ChainId::PolkadotAssetHub)
            .expect("PolkadotAssetHub must be present in from_known_chains()");

        assert_eq!(entry.display_name, "Polkadot Asset Hub");
        assert_eq!(entry.endpoint, "wss://polkadot-asset-hub-rpc.polkadot.io");
    }

    #[test]
    fn test_entry_has_chain_specs_for_smoldot_chains() {
        let registry = ChainRegistry::from_known_chains();
        let entry = registry
            .entries()
            .iter()
            .find(|e| e.id == ChainId::PolkadotAssetHub)
            .expect("PolkadotAssetHub must be present in from_known_chains()");

        // PolkadotAssetHub uses smoldot — chain_specs must be Some.
        assert!(
            entry.chain_specs.is_some(),
            "PolkadotAssetHub entry must carry chain_specs for smoldot backend"
        );
        let (relay_spec, para_spec) = entry.chain_specs.as_ref().unwrap();
        assert!(!relay_spec.is_empty(), "relay chain spec must not be empty");
        assert!(!para_spec.is_empty(), "para chain spec must not be empty");
    }

    #[test]
    fn test_entry_relay_db_key_matches_legacy() {
        let registry = ChainRegistry::from_known_chains();
        let entry = registry
            .entries()
            .iter()
            .find(|e| e.id == ChainId::PolkadotAssetHub)
            .expect("PolkadotAssetHub must be present in from_known_chains()");

        // Entry's relay_db_key must match the value returned by ChainId::relay_db_key().
        assert_eq!(
            entry.relay_db_key,
            ChainId::PolkadotAssetHub.relay_db_key(),
            "ChainRegistryEntry.relay_db_key must match ChainId::relay_db_key()"
        );
        assert_eq!(entry.relay_db_key, "polkadot-relay");
    }
}