1use std::{
2 collections::{HashMap, HashSet},
3 time::Instant,
4};
5
6use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, Log as PrimitiveLog};
7use alloy_rpc_types_eth::Log;
8use tokio::sync::{mpsc, oneshot};
9
10use super::{
11 BaseFlashblockBase, FlashblockContentCommitment, FlashblockIngressTiming, FlashblockRef,
12 ProviderRef, deserialize_optional_rpc_u64, flashblock_content_hash,
13 flashblock_transaction_hashes, non_placeholder_hash,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct RawJsonFlashblocksLimits {
20 pub max_frame_bytes: usize,
22 pub max_flashblocks_per_payload: usize,
24 pub max_transactions_per_payload: usize,
26 pub max_logs_per_payload: usize,
28}
29
30impl Default for RawJsonFlashblocksLimits {
31 fn default() -> Self {
32 Self {
33 max_frame_bytes: 16 * 1024 * 1024,
34 max_flashblocks_per_payload: 64,
35 max_transactions_per_payload: 50_000,
36 max_logs_per_payload: 200_000,
37 }
38 }
39}
40
41impl RawJsonFlashblocksLimits {
42 fn validate(self) -> Result<(), RawJsonFlashblocksError> {
43 if self.max_frame_bytes == 0 {
44 return Err(RawJsonFlashblocksError::InvalidLimits(
45 "max_frame_bytes must be greater than zero",
46 ));
47 }
48 if self.max_flashblocks_per_payload == 0 {
49 return Err(RawJsonFlashblocksError::InvalidLimits(
50 "max_flashblocks_per_payload must be greater than zero",
51 ));
52 }
53 if self.max_transactions_per_payload == 0 {
54 return Err(RawJsonFlashblocksError::InvalidLimits(
55 "max_transactions_per_payload must be greater than zero",
56 ));
57 }
58 if self.max_logs_per_payload == 0 {
59 return Err(RawJsonFlashblocksError::InvalidLimits(
60 "max_logs_per_payload must be greater than zero",
61 ));
62 }
63 Ok(())
64 }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
70#[non_exhaustive]
71pub struct FlashblockSnapshot {
72 pub flashblock: FlashblockRef,
74 pub logs: Vec<Log>,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80#[non_exhaustive]
81pub enum FlashblockInvalidationReason {
82 IndexGap,
84 ConflictingDuplicate,
86 MissingInitialIndex,
88 SourceReset,
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct FlashblockInvalidation {
96 pub provider: ProviderRef,
98 pub payload_id: FixedBytes<8>,
100 pub reason: FlashblockInvalidationReason,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum FlashblockUpdate {
108 Snapshot(Box<FlashblockSnapshot>),
110 Invalidated(FlashblockInvalidation),
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct TimedFlashblockUpdate {
121 update: FlashblockUpdate,
122 source_ingress_millis: u64,
123}
124
125impl TimedFlashblockUpdate {
126 const fn new(update: FlashblockUpdate, source_ingress_millis: u64) -> Self {
127 Self {
128 update,
129 source_ingress_millis,
130 }
131 }
132
133 pub const fn update(&self) -> &FlashblockUpdate {
135 &self.update
136 }
137
138 pub const fn source_ingress_millis(&self) -> u64 {
140 self.source_ingress_millis
141 }
142
143 pub fn into_update(self) -> FlashblockUpdate {
145 self.update
146 }
147}
148
149impl FlashblockUpdate {
150 pub const fn provider(&self) -> &ProviderRef {
152 match self {
153 Self::Snapshot(snapshot) => &snapshot.flashblock.provider,
154 Self::Invalidated(invalidation) => &invalidation.provider,
155 }
156 }
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
161#[non_exhaustive]
162pub enum FlashblockUpdateChannelError {
163 #[error("Flashblock update came from an unexpected provider endpoint")]
165 UnexpectedEndpoint,
166 #[error("Flashblock update channel is full")]
168 Full,
169 #[error("Flashblock update channel is closed")]
171 Closed,
172 #[error("Flashblock update was rejected by the subscriber")]
176 Rejected,
177}
178
179#[derive(Debug)]
184#[must_use = "await wait() to observe the subscriber validation verdict"]
185pub struct FlashblockUpdateAcknowledgement {
186 receiver: oneshot::Receiver<Result<(), FlashblockUpdateChannelError>>,
187}
188
189impl FlashblockUpdateAcknowledgement {
190 pub async fn wait(self) -> Result<(), FlashblockUpdateChannelError> {
198 self.receiver
199 .await
200 .unwrap_or(Err(FlashblockUpdateChannelError::Closed))
201 }
202}
203
204pub(crate) struct QueuedFlashblockUpdate {
205 pub(crate) update: FlashblockUpdate,
206 pub(crate) timing: FlashblockIngressTiming,
207 pub(crate) acknowledgement: oneshot::Sender<Result<(), FlashblockUpdateChannelError>>,
208}
209
210impl QueuedFlashblockUpdate {
211 fn new(
212 update: FlashblockUpdate,
213 timing: FlashblockIngressTiming,
214 ) -> (Self, FlashblockUpdateAcknowledgement) {
215 let (acknowledgement, receiver) = oneshot::channel();
216 (
217 Self {
218 update,
219 timing,
220 acknowledgement,
221 },
222 FlashblockUpdateAcknowledgement { receiver },
223 )
224 }
225}
226
227#[derive(Clone, Debug)]
235pub struct FlashblockUpdateSender {
236 provider: ProviderRef,
237 sender: mpsc::Sender<QueuedFlashblockUpdate>,
238}
239
240impl FlashblockUpdateSender {
241 pub(crate) const fn new(
242 provider: ProviderRef,
243 sender: mpsc::Sender<QueuedFlashblockUpdate>,
244 ) -> Self {
245 Self { provider, sender }
246 }
247
248 pub const fn provider(&self) -> &ProviderRef {
250 &self.provider
251 }
252
253 pub async fn send(&self, update: FlashblockUpdate) -> Result<(), FlashblockUpdateChannelError> {
265 self.send_with_ingress(update, FlashblockIngressTiming::new(Instant::now()))
266 .await
267 }
268
269 pub async fn send_with_ingress(
276 &self,
277 update: FlashblockUpdate,
278 timing: FlashblockIngressTiming,
279 ) -> Result<(), FlashblockUpdateChannelError> {
280 self.validate_endpoint(&update)?;
281 let (queued, acknowledgement) = QueuedFlashblockUpdate::new(update, timing);
282 self.sender
283 .send(queued)
284 .await
285 .map_err(|_| FlashblockUpdateChannelError::Closed)?;
286 acknowledgement.wait().await
287 }
288
289 pub fn try_send(
301 &self,
302 update: FlashblockUpdate,
303 ) -> Result<FlashblockUpdateAcknowledgement, FlashblockUpdateChannelError> {
304 self.try_send_with_ingress(update, FlashblockIngressTiming::new(Instant::now()))
305 }
306
307 pub fn try_send_with_ingress(
314 &self,
315 update: FlashblockUpdate,
316 timing: FlashblockIngressTiming,
317 ) -> Result<FlashblockUpdateAcknowledgement, FlashblockUpdateChannelError> {
318 self.validate_endpoint(&update)?;
319 let (queued, acknowledgement) = QueuedFlashblockUpdate::new(update, timing);
320 self.sender.try_send(queued).map_err(|error| match error {
321 mpsc::error::TrySendError::Full(_) => FlashblockUpdateChannelError::Full,
322 mpsc::error::TrySendError::Closed(_) => FlashblockUpdateChannelError::Closed,
323 })?;
324 Ok(acknowledgement)
325 }
326
327 fn validate_endpoint(
328 &self,
329 update: &FlashblockUpdate,
330 ) -> Result<(), FlashblockUpdateChannelError> {
331 if update.provider().endpoint != self.provider.endpoint {
332 return Err(FlashblockUpdateChannelError::UnexpectedEndpoint);
333 }
334 Ok(())
335 }
336}
337
338pub(crate) fn flashblock_update_channel(
339 provider: ProviderRef,
340 capacity: usize,
341) -> (
342 FlashblockUpdateSender,
343 mpsc::Receiver<QueuedFlashblockUpdate>,
344) {
345 let (sender, receiver) = mpsc::channel(capacity);
346 (FlashblockUpdateSender::new(provider, sender), receiver)
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
351#[non_exhaustive]
352pub enum RawJsonFlashblocksError {
353 #[error("invalid raw JSON Flashblocks limits: {0}")]
355 InvalidLimits(&'static str),
356 #[error("invalid raw JSON Flashblocks source transition: {0}")]
358 InvalidSourceTransition(&'static str),
359 #[error("raw JSON Flashblocks frame exceeds the configured byte limit")]
361 FrameTooLarge,
362 #[error("invalid raw JSON Flashblocks payload: {0}")]
364 InvalidPayload(String),
365 #[error("raw JSON Flashblocks payload exceeds the configured {0} limit")]
367 ResourceExhausted(&'static str),
368}
369
370#[derive(Clone, Debug)]
384pub struct RawJsonFlashblocksAdapter {
385 provider: ProviderRef,
386 limits: RawJsonFlashblocksLimits,
387 active: Option<RawPayloadState>,
388 ignored_payload: Option<FixedBytes<8>>,
389}
390
391impl RawJsonFlashblocksAdapter {
392 pub fn new(provider: ProviderRef) -> Self {
394 Self {
395 provider,
396 limits: RawJsonFlashblocksLimits::default(),
397 active: None,
398 ignored_payload: None,
399 }
400 }
401
402 pub fn with_limits(
404 provider: ProviderRef,
405 limits: RawJsonFlashblocksLimits,
406 ) -> Result<Self, RawJsonFlashblocksError> {
407 limits.validate()?;
408 Ok(Self {
409 provider,
410 limits,
411 active: None,
412 ignored_payload: None,
413 })
414 }
415
416 pub const fn provider(&self) -> &ProviderRef {
418 &self.provider
419 }
420
421 pub const fn limits(&self) -> RawJsonFlashblocksLimits {
423 self.limits
424 }
425
426 pub fn reset(
437 &mut self,
438 provider: ProviderRef,
439 ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
440 if provider.endpoint != self.provider.endpoint {
441 return Err(RawJsonFlashblocksError::InvalidSourceTransition(
442 "reset cannot change the configured endpoint identity",
443 ));
444 }
445 if provider.generation <= self.provider.generation {
446 return Err(RawJsonFlashblocksError::InvalidSourceTransition(
447 "reset requires a strictly newer provider generation",
448 ));
449 }
450 let invalidation = self.active.take().map(|active| {
451 FlashblockUpdate::Invalidated(FlashblockInvalidation {
452 provider: self.provider.clone(),
453 payload_id: active.payload_id,
454 reason: FlashblockInvalidationReason::SourceReset,
455 })
456 });
457 self.provider = provider;
458 self.ignored_payload = None;
459 Ok(invalidation)
460 }
461
462 pub fn ingest_json(
471 &mut self,
472 frame: &[u8],
473 ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
474 let payload = self.decode_json(frame)?;
475 self.ingest(payload)
476 }
477
478 fn decode_json(&self, frame: &[u8]) -> Result<RawFlashblockPayload, RawJsonFlashblocksError> {
479 if frame.len() > self.limits.max_frame_bytes {
480 return Err(RawJsonFlashblocksError::FrameTooLarge);
481 }
482 let payload: RawFlashblockPayload = serde_json::from_slice(frame)
483 .map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
484 self.validate_index(payload.index)?;
485 Ok(payload)
486 }
487
488 fn validate_index(&self, index: u64) -> Result<(), RawJsonFlashblocksError> {
489 if usize::try_from(index)
490 .ok()
491 .is_none_or(|index| index >= self.limits.max_flashblocks_per_payload)
492 {
493 return Err(RawJsonFlashblocksError::ResourceExhausted(
494 "Flashblock index",
495 ));
496 }
497 Ok(())
498 }
499
500 fn ingest(
501 &mut self,
502 payload: RawFlashblockPayload,
503 ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
504 self.validate_index(payload.index)?;
505 if self.ignored_payload == Some(payload.payload_id) {
506 return Ok(None);
507 }
508
509 let begins_new_payload = self
510 .active
511 .as_ref()
512 .is_none_or(|active| active.payload_id != payload.payload_id);
513 if begins_new_payload {
514 if payload.index != 0 {
515 let invalidated_payload = self
516 .active
517 .take()
518 .map_or(payload.payload_id, |active| active.payload_id);
519 self.ignored_payload = Some(payload.payload_id);
520 return Ok(Some(self.invalidation(
521 invalidated_payload,
522 FlashblockInvalidationReason::MissingInitialIndex,
523 )));
524 }
525 let base = payload.base.clone().ok_or_else(|| {
526 RawJsonFlashblocksError::InvalidPayload("index zero omitted its base header".into())
527 })?;
528 if let Some(metadata_number) = payload.metadata.block_number
529 && metadata_number != base.block_number
530 {
531 return Err(RawJsonFlashblocksError::InvalidPayload(
532 "base and metadata block numbers disagree".into(),
533 ));
534 }
535 let previous_active = self.active.take();
536 let previous_ignored_payload = self.ignored_payload.take();
537 self.active = Some(RawPayloadState {
538 payload_id: payload.payload_id,
539 base,
540 last_index: None,
541 cumulative_transactions: Vec::new(),
542 transaction_set: HashSet::new(),
543 next_log_index: 0,
544 cumulative_logs: 0,
545 index_commitments: HashMap::new(),
546 });
547 let result = self.ingest_active(payload);
548 if result.is_err() {
549 self.active = previous_active;
550 self.ignored_payload = previous_ignored_payload;
551 }
552 return result;
553 }
554
555 self.ingest_active(payload)
556 }
557
558 fn ingest_active(
559 &mut self,
560 payload: RawFlashblockPayload,
561 ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
562 let active = self.active.as_mut().expect("new payload initialized above");
563 if payload
564 .metadata
565 .block_number
566 .is_some_and(|number| number != active.base.block_number)
567 || payload
568 .base
569 .as_ref()
570 .is_some_and(|base| base != &active.base)
571 {
572 return Err(RawJsonFlashblocksError::InvalidPayload(
573 "base header or metadata block numbers disagree with the active payload".into(),
574 ));
575 }
576 let delta_transactions = flashblock_transaction_hashes(&payload.diff.transactions)
577 .map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
578 let receipt_hashes = payload
579 .metadata
580 .receipts
581 .keys()
582 .copied()
583 .collect::<HashSet<_>>();
584 let transaction_hashes = delta_transactions.iter().copied().collect::<HashSet<_>>();
585 if transaction_hashes.len() != delta_transactions.len() {
586 return Err(RawJsonFlashblocksError::InvalidPayload(
587 "the transaction delta contains a duplicate hash".into(),
588 ));
589 }
590 if receipt_hashes != transaction_hashes {
591 return Err(RawJsonFlashblocksError::InvalidPayload(
592 "receipt-map membership disagrees with the transaction delta".into(),
593 ));
594 }
595 let commitment = raw_payload_commitment(&payload, &delta_transactions);
596 if let Some(previous) = active.index_commitments.get(&payload.index) {
597 if *previous == commitment {
598 return Ok(None);
599 }
600 let payload_id = active.payload_id;
601 self.active = None;
602 self.ignored_payload = Some(payload_id);
603 return Ok(Some(self.invalidation(
604 payload_id,
605 FlashblockInvalidationReason::ConflictingDuplicate,
606 )));
607 }
608 let expected_index = active.last_index.map_or(0, |index| index.saturating_add(1));
609 if payload.index != expected_index {
610 let payload_id = active.payload_id;
611 self.active = None;
612 self.ignored_payload = Some(payload_id);
613 return Ok(Some(
614 self.invalidation(payload_id, FlashblockInvalidationReason::IndexGap),
615 ));
616 }
617
618 if active
619 .cumulative_transactions
620 .len()
621 .saturating_add(delta_transactions.len())
622 > self.limits.max_transactions_per_payload
623 {
624 return Err(RawJsonFlashblocksError::ResourceExhausted(
625 "transaction count",
626 ));
627 }
628 if delta_transactions
629 .iter()
630 .any(|hash| active.transaction_set.contains(hash))
631 {
632 return Err(RawJsonFlashblocksError::InvalidPayload(
633 "a transaction appeared in more than one indexed delta".into(),
634 ));
635 }
636
637 let transaction_offset = active.cumulative_transactions.len();
638 let mut logs = Vec::new();
639 for (delta_index, transaction_hash) in delta_transactions.iter().enumerate() {
640 let receipt = payload
641 .metadata
642 .receipts
643 .get(transaction_hash)
644 .expect("receipt membership checked above");
645 let transaction_index =
646 u64::try_from(transaction_offset.saturating_add(delta_index))
647 .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("transaction index"))?;
648 for raw_log in &receipt.logs {
649 if active.cumulative_logs.saturating_add(logs.len())
650 >= self.limits.max_logs_per_payload
651 {
652 return Err(RawJsonFlashblocksError::ResourceExhausted("log count"));
653 }
654 let inner = PrimitiveLog::new(
655 raw_log.address,
656 raw_log.topics.clone(),
657 raw_log.data.clone(),
658 )
659 .ok_or_else(|| {
660 RawJsonFlashblocksError::InvalidPayload(
661 "receipt log contains more than four topics".into(),
662 )
663 })?;
664 let log_index = active
665 .next_log_index
666 .checked_add(
667 u64::try_from(logs.len())
668 .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
669 )
670 .ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
671 logs.push(Log {
672 inner,
673 block_hash: None,
674 block_number: Some(active.base.block_number),
675 block_timestamp: Some(active.base.timestamp),
676 transaction_hash: Some(*transaction_hash),
677 transaction_index: Some(transaction_index),
678 log_index: Some(log_index),
679 removed: false,
680 });
681 }
682 }
683
684 let mut cumulative_transactions = active.cumulative_transactions.clone();
685 cumulative_transactions.extend(delta_transactions.iter().copied());
686 let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
687 let state_root = non_placeholder_hash(payload.diff.state_root);
688 let transactions_root = payload
689 .diff
690 .transactions_root
691 .and_then(non_placeholder_hash);
692 let parent_hash = non_placeholder_hash(active.base.parent_hash);
693 let prevrandao = active.base.prevrandao.and_then(non_placeholder_hash);
694 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
695 provider: &self.provider,
696 payload_id: Some(payload.payload_id),
697 index: Some(payload.index),
698 block_number: active.base.block_number,
699 partial_block_hash,
700 parent_hash,
701 state_root,
702 transactions_root,
703 transaction_hashes: &cumulative_transactions,
704 timestamp: Some(active.base.timestamp),
705 base_fee_per_gas: active.base.base_fee_per_gas,
706 beneficiary: active.base.beneficiary,
707 prevrandao,
708 gas_limit: active.base.gas_limit,
709 });
710 for log in &mut logs {
711 log.block_hash = Some(content_hash);
712 }
713 let flashblock = FlashblockRef {
714 provider: self.provider.clone(),
715 payload_id: Some(payload.payload_id),
716 index: Some(payload.index),
717 block_number: active.base.block_number,
718 content_hash,
719 partial_block_hash,
720 parent_hash,
721 state_root,
722 transactions_root,
723 transaction_hashes: cumulative_transactions.clone(),
724 timestamp: Some(active.base.timestamp),
725 base_fee_per_gas: active.base.base_fee_per_gas,
726 beneficiary: active.base.beneficiary,
727 prevrandao,
728 gas_limit: active.base.gas_limit,
729 };
730 let next_log_index = active
731 .next_log_index
732 .checked_add(
733 u64::try_from(logs.len())
734 .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
735 )
736 .ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
737 active.transaction_set.extend(delta_transactions);
738 active.cumulative_transactions = cumulative_transactions;
739 active.last_index = Some(payload.index);
740 active.index_commitments.insert(payload.index, commitment);
741 active.next_log_index = next_log_index;
742 active.cumulative_logs = active.cumulative_logs.saturating_add(logs.len());
743
744 Ok(Some(FlashblockUpdate::Snapshot(Box::new(
745 FlashblockSnapshot { flashblock, logs },
746 ))))
747 }
748
749 fn invalidation(
750 &self,
751 payload_id: FixedBytes<8>,
752 reason: FlashblockInvalidationReason,
753 ) -> FlashblockUpdate {
754 FlashblockUpdate::Invalidated(FlashblockInvalidation {
755 provider: self.provider.clone(),
756 payload_id,
757 reason,
758 })
759 }
760
761 fn invalidate_active(
762 &mut self,
763 payload_id: FixedBytes<8>,
764 reason: FlashblockInvalidationReason,
765 ) -> FlashblockUpdate {
766 self.active = None;
767 self.ignored_payload = Some(payload_id);
768 self.invalidation(payload_id, reason)
769 }
770}
771
772const MIN_BUFFERED_GAP_MILLIS: u64 = 300;
773const MAX_BUFFERED_GAP_MILLIS: u64 = 500;
774
775#[derive(Clone, Debug)]
785pub struct BufferedRawJsonFlashblocksAdapter {
786 inner: RawJsonFlashblocksAdapter,
787 gap_timeout_millis: u64,
788 buffered: Option<BufferedRawFlashblock>,
789}
790
791#[derive(Clone, Debug)]
792struct BufferedRawFlashblock {
793 payload: RawFlashblockPayload,
794 commitment: B256,
795 expected_index: u64,
796 source_ingress_millis: u64,
797 expires_at_millis: u64,
798}
799
800impl BufferedRawJsonFlashblocksAdapter {
801 pub fn new(
811 provider: ProviderRef,
812 limits: RawJsonFlashblocksLimits,
813 gap_timeout_millis: u64,
814 ) -> Result<Self, RawJsonFlashblocksError> {
815 if !(MIN_BUFFERED_GAP_MILLIS..=MAX_BUFFERED_GAP_MILLIS).contains(&gap_timeout_millis) {
816 return Err(RawJsonFlashblocksError::InvalidLimits(
817 "buffered gap timeout must be between 300 and 500 milliseconds",
818 ));
819 }
820 Ok(Self {
821 inner: RawJsonFlashblocksAdapter::with_limits(provider, limits)?,
822 gap_timeout_millis,
823 buffered: None,
824 })
825 }
826
827 pub const fn provider(&self) -> &ProviderRef {
829 self.inner.provider()
830 }
831
832 pub const fn limits(&self) -> RawJsonFlashblocksLimits {
834 self.inner.limits()
835 }
836
837 pub fn buffered_gap(&self) -> Option<(u64, u64, u64)> {
840 self.buffered.as_ref().map(|buffered| {
841 (
842 buffered.expected_index,
843 buffered.payload.index,
844 buffered.expires_at_millis,
845 )
846 })
847 }
848
849 pub fn ingest_json_at(
861 &mut self,
862 frame: &[u8],
863 now_millis: u64,
864 ) -> Result<Vec<FlashblockUpdate>, RawJsonFlashblocksError> {
865 self.ingest_json_timed_at(frame, now_millis).map(|updates| {
866 updates
867 .into_iter()
868 .map(TimedFlashblockUpdate::into_update)
869 .collect()
870 })
871 }
872
873 pub fn ingest_json_timed_at(
886 &mut self,
887 frame: &[u8],
888 now_millis: u64,
889 ) -> Result<Vec<TimedFlashblockUpdate>, RawJsonFlashblocksError> {
890 let payload = self.inner.decode_json(frame)?;
891 let mut updates = Vec::with_capacity(2);
892
893 if self
894 .buffered
895 .as_ref()
896 .is_some_and(|buffered| now_millis >= buffered.expires_at_millis)
897 {
898 let expired = self.buffered.as_ref().expect("checked above");
899 if payload.payload_id == expired.payload.payload_id {
900 let payload_id = expired.payload.payload_id;
901 self.buffered = None;
902 updates.push(TimedFlashblockUpdate::new(
903 self.inner
904 .invalidate_active(payload_id, FlashblockInvalidationReason::IndexGap),
905 now_millis,
906 ));
907 return Ok(updates);
908 }
909
910 let payload_id = expired.payload.payload_id;
914 let invalidation = self
915 .inner
916 .invalidation(payload_id, FlashblockInvalidationReason::IndexGap);
917 let mut staged = self.inner.clone();
918 let replacement = staged.ingest(payload)?;
919 self.inner = staged;
920 self.buffered = None;
921 updates.push(TimedFlashblockUpdate::new(invalidation, now_millis));
922 if let Some(update) = replacement {
923 updates.push(TimedFlashblockUpdate::new(update, now_millis));
924 }
925 return Ok(updates);
926 }
927
928 if let Some(buffered) = self.buffered.as_ref() {
929 if payload.payload_id == buffered.payload.payload_id {
930 if payload.index == buffered.expected_index {
931 let buffered = self.buffered.as_ref().expect("checked above").clone();
932 let mut staged = self.inner.clone();
933 if let Some(update) = staged.ingest(payload)? {
934 updates.push(TimedFlashblockUpdate::new(update, now_millis));
935 }
936 if let Some(update) = staged.ingest(buffered.payload)? {
937 updates.push(TimedFlashblockUpdate::new(
938 update,
939 buffered.source_ingress_millis,
940 ));
941 }
942 self.inner = staged;
943 self.buffered = None;
944 return Ok(updates);
945 }
946
947 if payload.index == buffered.payload.index {
948 let commitment = self.validate_bufferable_payload(&payload)?;
949 if commitment == buffered.commitment {
950 return Ok(updates);
951 }
952 let payload_id = payload.payload_id;
953 self.buffered = None;
954 updates.push(TimedFlashblockUpdate::new(
955 self.inner.invalidate_active(
956 payload_id,
957 FlashblockInvalidationReason::ConflictingDuplicate,
958 ),
959 now_millis,
960 ));
961 return Ok(updates);
962 }
963
964 if payload.index > buffered.payload.index {
965 self.validate_bufferable_payload(&payload)?;
966 let payload_id = payload.payload_id;
967 self.buffered = None;
968 updates.push(TimedFlashblockUpdate::new(
969 self.inner
970 .invalidate_active(payload_id, FlashblockInvalidationReason::IndexGap),
971 now_millis,
972 ));
973 return Ok(updates);
974 }
975 } else {
976 let mut staged = self.inner.clone();
978 let replacement = staged.ingest(payload)?;
979 self.inner = staged;
980 self.buffered = None;
981 if let Some(update) = replacement {
982 updates.push(TimedFlashblockUpdate::new(update, now_millis));
983 }
984 return Ok(updates);
985 }
986 }
987
988 if self.can_buffer_one_gap(&payload) {
989 let commitment = self.validate_bufferable_payload(&payload)?;
990 let active = self
991 .inner
992 .active
993 .as_ref()
994 .expect("buffering requires active state");
995 let expected_index = active.last_index.map_or(0, |index| index.saturating_add(1));
996 self.buffered = Some(BufferedRawFlashblock {
997 payload,
998 commitment,
999 expected_index,
1000 source_ingress_millis: now_millis,
1001 expires_at_millis: now_millis.saturating_add(self.gap_timeout_millis),
1002 });
1003 return Ok(updates);
1004 }
1005
1006 if self.is_more_than_one_index_ahead(&payload) {
1007 self.validate_bufferable_payload(&payload)?;
1008 }
1009
1010 if let Some(update) = self.inner.ingest(payload)? {
1011 if matches!(update, FlashblockUpdate::Invalidated(_)) {
1012 self.buffered = None;
1013 }
1014 updates.push(TimedFlashblockUpdate::new(update, now_millis));
1015 }
1016 Ok(updates)
1017 }
1018
1019 pub fn expire_gap_at(&mut self, now_millis: u64) -> Option<FlashblockUpdate> {
1025 let expired = self
1026 .buffered
1027 .as_ref()
1028 .is_some_and(|buffered| now_millis >= buffered.expires_at_millis);
1029 if !expired {
1030 return None;
1031 }
1032 let buffered = self.buffered.take().expect("checked above");
1033 Some(self.inner.invalidate_active(
1034 buffered.payload.payload_id,
1035 FlashblockInvalidationReason::IndexGap,
1036 ))
1037 }
1038
1039 pub fn reset(
1048 &mut self,
1049 provider: ProviderRef,
1050 ) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
1051 let invalidation = self.inner.reset(provider)?;
1052 self.buffered = None;
1053 Ok(invalidation)
1054 }
1055
1056 fn can_buffer_one_gap(&self, payload: &RawFlashblockPayload) -> bool {
1057 let Some(active) = self.inner.active.as_ref() else {
1058 return false;
1059 };
1060 if active.payload_id != payload.payload_id {
1061 return false;
1062 }
1063 let expected = active.last_index.map_or(0, |index| index.saturating_add(1));
1064 payload.index == expected.saturating_add(1)
1065 }
1066
1067 fn is_more_than_one_index_ahead(&self, payload: &RawFlashblockPayload) -> bool {
1068 let Some(active) = self.inner.active.as_ref() else {
1069 return false;
1070 };
1071 if active.payload_id != payload.payload_id {
1072 return false;
1073 }
1074 let expected = active.last_index.map_or(0, |index| index.saturating_add(1));
1075 payload.index > expected.saturating_add(1)
1076 }
1077
1078 fn validate_bufferable_payload(
1079 &self,
1080 payload: &RawFlashblockPayload,
1081 ) -> Result<B256, RawJsonFlashblocksError> {
1082 let active = self
1083 .inner
1084 .active
1085 .as_ref()
1086 .expect("buffer validation requires active state");
1087 if payload
1088 .metadata
1089 .block_number
1090 .is_some_and(|number| number != active.base.block_number)
1091 || payload
1092 .base
1093 .as_ref()
1094 .is_some_and(|base| base != &active.base)
1095 {
1096 return Err(RawJsonFlashblocksError::InvalidPayload(
1097 "base header or metadata block numbers disagree with the active payload".into(),
1098 ));
1099 }
1100 let transaction_hashes = flashblock_transaction_hashes(&payload.diff.transactions)
1101 .map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
1102 let unique_transactions = transaction_hashes.iter().copied().collect::<HashSet<_>>();
1103 if unique_transactions.len() != transaction_hashes.len() {
1104 return Err(RawJsonFlashblocksError::InvalidPayload(
1105 "the transaction delta contains a duplicate hash".into(),
1106 ));
1107 }
1108 let receipt_hashes = payload
1109 .metadata
1110 .receipts
1111 .keys()
1112 .copied()
1113 .collect::<HashSet<_>>();
1114 if receipt_hashes != unique_transactions {
1115 return Err(RawJsonFlashblocksError::InvalidPayload(
1116 "receipt-map membership disagrees with the transaction delta".into(),
1117 ));
1118 }
1119 if active
1120 .cumulative_transactions
1121 .len()
1122 .saturating_add(transaction_hashes.len())
1123 > self.inner.limits.max_transactions_per_payload
1124 {
1125 return Err(RawJsonFlashblocksError::ResourceExhausted(
1126 "transaction count",
1127 ));
1128 }
1129 if transaction_hashes
1130 .iter()
1131 .any(|hash| active.transaction_set.contains(hash))
1132 {
1133 return Err(RawJsonFlashblocksError::InvalidPayload(
1134 "a transaction appeared in more than one indexed delta".into(),
1135 ));
1136 }
1137 let log_count =
1138 payload
1139 .metadata
1140 .receipts
1141 .values()
1142 .try_fold(0_usize, |count, receipt| {
1143 for log in &receipt.logs {
1144 if log.topics.len() > 4 {
1145 return Err(RawJsonFlashblocksError::InvalidPayload(
1146 "receipt log contains more than four topics".into(),
1147 ));
1148 }
1149 }
1150 count
1151 .checked_add(receipt.logs.len())
1152 .ok_or(RawJsonFlashblocksError::ResourceExhausted("log count"))
1153 })?;
1154 if active.cumulative_logs.saturating_add(log_count) > self.inner.limits.max_logs_per_payload
1155 {
1156 return Err(RawJsonFlashblocksError::ResourceExhausted("log count"));
1157 }
1158 u64::try_from(
1159 active
1160 .cumulative_transactions
1161 .len()
1162 .saturating_add(transaction_hashes.len()),
1163 )
1164 .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("transaction index"))?;
1165 active
1166 .next_log_index
1167 .checked_add(
1168 u64::try_from(log_count)
1169 .map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
1170 )
1171 .ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
1172 Ok(raw_payload_commitment(payload, &transaction_hashes))
1173 }
1174}
1175
1176fn raw_payload_commitment(payload: &RawFlashblockPayload, transaction_hashes: &[B256]) -> B256 {
1177 let mut commitment = Keccak256::new();
1178 commitment.update(b"evm-fork-cache/raw-json-flashblock/v1");
1179 commitment.update(payload.payload_id.as_slice());
1180 commitment.update(payload.index.to_be_bytes());
1181 match payload.base.as_ref() {
1182 Some(base) => {
1183 commitment.update([1]);
1184 commitment.update(base.parent_hash.as_slice());
1185 commitment.update(base.block_number.to_be_bytes());
1186 commitment.update(base.timestamp.to_be_bytes());
1187 commit_optional_raw_u64(&mut commitment, base.gas_limit);
1188 commit_optional_raw_u64(&mut commitment, base.base_fee_per_gas);
1189 commit_optional_raw_bytes(
1190 &mut commitment,
1191 base.beneficiary.as_ref().map(|address| address.as_slice()),
1192 );
1193 commit_optional_raw_bytes(
1194 &mut commitment,
1195 base.prevrandao.as_ref().map(B256::as_slice),
1196 );
1197 }
1198 None => commitment.update([0]),
1199 }
1200 commitment.update(payload.diff.state_root.as_slice());
1201 commitment.update(payload.diff.block_hash.as_slice());
1202 commit_optional_raw_bytes(
1203 &mut commitment,
1204 payload.diff.transactions_root.as_ref().map(B256::as_slice),
1205 );
1206 commit_optional_raw_u64(&mut commitment, payload.metadata.block_number);
1207 commitment.update((transaction_hashes.len() as u64).to_be_bytes());
1208 for transaction_hash in transaction_hashes {
1209 commitment.update(transaction_hash.as_slice());
1210 let receipt = payload
1211 .metadata
1212 .receipts
1213 .get(transaction_hash)
1214 .expect("receipt membership validated before commitment");
1215 commitment.update((receipt.logs.len() as u64).to_be_bytes());
1216 for log in &receipt.logs {
1217 commitment.update(log.address.as_slice());
1218 commitment.update((log.topics.len() as u64).to_be_bytes());
1219 for topic in &log.topics {
1220 commitment.update(topic.as_slice());
1221 }
1222 commitment.update((log.data.len() as u64).to_be_bytes());
1223 commitment.update(log.data.as_ref());
1224 }
1225 }
1226 commitment.finalize()
1227}
1228
1229fn commit_optional_raw_u64(commitment: &mut Keccak256, value: Option<u64>) {
1230 match value {
1231 Some(value) => {
1232 commitment.update([1]);
1233 commitment.update(value.to_be_bytes());
1234 }
1235 None => commitment.update([0]),
1236 }
1237}
1238
1239fn commit_optional_raw_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
1240 match value {
1241 Some(value) => {
1242 commitment.update([1]);
1243 commitment.update((value.len() as u64).to_be_bytes());
1244 commitment.update(value);
1245 }
1246 None => commitment.update([0]),
1247 }
1248}
1249
1250#[derive(Clone, Debug)]
1251struct RawPayloadState {
1252 payload_id: FixedBytes<8>,
1253 base: BaseFlashblockBase,
1254 last_index: Option<u64>,
1255 cumulative_transactions: Vec<B256>,
1256 transaction_set: HashSet<B256>,
1257 next_log_index: u64,
1258 cumulative_logs: usize,
1259 index_commitments: HashMap<u64, B256>,
1260}
1261
1262#[derive(Clone, Debug, serde::Deserialize)]
1263struct RawFlashblockPayload {
1264 payload_id: FixedBytes<8>,
1265 index: u64,
1266 #[serde(default, alias = "static")]
1267 base: Option<BaseFlashblockBase>,
1268 diff: RawFlashblockDiff,
1269 metadata: RawFlashblockMetadata,
1270}
1271
1272#[derive(Clone, Debug, serde::Deserialize)]
1273struct RawFlashblockDiff {
1274 state_root: B256,
1275 block_hash: B256,
1276 #[serde(default)]
1277 transactions: Vec<serde_json::Value>,
1278 #[serde(default)]
1279 transactions_root: Option<B256>,
1280}
1281
1282#[derive(Clone, Debug, serde::Deserialize)]
1283struct RawFlashblockMetadata {
1284 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
1285 block_number: Option<u64>,
1286 #[serde(default, deserialize_with = "deserialize_receipts")]
1287 receipts: HashMap<B256, RawTransactionReceipt>,
1288}
1289
1290fn deserialize_receipts<'de, D>(
1291 deserializer: D,
1292) -> Result<HashMap<B256, RawTransactionReceipt>, D::Error>
1293where
1294 D: serde::Deserializer<'de>,
1295{
1296 struct ReceiptsVisitor;
1297
1298 impl<'de> serde::de::Visitor<'de> for ReceiptsVisitor {
1299 type Value = HashMap<B256, RawTransactionReceipt>;
1300
1301 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1302 formatter.write_str("a receipt map with unique transaction-hash keys")
1303 }
1304
1305 fn visit_map<A>(self, mut entries: A) -> Result<Self::Value, A::Error>
1306 where
1307 A: serde::de::MapAccess<'de>,
1308 {
1309 let mut receipts = HashMap::with_capacity(entries.size_hint().unwrap_or_default());
1310 while let Some((transaction_hash, receipt)) = entries.next_entry()? {
1311 if receipts.insert(transaction_hash, receipt).is_some() {
1312 return Err(serde::de::Error::custom("duplicate receipt key"));
1313 }
1314 }
1315 Ok(receipts)
1316 }
1317 }
1318
1319 deserializer.deserialize_map(ReceiptsVisitor)
1320}
1321
1322#[derive(Clone, Debug, serde::Deserialize)]
1323struct RawTransactionReceipt {
1324 #[serde(default)]
1325 logs: Vec<RawReceiptLog>,
1326}
1327
1328#[derive(Clone, Debug, serde::Deserialize)]
1329struct RawReceiptLog {
1330 address: Address,
1331 #[serde(default)]
1332 topics: Vec<B256>,
1333 data: Bytes,
1334}