1use ethrex_common::{
2 H256,
3 serde_utils::{self},
4 types::{Blob, Proof},
5};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use tracing::debug;
9
10use crate::{
11 rpc::{RpcApiContext, RpcHandler},
12 utils::RpcErr,
13};
14
15const GET_BLOBS_V1_REQUEST_MAX_SIZE: usize = 128;
17
18#[derive(Debug, Serialize, Deserialize)]
19pub struct BlobsV1Request {
20 blob_versioned_hashes: Vec<H256>,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct BlobsV2Request {
25 blob_versioned_hashes: Vec<H256>,
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29pub struct BlobsV3Request {
30 blob_versioned_hashes: Vec<H256>,
31}
32
33#[derive(Clone, Debug, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct BlobAndProofV1 {
36 #[serde(with = "serde_utils::blob")]
37 pub blob: Blob,
38 #[serde(with = "serde_utils::bytes48")]
39 pub proof: Proof,
40}
41
42#[derive(Clone, Debug, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct BlobAndProofV2 {
45 #[serde(with = "serde_utils::blob")]
46 pub blob: Blob,
47 #[serde(with = "serde_utils::bytes48::vec")]
48 pub proofs: Vec<Proof>,
49}
50
51impl RpcHandler for BlobsV1Request {
52 fn parse(params: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
53 let params = params
54 .as_ref()
55 .ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
56 if params.len() != 1 {
57 return Err(RpcErr::BadParams("Expected 1 param".to_owned()));
58 };
59 Ok(BlobsV1Request {
60 blob_versioned_hashes: serde_json::from_value(params[0].clone())?,
61 })
62 }
63
64 async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
65 debug!("Received new engine request: Requested Blobs");
66
67 if let Some(current_block_header) = context
70 .storage
71 .get_block_header(context.storage.get_latest_block_number().await?)?
72 && context
73 .storage
74 .get_chain_config()
75 .is_osaka_activated(current_block_header.timestamp)
76 {
77 return Err(RpcErr::UnsupportedFork(
78 "getBlobsV1 engine only supported before Osaka".to_string(),
79 ));
80 }
81
82 if self.blob_versioned_hashes.len() > GET_BLOBS_V1_REQUEST_MAX_SIZE {
83 return Err(RpcErr::TooLargeRequest);
84 }
85
86 let blob_tuples = context
87 .blockchain
88 .mempool
89 .get_blobs_data_by_versioned_hashes(&self.blob_versioned_hashes)?;
90
91 debug_assert_eq!(self.blob_versioned_hashes.len(), blob_tuples.len());
92
93 let res: Vec<Option<BlobAndProofV1>> = blob_tuples
94 .into_iter()
95 .map(|b| {
96 b.map(|(blob, _, proofs)| BlobAndProofV1 {
97 blob: *blob,
98 proof: proofs[0],
101 })
102 })
103 .collect();
104
105 serde_json::to_value(res).map_err(|error| RpcErr::Internal(error.to_string()))
106 }
107}
108
109impl RpcHandler for BlobsV2Request {
110 fn parse(params: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
111 let params = params
112 .as_ref()
113 .ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
114 if params.len() != 1 {
115 return Err(RpcErr::BadParams("Expected 1 param".to_owned()));
116 };
117 Ok(BlobsV2Request {
118 blob_versioned_hashes: serde_json::from_value(params[0].clone())?,
119 })
120 }
121
122 async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
123 debug!("Received new engine request: Requested Blobs V2");
124 let res = get_blobs_and_proof(&self.blob_versioned_hashes, context).await?;
125 if res.iter().any(|blob| blob.is_none()) {
126 return Ok(Value::Null);
127 }
128 serde_json::to_value(res).map_err(|error| RpcErr::Internal(error.to_string()))
129 }
130}
131
132impl RpcHandler for BlobsV3Request {
133 fn parse(params: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
134 let params = params
135 .as_ref()
136 .ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
137 if params.len() != 1 {
138 return Err(RpcErr::BadParams("Expected 1 param".to_owned()));
139 };
140 Ok(BlobsV3Request {
141 blob_versioned_hashes: serde_json::from_value(params[0].clone())?,
142 })
143 }
144
145 async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
146 debug!("Received new engine request: Requested Blobs V3");
147 let res = get_blobs_and_proof(&self.blob_versioned_hashes, context).await?;
148 serde_json::to_value(res).map_err(|error| RpcErr::Internal(error.to_string()))
149 }
150}
151
152async fn get_blobs_and_proof(
154 blob_versioned_hashes: &[H256],
155 context: RpcApiContext,
156) -> Result<Vec<Option<BlobAndProofV2>>, RpcErr> {
157 if blob_versioned_hashes.len() > GET_BLOBS_V1_REQUEST_MAX_SIZE {
158 return Err(RpcErr::TooLargeRequest);
159 }
160
161 let head_is_osaka = match context
169 .storage
170 .get_block_header(context.storage.get_latest_block_number().await?)?
171 {
172 Some(current_block_header) => context
173 .storage
174 .get_chain_config()
175 .is_osaka_activated(current_block_header.timestamp),
176 None => false,
178 };
179 if !head_is_osaka {
180 return Ok(vec![None; blob_versioned_hashes.len()]);
181 }
182
183 let blob_tuples = context
184 .blockchain
185 .mempool
186 .get_blobs_data_by_versioned_hashes(blob_versioned_hashes)?;
187
188 debug_assert_eq!(blob_versioned_hashes.len(), blob_tuples.len());
189
190 let res = blob_tuples
191 .into_iter()
192 .map(|b| {
193 b.map(|(blob, _, proofs)| BlobAndProofV2 {
194 blob: *blob,
195 proofs,
196 })
197 })
198 .collect();
199
200 Ok(res)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::test_utils::default_context_with_storage;
207 use ethrex_common::{
208 Address, H256,
209 types::{
210 BYTES_PER_BLOB, BlobsBundle, CELLS_PER_EXT_BLOB, ChainConfig, Commitment, Proof,
211 kzg_commitment_to_versioned_hash,
212 },
213 };
214 use ethrex_storage::{EngineType, Store};
215
216 fn sample_bundle(count: usize) -> (BlobsBundle, Vec<H256>) {
217 let blobs = vec![[1u8; BYTES_PER_BLOB]; count];
218 let commitments: Vec<Commitment> = (0..count).map(|i| [i as u8; 48]).collect();
219 let proofs: Vec<Proof> = vec![[2u8; 48]; count * CELLS_PER_EXT_BLOB];
220
221 let hashes = commitments
222 .iter()
223 .map(kzg_commitment_to_versioned_hash)
224 .collect();
225
226 let bundle = BlobsBundle {
227 blobs,
228 commitments,
229 proofs,
230 version: 1,
231 };
232 (bundle, hashes)
233 }
234
235 fn blob_and_proof(bundle: &BlobsBundle, index: usize) -> BlobAndProofV2 {
236 let start = index * CELLS_PER_EXT_BLOB;
237 let end = start + CELLS_PER_EXT_BLOB;
238 BlobAndProofV2 {
239 blob: bundle.blobs[index],
240 proofs: bundle.proofs[start..end].to_vec(),
241 }
242 }
243
244 fn chain_config(osaka_active: bool) -> ChainConfig {
245 ChainConfig {
246 chain_id: 1,
247 shanghai_time: Some(0),
248 cancun_time: Some(0),
249 prague_time: Some(0),
250 osaka_time: osaka_active.then_some(0),
251 deposit_contract_address: Address::zero(),
252 ..Default::default()
253 }
254 }
255
256 async fn context_with_chain_config(osaka_active: bool) -> RpcApiContext {
257 let mut storage =
258 Store::new("test-blobs", EngineType::InMemory).expect("Failed to create test store");
259 storage
260 .set_chain_config(&chain_config(osaka_active))
261 .await
262 .expect("Failed to set chain config");
263 default_context_with_storage(storage).await
264 }
265
266 #[tokio::test]
267 async fn blobs_v2_returns_null_when_missing_one() {
268 let context = context_with_chain_config(true).await;
269 let (bundle, hashes) = sample_bundle(2);
270 context
271 .blockchain
272 .mempool
273 .add_blobs_bundle(H256::from_low_u64_be(1), bundle)
274 .unwrap();
275
276 let request = BlobsV2Request {
277 blob_versioned_hashes: vec![hashes[0], H256::from_low_u64_be(999)],
278 };
279
280 let result = request.handle(context).await.unwrap();
281 assert_eq!(result, serde_json::Value::Null);
282 }
283
284 #[tokio::test]
285 async fn blobs_v2_returns_full_when_all_present() {
286 let context = context_with_chain_config(true).await;
287 let (bundle, hashes) = sample_bundle(2);
288 context
289 .blockchain
290 .mempool
291 .add_blobs_bundle(H256::from_low_u64_be(1), bundle.clone())
292 .unwrap();
293
294 let request = BlobsV2Request {
295 blob_versioned_hashes: hashes.clone(),
296 };
297
298 let result = request.handle(context).await.unwrap();
299 let expected = serde_json::to_value(vec![
300 Some(blob_and_proof(&bundle, 0)),
301 Some(blob_and_proof(&bundle, 1)),
302 ])
303 .unwrap();
304 assert_eq!(result, expected);
305 }
306
307 #[tokio::test]
308 async fn blobs_v3_returns_partial_results() {
309 let context = context_with_chain_config(true).await;
310 let (bundle, hashes) = sample_bundle(2);
311 context
312 .blockchain
313 .mempool
314 .add_blobs_bundle(H256::from_low_u64_be(1), bundle.clone())
315 .unwrap();
316
317 let request = BlobsV3Request {
318 blob_versioned_hashes: vec![hashes[0], H256::from_low_u64_be(999)],
319 };
320
321 let result = request.handle(context).await.unwrap();
322 let expected = serde_json::to_value(vec![Some(blob_and_proof(&bundle, 0)), None]).unwrap();
323 assert_eq!(result, expected);
324 }
325
326 #[tokio::test]
327 async fn blobs_v1_returns_full_before_osaka() {
328 let context = context_with_chain_config(false).await;
329 let (bundle, hashes) = sample_bundle(1);
330 context
331 .blockchain
332 .mempool
333 .add_blobs_bundle(H256::from_low_u64_be(1), bundle.clone())
334 .unwrap();
335
336 let request = BlobsV1Request {
337 blob_versioned_hashes: hashes,
338 };
339
340 let result = request.handle(context).await.unwrap();
341 let expected = serde_json::to_value(vec![Some(BlobAndProofV1 {
342 blob: bundle.blobs[0],
343 proof: bundle.proofs[0],
344 })])
345 .unwrap();
346 assert_eq!(result, expected);
347 }
348
349 #[tokio::test]
350 async fn blobs_v1_rejects_after_osaka() {
351 let context = context_with_chain_config(true).await;
352 let request = BlobsV1Request {
353 blob_versioned_hashes: vec![H256::from_low_u64_be(1)],
354 };
355
356 let err = request.handle(context).await.unwrap_err();
357 assert!(matches!(err, RpcErr::UnsupportedFork(_)));
358 }
359
360 #[tokio::test]
361 async fn blobs_v3_returns_null_before_osaka() {
362 let context = context_with_chain_config(false).await;
366 let request = BlobsV3Request {
367 blob_versioned_hashes: vec![H256::from_low_u64_be(1), H256::from_low_u64_be(2)],
368 };
369
370 let result = request.handle(context).await.unwrap();
371 let expected = serde_json::to_value(vec![None::<BlobAndProofV2>, None]).unwrap();
372 assert_eq!(result, expected);
373 }
374
375 #[tokio::test]
376 async fn blobs_v3_rejects_too_many_hashes() {
377 let context = context_with_chain_config(true).await;
378 let request = BlobsV3Request {
379 blob_versioned_hashes: vec![H256::zero(); GET_BLOBS_V1_REQUEST_MAX_SIZE + 1],
380 };
381
382 let err = request.handle(context).await.unwrap_err();
383 assert!(matches!(err, RpcErr::TooLargeRequest));
384 }
385
386 #[tokio::test]
387 async fn blobs_v3_accepts_exactly_max_size() {
388 let context = context_with_chain_config(true).await;
390 let request = BlobsV3Request {
391 blob_versioned_hashes: vec![H256::zero(); GET_BLOBS_V1_REQUEST_MAX_SIZE],
392 };
393 let result = request.handle(context).await;
394 assert!(!matches!(result, Err(RpcErr::TooLargeRequest)));
395 }
396
397 #[tokio::test]
398 async fn blobs_v1_accepts_exactly_max_size_before_osaka() {
399 let context = context_with_chain_config(false).await;
400 let request = BlobsV1Request {
401 blob_versioned_hashes: vec![H256::zero(); GET_BLOBS_V1_REQUEST_MAX_SIZE],
402 };
403 let result = request.handle(context).await;
404 assert!(!matches!(result, Err(RpcErr::TooLargeRequest)));
405 }
406}