Skip to main content

fabric/
lib.rs

1//! Wire format types for relay P2P communication.
2//!
3//! All protocol types use JSON serialization for cross-language compatibility.
4//! Only the `Message` type from libveritas remains binary (borsh).
5
6pub mod anchor;
7#[cfg(feature = "client")]
8pub mod client;
9pub mod seeds;
10#[cfg(feature = "signing")]
11pub mod signing;
12
13use std::collections::HashMap;
14use std::fmt;
15use std::net::IpAddr;
16use std::str::FromStr;
17
18use serde::{Deserialize, Serialize};
19
20// Re-export the entire libveritas crate
21pub extern crate libveritas;
22// Also re-export Message directly since it's used in the wire format
23pub use libveritas::msg::Message;
24use spaces_nums::RootAnchor;
25
26#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct TrustId([u8; 32]);
28
29impl TrustId {
30    pub fn to_bytes(self) -> [u8; 32] {
31        self.0
32    }
33}
34
35impl fmt::Display for TrustId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}", hex::encode(self.0))
38    }
39}
40
41impl From<[u8; 32]> for TrustId {
42    fn from(bytes: [u8; 32]) -> Self {
43        Self(bytes)
44    }
45}
46
47impl FromStr for TrustId {
48    type Err = hex::FromHexError;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        let bytes: [u8; 32] = hex::decode(s)?
52            .try_into()
53            .map_err(|_| hex::FromHexError::InvalidStringLength)?;
54
55        Ok(Self(bytes))
56    }
57}
58
59/// Capability flags for peers.
60///
61/// Reserved for future use. Capabilities allow peers to advertise
62/// what features they support.
63pub mod capabilities {
64    // No capabilities defined yet
65}
66
67/// A query for certificate data.
68#[derive(Clone, Debug, Serialize, Deserialize)]
69pub struct Query {
70    /// The space to query (e.g., "@bitcoin").
71    pub space: String,
72    /// Handles within the space to query.
73    pub handles: Vec<String>,
74    /// Optional epoch hint for optimizing proof size.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub epoch_hint: Option<EpochHint>,
77}
78
79impl Query {
80    pub fn new(space: impl Into<String>, handles: Vec<String>) -> Self {
81        Self {
82            space: space.into(),
83            handles,
84            epoch_hint: None,
85        }
86    }
87
88    pub fn with_epoch_hint(mut self, hint: EpochHint) -> Self {
89        self.epoch_hint = Some(hint);
90        self
91    }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
95pub struct HintsResponse {
96    pub anchor_tip: u32,
97    pub hints: Vec<SpaceHint>,
98}
99
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct AnchorSet {
102    pub entries: Vec<RootAnchor>,
103}
104
105#[derive(Clone, Debug, Serialize, Deserialize)]
106pub struct SpaceHint {
107    pub epoch_tip: u32,
108    pub name: String,
109    pub seq: u64,
110    pub delegate_seq: u64,
111    pub epochs: Vec<EpochResult>,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct EpochResult {
116    pub epoch: u32,
117    pub res: Vec<HandleHint>,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct HandleHint {
122    pub seq: u64,
123    pub name: String,
124}
125
126impl PartialEq for HintsResponse {
127    fn eq(&self, other: &Self) -> bool {
128        self.cmp(other) == std::cmp::Ordering::Equal
129    }
130}
131
132impl Eq for HintsResponse {}
133
134impl PartialOrd for HintsResponse {
135    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
136        Some(self.cmp(other))
137    }
138}
139
140impl Ord for HintsResponse {
141    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
142        let mut score: i32 = 0;
143
144        for space in &self.hints {
145            let Some(other_space) = other.hints.iter().find(|s| s.name == space.name) else {
146                score += 1;
147                continue;
148            };
149
150            score += cmp_score(space.epoch_tip, other_space.epoch_tip);
151            score += cmp_score(space.seq, other_space.seq);
152            score += cmp_score(space.delegate_seq, other_space.delegate_seq);
153
154            let self_handles = flatten_handles(space);
155            let other_handles = flatten_handles(other_space);
156
157            for (name, self_seq) in &self_handles {
158                match other_handles.get(*name) {
159                    Some(other_seq) => score += cmp_score(*self_seq, *other_seq),
160                    None => score += 1,
161                }
162            }
163            for name in other_handles.keys() {
164                if !self_handles.contains_key(*name) {
165                    score -= 1;
166                }
167            }
168        }
169
170        for other_space in &other.hints {
171            if !self.hints.iter().any(|s| s.name == other_space.name) {
172                score -= 1;
173            }
174        }
175
176        if score != 0 {
177            score.cmp(&0)
178        } else {
179            self.anchor_tip.cmp(&other.anchor_tip)
180        }
181    }
182}
183
184fn cmp_score<T: Ord>(a: T, b: T) -> i32 {
185    match a.cmp(&b) {
186        std::cmp::Ordering::Greater => 1,
187        std::cmp::Ordering::Less => -1,
188        std::cmp::Ordering::Equal => 0,
189    }
190}
191
192fn flatten_handles(space: &SpaceHint) -> HashMap<&str, u64> {
193    let mut map = HashMap::new();
194    for epoch in &space.epochs {
195        for handle in &epoch.res {
196            let existing = map.get(handle.name.as_str()).copied().unwrap_or(0);
197            if handle.seq > existing {
198                map.insert(handle.name.as_str(), handle.seq);
199            }
200        }
201    }
202    map
203}
204
205/// Epoch hint for query optimization.
206///
207/// If the client has a cached epoch root, providing this hint allows
208/// the relay to skip including redundant proofs.
209#[derive(Clone, Debug, Serialize, Deserialize)]
210pub struct EpochHint {
211    /// The merkle root of the cached epoch (hex-encoded).
212    pub root: String,
213    /// The block height of the cached epoch.
214    pub height: u32,
215}
216
217/// Request body for POST /query.
218#[derive(Clone, Debug, Serialize, Deserialize)]
219pub struct QueryRequest {
220    /// The queries to execute.
221    pub queries: Vec<Query>,
222}
223
224impl QueryRequest {
225    pub fn new(queries: Vec<Query>) -> Self {
226        Self { queries }
227    }
228
229    pub fn single(space: impl Into<String>, handles: Vec<String>) -> Self {
230        Self {
231            queries: vec![Query::new(space, handles)],
232        }
233    }
234}
235
236/// Announcement payload for POST /announce.
237///
238/// Sent by a peer to announce itself to another relay.
239#[derive(Clone, Debug, Serialize, Deserialize)]
240pub struct Announcement {
241    /// The URL where this peer can be reached.
242    pub url: String,
243    /// Capability flags indicating what this peer supports.
244    pub capabilities: u32,
245}
246
247impl Announcement {
248    pub fn new(url: impl Into<String>, capabilities: u32) -> Self {
249        Self {
250            url: url.into(),
251            capabilities,
252        }
253    }
254
255    pub fn has_capability(&self, cap: u32) -> bool {
256        self.capabilities & cap != 0
257    }
258}
259
260/// Information about a peer, returned from GET /peers.
261#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct PeerInfo {
263    /// The IP address that announced this peer.
264    pub source_ip: IpAddr,
265    /// The URL where this peer can be reached.
266    pub url: String,
267    /// Capability flags indicating what this peer supports.
268    pub capabilities: u32,
269}
270
271impl PeerInfo {
272    pub fn has_capability(&self, cap: u32) -> bool {
273        self.capabilities & cap != 0
274    }
275}
276
277/// A reverse record mapping a numeric identity to its human-readable name.
278#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct ReverseRecord {
280    pub id: String,
281    pub name: String,
282}
283
284/// Address lookup result — handles claiming an address.
285#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct AddrMatch {
287    pub address: String,
288    pub handles: Vec<AddrEntry>,
289}
290
291/// An entry in an address lookup result.
292#[derive(Clone, Debug, Serialize, Deserialize)]
293pub struct AddrEntry {
294    /// Canonical/flattened handle name.
295    pub handle: String,
296    /// Human-readable reverse name (from Sig record).
297    pub rev: String,
298}
299
300impl AnchorSet {
301    pub fn from_anchors(anchors: Vec<RootAnchor>) -> Self {
302        Self { entries: anchors }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_query_roundtrip() {
312        let query = Query::new("@bitcoin", vec!["alice".into()]);
313        let req = QueryRequest::new(vec![query]);
314
315        let json = serde_json::to_string(&req).unwrap();
316        let decoded: QueryRequest = serde_json::from_str(&json).unwrap();
317
318        assert_eq!(decoded.queries.len(), 1);
319        assert_eq!(decoded.queries[0].space, "@bitcoin");
320        assert_eq!(decoded.queries[0].handles, vec!["alice"]);
321    }
322
323    #[test]
324    fn test_announcement_roundtrip() {
325        let announcement = Announcement::new("https://relay.example.com", 0);
326        let json = serde_json::to_string(&announcement).unwrap();
327        let decoded: Announcement = serde_json::from_str(&json).unwrap();
328
329        assert_eq!(decoded.url, "https://relay.example.com");
330        assert_eq!(decoded.capabilities, 0);
331    }
332
333    #[test]
334    fn test_peer_info_roundtrip() {
335        let peer = PeerInfo {
336            source_ip: "192.168.1.1".parse().unwrap(),
337            url: "https://peer.example.com".to_string(),
338            capabilities: 0,
339        };
340        let json = serde_json::to_string(&peer).unwrap();
341        let decoded: PeerInfo = serde_json::from_str(&json).unwrap();
342
343        assert_eq!(decoded.url, "https://peer.example.com");
344        assert_eq!(decoded.source_ip.to_string(), "192.168.1.1");
345    }
346
347    #[test]
348    fn test_epoch_hint_skipped_when_none() {
349        let query = Query::new("@bitcoin", vec!["alice".into()]);
350        let json = serde_json::to_string(&query).unwrap();
351        assert!(!json.contains("epoch_hint"));
352    }
353}