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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct RawJsonFlashblocksLimits {
17 pub max_frame_bytes: usize,
19 pub max_flashblocks_per_payload: usize,
21 pub max_transactions_per_payload: usize,
23 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#[derive(Clone, Debug, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct FlashblockSnapshot {
69 pub flashblock: FlashblockRef,
71 pub logs: Vec<Log>,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub enum FlashblockInvalidationReason {
79 IndexGap,
81 ConflictingDuplicate,
83 MissingInitialIndex,
85 SourceReset,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
91#[non_exhaustive]
92pub struct FlashblockInvalidation {
93 pub provider: ProviderRef,
95 pub payload_id: FixedBytes<8>,
97 pub reason: FlashblockInvalidationReason,
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum FlashblockUpdate {
105 Snapshot(Box<FlashblockSnapshot>),
107 Invalidated(FlashblockInvalidation),
109}
110
111impl FlashblockUpdate {
112 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#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
123#[non_exhaustive]
124pub enum FlashblockUpdateChannelError {
125 #[error("Flashblock update came from an unexpected provider endpoint")]
127 UnexpectedEndpoint,
128 #[error("Flashblock update channel is full")]
130 Full,
131 #[error("Flashblock update channel is closed")]
133 Closed,
134 #[error("Flashblock update was rejected by the subscriber")]
138 Rejected,
139}
140
141#[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 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#[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 pub const fn provider(&self) -> &ProviderRef {
207 &self.provider
208 }
209
210 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 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#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
279#[non_exhaustive]
280pub enum RawJsonFlashblocksError {
281 #[error("invalid raw JSON Flashblocks limits: {0}")]
283 InvalidLimits(&'static str),
284 #[error("invalid raw JSON Flashblocks source transition: {0}")]
286 InvalidSourceTransition(&'static str),
287 #[error("raw JSON Flashblocks frame exceeds the configured byte limit")]
289 FrameTooLarge,
290 #[error("invalid raw JSON Flashblocks payload: {0}")]
292 InvalidPayload(String),
293 #[error("raw JSON Flashblocks payload exceeds the configured {0} limit")]
295 ResourceExhausted(&'static str),
296}
297
298#[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 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 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 pub const fn provider(&self) -> &ProviderRef {
346 &self.provider
347 }
348
349 pub const fn limits(&self) -> RawJsonFlashblocksLimits {
351 self.limits
352 }
353
354 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 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}