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