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