1use alloy::{
33 consensus::{SidecarCoder, SimpleCoder, Transaction},
34 primitives::B256,
35 providers::{Provider, ProviderBuilder, RootProvider},
36 rpc::types::beacon::{genesis::GenesisResponse, sidecar::GetBlobsResponse},
37 transports::{RpcError, TransportErrorKind},
38};
39use ethexe_common::{
40 CodeAndIdUnchecked, CodeBlobInfo,
41 db::{CodesStorageRO, OnChainStorageRO},
42 gear_core::ids::prelude::CodeIdExt,
43};
44use futures::{
45 FutureExt, Stream, StreamExt,
46 future::BoxFuture,
47 stream::{FusedStream, FuturesUnordered},
48};
49use gprimitives::{CodeId, H256};
50use reqwest::Client;
51use std::{collections::HashSet, fmt, num::NonZero, pin::Pin, task::Poll};
52use tokio::{sync::OnceCell, time::Duration};
53
54#[derive(Clone, PartialEq, Eq)]
55pub enum BlobLoaderEvent {
56 BlobLoaded(CodeAndIdUnchecked),
57}
58
59#[derive(thiserror::Error, Debug)]
60pub enum BlobLoaderError {
61 #[error("transport error: {0}")]
62 Transport(#[from] RpcError<TransportErrorKind>),
63 #[error("failed to get code blob info for: {0}")]
64 CodeBlobInfoNotFound(CodeId),
65}
66
67#[derive(thiserror::Error, Debug)]
68enum ReadBlobBundleError {
69 #[error("failed to read blob bundle from beacon node: {0}")]
70 Reqwest(#[from] reqwest::Error),
71 #[error("failed to decode blob bundle response: {0}")]
72 Serde(#[from] serde_json::Error),
73}
74
75#[derive(thiserror::Error, Debug)]
76enum ReaderError {
77 #[error("transport error: {0}")]
78 Transport(#[from] RpcError<TransportErrorKind>),
79 #[error("failed to find transaction by hash: {0}")]
80 TransactionNotFound(H256),
81 #[error("failed to get blob versioned hashes from transaction: {0}")]
82 BlobVersionedHashesNotFound(H256),
83 #[error("failed to get transaction block hash: {0}")]
84 TransactionBlockNotFound(H256),
85 #[error("failed to get block by hash: {0}")]
86 BlockNotFound(H256),
87 #[error("failed to read blob bundle: {0}")]
88 ReadBlob(#[from] ReadBlobBundleError),
89 #[error("failed to decode blobs")]
90 DecodeBlobs,
91 #[error("failed to access genesis time: {0}")]
92 GenesisTimeAccess(reqwest::Error),
93 #[error("received empty blob")]
94 EmptyBlob,
95 #[error("blob code id mismatch: expected {expected:?}, found {found:?}")]
96 CodeIdMismatch { expected: CodeId, found: CodeId },
97}
98
99type LoaderResult<T> = Result<T, BlobLoaderError>;
100type ReaderResult<T> = Result<T, ReaderError>;
101
102pub trait BlobLoaderService:
103 Stream<Item = LoaderResult<BlobLoaderEvent>> + FusedStream + Send + Unpin
104{
105 fn load_codes(&mut self, codes: HashSet<CodeId>) -> LoaderResult<()>;
106
107 fn into_box(self) -> Box<dyn BlobLoaderService>;
108
109 fn pending_codes_len(&self) -> usize;
110}
111
112impl fmt::Debug for BlobLoaderEvent {
113 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114 match self {
115 BlobLoaderEvent::BlobLoaded(data) => data.fmt(f),
116 }
117 }
118}
119
120#[derive(Clone)]
121pub struct ConsensusLayerConfig {
122 pub ethereum_rpc: String,
123 pub ethereum_beacon_rpc: String,
124 pub beacon_block_time: Duration,
125 pub attempts: NonZero<u8>,
126}
127
128#[derive(Clone)]
129struct ConsensusLayerBlobReader {
130 provider: RootProvider,
131 http_client: Client,
132 config: ConsensusLayerConfig,
133}
134
135impl ConsensusLayerBlobReader {
136 async fn read_blob(&self, expected_code_id: CodeId, tx_hash: H256) -> ReaderResult<Vec<u8>> {
137 let mut last_err = None;
138 let mut previously_received_code_id = None;
139 for attempt in 0..self.config.attempts.get() {
140 log::trace!("trying to get blob, attempt #{attempt}");
141 match self.try_query_blob(tx_hash).await {
142 Ok(blob) => {
143 match handle_blob(
144 blob,
145 expected_code_id,
146 &mut previously_received_code_id,
147 attempt,
148 ) {
149 Ok(blob) => return Ok(blob),
150 Err(err) => last_err = Some(err),
151 }
152 }
153 Err(err) => {
154 log::warn!("failed to get blob on attempt #{attempt}: {err}");
155 last_err = Some(err);
156 }
157 }
158
159 tokio::time::sleep(self.config.beacon_block_time).await;
160 }
161
162 Err(last_err.expect("Must be set, because attempts is not zero"))
163 }
164
165 async fn try_query_blob(&self, tx_hash: H256) -> ReaderResult<Vec<u8>> {
166 use ReaderError::*;
167
168 let tx = self
169 .provider
170 .get_transaction_by_hash(tx_hash.0.into())
171 .await?
172 .ok_or(TransactionNotFound(tx_hash))?;
173
174 let blob_versioned_hashes = tx
177 .blob_versioned_hashes()
178 .ok_or(BlobVersionedHashesNotFound(tx_hash))?;
179
180 let block_hash = tx.block_hash.ok_or(TransactionBlockNotFound(tx_hash))?;
181
182 let block = self
183 .provider
184 .get_block_by_hash(block_hash)
185 .await?
186 .ok_or(BlockNotFound(H256(block_hash.0)))?;
187
188 let slot = if let Some(chain_id) = tx.chain_id()
190 && chain_id == 31337
191 {
192 block.header.number
193 } else {
194 static BEACON_GENESIS_BLOCK_TIME: OnceCell<u64> = OnceCell::const_new();
195
196 let beacon_genesis_block_time = *BEACON_GENESIS_BLOCK_TIME
197 .get_or_try_init(|| self.read_genesis_time())
198 .await
199 .map_err(GenesisTimeAccess)?;
200 (block.header.timestamp - beacon_genesis_block_time)
201 / self.config.beacon_block_time.as_secs()
202 };
203
204 let blob_bundle = self
205 .read_blob_bundle(slot, blob_versioned_hashes)
206 .await
207 .map_err(ReadBlob)?;
208
209 let mut coder = SimpleCoder::default();
210 let data = coder
211 .decode_all(&blob_bundle.data)
212 .ok_or(DecodeBlobs)?
213 .concat();
214
215 Ok(data)
216 }
217
218 async fn read_genesis_time(&self) -> reqwest::Result<u64> {
219 let ethereum_beacon_rpc = &self.config.ethereum_beacon_rpc;
220 let response = self
221 .http_client
222 .get(format!("{ethereum_beacon_rpc}/eth/v1/beacon/genesis"))
223 .send()
224 .await?
225 .json::<GenesisResponse>()
226 .await?;
227
228 Ok(response.data.genesis_time)
229 }
230
231 async fn read_blob_bundle(
232 &self,
233 slot: u64,
234 versioned_hashes: &[B256],
235 ) -> Result<GetBlobsResponse, ReadBlobBundleError> {
236 let ethereum_beacon_rpc = &self.config.ethereum_beacon_rpc;
237 let bytes = self
238 .http_client
239 .get(format!(
240 "{ethereum_beacon_rpc}/eth/v1/beacon/blobs/{slot}?versioned_hashes={}",
241 versioned_hashes
242 .iter()
243 .map(|versioned_hash| versioned_hash.to_string())
244 .collect::<Vec<_>>()
245 .join(",")
246 ))
247 .send()
248 .await?
249 .bytes()
250 .await?;
251 let blobs_response =
252 serde_json::from_slice::<GetBlobsResponse>(&bytes).inspect_err(|err| {
253 log::trace!("failed to decode blob bundle response: {err}, bytes: {bytes:?}")
254 })?;
255 Ok(blobs_response)
256 }
257}
258
259pub trait Database: CodesStorageRO + OnChainStorageRO + Unpin + Send + Clone + 'static {}
260impl<T: CodesStorageRO + OnChainStorageRO + Unpin + Send + Clone + 'static> Database for T {}
261
262pub struct BlobLoader<DB: Database> {
263 futures: FuturesUnordered<BoxFuture<'static, ReaderResult<CodeAndIdUnchecked>>>,
264 codes_loading: HashSet<CodeId>,
265
266 blobs_reader: ConsensusLayerBlobReader,
267 db: DB,
268}
269
270impl<DB: Database> Stream for BlobLoader<DB> {
271 type Item = LoaderResult<BlobLoaderEvent>;
272
273 fn poll_next(
274 mut self: Pin<&mut Self>,
275 cx: &mut std::task::Context<'_>,
276 ) -> std::task::Poll<Option<Self::Item>> {
277 match futures::ready!(self.futures.poll_next_unpin(cx)) {
278 None => Poll::Pending,
279 Some(Err(err)) => {
280 log::error!("Failed to load blob: {err}, skipping");
282 Poll::Pending
283 }
284 Some(Ok(code_and_id)) => {
285 self.codes_loading.remove(&code_and_id.code_id);
286 Poll::Ready(Some(Ok(BlobLoaderEvent::BlobLoaded(code_and_id))))
287 }
288 }
289 }
290}
291
292impl<DB: Database> FusedStream for BlobLoader<DB> {
293 fn is_terminated(&self) -> bool {
294 false
295 }
296}
297
298impl<DB: Database> BlobLoader<DB> {
299 pub async fn new(db: DB, consensus_cfg: ConsensusLayerConfig) -> LoaderResult<Self> {
300 Ok(Self {
301 futures: FuturesUnordered::new(),
302 codes_loading: HashSet::new(),
303
304 blobs_reader: ConsensusLayerBlobReader {
305 provider: ProviderBuilder::default()
306 .connect(&consensus_cfg.ethereum_rpc)
307 .await?,
308 http_client: Client::new(),
309 config: consensus_cfg,
310 },
311 db,
312 })
313 }
314
315 #[cfg(test)]
316 fn new_with_consensus_reader(db: DB, blobs_reader: ConsensusLayerBlobReader) -> Self {
317 Self {
318 futures: FuturesUnordered::new(),
319 codes_loading: HashSet::new(),
320 blobs_reader,
321 db,
322 }
323 }
324}
325
326impl<DB: Database> BlobLoaderService for BlobLoader<DB> {
327 fn into_box(self) -> Box<dyn BlobLoaderService> {
328 Box::new(self)
329 }
330
331 fn pending_codes_len(&self) -> usize {
332 self.codes_loading.len()
333 }
334
335 fn load_codes(&mut self, codes: HashSet<CodeId>) -> LoaderResult<()> {
336 for code_id in codes {
337 if self.codes_loading.contains(&code_id) {
338 continue;
339 }
340
341 let CodeBlobInfo { tx_hash, .. } = self
342 .db
343 .code_blob_info(code_id)
344 .ok_or(BlobLoaderError::CodeBlobInfoNotFound(code_id))?;
345
346 self.codes_loading.insert(code_id);
347
348 if let Some(code) = self.db.original_code(code_id) {
349 log::warn!("Code {code_id} is already loaded, skipping loading from remote source");
350 self.futures
351 .push(futures::future::ready(Ok(CodeAndIdUnchecked { code_id, code })).boxed());
352 } else {
353 let blobs_reader = self.blobs_reader.clone();
354 self.futures.push(
355 async move {
356 blobs_reader
357 .read_blob(code_id, tx_hash)
358 .await
359 .map(|code| CodeAndIdUnchecked { code_id, code })
360 }
361 .boxed(),
362 );
363 }
364 }
365
366 Ok(())
367 }
368}
369
370fn handle_blob(
374 blob: Vec<u8>,
375 code_id: CodeId,
376 previously_received_code_id: &mut Option<CodeId>,
377 attempt: u8,
378) -> ReaderResult<Vec<u8>> {
379 if blob.is_empty() {
380 log::warn!("received empty blob on attempt #{attempt}");
381 return Err(ReaderError::EmptyBlob);
382 }
383
384 let received_code_id = CodeId::generate(&blob);
385 if *previously_received_code_id == Some(received_code_id) {
386 log::warn!(
387 "received same blob with invalid id on attempt #{attempt}, code id: {received_code_id:?}, consider blob as loaded then",
388 );
389 return Ok(blob);
390 }
391
392 if code_id != received_code_id {
393 *previously_received_code_id = Some(received_code_id);
394 log::warn!(
395 "received blob code id mismatch on attempt #{attempt}: expected {code_id:?}, got {received_code_id:?}",
396 );
397 return Err(ReaderError::CodeIdMismatch {
398 expected: code_id,
399 found: received_code_id,
400 });
401 }
402
403 Ok(blob)
404}
405
406#[cfg(test)]
407mod tests;