Skip to main content

evm_fork_cache/reactive/
raw_json_flashblocks.rs

1use std::collections::{HashMap, HashSet};
2
3use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, Log as PrimitiveLog};
4use alloy_rpc_types_eth::Log;
5use tokio::sync::{mpsc, oneshot};
6
7use super::{
8    BaseFlashblockBase, FlashblockContentCommitment, FlashblockRef, ProviderRef,
9    deserialize_optional_rpc_u64, flashblock_content_hash, flashblock_transaction_hashes,
10    non_placeholder_hash,
11};
12
13/// Resource bounds applied while converting receipt-enriched JSON Flashblocks.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct RawJsonFlashblocksLimits {
17    /// Largest accepted JSON frame.
18    pub max_frame_bytes: usize,
19    /// Largest accepted index for one payload generation.
20    pub max_flashblocks_per_payload: usize,
21    /// Largest cumulative transaction membership retained for one generation.
22    pub max_transactions_per_payload: usize,
23    /// Largest cumulative receipt-log count retained for one generation.
24    pub max_logs_per_payload: usize,
25}
26
27impl Default for RawJsonFlashblocksLimits {
28    fn default() -> Self {
29        Self {
30            max_frame_bytes: 16 * 1024 * 1024,
31            max_flashblocks_per_payload: 64,
32            max_transactions_per_payload: 50_000,
33            max_logs_per_payload: 200_000,
34        }
35    }
36}
37
38impl RawJsonFlashblocksLimits {
39    fn validate(self) -> Result<(), RawJsonFlashblocksError> {
40        if self.max_frame_bytes == 0 {
41            return Err(RawJsonFlashblocksError::InvalidLimits(
42                "max_frame_bytes must be greater than zero",
43            ));
44        }
45        if self.max_flashblocks_per_payload == 0 {
46            return Err(RawJsonFlashblocksError::InvalidLimits(
47                "max_flashblocks_per_payload must be greater than zero",
48            ));
49        }
50        if self.max_transactions_per_payload == 0 {
51            return Err(RawJsonFlashblocksError::InvalidLimits(
52                "max_transactions_per_payload must be greater than zero",
53            ));
54        }
55        if self.max_logs_per_payload == 0 {
56            return Err(RawJsonFlashblocksError::InvalidLimits(
57                "max_logs_per_payload must be greater than zero",
58            ));
59        }
60        Ok(())
61    }
62}
63
64/// One provider-provenanced cumulative preview plus the logs added by its
65/// latest indexed delta.
66#[derive(Clone, Debug, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct FlashblockSnapshot {
69    /// Identity and cumulative transaction membership for the preview.
70    pub flashblock: FlashblockRef,
71    /// Structured logs added by this exact indexed delta.
72    pub logs: Vec<Log>,
73}
74
75/// Why a speculative Flashblock payload generation was revoked.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub enum FlashblockInvalidationReason {
79    /// The source skipped at least one index within a payload generation.
80    IndexGap,
81    /// A repeated index carried different content.
82    ConflictingDuplicate,
83    /// A generation began after index zero, so its base header was unavailable.
84    MissingInitialIndex,
85    /// The caller replaced or disconnected the externally managed source.
86    SourceReset,
87}
88
89/// Observable fail-closed invalidation for one provider generation.
90#[derive(Clone, Debug, PartialEq, Eq)]
91#[non_exhaustive]
92pub struct FlashblockInvalidation {
93    /// Provider generation whose speculative payload was revoked.
94    pub provider: ProviderRef,
95    /// Payload identity that was active or could not be trusted.
96    pub payload_id: FixedBytes<8>,
97    /// Continuity or caller lifecycle transition that caused the revocation.
98    pub reason: FlashblockInvalidationReason,
99}
100
101/// Standardized update accepted by the existing preconfirmation pipeline.
102#[derive(Clone, Debug, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum FlashblockUpdate {
105    /// A cumulative preview and its newly added structured logs.
106    Snapshot(Box<FlashblockSnapshot>),
107    /// The active speculative provider generation must be discarded.
108    Invalidated(FlashblockInvalidation),
109}
110
111impl FlashblockUpdate {
112    /// Provider generation carried by this standardized update.
113    pub const fn provider(&self) -> &ProviderRef {
114        match self {
115            Self::Snapshot(snapshot) => &snapshot.flashblock.provider,
116            Self::Invalidated(invalidation) => &invalidation.provider,
117        }
118    }
119}
120
121/// Failure to enqueue a standardized update into an attached subscriber.
122#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
123#[non_exhaustive]
124pub enum FlashblockUpdateChannelError {
125    /// The update names a different configured endpoint.
126    #[error("Flashblock update came from an unexpected provider endpoint")]
127    UnexpectedEndpoint,
128    /// A non-blocking send found the bounded channel at capacity.
129    #[error("Flashblock update channel is full")]
130    Full,
131    /// The subscriber no longer owns the receiving half of the channel.
132    #[error("Flashblock update channel is closed")]
133    Closed,
134    /// The subscriber consumed the update but rejected its integrity or local
135    /// resource requirements. The application must revoke the source
136    /// generation before continuing.
137    #[error("Flashblock update was rejected by the subscriber")]
138    Rejected,
139}
140
141/// Subscriber verdict for one non-blocking standardized update admission.
142///
143/// Awaiting this receipt distinguishes bounded-queue admission from actual
144/// subscriber validation. Dropping it does not cancel the queued update.
145#[derive(Debug)]
146#[must_use = "await wait() to observe the subscriber validation verdict"]
147pub struct FlashblockUpdateAcknowledgement {
148    receiver: oneshot::Receiver<Result<(), FlashblockUpdateChannelError>>,
149}
150
151impl FlashblockUpdateAcknowledgement {
152    /// Wait until the subscriber accepts or rejects the queued update.
153    ///
154    /// # Errors
155    ///
156    /// Returns [`FlashblockUpdateChannelError::Rejected`] after subscriber
157    /// validation failure, or [`FlashblockUpdateChannelError::Closed`] when the
158    /// subscriber shuts down before producing a verdict.
159    pub async fn wait(self) -> Result<(), FlashblockUpdateChannelError> {
160        self.receiver
161            .await
162            .unwrap_or(Err(FlashblockUpdateChannelError::Closed))
163    }
164}
165
166pub(crate) struct QueuedFlashblockUpdate {
167    pub(crate) update: FlashblockUpdate,
168    pub(crate) acknowledgement: oneshot::Sender<Result<(), FlashblockUpdateChannelError>>,
169}
170
171impl QueuedFlashblockUpdate {
172    fn new(update: FlashblockUpdate) -> (Self, FlashblockUpdateAcknowledgement) {
173        let (acknowledgement, receiver) = oneshot::channel();
174        (
175            Self {
176                update,
177                acknowledgement,
178            },
179            FlashblockUpdateAcknowledgement { receiver },
180        )
181    }
182}
183
184/// Cloneable application handle for a subscriber-owned, bounded update queue.
185///
186/// This handle performs no network I/O and implements no retry policy. The
187/// application keeps it while the [`super::AlloySubscriber`] may be moved into
188/// another runtime owner, and sends only updates produced by a compatible
189/// adapter. Backpressure, reconnects, and source rotation remain application
190/// responsibilities.
191#[derive(Clone, Debug)]
192pub struct FlashblockUpdateSender {
193    provider: ProviderRef,
194    sender: mpsc::Sender<QueuedFlashblockUpdate>,
195}
196
197impl FlashblockUpdateSender {
198    pub(crate) const fn new(
199        provider: ProviderRef,
200        sender: mpsc::Sender<QueuedFlashblockUpdate>,
201    ) -> Self {
202        Self { provider, sender }
203    }
204
205    /// Configured endpoint and initial generation for this queue.
206    pub const fn provider(&self) -> &ProviderRef {
207        &self.provider
208    }
209
210    /// Await bounded queue capacity, enqueue one standardized update, and wait
211    /// for the subscriber's validation verdict.
212    ///
213    /// # Errors
214    ///
215    /// Returns [`FlashblockUpdateChannelError::UnexpectedEndpoint`] before
216    /// enqueueing an update from another endpoint, or
217    /// [`FlashblockUpdateChannelError::Closed`] after subscriber shutdown.
218    /// [`FlashblockUpdateChannelError::Rejected`] means the subscriber consumed
219    /// the update but rejected its integrity or local resource requirements;
220    /// revoke and replace that source generation before continuing.
221    pub async fn send(&self, update: FlashblockUpdate) -> Result<(), FlashblockUpdateChannelError> {
222        self.validate_endpoint(&update)?;
223        let (queued, acknowledgement) = QueuedFlashblockUpdate::new(update);
224        self.sender
225            .send(queued)
226            .await
227            .map_err(|_| FlashblockUpdateChannelError::Closed)?;
228        acknowledgement.wait().await
229    }
230
231    /// Enqueue one standardized update without waiting for capacity and return
232    /// a receipt for the subscriber's eventual validation verdict.
233    ///
234    /// # Errors
235    ///
236    /// In addition to endpoint and closure errors, returns
237    /// [`FlashblockUpdateChannelError::Full`] when the bounded queue has no
238    /// immediate capacity. A successful return proves only queue admission;
239    /// await [`FlashblockUpdateAcknowledgement::wait`] before treating the
240    /// update as accepted. The caller decides whether to wait, invalidate its
241    /// current generation, or reconnect the external source.
242    pub fn try_send(
243        &self,
244        update: FlashblockUpdate,
245    ) -> Result<FlashblockUpdateAcknowledgement, FlashblockUpdateChannelError> {
246        self.validate_endpoint(&update)?;
247        let (queued, acknowledgement) = QueuedFlashblockUpdate::new(update);
248        self.sender.try_send(queued).map_err(|error| match error {
249            mpsc::error::TrySendError::Full(_) => FlashblockUpdateChannelError::Full,
250            mpsc::error::TrySendError::Closed(_) => FlashblockUpdateChannelError::Closed,
251        })?;
252        Ok(acknowledgement)
253    }
254
255    fn validate_endpoint(
256        &self,
257        update: &FlashblockUpdate,
258    ) -> Result<(), FlashblockUpdateChannelError> {
259        if update.provider().endpoint != self.provider.endpoint {
260            return Err(FlashblockUpdateChannelError::UnexpectedEndpoint);
261        }
262        Ok(())
263    }
264}
265
266pub(crate) fn flashblock_update_channel(
267    provider: ProviderRef,
268    capacity: usize,
269) -> (
270    FlashblockUpdateSender,
271    mpsc::Receiver<QueuedFlashblockUpdate>,
272) {
273    let (sender, receiver) = mpsc::channel(capacity);
274    (FlashblockUpdateSender::new(provider, sender), receiver)
275}
276
277/// A malformed, unsupported, or resource-exhausting raw JSON Flashblocks frame.
278#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
279#[non_exhaustive]
280pub enum RawJsonFlashblocksError {
281    /// Adapter bounds are unusable.
282    #[error("invalid raw JSON Flashblocks limits: {0}")]
283    InvalidLimits(&'static str),
284    /// A caller attempted an ambiguous or regressing source transition.
285    #[error("invalid raw JSON Flashblocks source transition: {0}")]
286    InvalidSourceTransition(&'static str),
287    /// The frame exceeded the configured byte bound.
288    #[error("raw JSON Flashblocks frame exceeds the configured byte limit")]
289    FrameTooLarge,
290    /// The JSON document did not match the supported receipt-enriched profile.
291    #[error("invalid raw JSON Flashblocks payload: {0}")]
292    InvalidPayload(String),
293    /// A configured per-payload resource bound was exceeded.
294    #[error("raw JSON Flashblocks payload exceeds the configured {0} limit")]
295    ResourceExhausted(&'static str),
296}
297
298/// Stateful converter for receipt-enriched, indexed JSON Flashblock payloads.
299///
300/// Compatibility is defined by the accepted wire profile, not by chain id:
301/// `payload_id`, a monotonically increasing `index`, an index-zero `base` (or
302/// `static`) header, transaction deltas in `diff.transactions`, and an exact
303/// receipt map in `metadata.receipts`. Provider JSON-RPC subscription envelopes,
304/// receipt-less payloads, and binary SSZ frames are separate wire profiles and
305/// are not accepted by this adapter.
306///
307/// The adapter performs no I/O. The caller owns WebSocket control frames,
308/// authentication, timeouts, retry, backoff, and provider rotation. On source
309/// replacement or disconnect, call [`Self::reset`] and forward the returned
310/// invalidation before accepting updates from the new provider generation.
311#[derive(Debug)]
312pub struct RawJsonFlashblocksAdapter {
313    provider: ProviderRef,
314    limits: RawJsonFlashblocksLimits,
315    active: Option<RawPayloadState>,
316    ignored_payload: Option<FixedBytes<8>>,
317}
318
319impl RawJsonFlashblocksAdapter {
320    /// Construct an adapter with bounded production defaults.
321    pub fn new(provider: ProviderRef) -> Self {
322        Self {
323            provider,
324            limits: RawJsonFlashblocksLimits::default(),
325            active: None,
326            ignored_payload: None,
327        }
328    }
329
330    /// Construct an adapter with explicit resource bounds.
331    pub fn with_limits(
332        provider: ProviderRef,
333        limits: RawJsonFlashblocksLimits,
334    ) -> Result<Self, RawJsonFlashblocksError> {
335        limits.validate()?;
336        Ok(Self {
337            provider,
338            limits,
339            active: None,
340            ignored_payload: None,
341        })
342    }
343
344    /// Provider generation attached to newly normalized snapshots.
345    pub const fn provider(&self) -> &ProviderRef {
346        &self.provider
347    }
348
349    /// Resource limits applied by this adapter.
350    pub const fn limits(&self) -> RawJsonFlashblocksLimits {
351        self.limits
352    }
353
354    /// Revoke the active payload and begin a caller-managed provider generation.
355    ///
356    /// This method never reconnects, sleeps, or performs I/O. `None` means the
357    /// prior source had no active payload to revoke.
358    ///
359    /// # Errors
360    ///
361    /// Returns [`RawJsonFlashblocksError::InvalidSourceTransition`] when the
362    /// endpoint identity changes or the generation does not increase. Rebuild
363    /// the adapter and subscriber binding to select a different endpoint.
364    pub fn reset(
365        &mut self,
366        provider: ProviderRef,
367    ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
368        if provider.endpoint != self.provider.endpoint {
369            return Err(RawJsonFlashblocksError::InvalidSourceTransition(
370                "reset cannot change the configured endpoint identity",
371            ));
372        }
373        if provider.generation <= self.provider.generation {
374            return Err(RawJsonFlashblocksError::InvalidSourceTransition(
375                "reset requires a strictly newer provider generation",
376            ));
377        }
378        let invalidation = self.active.take().map(|active| {
379            FlashblockUpdate::Invalidated(FlashblockInvalidation {
380                provider: self.provider.clone(),
381                payload_id: active.payload_id,
382                reason: FlashblockInvalidationReason::SourceReset,
383            })
384        });
385        self.provider = provider;
386        self.ignored_payload = None;
387        Ok(invalidation)
388    }
389
390    /// Decode and normalize one raw JSON application-data frame.
391    ///
392    /// `Ok(None)` denotes an identical duplicate or a later delta from a
393    /// generation already invalidated for continuity loss. `Err` leaves the
394    /// last successfully published adapter state unchanged. A live caller that
395    /// cannot prove an application-data error irrelevant must call
396    /// [`Self::reset`] with a newer generation and forward the returned
397    /// invalidation before accepting more frames.
398    pub fn ingest_json(
399        &mut self,
400        frame: &[u8],
401    ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
402        if frame.len() > self.limits.max_frame_bytes {
403            return Err(RawJsonFlashblocksError::FrameTooLarge);
404        }
405        let payload: RawFlashblockPayload = serde_json::from_slice(frame)
406            .map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
407        self.ingest(payload)
408    }
409
410    fn ingest(
411        &mut self,
412        payload: RawFlashblockPayload,
413    ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
414        if usize::try_from(payload.index)
415            .ok()
416            .is_none_or(|index| index >= self.limits.max_flashblocks_per_payload)
417        {
418            return Err(RawJsonFlashblocksError::ResourceExhausted(
419                "Flashblock index",
420            ));
421        }
422        if self.ignored_payload == Some(payload.payload_id) {
423            return Ok(None);
424        }
425
426        let begins_new_payload = self
427            .active
428            .as_ref()
429            .is_none_or(|active| active.payload_id != payload.payload_id);
430        if begins_new_payload {
431            if payload.index != 0 {
432                let invalidated_payload = self
433                    .active
434                    .take()
435                    .map_or(payload.payload_id, |active| active.payload_id);
436                self.ignored_payload = Some(payload.payload_id);
437                return Ok(Some(self.invalidation(
438                    invalidated_payload,
439                    FlashblockInvalidationReason::MissingInitialIndex,
440                )));
441            }
442            let base = payload.base.clone().ok_or_else(|| {
443                RawJsonFlashblocksError::InvalidPayload("index zero omitted its base header".into())
444            })?;
445            if let Some(metadata_number) = payload.metadata.block_number
446                && metadata_number != base.block_number
447            {
448                return Err(RawJsonFlashblocksError::InvalidPayload(
449                    "base and metadata block numbers disagree".into(),
450                ));
451            }
452            let previous_active = self.active.take();
453            let previous_ignored_payload = self.ignored_payload.take();
454            self.active = Some(RawPayloadState {
455                payload_id: payload.payload_id,
456                base,
457                last_index: None,
458                cumulative_transactions: Vec::new(),
459                transaction_set: HashSet::new(),
460                next_log_index: 0,
461                cumulative_logs: 0,
462                index_commitments: HashMap::new(),
463            });
464            let result = self.ingest_active(payload);
465            if result.is_err() {
466                self.active = previous_active;
467                self.ignored_payload = previous_ignored_payload;
468            }
469            return result;
470        }
471
472        self.ingest_active(payload)
473    }
474
475    fn ingest_active(
476        &mut self,
477        payload: RawFlashblockPayload,
478    ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
479        let active = self.active.as_mut().expect("new payload initialized above");
480        if payload
481            .metadata
482            .block_number
483            .is_some_and(|number| number != active.base.block_number)
484            || payload
485                .base
486                .as_ref()
487                .is_some_and(|base| base != &active.base)
488        {
489            return Err(RawJsonFlashblocksError::InvalidPayload(
490                "base header or metadata block numbers disagree with the active payload".into(),
491            ));
492        }
493        let delta_transactions = flashblock_transaction_hashes(&payload.diff.transactions)
494            .map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
495        let receipt_hashes = payload
496            .metadata
497            .receipts
498            .keys()
499            .copied()
500            .collect::<HashSet<_>>();
501        let transaction_hashes = delta_transactions.iter().copied().collect::<HashSet<_>>();
502        if transaction_hashes.len() != delta_transactions.len() {
503            return Err(RawJsonFlashblocksError::InvalidPayload(
504                "the transaction delta contains a duplicate hash".into(),
505            ));
506        }
507        if receipt_hashes != transaction_hashes {
508            return Err(RawJsonFlashblocksError::InvalidPayload(
509                "receipt-map membership disagrees with the transaction delta".into(),
510            ));
511        }
512        let commitment = raw_payload_commitment(&payload, &delta_transactions);
513        if let Some(previous) = active.index_commitments.get(&payload.index) {
514            if *previous == commitment {
515                return Ok(None);
516            }
517            let payload_id = active.payload_id;
518            self.active = None;
519            self.ignored_payload = Some(payload_id);
520            return Ok(Some(self.invalidation(
521                payload_id,
522                FlashblockInvalidationReason::ConflictingDuplicate,
523            )));
524        }
525        let expected_index = active.last_index.map_or(0, |index| index.saturating_add(1));
526        if payload.index != expected_index {
527            let payload_id = active.payload_id;
528            self.active = None;
529            self.ignored_payload = Some(payload_id);
530            return Ok(Some(
531                self.invalidation(payload_id, FlashblockInvalidationReason::IndexGap),
532            ));
533        }
534
535        if active
536            .cumulative_transactions
537            .len()
538            .saturating_add(delta_transactions.len())
539            > self.limits.max_transactions_per_payload
540        {
541            return Err(RawJsonFlashblocksError::ResourceExhausted(
542                "transaction count",
543            ));
544        }
545        if delta_transactions
546            .iter()
547            .any(|hash| active.transaction_set.contains(hash))
548        {
549            return Err(RawJsonFlashblocksError::InvalidPayload(
550                "a transaction appeared in more than one indexed delta".into(),
551            ));
552        }
553
554        let transaction_offset = active.cumulative_transactions.len();
555        let mut logs = Vec::new();
556        for (delta_index, transaction_hash) in delta_transactions.iter().enumerate() {
557            let receipt = payload
558                .metadata
559                .receipts
560                .get(transaction_hash)
561                .expect("receipt membership checked above");
562            let transaction_index =
563                u64::try_from(transaction_offset.saturating_add(delta_index))
564                    .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("transaction index"))?;
565            for raw_log in &receipt.logs {
566                if active.cumulative_logs.saturating_add(logs.len())
567                    >= self.limits.max_logs_per_payload
568                {
569                    return Err(RawJsonFlashblocksError::ResourceExhausted("log count"));
570                }
571                let inner = PrimitiveLog::new(
572                    raw_log.address,
573                    raw_log.topics.clone(),
574                    raw_log.data.clone(),
575                )
576                .ok_or_else(|| {
577                    RawJsonFlashblocksError::InvalidPayload(
578                        "receipt log contains more than four topics".into(),
579                    )
580                })?;
581                let log_index = active
582                    .next_log_index
583                    .checked_add(
584                        u64::try_from(logs.len())
585                            .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
586                    )
587                    .ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
588                logs.push(Log {
589                    inner,
590                    block_hash: None,
591                    block_number: Some(active.base.block_number),
592                    block_timestamp: Some(active.base.timestamp),
593                    transaction_hash: Some(*transaction_hash),
594                    transaction_index: Some(transaction_index),
595                    log_index: Some(log_index),
596                    removed: false,
597                });
598            }
599        }
600
601        let mut cumulative_transactions = active.cumulative_transactions.clone();
602        cumulative_transactions.extend(delta_transactions.iter().copied());
603        let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
604        let state_root = non_placeholder_hash(payload.diff.state_root);
605        let transactions_root = payload
606            .diff
607            .transactions_root
608            .and_then(non_placeholder_hash);
609        let parent_hash = non_placeholder_hash(active.base.parent_hash);
610        let prevrandao = active.base.prevrandao.and_then(non_placeholder_hash);
611        let content_hash = flashblock_content_hash(FlashblockContentCommitment {
612            provider: &self.provider,
613            payload_id: Some(payload.payload_id),
614            index: Some(payload.index),
615            block_number: active.base.block_number,
616            partial_block_hash,
617            parent_hash,
618            state_root,
619            transactions_root,
620            transaction_hashes: &cumulative_transactions,
621            timestamp: Some(active.base.timestamp),
622            base_fee_per_gas: active.base.base_fee_per_gas,
623            beneficiary: active.base.beneficiary,
624            prevrandao,
625            gas_limit: active.base.gas_limit,
626        });
627        for log in &mut logs {
628            log.block_hash = Some(content_hash);
629        }
630        let flashblock = FlashblockRef {
631            provider: self.provider.clone(),
632            payload_id: Some(payload.payload_id),
633            index: Some(payload.index),
634            block_number: active.base.block_number,
635            content_hash,
636            partial_block_hash,
637            parent_hash,
638            state_root,
639            transactions_root,
640            transaction_hashes: cumulative_transactions.clone(),
641            timestamp: Some(active.base.timestamp),
642            base_fee_per_gas: active.base.base_fee_per_gas,
643            beneficiary: active.base.beneficiary,
644            prevrandao,
645            gas_limit: active.base.gas_limit,
646        };
647        let next_log_index = active
648            .next_log_index
649            .checked_add(
650                u64::try_from(logs.len())
651                    .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
652            )
653            .ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
654        active.transaction_set.extend(delta_transactions);
655        active.cumulative_transactions = cumulative_transactions;
656        active.last_index = Some(payload.index);
657        active.index_commitments.insert(payload.index, commitment);
658        active.next_log_index = next_log_index;
659        active.cumulative_logs = active.cumulative_logs.saturating_add(logs.len());
660
661        Ok(Some(FlashblockUpdate::Snapshot(Box::new(
662            FlashblockSnapshot { flashblock, logs },
663        ))))
664    }
665
666    fn invalidation(
667        &self,
668        payload_id: FixedBytes<8>,
669        reason: FlashblockInvalidationReason,
670    ) -> FlashblockUpdate {
671        FlashblockUpdate::Invalidated(FlashblockInvalidation {
672            provider: self.provider.clone(),
673            payload_id,
674            reason,
675        })
676    }
677}
678
679fn raw_payload_commitment(payload: &RawFlashblockPayload, transaction_hashes: &[B256]) -> B256 {
680    let mut commitment = Keccak256::new();
681    commitment.update(b"evm-fork-cache/raw-json-flashblock/v1");
682    commitment.update(payload.payload_id.as_slice());
683    commitment.update(payload.index.to_be_bytes());
684    match payload.base.as_ref() {
685        Some(base) => {
686            commitment.update([1]);
687            commitment.update(base.parent_hash.as_slice());
688            commitment.update(base.block_number.to_be_bytes());
689            commitment.update(base.timestamp.to_be_bytes());
690            commit_optional_raw_u64(&mut commitment, base.gas_limit);
691            commit_optional_raw_u64(&mut commitment, base.base_fee_per_gas);
692            commit_optional_raw_bytes(
693                &mut commitment,
694                base.beneficiary.as_ref().map(|address| address.as_slice()),
695            );
696            commit_optional_raw_bytes(
697                &mut commitment,
698                base.prevrandao.as_ref().map(B256::as_slice),
699            );
700        }
701        None => commitment.update([0]),
702    }
703    commitment.update(payload.diff.state_root.as_slice());
704    commitment.update(payload.diff.block_hash.as_slice());
705    commit_optional_raw_bytes(
706        &mut commitment,
707        payload.diff.transactions_root.as_ref().map(B256::as_slice),
708    );
709    commit_optional_raw_u64(&mut commitment, payload.metadata.block_number);
710    commitment.update((transaction_hashes.len() as u64).to_be_bytes());
711    for transaction_hash in transaction_hashes {
712        commitment.update(transaction_hash.as_slice());
713        let receipt = payload
714            .metadata
715            .receipts
716            .get(transaction_hash)
717            .expect("receipt membership validated before commitment");
718        commitment.update((receipt.logs.len() as u64).to_be_bytes());
719        for log in &receipt.logs {
720            commitment.update(log.address.as_slice());
721            commitment.update((log.topics.len() as u64).to_be_bytes());
722            for topic in &log.topics {
723                commitment.update(topic.as_slice());
724            }
725            commitment.update((log.data.len() as u64).to_be_bytes());
726            commitment.update(log.data.as_ref());
727        }
728    }
729    commitment.finalize()
730}
731
732fn commit_optional_raw_u64(commitment: &mut Keccak256, value: Option<u64>) {
733    match value {
734        Some(value) => {
735            commitment.update([1]);
736            commitment.update(value.to_be_bytes());
737        }
738        None => commitment.update([0]),
739    }
740}
741
742fn commit_optional_raw_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
743    match value {
744        Some(value) => {
745            commitment.update([1]);
746            commitment.update((value.len() as u64).to_be_bytes());
747            commitment.update(value);
748        }
749        None => commitment.update([0]),
750    }
751}
752
753#[derive(Debug)]
754struct RawPayloadState {
755    payload_id: FixedBytes<8>,
756    base: BaseFlashblockBase,
757    last_index: Option<u64>,
758    cumulative_transactions: Vec<B256>,
759    transaction_set: HashSet<B256>,
760    next_log_index: u64,
761    cumulative_logs: usize,
762    index_commitments: HashMap<u64, B256>,
763}
764
765#[derive(Clone, Debug, serde::Deserialize)]
766struct RawFlashblockPayload {
767    payload_id: FixedBytes<8>,
768    index: u64,
769    #[serde(default, alias = "static")]
770    base: Option<BaseFlashblockBase>,
771    diff: RawFlashblockDiff,
772    metadata: RawFlashblockMetadata,
773}
774
775#[derive(Clone, Debug, serde::Deserialize)]
776struct RawFlashblockDiff {
777    state_root: B256,
778    block_hash: B256,
779    #[serde(default)]
780    transactions: Vec<serde_json::Value>,
781    #[serde(default)]
782    transactions_root: Option<B256>,
783}
784
785#[derive(Clone, Debug, serde::Deserialize)]
786struct RawFlashblockMetadata {
787    #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
788    block_number: Option<u64>,
789    #[serde(default, deserialize_with = "deserialize_receipts")]
790    receipts: HashMap<B256, RawTransactionReceipt>,
791}
792
793fn deserialize_receipts<'de, D>(
794    deserializer: D,
795) -> Result<HashMap<B256, RawTransactionReceipt>, D::Error>
796where
797    D: serde::Deserializer<'de>,
798{
799    struct ReceiptsVisitor;
800
801    impl<'de> serde::de::Visitor<'de> for ReceiptsVisitor {
802        type Value = HashMap<B256, RawTransactionReceipt>;
803
804        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805            formatter.write_str("a receipt map with unique transaction-hash keys")
806        }
807
808        fn visit_map<A>(self, mut entries: A) -> Result<Self::Value, A::Error>
809        where
810            A: serde::de::MapAccess<'de>,
811        {
812            let mut receipts = HashMap::with_capacity(entries.size_hint().unwrap_or_default());
813            while let Some((transaction_hash, receipt)) = entries.next_entry()? {
814                if receipts.insert(transaction_hash, receipt).is_some() {
815                    return Err(serde::de::Error::custom("duplicate receipt key"));
816                }
817            }
818            Ok(receipts)
819        }
820    }
821
822    deserializer.deserialize_map(ReceiptsVisitor)
823}
824
825#[derive(Clone, Debug, serde::Deserialize)]
826struct RawTransactionReceipt {
827    #[serde(default)]
828    logs: Vec<RawReceiptLog>,
829}
830
831#[derive(Clone, Debug, serde::Deserialize)]
832struct RawReceiptLog {
833    address: Address,
834    #[serde(default)]
835    topics: Vec<B256>,
836    data: Bytes,
837}