mx-core 0.1.0

Core utilities for MultiversX Rust services.
Documentation
//! Storage-layer P2P topic naming (request/response pattern).
//!
//! These topics use a different naming convention from gossip topics.
//! They follow the pattern: `{baseTopic}_{shardId}` with optional `_REQUEST` suffix.
//!
//! Currently scaffolded for mx-state sync; not yet wired into consumers.
#![allow(dead_code)]

use crate::constants::METACHAIN_SHARD_ID;

/// Base topic names for storage data types.
pub mod names {
    pub const TRIE_NODES: &str = "trieNodes";
    pub const TRANSACTIONS: &str = "transactionTopic";
    pub const UNSIGNED_TRANSACTIONS: &str = "unsignedTransactionTopic";
    pub const MINI_BLOCKS: &str = "miniBlockTopic";
    pub const SHARD_BLOCKS: &str = "blockTopic";
    pub const META_BLOCKS: &str = "metaBlockTopic";
    pub const VALIDATOR_INFO: &str = "validatorInfoTopic";
    pub const PEER_AUTH: &str = "peerAuthenticationTopic";
    pub const HEARTBEAT: &str = "heartbeat";
}

/// Request topic suffix.
pub const REQUEST_SUFFIX: &str = "_REQUEST";

/// Topic builder for constructing storage P2P topic names.
#[derive(Debug, Clone)]
pub struct TopicBuilder {
    chain_id: String,
}

impl TopicBuilder {
    pub fn new(chain_id: impl Into<String>) -> Self {
        Self {
            chain_id: chain_id.into(),
        }
    }

    pub fn topic_for_shard(&self, base_topic: &str, shard_id: u32) -> String {
        format!("{}_{}", base_topic, shard_identifier(shard_id))
    }

    pub fn request_topic_for_shard(&self, base_topic: &str, shard_id: u32) -> String {
        format!(
            "{}{REQUEST_SUFFIX}",
            self.topic_for_shard(base_topic, shard_id)
        )
    }

    pub fn trie_nodes_topic(&self, shard_id: u32) -> String {
        self.topic_for_shard(names::TRIE_NODES, shard_id)
    }

    pub fn trie_nodes_request_topic(&self, shard_id: u32) -> String {
        self.request_topic_for_shard(names::TRIE_NODES, shard_id)
    }

    pub fn transaction_topic(&self, sender_shard: u32, receiver_shard: u32) -> String {
        format!(
            "{}_{}_{}",
            names::TRANSACTIONS,
            shard_identifier(sender_shard),
            shard_identifier(receiver_shard)
        )
    }

    pub fn mini_blocks_topic(&self, shard_id: u32) -> String {
        self.topic_for_shard(names::MINI_BLOCKS, shard_id)
    }

    pub fn shard_blocks_topic(&self, shard_id: u32) -> String {
        self.topic_for_shard(names::SHARD_BLOCKS, shard_id)
    }

    pub fn meta_blocks_topic(&self) -> String {
        self.topic_for_shard(names::META_BLOCKS, METACHAIN_SHARD_ID)
    }

    pub fn all_trie_node_topics(&self, shard_ids: &[u32]) -> Vec<String> {
        shard_ids
            .iter()
            .map(|&id| self.trie_nodes_topic(id))
            .collect()
    }

    pub fn chain_id(&self) -> &str {
        &self.chain_id
    }
}

/// Pre-built request topic pair for easy use.
#[derive(Debug, Clone)]
pub struct RequestTopic {
    pub broadcast: String,
    pub request: String,
    pub shard_id: u32,
}

impl RequestTopic {
    pub fn new(base_topic: &str, shard_id: u32) -> Self {
        let builder = TopicBuilder::new("");
        Self {
            broadcast: builder.topic_for_shard(base_topic, shard_id),
            request: builder.request_topic_for_shard(base_topic, shard_id),
            shard_id,
        }
    }

    pub fn trie_nodes(shard_id: u32) -> Self {
        Self::new(names::TRIE_NODES, shard_id)
    }

    pub fn mini_blocks(shard_id: u32) -> Self {
        Self::new(names::MINI_BLOCKS, shard_id)
    }

    pub fn shard_blocks(shard_id: u32) -> Self {
        Self::new(names::SHARD_BLOCKS, shard_id)
    }
}

/// Converts a shard ID to its string representation.
fn shard_identifier(shard_id: u32) -> String {
    if shard_id == METACHAIN_SHARD_ID {
        "META".to_string()
    } else {
        shard_id.to_string()
    }
}

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

    #[test]
    fn test_topic_for_shard() {
        let builder = TopicBuilder::new("1");
        assert_eq!(builder.trie_nodes_topic(0), "trieNodes_0");
        assert_eq!(builder.trie_nodes_topic(1), "trieNodes_1");
        assert_eq!(
            builder.trie_nodes_topic(METACHAIN_SHARD_ID),
            "trieNodes_META"
        );
    }

    #[test]
    fn test_request_topic() {
        let builder = TopicBuilder::new("1");
        assert_eq!(builder.trie_nodes_request_topic(0), "trieNodes_0_REQUEST");
        assert_eq!(
            builder.trie_nodes_request_topic(METACHAIN_SHARD_ID),
            "trieNodes_META_REQUEST"
        );
    }

    #[test]
    fn test_transaction_topic() {
        let builder = TopicBuilder::new("1");
        assert_eq!(builder.transaction_topic(0, 1), "transactionTopic_0_1");
        assert_eq!(
            builder.transaction_topic(0, METACHAIN_SHARD_ID),
            "transactionTopic_0_META"
        );
    }

    #[test]
    fn test_request_topic_struct() {
        let topic = RequestTopic::trie_nodes(0);
        assert_eq!(topic.broadcast, "trieNodes_0");
        assert_eq!(topic.request, "trieNodes_0_REQUEST");
        assert_eq!(topic.shard_id, 0);
    }
}