Skip to main content

hashtree_network/
protocol.rs

1//! Wire protocol for hashtree WebRTC data exchange
2//!
3//! Compatible with hashtree-ts wire format:
4//! - Request:        [0x00][msgpack: {h: bytes32, htl?: u8, q?: u64}]
5//! - Response:       [0x01][msgpack: {h: bytes32, d: bytes, i?: u32, n?: u32}]
6//! - QuoteRequest:   [0x02][msgpack: {h: bytes32, p: u64, t: u32, m?: string}]
7//! - QuoteResponse:  [0x03][msgpack: {h: bytes32, a: bool, q?: u64, p?: u64, t?: u32, m?: string}]
8//!
9//! Fragmented responses include `i` (index) and `n` (total), unfragmented omit them.
10
11use hashtree_core::Hash;
12use serde::{Deserialize, Serialize};
13
14/// Message type bytes (prefix before MessagePack body)
15pub const MSG_TYPE_REQUEST: u8 = 0x00;
16pub const MSG_TYPE_RESPONSE: u8 = 0x01;
17pub const MSG_TYPE_QUOTE_REQUEST: u8 = 0x02;
18pub const MSG_TYPE_QUOTE_RESPONSE: u8 = 0x03;
19
20/// Fragment size for large data (32KB - safe limit for WebRTC)
21pub const FRAGMENT_SIZE: usize = 32 * 1024;
22
23/// Data request message body
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct DataRequest {
26    /// 32-byte hash
27    #[serde(with = "serde_bytes")]
28    pub h: Vec<u8>,
29    /// Hops To Live (optional, defaults to MAX_HTL)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub htl: Option<u8>,
32    /// Optional quote identifier for paid retrieval.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub q: Option<u64>,
35}
36
37/// Data response message body
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct DataResponse {
40    /// 32-byte hash
41    #[serde(with = "serde_bytes")]
42    pub h: Vec<u8>,
43    /// Data (fragment or full)
44    #[serde(with = "serde_bytes")]
45    pub d: Vec<u8>,
46    /// Fragment index (0-based), absent = unfragmented
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub i: Option<u32>,
49    /// Total fragments, absent = unfragmented
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub n: Option<u32>,
52}
53
54/// Quote request message body
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DataQuoteRequest {
57    /// 32-byte hash
58    #[serde(with = "serde_bytes")]
59    pub h: Vec<u8>,
60    /// Offered payment amount in sat.
61    pub p: u64,
62    /// Quote validity window in milliseconds.
63    pub t: u32,
64    /// Optional settlement mint URL.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub m: Option<String>,
67}
68
69/// Quote response message body
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct DataQuoteResponse {
72    /// 32-byte hash
73    #[serde(with = "serde_bytes")]
74    pub h: Vec<u8>,
75    /// Whether the peer is willing and able to serve the request.
76    pub a: bool,
77    /// Quote identifier to include in the follow-up request.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub q: Option<u64>,
80    /// Accepted payment amount in sat.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub p: Option<u64>,
83    /// Quote validity window in milliseconds.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub t: Option<u32>,
86    /// Settlement mint URL accepted for this quote.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub m: Option<String>,
89}
90
91/// Parsed data message
92#[derive(Debug, Clone)]
93pub enum DataMessage {
94    Request(DataRequest),
95    Response(DataResponse),
96    QuoteRequest(DataQuoteRequest),
97    QuoteResponse(DataQuoteResponse),
98}
99
100/// Encode a request message to wire format
101/// Uses named/map encoding for compatibility with hashtree-ts and to support optional fields
102pub fn encode_request(req: &DataRequest) -> Vec<u8> {
103    let body = rmp_serde::to_vec_named(req).expect("Failed to encode request");
104    let mut result = Vec::with_capacity(1 + body.len());
105    result.push(MSG_TYPE_REQUEST);
106    result.extend(body);
107    result
108}
109
110/// Encode a response message to wire format
111/// Uses named/map encoding for compatibility with hashtree-ts and to support optional fields
112pub fn encode_response(res: &DataResponse) -> Vec<u8> {
113    let body = rmp_serde::to_vec_named(res).expect("Failed to encode response");
114    let mut result = Vec::with_capacity(1 + body.len());
115    result.push(MSG_TYPE_RESPONSE);
116    result.extend(body);
117    result
118}
119
120/// Encode a quote request message to wire format.
121pub fn encode_quote_request(req: &DataQuoteRequest) -> Vec<u8> {
122    let body = rmp_serde::to_vec_named(req).expect("Failed to encode quote request");
123    let mut result = Vec::with_capacity(1 + body.len());
124    result.push(MSG_TYPE_QUOTE_REQUEST);
125    result.extend(body);
126    result
127}
128
129/// Encode a quote response message to wire format.
130pub fn encode_quote_response(res: &DataQuoteResponse) -> Vec<u8> {
131    let body = rmp_serde::to_vec_named(res).expect("Failed to encode quote response");
132    let mut result = Vec::with_capacity(1 + body.len());
133    result.push(MSG_TYPE_QUOTE_RESPONSE);
134    result.extend(body);
135    result
136}
137
138/// Parse a wire format message
139pub fn parse_message(data: &[u8]) -> Option<DataMessage> {
140    if data.len() < 2 {
141        return None;
142    }
143
144    let msg_type = data[0];
145    let body = &data[1..];
146
147    match msg_type {
148        MSG_TYPE_REQUEST => rmp_serde::from_slice::<DataRequest>(body)
149            .ok()
150            .map(DataMessage::Request),
151        MSG_TYPE_RESPONSE => rmp_serde::from_slice::<DataResponse>(body)
152            .ok()
153            .map(DataMessage::Response),
154        MSG_TYPE_QUOTE_REQUEST => rmp_serde::from_slice::<DataQuoteRequest>(body)
155            .ok()
156            .map(DataMessage::QuoteRequest),
157        MSG_TYPE_QUOTE_RESPONSE => rmp_serde::from_slice::<DataQuoteResponse>(body)
158            .ok()
159            .map(DataMessage::QuoteResponse),
160        _ => None,
161    }
162}
163
164/// Create a request
165pub fn create_request(hash: &Hash, htl: u8) -> DataRequest {
166    DataRequest {
167        h: hash.to_vec(),
168        htl: Some(htl),
169        q: None,
170    }
171}
172
173/// Create a request that references a previously accepted quote.
174pub fn create_request_with_quote(hash: &Hash, htl: u8, quote_id: u64) -> DataRequest {
175    DataRequest {
176        h: hash.to_vec(),
177        htl: Some(htl),
178        q: Some(quote_id),
179    }
180}
181
182/// Create an unfragmented response
183pub fn create_response(hash: &Hash, data: Vec<u8>) -> DataResponse {
184    DataResponse {
185        h: hash.to_vec(),
186        d: data,
187        i: None,
188        n: None,
189    }
190}
191
192/// Create a quote request.
193pub fn create_quote_request(
194    hash: &Hash,
195    ttl_ms: u32,
196    payment_sat: u64,
197    mint_url: Option<&str>,
198) -> DataQuoteRequest {
199    DataQuoteRequest {
200        h: hash.to_vec(),
201        p: payment_sat,
202        t: ttl_ms,
203        m: mint_url.map(str::to_string),
204    }
205}
206
207/// Create an accepted quote response.
208pub fn create_quote_response_available(
209    hash: &Hash,
210    quote_id: u64,
211    payment_sat: u64,
212    ttl_ms: u32,
213    mint_url: Option<&str>,
214) -> DataQuoteResponse {
215    DataQuoteResponse {
216        h: hash.to_vec(),
217        a: true,
218        q: Some(quote_id),
219        p: Some(payment_sat),
220        t: Some(ttl_ms),
221        m: mint_url.map(str::to_string),
222    }
223}
224
225/// Create a declined quote response.
226pub fn create_quote_response_unavailable(hash: &Hash) -> DataQuoteResponse {
227    DataQuoteResponse {
228        h: hash.to_vec(),
229        a: false,
230        q: None,
231        p: None,
232        t: None,
233        m: None,
234    }
235}
236
237/// Create a fragmented response
238pub fn create_fragment_response(
239    hash: &Hash,
240    data: Vec<u8>,
241    index: u32,
242    total: u32,
243) -> DataResponse {
244    DataResponse {
245        h: hash.to_vec(),
246        d: data,
247        i: Some(index),
248        n: Some(total),
249    }
250}
251
252/// Check if a response is fragmented
253pub fn is_fragmented(res: &DataResponse) -> bool {
254    res.i.is_some() && res.n.is_some()
255}
256
257/// Convert hash bytes to hex string for use as map key
258pub fn hash_to_key(hash: &[u8]) -> String {
259    hex::encode(hash)
260}
261
262/// Convert Hash to bytes
263pub fn hash_to_bytes(hash: &Hash) -> Vec<u8> {
264    hash.to_vec()
265}
266
267/// Convert bytes to Hash
268pub fn bytes_to_hash(bytes: &[u8]) -> Option<Hash> {
269    if bytes.len() == 32 {
270        let mut hash = [0u8; 32];
271        hash.copy_from_slice(bytes);
272        Some(hash)
273    } else {
274        None
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_encode_decode_request() {
284        let hash = [0xab; 32];
285        let req = create_request(&hash, 10);
286        let encoded = encode_request(&req);
287
288        assert_eq!(encoded[0], MSG_TYPE_REQUEST);
289
290        let parsed = parse_message(&encoded).unwrap();
291        match parsed {
292            DataMessage::Request(r) => {
293                assert_eq!(r.h, hash.to_vec());
294                assert_eq!(r.htl, Some(10));
295            }
296            _ => panic!("Expected request"),
297        }
298    }
299
300    #[test]
301    fn test_encode_decode_response() {
302        let hash = [0xcd; 32];
303        let data = vec![1, 2, 3, 4, 5];
304        let res = create_response(&hash, data.clone());
305        let encoded = encode_response(&res);
306
307        assert_eq!(encoded[0], MSG_TYPE_RESPONSE);
308
309        let parsed = parse_message(&encoded).unwrap();
310        match parsed {
311            DataMessage::Response(r) => {
312                assert_eq!(r.h, hash.to_vec());
313                assert_eq!(r.d, data);
314                assert!(!is_fragmented(&r));
315            }
316            _ => panic!("Expected response"),
317        }
318    }
319
320    #[test]
321    fn test_encode_decode_fragment_response() {
322        let hash = [0xef; 32];
323        let data = vec![10, 20, 30];
324        let res = create_fragment_response(&hash, data.clone(), 2, 5);
325        let encoded = encode_response(&res);
326
327        let parsed = parse_message(&encoded).unwrap();
328        match parsed {
329            DataMessage::Response(r) => {
330                assert_eq!(r.h, hash.to_vec());
331                assert_eq!(r.d, data);
332                assert!(is_fragmented(&r));
333                assert_eq!(r.i, Some(2));
334                assert_eq!(r.n, Some(5));
335            }
336            _ => panic!("Expected response"),
337        }
338    }
339
340    #[test]
341    fn test_encode_decode_quote_request() {
342        let hash = [0x44; 32];
343        let req = create_quote_request(&hash, 7, 2_500, Some("https://mint.example"));
344        let encoded = encode_quote_request(&req);
345
346        assert_eq!(encoded[0], MSG_TYPE_QUOTE_REQUEST);
347
348        let parsed = parse_message(&encoded).unwrap();
349        match parsed {
350            DataMessage::QuoteRequest(r) => {
351                assert_eq!(r.h, hash.to_vec());
352                assert_eq!(r.t, 7);
353                assert_eq!(r.p, 2_500);
354                assert_eq!(r.m.as_deref(), Some("https://mint.example"));
355            }
356            _ => panic!("Expected quote request"),
357        }
358    }
359
360    #[test]
361    fn test_encode_decode_quote_response_and_quoted_request() {
362        let hash = [0x55; 32];
363        let quote =
364            create_quote_response_available(&hash, 19, 2_500, 7, Some("https://mint.example"));
365        let encoded_quote = encode_quote_response(&quote);
366
367        assert_eq!(encoded_quote[0], MSG_TYPE_QUOTE_RESPONSE);
368
369        let parsed_quote = parse_message(&encoded_quote).unwrap();
370        match parsed_quote {
371            DataMessage::QuoteResponse(r) => {
372                assert_eq!(r.h, hash.to_vec());
373                assert!(r.a);
374                assert_eq!(r.q, Some(19));
375                assert_eq!(r.p, Some(2_500));
376                assert_eq!(r.t, Some(7));
377                assert_eq!(r.m.as_deref(), Some("https://mint.example"));
378            }
379            _ => panic!("Expected quote response"),
380        }
381
382        let req = create_request_with_quote(&hash, 9, 19);
383        let encoded_req = encode_request(&req);
384        let parsed_req = parse_message(&encoded_req).unwrap();
385        match parsed_req {
386            DataMessage::Request(r) => {
387                assert_eq!(r.h, hash.to_vec());
388                assert_eq!(r.htl, Some(9));
389                assert_eq!(r.q, Some(19));
390            }
391            _ => panic!("Expected quoted request"),
392        }
393    }
394
395    #[test]
396    fn test_hash_conversions() {
397        let hash = [0x12; 32];
398        let bytes = hash_to_bytes(&hash);
399        let back = bytes_to_hash(&bytes).unwrap();
400        assert_eq!(hash, back);
401    }
402}