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.and_then(|(blob, _, proofs)| {
97 (proofs.len() == 1).then(|| BlobAndProofV1 {
104 blob: *blob,
105 proof: proofs[0],
106 })
107 })
108 })
109 .collect();
110
111 serde_json::to_value(res).map_err(|error| RpcErr::Internal(error.to_string()))
112 }
113}
114
115impl RpcHandler for BlobsV2Request {
116 fn parse(params: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
117 let params = params
118 .as_ref()
119 .ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
120 if params.len() != 1 {
121 return Err(RpcErr::BadParams("Expected 1 param".to_owned()));
122 };
123 Ok(BlobsV2Request {
124 blob_versioned_hashes: serde_json::from_value(params[0].clone())?,
125 })
126 }
127
128 async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
129 debug!("Received new engine request: Requested Blobs V2");
130 let res = get_blobs_and_proof(&self.blob_versioned_hashes, context).await?;
131 if res.iter().any(|blob| blob.is_none()) {
132 return Ok(Value::Null);
133 }
134 serde_json::to_value(res).map_err(|error| RpcErr::Internal(error.to_string()))
135 }
136}
137
138impl RpcHandler for BlobsV3Request {
139 fn parse(params: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
140 let params = params
141 .as_ref()
142 .ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
143 if params.len() != 1 {
144 return Err(RpcErr::BadParams("Expected 1 param".to_owned()));
145 };
146 Ok(BlobsV3Request {
147 blob_versioned_hashes: serde_json::from_value(params[0].clone())?,
148 })
149 }
150
151 async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
152 debug!("Received new engine request: Requested Blobs V3");
153 let res = get_blobs_and_proof(&self.blob_versioned_hashes, context).await?;
154 serde_json::to_value(res).map_err(|error| RpcErr::Internal(error.to_string()))
155 }
156}
157
158async fn get_blobs_and_proof(
160 blob_versioned_hashes: &[H256],
161 context: RpcApiContext,
162) -> Result<Vec<Option<BlobAndProofV2>>, RpcErr> {
163 if blob_versioned_hashes.len() > GET_BLOBS_V1_REQUEST_MAX_SIZE {
164 return Err(RpcErr::TooLargeRequest);
165 }
166
167 let head_is_osaka = match context
175 .storage
176 .get_block_header(context.storage.get_latest_block_number().await?)?
177 {
178 Some(current_block_header) => context
179 .storage
180 .get_chain_config()
181 .is_osaka_activated(current_block_header.timestamp),
182 None => false,
184 };
185 if !head_is_osaka {
186 return Ok(vec![None; blob_versioned_hashes.len()]);
187 }
188
189 let blob_tuples = context
190 .blockchain
191 .mempool
192 .get_blobs_data_by_versioned_hashes(blob_versioned_hashes)?;
193
194 debug_assert_eq!(blob_versioned_hashes.len(), blob_tuples.len());
195
196 let res = blob_tuples
197 .into_iter()
198 .map(|b| {
199 b.map(|(blob, _, proofs)| BlobAndProofV2 {
200 blob: *blob,
201 proofs,
202 })
203 })
204 .collect();
205
206 Ok(res)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::test_utils::default_context_with_storage;
213 use ethrex_common::{
214 Address, H256,
215 types::{
216 BYTES_PER_BLOB, BlobsBundle, CELLS_PER_EXT_BLOB, ChainConfig, Commitment, Proof,
217 kzg_commitment_to_versioned_hash,
218 },
219 };
220 use ethrex_storage::{EngineType, Store};
221
222 fn sample_bundle(count: usize) -> (BlobsBundle, Vec<H256>) {
223 let blobs = vec![[1u8; BYTES_PER_BLOB]; count];
224 let commitments: Vec<Commitment> = (0..count).map(|i| [i as u8; 48]).collect();
225 let proofs: Vec<Proof> = vec![[2u8; 48]; count * CELLS_PER_EXT_BLOB];
226
227 let hashes = commitments
228 .iter()
229 .map(kzg_commitment_to_versioned_hash)
230 .collect();
231
232 let bundle = BlobsBundle {
233 blobs,
234 commitments,
235 proofs,
236 version: 1,
237 };
238 (bundle, hashes)
239 }
240
241 fn sample_v0_bundle(count: usize) -> (BlobsBundle, Vec<H256>) {
242 let blobs = vec![[1u8; BYTES_PER_BLOB]; count];
243 let commitments: Vec<Commitment> = (0..count).map(|i| [i as u8; 48]).collect();
244 let proofs: Vec<Proof> = vec![[2u8; 48]; count];
246
247 let hashes = commitments
248 .iter()
249 .map(kzg_commitment_to_versioned_hash)
250 .collect();
251
252 let bundle = BlobsBundle {
253 blobs,
254 commitments,
255 proofs,
256 version: 0,
257 };
258 (bundle, hashes)
259 }
260
261 fn blob_and_proof(bundle: &BlobsBundle, index: usize) -> BlobAndProofV2 {
262 let start = index * CELLS_PER_EXT_BLOB;
263 let end = start + CELLS_PER_EXT_BLOB;
264 BlobAndProofV2 {
265 blob: bundle.blobs[index],
266 proofs: bundle.proofs[start..end].to_vec(),
267 }
268 }
269
270 fn chain_config(osaka_active: bool) -> ChainConfig {
271 ChainConfig {
272 chain_id: 1,
273 shanghai_time: Some(0),
274 cancun_time: Some(0),
275 prague_time: Some(0),
276 osaka_time: osaka_active.then_some(0),
277 deposit_contract_address: Address::zero(),
278 ..Default::default()
279 }
280 }
281
282 async fn context_with_chain_config(osaka_active: bool) -> RpcApiContext {
283 let mut storage =
284 Store::new("test-blobs", EngineType::InMemory).expect("Failed to create test store");
285 storage
286 .set_chain_config(&chain_config(osaka_active))
287 .await
288 .expect("Failed to set chain config");
289 default_context_with_storage(storage).await
290 }
291
292 #[tokio::test]
293 async fn blobs_v2_returns_null_when_missing_one() {
294 let context = context_with_chain_config(true).await;
295 let (bundle, hashes) = sample_bundle(2);
296 context
297 .blockchain
298 .mempool
299 .add_blobs_bundle(H256::from_low_u64_be(1), bundle)
300 .unwrap();
301
302 let request = BlobsV2Request {
303 blob_versioned_hashes: vec![hashes[0], H256::from_low_u64_be(999)],
304 };
305
306 let result = request.handle(context).await.unwrap();
307 assert_eq!(result, serde_json::Value::Null);
308 }
309
310 #[tokio::test]
311 async fn blobs_v2_returns_full_when_all_present() {
312 let context = context_with_chain_config(true).await;
313 let (bundle, hashes) = sample_bundle(2);
314 context
315 .blockchain
316 .mempool
317 .add_blobs_bundle(H256::from_low_u64_be(1), bundle.clone())
318 .unwrap();
319
320 let request = BlobsV2Request {
321 blob_versioned_hashes: hashes.clone(),
322 };
323
324 let result = request.handle(context).await.unwrap();
325 let expected = serde_json::to_value(vec![
326 Some(blob_and_proof(&bundle, 0)),
327 Some(blob_and_proof(&bundle, 1)),
328 ])
329 .unwrap();
330 assert_eq!(result, expected);
331 }
332
333 #[tokio::test]
334 async fn blobs_v3_returns_partial_results() {
335 let context = context_with_chain_config(true).await;
336 let (bundle, hashes) = sample_bundle(2);
337 context
338 .blockchain
339 .mempool
340 .add_blobs_bundle(H256::from_low_u64_be(1), bundle.clone())
341 .unwrap();
342
343 let request = BlobsV3Request {
344 blob_versioned_hashes: vec![hashes[0], H256::from_low_u64_be(999)],
345 };
346
347 let result = request.handle(context).await.unwrap();
348 let expected = serde_json::to_value(vec![Some(blob_and_proof(&bundle, 0)), None]).unwrap();
349 assert_eq!(result, expected);
350 }
351
352 #[tokio::test]
353 async fn blobs_v1_returns_v0_proof_before_osaka() {
354 let context = context_with_chain_config(false).await;
355 let (bundle, hashes) = sample_v0_bundle(1);
356 context
357 .blockchain
358 .mempool
359 .add_blobs_bundle(H256::from_low_u64_be(1), bundle.clone())
360 .unwrap();
361
362 let request = BlobsV1Request {
363 blob_versioned_hashes: hashes,
364 };
365
366 let result = request.handle(context).await.unwrap();
367 let expected = serde_json::to_value(vec![Some(BlobAndProofV1 {
368 blob: bundle.blobs[0],
369 proof: bundle.proofs[0],
370 })])
371 .unwrap();
372 assert_eq!(result, expected);
373 }
374
375 #[tokio::test]
376 async fn blobs_v1_returns_null_for_v1_sidecar_before_osaka() {
377 let context = context_with_chain_config(false).await;
381 let (bundle, hashes) = sample_bundle(1);
382 context
383 .blockchain
384 .mempool
385 .add_blobs_bundle(H256::from_low_u64_be(1), bundle)
386 .unwrap();
387
388 let request = BlobsV1Request {
389 blob_versioned_hashes: hashes,
390 };
391
392 let result = request.handle(context).await.unwrap();
393 let expected = serde_json::to_value(vec![None::<BlobAndProofV1>]).unwrap();
394 assert_eq!(result, expected);
395 }
396
397 #[tokio::test]
398 async fn blobs_v1_rejects_after_osaka() {
399 let context = context_with_chain_config(true).await;
400 let request = BlobsV1Request {
401 blob_versioned_hashes: vec![H256::from_low_u64_be(1)],
402 };
403
404 let err = request.handle(context).await.unwrap_err();
405 assert!(matches!(err, RpcErr::UnsupportedFork(_)));
406 }
407
408 #[tokio::test]
409 async fn blobs_v3_returns_null_before_osaka() {
410 let context = context_with_chain_config(false).await;
414 let request = BlobsV3Request {
415 blob_versioned_hashes: vec![H256::from_low_u64_be(1), H256::from_low_u64_be(2)],
416 };
417
418 let result = request.handle(context).await.unwrap();
419 let expected = serde_json::to_value(vec![None::<BlobAndProofV2>, None]).unwrap();
420 assert_eq!(result, expected);
421 }
422
423 #[tokio::test]
424 async fn blobs_v3_rejects_too_many_hashes() {
425 let context = context_with_chain_config(true).await;
426 let request = BlobsV3Request {
427 blob_versioned_hashes: vec![H256::zero(); GET_BLOBS_V1_REQUEST_MAX_SIZE + 1],
428 };
429
430 let err = request.handle(context).await.unwrap_err();
431 assert!(matches!(err, RpcErr::TooLargeRequest));
432 }
433
434 #[tokio::test]
435 async fn blobs_v3_accepts_exactly_max_size() {
436 let context = context_with_chain_config(true).await;
438 let request = BlobsV3Request {
439 blob_versioned_hashes: vec![H256::zero(); GET_BLOBS_V1_REQUEST_MAX_SIZE],
440 };
441 let result = request.handle(context).await;
442 assert!(!matches!(result, Err(RpcErr::TooLargeRequest)));
443 }
444
445 #[tokio::test]
446 async fn blobs_v1_accepts_exactly_max_size_before_osaka() {
447 let context = context_with_chain_config(false).await;
448 let request = BlobsV1Request {
449 blob_versioned_hashes: vec![H256::zero(); GET_BLOBS_V1_REQUEST_MAX_SIZE],
450 };
451 let result = request.handle(context).await;
452 assert!(!matches!(result, Err(RpcErr::TooLargeRequest)));
453 }
454}