diff --git a/crates/derive/src/online/blob_provider.rs b/crates/derive/src/online/blob_provider.rs index bd0336d40..156e1a195 100644 --- a/crates/derive/src/online/blob_provider.rs +++ b/crates/derive/src/online/blob_provider.rs @@ -1,15 +1,15 @@ //! Contains an online implementation of the [BlobProvider] trait. use crate::{ - online::{blobs_from_sidecars, BeaconClient}, + online::BeaconClient, traits::BlobProvider, - types::{APIBlobSidecar, Blob, BlobSidecar, BlockInfo, IndexedBlobHash}, + types::{APIBlobSidecar, Blob, BlobProviderError, BlobSidecar, BlockInfo, IndexedBlobHash}, }; use alloc::{boxed::Box, vec::Vec}; use alloy_provider::Provider; use alloy_transport_http::Http; use async_trait::async_trait; -use core::{fmt::Display, marker::PhantomData}; +use core::marker::PhantomData; use reqwest::Client; use tracing::debug; @@ -19,42 +19,6 @@ pub trait SlotDerivation { fn slot(genesis: u64, slot_time: u64, timestamp: u64) -> anyhow::Result; } -/// An error returned by the [OnlineBlobProvider]. -#[derive(Debug)] -pub enum OnlineBlobProviderError { - /// The number of specified blob hashes did not match the number of returned sidecars. - SidecarLengthMismatch(usize, usize), - /// A custom [anyhow::Error] occurred. - Custom(anyhow::Error), -} - -impl PartialEq for OnlineBlobProviderError { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::SidecarLengthMismatch(a, b), Self::SidecarLengthMismatch(c, d)) => { - a == c && b == d - } - (Self::Custom(_), Self::Custom(_)) => true, - _ => false, - } - } -} - -impl Display for OnlineBlobProviderError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::SidecarLengthMismatch(a, b) => write!(f, "expected {} sidecars but got {}", a, b), - Self::Custom(err) => write!(f, "{}", err), - } - } -} - -impl From for OnlineBlobProviderError { - fn from(err: anyhow::Error) -> Self { - Self::Custom(err) - } -} - /// An online implementation of the [BlobProvider] trait. #[derive(Debug, Clone)] pub struct OnlineBlobProvider>, B: BeaconClient, S: SlotDerivation> { @@ -96,7 +60,7 @@ impl>, B: BeaconClient, S: SlotDerivation> OnlineBlobPr } /// Loads the beacon genesis and config spec - pub async fn load_configs(&mut self) -> Result<(), OnlineBlobProviderError> { + pub async fn load_configs(&mut self) -> Result<(), BlobProviderError> { if self.genesis_time.is_none() { debug!("Loading missing BeaconGenesis"); self.genesis_time = Some(self.beacon_client.beacon_genesis().await?.data.genesis_time); @@ -114,22 +78,47 @@ impl>, B: BeaconClient, S: SlotDerivation> OnlineBlobPr &self, slot: u64, hashes: &[IndexedBlobHash], - ) -> Result, OnlineBlobProviderError> { + ) -> Result, BlobProviderError> { self.beacon_client .beacon_blob_side_cars(self.fetch_all_sidecars, slot, hashes) .await .map(|r| r.data) .map_err(|e| e.into()) } +} + +/// Minimal slot derivation implementation. +#[derive(Debug, Default, Clone)] +pub struct SimpleSlotDerivation; + +impl SlotDerivation for SimpleSlotDerivation { + fn slot(genesis: u64, slot_time: u64, timestamp: u64) -> anyhow::Result { + if timestamp < genesis { + return Err(anyhow::anyhow!( + "provided timestamp ({}) precedes genesis time ({})", + timestamp, + genesis + )); + } + Ok((timestamp - genesis) / slot_time) + } +} +#[async_trait] +impl BlobProvider for OnlineBlobProvider +where + T: Provider> + Send, + B: BeaconClient + Send + Sync, + S: SlotDerivation + Send + Sync, +{ /// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed - /// hashes. Order of the returned sidecars is guaranteed to be that of the hashes. Blob data is - /// not checked for validity. - pub async fn get_blob_sidecars( + /// hashes. The blobs are validated for their index and hashes using the specified + /// [IndexedBlobHash]. + async fn get_blobs( &mut self, block_ref: &BlockInfo, blob_hashes: &[IndexedBlobHash], - ) -> Result, OnlineBlobProviderError> { + ) -> Result, BlobProviderError> { if blob_hashes.is_empty() { return Ok(Vec::new()); } @@ -143,7 +132,8 @@ impl>, B: BeaconClient, S: SlotDerivation> OnlineBlobPr let interval = self.slot_interval.expect("Config Spec Loaded"); // Calculate the slot for the given timestamp. - let slot = S::slot(genesis, interval, block_ref.timestamp)?; + let slot = + S::slot(genesis, interval, block_ref.timestamp).map_err(BlobProviderError::Slot)?; // Fetch blob sidecars for the slot using the given blob hashes. let sidecars = self.fetch_sidecars(slot, blob_hashes).await?; @@ -157,50 +147,24 @@ impl>, B: BeaconClient, S: SlotDerivation> OnlineBlobPr // Validate the correct number of blob sidecars were retrieved. if blob_hashes.len() != filtered.len() { - return Err(OnlineBlobProviderError::SidecarLengthMismatch( - blob_hashes.len(), - filtered.len(), - )); + return Err(BlobProviderError::SidecarLengthMismatch(blob_hashes.len(), filtered.len())); } - Ok(filtered.into_iter().map(|s| s.inner).collect::>()) - } -} - -/// Minimal slot derivation implementation. -#[derive(Debug, Default, Clone)] -pub struct SimpleSlotDerivation; - -impl SlotDerivation for SimpleSlotDerivation { - fn slot(genesis: u64, slot_time: u64, timestamp: u64) -> anyhow::Result { - if timestamp < genesis { - return Err(anyhow::anyhow!( - "provided timestamp ({}) precedes genesis time ({})", - timestamp, - genesis - )); - } - Ok((timestamp - genesis) / slot_time) - } -} + // Validate the blob sidecars straight away with the `IndexedBlobHash`es. + let sidecars = filtered.into_iter().map(|s| s.inner).collect::>(); + let blobs = sidecars + .into_iter() + .enumerate() + .map(|(i, sidecar)| { + let hash = blob_hashes.get(i).ok_or(anyhow::anyhow!("failed to get blob hash"))?; + match sidecar.verify_blob(hash) { + Ok(_) => Ok(sidecar.blob), + Err(e) => Err(e), + } + }) + .collect::>>()?; -#[async_trait] -impl BlobProvider for OnlineBlobProvider -where - T: Provider> + Send, - B: BeaconClient + Send + Sync, - S: SlotDerivation + Send + Sync, -{ - async fn get_blobs( - &mut self, - block_ref: &BlockInfo, - blob_hashes: Vec, - ) -> anyhow::Result> { - let sidecars = self - .get_blob_sidecars(block_ref, &blob_hashes) - .await - .map_err(|e| anyhow::anyhow!(e))?; - blobs_from_sidecars(&sidecars, &blob_hashes) + Ok(blobs) } } @@ -214,6 +178,24 @@ mod tests { use alloc::vec; use alloy_primitives::b256; + #[tokio::test] + async fn test_load_config_succeeds() { + let (provider, _anvil) = spawn_anvil(); + let genesis_time = 10; + let seconds_per_slot = 12; + let beacon_client = MockBeaconClient { + beacon_genesis: Some(APIGenesisResponse::new(genesis_time)), + config_spec: Some(APIConfigResponse::new(seconds_per_slot)), + ..Default::default() + }; + let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = + OnlineBlobProvider::new(provider, true, beacon_client, None, None); + let result = blob_provider.load_configs().await; + assert!(result.is_ok()); + assert_eq!(blob_provider.genesis_time, Some(genesis_time)); + assert_eq!(blob_provider.slot_interval, Some(seconds_per_slot)); + } + #[tokio::test] async fn test_get_blobs() { let (provider, _anvil) = spawn_anvil(); @@ -250,39 +232,39 @@ mod tests { let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo { timestamp: 15, ..Default::default() }; - let blobs = blob_provider.get_blobs(&block_ref, blob_hashes).await.unwrap(); + let blobs = blob_provider.get_blobs(&block_ref, &blob_hashes).await.unwrap(); assert_eq!(blobs.len(), 5); } #[tokio::test] - async fn test_get_blob_sidecars_empty_hashes() { + async fn test_get_blobs_empty_hashes() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient::default(); let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo::default(); let blob_hashes = Vec::new(); - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; assert!(result.unwrap().is_empty()); } #[tokio::test] - async fn test_get_blob_sidecars_beacon_genesis_fetch_fails() { + async fn test_get_blobs_beacon_genesis_fetch_fails() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient::default(); let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo::default(); let blob_hashes = vec![IndexedBlobHash::default()]; - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; assert_eq!( result.unwrap_err(), - OnlineBlobProviderError::Custom(anyhow::anyhow!("failed to get beacon genesis")) + BlobProviderError::Custom(anyhow::anyhow!("failed to get beacon genesis")) ); } #[tokio::test] - async fn test_get_blob_sidecars_config_spec_fetch_fails() { + async fn test_get_blobs_config_spec_fetch_fails() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient { beacon_genesis: Some(APIGenesisResponse::default()), @@ -292,33 +274,36 @@ mod tests { OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo::default(); let blob_hashes = vec![IndexedBlobHash::default()]; - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; assert_eq!( result.unwrap_err(), - OnlineBlobProviderError::Custom(anyhow::anyhow!("failed to get config spec")) + BlobProviderError::Custom(anyhow::anyhow!("failed to get config spec")) ); } #[tokio::test] - async fn test_load_config_succeeds() { + async fn test_get_blobs_before_genesis_fails() { let (provider, _anvil) = spawn_anvil(); - let genesis_time = 10; - let seconds_per_slot = 12; let beacon_client = MockBeaconClient { - beacon_genesis: Some(APIGenesisResponse::new(genesis_time)), - config_spec: Some(APIConfigResponse::new(seconds_per_slot)), + beacon_genesis: Some(APIGenesisResponse::new(10)), + config_spec: Some(APIConfigResponse::new(12)), ..Default::default() }; let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); - let result = blob_provider.load_configs().await; - assert!(result.is_ok()); - assert_eq!(blob_provider.genesis_time, Some(genesis_time)); - assert_eq!(blob_provider.slot_interval, Some(seconds_per_slot)); + let block_ref = BlockInfo { timestamp: 5, ..Default::default() }; + let blob_hashes = vec![IndexedBlobHash::default()]; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; + assert_eq!( + result.unwrap_err(), + BlobProviderError::Slot(anyhow::anyhow!( + "provided timestamp (5) precedes genesis time (10)" + )) + ); } #[tokio::test] - async fn test_get_blob_sidecars_before_genesis_fails() { + async fn test_get_blob_sidecars_fetch_fails() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient { beacon_genesis: Some(APIGenesisResponse::new(10)), @@ -327,71 +312,123 @@ mod tests { }; let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); - let block_ref = BlockInfo { timestamp: 5, ..Default::default() }; + let block_ref = BlockInfo { timestamp: 15, ..Default::default() }; let blob_hashes = vec![IndexedBlobHash::default()]; - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; assert_eq!( result.unwrap_err(), - OnlineBlobProviderError::Custom(anyhow::anyhow!( - "provided timestamp (5) precedes genesis time (10)" - )) + BlobProviderError::Custom(anyhow::anyhow!("blob_sidecars not set")) ); } #[tokio::test] - async fn test_get_blob_sidecars_fetch_fails() { + async fn test_get_blob_sidecars_length_mismatch() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient { beacon_genesis: Some(APIGenesisResponse::new(10)), config_spec: Some(APIConfigResponse::new(12)), + blob_sidecars: Some(APIGetBlobSidecarsResponse { + data: vec![APIBlobSidecar::default()], + }), ..Default::default() }; let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo { timestamp: 15, ..Default::default() }; - let blob_hashes = vec![IndexedBlobHash::default()]; - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; + let blob_hashes = vec![IndexedBlobHash { index: 1, ..Default::default() }]; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; + assert_eq!(result.unwrap_err(), BlobProviderError::SidecarLengthMismatch(1, 0)); + } + + #[tokio::test] + async fn test_get_blobs_invalid_ordering() { + let (provider, _anvil) = spawn_anvil(); + let json_bytes = include_bytes!("testdata/eth_v1_beacon_sidecars_goerli.json"); + let sidecars: APIGetBlobSidecarsResponse = serde_json::from_slice(json_bytes).unwrap(); + let beacon_client = MockBeaconClient { + beacon_genesis: Some(APIGenesisResponse::new(10)), + config_spec: Some(APIConfigResponse::new(12)), + blob_sidecars: Some(sidecars), + ..Default::default() + }; + let blob_hashes = vec![ + IndexedBlobHash { + index: 4, + hash: b256!("01e5ee2f6cbbafb3c03f05f340e795fe5b5a8edbcc9ac3fc7bd3d1940b99ef3c"), + }, + IndexedBlobHash { + index: 0, + hash: b256!("011075cbb20f3235b3179a5dff22689c410cd091692180f4b6a12be77ea0f586"), + }, + IndexedBlobHash { + index: 1, + hash: b256!("010a9e10aab79bab62e10a5b83c164a91451b6ef56d31ac95a9514ffe6d6b4e6"), + }, + IndexedBlobHash { + index: 2, + hash: b256!("016122c8e41c69917b688240707d107aa6d2a480343e4e323e564241769a6b4a"), + }, + IndexedBlobHash { + index: 3, + hash: b256!("01df1f9ae707f5847513c9c430b683182079edf2b1f94ee12e4daae7f3c8c309"), + }, + ]; + let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = + OnlineBlobProvider::new(provider, true, beacon_client, None, None); + let block_ref = BlockInfo { timestamp: 15, ..Default::default() }; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; assert_eq!( result.unwrap_err(), - OnlineBlobProviderError::Custom(anyhow::anyhow!("blob_sidecars not set")) + BlobProviderError::Custom(anyhow::anyhow!( + "invalid sidecar ordering, blob hash index 4 does not match sidecar index 0" + )) ); } #[tokio::test] - async fn test_get_blob_sidecars_length_mismatch() { + async fn test_get_blobs_invalid_hash() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient { beacon_genesis: Some(APIGenesisResponse::new(10)), config_spec: Some(APIConfigResponse::new(12)), blob_sidecars: Some(APIGetBlobSidecarsResponse { - data: vec![APIBlobSidecar::default()], + data: vec![APIBlobSidecar { inner: BlobSidecar::default(), ..Default::default() }], }), ..Default::default() }; let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo { timestamp: 15, ..Default::default() }; - let blob_hashes = vec![IndexedBlobHash { index: 1, ..Default::default() }]; - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; - assert_eq!(result.unwrap_err(), OnlineBlobProviderError::SidecarLengthMismatch(1, 0)); + let blob_hashes = vec![IndexedBlobHash { + hash: alloy_primitives::FixedBytes::from([1; 32]), + ..Default::default() + }]; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; + assert_eq!(result.unwrap_err(), BlobProviderError::Custom(anyhow::anyhow!("expected hash 0x0101010101010101010101010101010101010101010101010101010101010101 for blob at index 0 but got 0x01b0761f87b081d5cf10757ccc89f12be355c70e2e29df288b65b30710dcbcd1"))); } #[tokio::test] - async fn test_get_blob_sidecars_succeeds() { + async fn test_get_blobs_failed_verification() { let (provider, _anvil) = spawn_anvil(); let beacon_client = MockBeaconClient { beacon_genesis: Some(APIGenesisResponse::new(10)), config_spec: Some(APIConfigResponse::new(12)), blob_sidecars: Some(APIGetBlobSidecarsResponse { - data: vec![APIBlobSidecar::default()], + data: vec![APIBlobSidecar { inner: BlobSidecar::default(), ..Default::default() }], }), ..Default::default() }; let mut blob_provider: OnlineBlobProvider<_, _, SimpleSlotDerivation> = OnlineBlobProvider::new(provider, true, beacon_client, None, None); let block_ref = BlockInfo { timestamp: 15, ..Default::default() }; - let blob_hashes = vec![IndexedBlobHash::default()]; - let result = blob_provider.get_blob_sidecars(&block_ref, &blob_hashes).await; - assert!(result.is_ok()); + let blob_hashes = vec![IndexedBlobHash { + hash: b256!("01b0761f87b081d5cf10757ccc89f12be355c70e2e29df288b65b30710dcbcd1"), + ..Default::default() + }]; + let result = blob_provider.get_blobs(&block_ref, &blob_hashes).await; + assert_eq!( + result.unwrap_err(), + BlobProviderError::Custom(anyhow::anyhow!("blob at index 0 failed verification")) + ); } } diff --git a/crates/derive/src/online/mod.rs b/crates/derive/src/online/mod.rs index baff451a3..90d9099f1 100644 --- a/crates/derive/src/online/mod.rs +++ b/crates/derive/src/online/mod.rs @@ -63,6 +63,3 @@ pub use alloy_providers::{AlloyChainProvider, AlloyL2ChainProvider}; mod blob_provider; pub use blob_provider::{OnlineBlobProvider, SimpleSlotDerivation}; - -mod utils; -pub(crate) use utils::blobs_from_sidecars; diff --git a/crates/derive/src/online/utils.rs b/crates/derive/src/online/utils.rs deleted file mode 100644 index be7ddccd1..000000000 --- a/crates/derive/src/online/utils.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Contains utilities for online providers. - -use crate::types::{Blob, BlobSidecar, IndexedBlobHash}; -use alloc::vec::Vec; -use alloy_primitives::B256; - -/// Constructs a list of [Blob]s from [BlobSidecar]s and the specified [IndexedBlobHash]es. -pub(crate) fn blobs_from_sidecars( - sidecars: &[BlobSidecar], - hashes: &[IndexedBlobHash], -) -> anyhow::Result> { - if sidecars.len() != hashes.len() { - return Err(anyhow::anyhow!( - "blob sidecars and hashes length mismatch, {} != {}", - sidecars.len(), - hashes.len() - )); - } - - let mut blobs = Vec::with_capacity(sidecars.len()); - for (i, sidecar) in sidecars.iter().enumerate() { - let hash = hashes.get(i).ok_or(anyhow::anyhow!("failed to get blob hash"))?; - if sidecar.index as usize != hash.index { - return Err(anyhow::anyhow!( - "invalid sidecar ordering, blob hash index {} does not match sidecar index {}", - hash.index, - sidecar.index - )); - } - - // Ensure the blob's kzg commitment hashes to the expected value. - if sidecar.to_kzg_versioned_hash() != hash.hash { - return Err(anyhow::anyhow!( - "expected hash {} for blob at index {} but got {}", - hash.hash, - hash.index, - B256::from(sidecar.to_kzg_versioned_hash()) - )); - } - - // Confirm blob data is valid by verifying its proof against the commitment - match sidecar.verify_blob_kzg_proof() { - Ok(true) => (), - Ok(false) => { - return Err(anyhow::anyhow!("blob at index {} failed verification", i)); - } - Err(e) => { - return Err(anyhow::anyhow!("blob at index {} failed verification: {}", i, e)); - } - } - - blobs.push(sidecar.blob); - } - Ok(blobs) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::APIGetBlobSidecarsResponse; - use alloc::{string::ToString, vec}; - use alloy_primitives::{b256, FixedBytes}; - - #[test] - fn test_blobs_from_sidecars_length_mismatch() { - let sidecars = vec![BlobSidecar::default()]; - let hashes = vec![IndexedBlobHash::default(), IndexedBlobHash::default()]; - let err = blobs_from_sidecars(&sidecars, &hashes).unwrap_err(); - assert_eq!(err.to_string(), "blob sidecars and hashes length mismatch, 1 != 2"); - } - - #[test] - fn test_blobs_from_sidecars_invalid_ordering() { - let sidecars = vec![BlobSidecar::default()]; - let hashes = vec![IndexedBlobHash { index: 1, ..Default::default() }]; - let err = blobs_from_sidecars(&sidecars, &hashes).unwrap_err(); - assert_eq!( - err.to_string(), - "invalid sidecar ordering, blob hash index 1 does not match sidecar index 0" - ); - } - - #[test] - fn test_blobs_from_sidecars_invalid_hash() { - let sidecars = vec![BlobSidecar::default()]; - let hashes = - vec![IndexedBlobHash { hash: FixedBytes::from([1; 32]), ..Default::default() }]; - let err = blobs_from_sidecars(&sidecars, &hashes).unwrap_err(); - assert_eq!( - err.to_string(), - "expected hash 0x0101010101010101010101010101010101010101010101010101010101010101 for blob at index 0 but got 0x01b0761f87b081d5cf10757ccc89f12be355c70e2e29df288b65b30710dcbcd1" - ); - } - - #[test] - fn test_blobs_from_sidecars_failed_verification() { - let sidecars = vec![BlobSidecar::default()]; - let hashes = vec![IndexedBlobHash { - hash: b256!("01b0761f87b081d5cf10757ccc89f12be355c70e2e29df288b65b30710dcbcd1"), - ..Default::default() - }]; - let err = blobs_from_sidecars(&sidecars, &hashes).unwrap_err(); - assert_eq!(err.to_string(), "blob at index 0 failed verification"); - } - - #[test] - fn test_blobs_from_sidecars_succeeds() { - // Read in the test data - let json_bytes = include_bytes!("testdata/eth_v1_beacon_sidecars_goerli.json"); - let sidecars: APIGetBlobSidecarsResponse = serde_json::from_slice(json_bytes).unwrap(); - let hashes = vec![ - IndexedBlobHash { - index: 0, - hash: b256!("011075cbb20f3235b3179a5dff22689c410cd091692180f4b6a12be77ea0f586"), - }, - IndexedBlobHash { - index: 1, - hash: b256!("010a9e10aab79bab62e10a5b83c164a91451b6ef56d31ac95a9514ffe6d6b4e6"), - }, - IndexedBlobHash { - index: 2, - hash: b256!("016122c8e41c69917b688240707d107aa6d2a480343e4e323e564241769a6b4a"), - }, - IndexedBlobHash { - index: 3, - hash: b256!("01df1f9ae707f5847513c9c430b683182079edf2b1f94ee12e4daae7f3c8c309"), - }, - IndexedBlobHash { - index: 4, - hash: b256!("01e5ee2f6cbbafb3c03f05f340e795fe5b5a8edbcc9ac3fc7bd3d1940b99ef3c"), - }, - ]; - let blob_sidecars = sidecars.data.into_iter().map(|s| s.inner).collect::>(); - let blobs = blobs_from_sidecars(&blob_sidecars, &hashes).unwrap(); - assert_eq!(blobs.len(), 5); - for (i, blob) in blobs.iter().enumerate() { - assert_eq!(blob.len(), 131072, "blob {} has incorrect length", i); - } - } -} diff --git a/crates/derive/src/sources/blobs.rs b/crates/derive/src/sources/blobs.rs index 245168dff..59405cacb 100644 --- a/crates/derive/src/sources/blobs.rs +++ b/crates/derive/src/sources/blobs.rs @@ -136,7 +136,11 @@ where return Ok(()); } - let blobs = self.blob_fetcher.get_blobs(&self.block_ref, blob_hashes).await?; + let blobs = + self.blob_fetcher.get_blobs(&self.block_ref, &blob_hashes).await.map_err(|e| { + warn!("Failed to fetch blobs: {e}"); + anyhow::anyhow!("Failed to fetch blobs: {e}") + })?; // Fill the blob pointers. let mut blob_index = 0; diff --git a/crates/derive/src/traits/data_sources.rs b/crates/derive/src/traits/data_sources.rs index 6c1e2b0a2..b5a4b2d90 100644 --- a/crates/derive/src/traits/data_sources.rs +++ b/crates/derive/src/traits/data_sources.rs @@ -1,7 +1,7 @@ //! Contains traits that describe the functionality of various data sources used in the derivation //! pipeline's stages. -use crate::types::{Blob, BlockInfo, IndexedBlobHash, StageResult}; +use crate::types::{Blob, BlobProviderError, BlockInfo, IndexedBlobHash, StageResult}; use alloc::{boxed::Box, fmt::Debug, vec::Vec}; use alloy_primitives::{Address, Bytes}; use anyhow::Result; @@ -14,8 +14,8 @@ pub trait BlobProvider { async fn get_blobs( &mut self, block_ref: &BlockInfo, - blob_hashes: Vec, - ) -> Result>; + blob_hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError>; } /// Describes the functionality of a data source that can provide data availability information. diff --git a/crates/derive/src/types/errors.rs b/crates/derive/src/types/errors.rs index afed20ff3..a0f413e30 100644 --- a/crates/derive/src/types/errors.rs +++ b/crates/derive/src/types/errors.rs @@ -123,6 +123,47 @@ impl Display for StageError { } } +/// An error returned by the [BlobProviderError]. +#[derive(Debug)] +pub enum BlobProviderError { + /// The number of specified blob hashes did not match the number of returned sidecars. + SidecarLengthMismatch(usize, usize), + /// Slot derivation error. + Slot(anyhow::Error), + /// A custom [anyhow::Error] occurred. + Custom(anyhow::Error), +} + +impl PartialEq for BlobProviderError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::SidecarLengthMismatch(a, b), Self::SidecarLengthMismatch(c, d)) => { + a == c && b == d + } + (Self::Slot(_), Self::Slot(_)) | (Self::Custom(_), Self::Custom(_)) => true, + _ => false, + } + } +} + +impl Display for BlobProviderError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::SidecarLengthMismatch(a, b) => write!(f, "expected {} sidecars but got {}", a, b), + Self::Slot(e) => { + write!(f, "Slot Derivation Error: {}", e) + } + Self::Custom(err) => write!(f, "{}", err), + } + } +} + +impl From for BlobProviderError { + fn from(err: anyhow::Error) -> Self { + Self::Custom(err) + } +} + /// A reset error #[derive(Debug)] pub enum ResetError { diff --git a/crates/derive/src/types/sidecar.rs b/crates/derive/src/types/sidecar.rs index 3d2a34dc5..6ca1c6b2e 100644 --- a/crates/derive/src/types/sidecar.rs +++ b/crates/derive/src/types/sidecar.rs @@ -3,6 +3,11 @@ use crate::types::Blob; use alloc::{string::String, vec::Vec}; use alloy_primitives::FixedBytes; + +#[cfg(feature = "online")] +use crate::types::IndexedBlobHash; +#[cfg(feature = "online")] +use alloy_primitives::B256; #[cfg(feature = "online")] use c_kzg::{Bytes48, KzgProof, KzgSettings}; #[cfg(feature = "online")] @@ -37,6 +42,7 @@ where { String::deserialize(de)?.parse().map_err(serde::de::Error::custom) } + /// A blob sidecar. #[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -55,6 +61,37 @@ pub struct BlobSidecar { } impl BlobSidecar { + /// Verify the blob sidecar against it's [IndexedBlobHash]. + #[cfg(feature = "online")] + pub fn verify_blob(&self, hash: &IndexedBlobHash) -> anyhow::Result<()> { + if self.index as usize != hash.index { + return Err(anyhow::anyhow!( + "invalid sidecar ordering, blob hash index {} does not match sidecar index {}", + hash.index, + self.index + )); + } + + // Ensure the blob's kzg commitment hashes to the expected value. + if self.to_kzg_versioned_hash() != hash.hash { + return Err(anyhow::anyhow!( + "expected hash {} for blob at index {} but got {}", + hash.hash, + hash.index, + B256::from(self.to_kzg_versioned_hash()) + )); + } + + // Confirm blob data is valid by verifying its proof against the commitment + match self.verify_blob_kzg_proof() { + Ok(true) => Ok(()), + Ok(false) => Err(anyhow::anyhow!("blob at index {} failed verification", self.index)), + Err(e) => { + Err(anyhow::anyhow!("blob at index {} failed verification: {}", self.index, e)) + } + } + } + /// Verifies the blob kzg proof. #[cfg(feature = "online")] pub fn verify_blob_kzg_proof(&self) -> anyhow::Result {