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//! - Payment:        [0x04][msgpack: {h: bytes32, q: u64, c: u32, p: u64, m?: string, tok: string}]
9//! - PaymentAck:     [0x05][msgpack: {h: bytes32, q: u64, c: u32, a: bool, e?: string}]
10//! - Chunk:          [0x06][msgpack: {h: bytes32, q: u64, c: u32, n: u32, p: u64, d: bytes}]
11//!
12//! Fragmented responses include `i` (index) and `n` (total), unfragmented omit them.
13
14use hashtree_core::Hash;
15use serde::{Deserialize, Serialize};
16
17fn default_htl() -> u8 {
18    crate::types::MAX_HTL
19}
20
21fn is_max_htl(htl: &u8) -> bool {
22    *htl == crate::types::MAX_HTL
23}
24
25/// Message type bytes (prefix before MessagePack body)
26pub const MSG_TYPE_REQUEST: u8 = 0x00;
27pub const MSG_TYPE_RESPONSE: u8 = 0x01;
28pub const MSG_TYPE_QUOTE_REQUEST: u8 = 0x02;
29pub const MSG_TYPE_QUOTE_RESPONSE: u8 = 0x03;
30pub const MSG_TYPE_PAYMENT: u8 = 0x04;
31pub const MSG_TYPE_PAYMENT_ACK: u8 = 0x05;
32pub const MSG_TYPE_CHUNK: u8 = 0x06;
33
34/// Fragment size for large data (32KB - safe limit for WebRTC)
35pub const FRAGMENT_SIZE: usize = 32 * 1024;
36
37/// Data request message body
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct DataRequest {
40    /// 32-byte hash
41    #[serde(with = "serde_bytes")]
42    pub h: Vec<u8>,
43    /// Hops To Live (defaults to MAX_HTL when omitted on the wire)
44    #[serde(default = "default_htl", skip_serializing_if = "is_max_htl")]
45    pub htl: u8,
46    /// Optional quote identifier for paid retrieval.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub q: Option<u64>,
49}
50
51/// Data response message body
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct DataResponse {
54    /// 32-byte hash
55    #[serde(with = "serde_bytes")]
56    pub h: Vec<u8>,
57    /// Data (fragment or full)
58    #[serde(with = "serde_bytes")]
59    pub d: Vec<u8>,
60    /// Fragment index (0-based), absent = unfragmented
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub i: Option<u32>,
63    /// Total fragments, absent = unfragmented
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub n: Option<u32>,
66}
67
68/// Quote request message body
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DataQuoteRequest {
71    /// 32-byte hash
72    #[serde(with = "serde_bytes")]
73    pub h: Vec<u8>,
74    /// Offered payment amount in sat.
75    pub p: u64,
76    /// Quote validity window in milliseconds.
77    pub t: u32,
78    /// Optional settlement mint URL.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub m: Option<String>,
81}
82
83/// Quote response message body
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct DataQuoteResponse {
86    /// 32-byte hash
87    #[serde(with = "serde_bytes")]
88    pub h: Vec<u8>,
89    /// Whether the peer is willing and able to serve the request.
90    pub a: bool,
91    /// Quote identifier to include in the follow-up request.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub q: Option<u64>,
94    /// Accepted payment amount in sat.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub p: Option<u64>,
97    /// Quote validity window in milliseconds.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub t: Option<u32>,
100    /// Settlement mint URL accepted for this quote.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub m: Option<String>,
103}
104
105/// Payment message body for chunk-by-chunk settlement.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct DataPayment {
108    #[serde(with = "serde_bytes")]
109    pub h: Vec<u8>,
110    pub q: u64,
111    pub c: u32,
112    pub p: u64,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub m: Option<String>,
115    pub tok: String,
116}
117
118/// Payment acknowledgement for quoted chunk settlement.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct DataPaymentAck {
121    #[serde(with = "serde_bytes")]
122    pub h: Vec<u8>,
123    pub q: u64,
124    pub c: u32,
125    pub a: bool,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub e: Option<String>,
128}
129
130/// Quoted data chunk delivered after payment negotiation.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct DataChunk {
133    #[serde(with = "serde_bytes")]
134    pub h: Vec<u8>,
135    pub q: u64,
136    pub c: u32,
137    pub n: u32,
138    pub p: u64,
139    #[serde(with = "serde_bytes")]
140    pub d: Vec<u8>,
141}
142
143/// Parsed data message
144#[derive(Debug, Clone)]
145pub enum DataMessage {
146    Request(DataRequest),
147    Response(DataResponse),
148    QuoteRequest(DataQuoteRequest),
149    QuoteResponse(DataQuoteResponse),
150    Payment(DataPayment),
151    PaymentAck(DataPaymentAck),
152    Chunk(DataChunk),
153}
154
155/// Encode a request message to wire format
156/// Uses named/map encoding for compatibility with hashtree-ts and to support optional fields
157pub fn encode_request(req: &DataRequest) -> Vec<u8> {
158    let body = rmp_serde::to_vec_named(req).expect("Failed to encode request");
159    let mut result = Vec::with_capacity(1 + body.len());
160    result.push(MSG_TYPE_REQUEST);
161    result.extend(body);
162    result
163}
164
165/// Encode a response message to wire format
166/// Uses named/map encoding for compatibility with hashtree-ts and to support optional fields
167pub fn encode_response(res: &DataResponse) -> Vec<u8> {
168    let body = rmp_serde::to_vec_named(res).expect("Failed to encode response");
169    let mut result = Vec::with_capacity(1 + body.len());
170    result.push(MSG_TYPE_RESPONSE);
171    result.extend(body);
172    result
173}
174
175/// Encode a quote request message to wire format.
176pub fn encode_quote_request(req: &DataQuoteRequest) -> Vec<u8> {
177    let body = rmp_serde::to_vec_named(req).expect("Failed to encode quote request");
178    let mut result = Vec::with_capacity(1 + body.len());
179    result.push(MSG_TYPE_QUOTE_REQUEST);
180    result.extend(body);
181    result
182}
183
184/// Encode a quote response message to wire format.
185pub fn encode_quote_response(res: &DataQuoteResponse) -> Vec<u8> {
186    let body = rmp_serde::to_vec_named(res).expect("Failed to encode quote response");
187    let mut result = Vec::with_capacity(1 + body.len());
188    result.push(MSG_TYPE_QUOTE_RESPONSE);
189    result.extend(body);
190    result
191}
192
193/// Encode a payment message to wire format.
194pub fn encode_payment(req: &DataPayment) -> Vec<u8> {
195    let body = rmp_serde::to_vec_named(req).expect("Failed to encode payment");
196    let mut result = Vec::with_capacity(1 + body.len());
197    result.push(MSG_TYPE_PAYMENT);
198    result.extend(body);
199    result
200}
201
202/// Encode a payment acknowledgement to wire format.
203pub fn encode_payment_ack(res: &DataPaymentAck) -> Vec<u8> {
204    let body = rmp_serde::to_vec_named(res).expect("Failed to encode payment ack");
205    let mut result = Vec::with_capacity(1 + body.len());
206    result.push(MSG_TYPE_PAYMENT_ACK);
207    result.extend(body);
208    result
209}
210
211/// Encode a quoted data chunk to wire format.
212pub fn encode_chunk(chunk: &DataChunk) -> Vec<u8> {
213    let body = rmp_serde::to_vec_named(chunk).expect("Failed to encode chunk");
214    let mut result = Vec::with_capacity(1 + body.len());
215    result.push(MSG_TYPE_CHUNK);
216    result.extend(body);
217    result
218}
219
220/// Parse a wire format message
221pub fn parse_message(data: &[u8]) -> Option<DataMessage> {
222    if data.len() < 2 {
223        return None;
224    }
225
226    let msg_type = data[0];
227    let body = &data[1..];
228
229    match msg_type {
230        MSG_TYPE_REQUEST => rmp_serde::from_slice::<DataRequest>(body)
231            .ok()
232            .map(DataMessage::Request),
233        MSG_TYPE_RESPONSE => rmp_serde::from_slice::<DataResponse>(body)
234            .ok()
235            .map(DataMessage::Response),
236        MSG_TYPE_QUOTE_REQUEST => rmp_serde::from_slice::<DataQuoteRequest>(body)
237            .ok()
238            .map(DataMessage::QuoteRequest),
239        MSG_TYPE_QUOTE_RESPONSE => rmp_serde::from_slice::<DataQuoteResponse>(body)
240            .ok()
241            .map(DataMessage::QuoteResponse),
242        MSG_TYPE_PAYMENT => rmp_serde::from_slice::<DataPayment>(body)
243            .ok()
244            .map(DataMessage::Payment),
245        MSG_TYPE_PAYMENT_ACK => rmp_serde::from_slice::<DataPaymentAck>(body)
246            .ok()
247            .map(DataMessage::PaymentAck),
248        MSG_TYPE_CHUNK => rmp_serde::from_slice::<DataChunk>(body)
249            .ok()
250            .map(DataMessage::Chunk),
251        _ => None,
252    }
253}
254
255/// Create a request
256pub fn create_request(hash: &Hash, htl: u8) -> DataRequest {
257    DataRequest {
258        h: hash.to_vec(),
259        htl,
260        q: None,
261    }
262}
263
264/// Create a request that references a previously accepted quote.
265pub fn create_request_with_quote(hash: &Hash, htl: u8, quote_id: u64) -> DataRequest {
266    DataRequest {
267        h: hash.to_vec(),
268        htl,
269        q: Some(quote_id),
270    }
271}
272
273/// Create an unfragmented response
274pub fn create_response(hash: &Hash, data: Vec<u8>) -> DataResponse {
275    DataResponse {
276        h: hash.to_vec(),
277        d: data,
278        i: None,
279        n: None,
280    }
281}
282
283/// Create a quote request.
284pub fn create_quote_request(
285    hash: &Hash,
286    ttl_ms: u32,
287    payment_sat: u64,
288    mint_url: Option<&str>,
289) -> DataQuoteRequest {
290    DataQuoteRequest {
291        h: hash.to_vec(),
292        p: payment_sat,
293        t: ttl_ms,
294        m: mint_url.map(str::to_string),
295    }
296}
297
298/// Create an accepted quote response.
299pub fn create_quote_response_available(
300    hash: &Hash,
301    quote_id: u64,
302    payment_sat: u64,
303    ttl_ms: u32,
304    mint_url: Option<&str>,
305) -> DataQuoteResponse {
306    DataQuoteResponse {
307        h: hash.to_vec(),
308        a: true,
309        q: Some(quote_id),
310        p: Some(payment_sat),
311        t: Some(ttl_ms),
312        m: mint_url.map(str::to_string),
313    }
314}
315
316/// Create a declined quote response.
317pub fn create_quote_response_unavailable(hash: &Hash) -> DataQuoteResponse {
318    DataQuoteResponse {
319        h: hash.to_vec(),
320        a: false,
321        q: None,
322        p: None,
323        t: None,
324        m: None,
325    }
326}
327
328/// Create a fragmented response
329pub fn create_fragment_response(
330    hash: &Hash,
331    data: Vec<u8>,
332    index: u32,
333    total: u32,
334) -> DataResponse {
335    DataResponse {
336        h: hash.to_vec(),
337        d: data,
338        i: Some(index),
339        n: Some(total),
340    }
341}
342
343/// Check if a response is fragmented
344pub fn is_fragmented(res: &DataResponse) -> bool {
345    res.i.is_some() && res.n.is_some()
346}
347
348/// Convert hash bytes to hex string for use as map key
349pub fn hash_to_key(hash: &[u8]) -> String {
350    hex::encode(hash)
351}
352
353/// Convert Hash to bytes
354pub fn hash_to_bytes(hash: &Hash) -> Vec<u8> {
355    hash.to_vec()
356}
357
358/// Convert bytes to Hash
359pub fn bytes_to_hash(bytes: &[u8]) -> Option<Hash> {
360    if bytes.len() == 32 {
361        let mut hash = [0u8; 32];
362        hash.copy_from_slice(bytes);
363        Some(hash)
364    } else {
365        None
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_encode_decode_request() {
375        let hash = [0xab; 32];
376        let req = create_request(&hash, 10);
377        let encoded = encode_request(&req);
378
379        assert_eq!(encoded[0], MSG_TYPE_REQUEST);
380
381        let parsed = parse_message(&encoded).unwrap();
382        match parsed {
383            DataMessage::Request(r) => {
384                assert_eq!(r.h, hash.to_vec());
385                assert_eq!(r.htl, 10);
386            }
387            _ => panic!("Expected request"),
388        }
389    }
390
391    #[test]
392    fn test_decode_request_without_explicit_htl_defaults_to_max() {
393        #[derive(Serialize)]
394        struct LegacyRequest {
395            #[serde(with = "serde_bytes")]
396            h: Vec<u8>,
397        }
398
399        let hash = [0x21; 32];
400        let body = rmp_serde::to_vec_named(&LegacyRequest { h: hash.to_vec() }).unwrap();
401        let mut encoded = vec![MSG_TYPE_REQUEST];
402        encoded.extend(body);
403
404        let parsed = parse_message(&encoded).unwrap();
405        match parsed {
406            DataMessage::Request(r) => {
407                assert_eq!(r.h, hash.to_vec());
408                assert_eq!(r.htl, crate::types::MAX_HTL);
409            }
410            _ => panic!("Expected request"),
411        }
412    }
413
414    #[test]
415    fn test_encode_decode_response() {
416        let hash = [0xcd; 32];
417        let data = vec![1, 2, 3, 4, 5];
418        let res = create_response(&hash, data.clone());
419        let encoded = encode_response(&res);
420
421        assert_eq!(encoded[0], MSG_TYPE_RESPONSE);
422
423        let parsed = parse_message(&encoded).unwrap();
424        match parsed {
425            DataMessage::Response(r) => {
426                assert_eq!(r.h, hash.to_vec());
427                assert_eq!(r.d, data);
428                assert!(!is_fragmented(&r));
429            }
430            _ => panic!("Expected response"),
431        }
432    }
433
434    #[test]
435    fn test_encode_decode_fragment_response() {
436        let hash = [0xef; 32];
437        let data = vec![10, 20, 30];
438        let res = create_fragment_response(&hash, data.clone(), 2, 5);
439        let encoded = encode_response(&res);
440
441        let parsed = parse_message(&encoded).unwrap();
442        match parsed {
443            DataMessage::Response(r) => {
444                assert_eq!(r.h, hash.to_vec());
445                assert_eq!(r.d, data);
446                assert!(is_fragmented(&r));
447                assert_eq!(r.i, Some(2));
448                assert_eq!(r.n, Some(5));
449            }
450            _ => panic!("Expected response"),
451        }
452    }
453
454    #[test]
455    fn test_encode_decode_quote_request() {
456        let hash = [0x44; 32];
457        let req = create_quote_request(&hash, 7, 2_500, Some("https://mint.example"));
458        let encoded = encode_quote_request(&req);
459
460        assert_eq!(encoded[0], MSG_TYPE_QUOTE_REQUEST);
461
462        let parsed = parse_message(&encoded).unwrap();
463        match parsed {
464            DataMessage::QuoteRequest(r) => {
465                assert_eq!(r.h, hash.to_vec());
466                assert_eq!(r.t, 7);
467                assert_eq!(r.p, 2_500);
468                assert_eq!(r.m.as_deref(), Some("https://mint.example"));
469            }
470            _ => panic!("Expected quote request"),
471        }
472    }
473
474    #[test]
475    fn test_encode_decode_quote_response_and_quoted_request() {
476        let hash = [0x55; 32];
477        let quote =
478            create_quote_response_available(&hash, 19, 2_500, 7, Some("https://mint.example"));
479        let encoded_quote = encode_quote_response(&quote);
480
481        assert_eq!(encoded_quote[0], MSG_TYPE_QUOTE_RESPONSE);
482
483        let parsed_quote = parse_message(&encoded_quote).unwrap();
484        match parsed_quote {
485            DataMessage::QuoteResponse(r) => {
486                assert_eq!(r.h, hash.to_vec());
487                assert!(r.a);
488                assert_eq!(r.q, Some(19));
489                assert_eq!(r.p, Some(2_500));
490                assert_eq!(r.t, Some(7));
491                assert_eq!(r.m.as_deref(), Some("https://mint.example"));
492            }
493            _ => panic!("Expected quote response"),
494        }
495
496        let req = create_request_with_quote(&hash, 9, 19);
497        let encoded_req = encode_request(&req);
498        let parsed_req = parse_message(&encoded_req).unwrap();
499        match parsed_req {
500            DataMessage::Request(r) => {
501                assert_eq!(r.h, hash.to_vec());
502                assert_eq!(r.htl, 9);
503                assert_eq!(r.q, Some(19));
504            }
505            _ => panic!("Expected quoted request"),
506        }
507    }
508
509    #[test]
510    fn test_hash_conversions() {
511        let hash = [0x12; 32];
512        let bytes = hash_to_bytes(&hash);
513        let back = bytes_to_hash(&bytes).unwrap();
514        assert_eq!(hash, back);
515    }
516
517    #[test]
518    fn test_encode_decode_payment_ack_and_chunk() {
519        let payment = DataPayment {
520            h: vec![0x61; 32],
521            q: 9,
522            c: 1,
523            p: 3,
524            m: Some("https://mint.example".to_string()),
525            tok: "cashuBtoken".to_string(),
526        };
527        let payment_ack = DataPaymentAck {
528            h: vec![0x62; 32],
529            q: 9,
530            c: 1,
531            a: true,
532            e: None,
533        };
534        let chunk = DataChunk {
535            h: vec![0x63; 32],
536            q: 9,
537            c: 1,
538            n: 2,
539            p: 3,
540            d: vec![1, 2, 3],
541        };
542
543        match parse_message(&encode_payment(&payment)).unwrap() {
544            DataMessage::Payment(parsed) => {
545                assert_eq!(parsed.q, payment.q);
546                assert_eq!(parsed.p, payment.p);
547                assert_eq!(parsed.m, payment.m);
548            }
549            _ => panic!("Expected payment"),
550        }
551
552        match parse_message(&encode_payment_ack(&payment_ack)).unwrap() {
553            DataMessage::PaymentAck(parsed) => {
554                assert_eq!(parsed.q, payment_ack.q);
555                assert_eq!(parsed.c, payment_ack.c);
556                assert!(parsed.a);
557            }
558            _ => panic!("Expected payment ack"),
559        }
560
561        match parse_message(&encode_chunk(&chunk)).unwrap() {
562            DataMessage::Chunk(parsed) => {
563                assert_eq!(parsed.q, chunk.q);
564                assert_eq!(parsed.n, chunk.n);
565                assert_eq!(parsed.d, chunk.d);
566            }
567            _ => panic!("Expected chunk"),
568        }
569    }
570}