Skip to main content

p2panda_core/
topic.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Identifiers for gossip- or sync topics.
4use std::fmt::Display;
5use std::hash::Hash as StdHash;
6use std::str::FromStr;
7
8use rand::Rng;
9use rand::rngs::OsRng;
10use thiserror::Error;
11
12use crate::{Hash, VerifyingKey};
13
14pub const TOPIC_LENGTH: usize = 32;
15
16/// Identifier for a gossip- or sync topic.
17///
18/// A topic identifier is required when subscribing or publishing to a stream.
19///
20/// Topics usually describe concrete data which nodes want to exchange over, for example a document
21/// id or chat group id and so forth. Applications usually want to share topics via a secure side
22/// channel.
23///
24/// **WARNING:** Sensitive topics have to be treated like secret values and generated using a
25/// cryptographically secure pseudorandom number generator (CSPRNG). Otherwise they can be easily
26/// guessed by third parties or leaked during discovery.
27#[derive(Clone, Copy, Debug, Ord, PartialOrd, PartialEq, Eq, StdHash)]
28pub struct Topic(pub(crate) [u8; TOPIC_LENGTH]);
29
30impl Topic {
31    pub fn random() -> Self {
32        let mut rng = OsRng;
33        Self::from_rng(&mut rng)
34    }
35
36    pub fn from_rng<R: Rng>(rng: &mut R) -> Self {
37        Self(rng.r#gen())
38    }
39
40    pub fn from_bytes(bytes: &[u8]) -> Result<Self, TopicError> {
41        Self::try_from(bytes)
42    }
43
44    pub fn as_bytes(&self) -> &[u8; TOPIC_LENGTH] {
45        &self.0
46    }
47
48    pub fn to_bytes(self) -> [u8; TOPIC_LENGTH] {
49        self.0
50    }
51
52    pub fn to_hex(&self) -> String {
53        hex::encode(self.0)
54    }
55}
56
57impl Default for Topic {
58    fn default() -> Self {
59        Self::random()
60    }
61}
62
63impl From<[u8; TOPIC_LENGTH]> for Topic {
64    fn from(topic: [u8; TOPIC_LENGTH]) -> Self {
65        Self(topic)
66    }
67}
68
69impl From<Topic> for [u8; TOPIC_LENGTH] {
70    fn from(topic: Topic) -> Self {
71        topic.0
72    }
73}
74
75impl From<Hash> for Topic {
76    fn from(value: Hash) -> Self {
77        Self(*value.as_bytes())
78    }
79}
80
81impl From<Topic> for Hash {
82    fn from(topic: Topic) -> Self {
83        Hash::from_bytes(topic.0)
84    }
85}
86
87impl From<VerifyingKey> for Topic {
88    fn from(value: VerifyingKey) -> Self {
89        Self(*value.as_bytes())
90    }
91}
92
93impl Display for Topic {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "{}", hex::encode(self.0))
96    }
97}
98
99impl FromStr for Topic {
100    type Err = TopicError;
101
102    fn from_str(value: &str) -> Result<Self, Self::Err> {
103        Self::try_from(hex::decode(value)?.as_slice())
104    }
105}
106
107impl TryFrom<&[u8]> for Topic {
108    type Error = TopicError;
109
110    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
111        let value_len = value.len();
112
113        let checked_value: [u8; TOPIC_LENGTH] = value
114            .try_into()
115            .map_err(|_| TopicError::InvalidLength(value_len, TOPIC_LENGTH))?;
116
117        Ok(Self::from(checked_value))
118    }
119}
120
121impl TryFrom<Vec<u8>> for Topic {
122    type Error = TopicError;
123
124    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
125        let value_len = value.len();
126
127        let checked_value: [u8; TOPIC_LENGTH] = value
128            .try_into()
129            .map_err(|_| TopicError::InvalidLength(value_len, TOPIC_LENGTH))?;
130
131        Ok(Self::from(checked_value))
132    }
133}
134
135#[derive(Debug, Error)]
136pub enum TopicError {
137    /// Invalid number of bytes.
138    #[error("invalid bytes length of {0}, expected {1} bytes")]
139    InvalidLength(usize, usize),
140
141    /// String contains invalid hexadecimal characters.
142    #[error("invalid hex encoding in string")]
143    InvalidHexEncoding(#[from] hex::FromHexError),
144}