hashtree_core/
blob_route.rs1use 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;
12pub const BLOB_MAX_HTL: u8 = 10;
14pub 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#[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
61pub 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}