Skip to main content

hashtree_core/
blob_route.rs

1//! One-hop blob routing values and their compact process/network wire codec.
2
3use std::sync::Arc;
4use std::time::Instant;
5
6use async_trait::async_trait;
7use thiserror::Error;
8
9use crate::{Hash, Store, StoreError};
10
11pub const BLOB_MAX_BYTES: usize = 16 * 1024 * 1024;
12/// Maximum number of Hashtree mesh forwarding hops accepted by the protocol.
13pub const BLOB_MAX_HTL: u8 = 10;
14/// Default Hashtree mesh-search budget used by standalone resolvers.
15pub const BLOB_DEFAULT_HTL: u8 = BLOB_MAX_HTL;
16pub const BLOB_REQUEST_BYTES: usize = 36;
17pub const BLOB_REPLY_HEADER_BYTES: usize = 7;
18
19const MAGIC: u8 = 0x48;
20const VERSION: u8 = 1;
21const GET: u8 = 1;
22const DATA: u8 = 1;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct BlobRequest {
26    pub hash: Hash,
27    pub htl: u8,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum BlobReply {
32    Data(Vec<u8>),
33    NoResult,
34}
35
36/// Per-search limits supplied by an outer router to one opaque route.
37///
38/// These limits are process-local and are deliberately not part of the
39/// published [`BlobRequest`] wire format. Terminal routes may ignore the
40/// attempt budget; composite routes use it to bound their own internal peer
41/// selection.
42#[derive(Clone, Copy, Debug)]
43pub struct BlobRouteContext {
44    pub deadline: Instant,
45    pub attempt_budget: usize,
46}
47
48#[async_trait]
49pub trait BlobRoute: Send + Sync {
50    async fn route(&self, request: BlobRequest) -> Result<BlobReply, StoreError>;
51
52    async fn route_with_context(
53        &self,
54        request: BlobRequest,
55        _context: BlobRouteContext,
56    ) -> Result<BlobReply, StoreError> {
57        self.route(request).await
58    }
59}
60
61/// A terminal route that performs exactly one lookup and does not interpret HTL.
62pub struct StoreBlobRoute<S: Store + ?Sized> {
63    store: Arc<S>,
64}
65
66impl<S: Store + ?Sized> StoreBlobRoute<S> {
67    pub fn new(store: Arc<S>) -> Self {
68        Self { store }
69    }
70}
71
72#[async_trait]
73impl<S: Store + ?Sized + 'static> BlobRoute for StoreBlobRoute<S> {
74    async fn route(&self, request: BlobRequest) -> Result<BlobReply, StoreError> {
75        Ok(match self.store.get(&request.hash).await? {
76            Some(data) if data.len() > BLOB_MAX_BYTES => {
77                return Err(StoreError::Other(format!(
78                    "blob route returned {} bytes, exceeding the {BLOB_MAX_BYTES}-byte limit",
79                    data.len()
80                )));
81            }
82            Some(data) if crate::sha256(&data) != request.hash => {
83                return Err(StoreError::Other(
84                    "blob route returned content with the wrong hash".to_string(),
85                ));
86            }
87            Some(data) => BlobReply::Data(data),
88            None => BlobReply::NoResult,
89        })
90    }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum BlobReplyHeader {
95    Data(usize),
96    NoResult,
97}
98
99#[derive(Debug, Error, PartialEq, Eq)]
100pub enum BlobCodecError {
101    #[error("invalid Hashtree blob wire message: {0}")]
102    Invalid(&'static str),
103    #[error("Hashtree blob size {0} exceeds the 16 MiB limit")]
104    BlobTooLarge(usize),
105    #[error("Hashtree blob HTL {0} exceeds the maximum of {BLOB_MAX_HTL}")]
106    HtlTooLarge(u8),
107}
108
109pub fn encode_blob_request(request: &BlobRequest) -> [u8; BLOB_REQUEST_BYTES] {
110    let mut bytes = [0; BLOB_REQUEST_BYTES];
111    bytes[..4].copy_from_slice(&[MAGIC, VERSION, GET, request.htl]);
112    bytes[4..].copy_from_slice(&request.hash);
113    bytes
114}
115
116pub fn decode_blob_request(bytes: &[u8]) -> Result<BlobRequest, BlobCodecError> {
117    if bytes.len() != BLOB_REQUEST_BYTES {
118        return Err(BlobCodecError::Invalid("request has the wrong length"));
119    }
120    if bytes[..3] != [MAGIC, VERSION, GET] {
121        return Err(BlobCodecError::Invalid(
122            "request has an unsupported prelude",
123        ));
124    }
125    if bytes[3] > BLOB_MAX_HTL {
126        return Err(BlobCodecError::HtlTooLarge(bytes[3]));
127    }
128    let mut hash = [0; 32];
129    hash.copy_from_slice(&bytes[4..]);
130    Ok(BlobRequest {
131        hash,
132        htl: bytes[3],
133    })
134}
135
136pub fn encode_blob_reply_header(
137    reply: &BlobReply,
138) -> Result<[u8; BLOB_REPLY_HEADER_BYTES], BlobCodecError> {
139    let (status, size) = match reply {
140        BlobReply::NoResult => (0, 0),
141        BlobReply::Data(data) => (DATA, data.len()),
142    };
143    if size > BLOB_MAX_BYTES {
144        return Err(BlobCodecError::BlobTooLarge(size));
145    }
146    let mut header = [0; BLOB_REPLY_HEADER_BYTES];
147    header[..3].copy_from_slice(&[MAGIC, VERSION, status]);
148    header[3..].copy_from_slice(&(size as u32).to_be_bytes());
149    Ok(header)
150}
151
152pub fn decode_blob_reply_header(bytes: &[u8]) -> Result<BlobReplyHeader, BlobCodecError> {
153    if bytes.len() != BLOB_REPLY_HEADER_BYTES {
154        return Err(BlobCodecError::Invalid(
155            "response header has the wrong length",
156        ));
157    }
158    if bytes[0] != MAGIC || bytes[1] != VERSION {
159        return Err(BlobCodecError::Invalid(
160            "response has an unsupported prelude",
161        ));
162    }
163    let size = u32::from_be_bytes(bytes[3..].try_into().expect("four-byte length")) as usize;
164    if size > BLOB_MAX_BYTES {
165        return Err(BlobCodecError::BlobTooLarge(size));
166    }
167    match (bytes[2], size) {
168        (0, 0) => Ok(BlobReplyHeader::NoResult),
169        (0, _) => Err(BlobCodecError::Invalid(
170            "NoResult response has a non-zero payload length",
171        )),
172        (DATA, size) => Ok(BlobReplyHeader::Data(size)),
173        _ => Err(BlobCodecError::Invalid(
174            "response has an unsupported status",
175        )),
176    }
177}