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